From bda21bf2cc3d787d0d77d749f45f69d3cf08661c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 27 May 2025 14:32:35 -0400 Subject: [PATCH] WIP --- packages/svelte/src/compiler/migrate/index.js | 2 +- .../2-analyze/visitors/VariableDeclarator.js | 3 +- .../3-transform/client/visitors/EachBlock.js | 4 +- .../client/visitors/SnippetBlock.js | 4 +- .../client/visitors/VariableDeclaration.js | 23 +-- .../server/visitors/VariableDeclaration.js | 6 +- .../phases/3-transform/shared/assignments.js | 4 +- packages/svelte/src/compiler/utils/ast.js | 192 +----------------- 8 files changed, 31 insertions(+), 207 deletions(-) diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 753a07bbb0..5ca9adb98b 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -604,7 +604,7 @@ const instance_script = { // Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = .. // means that foo and bar are the props (i.e. the leafs are the prop names), not x and z. // const tmp = b.id(state.scope.generate('tmp')); - // const paths = destructure(declarator.id, tmp); + // const paths = extract_paths(declarator.id, tmp); // state.props_pre.push( // b.declaration('const', tmp, visit(declarator.init!) as Expression) // ); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js index c4b4272aba..d98fdfca47 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js @@ -7,6 +7,7 @@ import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { extract_paths } from '../../../utils/ast.js'; import { equal } from '../../../utils/assert.js'; +import * as b from '#compiler/builders'; /** * @param {VariableDeclarator} node @@ -18,7 +19,7 @@ export function VariableDeclarator(node, context) { if (context.state.analysis.runes) { const init = node.init; const rune = get_rune(init, context.state.scope); - const paths = extract_paths(node.id); + const paths = extract_paths(node.id, b.id('dummy')); for (const path of paths) { validate_identifier_name(context.state.scope.get(/** @type {Identifier} */ (path.node).name)); 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 4ab2f4f8c4..4267067210 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 @@ -10,7 +10,7 @@ import { EACH_ITEM_REACTIVE } from '../../../../../constants.js'; import { dev } from '../../../../state.js'; -import { destructure, object } from '../../../../utils/ast.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'; @@ -234,7 +234,7 @@ export function EachBlock(node, context) { } else if (node.context) { const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item; - for (const path of destructure(node.context, unwrapped)) { + for (const path of extract_paths(node.context, unwrapped)) { const name = /** @type {Identifier} */ (path.node).name; const needs_derived = path.has_default_value; // to ensure that default value is only called once diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index 32b8c07fae..a04f817226 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; -import { destructure } from '../../../../utils/ast.js'; +import { extract_paths } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { get_value } from './shared/declarations.js'; @@ -43,7 +43,7 @@ export function SnippetBlock(node, context) { let arg_alias = `$$arg${i}`; args.push(b.id(arg_alias)); - const paths = destructure(argument, b.maybe_call(b.id(arg_alias))); + const paths = extract_paths(argument, b.maybe_call(b.id(arg_alias))); for (const path of paths) { const name = /** @type {Identifier} */ (path.node).name; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 5287194925..7e11dc6336 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -2,7 +2,7 @@ /** @import { Binding } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; -import { destructure, extract_paths } from '../../../../utils/ast.js'; +import { extract_paths } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; @@ -142,7 +142,7 @@ export function VariableDeclaration(node, context) { ); } else { const tmp = b.id(context.state.scope.generate('tmp')); - const paths = destructure(declarator.id, tmp); + const paths = extract_paths(declarator.id, tmp); declarations.push( b.declarator(tmp, value), ...paths.map((path) => { @@ -170,18 +170,12 @@ export function VariableDeclaration(node, context) { ) ); } else { - const paths = extract_paths(declarator.id); - const init = /** @type {CallExpression} */ (declarator.init); - /** @type {Identifier} */ - let id; let rhs = value; - if (rune === '$derived' && init.arguments[0].type === 'Identifier') { - id = init.arguments[0]; - } else { - id = b.id(context.state.scope.generate('$$d')); + if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') { + const id = b.id(context.state.scope.generate('$$d')); rhs = b.call('$.get', id); declarations.push( @@ -189,10 +183,9 @@ export function VariableDeclaration(node, context) { ); } - for (let i = 0; i < paths.length; i++) { - const path = paths[i]; + for (const path of extract_paths(declarator.id, rhs)) { declarations.push( - b.declarator(path.node, b.call('$.derived', b.thunk(path.expression(rhs)))) + b.declarator(path.node, b.call('$.derived', b.thunk(path.expression))) ); } } @@ -225,7 +218,7 @@ export function VariableDeclaration(node, context) { // Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = .. // means that foo and bar are the props (i.e. the leafs are the prop names), not x and z. const tmp = b.id(context.state.scope.generate('tmp')); - const paths = destructure(declarator.id, tmp); + const paths = extract_paths(declarator.id, tmp); declarations.push( b.declarator( @@ -305,7 +298,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) { } const tmp = b.id(scope.generate('tmp')); - const paths = destructure(declarator.id, tmp); + const paths = extract_paths(declarator.id, tmp); return [ b.declarator(tmp, value), ...paths.map((path) => { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index 0eca518db6..25994091ba 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js @@ -3,7 +3,7 @@ /** @import { Context } from '../types.js' */ /** @import { ComponentAnalysis } from '../../../types.js' */ /** @import { Scope } from '../../../scope.js' */ -import { build_fallback, destructure } from '../../../../utils/ast.js'; +import { build_fallback, extract_paths } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { get_rune } from '../../../scope.js'; import { walk } from 'zimmerframe'; @@ -121,7 +121,7 @@ export function VariableDeclaration(node, context) { // Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = .. // means that foo and bar are the props (i.e. the leafs are the prop names), not x and z. const tmp = b.id(context.state.scope.generate('tmp')); - const paths = destructure(declarator.id, tmp); + const paths = extract_paths(declarator.id, tmp); declarations.push( b.declarator( tmp, @@ -189,7 +189,7 @@ function create_state_declarators(declarator, scope, value) { } const tmp = b.id(scope.generate('tmp')); - const paths = destructure(declarator.id, tmp); + const paths = extract_paths(declarator.id, tmp); return [ b.declarator(tmp, value), // TODO inject declarator for opts, so we can use it below ...paths.map((path) => { diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js index ff257d70ff..abd3b37c32 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js @@ -1,7 +1,7 @@ /** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern } from 'estree' */ /** @import { Context as ClientContext } from '../client/types.js' */ /** @import { Context as ServerContext } from '../server/types.js' */ -import { destructure, is_expression_async } from '../../../utils/ast.js'; +import { extract_paths, is_expression_async } from '../../../utils/ast.js'; import * as b from '#compiler/builders'; /** @@ -23,7 +23,7 @@ export function visit_assignment_expression(node, context, build_assignment) { let changed = false; - const assignments = destructure(node.left, rhs).map((path) => { + const assignments = extract_paths(node.left, rhs).map((path) => { const value = path.expression; let assignment = build_assignment('=', path.node, value, context); diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 16608be932..9197815761 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -227,176 +227,6 @@ export function extract_identifiers_from_destructuring(node, nodes = []) { * @property {ESTree.Identifier | ESTree.MemberExpression} node The node the destructuring path end in. Can be a member expression only for assignment expressions * @property {boolean} is_rest `true` if this is a `...rest` destructuring * @property {boolean} has_default_value `true` if this has a fallback value like `const { foo = 'bar } = ..` - * @property {(expression: ESTree.Expression) => ESTree.Identifier | ESTree.MemberExpression | ESTree.CallExpression | ESTree.AwaitExpression} expression Returns an expression which walks the path starting at the given expression. - * This will be a call expression if a rest element or default is involved — e.g. `const { foo: { bar: baz = 42 }, ...rest } = quux` — since we can't represent `baz` or `rest` purely as a path - * Will be an await expression in case of an async default value (`const { foo = await bar } = ...`) - * @property {(expression: ESTree.Expression) => ESTree.Identifier | ESTree.MemberExpression | ESTree.CallExpression | ESTree.AwaitExpression} update_expression Like `expression` but without default values. - */ - -/** - * Extracts all destructured assignments from a pattern. - * @param {ESTree.Node} param - * @returns {DestructuredAssignment[]} - */ -export function extract_paths(param) { - return _extract_paths( - [], - param, - (node) => /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node), - (node) => /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node), - false - ); -} - -/** - * @param {DestructuredAssignment[]} assignments - * @param {ESTree.Node} param - * @param {DestructuredAssignment['expression']} expression - * @param {DestructuredAssignment['update_expression']} update_expression - * @param {boolean} has_default_value - * @returns {DestructuredAssignment[]} - */ -function _extract_paths(assignments = [], param, expression, update_expression, has_default_value) { - switch (param.type) { - case 'Identifier': - case 'MemberExpression': - assignments.push({ - node: param, - is_rest: false, - has_default_value, - expression, - update_expression - }); - break; - - case 'ObjectPattern': - for (const prop of param.properties) { - if (prop.type === 'RestElement') { - /** @type {DestructuredAssignment['expression']} */ - const rest_expression = (object) => { - /** @type {ESTree.Expression[]} */ - const props = []; - - for (const p of param.properties) { - if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') { - if (p.key.type === 'Identifier' && !p.computed) { - props.push(b.literal(p.key.name)); - } else if (p.key.type === 'Literal') { - props.push(b.literal(String(p.key.value))); - } else { - props.push(b.call('String', p.key)); - } - } - } - - return b.call('$.exclude_from_object', expression(object), b.array(props)); - }; - - if (prop.argument.type === 'Identifier') { - assignments.push({ - node: prop.argument, - is_rest: true, - has_default_value, - expression: rest_expression, - update_expression: rest_expression - }); - } else { - _extract_paths( - assignments, - prop.argument, - rest_expression, - rest_expression, - has_default_value - ); - } - } else { - /** @type {DestructuredAssignment['expression']} */ - const object_expression = (object) => - b.member(expression(object), prop.key, prop.computed || prop.key.type !== 'Identifier'); - _extract_paths( - assignments, - prop.value, - object_expression, - object_expression, - has_default_value - ); - } - } - - break; - - case 'ArrayPattern': - for (let i = 0; i < param.elements.length; i += 1) { - const element = param.elements[i]; - if (element) { - if (element.type === 'RestElement') { - /** @type {DestructuredAssignment['expression']} */ - const rest_expression = (object) => - b.call(b.member(expression(object), 'slice'), b.literal(i)); - if (element.argument.type === 'Identifier') { - assignments.push({ - node: element.argument, - is_rest: true, - has_default_value, - expression: rest_expression, - update_expression: rest_expression - }); - } else { - _extract_paths( - assignments, - element.argument, - rest_expression, - rest_expression, - has_default_value - ); - } - } else { - /** @type {DestructuredAssignment['expression']} */ - const array_expression = (object) => b.member(expression(object), b.literal(i), true); - _extract_paths( - assignments, - element, - array_expression, - array_expression, - has_default_value - ); - } - } - } - - break; - - case 'AssignmentPattern': { - /** @type {DestructuredAssignment['expression']} */ - const fallback_expression = (object) => build_fallback(expression(object), param.right); - - if (param.left.type === 'Identifier') { - assignments.push({ - node: param.left, - is_rest: false, - has_default_value: true, - expression: fallback_expression, - update_expression - }); - } else { - _extract_paths(assignments, param.left, fallback_expression, update_expression, true); - } - - break; - } - } - - return assignments; -} - -/** - * Represents the path of a destructured assignment from either a declaration - * or assignment expression. For example, given `const { foo: { bar: baz } } = quux`, - * the path of `baz` is `foo.bar` - * @typedef {Object} DestructuredAssignment2 - * @property {ESTree.Identifier | ESTree.MemberExpression} node The node the destructuring path end in. Can be a member expression only for assignment expressions - * @property {boolean} is_rest `true` if this is a `...rest` destructuring - * @property {boolean} has_default_value `true` if this has a fallback value like `const { foo = 'bar } = ..` * @property {ESTree.Expression} expression Returns an expression which walks the path starting at the given expression. * This will be a call expression if a rest element or default is involved — e.g. `const { foo: { bar: baz = 42 }, ...rest } = quux` — since we can't represent `baz` or `rest` purely as a path * Will be an await expression in case of an async default value (`const { foo = await bar } = ...`) @@ -407,21 +237,21 @@ function _extract_paths(assignments = [], param, expression, update_expression, * Extracts all destructured assignments from a pattern. * @param {ESTree.Node} param * @param {ESTree.Expression} initial - * @returns {DestructuredAssignment2[]} + * @returns {DestructuredAssignment[]} */ -export function destructure(param, initial) { - return _destructure([], param, initial, initial, false); +export function extract_paths(param, initial) { + return _extract_paths([], param, initial, initial, false); } /** - * @param {DestructuredAssignment2[]} assignments + * @param {DestructuredAssignment[]} assignments * @param {ESTree.Node} param * @param {ESTree.Expression} expression * @param {ESTree.Expression} update_expression * @param {boolean} has_default_value - * @returns {DestructuredAssignment2[]} + * @returns {DestructuredAssignment[]} */ -function _destructure(assignments = [], param, expression, update_expression, has_default_value) { +function _extract_paths(assignments = [], param, expression, update_expression, has_default_value) { switch (param.type) { case 'Identifier': case 'MemberExpression': @@ -463,7 +293,7 @@ function _destructure(assignments = [], param, expression, update_expression, ha update_expression: rest_expression }); } else { - _destructure( + _extract_paths( assignments, prop.argument, rest_expression, @@ -478,7 +308,7 @@ function _destructure(assignments = [], param, expression, update_expression, ha prop.computed || prop.key.type !== 'Identifier' ); - _destructure( + _extract_paths( assignments, prop.value, object_expression, @@ -506,7 +336,7 @@ function _destructure(assignments = [], param, expression, update_expression, ha update_expression: rest_expression }); } else { - _destructure( + _extract_paths( assignments, element.argument, rest_expression, @@ -517,7 +347,7 @@ function _destructure(assignments = [], param, expression, update_expression, ha } else { const array_expression = b.member(expression, b.literal(i), true); - _destructure( + _extract_paths( assignments, element, array_expression, @@ -542,7 +372,7 @@ function _destructure(assignments = [], param, expression, update_expression, ha update_expression }); } else { - _destructure(assignments, param.left, fallback_expression, update_expression, true); + _extract_paths(assignments, param.left, fallback_expression, update_expression, true); } break;