diff --git a/.changeset/spotty-sheep-fetch.md b/.changeset/spotty-sheep-fetch.md new file mode 100644 index 0000000000..390aa8e79e --- /dev/null +++ b/.changeset/spotty-sheep-fetch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid marking subtree as dynamic for inlined attributes diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js index 32c8d2ca36..f0e74e3dc9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js @@ -2,6 +2,7 @@ /** @import { Context } from '../types' */ import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js'; import * as e from '../../../errors.js'; +import { is_inlinable_expression } from '../../utils.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; /** @@ -15,9 +16,17 @@ export function ExpressionTag(node, context) { } } - // TODO ideally we wouldn't do this here, we'd just do it on encountering - // an `Identifier` within the tag. But we currently need to handle `{42}` etc - mark_subtree_dynamic(context.path); + const attribute_parent = context.path.find((parent) => parent.type === 'Attribute'); + /** + * if the expression tag is part of an attribute we want to check if it's inlinable before marking + * the subtree as dynamic. This is because if it's inlinable it will be inlined in the template + * directly making the whole thing actually static. + */ + if (attribute_parent && !is_inlinable_expression(node, context.state.scope)) { + // TODO ideally we wouldn't do this here, we'd just do it on encountering + // an `Identifier` within the tag. But we currently need to handle `{42}` etc + mark_subtree_dynamic(context.path); + } context.next({ ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index 79dccd5a7c..0b38815a40 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -7,6 +7,7 @@ import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { is_rune } from '../../../../utils.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; +import { is_inlinable_expression } from '../../utils.js'; /** * @param {Identifier} node @@ -20,7 +21,21 @@ export function Identifier(node, context) { return; } - mark_subtree_dynamic(context.path); + const expression_tag_parent = context.path.find((parent) => parent.type === 'ExpressionTag'); + const attribute_parent = context.path.find((parent) => parent.type === 'Attribute'); + + /** + * if the identifier is part of an expression tag of an attribute we want to check if it's inlinable + * before marking the subtree as dynamic. This is because if it's inlinable it will be inlined in the template + * directly making the whole thing actually static. + */ + if ( + !attribute_parent || + !expression_tag_parent || + !is_inlinable_expression(expression_tag_parent, context.state.scope) + ) { + mark_subtree_dynamic(context.path); + } // If we are using arguments outside of a function, then throw an error if ( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index c460905977..910f173f79 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,18 +1,18 @@ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */ -/** @import { AST, Binding, SvelteNode } from '#compiler' */ +/** @import { Binding, SvelteNode } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { Analysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ -import * as b from '../../../utils/builders.js'; -import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; import { - PROPS_IS_LAZY_INITIAL, + PROPS_IS_BINDABLE, PROPS_IS_IMMUTABLE, + PROPS_IS_LAZY_INITIAL, PROPS_IS_RUNES, - PROPS_IS_UPDATED, - PROPS_IS_BINDABLE + PROPS_IS_UPDATED } from '../../../../constants.js'; import { dev } from '../../../state.js'; +import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; +import * as b from '../../../utils/builders.js'; import { get_value } from './visitors/shared/declarations.js'; /** @@ -311,43 +311,3 @@ export function create_derived_block_argument(node, context) { export function create_derived(state, arg) { return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); } - -/** - * Whether a variable can be referenced directly from template string. - * @param {import('#compiler').Binding | undefined} binding - * @returns {boolean} - */ -export function can_inline_variable(binding) { - return ( - !!binding && - // in a `