Skip to content

Commit 140d6b1

Browse files
committed
feat: allow reserved snippet names when embedded in component
1 parent 551284c commit 140d6b1

File tree

11 files changed

+82
-16
lines changed

11 files changed

+82
-16
lines changed
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+
feat: allow reserved snippet names when embedded in component

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ function open(parser) {
271271
parser.require_whitespace();
272272

273273
const name_start = parser.index;
274-
const name = parser.read_identifier();
274+
const name = parser.read_identifier(parser.stack.at(-1)?.type === 'Component');
275275
const name_end = parser.index;
276276

277277
if (name === null) {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,17 @@ export function can_inline_variable(binding) {
326326
binding.initial?.type === 'Literal'
327327
);
328328
}
329+
330+
/**
331+
* @param {Statement[]} statements
332+
*/
333+
export function get_variable_declaration_name(statements) {
334+
if (
335+
statements.length !== 1 ||
336+
statements[0].type !== 'VariableDeclaration' ||
337+
statements[0].declarations[0].id?.type !== 'Identifier'
338+
) {
339+
return null;
340+
}
341+
return statements[0].declarations[0].id.name;
342+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
4+
import { is_reserved } from '../../../../../utils.js';
45
import { dev } from '../../../../state.js';
56
import { extract_paths } from '../../../../utils/ast.js';
67
import * as b from '../../../../utils/builders.js';
@@ -79,7 +80,11 @@ export function SnippetBlock(node, context) {
7980
snippet = b.call('$.wrap_snippet', b.id(context.state.analysis.name), snippet);
8081
}
8182

82-
const declaration = b.const(node.expression, snippet);
83+
const id_expression =
84+
node.expression.type === 'Identifier' && is_reserved(node.expression.name)
85+
? b.id(`$_${node.expression.name}`)
86+
: node.expression;
87+
const declaration = b.const(id_expression, snippet);
8388

8489
// Top-level snippets are hoisted so they can be referenced in the `<script>`
8590
if (context.path.length === 1 && context.path[0].type === 'Fragment') {

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { dev, is_ignored } from '../../../../../state.js';
55
import { get_attribute_chunks } from '../../../../../utils/ast.js';
66
import * as b from '../../../../../utils/builders.js';
7-
import { create_derived } from '../../utils.js';
7+
import { create_derived, get_variable_declaration_name } from '../../utils.js';
88
import { build_bind_this, validate_binding } from '../shared/utils.js';
99
import { build_attribute_value } from '../shared/element.js';
1010
import { build_event_handler } from './events.js';
@@ -230,15 +230,16 @@ export function build_component(node, component_name, context, anchor = context.
230230
// Group children by slot
231231
for (const child of node.fragment.nodes) {
232232
if (child.type === 'SnippetBlock') {
233+
/** @type {Statement[]} */
234+
const init = [];
233235
// the SnippetBlock visitor adds a declaration to `init`, but if it's directly
234236
// inside a component then we want to hoist them into a block so that they
235237
// can be used as props without creating conflicts
236-
context.visit(child, {
237-
...context.state,
238-
init: snippet_declarations
239-
});
238+
context.visit(child, { ...context.state, init });
239+
snippet_declarations.push(...init);
240+
const name = /** @type { string } */ (get_variable_declaration_name(init));
240241

241-
push_prop(b.prop('init', child.expression, child.expression));
242+
push_prop(b.prop('init', child.expression, b.id(name)));
242243

243244
// Interop: allows people to pass snippets when component still uses slots
244245
serialized_slots.push(

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
/** @import { BlockStatement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types.js' */
4+
import { is_reserved } from '../../../../../utils.js';
45
import * as b from '../../../../utils/builders.js';
56

67
/**
78
* @param {AST.SnippetBlock} node
89
* @param {ComponentContext} context
910
*/
1011
export function SnippetBlock(node, context) {
12+
const id_expression =
13+
node.expression.type === 'Identifier' && is_reserved(node.expression.name)
14+
? b.id(`$_${node.expression.name}`)
15+
: node.expression;
16+
1117
const fn = b.function_declaration(
12-
node.expression,
18+
id_expression,
1319
[b.id('$$payload'), ...node.parameters],
1420
/** @type {BlockStatement} */ (context.visit(node.body))
1521
);

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { empty_comment, build_attribute_value } from './utils.js';
55
import * as b from '../../../../../utils/builders.js';
66
import { is_element_node } from '../../../../nodes.js';
7+
import { get_variable_declaration_name } from '../../../utils.js';
78

89
/**
910
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
@@ -109,19 +110,20 @@ export function build_inline_component(node, expression, context) {
109110
// Group children by slot
110111
for (const child of node.fragment.nodes) {
111112
if (child.type === 'SnippetBlock') {
113+
/** @type {Statement[]} */
114+
const init = [];
112115
// the SnippetBlock visitor adds a declaration to `init`, but if it's directly
113116
// inside a component then we want to hoist them into a block so that they
114117
// can be used as props without creating conflicts
115-
context.visit(child, {
116-
...context.state,
117-
init: snippet_declarations
118-
});
118+
context.visit(child, { ...context.state, init });
119+
snippet_declarations.push(...init);
120+
const name = /** @type { string } */ (get_variable_declaration_name(init));
119121

120-
push_prop(b.prop('init', child.expression, child.expression));
122+
push_prop(b.prop('init', child.expression, b.id(name)));
121123

122124
// Interop: allows people to pass snippets when component still uses slots
123125
serialized_slots.push(
124-
b.init(child.expression.name === 'children' ? 'default' : child.expression.name, b.true)
126+
b.init(child.expression.name === 'children' ? 'default' : name, b.true)
125127
);
126128

127129
continue;

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { Context } from 'zimmerframe' */
22
/** @import { TransformState } from './types.js' */
33
/** @import { AST, Binding, Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler' */
4-
/** @import { Node, Expression, CallExpression } from 'estree' */
4+
/** @import { Node, Expression, CallExpression, Statement } from 'estree' */
55
import {
66
regex_ends_with_whitespaces,
77
regex_not_whitespace,
@@ -452,3 +452,17 @@ export function transform_inspect_rune(node, context) {
452452
return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg));
453453
}
454454
}
455+
456+
/**
457+
* @param {Statement[]} statements
458+
*/
459+
export function get_variable_declaration_name(statements) {
460+
if (
461+
statements.length !== 1 ||
462+
statements[0].type !== 'FunctionDeclaration' ||
463+
statements[0].id.type !== 'Identifier'
464+
) {
465+
return null;
466+
}
467+
return statements[0].id.name;
468+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
const { catch: catch_snippet } = $props();
3+
</script>
4+
5+
{@render catch_snippet()}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<p>hello world</p>`
5+
});

0 commit comments

Comments
 (0)