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 5f0c329c75..65777dbee9 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 @@ -234,7 +234,22 @@ export function EachBlock(node, context) { } else if (node.context) { const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item; - const { paths } = extract_paths(node.context, unwrapped); + const { inserts, paths } = extract_paths(node.context, unwrapped); + + for (const { id, value } of inserts) { + id.name = context.state.scope.generate('$$array'); + child_state.transform[id.name] = { read: get_value }; + + declarations.push( + b.var( + id, + b.call( + '$.derived', + /** @type {Expression} */ (context.visit(b.thunk(value), child_state)) + ) + ) + ); + } for (const path of paths) { const name = /** @type {Identifier} */ (path.node).name; 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 0dd46fafc6..0809aa21b2 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 @@ -43,7 +43,16 @@ export function SnippetBlock(node, context) { let arg_alias = `$$arg${i}`; args.push(b.id(arg_alias)); - const { paths } = extract_paths(argument, b.maybe_call(b.id(arg_alias))); + const { inserts, paths } = extract_paths(argument, b.maybe_call(b.id(arg_alias))); + + for (const { id, value } of inserts) { + id.name = context.state.scope.generate('$$array'); + transform[id.name] = { read: get_value }; + + declarations.push( + b.var(id, b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value))))) + ); + } 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 2636c59621..f43e1d5999 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 @@ -8,6 +8,7 @@ import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; import { is_hoisted_function } from '../../utils.js'; +import { get_value } from './shared/declarations.js'; /** * @param {VariableDeclaration} node @@ -116,7 +117,7 @@ export function VariableDeclaration(node, context) { } const args = /** @type {CallExpression} */ (init).arguments; - const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; + const value = /** @type {Expression} */ (args[0]) ?? b.void0; // TODO do we need the void 0? can we just omit it altogether? if (rune === '$state' || rune === '$state.raw') { /** @@ -138,15 +139,31 @@ export function VariableDeclaration(node, context) { if (declarator.id.type === 'Identifier') { declarations.push( - b.declarator(declarator.id, create_state_declarator(declarator.id, value)) + b.declarator( + declarator.id, + create_state_declarator( + declarator.id, + /** @type {Expression} */ (context.visit(value)) + ) + ) ); } else { const tmp = b.id(context.state.scope.generate('tmp')); - const { paths } = extract_paths(declarator.id, tmp); + const { inserts, paths } = extract_paths(declarator.id, tmp); + declarations.push( b.declarator(tmp, value), + ...inserts.map(({ id, value }) => { + id.name = context.state.scope.generate('$$array'); + context.state.transform[id.name] = { read: get_value }; + + return b.declarator( + id, + b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value)))) + ); + }), ...paths.map((path) => { - const value = path.expression; + const value = /** @type {Expression} */ (context.visit(path.expression)); const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name); return b.declarator( path.node, @@ -163,12 +180,10 @@ export function VariableDeclaration(node, context) { if (rune === '$derived' || rune === '$derived.by') { if (declarator.id.type === 'Identifier') { - declarations.push( - b.declarator( - declarator.id, - b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)) - ) - ); + let expression = /** @type {Expression} */ (context.visit(value)); + if (rune === '$derived') expression = b.thunk(expression); + + declarations.push(b.declarator(declarator.id, b.call('$.derived', expression))); } else { const init = /** @type {CallExpression} */ (declarator.init); @@ -178,24 +193,32 @@ export function VariableDeclaration(node, context) { const id = b.id(context.state.scope.generate('$$d')); rhs = b.call('$.get', id); - declarations.push( - b.declarator(id, b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))) - ); + let expression = /** @type {Expression} */ (context.visit(value)); + if (rune === '$derived') expression = b.thunk(expression); + + declarations.push(b.declarator(id, b.call('$.derived', expression))); } const { inserts, paths } = extract_paths(declarator.id, rhs); - for (const insert of inserts) { - insert.id.name = context.state.scope.generate('$$array'); - declarations.push(b.declarator(insert.id, insert.value)); - } + for (const { id, value } of inserts) { + id.name = context.state.scope.generate('$$array'); + context.state.transform[id.name] = { read: get_value }; - for (const path of paths) { declarations.push( - b.declarator(path.node, b.call('$.derived', b.thunk(path.expression))) + b.declarator( + id, + b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value)))) + ) ); } + + for (const path of paths) { + const expression = /** @type {Expression} */ (context.visit(path.expression)); + declarations.push(b.declarator(path.node, b.call('$.derived', b.thunk(expression)))); + } } + continue; } } @@ -225,7 +248,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 } = extract_paths(declarator.id, tmp); + const { inserts, paths } = extract_paths(declarator.id, tmp); declarations.push( b.declarator( @@ -234,10 +257,23 @@ export function VariableDeclaration(node, context) { ) ); + for (const { id, value } of inserts) { + id.name = context.state.scope.generate('$$array'); + context.state.transform[id.name] = { read: get_value }; + + declarations.push( + b.declarator( + id, + b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value)))) + ) + ); + } + 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; + const value = /** @type {Expression} */ (context.visit(path.expression)); + declarations.push( b.declarator( path.node, @@ -271,7 +307,7 @@ export function VariableDeclaration(node, context) { declarations.push( ...create_state_declarators( declarator, - context.state, + context, /** @type {Expression} */ (declarator.init && context.visit(declarator.init)) ) ); @@ -291,30 +327,41 @@ export function VariableDeclaration(node, context) { /** * Creates the output for a state declaration in legacy mode. * @param {VariableDeclarator} declarator - * @param {ComponentClientTransformState} scope + * @param {ComponentContext} context * @param {Expression} value */ -function create_state_declarators(declarator, { scope, analysis }, value) { +function create_state_declarators(declarator, context, value) { if (declarator.id.type === 'Identifier') { return [ b.declarator( declarator.id, - b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined) + b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined) ) ]; } - const tmp = b.id(scope.generate('tmp')); - const { paths } = extract_paths(declarator.id, tmp); + const tmp = b.id(context.state.scope.generate('tmp')); + const { inserts, paths } = extract_paths(declarator.id, tmp); + return [ b.declarator(tmp, value), + ...inserts.map(({ id, value }) => { + id.name = context.state.scope.generate('$$array'); + context.state.transform[id.name] = { read: get_value }; + + return b.declarator( + id, + b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value)))) + ); + }), ...paths.map((path) => { - const value = path.expression; - const binding = scope.get(/** @type {Identifier} */ (path.node).name); + const value = /** @type {Expression} */ (context.visit(path.expression)); + const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name); + return b.declarator( path.node, binding?.kind === 'state' - ? b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined) + ? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined) : value ); }) 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 0e3ee40af3..1f0e6be77c 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 @@ -121,13 +121,20 @@ 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 } = extract_paths(declarator.id, tmp); + const { inserts, paths } = extract_paths(declarator.id, tmp); + declarations.push( b.declarator( tmp, /** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init))) ) ); + + for (const { id, value } of inserts) { + id.name = context.state.scope.generate('$$array'); + declarations.push(b.declarator(id, value)); + } + for (const path of paths) { const value = path.expression; const name = /** @type {Identifier} */ (path.node).name; @@ -135,6 +142,7 @@ export function VariableDeclaration(node, context) { const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true); declarations.push(b.declarator(path.node, build_fallback(prop, value))); } + continue; } 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 228ea37010..175b44f4fe 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js @@ -1,8 +1,9 @@ -/** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern } from 'estree' */ +/** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern, Statement } 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 * as b from '#compiler/builders'; +import { get_value } from '../client/visitors/shared/declarations.js'; /** * @template {ClientContext | ServerContext} Context @@ -23,7 +24,11 @@ export function visit_assignment_expression(node, context, build_assignment) { let changed = false; - const { paths } = extract_paths(node.left, rhs); + const { inserts, paths } = extract_paths(node.left, rhs); + + for (const { id } of inserts) { + id.name = context.state.scope.generate('$$array'); + } const assignments = paths.map((path) => { const value = path.expression; @@ -47,16 +52,20 @@ export function visit_assignment_expression(node, context, build_assignment) { } const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement'); - const sequence = b.sequence(assignments); - if (!is_standalone) { - // this is part of an expression, we need the sequence to end with the value - sequence.expressions.push(rhs); - } + if (inserts.length > 0 || should_cache) { + /** @type {Statement[]} */ + const statements = [ + ...inserts.map(({ id, value }) => b.var(id, value)), + ...assignments.map(b.stmt) + ]; + + if (!is_standalone) { + // this is part of an expression, we need the sequence to end with the value + statements.push(b.return(rhs)); + } - if (should_cache) { - // the right hand side is a complex expression, wrap in an IIFE to cache it - const iife = b.arrow([rhs], sequence); + const iife = b.arrow([rhs], b.block(statements)); const iife_is_async = is_expression_async(value) || @@ -65,6 +74,13 @@ export function visit_assignment_expression(node, context, build_assignment) { return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value); } + const sequence = b.sequence(assignments); + + if (!is_standalone) { + // this is part of an expression, we need the sequence to end with the value + sequence.expressions.push(rhs); + } + return sequence; } diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 35fde34860..d8f2ec0f8c 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -341,22 +341,19 @@ function _extract_paths(paths, inserts, param, expression, update_expression, ha // the consumer is responsible for setting the name of the identifier const id = b.id('#'); - const is_rest = param.elements.at(-1)?.type === 'RestElement'; - const call = b.call( + const value = b.call( '$.to_array', expression, - is_rest ? undefined : b.literal(param.elements.length) + param.elements.at(-1)?.type === 'RestElement' ? undefined : b.literal(param.elements.length) ); - inserts.push({ id, value: b.call('$.derived', b.thunk(call)) }); - - const array = b.call('$.get', id); + inserts.push({ id, value }); 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(array, 'slice'), b.literal(i)); + const rest_expression = b.call(b.member(id, 'slice'), b.literal(i)); if (element.argument.type === 'Identifier') { paths.push({ @@ -377,7 +374,7 @@ function _extract_paths(paths, inserts, param, expression, update_expression, ha ); } } else { - const array_expression = b.member(array, b.literal(i), true); + const array_expression = b.member(id, b.literal(i), true); _extract_paths( paths, diff --git a/packages/svelte/src/internal/client/destructuring.js b/packages/svelte/src/internal/client/destructuring.js deleted file mode 100644 index 46719450b2..0000000000 --- a/packages/svelte/src/internal/client/destructuring.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * When encountering a situation like `let [a, b, c] = $derived(blah())`, - * we need to stash an intermediate value that `a`, `b`, and `c` derive - * from, in case it's an iterable - * @template T - * @param {ArrayLike | Iterable} value - * @param {number} [n] - * @returns {Array} - */ -export function to_array(value, n) { - // return arrays unchanged - if (Array.isArray(value)) { - return value; - } - - // if value is not iterable, or `n` is unspecified (indicates a rest - // element, which means we're not concerned about unbounded iterables) - // convert to an array with `Array.from` - if (n === undefined || !(Symbol.iterator in value)) { - return Array.from(value); - } - - // otherwise, populate an array with `n` values - - /** @type {T[]} */ - const array = []; - - for (const element of value) { - array.push(element); - if (array.length === n) break; - } - - return array; -} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 490834addd..62cb3e6513 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -1,7 +1,6 @@ export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; export { push, pop } from './context.js'; -export { to_array } from './destructuring.js'; export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; export { cleanup_styles } from './dev/css.js'; export { add_locations } from './dev/elements.js'; @@ -155,7 +154,7 @@ export { } from './dom/operations.js'; export { attr, clsx } from '../shared/attributes.js'; export { snapshot } from '../shared/clone.js'; -export { noop, fallback } from '../shared/utils.js'; +export { noop, fallback, to_array } from '../shared/utils.js'; export { invalid_default_snippet, validate_dynamic_element_tag, diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 29e09fe4dd..85c059e09b 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -504,7 +504,7 @@ export { assign_payload, copy_payload } from './payload.js'; export { snapshot } from '../shared/clone.js'; -export { fallback } from '../shared/utils.js'; +export { fallback, to_array } from '../shared/utils.js'; export { invalid_default_snippet, diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js index 5e7f3152d8..10f8597520 100644 --- a/packages/svelte/src/internal/shared/utils.js +++ b/packages/svelte/src/internal/shared/utils.js @@ -81,3 +81,38 @@ export function fallback(value, fallback, lazy = false) { : /** @type {V} */ (fallback) : value; } + +/** + * When encountering a situation like `let [a, b, c] = $derived(blah())`, + * we need to stash an intermediate value that `a`, `b`, and `c` derive + * from, in case it's an iterable + * @template T + * @param {ArrayLike | Iterable} value + * @param {number} [n] + * @returns {Array} + */ +export function to_array(value, n) { + // return arrays unchanged + if (Array.isArray(value)) { + return value; + } + + // if value is not iterable, or `n` is unspecified (indicates a rest + // element, which means we're not concerned about unbounded iterables) + // convert to an array with `Array.from` + if (n === undefined || !(Symbol.iterator in value)) { + return Array.from(value); + } + + // otherwise, populate an array with `n` values + + /** @type {T[]} */ + const array = []; + + for (const element of value) { + array.push(element); + if (array.length === n) break; + } + + return array; +} diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js index 47f297bce9..e9cf5b573d 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js @@ -7,10 +7,12 @@ let c = 3; let d = 4; export function update(array) { - ( - $.set(a, array[0], true), - $.set(b, array[1], true) - ); + ((array) => { + var $$array = $.to_array(array, 2); + + $.set(a, $$array[0], true); + $.set(b, $$array[1], true); + })(array); [c, d] = array; } \ No newline at end of file