From ff2e84031e9334fd46178f1ad3515a5a83d25528 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 22 Dec 2023 15:18:14 -0500 Subject: [PATCH 01/10] feat: Variadic snippets --- packages/svelte/elements.d.ts | 2 +- packages/svelte/src/main/public.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 09df6ddbc8ef..41b0f07bf8ac 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -67,7 +67,7 @@ export type MessageEventHandler = EventHandler { // Implicit children prop every element has // Add this here so that libraries doing `$props()` don't need a separate interface - children?: import('svelte').Snippet; + children?: import('svelte').Snippet; // Clipboard Events 'on:copy'?: ClipboardEventHandler | undefined | null; diff --git a/packages/svelte/src/main/public.d.ts b/packages/svelte/src/main/public.d.ts index 5849835ea285..baae7d4d1281 100644 --- a/packages/svelte/src/main/public.d.ts +++ b/packages/svelte/src/main/public.d.ts @@ -195,8 +195,8 @@ declare const SnippetReturn: unique symbol; * ``` * You can only call a snippet through the `{@render ...}` tag. */ -export interface Snippet { - (arg: T): typeof SnippetReturn & { +export interface Snippet { + (...args: T): typeof SnippetReturn & { _: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"'; }; } From e76a80982ce5bab143b69d0025bb7972a5ac1308 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 22 Dec 2023 15:20:02 -0500 Subject: [PATCH 02/10] changeset --- .changeset/big-ears-do.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/big-ears-do.md diff --git a/.changeset/big-ears-do.md b/.changeset/big-ears-do.md new file mode 100644 index 000000000000..d98ed776209a --- /dev/null +++ b/.changeset/big-ears-do.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: Support variadic Snippet types From 08f9be28d7abb05238dfde19562d4643bfa306bc Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 22 Dec 2023 15:34:17 -0500 Subject: [PATCH 03/10] fix: this type --- packages/svelte/src/main/public.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/main/public.d.ts b/packages/svelte/src/main/public.d.ts index baae7d4d1281..f916a158d677 100644 --- a/packages/svelte/src/main/public.d.ts +++ b/packages/svelte/src/main/public.d.ts @@ -196,7 +196,7 @@ declare const SnippetReturn: unique symbol; * You can only call a snippet through the `{@render ...}` tag. */ export interface Snippet { - (...args: T): typeof SnippetReturn & { + (this: void, ...args: T): typeof SnippetReturn & { _: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"'; }; } From ec0f3d6a735e66a01959127f60faf72dbd729a20 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 22 Dec 2023 16:22:41 -0500 Subject: [PATCH 04/10] feat: Is this all that's required? --- .../svelte/src/compiler/phases/1-parse/state/tag.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index ebfebb73b187..40681bee1c1f 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -274,7 +274,12 @@ function open(parser) { parser.allow_whitespace(); - const context = parser.match(')') ? null : read_context(parser); + const elements = []; + while (!parser.match(')')) { + elements.push(read_context(parser)); + parser.eat(','); + parser.allow_whitespace(); + } parser.allow_whitespace(); parser.eat(')', true); @@ -294,7 +299,10 @@ function open(parser) { end: name_end, name }, - context, + context: { + type: 'ArrayPattern', + elements + }, body: create_fragment() }) ); From d0c61a8596e6dd540cf940edcaa3cb1e8f2d9552 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 22 Dec 2023 16:36:32 -0500 Subject: [PATCH 05/10] fix: lint --- packages/svelte/src/main/public.d.ts | 5 ++++- packages/svelte/tests/types/snippet.ts | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/main/public.d.ts b/packages/svelte/src/main/public.d.ts index f916a158d677..32675e55405c 100644 --- a/packages/svelte/src/main/public.d.ts +++ b/packages/svelte/src/main/public.d.ts @@ -196,7 +196,10 @@ declare const SnippetReturn: unique symbol; * You can only call a snippet through the `{@render ...}` tag. */ export interface Snippet { - (this: void, ...args: T): typeof SnippetReturn & { + ( + this: void, + ...args: T + ): typeof SnippetReturn & { _: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"'; }; } diff --git a/packages/svelte/tests/types/snippet.ts b/packages/svelte/tests/types/snippet.ts index 5a1e46c24149..edc5aba12378 100644 --- a/packages/svelte/tests/types/snippet.ts +++ b/packages/svelte/tests/types/snippet.ts @@ -20,18 +20,18 @@ const d: Snippet = (a: string, b: number) => { const e: Snippet = (a: string) => { return return_type; }; +// @ts-expect-error const f: Snippet = (a) => { - // @ts-expect-error a?.x; return return_type; }; -const g: Snippet = (a) => { +const g: Snippet<[boolean]> = (a) => { // @ts-expect-error a === ''; a === true; return return_type; }; -const h: Snippet<{ a: true }> = (a) => { +const h: Snippet<[{ a: true }]> = (a) => { a.a === true; return return_type; }; From f10c6ffd9a06a70dd00bc7435da35628c641385f Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 22 Dec 2023 21:30:48 -0500 Subject: [PATCH 06/10] maybe --- .../3-transform/client/visitors/template.js | 30 +++++++++---------- .../svelte/src/compiler/types/template.d.ts | 3 +- 2 files changed, 16 insertions(+), 17 deletions(-) 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 6ad1026cc744..4693ba9630fa 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 @@ -2445,21 +2445,21 @@ export const template_visitors = { /** @type {import('estree').BlockStatement} */ let body; - if (node.context) { - const id = node.context.type === 'Identifier' ? node.context : b.id('$$context'); - args.push(id); + /** @type {import('estree').Statement[]} */ + const declarations = []; - /** @type {import('estree').Statement[]} */ - const declarations = []; + node.context.elements.forEach((element, i) => { + if (!element) return; + const id = element.type === 'Identifier' ? element : b.id(`$$context${i}`); + args.push(id); - // some of this is duplicated with EachBlock — TODO dedupe? - if (node.context.type === 'Identifier') { + if (element.type === 'Identifier') { const binding = /** @type {import('#compiler').Binding} */ ( context.state.scope.get(id.name) ); binding.expression = b.call(id); } else { - const paths = extract_paths(node.context); + const paths = extract_paths(element); for (const path of paths) { const name = /** @type {import('estree').Identifier} */ (path.node).name; @@ -2471,7 +2471,7 @@ export const template_visitors = { path.node, b.thunk( /** @type {import('estree').Expression} */ ( - context.visit(path.expression?.(b.call('$$context'))) + context.visit(path.expression?.(b.call(`$$context${i}`))) ) ) ) @@ -2486,14 +2486,12 @@ export const template_visitors = { binding.expression = b.call(name); } } + }); - body = b.block([ - ...declarations, - .../** @type {import('estree').BlockStatement} */ (context.visit(node.body)).body - ]); - } else { - body = /** @type {import('estree').BlockStatement} */ (context.visit(node.body)); - } + body = b.block([ + ...declarations, + .../** @type {import('estree').BlockStatement} */ (context.visit(node.body)).body + ]); const path = context.path; // If we're top-level, then we can create a function for the snippet so that it can be referenced diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 4646dc6d32b2..502f9c7d5f0b 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -2,6 +2,7 @@ import type { Binding } from '#compiler'; import type { ArrayExpression, ArrowFunctionExpression, + ArrayPattern, VariableDeclaration, VariableDeclarator, Expression, @@ -413,7 +414,7 @@ export interface KeyBlock extends BaseNode { export interface SnippetBlock extends BaseNode { type: 'SnippetBlock'; expression: Identifier; - context: null | Pattern; + context: ArrayPattern; body: Fragment; } From 99c844a964f9e593e4b91d88f10abcf480dce7af Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 22 Dec 2023 21:44:09 -0500 Subject: [PATCH 07/10] work you slob --- packages/svelte/src/compiler/phases/1-parse/state/tag.js | 6 +----- .../compiler/phases/3-transform/client/visitors/template.js | 6 +++--- packages/svelte/src/compiler/types/template.d.ts | 5 +++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 40681bee1c1f..a378055b5109 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -597,10 +597,6 @@ function special(parser) { error(expression, 'TODO', 'expected an identifier followed by (...)'); } - if (expression.arguments.length > 1) { - error(expression.arguments[1], 'TODO', 'expected at most one argument'); - } - parser.allow_whitespace(); parser.eat('}', true); @@ -610,7 +606,7 @@ function special(parser) { start, end: parser.index, expression: expression.callee, - argument: expression.arguments[0] ?? null + arguments: expression.arguments }) ); } 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 4693ba9630fa..cace8d0fd056 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 @@ -1755,9 +1755,9 @@ export const template_visitors = { /** @type {import('estree').Expression[]} */ const args = [context.state.node]; - if (node.argument) { - args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.argument)))); - } + node.arguments.forEach((arg) => + args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg)))) + ); let snippet_function = /** @type {import('estree').Expression} */ ( context.visit(node.expression) diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 502f9c7d5f0b..0b60b1ae7af8 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -13,7 +13,8 @@ import type { Node, ObjectExpression, Pattern, - Program + Program, + SpreadElement } from 'estree'; export interface BaseNode { @@ -147,7 +148,7 @@ export interface DebugTag extends BaseNode { export interface RenderTag extends BaseNode { type: 'RenderTag'; expression: Identifier; - argument: null | Expression; + arguments: (Expression | SpreadElement)[]; } type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag; From 5d73c6b6c4aabbdd59bc709a9a061a91e59136f9 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 22 Dec 2023 22:02:54 -0500 Subject: [PATCH 08/10] resolve lint errors --- .../compiler/phases/3-transform/server/transform-server.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 79c62e396d47..b1aab0c3c630 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1127,14 +1127,16 @@ const template_visitors = { const snippet_function = state.options.dev ? b.call('$.validate_snippet', node.expression) : node.expression; - if (node.argument) { + if (node.arguments.length > 0) { state.template.push( t_statement( b.stmt( b.call( snippet_function, b.id('$$payload'), - /** @type {import('estree').Expression} */ (context.visit(node.argument)) + ...node.arguments.map( + (arg) => /** @type {import('estree').Expression} */ (context.visit(arg)) + ) ) ) ) From e8153dcbbccfe1200b8a95aa3e495c39f76e9015 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 22 Dec 2023 22:05:35 -0500 Subject: [PATCH 09/10] fix: simplify --- .../3-transform/server/transform-server.js | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index b1aab0c3c630..c8530a87ddb5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1127,23 +1127,19 @@ const template_visitors = { const snippet_function = state.options.dev ? b.call('$.validate_snippet', node.expression) : node.expression; - if (node.arguments.length > 0) { - state.template.push( - t_statement( - b.stmt( - b.call( - snippet_function, - b.id('$$payload'), - ...node.arguments.map( - (arg) => /** @type {import('estree').Expression} */ (context.visit(arg)) - ) + state.template.push( + t_statement( + b.stmt( + b.call( + snippet_function, + b.id('$$payload'), + ...node.arguments.map( + (arg) => /** @type {import('estree').Expression} */ (context.visit(arg)) ) ) ) - ); - } else { - state.template.push(t_statement(b.stmt(b.call(snippet_function, b.id('$$payload'))))); - } + ) + ); state.template.push(t_expression(anchor_id)); }, From 9b0199946b256775c3dd779665695e825bc83aa7 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 22 Dec 2023 22:29:27 -0500 Subject: [PATCH 10/10] idk man --- packages/svelte/src/compiler/phases/scope.js | 6 ++---- packages/svelte/src/internal/client/render.js | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 9bbeb3dd69e8..adcd08278446 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -592,10 +592,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const child_scope = state.scope.child(); scopes.set(node, child_scope); - if (node.context) { - for (const id of extract_identifiers(node.context)) { - child_scope.declare(id, 'each', 'let'); - } + for (const id of extract_identifiers(node.context)) { + child_scope.declare(id, 'each', 'let'); } context.next({ scope: child_scope }); diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index a963c832c381..4aeb067f985d 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -2843,16 +2843,16 @@ export function sanitize_slots(props) { /** * @param {() => Function} get_snippet * @param {Node} node - * @param {() => any} args + * @param {(() => any)[]} args * @returns {void} */ -export function snippet_effect(get_snippet, node, args) { +export function snippet_effect(get_snippet, node, ...args) { const block = create_snippet_block(); render_effect(() => { // Only rerender when the snippet function itself changes, // not when an eagerly-read prop inside the snippet function changes const snippet = get_snippet(); - untrack(() => snippet(node, args)); + untrack(() => snippet(node, ...args)); return () => { if (block.d !== null) { remove(block.d);