diff --git a/.changeset/mighty-ads-appear.md b/.changeset/mighty-ads-appear.md new file mode 100644 index 000000000000..c1b3b6a74606 --- /dev/null +++ b/.changeset/mighty-ads-appear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: consider static attributes that are inlined in the template 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 2647f0a3be3b..c46090597709 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,5 +1,5 @@ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */ -/** @import { Binding, SvelteNode } from '#compiler' */ +/** @import { AST, Binding, SvelteNode } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { Analysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ @@ -326,3 +326,28 @@ export function can_inline_variable(binding) { binding.initial?.type === 'Literal' ); } + +/** + * @param {(AST.Text | AST.ExpressionTag) | (AST.Text | AST.ExpressionTag)[]} node_or_nodes + * @param {import('./types.js').ComponentClientTransformState} state + */ +export function is_inlinable_expression(node_or_nodes, state) { + let nodes = Array.isArray(node_or_nodes) ? node_or_nodes : [node_or_nodes]; + let has_expression_tag = false; + for (let value of nodes) { + if (value.type === 'ExpressionTag') { + if (value.expression.type === 'Identifier') { + const binding = state.scope + .owner(value.expression.name) + ?.declarations.get(value.expression.name); + if (!can_inline_variable(binding)) { + return false; + } + } else { + return false; + } + has_expression_tag = true; + } + } + return has_expression_tag; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index dafe1e74ae3a..4d3cebcee6fe 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -19,7 +19,12 @@ import { import * as b from '../../../../utils/builders.js'; import { is_custom_element_node } from '../../../nodes.js'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; -import { build_getter, can_inline_variable, create_derived } from '../utils.js'; +import { + build_getter, + can_inline_variable, + create_derived, + is_inlinable_expression +} from '../utils.js'; import { get_attribute_name, build_attribute_value, @@ -584,10 +589,7 @@ function build_element_attribute_update_assignment(element, node_id, attribute, const inlinable_expression = attribute.value === true ? false // not an expression - : is_inlinable_expression( - Array.isArray(attribute.value) ? attribute.value : [attribute.value], - context.state - ); + : is_inlinable_expression(attribute.value, context.state); if (attribute.metadata.expression.has_state) { if (has_call) { state.init.push(build_update(update)); @@ -605,30 +607,6 @@ function build_element_attribute_update_assignment(element, node_id, attribute, } } -/** - * @param {(AST.Text | AST.ExpressionTag)[]} nodes - * @param {import('../types.js').ComponentClientTransformState} state - */ -function is_inlinable_expression(nodes, state) { - let has_expression_tag = false; - for (let value of nodes) { - if (value.type === 'ExpressionTag') { - if (value.expression.type === 'Identifier') { - const binding = state.scope - .owner(value.expression.name) - ?.declarations.get(value.expression.name); - if (!can_inline_variable(binding)) { - return false; - } - } else { - return false; - } - has_expression_tag = true; - } - } - return has_expression_tag; -} - /** * Like `build_element_attribute_update_assignment` but without any special attribute treatment. * @param {Identifier} node_id diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 98272817df7d..3b009fb5b589 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -3,6 +3,7 @@ /** @import { ComponentContext } from '../../types' */ import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; +import { is_inlinable_expression } from '../../utils.js'; import { build_template_literal, build_update } from './utils.js'; /** @@ -97,7 +98,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { let child_state = state; - if (is_static_element(node)) { + if (is_static_element(node, state)) { skipped += 1; } else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) { node.metadata.is_controlled = true; @@ -124,10 +125,12 @@ export function process_children(nodes, initial, is_element, { visit, state }) { /** * @param {SvelteNode} node + * @param {ComponentContext["state"]} state */ -function is_static_element(node) { +function is_static_element(node, state) { if (node.type !== 'RegularElement') return false; if (node.fragment.metadata.dynamic) return false; + if (node.name.includes('-')) return false; // we're setting all attributes on custom elements through properties for (const attribute of node.attributes) { if (attribute.type !== 'Attribute') { @@ -138,10 +141,6 @@ function is_static_element(node) { return false; } - if (attribute.value !== true && !is_text_attribute(attribute)) { - return false; - } - if (attribute.name === 'autofocus' || attribute.name === 'muted') { return false; } @@ -155,8 +154,14 @@ function is_static_element(node) { return false; } - if (node.name.includes('-')) { - return false; // we're setting all attributes on custom elements through properties + if ( + attribute.value !== true && + !is_text_attribute(attribute) && + // If the attribute is not a text attribute but is inlinable we will directly inline it in the + // the template so before returning false we need to check that the attribute is not inlinable + !is_inlinable_expression(attribute.value, state) + ) { + return false; } } diff --git a/packages/svelte/tests/snapshot/samples/inline-module-vars/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/inline-module-vars/_expected/client/index.svelte.js index 69c6901d50fb..46ed3c1913f3 100644 --- a/packages/svelte/tests/snapshot/samples/inline-module-vars/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/inline-module-vars/_expected/client/index.svelte.js @@ -9,11 +9,8 @@ var root = $.template(`

we don't need to traverse these nodes

or

these

ones

these

trailing

nodes

can

be

completely

ignored

`, 3); +var root = $.template(`

we don't need to traverse these nodes

or

these

ones

these

trailing

nodes

can

be

completely

ignored

`, 3); export default function Skip_static_subtree($$anchor, $$props) { var fragment = root(); @@ -22,6 +22,28 @@ export default function Skip_static_subtree($$anchor, $$props) { $.set_custom_element_data(custom_elements, "with", "attributes"); $.reset(cant_skip); + + var div = $.sibling(cant_skip, 2); + var input = $.child(div); + + $.autofocus(input, true); + $.reset(div); + + var div_1 = $.sibling(div, 2); + var source = $.child(div_1); + + source.muted = true; + $.reset(div_1); + + var select = $.sibling(div_1, 2); + var option = $.child(select); + + option.value = null == (option.__value = "a") ? "" : "a"; + $.reset(select); + + var img = $.sibling(select, 2); + $.template_effect(() => $.set_text(text, $$props.title)); + $.handle_lazy_img(img); $.append($$anchor, fragment); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js index 2bd910508d0f..d09ac657f925 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js @@ -3,5 +3,5 @@ import * as $ from "svelte/internal/server"; export default function Skip_static_subtree($$payload, $$props) { let { title, content } = $$props; - $$payload.out += `

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`; + $$payload.out += `

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/index.svelte b/packages/svelte/tests/snapshot/samples/skip-static-subtree/index.svelte index de399169b46b..cb73eba707d6 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/index.svelte +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/index.svelte @@ -30,3 +30,18 @@ + +
+ + +
+ +
+ +
+ + + + \ No newline at end of file