Skip to content

Commit d309a9d

Browse files
elliott-with-the-longest-name-on-githubdummdidummRich-Harris
authored
feat: Variadic snippets (#9988)
* give this another try * fix: lint * fix: Forgot to save * feat: it works boiiii * look, ok, it did work, i just needed to update the snapshots * bruh * changeset * feat: ok I think the client snippet block finally works * feat: current tests pass; I'm sure I'm missing stuff for new things * fix: snapshot * feat: I think non-destructured rest should work now? * chore: duplicated computation * feat: Tests (passing and failing * feat: it's... alive? * chore: Clean up my messes * chore: devtime stuff * fix: fmt * chore: see if this fixes repl * chore: make naming more offensive * fix: Don't throw on missing keys, return undefined as it usually would * Update packages/svelte/src/compiler/phases/1-parse/state/tag.js Co-authored-by: Simon H <[email protected]> * Update packages/svelte/src/compiler/phases/1-parse/state/tag.js Co-authored-by: Simon H <[email protected]> * fix: Hopefully default param values now work * dumb * types * feat: Test it * fix: Turns out javascript parameters are optional * feat: The Final Solution * document function * feat: Better bracket matching, unit tests * feat: exclude test files from publish * feat: More unit tests * feat: Use more efficient parsing for @const * Update .changeset/curvy-cups-cough.md Co-authored-by: Simon H <[email protected]> * Update packages/svelte/package.json Co-authored-by: Simon H <[email protected]> * Update packages/svelte/src/compiler/phases/1-parse/utils/bracket.js Co-authored-by: Simon H <[email protected]> * fix: changesets * chore: additional comments * fix: kill foreach * fix: foreach again * feat: Docs * Revert "fix: kill foreach" This reverts commit 9a688cc. * fix: My own stupidity * fix: style * fix - maybe * Update sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md * Update tag.js Co-authored-by: Rich Harris <[email protected]> * Update .changeset/curvy-cups-cough.md Co-authored-by: Simon H <[email protected]> * chore: Remove rest params * Delete .changeset/eighty-rivers-wash.md * fix: Honestly idk why it was broken but it's fixed now * fix: var name lol * fix: typegen * fix: idk * fix: It looks like a bunch of unformatted shit came in through main?? idk * Revert "fix: It looks like a bunch of unformatted shit came in through main?? idk" This reverts commit ab851d5. * fix: format again * this is getting ridiculous * Update tag.js Co-authored-by: Rich Harris <[email protected]> * fix errors * simplify a bit * use read_context * use read_context for const as well * remove unused code * unused import * unused export * remove spread args. sorry elliott * tidy up SnippetBlock interface * fix test * simplify * tweak * revert example, so that it matches the surrounding text * move PropsWithChildren back to public.d.ts * update typing docs, so that it flows from previous example * temporarily revert const parsing changes, to get prettier working again (???) * oops --------- Co-authored-by: Simon H <[email protected]> Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent d6fa5c7 commit d309a9d

File tree

33 files changed

+461
-182
lines changed

33 files changed

+461
-182
lines changed

.changeset/curvy-cups-cough.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
breaking: snippets can now take multiple arguments, support default parameters. Because of this, the type signature has changed

packages/svelte/elements.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export type ToggleEventHandler<T extends EventTarget> = EventHandler<ToggleEvent
6868
export interface DOMAttributes<T extends EventTarget> {
6969
// Implicit children prop every element has
7070
// Add this here so that libraries doing `$props<HTMLButtonAttributes>()` don't need a separate interface
71-
children?: import('svelte').Snippet<void>;
71+
children?: import('svelte').Snippet;
7272

7373
// Clipboard Events
7474
'on:copy'?: ClipboardEventHandler<T> | undefined | null;

packages/svelte/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
},
1111
"files": [
1212
"src",
13+
"!src/**/*.test.*",
1314
"types",
1415
"compiler.cjs",
1516
"*.d.ts",

packages/svelte/src/compiler/errors.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ const parse = {
9090
'duplicate-script-element': () =>
9191
`A component can have a single top-level <script> element and/or a single top-level <script context="module"> element`,
9292
'invalid-render-expression': () => 'expected an identifier followed by (...)',
93-
'invalid-render-arguments': () => 'expected at most one argument'
93+
'invalid-render-arguments': () => 'expected at most one argument',
94+
'invalid-render-spread-argument': () => 'cannot use spread arguments in {@render ...} tags',
95+
'invalid-snippet-rest-parameter': () =>
96+
'snippets do not support rest parameters; use an array instead'
9497
};
9598

9699
/** @satisfies {Errors} */

packages/svelte/src/compiler/legacy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ export function convert(source, ast) {
356356
start: node.start,
357357
end: node.end,
358358
expression: node.expression,
359-
context: node.context,
359+
parameters: node.parameters,
360360
children: node.body.nodes.map((child) => visit(child))
361361
};
362362
},

packages/svelte/src/compiler/phases/1-parse/read/context.js

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,24 +98,40 @@ export default function read_context(parser) {
9898
* @returns {any}
9999
*/
100100
function read_type_annotation(parser) {
101-
const index = parser.index;
101+
const start = parser.index;
102102
parser.allow_whitespace();
103103

104-
if (parser.eat(':')) {
105-
// we need to trick Acorn into parsing the type annotation
106-
const insert = '_ as ';
107-
let a = parser.index - insert.length;
108-
const template = ' '.repeat(a) + insert + parser.template.slice(parser.index);
109-
let expression = parse_expression_at(template, parser.ts, a);
104+
if (!parser.eat(':')) {
105+
parser.index = start;
106+
return undefined;
107+
}
110108

111-
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that
112-
if (expression.type === 'SequenceExpression') {
113-
expression = expression.expressions[0];
114-
}
109+
// we need to trick Acorn into parsing the type annotation
110+
const insert = '_ as ';
111+
let a = parser.index - insert.length;
112+
const template =
113+
parser.template.slice(0, a).replace(/[^\n]/g, ' ') +
114+
insert +
115+
parser.template.slice(parser.index);
116+
let expression = parse_expression_at(template, parser.ts, a);
115117

116-
parser.index = /** @type {number} */ (expression.end);
117-
return /** @type {any} */ (expression).typeAnnotation;
118-
} else {
119-
parser.index = index;
118+
// `foo: bar = baz` gets mangled — fix it
119+
if (expression.type === 'AssignmentExpression') {
120+
let b = expression.right.start;
121+
while (template[b] !== '=') b -= 1;
122+
expression = parse_expression_at(template.slice(0, b), parser.ts, a);
120123
}
124+
125+
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that
126+
if (expression.type === 'SequenceExpression') {
127+
expression = expression.expressions[0];
128+
}
129+
130+
parser.index = /** @type {number} */ (expression.end);
131+
return {
132+
type: 'TSTypeAnnotation',
133+
start,
134+
end: parser.index,
135+
typeAnnotation: /** @type {any} */ (expression).typeAnnotation
136+
};
121137
}

packages/svelte/src/compiler/phases/1-parse/state/tag.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,28 @@ function open(parser) {
274274

275275
parser.allow_whitespace();
276276

277-
const context = parser.match(')') ? null : read_context(parser);
277+
/** @type {import('estree').Pattern[]} */
278+
const parameters = [];
279+
280+
while (!parser.match(')')) {
281+
let pattern = read_context(parser);
282+
283+
parser.allow_whitespace();
284+
if (parser.eat('=')) {
285+
parser.allow_whitespace();
286+
pattern = {
287+
type: 'AssignmentPattern',
288+
left: pattern,
289+
right: read_expression(parser)
290+
};
291+
}
292+
293+
parameters.push(pattern);
294+
295+
if (!parser.eat(',')) break;
296+
parser.allow_whitespace();
297+
}
278298

279-
parser.allow_whitespace();
280299
parser.eat(')', true);
281300

282301
parser.allow_whitespace();
@@ -294,7 +313,7 @@ function open(parser) {
294313
end: name_end,
295314
name
296315
},
297-
context,
316+
parameters,
298317
body: create_fragment()
299318
})
300319
);
@@ -589,10 +608,6 @@ function special(parser) {
589608
error(expression, 'invalid-render-expression');
590609
}
591610

592-
if (expression.arguments.length > 1) {
593-
error(expression.arguments[1], 'invalid-render-arguments');
594-
}
595-
596611
parser.allow_whitespace();
597612
parser.eat('}', true);
598613

@@ -602,7 +617,7 @@ function special(parser) {
602617
start,
603618
end: parser.index,
604619
expression: expression.callee,
605-
argument: expression.arguments[0] ?? null
620+
arguments: expression.arguments
606621
})
607622
);
608623
}

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,13 @@ const validation = {
566566
parent_element: node.name
567567
});
568568
},
569+
RenderTag(node, context) {
570+
for (const arg of node.arguments) {
571+
if (arg.type === 'SpreadElement') {
572+
error(arg, 'invalid-render-spread-argument');
573+
}
574+
}
575+
},
569576
SvelteHead(node) {
570577
const attribute = node.attributes[0];
571578
if (attribute) {

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,13 +1498,7 @@ function process_children(nodes, parent, { visit, state }) {
14981498
);
14991499
const assignment = serialize_template_literal(sequence, visit, state)[1];
15001500
const init = b.stmt(b.assignment('=', b.member(text_id, b.id('nodeValue')), assignment));
1501-
const singular = b.stmt(
1502-
b.call(
1503-
'$.text_effect',
1504-
text_id,
1505-
b.thunk(serialize_template_literal(sequence, visit, state)[1])
1506-
)
1507-
);
1501+
const singular = b.stmt(b.call('$.text_effect', text_id, b.thunk(assignment)));
15081502

15091503
if (contains_call_expression && !within_bound_contenteditable) {
15101504
state.update_effects.push(singular);
@@ -1776,8 +1770,8 @@ export const template_visitors = {
17761770

17771771
/** @type {import('estree').Expression[]} */
17781772
const args = [context.state.node];
1779-
if (node.argument) {
1780-
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.argument))));
1773+
for (const arg of node.arguments) {
1774+
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg))));
17811775
}
17821776

