diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 2d5a4dcd9e..753a07bbb0 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -603,15 +603,15 @@ 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 = state.scope.generate('tmp'); - // const paths = extract_paths(declarator.id); + // const tmp = b.id(state.scope.generate('tmp')); + // const paths = destructure(declarator.id, tmp); // state.props_pre.push( - // b.declaration('const', b.id(tmp), visit(declarator.init!) as Expression) + // b.declaration('const', tmp, visit(declarator.init!) as Expression) // ); // for (const path of paths) { // const name = (path.node as Identifier).name; // const binding = state.scope.get(name)!; - // const value = path.expression!(b.id(tmp)); + // const value = path.expression; // if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') { // state.props.push({ // local: 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 f3c8be8cbb..4ab2f4f8c4 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 { extract_paths, object } from '../../../../utils/ast.js'; +import { destructure, 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,13 +234,11 @@ 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 extract_paths(node.context)) { + for (const path of destructure(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 - const fn = b.thunk( - /** @type {Expression} */ (context.visit(path.expression(unwrapped), child_state)) - ); + const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression, child_state))); declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); @@ -249,7 +247,7 @@ export function EachBlock(node, context) { child_state.transform[name] = { read, assign: (_, value) => { - const left = /** @type {Pattern} */ (path.update_expression(unwrapped)); + const left = /** @type {Pattern} */ (path.update_expression); return b.sequence([b.assignment('=', left, value), ...sequence]); }, mutate: (_, mutation) => { 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 4dd69c81f7..32b8c07fae 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 { extract_paths } from '../../../../utils/ast.js'; +import { destructure } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { get_value } from './shared/declarations.js'; @@ -43,14 +43,12 @@ export function SnippetBlock(node, context) { let arg_alias = `$$arg${i}`; args.push(b.id(arg_alias)); - const paths = extract_paths(argument); + const paths = destructure(argument, b.maybe_call(b.id(arg_alias))); for (const path of paths) { const name = /** @type {Identifier} */ (path.node).name; const needs_derived = path.has_default_value; // to ensure that default value is only called once - const fn = b.thunk( - /** @type {Expression} */ (context.visit(path.expression(b.maybe_call(b.id(arg_alias))))) - ); + const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression))); declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); 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 f2830528d0..5287194925 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 { extract_paths } from '../../../../utils/ast.js'; +import { destructure, 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,11 +142,11 @@ export function VariableDeclaration(node, context) { ); } else { const tmp = b.id(context.state.scope.generate('tmp')); - const paths = extract_paths(declarator.id); + const paths = destructure(declarator.id, tmp); declarations.push( b.declarator(tmp, value), ...paths.map((path) => { - const value = path.expression(tmp); + const value = path.expression; const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name); return b.declarator( path.node, @@ -224,12 +224,12 @@ export function VariableDeclaration(node, context) { if (declarator.id.type !== 'Identifier') { // 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 = context.state.scope.generate('tmp'); - const paths = extract_paths(declarator.id); + const tmp = b.id(context.state.scope.generate('tmp')); + const paths = destructure(declarator.id, tmp); declarations.push( b.declarator( - b.id(tmp), + tmp, /** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init))) ) ); @@ -237,7 +237,7 @@ export function VariableDeclaration(node, context) { for (const path of paths) { const name = /** @type {Identifier} */ (path.node).name; const binding = /** @type {Binding} */ (context.state.scope.get(name)); - const value = path.expression(b.id(tmp)); + const value = path.expression; declarations.push( b.declarator( path.node, @@ -304,12 +304,12 @@ function create_state_declarators(declarator, { scope, analysis }, value) { ]; } - const tmp = scope.generate('tmp'); - const paths = extract_paths(declarator.id); + const tmp = b.id(scope.generate('tmp')); + const paths = destructure(declarator.id, tmp); return [ - b.declarator(b.id(tmp), value), + b.declarator(tmp, value), ...paths.map((path) => { - const value = path.expression(b.id(tmp)); + const value = path.expression; const binding = scope.get(/** @type {Identifier} */ (path.node).name); return b.declarator( path.node, 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 8110576d13..0eca518db6 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, extract_paths } from '../../../../utils/ast.js'; +import { build_fallback, destructure } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { get_rune } from '../../../scope.js'; import { walk } from 'zimmerframe'; @@ -120,16 +120,16 @@ export function VariableDeclaration(node, context) { if (declarator.id.type !== 'Identifier') { // 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 = context.state.scope.generate('tmp'); - const paths = extract_paths(declarator.id); + const tmp = b.id(context.state.scope.generate('tmp')); + const paths = destructure(declarator.id, tmp); declarations.push( b.declarator( - b.id(tmp), + tmp, /** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init))) ) ); for (const path of paths) { - const value = path.expression(b.id(tmp)); + const value = path.expression; const name = /** @type {Identifier} */ (path.node).name; const binding = /** @type {Binding} */ (context.state.scope.get(name)); const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true); @@ -189,11 +189,11 @@ function create_state_declarators(declarator, scope, value) { } const tmp = b.id(scope.generate('tmp')); - const paths = extract_paths(declarator.id); + const paths = destructure(declarator.id, tmp); return [ b.declarator(tmp, value), // TODO inject declarator for opts, so we can use it below ...paths.map((path) => { - const value = path.expression(tmp); + const value = path.expression; return b.declarator(path.node, value); }) ]; 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 3e6bb0c4c6..ff257d70ff 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 { extract_paths, is_expression_async } from '../../../utils/ast.js'; +import { destructure, is_expression_async } from '../../../utils/ast.js'; import * as b from '#compiler/builders'; /** @@ -23,8 +23,8 @@ export function visit_assignment_expression(node, context, build_assignment) { let changed = false; - const assignments = extract_paths(node.left).map((path) => { - const value = path.expression?.(rhs); + const assignments = destructure(node.left, rhs).map((path) => { + const value = path.expression; let assignment = build_assignment('=', path.node, value, context); if (assignment !== null) changed = true; diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 23a95a1026..16608be932 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -389,6 +389,169 @@ function _extract_paths(assignments = [], param, expression, update_expression, 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 } = ...`) + * @property {ESTree.Expression} update_expression Like `expression` but without default values. + */ + +/** + * Extracts all destructured assignments from a pattern. + * @param {ESTree.Node} param + * @param {ESTree.Expression} initial + * @returns {DestructuredAssignment2[]} + */ +export function destructure(param, initial) { + return _destructure([], param, initial, initial, false); +} + +/** + * @param {DestructuredAssignment2[]} assignments + * @param {ESTree.Node} param + * @param {ESTree.Expression} expression + * @param {ESTree.Expression} update_expression + * @param {boolean} has_default_value + * @returns {DestructuredAssignment2[]} + */ +function _destructure(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 {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)); + } + } + } + + const rest_expression = b.call('$.exclude_from_object', expression, 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 { + _destructure( + assignments, + prop.argument, + rest_expression, + rest_expression, + has_default_value + ); + } + } else { + const object_expression = b.member( + expression, + prop.key, + prop.computed || prop.key.type !== 'Identifier' + ); + + _destructure( + 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') { + const rest_expression = b.call(b.member(expression, '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 { + _destructure( + assignments, + element.argument, + rest_expression, + rest_expression, + has_default_value + ); + } + } else { + const array_expression = b.member(expression, b.literal(i), true); + + _destructure( + assignments, + element, + array_expression, + array_expression, + has_default_value + ); + } + } + } + + break; + + case 'AssignmentPattern': { + const fallback_expression = build_fallback(expression, 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 { + _destructure(assignments, param.left, fallback_expression, update_expression, true); + } + + break; + } + } + + return assignments; +} + /** * Like `path.at(x)`, but skips over `TSNonNullExpression` and `TSAsExpression` nodes and eases assertions a bit * by removing the `| undefined` from the resulting type.