From cad332a0670eb974cbcb637bf298aa619ca0e472 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 1 Jul 2025 22:47:15 +0200 Subject: [PATCH] make render appear in tree, keep tree in sync when rerenders occur --- .../3-transform/client/visitors/RenderTag.js | 14 +- .../client/visitors/shared/utils.js | 18 +- .../src/internal/client/dom/blocks/snippet.js | 8 +- .../src/internal/client/reactivity/effects.js | 8 +- .../src/internal/client/reactivity/types.d.ts | 10 +- .../svelte/src/internal/client/runtime.js | 9 +- .../svelte/src/internal/client/types.d.ts | 2 +- .../samples/svelte-meta-parent/_config.js | 229 +++++++++++++----- .../samples/svelte-meta-parent/main.svelte | 16 ++ .../svelte-meta-parent/passthrough.svelte | 8 +- 10 files changed, 227 insertions(+), 95 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index c3615d9d50..7f1a4ae7ba 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -3,7 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_expression } from './shared/utils.js'; +import { add_svelte_meta, build_expression } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -48,16 +48,22 @@ export function RenderTag(node, context) { } context.state.init.push( - b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args)) + add_svelte_meta( + b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args), + node, + 'render' + ) ); } else { context.state.init.push( - b.stmt( + add_svelte_meta( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( snippet_function, context.state.node, ...args - ) + ), + node, + 'render' ) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index e5ff8635f8..9b45768eac 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern, CallExpression, Statement } from 'estree' */ +/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; @@ -397,26 +397,26 @@ export function build_expression(context, expression, metadata, state = context. /** * Wraps a statement/expression with dev stack tracking in dev mode - * @param {CallExpression} call_expression - The function call to wrap (e.g., $.if, $.each, etc.) + * @param {Expression} expression - The function call to wrap (e.g., $.if, $.each, etc.) * @param {{ start?: number }} node - AST node for location info - * @param {'component' | 'if' | 'each' | 'await' | 'key'} type - Type of block/component + * @param {'component' | 'if' | 'each' | 'await' | 'key' | 'render'} type - Type of block/component * @param {Record} [additional] - Any additional properties to add to the dev stack entry - * @returns {Statement} - Statement with or without dev stack wrapping + * @returns {ExpressionStatement} - Statement with or without dev stack wrapping */ -export function add_svelte_meta(call_expression, node, type, additional) { +export function add_svelte_meta(expression, node, type, additional) { if (!dev) { - return b.stmt(call_expression); + return b.stmt(expression); } - const location = node.start && locator(node.start); + const location = node.start !== undefined && locator(node.start); if (!location) { - return b.stmt(call_expression); + return b.stmt(expression); } return b.stmt( b.call( '$.add_svelte_meta', - b.arrow([], call_expression), + b.arrow([], expression), b.literal(type), b.id(component_name), b.literal(location.line), diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 3a794efe14..32d88d4c60 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -5,9 +5,7 @@ import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js'; import { dev_current_component_function, - dev_stack, - set_dev_current_component_function, - set_dev_stack + set_dev_current_component_function } from '../../context.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_fragment_from_html } from '../reconciler.js'; @@ -63,18 +61,14 @@ export function snippet(node, get_snippet, ...args) { * @param {(node: TemplateNode, ...args: any[]) => void} fn */ export function wrap_snippet(component, fn) { - var original_stack = dev_stack; const snippet = (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => { var previous_component_function = dev_current_component_function; - var previous_stack = dev_stack; set_dev_current_component_function(component); - set_dev_stack(original_stack); try { return fn(node, ...args); } finally { set_dev_current_component_function(previous_component_function); - set_dev_stack(previous_stack); } }; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index a2806bde81..a2de8940a6 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -41,7 +41,7 @@ import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { derived } from './deriveds.js'; -import { component_context, dev_current_component_function } from '../context.js'; +import { component_context, dev_current_component_function, dev_stack } from '../context.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -359,7 +359,11 @@ export function template_effect(fn, thunks = [], d = derived) { * @param {number} flags */ export function block(fn, flags = 0) { - return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); + var effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); + if (DEV) { + effect.dev_stack = dev_stack; + } + return effect; } /** diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 88c84f27fe..80c4155705 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -1,4 +1,10 @@ -import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client'; +import type { + ComponentContext, + DevStackEntry, + Equals, + TemplateNode, + TransitionManager +} from '#client'; export interface Signal { /** Flags bitmask */ @@ -80,6 +86,8 @@ export interface Effect extends Reaction { parent: Effect | null; /** Dev only */ component_function?: any; + /** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */ + dev_stack?: DevStackEntry | null; } export type Source = Value; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3e70537d7c..99193b5d2d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -35,12 +35,13 @@ import { tracing_expressions, get_stack } from './dev/tracing.js'; import { component_context, dev_current_component_function, + dev_stack, is_runes, set_component_context, - set_dev_current_component_function + set_dev_current_component_function, + set_dev_stack } from './context.js'; import { handle_error, invoke_error_boundary } from './error-handling.js'; -import { snapshot } from '../shared/clone.js'; let is_flushing = false; @@ -445,6 +446,9 @@ export function update_effect(effect) { if (DEV) { var previous_component_fn = dev_current_component_function; set_dev_current_component_function(effect.component_function); + var previous_stack = /** @type {any} */ (dev_stack); + // only block effects have a dev stack, keep the current one otherwise + set_dev_stack(effect.dev_stack ?? dev_stack); } try { @@ -479,6 +483,7 @@ export function update_effect(effect) { if (DEV) { set_dev_current_component_function(previous_component_fn); + set_dev_stack(previous_stack); } } } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 600f0a4a9b..0b7310e172 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -189,7 +189,7 @@ export type SourceLocation = export interface DevStackEntry { file: string; - type: 'component' | 'if' | 'each' | 'await' | 'key'; + type: 'component' | 'if' | 'each' | 'await' | 'key' | 'render'; line: number; column: number; parent: DevStackEntry | null; diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js index 3b01d9ef32..eba85d5098 100644 --- a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js @@ -6,86 +6,181 @@ export default test({ compileOptions: { dev: true }, - html: `

no parent

if

each

loading

key

hi

hi

hi

`, + html: ` +

no parent

+ +

if

+

each

+

loading

+

key

+

hi

+

hi

+

hi

+

hi

+

hi

+ `, async test({ target, assert }) { - await tick(); - const [main, if_, each, await_, key, child1, child2, child3] = target.querySelectorAll('p'); - - // @ts-expect-error - assert.deepEqual(main.__svelte_meta.parent, null); + function check() { + const [main, if_, each, await_, key, child1, child2, child3, child4, dynamic] = + target.querySelectorAll('p'); - // @ts-expect-error - assert.deepEqual(if_.__svelte_meta.parent, { - file: 'main.svelte', - type: 'if', - line: 10, - column: 0, - parent: null - }); + // @ts-expect-error + assert.deepEqual(main.__svelte_meta.parent, null); - // @ts-expect-error - assert.deepEqual(each.__svelte_meta.parent, { - file: 'main.svelte', - type: 'each', - line: 14, - column: 0, - parent: null - }); + // @ts-expect-error + assert.deepEqual(if_.__svelte_meta.parent, { + file: 'main.svelte', + type: 'if', + line: 12, + column: 0, + parent: null + }); - // @ts-expect-error - assert.deepEqual(await_.__svelte_meta.parent, { - file: 'main.svelte', - type: 'await', - line: 18, - column: 0, - parent: null - }); + // @ts-expect-error + assert.deepEqual(each.__svelte_meta.parent, { + file: 'main.svelte', + type: 'each', + line: 16, + column: 0, + parent: null + }); - // @ts-expect-error - assert.deepEqual(key.__svelte_meta.parent, { - file: 'main.svelte', - type: 'key', - line: 24, - column: 0, - parent: null - }); + // @ts-expect-error + assert.deepEqual(await_.__svelte_meta.parent, { + file: 'main.svelte', + type: 'await', + line: 20, + column: 0, + parent: null + }); - // @ts-expect-error - assert.deepEqual(child1.__svelte_meta.parent, { - file: 'main.svelte', - type: 'component', - componentTag: 'Child', - line: 28, - column: 0, - parent: null - }); + // @ts-expect-error + assert.deepEqual(key.__svelte_meta.parent, { + file: 'main.svelte', + type: 'key', + line: 26, + column: 0, + parent: null + }); - // @ts-expect-error - assert.deepEqual(child2.__svelte_meta.parent, { - file: 'main.svelte', - type: 'component', - componentTag: 'Child', - line: 31, - column: 1, - parent: { + // @ts-expect-error + assert.deepEqual(child1.__svelte_meta.parent, { file: 'main.svelte', type: 'component', - componentTag: 'Passthrough', + componentTag: 'Child', line: 30, column: 0, parent: null - } - }); + }); + + // @ts-expect-error + assert.deepEqual(child2.__svelte_meta.parent, { + file: 'main.svelte', + type: 'component', + componentTag: 'Child', + line: 33, + column: 1, + parent: { + file: 'passthrough.svelte', + type: 'render', + line: 5, + column: 0, + parent: { + file: 'main.svelte', + type: 'component', + componentTag: 'Passthrough', + line: 32, + column: 0, + parent: null + } + } + }); + + // @ts-expect-error + assert.deepEqual(child3.__svelte_meta.parent, { + file: 'main.svelte', + type: 'component', + componentTag: 'Child', + line: 38, + column: 2, + parent: { + file: 'passthrough.svelte', + type: 'render', + line: 5, + column: 0, + parent: { + file: 'main.svelte', + type: 'component', + componentTag: 'Passthrough', + line: 37, + column: 1, + parent: { + file: 'passthrough.svelte', + type: 'render', + line: 5, + column: 0, + parent: { + file: 'main.svelte', + type: 'component', + componentTag: 'Passthrough', + line: 36, + column: 0, + parent: null + } + } + } + } + }); + + // @ts-expect-error + assert.deepEqual(child4.__svelte_meta.parent, { + file: 'passthrough.svelte', + type: 'render', + line: 8, + column: 1, + parent: { + file: 'passthrough.svelte', + type: 'if', + line: 7, + column: 0, + parent: { + file: 'main.svelte', + type: 'component', + componentTag: 'Passthrough', + line: 43, + column: 1, + parent: { + file: 'main.svelte', + type: 'if', + line: 42, + column: 0, + parent: null + } + } + } + }); + + // @ts-expect-error + assert.deepEqual(dynamic.__svelte_meta.parent, { + file: 'main.svelte', + type: 'component', + componentTag: 'x.y', + line: 50, + column: 0, + parent: null + }); + } - // @ts-expect-error - assert.deepEqual(child3.__svelte_meta.parent, { - file: 'main.svelte', - type: 'component', - componentTag: 'x.y', - line: 34, - column: 0, - parent: null - }); + await tick(); + check(); + + // Test that stack is kept when re-rendering + const button = target.querySelector('button'); + button?.click(); + await tick(); + button?.click(); + await tick(); + check(); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte index e8c8a91632..b9bd46d8f7 100644 --- a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte @@ -3,9 +3,11 @@ import Passthrough from "./passthrough.svelte"; let x = { y: Child } let key = 'test'; + let show = $state(true);

no parent

+ {#if true}

if

@@ -31,4 +33,18 @@ + + + + + + +{#if show} + + {#snippet named()} +

hi

+ {/snippet} +
+{/if} + diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte index bf79ef5d9e..70ba81a4c1 100644 --- a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte @@ -1,5 +1,9 @@ -{@render children()} +{@render children?.()} + +{#if true} + {@render named?.()} +{/if}