From b42a89471b65a0955221f135505fc8f43ea3a16a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 7 Jun 2025 07:27:11 -0400 Subject: [PATCH] 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 9d19e5d4a7..8b1570c7dc 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 fcac636d7d..7873cf3ddb 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 0a97a98156..c1be1e3220 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 959ac4723d..201c4b278f 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 19a4a7de17..fb59967996 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 ad6521e295..deab040e50 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 a8b66bbe2c..2f17479c7e 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 5b20403a63..4e09be2a4a 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 4b591c48e6..88d06299bf 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))]); -}