From 1f51993b93bfd24b2f290d7d8f9211f47f932a04 Mon Sep 17 00:00:00 2001 From: 7nik Date: Tue, 3 Jun 2025 17:13:18 +0300 Subject: [PATCH] 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 062604cacc..7ec9e63172 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 30e370327f..80453027a5 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 2f3c0b3d0e..cde0fd8bc2 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 6c651464f1..522a49e7c0 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 405b400b42..b67eff6109 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 3702a47bc9..9a67e33645 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 5e63f7e872..c3b4344da0 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 fec7b5762a..825f18e3ec 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 ebf88e878f..36a3707e74 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 0000000000..6008048ccb --- /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 0000000000..67190669ed --- /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 0000000000..0e1a5a8150 --- /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 0000000000..3f4c6f0107 --- /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 0000000000..0e1a5a8150 --- /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 0000000000..4041be4f6f --- /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} + +