From 1f51993b93bfd24b2f290d7d8f9211f47f932a04 Mon Sep 17 00:00:00 2001 From: 7nik Date: Tue, 3 Jun 2025 17:13:18 +0300 Subject: [PATCH 01/21] base fix --- .../3-transform/client/visitors/AttachTag.js | 6 +- .../3-transform/client/visitors/AwaitBlock.js | 7 ++- .../3-transform/client/visitors/ConstTag.js | 11 +++- .../3-transform/client/visitors/EachBlock.js | 17 ++++-- .../3-transform/client/visitors/HtmlTag.js | 5 +- .../3-transform/client/visitors/IfBlock.js | 8 ++- .../3-transform/client/visitors/KeyBlock.js | 5 +- .../3-transform/client/visitors/RenderTag.js | 5 +- .../client/visitors/shared/utils.js | 58 ++++++++++++++++++- .../block-expression-assign/_config.js | 12 ++++ .../block-expression-assign/main.svelte | 45 ++++++++++++++ .../block-expression-fn-call/_config.js | 12 ++++ .../block-expression-fn-call/main.svelte | 36 ++++++++++++ .../block-expression-member-access/_config.js | 12 ++++ .../main.svelte | 46 +++++++++++++++ 15 files changed, 269 insertions(+), 16 deletions(-) create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js index 062604cacc16..7ec9e631720e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js @@ -2,18 +2,22 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; +import { build_legacy_expression } from './shared/utils.js'; /** * @param {AST.AttachTag} node * @param {ComponentContext} context */ export function AttachTag(node, context) { + const expression = context.state.analysis.runes + ? /** @type {Expression} */ (context.visit(node.expression)) + : build_legacy_expression(node.expression, context); context.state.init.push( b.stmt( b.call( '$.attach', context.state.node, - b.thunk(/** @type {Expression} */ (context.visit(node.expression))) + b.thunk(expression) ) ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 30e370327fa1..80453027a5e9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -5,6 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_legacy_expression } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -14,7 +15,11 @@ export function AwaitBlock(node, context) { context.state.template.push_comment(); // Visit {#await } first to ensure that scopes are in the correct order - const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression))); + const expression = b.thunk( + context.state.analysis.runes + ? /** @type {Expression} */ (context.visit(node.expression)) + : build_legacy_expression(node.expression, context) + ); let then_block; let catch_block; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 2f3c0b3d0ed1..cde0fd8bc21c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -6,6 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_legacy_expression } from './shared/utils.js'; /** * @param {AST.ConstTag} node @@ -15,12 +16,15 @@ export function ConstTag(node, context) { const declaration = node.declaration.declarations[0]; // TODO we can almost certainly share some code with $derived(...) if (declaration.id.type === 'Identifier') { + const init = context.state.analysis.runes + ? /** @type {Expression} */ (context.visit(declaration.init)) + : build_legacy_expression(declaration.init, context); context.state.init.push( b.const( declaration.id, create_derived( context.state, - b.thunk(/** @type {Expression} */ (context.visit(declaration.init))) + b.thunk(init) ) ) ); @@ -48,12 +52,15 @@ export function ConstTag(node, context) { // TODO optimise the simple `{ x } = y` case — we can just return `y` // instead of destructuring it only to return a new object + const init = context.state.analysis.runes + ? /** @type {Expression} */ (context.visit(declaration.init, child_state)) + : build_legacy_expression(declaration.init, { ...context, state: child_state }); const fn = b.arrow( [], b.block([ b.const( /** @type {Pattern} */ (context.visit(declaration.id, child_state)), - /** @type {Expression} */ (context.visit(declaration.init, child_state)) + init, ), b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) ]) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 6c651464f1e8..522a49e7c066 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -14,6 +14,7 @@ import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { build_getter } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_legacy_expression } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -24,12 +25,16 @@ export function EachBlock(node, context) { // expression should be evaluated in the parent scope, not the scope // created by the each block itself - const collection = /** @type {Expression} */ ( - context.visit(node.expression, { - ...context.state, - scope: /** @type {Scope} */ (context.state.scope.parent) - }) - ); + const parent_scope_state = { + ...context.state, + scope: /** @type {Scope} */ (context.state.scope.parent) + }; + const collection = context.state.analysis.runes + ? /** @type {Expression} */ (context.visit(node.expression, parent_scope_state)) + : build_legacy_expression(node.expression, { + ...context, + state: parent_scope_state + }); if (!each_node_meta.is_controlled) { context.state.template.push_comment(); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js index 405b400b428d..b67eff61095d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js @@ -3,6 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { is_ignored } from '../../../../state.js'; import * as b from '#compiler/builders'; +import { build_legacy_expression } from './shared/utils.js'; /** * @param {AST.HtmlTag} node @@ -11,7 +12,9 @@ import * as b from '#compiler/builders'; export function HtmlTag(node, context) { context.state.template.push_comment(); - const expression = /** @type {Expression} */ (context.visit(node.expression)); + const expression = context.state.analysis.runes + ? /** @type {Expression} */ (context.visit(node.expression)) + : build_legacy_expression(node.expression, context); const is_svg = context.state.metadata.namespace === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index 3702a47bc9e3..9a67e3364533 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { build_legacy_expression } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -31,6 +32,10 @@ export function IfBlock(node, context) { statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate))); } + const test = context.state.analysis.runes + ? /** @type {Expression} */ (context.visit(node.test)) + : build_legacy_expression(node.test, context); + /** @type {Expression[]} */ const args = [ node.elseif ? b.id('$$anchor') : context.state.node, @@ -38,7 +43,7 @@ export function IfBlock(node, context) { [b.id('$$render')], b.block([ b.if( - /** @type {Expression} */ (context.visit(node.test)), + test, b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined ) @@ -46,6 +51,7 @@ export function IfBlock(node, context) { ) ]; + if (node.elseif) { // We treat this... // diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index 5e63f7e87200..c3b4344da0ef 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { build_legacy_expression } from './shared/utils.js'; /** * @param {AST.KeyBlock} node @@ -10,7 +11,9 @@ import * as b from '#compiler/builders'; export function KeyBlock(node, context) { context.state.template.push_comment(); - const key = /** @type {Expression} */ (context.visit(node.expression)); + const key = context.state.analysis.runes + ? /** @type {Expression} */ (context.visit(node.expression)) + : build_legacy_expression(node.expression, context); const body = /** @type {Expression} */ (context.visit(node.fragment)); context.state.init.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index fec7b5762a11..825f18e3ecb0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -3,6 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; +import { build_legacy_expression } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -31,7 +32,9 @@ export function RenderTag(node, context) { } } - let snippet_function = /** @type {Expression} */ (context.visit(callee)); + let snippet_function = context.state.analysis.runes + ? /** @type {Expression} */ (context.visit(callee)) + : build_legacy_expression(/** @type {Expression} */(callee), context); if (node.metadata.dynamic) { // If we have a chain expression then ensure a nullish snippet function gets turned into an empty one diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index ebf88e878f8d..36a3707e7443 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -1,6 +1,6 @@ /** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ -/** @import { ComponentClientTransformState, Context } from '../../types' */ +/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; @@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator } from '../../../../../state.js'; -import { create_derived } from '../../utils.js'; +import { build_getter, create_derived } from '../../utils.js'; /** * @param {ComponentClientTransformState} state @@ -360,3 +360,57 @@ export function validate_mutation(node, context, expression) { loc && b.literal(loc.column) ); } + +/** + * Serializes an expression with reactivity like in Svelte 4 + * @param {Expression} expression + * @param {ComponentContext} context + */ +export function build_legacy_expression(expression, context) { + // To recreate Svelte 4 behaviour, we track the dependencies + // the compiler can 'see', but we untrack the effect itself + const serialized_expression = /** @type {Expression} */ (context.visit(expression)); + if (expression.type === "Identifier") return serialized_expression; + + /** @type {Expression[]} */ + const sequence = []; + + for (const [name, nodes] of context.state.scope.references) { + const binding = context.state.scope.get(name); + + if (binding === null || binding.kind === 'normal' && binding.declaration_kind !== 'import') continue; + + let used = false; + for (const { node, path } of nodes) { + const expressionIdx = path.indexOf(expression); + if (expressionIdx < 0) continue; + // in Svelte 4, #if, #each and #await copy context, so assignments + // aren't propagated to the parent block / component root + const track_assignment = !path.find((node, i) => + i < expressionIdx - 1 && ["IfBlock", "EachBlock", "AwaitBlock"].includes(node.type) + ) + if (track_assignment) { + used = true; + break; + } + const assignment = /** @type {AssignmentExpression|undefined} */(path.find((node, i) => i >= expressionIdx && node.type === "AssignmentExpression")); + if (!assignment || assignment.left !== node && !path.includes(assignment.left)) { + used = true; + break; + } + } + if (!used) continue; + + let serialized = build_getter(b.id(name), context.state); + + // If the binding is a prop, we need to deep read it because it could be fine-grained $state + // from a runes-component, where mutations don't trigger an update on the prop as a whole. + if (name === '$$props' || name === '$$restProps' || binding.kind === 'bindable_prop') { + serialized = b.call('$.deep_read_state', serialized); + } + + sequence.push(serialized); + } + + return b.sequence([...sequence, b.call('$.untrack', b.thunk(serialized_expression))]); +} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js new file mode 100644 index 000000000000..6008048ccbea --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,0]`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,1]`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte new file mode 100644 index 000000000000..67190669ed98 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte @@ -0,0 +1,45 @@ + + +{#if a = 0}{/if} + +{#each [b = 0] as x}{x,''}{/each} + +{#key c = 0}{/key} + +{#await d = 0}{/await} + +{#snippet snip()}{/snippet} + +{@render (e = 0, snip)()} + +{@html f = 0, ''} + +
+ +{#key 1} + {@const x = (h = 0)} + {x, ''} +{/key} + +{#if 1} + {@const x = (i = 0)} + {x, ''} +{/if} + + +[{a},{b},{c},{d},{e},{f},{g},{h},{i}] + + diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js new file mode 100644 index 000000000000..0e1a5a81502f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
10 - 10`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
11 - 10`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte new file mode 100644 index 000000000000..3f4c6f01072d --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte @@ -0,0 +1,36 @@ + + +{#if fn(false)}{:else if fn(true)}{/if} + +{#each fn([]) as x}{x, ''}{/each} + +{#key fn(1)}{/key} + +{#await fn(Promise.resolve())}{/await} + +{#snippet snip()}{/snippet} + +{@render fn(snip)()} + +{@html fn('')} + +
{})}>
+ +{#key 1} + {@const x = fn('')} + {x} +{/key} + + +{count1} - {count2} + + diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js new file mode 100644 index 000000000000..0e1a5a81502f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
10 - 10`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
11 - 10`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte new file mode 100644 index 000000000000..4041be4f6fda --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte @@ -0,0 +1,46 @@ + + +{#if obj.false}{:else if obj.true}{/if} + +{#each obj.array as x}{x, ''}{/each} + +{#key obj.string}{/key} + +{#await obj.promise}{/await} + +{#snippet snip()}{/snippet} + +{@render obj.snippet()} + +{@html obj.string} + +
+ +{#key 1} + {@const x = obj.string} + {x} +{/key} + + +{count1} - {count2} + + From bcb6b72af2b1a4854f148ed64763c7d80f0a258b Mon Sep 17 00:00:00 2001 From: 7nik Date: Wed, 4 Jun 2025 14:32:46 +0300 Subject: [PATCH 02/21] bail out pure expressions --- .../client/visitors/shared/utils.js | 52 ++++++++++++++++++- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../samples/each-index-non-null/index.svelte | 2 +- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 36a3707e7443..d3b5b09bc4fe 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */ +/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; @@ -361,6 +361,54 @@ export function validate_mutation(node, context, expression) { ); } +/** + * Checks whether the expression contains assignments, function calls, or member accesses + * @param {Expression|Pattern} expression + * @returns {boolean} + */ +function is_pure_expression(expression) { + // It's supposed that values do not have custom @@toPrimitive() or toString(), + // which may be implicitly called in expressions like `a + b`, `a & b`, `+a`, `str: ${a}` + switch (expression.type) { + case "ArrayExpression": return expression.elements.every((element) => element == null || (element.type === "SpreadElement" ? false : is_pure_expression(element))); + case "BinaryExpression": return expression.left.type !== "PrivateIdentifier" && is_pure_expression(expression.left) && is_pure_expression(expression.right); + case "ConditionalExpression": return is_pure_expression(expression.test) && is_pure_expression(expression.consequent) && is_pure_expression(expression.alternate); + case "Identifier": return true; + case "Literal": return true; + case "LogicalExpression": return is_pure_expression(expression.left) && is_pure_expression(expression.right); + case "MetaProperty": return true; // new.target + case "ObjectExpression": return expression.properties.every((property) => + property.type !== "SpreadElement" + && property.key.type !== "PrivateIdentifier" + && is_pure_expression(property.key) + && is_pure_expression(property.value) + ); + case "SequenceExpression": return expression.expressions.every(is_pure_expression); + case "TemplateLiteral": return expression.expressions.every(is_pure_expression); + case "ThisExpression": return true; + case "UnaryExpression": return is_pure_expression(expression.argument); + case "YieldExpression": return expression.argument == null || is_pure_expression(expression.argument); + + case "ArrayPattern": + case "ArrowFunctionExpression": + case "AssignmentExpression": + case "AssignmentPattern": + case "AwaitExpression": + case "CallExpression": + case "ChainExpression": + case "ClassExpression": + case "FunctionExpression": + case "ImportExpression": + case "MemberExpression": + case "NewExpression": + case "ObjectPattern": + case "RestElement": + case "TaggedTemplateExpression": + case "UpdateExpression": + return false; + } +} + /** * Serializes an expression with reactivity like in Svelte 4 * @param {Expression} expression @@ -370,7 +418,7 @@ export function build_legacy_expression(expression, context) { // To recreate Svelte 4 behaviour, we track the dependencies // the compiler can 'see', but we untrack the effect itself const serialized_expression = /** @type {Expression} */ (context.visit(expression)); - if (expression.type === "Identifier") return serialized_expression; + if (is_pure_expression(expression)) return serialized_expression; /** @type {Expression[]} */ const sequence = []; diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js index 804a7c26f182..b6cc2b0d7676 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -8,7 +8,7 @@ export default function Each_index_non_null($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => { + $.each(node, 0, () => [,,,,,], $.index, ($$anchor, $$item, i) => { var p = root_1(); p.textContent = `index: ${i}`; diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js index 3431e36833b5..afa9f32e76e8 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Each_index_non_null($$payload) { - const each_array = $.ensure_array_like(Array(10)); + const each_array = $.ensure_array_like([,,,,,]); $$payload.out += ``; diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte index 03bfc9e37299..3407f9ab5943 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte @@ -1,3 +1,3 @@ -{#each Array(10), i} +{#each [,,,,,], i}

index: {i}

{/each} From ac9c1308879a6e327dfb8c7d880216c49606580b Mon Sep 17 00:00:00 2001 From: 7nik Date: Wed, 4 Jun 2025 14:36:42 +0300 Subject: [PATCH 03/21] lint --- .../3-transform/client/visitors/AttachTag.js | 10 +- .../3-transform/client/visitors/AwaitBlock.js | 2 +- .../3-transform/client/visitors/ConstTag.js | 15 +-- .../3-transform/client/visitors/EachBlock.js | 6 +- .../3-transform/client/visitors/IfBlock.js | 1 - .../3-transform/client/visitors/RenderTag.js | 2 +- .../client/visitors/shared/utils.js | 118 +++++++++++------- 7 files changed, 82 insertions(+), 72 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js index 7ec9e631720e..85b3720e9c7d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js @@ -12,14 +12,6 @@ export function AttachTag(node, context) { const expression = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(node.expression)) : build_legacy_expression(node.expression, context); - context.state.init.push( - b.stmt( - b.call( - '$.attach', - context.state.node, - b.thunk(expression) - ) - ) - ); + context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression)))); context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 80453027a5e9..044424d14d08 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -19,7 +19,7 @@ export function AwaitBlock(node, context) { context.state.analysis.runes ? /** @type {Expression} */ (context.visit(node.expression)) : build_legacy_expression(node.expression, context) - ); + ); let then_block; let catch_block; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index cde0fd8bc21c..c61df712220c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -19,15 +19,7 @@ export function ConstTag(node, context) { const init = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(declaration.init)) : build_legacy_expression(declaration.init, context); - context.state.init.push( - b.const( - declaration.id, - create_derived( - context.state, - b.thunk(init) - ) - ) - ); + context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init)))); context.state.transform[declaration.id.name] = { read: get_value }; @@ -58,10 +50,7 @@ export function ConstTag(node, context) { const fn = b.arrow( [], b.block([ - b.const( - /** @type {Pattern} */ (context.visit(declaration.id, child_state)), - init, - ), + b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init), b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) ]) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 522a49e7c066..27fdebf9d5da 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -32,9 +32,9 @@ export function EachBlock(node, context) { const collection = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(node.expression, parent_scope_state)) : build_legacy_expression(node.expression, { - ...context, - state: parent_scope_state - }); + ...context, + state: parent_scope_state + }); if (!each_node_meta.is_controlled) { context.state.template.push_comment(); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index 9a67e3364533..3b3badce4bcd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -51,7 +51,6 @@ export function IfBlock(node, context) { ) ]; - if (node.elseif) { // We treat this... // diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index 825f18e3ecb0..083011681fe1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -34,7 +34,7 @@ export function RenderTag(node, context) { let snippet_function = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(callee)) - : build_legacy_expression(/** @type {Expression} */(callee), context); + : build_legacy_expression(/** @type {Expression} */ (callee), context); if (node.metadata.dynamic) { // If we have a chain expression then ensure a nullish snippet function gets turned into an empty one diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index d3b5b09bc4fe..4703faedb67d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -370,41 +370,67 @@ function is_pure_expression(expression) { // It's supposed that values do not have custom @@toPrimitive() or toString(), // which may be implicitly called in expressions like `a + b`, `a & b`, `+a`, `str: ${a}` switch (expression.type) { - case "ArrayExpression": return expression.elements.every((element) => element == null || (element.type === "SpreadElement" ? false : is_pure_expression(element))); - case "BinaryExpression": return expression.left.type !== "PrivateIdentifier" && is_pure_expression(expression.left) && is_pure_expression(expression.right); - case "ConditionalExpression": return is_pure_expression(expression.test) && is_pure_expression(expression.consequent) && is_pure_expression(expression.alternate); - case "Identifier": return true; - case "Literal": return true; - case "LogicalExpression": return is_pure_expression(expression.left) && is_pure_expression(expression.right); - case "MetaProperty": return true; // new.target - case "ObjectExpression": return expression.properties.every((property) => - property.type !== "SpreadElement" - && property.key.type !== "PrivateIdentifier" - && is_pure_expression(property.key) - && is_pure_expression(property.value) - ); - case "SequenceExpression": return expression.expressions.every(is_pure_expression); - case "TemplateLiteral": return expression.expressions.every(is_pure_expression); - case "ThisExpression": return true; - case "UnaryExpression": return is_pure_expression(expression.argument); - case "YieldExpression": return expression.argument == null || is_pure_expression(expression.argument); - - case "ArrayPattern": - case "ArrowFunctionExpression": - case "AssignmentExpression": - case "AssignmentPattern": - case "AwaitExpression": - case "CallExpression": - case "ChainExpression": - case "ClassExpression": - case "FunctionExpression": - case "ImportExpression": - case "MemberExpression": - case "NewExpression": - case "ObjectPattern": - case "RestElement": - case "TaggedTemplateExpression": - case "UpdateExpression": + case 'ArrayExpression': + return expression.elements.every( + (element) => + element == null || + (element.type === 'SpreadElement' ? false : is_pure_expression(element)) + ); + case 'BinaryExpression': + return ( + expression.left.type !== 'PrivateIdentifier' && + is_pure_expression(expression.left) && + is_pure_expression(expression.right) + ); + case 'ConditionalExpression': + return ( + is_pure_expression(expression.test) && + is_pure_expression(expression.consequent) && + is_pure_expression(expression.alternate) + ); + case 'Identifier': + return true; + case 'Literal': + return true; + case 'LogicalExpression': + return is_pure_expression(expression.left) && is_pure_expression(expression.right); + case 'MetaProperty': + return true; // new.target + case 'ObjectExpression': + return expression.properties.every( + (property) => + property.type !== 'SpreadElement' && + property.key.type !== 'PrivateIdentifier' && + is_pure_expression(property.key) && + is_pure_expression(property.value) + ); + case 'SequenceExpression': + return expression.expressions.every(is_pure_expression); + case 'TemplateLiteral': + return expression.expressions.every(is_pure_expression); + case 'ThisExpression': + return true; + case 'UnaryExpression': + return is_pure_expression(expression.argument); + case 'YieldExpression': + return expression.argument == null || is_pure_expression(expression.argument); + + case 'ArrayPattern': + case 'ArrowFunctionExpression': + case 'AssignmentExpression': + case 'AssignmentPattern': + case 'AwaitExpression': + case 'CallExpression': + case 'ChainExpression': + case 'ClassExpression': + case 'FunctionExpression': + case 'ImportExpression': + case 'MemberExpression': + case 'NewExpression': + case 'ObjectPattern': + case 'RestElement': + case 'TaggedTemplateExpression': + case 'UpdateExpression': return false; } } @@ -426,23 +452,27 @@ export function build_legacy_expression(expression, context) { for (const [name, nodes] of context.state.scope.references) { const binding = context.state.scope.get(name); - if (binding === null || binding.kind === 'normal' && binding.declaration_kind !== 'import') continue; + if (binding === null || (binding.kind === 'normal' && binding.declaration_kind !== 'import')) + continue; let used = false; for (const { node, path } of nodes) { - const expressionIdx = path.indexOf(expression); - if (expressionIdx < 0) continue; + const expression_idx = path.indexOf(expression); + if (expression_idx < 0) continue; // in Svelte 4, #if, #each and #await copy context, so assignments // aren't propagated to the parent block / component root - const track_assignment = !path.find((node, i) => - i < expressionIdx - 1 && ["IfBlock", "EachBlock", "AwaitBlock"].includes(node.type) - ) + const track_assignment = !path.find( + (node, i) => + i < expression_idx - 1 && ['IfBlock', 'EachBlock', 'AwaitBlock'].includes(node.type) + ); if (track_assignment) { used = true; break; } - const assignment = /** @type {AssignmentExpression|undefined} */(path.find((node, i) => i >= expressionIdx && node.type === "AssignmentExpression")); - if (!assignment || assignment.left !== node && !path.includes(assignment.left)) { + const assignment = /** @type {AssignmentExpression|undefined} */ ( + path.find((node, i) => i >= expression_idx && node.type === 'AssignmentExpression') + ); + if (!assignment || (assignment.left !== node && !path.includes(assignment.left))) { used = true; break; } @@ -461,4 +491,4 @@ export function build_legacy_expression(expression, context) { } return b.sequence([...sequence, b.call('$.untrack', b.thunk(serialized_expression))]); -} \ No newline at end of file +} From efb3b6acc5b4577b224e0decc26b2137c1ab77f4 Mon Sep 17 00:00:00 2001 From: 7nik Date: Wed, 4 Jun 2025 14:37:42 +0300 Subject: [PATCH 04/21] changeset --- .changeset/popular-dancers-switch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/popular-dancers-switch.md diff --git a/.changeset/popular-dancers-switch.md b/.changeset/popular-dancers-switch.md new file mode 100644 index 000000000000..8d39dce64d2e --- /dev/null +++ b/.changeset/popular-dancers-switch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: legacy mode: use coarse reactivity in the block's expressions From 1d188f595c9db864642cdb979260f7132ecd01c0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Jun 2025 18:53:56 -0400 Subject: [PATCH 05/21] WIP --- .../src/compiler/phases/1-parse/state/tag.js | 29 ++++++++++++++--- .../phases/2-analyze/visitors/AwaitBlock.js | 5 ++- .../phases/2-analyze/visitors/ConstTag.js | 5 ++- .../phases/2-analyze/visitors/HtmlTag.js | 2 +- .../phases/2-analyze/visitors/IfBlock.js | 8 ++++- .../phases/2-analyze/visitors/KeyBlock.js | 3 +- .../phases/2-analyze/visitors/RenderTag.js | 2 +- .../3-transform/client/visitors/AttachTag.js | 4 +-- .../3-transform/client/visitors/AwaitBlock.js | 4 +-- .../3-transform/client/visitors/EachBlock.js | 14 ++++++--- .../3-transform/client/visitors/HtmlTag.js | 4 +-- .../3-transform/client/visitors/IfBlock.js | 4 +-- .../3-transform/client/visitors/KeyBlock.js | 4 +-- .../3-transform/client/visitors/RenderTag.js | 8 +++-- .../client/visitors/shared/utils.js | 31 +++++++++++++++++++ .../svelte/src/compiler/types/template.d.ts | 21 +++++++++++++ 16 files changed, 120 insertions(+), 28 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 4153463c8361..5d77d6a8f4b6 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -63,7 +63,10 @@ function open(parser) { end: -1, test: read_expression(parser), consequent: create_fragment(), - alternate: null + alternate: null, + metadata: { + expression: create_expression_metadata() + } }); parser.allow_whitespace(); @@ -244,7 +247,10 @@ function open(parser) { error: null, pending: null, then: null, - catch: null + catch: null, + metadata: { + expression: create_expression_metadata() + } }); if (parser.eat('then')) { @@ -326,7 +332,10 @@ function open(parser) { start, end: -1, expression, - fragment: create_fragment() + fragment: create_fragment(), + metadata: { + expression: create_expression_metadata() + } }); parser.stack.push(block); @@ -461,7 +470,10 @@ function next(parser) { elseif: true, test: expression, consequent: create_fragment(), - alternate: null + alternate: null, + metadata: { + expression: create_expression_metadata() + } }); parser.stack.push(child); @@ -624,7 +636,10 @@ function special(parser) { type: 'HtmlTag', start, end: parser.index, - expression + expression, + metadata: { + expression: create_expression_metadata() + } }); return; @@ -699,6 +714,9 @@ function special(parser) { declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }], start: start + 2, // start at const, not at @const end: parser.index - 1 + }, + metadata: { + expression: create_expression_metadata() } }); } @@ -725,6 +743,7 @@ function special(parser) { end: parser.index, expression: /** @type {AST.RenderTag['expression']} */ (expression), metadata: { + expression: create_expression_metadata(), dynamic: false, arguments: [], path: [], diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js index a71f325154ff..5aa04ba3b9a8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js @@ -41,5 +41,8 @@ export function AwaitBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); + if (node.pending) context.visit(node.pending); + if (node.then) context.visit(node.then); + if (node.catch) context.visit(node.catch); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js index f723f8447cd2..d5f5f7b2e0a0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -32,5 +32,8 @@ export function ConstTag(node, context) { e.const_tag_invalid_placement(node); } - context.next(); + const declaration = node.declaration.declarations[0]; + + context.visit(declaration.id); + context.visit(declaration.init, { ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js index c89b11ad3695..7b0e501760f0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js @@ -15,5 +15,5 @@ export function HtmlTag(node, context) { // unfortunately this is necessary in order to fix invalid HTML mark_subtree_dynamic(context.path); - context.next(); + context.next({ ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js index a65771bcfca9..dcdae3587f63 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js @@ -17,5 +17,11 @@ export function IfBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.test, { + ...context.state, + expression: node.metadata.expression + }); + + context.visit(node.consequent); + if (node.alternate) context.visit(node.alternate); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js index 88bb6a98e748..09e604ea66be 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js @@ -16,5 +16,6 @@ export function KeyBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); + context.visit(node.fragment); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index a8c9d408bdad..1230ef6b048c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -54,7 +54,7 @@ export function RenderTag(node, context) { mark_subtree_dynamic(context.path); - context.visit(callee); + context.visit(callee, { ...context.state, expression: node.metadata.expression }); for (const arg of expression.arguments) { const metadata = create_expression_metadata(); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js index 85b3720e9c7d..9d19e5d4a769 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; -import { build_legacy_expression } from './shared/utils.js'; +import { build_legacy_expression_2 } from './shared/utils.js'; /** * @param {AST.AttachTag} node @@ -11,7 +11,7 @@ import { build_legacy_expression } from './shared/utils.js'; export function AttachTag(node, context) { const expression = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(node.expression)) - : build_legacy_expression(node.expression, context); + : build_legacy_expression_2(context, node.expression, node.metadata.expression); context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression)))); context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 044424d14d08..fcac636d7dc4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -5,7 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; -import { build_legacy_expression } from './shared/utils.js'; +import { build_legacy_expression_2 } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -18,7 +18,7 @@ export function AwaitBlock(node, context) { const expression = b.thunk( context.state.analysis.runes ? /** @type {Expression} */ (context.visit(node.expression)) - : build_legacy_expression(node.expression, context) + : build_legacy_expression_2(context, node.expression, node.metadata.expression) ); let then_block; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 27fdebf9d5da..959ac4723d11 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -14,7 +14,7 @@ import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { build_getter } from '../utils.js'; import { get_value } from './shared/declarations.js'; -import { build_legacy_expression } from './shared/utils.js'; +import { build_legacy_expression_2 } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -31,10 +31,14 @@ export function EachBlock(node, context) { }; const collection = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(node.expression, parent_scope_state)) - : build_legacy_expression(node.expression, { - ...context, - state: parent_scope_state - }); + : build_legacy_expression_2( + { + ...context, + state: parent_scope_state + }, + node.expression, + node.metadata.expression + ); if (!each_node_meta.is_controlled) { context.state.template.push_comment(); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js index b67eff61095d..19a4a7de17ce 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js @@ -3,7 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { is_ignored } from '../../../../state.js'; import * as b from '#compiler/builders'; -import { build_legacy_expression } from './shared/utils.js'; +import { build_legacy_expression_2 } from './shared/utils.js'; /** * @param {AST.HtmlTag} node @@ -14,7 +14,7 @@ export function HtmlTag(node, context) { const expression = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(node.expression)) - : build_legacy_expression(node.expression, context); + : build_legacy_expression_2(context, node.expression, node.metadata.expression); const is_svg = context.state.metadata.namespace === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index 3b3badce4bcd..ad6521e2953d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_legacy_expression } from './shared/utils.js'; +import { build_legacy_expression_2 } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -34,7 +34,7 @@ export function IfBlock(node, context) { const test = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(node.test)) - : build_legacy_expression(node.test, context); + : build_legacy_expression_2(context, node.test, node.metadata.expression); /** @type {Expression[]} */ const args = [ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index c3b4344da0ef..a8b66bbe2ce5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_legacy_expression } from './shared/utils.js'; +import { build_legacy_expression_2 } from './shared/utils.js'; /** * @param {AST.KeyBlock} node @@ -13,7 +13,7 @@ export function KeyBlock(node, context) { const key = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(node.expression)) - : build_legacy_expression(node.expression, context); + : build_legacy_expression_2(context, node.expression, node.metadata.expression); const body = /** @type {Expression} */ (context.visit(node.fragment)); context.state.init.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index 083011681fe1..5b20403a630e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -3,7 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_legacy_expression } from './shared/utils.js'; +import { build_legacy_expression_2 } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -34,7 +34,11 @@ export function RenderTag(node, context) { let snippet_function = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(callee)) - : build_legacy_expression(/** @type {Expression} */ (callee), context); + : build_legacy_expression_2( + context, + /** @type {Expression} */ (callee), + node.metadata.expression + ); if (node.metadata.dynamic) { // If we have a chain expression then ensure a nullish snippet function gets turned into an empty one diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 4703faedb67d..cce6f1539688 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -435,6 +435,37 @@ function is_pure_expression(expression) { } } +/** + * + * @param {ComponentContext} context + * @param {Expression} expression + * @param {ExpressionMetadata} metadata + */ +export function build_legacy_expression_2(context, expression, metadata) { + const sequence = b.sequence([]); + + for (const binding of metadata.dependencies) { + if (binding.kind === 'normal') { + continue; + } + + var getter = build_getter({ ...binding.node }, context.state); + + if (binding.kind === 'rest_prop') { + getter = b.call('Object.keys', getter); + } else if (binding.kind === 'bindable_prop') { + getter = b.call('$.deep_read_state', getter); + } + + sequence.expressions.push(getter); + } + + const value = /** @type {Expression} */ (context.visit(expression)); + sequence.expressions.push(b.call('$.untrack', b.thunk(value))); + + return sequence; +} + /** * Serializes an expression with reactivity like in Svelte 4 * @param {Expression} expression diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index cefc7fa7a20d..2a7ec7b5c6dc 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -135,6 +135,10 @@ export namespace AST { export interface HtmlTag extends BaseNode { type: 'HtmlTag'; expression: Expression; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** An HTML comment */ @@ -151,6 +155,10 @@ export namespace AST { declaration: VariableDeclaration & { declarations: [VariableDeclarator & { id: Pattern; init: Expression }]; }; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** A `{@debug ...}` tag */ @@ -165,6 +173,7 @@ export namespace AST { expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); /** @internal */ metadata: { + expression: ExpressionMetadata; dynamic: boolean; arguments: ExpressionMetadata[]; path: SvelteNode[]; @@ -447,6 +456,10 @@ export namespace AST { test: Expression; consequent: Fragment; alternate: Fragment | null; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** An `{#await ...}` block */ @@ -461,12 +474,20 @@ export namespace AST { pending: Fragment | null; then: Fragment | null; catch: Fragment | null; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } export interface KeyBlock extends BaseNode { type: 'KeyBlock'; expression: Expression; fragment: Fragment; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } export interface SnippetBlock extends BaseNode { From 90baec16e68879a2eb1d898f706a34234bae4101 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Jun 2025 19:12:48 -0400 Subject: [PATCH 06/21] WIP --- .../phases/2-analyze/visitors/AssignmentExpression.js | 4 ++++ .../compiler/phases/2-analyze/visitors/MemberExpression.js | 5 +++-- .../compiler/phases/2-analyze/visitors/UpdateExpression.js | 4 ++++ .../phases/3-transform/client/visitors/shared/utils.js | 7 ++++++- packages/svelte/src/compiler/phases/nodes.js | 4 +++- packages/svelte/src/compiler/types/index.d.ts | 4 ++++ 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js index 673c79f2df0f..39358f72fc1b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js @@ -23,5 +23,9 @@ export function AssignmentExpression(node, context) { } } + if (context.state.expression) { + context.state.expression.has_assignment = true; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js index 245a164c71fb..0a3b3861986c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js @@ -15,8 +15,9 @@ export function MemberExpression(node, context) { } } - if (context.state.expression && !is_pure(node, context)) { - context.state.expression.has_state = true; + if (context.state.expression) { + context.state.expression.has_member_expression = true; + context.state.expression.has_state ||= !is_pure(node, context); } if (!is_safe_identifier(node, context.state.scope)) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js index 13f4b9019e8b..ed48e026ac65 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js @@ -21,5 +21,9 @@ export function UpdateExpression(node, context) { } } + if (context.state.expression) { + context.state.expression.has_assignment = true; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index cce6f1539688..3b1134171279 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -442,6 +442,12 @@ function is_pure_expression(expression) { * @param {ExpressionMetadata} metadata */ export function build_legacy_expression_2(context, expression, metadata) { + const value = /** @type {Expression} */ (context.visit(expression)); + + if (!metadata.has_call && !metadata.has_member_expression && !metadata.has_assignment) { + return value; + } + const sequence = b.sequence([]); for (const binding of metadata.dependencies) { @@ -460,7 +466,6 @@ export function build_legacy_expression_2(context, expression, metadata) { sequence.expressions.push(getter); } - const value = /** @type {Expression} */ (context.visit(expression)); sequence.expressions.push(b.call('$.untrack', b.thunk(value))); return sequence; diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 2043747ed07e..954f23242440 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -63,7 +63,9 @@ export function create_expression_metadata() { return { dependencies: new Set(), has_state: false, - has_call: false + has_call: false, + has_member_expression: false, + has_assignment: false }; } diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index fdd602472608..5bcc99e14712 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -285,6 +285,10 @@ export interface ExpressionMetadata { has_state: boolean; /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ has_call: boolean; + /** True if the expression includes a member expression */ + has_member_expression: boolean; + /** True if the expression includes an assignment or an update */ + has_assignment: boolean; } export interface StateField { From df9e8758b0be51c6df935283f4988ff62d2ccbcc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Jun 2025 19:23:48 -0400 Subject: [PATCH 07/21] note to self --- .../compiler/phases/3-transform/client/visitors/shared/utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 3b1134171279..ad5f734ff057 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -457,6 +457,7 @@ export function build_legacy_expression_2(context, expression, metadata) { var getter = build_getter({ ...binding.node }, context.state); + // TODO do we need all this? if (binding.kind === 'rest_prop') { getter = b.call('Object.keys', getter); } else if (binding.kind === 'bindable_prop') { From d2e14e328b6daab96bc96e848088f5835bb6280b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 7 Jun 2025 06:34:58 -0400 Subject: [PATCH 08/21] WIP --- .../compiler/phases/2-analyze/visitors/Identifier.js | 1 + .../phases/2-analyze/visitors/shared/function.js | 10 ++++++++++ .../phases/3-transform/client/visitors/ConstTag.js | 10 +++++++--- .../phases/3-transform/client/visitors/shared/utils.js | 2 +- packages/svelte/src/compiler/phases/nodes.js | 1 + packages/svelte/src/compiler/types/index.d.ts | 4 +++- 6 files changed, 23 insertions(+), 5 deletions(-) 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 abf70769c013..cced326f9baa 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -90,6 +90,7 @@ export function Identifier(node, context) { if (binding) { if (context.state.expression) { context.state.expression.dependencies.add(binding); + context.state.expression.references.add(binding); context.state.expression.has_state ||= binding.kind !== 'static' && !binding.is_function() && diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js index c892efd421d1..1f399f2b431d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js @@ -13,6 +13,16 @@ export function visit_function(node, context) { scope: context.state.scope }; + if (context.state.expression) { + for (const [name] of context.state.scope.references) { + const binding = context.state.scope.get(name); + + if (binding) { + context.state.expression.references.add(binding); + } + } + } + context.next({ ...context.state, function_depth: context.state.function_depth + 1, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index c61df712220c..0a97a9815670 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -6,7 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; -import { build_legacy_expression } from './shared/utils.js'; +import { build_legacy_expression, build_legacy_expression_2 } from './shared/utils.js'; /** * @param {AST.ConstTag} node @@ -18,7 +18,7 @@ export function ConstTag(node, context) { if (declaration.id.type === 'Identifier') { const init = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(declaration.init)) - : build_legacy_expression(declaration.init, context); + : build_legacy_expression_2(context, declaration.init, node.metadata.expression); context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init)))); context.state.transform[declaration.id.name] = { read: get_value }; @@ -46,7 +46,11 @@ export function ConstTag(node, context) { // instead of destructuring it only to return a new object const init = context.state.analysis.runes ? /** @type {Expression} */ (context.visit(declaration.init, child_state)) - : build_legacy_expression(declaration.init, { ...context, state: child_state }); + : build_legacy_expression_2( + { ...context, state: child_state }, + declaration.init, + node.metadata.expression + ); const fn = b.arrow( [], b.block([ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index ad5f734ff057..4b591c48e6a8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -450,7 +450,7 @@ export function build_legacy_expression_2(context, expression, metadata) { const sequence = b.sequence([]); - for (const binding of metadata.dependencies) { + for (const binding of metadata.references) { if (binding.kind === 'normal') { continue; } diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 954f23242440..c35f194b7531 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -62,6 +62,7 @@ export function create_attribute(name, start, end, value) { export function create_expression_metadata() { return { dependencies: new Set(), + references: new Set(), has_state: false, has_call: false, has_member_expression: false, diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 5bcc99e14712..558ee558f7a7 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -279,8 +279,10 @@ export type DeclarationKind = | 'synthetic'; export interface ExpressionMetadata { - /** All the bindings that are referenced inside this expression */ + /** All the bindings that are referenced eagerly (not inside functions) in this expression */ dependencies: Set; + /** All the bindings that are referenced inside this expression, including inside functions */ + references: Set; /** True if the expression references state directly, or _might_ (via member/call expressions) */ has_state: boolean; /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ From b42a89471b65a0955221f135505fc8f43ea3a16a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 7 Jun 2025 07:27:11 -0400 Subject: [PATCH 09/21] simplify --- .../3-transform/client/visitors/AttachTag.js | 7 +- .../3-transform/client/visitors/AwaitBlock.js | 10 +- .../3-transform/client/visitors/ConstTag.js | 20 +-- .../3-transform/client/visitors/EachBlock.js | 24 ++- .../3-transform/client/visitors/HtmlTag.js | 7 +- .../3-transform/client/visitors/IfBlock.js | 6 +- .../3-transform/client/visitors/KeyBlock.js | 6 +- .../3-transform/client/visitors/RenderTag.js | 14 +- .../client/visitors/shared/utils.js | 138 +----------------- 9 files changed, 41 insertions(+), 191 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js index 9d19e5d4a769..8b1570c7dc3c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js @@ -1,17 +1,14 @@ -/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; -import { build_legacy_expression_2 } from './shared/utils.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.AttachTag} node * @param {ComponentContext} context */ export function AttachTag(node, context) { - const expression = context.state.analysis.runes - ? /** @type {Expression} */ (context.visit(node.expression)) - : build_legacy_expression_2(context, node.expression, node.metadata.expression); + const expression = build_expression(context, node.expression, node.metadata.expression); context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression)))); context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index fcac636d7dc4..7873cf3ddbd7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -1,11 +1,11 @@ -/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */ +/** @import { BlockStatement, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; -import { build_legacy_expression_2 } from './shared/utils.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -15,11 +15,7 @@ export function AwaitBlock(node, context) { context.state.template.push_comment(); // Visit {#await } first to ensure that scopes are in the correct order - const expression = b.thunk( - context.state.analysis.runes - ? /** @type {Expression} */ (context.visit(node.expression)) - : build_legacy_expression_2(context, node.expression, node.metadata.expression) - ); + const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression)); let then_block; let catch_block; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 0a97a9815670..c1be1e3220b0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -1,4 +1,4 @@ -/** @import { Expression, Pattern } from 'estree' */ +/** @import { Pattern } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; @@ -6,7 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; -import { build_legacy_expression, build_legacy_expression_2 } from './shared/utils.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.ConstTag} node @@ -16,9 +16,7 @@ export function ConstTag(node, context) { const declaration = node.declaration.declarations[0]; // TODO we can almost certainly share some code with $derived(...) if (declaration.id.type === 'Identifier') { - const init = context.state.analysis.runes - ? /** @type {Expression} */ (context.visit(declaration.init)) - : build_legacy_expression_2(context, declaration.init, node.metadata.expression); + const init = build_expression(context, declaration.init, node.metadata.expression); context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init)))); context.state.transform[declaration.id.name] = { read: get_value }; @@ -44,13 +42,11 @@ export function ConstTag(node, context) { // TODO optimise the simple `{ x } = y` case — we can just return `y` // instead of destructuring it only to return a new object - const init = context.state.analysis.runes - ? /** @type {Expression} */ (context.visit(declaration.init, child_state)) - : build_legacy_expression_2( - { ...context, state: child_state }, - declaration.init, - node.metadata.expression - ); + const init = build_expression( + { ...context, state: child_state }, + declaration.init, + node.metadata.expression + ); const fn = b.arrow( [], b.block([ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 959ac4723d11..201c4b278f78 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, Identifier, Pattern, SequenceExpression, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { ComponentContext } from '../types' */ /** @import { Scope } from '../../../scope' */ @@ -12,9 +12,8 @@ import { import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_getter } from '../utils.js'; import { get_value } from './shared/declarations.js'; -import { build_legacy_expression_2 } from './shared/utils.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -29,16 +28,15 @@ export function EachBlock(node, context) { ...context.state, scope: /** @type {Scope} */ (context.state.scope.parent) }; - const collection = context.state.analysis.runes - ? /** @type {Expression} */ (context.visit(node.expression, parent_scope_state)) - : build_legacy_expression_2( - { - ...context, - state: parent_scope_state - }, - node.expression, - node.metadata.expression - ); + + const collection = build_expression( + { + ...context, + state: parent_scope_state + }, + node.expression, + node.metadata.expression + ); if (!each_node_meta.is_controlled) { context.state.template.push_comment(); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js index 19a4a7de17ce..fb599679968a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js @@ -1,9 +1,8 @@ -/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { is_ignored } from '../../../../state.js'; import * as b from '#compiler/builders'; -import { build_legacy_expression_2 } from './shared/utils.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.HtmlTag} node @@ -12,9 +11,7 @@ import { build_legacy_expression_2 } from './shared/utils.js'; export function HtmlTag(node, context) { context.state.template.push_comment(); - const expression = context.state.analysis.runes - ? /** @type {Expression} */ (context.visit(node.expression)) - : build_legacy_expression_2(context, node.expression, node.metadata.expression); + const expression = build_expression(context, node.expression, node.metadata.expression); const is_svg = context.state.metadata.namespace === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index ad6521e2953d..deab040e509b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_legacy_expression_2 } from './shared/utils.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -32,9 +32,7 @@ export function IfBlock(node, context) { statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate))); } - const test = context.state.analysis.runes - ? /** @type {Expression} */ (context.visit(node.test)) - : build_legacy_expression_2(context, node.test, node.metadata.expression); + const test = build_expression(context, node.test, node.metadata.expression); /** @type {Expression[]} */ const args = [ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index a8b66bbe2ce5..2f17479c7e9e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_legacy_expression_2 } from './shared/utils.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.KeyBlock} node @@ -11,9 +11,7 @@ import { build_legacy_expression_2 } from './shared/utils.js'; export function KeyBlock(node, context) { context.state.template.push_comment(); - const key = context.state.analysis.runes - ? /** @type {Expression} */ (context.visit(node.expression)) - : build_legacy_expression_2(context, node.expression, node.metadata.expression); + const key = build_expression(context, node.expression, node.metadata.expression); const body = /** @type {Expression} */ (context.visit(node.fragment)); context.state.init.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index 5b20403a630e..4e09be2a4a14 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -3,7 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_legacy_expression_2 } from './shared/utils.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -32,13 +32,11 @@ export function RenderTag(node, context) { } } - let snippet_function = context.state.analysis.runes - ? /** @type {Expression} */ (context.visit(callee)) - : build_legacy_expression_2( - context, - /** @type {Expression} */ (callee), - node.metadata.expression - ); + let snippet_function = build_expression( + context, + /** @type {Expression} */ (callee), + node.metadata.expression + ); if (node.metadata.dynamic) { // If we have a chain expression then ensure a nullish snippet function gets turned into an empty one diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 4b591c48e6a8..88d06299bf1d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -361,89 +361,19 @@ export function validate_mutation(node, context, expression) { ); } -/** - * Checks whether the expression contains assignments, function calls, or member accesses - * @param {Expression|Pattern} expression - * @returns {boolean} - */ -function is_pure_expression(expression) { - // It's supposed that values do not have custom @@toPrimitive() or toString(), - // which may be implicitly called in expressions like `a + b`, `a & b`, `+a`, `str: ${a}` - switch (expression.type) { - case 'ArrayExpression': - return expression.elements.every( - (element) => - element == null || - (element.type === 'SpreadElement' ? false : is_pure_expression(element)) - ); - case 'BinaryExpression': - return ( - expression.left.type !== 'PrivateIdentifier' && - is_pure_expression(expression.left) && - is_pure_expression(expression.right) - ); - case 'ConditionalExpression': - return ( - is_pure_expression(expression.test) && - is_pure_expression(expression.consequent) && - is_pure_expression(expression.alternate) - ); - case 'Identifier': - return true; - case 'Literal': - return true; - case 'LogicalExpression': - return is_pure_expression(expression.left) && is_pure_expression(expression.right); - case 'MetaProperty': - return true; // new.target - case 'ObjectExpression': - return expression.properties.every( - (property) => - property.type !== 'SpreadElement' && - property.key.type !== 'PrivateIdentifier' && - is_pure_expression(property.key) && - is_pure_expression(property.value) - ); - case 'SequenceExpression': - return expression.expressions.every(is_pure_expression); - case 'TemplateLiteral': - return expression.expressions.every(is_pure_expression); - case 'ThisExpression': - return true; - case 'UnaryExpression': - return is_pure_expression(expression.argument); - case 'YieldExpression': - return expression.argument == null || is_pure_expression(expression.argument); - - case 'ArrayPattern': - case 'ArrowFunctionExpression': - case 'AssignmentExpression': - case 'AssignmentPattern': - case 'AwaitExpression': - case 'CallExpression': - case 'ChainExpression': - case 'ClassExpression': - case 'FunctionExpression': - case 'ImportExpression': - case 'MemberExpression': - case 'NewExpression': - case 'ObjectPattern': - case 'RestElement': - case 'TaggedTemplateExpression': - case 'UpdateExpression': - return false; - } -} - /** * * @param {ComponentContext} context * @param {Expression} expression * @param {ExpressionMetadata} metadata */ -export function build_legacy_expression_2(context, expression, metadata) { +export function build_expression(context, expression, metadata) { const value = /** @type {Expression} */ (context.visit(expression)); + if (context.state.analysis.runes) { + return value; + } + if (!metadata.has_call && !metadata.has_member_expression && !metadata.has_assignment) { return value; } @@ -471,61 +401,3 @@ export function build_legacy_expression_2(context, expression, metadata) { return sequence; } - -/** - * Serializes an expression with reactivity like in Svelte 4 - * @param {Expression} expression - * @param {ComponentContext} context - */ -export function build_legacy_expression(expression, context) { - // To recreate Svelte 4 behaviour, we track the dependencies - // the compiler can 'see', but we untrack the effect itself - const serialized_expression = /** @type {Expression} */ (context.visit(expression)); - if (is_pure_expression(expression)) return serialized_expression; - - /** @type {Expression[]} */ - const sequence = []; - - for (const [name, nodes] of context.state.scope.references) { - const binding = context.state.scope.get(name); - - if (binding === null || (binding.kind === 'normal' && binding.declaration_kind !== 'import')) - continue; - - let used = false; - for (const { node, path } of nodes) { - const expression_idx = path.indexOf(expression); - if (expression_idx < 0) continue; - // in Svelte 4, #if, #each and #await copy context, so assignments - // aren't propagated to the parent block / component root - const track_assignment = !path.find( - (node, i) => - i < expression_idx - 1 && ['IfBlock', 'EachBlock', 'AwaitBlock'].includes(node.type) - ); - if (track_assignment) { - used = true; - break; - } - const assignment = /** @type {AssignmentExpression|undefined} */ ( - path.find((node, i) => i >= expression_idx && node.type === 'AssignmentExpression') - ); - if (!assignment || (assignment.left !== node && !path.includes(assignment.left))) { - used = true; - break; - } - } - if (!used) continue; - - let serialized = build_getter(b.id(name), context.state); - - // If the binding is a prop, we need to deep read it because it could be fine-grained $state - // from a runes-component, where mutations don't trigger an update on the prop as a whole. - if (name === '$$props' || name === '$$restProps' || binding.kind === 'bindable_prop') { - serialized = b.call('$.deep_read_state', serialized); - } - - sequence.push(serialized); - } - - return b.sequence([...sequence, b.call('$.untrack', b.thunk(serialized_expression))]); -} From c9ee8df1a5b54d6499d94742acc5f0ce05401695 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 7 Jun 2025 07:56:15 -0400 Subject: [PATCH 10/21] apparently not? --- .../phases/3-transform/client/visitors/shared/utils.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 88d06299bf1d..1fcc05616fc3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -387,10 +387,7 @@ export function build_expression(context, expression, metadata) { var getter = build_getter({ ...binding.node }, context.state); - // TODO do we need all this? - if (binding.kind === 'rest_prop') { - getter = b.call('Object.keys', getter); - } else if (binding.kind === 'bindable_prop') { + if (binding.kind === 'bindable_prop') { getter = b.call('$.deep_read_state', getter); } From 3536a9fd82af68a989ffe1ca74ef06dfe320f0b7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 7 Jun 2025 08:20:11 -0400 Subject: [PATCH 11/21] fix --- .../src/compiler/phases/2-analyze/visitors/shared/function.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js index 1f399f2b431d..177616785026 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js @@ -17,7 +17,7 @@ export function visit_function(node, context) { for (const [name] of context.state.scope.references) { const binding = context.state.scope.get(name); - if (binding) { + if (binding && binding.scope.function_depth < context.state.scope.function_depth) { context.state.expression.references.add(binding); } } From 5e8f84832de70dbc5add4c5814345419ac097f5f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 8 Jun 2025 07:43:03 -0400 Subject: [PATCH 12/21] update test --- .../runtime-legacy/samples/block-expression-assign/_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js index 6008048ccbea..15adef2c9be7 100644 --- a/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js @@ -7,6 +7,6 @@ export default test({ assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,0]`); flushSync(() => button?.click()); - assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,1]`); + assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,0]`); } }); From a14bb0245e754ec11913ebacdb0a8fbb9a1c9594 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 11 Jun 2025 20:59:14 -0400 Subject: [PATCH 13/21] revert test change --- .../each-index-non-null/_expected/client/index.svelte.js | 2 +- .../each-index-non-null/_expected/server/index.svelte.js | 2 +- .../tests/snapshot/samples/each-index-non-null/index.svelte | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js index b6cc2b0d7676..804a7c26f182 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -8,7 +8,7 @@ export default function Each_index_non_null($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.each(node, 0, () => [,,,,,], $.index, ($$anchor, $$item, i) => { + $.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => { var p = root_1(); p.textContent = `index: ${i}`; diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js index afa9f32e76e8..3431e36833b5 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Each_index_non_null($$payload) { - const each_array = $.ensure_array_like([,,,,,]); + const each_array = $.ensure_array_like(Array(10)); $$payload.out += ``; diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte index 3407f9ab5943..03bfc9e37299 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte @@ -1,3 +1,3 @@ -{#each [,,,,,], i} +{#each Array(10), i}

index: {i}

{/each} From 4d53ca21f4de8968269f8fd9adaeb5f4d9214c14 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 11 Jun 2025 21:55:14 -0400 Subject: [PATCH 14/21] apply same treatment to render tag args --- .../compiler/phases/3-transform/client/visitors/RenderTag.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index 4e09be2a4a14..c3615d9d5085 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -20,7 +20,10 @@ export function RenderTag(node, context) { /** @type {Expression[]} */ let args = []; for (let i = 0; i < raw_args.length; i++) { - let thunk = b.thunk(/** @type {Expression} */ (context.visit(raw_args[i]))); + let thunk = b.thunk( + build_expression(context, /** @type {Expression} */ (raw_args[i]), node.metadata.arguments[i]) + ); + const { has_call } = node.metadata.arguments[i]; if (has_call) { From 57b13d363aff989336711e774bcd9cc6fdcc7aa8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 12 Jun 2025 08:08:43 -0400 Subject: [PATCH 15/21] Update .changeset/popular-dancers-switch.md --- .changeset/popular-dancers-switch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/popular-dancers-switch.md b/.changeset/popular-dancers-switch.md index 8d39dce64d2e..b8c26c210e63 100644 --- a/.changeset/popular-dancers-switch.md +++ b/.changeset/popular-dancers-switch.md @@ -2,4 +2,4 @@ 'svelte': patch --- -fix: legacy mode: use coarse reactivity in the block's expressions +fix: use compiler-driven reactivity in legacy mode template expressions From c2a62070ee3ae7d07d113041204b1a46754fe2d9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 12 Jun 2025 08:44:57 -0400 Subject: [PATCH 16/21] WIP --- .../client/visitors/RegularElement.js | 2 +- .../client/visitors/TitleElement.js | 3 +- .../client/visitors/shared/element.js | 2 +- .../client/visitors/shared/fragment.js | 28 +++++++++---------- .../client/visitors/shared/utils.js | 8 +++--- 5 files changed, 21 insertions(+), 22 deletions(-) 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 e82379299315..1aefff0db08e 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 @@ -331,7 +331,7 @@ export function RegularElement(node, context) { trimmed.some((node) => node.type === 'ExpressionTag'); if (use_text_content) { - const { value } = build_template_chunk(trimmed, context.visit, child_state); + const { value } = build_template_chunk(trimmed, context, child_state); const empty_string = value.type === 'Literal' && value.value === ''; if (!empty_string) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 7bfdaf1850d2..e6f4202a0189 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -10,8 +10,7 @@ import { build_template_chunk } from './shared/utils.js'; export function TitleElement(node, context) { const { has_state, value } = build_template_chunk( /** @type {any} */ (node.fragment.nodes), - context.visit, - context.state + context ); const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value)); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 67de25b77041..c734897e3b8c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -129,7 +129,7 @@ export function build_attribute_value(value, context, memoize = (value) => value }; } - return build_template_chunk(value, context.visit, context.state, memoize); + return build_template_chunk(value, context, context.state, memoize); } /** 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 7af2c2d4aaa8..62d07014eea4 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 @@ -16,8 +16,8 @@ import { build_template_chunk } from './utils.js'; * @param {boolean} is_element * @param {ComponentContext} context */ -export function process_children(nodes, initial, is_element, { visit, state }) { - const within_bound_contenteditable = state.metadata.bound_contenteditable; +export function process_children(nodes, initial, is_element, context) { + const within_bound_contenteditable = context.state.metadata.bound_contenteditable; let prev = initial; let skipped = 0; @@ -48,8 +48,8 @@ export function process_children(nodes, initial, is_element, { visit, state }) { let id = expression; if (id.type !== 'Identifier') { - id = b.id(state.scope.generate(name)); - state.init.push(b.var(id, expression)); + id = b.id(context.state.scope.generate(name)); + context.state.init.push(b.var(id, expression)); } prev = () => id; @@ -64,13 +64,13 @@ export function process_children(nodes, initial, is_element, { visit, state }) { function flush_sequence(sequence) { if (sequence.every((node) => node.type === 'Text')) { skipped += 1; - state.template.push_text(sequence); + context.state.template.push_text(sequence); return; } - state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]); + context.state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]); - const { has_state, value } = build_template_chunk(sequence, visit, state); + const { has_state, value } = build_template_chunk(sequence, context); // if this is a standalone `{expression}`, make sure we handle the case where // no text node was created because the expression was empty during SSR @@ -80,9 +80,9 @@ export function process_children(nodes, initial, is_element, { visit, state }) { const update = b.stmt(b.call('$.set_text', id, value)); if (has_state && !within_bound_contenteditable) { - state.update.push(update); + context.state.update.push(update); } else { - state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); + context.state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); } } @@ -95,18 +95,18 @@ export function process_children(nodes, initial, is_element, { visit, state }) { sequence = []; } - let child_state = state; + let child_state = context.state; - if (is_static_element(node, state)) { + if (is_static_element(node, context.state)) { skipped += 1; } else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) { node.metadata.is_controlled = true; } else { const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node'); - child_state = { ...state, node: id }; + child_state = { ...context.state, node: id }; } - visit(node, child_state); + context.visit(node, child_state); } } @@ -118,7 +118,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { // traverse to the last (n - 1) one when hydrating if (skipped > 1) { skipped -= 1; - state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped)))); + context.state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped)))); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 1fcc05616fc3..48a159a2a369 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -31,15 +31,15 @@ export function get_expression_id(expressions, value) { /** * @param {Array} values - * @param {(node: AST.SvelteNode, state: any) => any} visit + * @param {ComponentContext} context * @param {ComponentClientTransformState} state * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize * @returns {{ value: Expression, has_state: boolean }} */ export function build_template_chunk( values, - visit, - state, + context, + state = context.state, memoize = (value, metadata) => metadata.has_call ? get_expression_id(state.expressions, value) : value ) { @@ -66,7 +66,7 @@ export function build_template_chunk( state.scope.get('undefined') ) { let value = memoize( - /** @type {Expression} */ (visit(node.expression, state)), + /** @type {Expression} */ (context.visit(node.expression, state)), node.metadata.expression ); From 9d01d366491475ef71dc26d33f3dd5078b4cef4b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 12 Jun 2025 08:48:25 -0400 Subject: [PATCH 17/21] fix template effects --- .../phases/3-transform/client/visitors/shared/element.js | 4 ++-- .../phases/3-transform/client/visitors/shared/utils.js | 8 ++++---- .../samples/block-expression-fn-call/_config.js | 4 ++-- .../samples/block-expression-fn-call/main.svelte | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index c734897e3b8c..10f942b7d46b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; -import { build_template_chunk, get_expression_id } from './utils.js'; +import { build_expression, build_template_chunk, get_expression_id } from './utils.js'; /** * @param {Array} attributes @@ -121,7 +121,7 @@ export function build_attribute_value(value, context, memoize = (value) => value return { value: b.literal(chunk.data), has_state: false }; } - let expression = /** @type {Expression} */ (context.visit(chunk.expression)); + let expression = build_expression(context, chunk.expression, chunk.metadata.expression); return { value: memoize(expression, chunk.metadata.expression), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 48a159a2a369..68299668d487 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -66,7 +66,7 @@ export function build_template_chunk( state.scope.get('undefined') ) { let value = memoize( - /** @type {Expression} */ (context.visit(node.expression, state)), + build_expression(context, node.expression, node.metadata.expression, state), node.metadata.expression ); @@ -367,8 +367,8 @@ export function validate_mutation(node, context, expression) { * @param {Expression} expression * @param {ExpressionMetadata} metadata */ -export function build_expression(context, expression, metadata) { - const value = /** @type {Expression} */ (context.visit(expression)); +export function build_expression(context, expression, metadata, state = context.state) { + const value = /** @type {Expression} */ (context.visit(expression, state)); if (context.state.analysis.runes) { return value; @@ -385,7 +385,7 @@ export function build_expression(context, expression, metadata) { continue; } - var getter = build_getter({ ...binding.node }, context.state); + var getter = build_getter({ ...binding.node }, state); if (binding.kind === 'bindable_prop') { getter = b.call('$.deep_read_state', getter); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js index 0e1a5a81502f..523dcd625dce 100644 --- a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js @@ -5,8 +5,8 @@ export default test({ test({ assert, target }) { const button = target.querySelector('button'); - assert.htmlEqual(target.innerHTML, `
10 - 10`); + assert.htmlEqual(target.innerHTML, `
12 - 12`); flushSync(() => button?.click()); - assert.htmlEqual(target.innerHTML, `
11 - 10`); + assert.htmlEqual(target.innerHTML, `
13 - 12`); } }); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte index 3f4c6f01072d..37838f091fdf 100644 --- a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte @@ -30,7 +30,7 @@ {x} {/key} - + {count1} - {count2} From bf5b7216c9d22757fde1abb56f78be3cacfad23c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 12 Jun 2025 09:01:40 -0400 Subject: [PATCH 18/21] WIP --- .../phases/3-transform/client/visitors/shared/utils.js | 6 +++++- .../samples/purity/_expected/client/index.svelte.js | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 68299668d487..f69e1db2d437 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -387,7 +387,11 @@ export function build_expression(context, expression, metadata, state = context. var getter = build_getter({ ...binding.node }, state); - if (binding.kind === 'bindable_prop') { + if ( + binding.kind === 'bindable_prop' || + binding.node.name === '$$props' || + binding.node.name === '$$restProps' + ) { getter = b.call('$.deep_read_state', getter); } diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js index a351851875ed..da6fdf44d881 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js @@ -8,11 +8,13 @@ export default function Purity($$anchor) { var fragment = root(); var p = $.first_child(fragment); - p.textContent = '0'; + p.textContent = ( + $.untrack(() => Math.max(0, Math.min(0, 100))) + ); var p_1 = $.sibling(p, 2); - p_1.textContent = location.href; + p_1.textContent = ($.untrack(() => location.href)); var node = $.sibling(p_1, 2); From 678ba79a50f038942d6908810f78af0c89e1b4d4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 12 Jun 2025 09:05:25 -0400 Subject: [PATCH 19/21] WIP --- .../compiler/phases/3-transform/client/visitors/shared/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index f69e1db2d437..6295147ffa0a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -381,7 +381,7 @@ export function build_expression(context, expression, metadata, state = context. const sequence = b.sequence([]); for (const binding of metadata.references) { - if (binding.kind === 'normal') { + if (binding.kind === 'normal' && binding.declaration_kind !== 'import') { continue; } From f6c2433298704df14474e23abaf777f5501621f0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 12 Jun 2025 09:16:20 -0400 Subject: [PATCH 20/21] handle irksome edge cases --- .../phases/3-transform/client/visitors/shared/utils.js | 2 ++ .../samples/lifecycle-render-order-for-children/Item.svelte | 4 ++-- .../samples/lifecycle-render-order-for-children/main.svelte | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 6295147ffa0a..ad171af8f32f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -389,6 +389,8 @@ export function build_expression(context, expression, metadata, state = context. if ( binding.kind === 'bindable_prop' || + binding.kind === 'template' || + binding.declaration_kind === 'import' || binding.node.name === '$$props' || binding.node.name === '$$restProps' ) { diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte index b2e6cd046c8e..4127e857d5d5 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte @@ -5,7 +5,7 @@ export let index; export let n; - function logRender () { + function logRender (n) { order.push(`${index}: render ${n}`); return index; } @@ -24,5 +24,5 @@
  • - {logRender()} + {logRender(n)}
  • diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte index b05b1476fdcc..51dee3bc0c25 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte @@ -5,7 +5,7 @@ export let n = 0; - function logRender () { + function logRender (n) { order.push(`parent: render ${n}`); return 'parent'; } @@ -23,7 +23,7 @@ }) -{logRender()} +{logRender(n)}
      {#each [1,2,3] as index} From d2932a5709d4ac945a32ae00c76f4eebdfb1d9d9 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 17 Jun 2025 18:42:07 +0200 Subject: [PATCH 21/21] Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js --- .../compiler/phases/3-transform/client/visitors/shared/utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index ad171af8f32f..15982899c9d7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -378,6 +378,7 @@ export function build_expression(context, expression, metadata, state = context. return value; } + // Legacy reactivity is coarse-grained, looking at the statically visible dependencies. Replicate that here const sequence = b.sequence([]); for (const binding of metadata.references) {