Skip to content

Commit edefc84

Browse files
authored
fix: set correct component context when rendering snippets (#11401)
fixes #11399
1 parent f64d169 commit edefc84

File tree

12 files changed

+88
-23
lines changed

12 files changed

+88
-23
lines changed

.changeset/good-plums-type.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+
fix: set correct component context when rendering snippets

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ export function analyze_component(root, source, options) {
384384
reactive_statements: new Map(),
385385
binding_groups: new Map(),
386386
slot_names: new Map(),
387+
top_level_snippets: [],
387388
css: {
388389
ast: root.css,
389390
hash: root.css

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ export function client_component(source, analysis, options) {
354354
...store_setup,
355355
...legacy_reactive_declarations,
356356
...group_binding_declarations,
357+
...analysis.top_level_snippets,
357358
.../** @type {import('estree').Statement[]} */ (instance.body),
358359
analysis.runes || !analysis.needs_context ? b.empty : b.stmt(b.call('$.init')),
359360
.../** @type {import('estree').Statement[]} */ (template.body)

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -835,10 +835,7 @@ function serialize_inline_component(node, component_name, context) {
835835

836836
if (slot_name === 'default') {
837837
push_prop(
838-
b.init(
839-
'children',
840-
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
841-
)
838+
b.init('children', context.state.options.dev ? b.call('$.wrap_snippet', slot_fn) : slot_fn)
842839
);
843840
} else {
844841
serialized_slots.push(b.init(slot_name, slot_fn));
@@ -2566,16 +2563,20 @@ export const template_visitors = {
25662563
.../** @type {import('estree').BlockStatement} */ (context.visit(node.body)).body
25672564
]);
25682565

2569-
const path = context.path;
2570-
// If we're top-level, then we can create a function for the snippet so that it can be referenced
2571-
// in the props declaration (default value pattern).
2572-
if (path.length === 1 && path[0].type === 'Fragment') {
2573-
context.state.init.push(b.function_declaration(node.expression, args, body));
2574-
} else {
2575-
context.state.init.push(b.const(node.expression, b.arrow(args, body)));
2576-
}
2566+
/** @type {import('estree').Expression} */
2567+
let snippet = b.arrow(args, body);
2568+
25772569
if (context.state.options.dev) {
2578-
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
2570+
snippet = b.call('$.wrap_snippet', snippet);
2571+
}
2572+
2573+
const declaration = b.var(node.expression, snippet);
2574+
2575+
// Top-level snippets are hoisted so they can be referenced in the `<script>`
2576+
if (context.path.length === 1 && context.path[0].type === 'Fragment') {
2577+
context.state.analysis.top_level_snippets.push(declaration);
2578+
} else {
2579+
context.state.init.push(declaration);
25792580
}
25802581
},
25812582
FunctionExpression: function_visitor,

packages/svelte/src/compiler/phases/types.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import type {
77
SlotElement,
88
SvelteElement,
99
SvelteNode,
10-
SvelteOptions,
11-
Warning
10+
SvelteOptions
1211
} from '#compiler';
13-
import type { Identifier, LabeledStatement, Program } from 'estree';
12+
import type { Identifier, LabeledStatement, Program, Statement, VariableDeclaration } from 'estree';
1413
import type { Scope, ScopeRoot } from './scope.js';
1514

1615
export interface Js {
@@ -68,6 +67,7 @@ export interface ComponentAnalysis extends Analysis {
6867
/** If `true`, should append styles through JavaScript */
6968
inject_styles: boolean;
7069
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
70+
top_level_snippets: VariableDeclaration[];
7171
/** Identifiers that make up the `bind:group` expression -> internal group binding name */
7272
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
7373
slot_names: Map<string, SlotElement>;

packages/svelte/src/internal/client/dom/blocks/snippet.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { add_snippet_symbol } from '../../../shared/validate.js';
12
import { EFFECT_TRANSPARENT } from '../../constants.js';
23
import { branch, block, destroy_effect } from '../../reactivity/effects.js';
4+
import { current_component_context, set_current_component_context } from '../../runtime.js';
35

46
/**
57
* @template {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} SnippetFn
@@ -28,3 +30,26 @@ export function snippet(get_snippet, node, ...args) {
2830
}
2931
}, EFFECT_TRANSPARENT);
3032
}
33+
34+
/**
35+
* In development, wrap the snippet function so that it passes validation, and so that the
36+
* correct component context is set for ownership checks
37+
* @param {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} fn
38+
* @returns
39+
*/
40+
export function wrap_snippet(fn) {
41+
let component = current_component_context;
42+
43+
return add_snippet_symbol(
44+
(/** @type {import('#client').TemplateNode} */ node, /** @type {any[]} */ ...args) => {
45+
var previous_component_context = current_component_context;
46+
set_current_component_context(component);
47+
48+
try {
49+
return fn(node, ...args);
50+
} finally {
51+
set_current_component_context(previous_component_context);
52+
}
53+
}
54+
);
55+
}

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export { key_block as key } from './dom/blocks/key.js';
77
export { css_props } from './dom/blocks/css-props.js';
88
export { index, each } from './dom/blocks/each.js';
99
export { html } from './dom/blocks/html.js';
10-
export { snippet } from './dom/blocks/snippet.js';
10+
export { snippet, wrap_snippet } from './dom/blocks/snippet.js';
1111
export { component } from './dom/blocks/svelte-component.js';
1212
export { element } from './dom/blocks/svelte-element.js';
1313
export { head } from './dom/blocks/svelte-head.js';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let { object = $bindable() } = $props();
3+
</script>
4+
5+
<button onclick={() => object.count += 1}>
6+
clicks: {object.count}
7+
</button>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
let { children } = $props();
3+
</script>
4+
5+
{@render children?.()}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<button>clicks: 0</button>`,
5+
6+
compileOptions: {
7+
dev: true
8+
},
9+
10+
warnings: []
11+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
import Outer from './Outer.svelte';
3+
import Inner from './Inner.svelte';
4+
5+
let object = $state({ count: 0 });
6+
</script>
7+
8+
<Outer>
9+
<Inner bind:object />
10+
</Outer>

packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ var root_1 = $.template(`Something`, 1);
66
var root = $.template(`<!> `, 1);
77

88
export default function Bind_component_snippet($$anchor) {
9-
let value = $.source('');
10-
const _snippet = snippet;
11-
var fragment_1 = root();
12-
13-
function snippet($$anchor) {
9+
var snippet = ($$anchor) => {
1410
var fragment = root_1();
1511

1612
$.append($$anchor, fragment);
17-
}
13+
};
1814

15+
let value = $.source('');
16+
const _snippet = snippet;
17+
var fragment_1 = root();
1918
var node = $.first_child(fragment_1);
2019

2120
TextInput(node, {

0 commit comments

Comments
 (0)