Skip to content

Commit

Permalink
feat: allow reserved snippet names when embedded in component
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm committed Nov 4, 2024
1 parent 551284c commit 140d6b1
Show file tree
Hide file tree
Showing 11 changed files with 82 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/strong-donkeys-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: allow reserved snippet names when embedded in component
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/phases/1-parse/state/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ function open(parser) {
parser.require_whitespace();

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

if (name === null) {
Expand Down
14 changes: 14 additions & 0 deletions packages/svelte/src/compiler/phases/3-transform/client/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,17 @@ export function can_inline_variable(binding) {
binding.initial?.type === 'Literal'
);
}

/**
* @param {Statement[]} statements
*/
export function get_variable_declaration_name(statements) {
if (
statements.length !== 1 ||
statements[0].type !== 'VariableDeclaration' ||
statements[0].declarations[0].id?.type !== 'Identifier'
) {
return null;
}
return statements[0].declarations[0].id.name;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_reserved } from '../../../../../utils.js';
import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
Expand Down Expand Up @@ -79,7 +80,11 @@ export function SnippetBlock(node, context) {
snippet = b.call('$.wrap_snippet', b.id(context.state.analysis.name), snippet);
}

const declaration = b.const(node.expression, snippet);
const id_expression =
node.expression.type === 'Identifier' && is_reserved(node.expression.name)
? b.id(`$_${node.expression.name}`)
: node.expression;
const declaration = b.const(id_expression, snippet);

// Top-level snippets are hoisted so they can be referenced in the `<script>`
if (context.path.length === 1 && context.path[0].type === 'Fragment') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { create_derived } from '../../utils.js';
import { create_derived, get_variable_declaration_name } from '../../utils.js';
import { build_bind_this, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
Expand Down Expand Up @@ -230,15 +230,16 @@ export function build_component(node, component_name, context, anchor = context.
// Group children by slot
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock') {
/** @type {Statement[]} */
const init = [];
// the SnippetBlock visitor adds a declaration to `init`, but if it's directly
// inside a component then we want to hoist them into a block so that they
// can be used as props without creating conflicts
context.visit(child, {
...context.state,
init: snippet_declarations
});
context.visit(child, { ...context.state, init });
snippet_declarations.push(...init);
const name = /** @type { string } */ (get_variable_declaration_name(init));

push_prop(b.prop('init', child.expression, child.expression));
push_prop(b.prop('init', child.expression, b.id(name)));

// Interop: allows people to pass snippets when component still uses slots
serialized_slots.push(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { is_reserved } from '../../../../../utils.js';
import * as b from '../../../../utils/builders.js';

/**
* @param {AST.SnippetBlock} node
* @param {ComponentContext} context
*/
export function SnippetBlock(node, context) {
const id_expression =
node.expression.type === 'Identifier' && is_reserved(node.expression.name)
? b.id(`$_${node.expression.name}`)
: node.expression;

const fn = b.function_declaration(
node.expression,
id_expression,
[b.id('$$payload'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body))
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { empty_comment, build_attribute_value } from './utils.js';
import * as b from '../../../../../utils/builders.js';
import { is_element_node } from '../../../../nodes.js';
import { get_variable_declaration_name } from '../../../utils.js';

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

push_prop(b.prop('init', child.expression, child.expression));
push_prop(b.prop('init', child.expression, b.id(name)));

// Interop: allows people to pass snippets when component still uses slots
serialized_slots.push(
b.init(child.expression.name === 'children' ? 'default' : child.expression.name, b.true)
b.init(child.expression.name === 'children' ? 'default' : name, b.true)
);

continue;
Expand Down
16 changes: 15 additions & 1 deletion packages/svelte/src/compiler/phases/3-transform/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @import { Context } from 'zimmerframe' */
/** @import { TransformState } from './types.js' */
/** @import { AST, Binding, Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler' */
/** @import { Node, Expression, CallExpression } from 'estree' */
/** @import { Node, Expression, CallExpression, Statement } from 'estree' */
import {
regex_ends_with_whitespaces,
regex_not_whitespace,
Expand Down Expand Up @@ -452,3 +452,17 @@ export function transform_inspect_rune(node, context) {
return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg));
}
}

/**
* @param {Statement[]} statements
*/
export function get_variable_declaration_name(statements) {
if (
statements.length !== 1 ||
statements[0].type !== 'FunctionDeclaration' ||
statements[0].id.type !== 'Identifier'
) {
return null;
}
return statements[0].id.name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
const { catch: catch_snippet } = $props();
</script>

{@render catch_snippet()}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { test } from '../../test';

export default test({
html: `<p>hello world</p>`
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script>
import Child from './Child.svelte';
</script>

<Child>
{#snippet catch()}
<p>hello world</p>
{/snippet}
</Child>

0 comments on commit 140d6b1

Please sign in to comment.