diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index dcf53155a2..5f80a1d6ca 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1758,6 +1758,7 @@ export const template_visitors = { context.visit(b.spread(b.call('$.shallow_thunk', arg.argument))) ) ); + return; } args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg)))); }); @@ -2454,7 +2455,7 @@ export const template_visitors = { node.context.elements.forEach((argument, i) => { if (!argument) return; - const arg_alias = `$$arg${i}`; + let arg_alias = `$$arg${i}`; /** @type {import('estree').Identifier | undefined} */ let identifier; /** @type {import('estree').Identifier | import('estree').RestElement | string} */ @@ -2478,16 +2479,24 @@ export const template_visitors = { } if (argument.type === 'RestElement' && argument.argument.type === 'Identifier') { - // this is a "simple" rest argument (not destructured), so we just need to thunkify it - const binding = /** @type {import('#compiler').Binding} */ ( - context.state.scope.get(argument.argument.name) - ); - binding.expression = b.call(argument.argument.name); + // this is a "simple" rest argument (not destructured), so we just need to proxy it + // const binding = /** @type {import('#compiler').Binding} */ ( + // context.state.scope.get(argument.argument.name) + // ); + // binding.expression = b.call(argument.argument.name); // TODO: where does `context.visit` go here? does it matter? i don't understand what it's for :thinkies: - declarations.push(b.let(argument.argument.name, b.thunk(b.id(`$$arg${i}`)))); + declarations.push( + b.let(argument.argument.name, b.call('$.proxy_rest_array', b.id(arg_alias))) + ); return; } + if (argument.type === 'RestElement') { + const new_arg_alias = `$$proxied_arg${i}`; + declarations.push(b.let(new_arg_alias, b.call('$.proxy_rest_array', b.id(arg_alias)))); + arg_alias = new_arg_alias; + } + // If, on the other hand, it's a destructuring expression, which could be // a rest destructure (eg. `...[foo, bar, { baz }, ...rest]`, which, absurdly, is all valid syntax), // we need to follow the destructuring expression to figure out what variables are being extracted. @@ -2501,7 +2510,11 @@ export const template_visitors = { path.node, b.thunk( /** @type {import('estree').Expression} */ ( - context.visit(path.expression?.(b.call(`$$arg${i}`))) + context.visit( + path.expression?.( + argument.type === 'RestElement' ? b.id(arg_alias) : b.call(arg_alias) + ) + ) ) ) ) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6fb4cc927a..4fb93502c8 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1899,3 +1899,37 @@ if (DEV) { throw_rune_error('$inspect'); throw_rune_error('$props'); } + +/** + * @template {Iterable} T + * @param {T} iterable + * @returns {{ [P in keyof T]: () => T[P] }} + */ +export function shallow_thunk(iterable) { + const thunks = []; + for (const item of iterable) { + thunks.push(() => item); + } + // @ts-expect-error -- for obvious reasons + return thunks; +} + +/** + * @template {unknown[]} T + * @param {T} items + * @returns {T} + */ +export function proxy_rest_array(items) { + return new Proxy(items, { + get(target, property) { + // @ts-expect-error -- It thinks arrays can't have properties that aren't numeric + if (typeof property === 'symbol') return target[property]; + if (!isNaN(parseInt(property))) { + // @ts-expect-error -- It thinks arrays can't have properties that aren't numeric + return target[property](); + } + // @ts-expect-error -- It thinks arrays can't have properties that aren't numeric + return target[property]; + } + }); +} diff --git a/packages/svelte/src/internal/client/utils.js b/packages/svelte/src/internal/client/utils.js index 79e5b3d79d..627d2b34fb 100644 --- a/packages/svelte/src/internal/client/utils.js +++ b/packages/svelte/src/internal/client/utils.js @@ -16,18 +16,3 @@ export var get_descriptors = Object.getOwnPropertyDescriptors; export function is_function(thing) { return typeof thing === 'function'; } - -/** - * TODO: Do the types matter on this? If so, can we improve them so that - * `shallow_thunk(['one', 2])` returns a tuple type instead of a union? - * @template {unknown} T - * @param {Iterable} iterable - * @returns {(() => T)[]} - */ -function shallow_thunk(iterable) { - const thunks = []; - for (const item of iterable) { - thunks.push(() => item); - } - return thunks; -} diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 048105d0f9..56fd9031a1 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -36,7 +36,9 @@ export { effect_active, user_root_effect, inspect, - unwrap + unwrap, + proxy_rest_array, + shallow_thunk } from './client/runtime.js'; export * from './client/each.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-spread-args/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-spread-args/_config.js new file mode 100644 index 0000000000..82418110df --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-spread-args/_config.js @@ -0,0 +1,21 @@ +import { test } from '../../test'; + +export default test({ + html: ` +

clicks: 0, doubled: 0, tripled: 0, quadrupled: 0, something else: 0

+ + `, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual( + target.innerHTML, + ` +

clicks: 1, doubled: 2, tripled: 3, quadrupled: 4, something else: 5

+ + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-spread-args/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-spread-args/main.svelte new file mode 100644 index 0000000000..b2f2ae13e2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-spread-args/main.svelte @@ -0,0 +1,38 @@ + + +{#snippet foo(n: number, ...[doubled, { tripled }, ...rest]: number[])} +

clicks: {n.value}, doubled: {doubled.value}, tripled: {tripled.value}, quadrupled: {rest[0].value}, something else: {rest[1].value}

+{/snippet} + +{@render foo(...[count, doubled, {tripled}, quadrupled, whatever_comes_after_that])} + +