Skip to content

feat: Variadic snippets #9987

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5 changes: 5 additions & 0 deletions .changeset/big-ears-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: Support variadic Snippet types
2 changes: 1 addition & 1 deletion packages/svelte/elements.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export type MessageEventHandler<T extends EventTarget> = EventHandler<MessageEve
export interface DOMAttributes<T extends EventTarget> {
// Implicit children prop every element has
// Add this here so that libraries doing `$props<HTMLButtonAttributes>()` don't need a separate interface
children?: import('svelte').Snippet<void>;
children?: import('svelte').Snippet;

// Clipboard Events
'on:copy'?: ClipboardEventHandler<T> | undefined | null;
Expand Down
18 changes: 11 additions & 7 deletions packages/svelte/src/compiler/phases/1-parse/state/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -294,7 +299,10 @@ function open(parser) {
end: name_end,
name
},
context,
context: {
type: 'ArrayPattern',
elements
},
body: create_fragment()
})
);
Expand Down Expand Up @@ -589,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);

Expand All @@ -602,7 +606,7 @@ function special(parser) {
start,
end: parser.index,
expression: expression.callee,
argument: expression.arguments[0] ?? null
arguments: expression.arguments
})
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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}`)))
)
)
)
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1127,21 +1127,19 @@ const template_visitors = {
const snippet_function = state.options.dev
? b.call('$.validate_snippet', node.expression)
: node.expression;
if (node.argument) {
state.template.push(
t_statement(
b.stmt(
b.call(
snippet_function,
b.id('$$payload'),
/** @type {import('estree').Expression} */ (context.visit(node.argument))
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));
},
Expand Down
6 changes: 2 additions & 4 deletions packages/svelte/src/compiler/phases/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
8 changes: 5 additions & 3 deletions packages/svelte/src/compiler/types/template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Binding } from '#compiler';
import type {
ArrayExpression,
ArrowFunctionExpression,
ArrayPattern,
VariableDeclaration,
VariableDeclarator,
Expression,
Expand All @@ -12,7 +13,8 @@ import type {
Node,
ObjectExpression,
Pattern,
Program
Program,
SpreadElement
} from 'estree';

export interface BaseNode {
Expand Down Expand Up @@ -146,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;
Expand Down Expand Up @@ -413,7 +415,7 @@ export interface KeyBlock extends BaseNode {
export interface SnippetBlock extends BaseNode {
type: 'SnippetBlock';
expression: Identifier;
context: null | Pattern;
context: ArrayPattern;
body: Fragment;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions packages/svelte/src/main/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,11 @@ declare const SnippetReturn: unique symbol;
* ```
* You can only call a snippet through the `{@render ...}` tag.
*/
export interface Snippet<T = void> {
(arg: T): typeof SnippetReturn & {
export interface Snippet<T extends unknown[] = []> {
(
this: void,
...args: T
): typeof SnippetReturn & {
_: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"';
};
}
Expand Down
6 changes: 3 additions & 3 deletions packages/svelte/tests/types/snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ const d: Snippet<boolean> = (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<boolean> = (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;
};
Expand Down