fix: set correct component context when rendering snippets (#11401)

fixes #11399
pull/11432/head
Rich Harris 1 year ago committed by GitHub
parent f64d16931d
commit edefc846c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: set correct component context when rendering snippets

@ -384,6 +384,7 @@ export function analyze_component(root, source, options) {
reactive_statements: new Map(),
binding_groups: new Map(),
slot_names: new Map(),
top_level_snippets: [],
css: {
ast: root.css,
hash: root.css

@ -354,6 +354,7 @@ export function client_component(source, analysis, options) {
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
...analysis.top_level_snippets,
.../** @type {import('estree').Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context ? b.empty : b.stmt(b.call('$.init')),
.../** @type {import('estree').Statement[]} */ (template.body)

@ -835,10 +835,7 @@ function serialize_inline_component(node, component_name, context) {
if (slot_name === 'default') {
push_prop(
b.init(
'children',
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
)
b.init('children', context.state.options.dev ? b.call('$.wrap_snippet', slot_fn) : slot_fn)
);
} else {
serialized_slots.push(b.init(slot_name, slot_fn));
@ -2566,16 +2563,20 @@ export const template_visitors = {
.../** @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
// in the props declaration (default value pattern).
if (path.length === 1 && path[0].type === 'Fragment') {
context.state.init.push(b.function_declaration(node.expression, args, body));
} else {
context.state.init.push(b.const(node.expression, b.arrow(args, body)));
}
/** @type {import('estree').Expression} */
let snippet = b.arrow(args, body);
if (context.state.options.dev) {
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
snippet = b.call('$.wrap_snippet', snippet);
}
const declaration = b.var(node.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') {
context.state.analysis.top_level_snippets.push(declaration);
} else {
context.state.init.push(declaration);
}
},
FunctionExpression: function_visitor,

@ -7,10 +7,9 @@ import type {
SlotElement,
SvelteElement,
SvelteNode,
SvelteOptions,
Warning
SvelteOptions
} from '#compiler';
import type { Identifier, LabeledStatement, Program } from 'estree';
import type { Identifier, LabeledStatement, Program, Statement, VariableDeclaration } from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
export interface Js {
@ -68,6 +67,7 @@ export interface ComponentAnalysis extends Analysis {
/** If `true`, should append styles through JavaScript */
inject_styles: boolean;
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
top_level_snippets: VariableDeclaration[];
/** Identifiers that make up the `bind:group` expression -> internal group binding name */
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
slot_names: Map<string, SlotElement>;

@ -1,5 +1,7 @@
import { add_snippet_symbol } from '../../../shared/validate.js';
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { branch, block, destroy_effect } from '../../reactivity/effects.js';
import { current_component_context, set_current_component_context } from '../../runtime.js';
/**
* @template {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} SnippetFn
@ -28,3 +30,26 @@ export function snippet(get_snippet, node, ...args) {
}
}, EFFECT_TRANSPARENT);
}
/**
* In development, wrap the snippet function so that it passes validation, and so that the
* correct component context is set for ownership checks
* @param {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} fn
* @returns
*/
export function wrap_snippet(fn) {
let component = current_component_context;
return add_snippet_symbol(
(/** @type {import('#client').TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_context = current_component_context;
set_current_component_context(component);
try {
return fn(node, ...args);
} finally {
set_current_component_context(previous_component_context);
}
}
);
}

@ -7,7 +7,7 @@ export { key_block as key } from './dom/blocks/key.js';
export { css_props } from './dom/blocks/css-props.js';
export { index, each } from './dom/blocks/each.js';
export { html } from './dom/blocks/html.js';
export { snippet } from './dom/blocks/snippet.js';
export { snippet, wrap_snippet } from './dom/blocks/snippet.js';
export { component } from './dom/blocks/svelte-component.js';
export { element } from './dom/blocks/svelte-element.js';
export { head } from './dom/blocks/svelte-head.js';

@ -0,0 +1,7 @@
<script>
let { object = $bindable() } = $props();
</script>
<button onclick={() => object.count += 1}>
clicks: {object.count}
</button>

@ -0,0 +1,5 @@
<script>
let { children } = $props();
</script>
{@render children?.()}

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
html: `<button>clicks: 0</button>`,
compileOptions: {
dev: true
},
warnings: []
});

@ -0,0 +1,10 @@
<script>
import Outer from './Outer.svelte';
import Inner from './Inner.svelte';
let object = $state({ count: 0 });
</script>
<Outer>
<Inner bind:object />
</Outer>

@ -6,16 +6,15 @@ var root_1 = $.template(`Something`, 1);
var root = $.template(`<!> `, 1);
export default function Bind_component_snippet($$anchor) {
let value = $.source('');
const _snippet = snippet;
var fragment_1 = root();
function snippet($$anchor) {
var snippet = ($$anchor) => {
var fragment = root_1();
$.append($$anchor, fragment);
}
};
let value = $.source('');
const _snippet = snippet;
var fragment_1 = root();
var node = $.first_child(fragment_1);
TextInput(node, {

Loading…
Cancel
Save