17831777
let snippet_function = /** @type {import('estree').Expression} */ (
@@ -2472,61 +2466,67 @@ export const template_visitors = {
24722466
},
24732467
SnippetBlock(node, context) {
24742468
// TODO hoist where possible
2469+
/** @type {import('estree').Pattern[]} */
24752470
const args = [b.id('$$anchor')];
24762471

24772472
/** @type {import('estree').BlockStatement} */
24782473
let body;
24792474

2480-
if (node.context) {
2481-
const id = node.context.type === 'Identifier' ? node.context : b.id('$$context');
2482-
args.push(id);
2475+
/** @type {import('estree').Statement[]} */
2476+
const declarations = [];
2477+
2478+
for (let i = 0; i < node.parameters.length; i++) {
2479+
const argument = node.parameters[i];
24832480

2484-
/** @type {import('estree').Statement[]} */
2485-
const declarations = [];
2481+
if (!argument) continue;
24862482

2487-
// some of this is duplicated with EachBlock — TODO dedupe?
2488-
if (node.context.type === 'Identifier') {
2483+
if (argument.type === 'Identifier') {
2484+
args.push({
2485+
type: 'AssignmentPattern',
2486+
left: argument,
2487+
right: b.id('$.noop')
2488+
});
24892489
const binding = /** @type {import('#compiler').Binding} */ (
2490-
context.state.scope.get(id.name)
2490+
context.state.scope.get(argument.name)
24912491
);
2492-
binding.expression = b.call(id);
2493-
} else {
2494-
const paths = extract_paths(node.context);
2492+
binding.expression = b.call(argument);
2493+
continue;
2494+
}
24952495

2496-
for (const path of paths) {
2497-
const name = /** @type {import('estree').Identifier} */ (path.node).name;
2498-
const binding = /** @type {import('#compiler').Binding} */ (
2499-
context.state.scope.get(name)
2500-
);
2501-
declarations.push(
2502-
b.let(
2503-
path.node,
2504-
b.thunk(
2505-
/** @type {import('estree').Expression} */ (
2506-
context.visit(path.expression?.(b.call('$$context')))
2507-
)
2496+
let arg_alias = `$$arg${i}`;
2497+
args.push(b.id(arg_alias));
2498+
2499+
const paths = extract_paths(argument);
2500+
2501+
for (const path of paths) {
2502+
const name = /** @type {import('estree').Identifier} */ (path.node).name;
2503+
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(name));
2504+
declarations.push(
2505+
b.let(
2506+
path.node,
2507+
b.thunk(
2508+
/** @type {import('estree').Expression} */ (
2509+
context.visit(path.expression?.(b.maybe_call(b.id(arg_alias))))
25082510
)
25092511
)
2510-
);
2511-
2512-
// we need to eagerly evaluate the expression in order to hit any
2513-
// 'Cannot access x before initialization' errors
2514-
if (context.state.options.dev) {
2515-
declarations.push(b.stmt(b.call(name)));
2516-
}
2512+
)
2513+
);
25172514

2518-
binding.expression = b.call(name);
2515+
// we need to eagerly evaluate the expression in order to hit any
2516+
// 'Cannot access x before initialization' errors
2517+
if (context.state.options.dev) {
2518+
declarations.push(b.stmt(b.call(name)));
25192519
}
2520-
}
25212520

2522-
body = b.block([
2523-
...declarations,
2524-
.../** @type {import('estree').BlockStatement} */ (context.visit(node.body)).body
2525-
]);
2526-
} else {
2527-
body = /** @type {import('estree').BlockStatement} */ (context.visit(node.body));
2521+
binding.expression = b.call(name);
2522+
}
25282523
}
25292524

2525+
body = b.block([
2526+
...declarations,
2527+
.../** @type {import('estree').BlockStatement} */ (context.visit(node.body)).body
2528+
]);
2529+
25302530
const path = context.path;
25312531
// If we're top-level, then we can create a function for the snippet so that it can be referenced
25322532
// in the props declaration (default value pattern).

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,21 +1134,14 @@ const template_visitors = {
11341134
const snippet_function = state.options.dev
11351135
? b.call('$.validate_snippet', expression)
11361136
: expression;
1137-
if (node.argument) {
1138-
state.template.push(
1139-
t_statement(
1140-
b.stmt(
1141-
b.call(
1142-
snippet_function,
1143-
b.id('$$payload'),
1144-
/** @type {import('estree').Expression} */ (context.visit(node.argument))
1145-
)
1146-
)
1147-
)
1148-
);
1149-
} else {
1150-
state.template.push(t_statement(b.stmt(b.call(snippet_function, b.id('$$payload')))));
1151-
}
1137+
1138+
const snippet_args = node.arguments.map((arg) => {
1139+
return /** @type {import('estree').Expression} */ (context.visit(arg));
1140+
});
1141+
1142+
state.template.push(
1143+
t_statement(b.stmt(b.call(snippet_function, b.id('$$payload'), ...snippet_args)))
1144+
);
11521145

11531146
state.template.push(t_expression(anchor_id));
11541147
},
@@ -1451,19 +1444,14 @@ const template_visitors = {
14511444
},
14521445
SnippetBlock(node, context) {
14531446
// TODO hoist where possible
1454-
/** @type {import('estree').Pattern[]} */
1455-
const args = [b.id('$$payload')];
1456-
if (node.context) {
1457-
args.push(node.context);
1458-
}
1459-
14601447
context.state.init.push(
14611448
b.function_declaration(
14621449
node.expression,
1463-
args,
1450+
[b.id('$$payload'), ...node.parameters],
14641451
/** @type {import('estree').BlockStatement} */ (context.visit(node.body))
14651452
)
14661453
);
1454+
14671455
if (context.state.options.dev) {
14681456
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
14691457
}

packages/svelte/src/compiler/phases/scope.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,8 +592,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
592592
const child_scope = state.scope.child();
593593
scopes.set(node, child_scope);
594594

595-
if (node.context) {
596-
for (const id of extract_identifiers(node.context)) {
595+
for (const param of node.parameters) {
596+
for (const id of extract_identifiers(param)) {
597597
child_scope.declare(id, 'each', 'let');
598598
}
599599
}

0 commit comments

Comments
 (0)