From 338500085a248cbb1d540e124e70ea16ff55c425 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 27 Jun 2025 23:08:32 +0200 Subject: [PATCH 1/6] feat: add parent hierarchy to `__svelte_meta` objects at dev time This adds a `parent` property to the `__svelte_meta` properties that are added to elements at dev time. This property represents the closest non-element parent the element is related to. For example for `{#if ...}
foo
{/if}` the `parent` of the div would be the line/column of the if block. The parent is recursive and goes upwards (through component boundaries) until the root component is reached, which has no parent. part of #11389 --- .changeset/hot-buses-end.md | 5 ++ .../3-transform/client/transform-client.js | 3 + .../3-transform/client/visitors/AwaitBlock.js | 8 +- .../3-transform/client/visitors/EachBlock.js | 4 +- .../3-transform/client/visitors/IfBlock.js | 4 +- .../3-transform/client/visitors/KeyBlock.js | 8 +- .../client/visitors/shared/component.js | 12 ++- .../client/visitors/shared/utils.js | 38 ++++++++- .../svelte/src/internal/client/context.js | 40 ++++++++- .../src/internal/client/dev/elements.js | 2 + .../src/internal/client/dom/blocks/await.js | 16 +++- .../src/internal/client/dom/blocks/snippet.js | 8 +- .../client/dom/blocks/svelte-element.js | 3 +- packages/svelte/src/internal/client/index.js | 2 +- .../svelte/src/internal/client/types.d.ts | 9 ++ .../samples/svelte-meta-parent/_config.js | 82 +++++++++++++++++++ .../samples/svelte-meta-parent/child.svelte | 1 + .../samples/svelte-meta-parent/main.svelte | 32 ++++++++ .../svelte-meta-parent/passthrough.svelte | 5 ++ 19 files changed, 262 insertions(+), 20 deletions(-) create mode 100644 .changeset/hot-buses-end.md create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte diff --git a/.changeset/hot-buses-end.md b/.changeset/hot-buses-end.md new file mode 100644 index 000000000000..d98860c54c4c --- /dev/null +++ b/.changeset/hot-buses-end.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add parent hierarchy to \_\_svelte_meta objects at dev time diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index e2e006c14bec..f9e11205753c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -453,6 +453,9 @@ export function client_component(analysis, options) { if (analysis.custom_element) { to_remove.push(b.literal('$$host')); } + if (dev) { + to_remove.push(b.literal('$$dev')); + } component_block.body.unshift( b.const( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 7873cf3ddbd7..e4b94d1c3a1c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -5,7 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; -import { build_expression } from './shared/utils.js'; +import { build_expression, with_dev_stack } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -54,7 +54,7 @@ export function AwaitBlock(node, context) { } context.state.init.push( - b.stmt( + with_dev_stack( b.call( '$.await', context.state.node, @@ -64,7 +64,9 @@ export function AwaitBlock(node, context) { : b.null, then_block, catch_block - ) + ), + node, + 'await' ) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 201c4b278f78..ae8cc5320336 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -13,7 +13,7 @@ import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { get_value } from './shared/declarations.js'; -import { build_expression } from './shared/utils.js'; +import { build_expression, with_dev_stack } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -337,7 +337,7 @@ export function EachBlock(node, context) { ); } - context.state.init.push(b.stmt(b.call('$.each', ...args))); + context.state.init.push(with_dev_stack(b.call('$.each', ...args), node, 'each')); } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index deab040e509b..858f23476bef 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_expression } from './shared/utils.js'; +import { build_expression, with_dev_stack } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -74,7 +74,7 @@ export function IfBlock(node, context) { args.push(b.id('$$elseif')); } - statements.push(b.stmt(b.call('$.if', ...args))); + statements.push(with_dev_stack(b.call('$.if', ...args), node, 'if')); context.state.init.push(b.block(statements)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index 2f17479c7e9e..d0f1b107e642 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_expression } from './shared/utils.js'; +import { build_expression, with_dev_stack } from './shared/utils.js'; /** * @param {AST.KeyBlock} node @@ -15,6 +15,10 @@ export function KeyBlock(node, context) { const body = /** @type {Expression} */ (context.visit(node.fragment)); context.state.init.push( - b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body))) + with_dev_stack( + b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)), + node, + 'key' + ) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index cb6e4de47876..a638c329f4ef 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -4,7 +4,12 @@ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js'; +import { + build_bind_this, + memoize_expression, + validate_binding, + with_dev_stack +} from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; import { determine_slot } from '../../../../../utils/slot.js'; @@ -483,7 +488,10 @@ export function build_component(node, component_name, context) { ); } else { context.state.template.push_comment(); - statements.push(b.stmt(fn(anchor))); + + statements.push( + with_dev_stack(fn(anchor), node, 'component', { componentTag: component_name }) + ); } return statements.length > 1 ? b.block(statements) : statements[0]; 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 0bd9bfb128c7..7619ef29431e 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, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */ +/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern, CallExpression, Statement } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; @@ -7,7 +7,7 @@ import * as b from '#compiler/builders'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; -import { dev, is_ignored, locator } from '../../../../../state.js'; +import { dev, is_ignored, locator, filename } from '../../../../../state.js'; import { build_getter, create_derived } from '../../utils.js'; /** @@ -394,3 +394,37 @@ export function build_expression(context, expression, metadata, state = context. return sequence; } + +/** + * 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 {{ start?: number }} node - AST node for location info + * @param {'component' | 'if' | 'each' | 'await' | 'key'} 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 + */ +export function with_dev_stack(call_expression, node, type, additional) { + if (!dev) { + return b.stmt(call_expression); + } + + const location = node.start && locator(node.start); + if (!location) { + return b.stmt(call_expression); + } + + // Wrap with dev stack tracking + return b.stmt( + b.call( + '$.with_dev_stack', + b.arrow([], b.block([b.stmt(call_expression)])), + b.literal(type), + b.literal(filename), + b.literal(location.line), + b.literal(location.column), + additional + ? b.object(Object.entries(additional).map(([key, value]) => b.init(key, b.literal(value)))) + : b.null + ) + ); +} diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 7c7213b7a2de..ee90241b7ee0 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -1,4 +1,4 @@ -/** @import { ComponentContext } from '#client' */ +/** @import { ComponentContext, DevStackEntry } from '#client' */ import { DEV } from 'esm-env'; import { lifecycle_outside_component } from '../shared/errors.js'; @@ -20,6 +20,44 @@ export function set_component_context(context) { component_context = context; } +/** @type {DevStackEntry | null} */ +export let dev_stack = null; + +/** @param {DevStackEntry | null} stack */ +export function set_dev_stack(stack) { + dev_stack = stack; +} + +/** + * Execute a callback with a new dev stack entry + * @param {() => any} callback - Function to execute + * @param {DevStackEntry['type']} type - Type of block/component + * @param {string} file - Source file + * @param {number} line - Line number + * @param {number} column - Column number + * @param {Record} [additional] - Any additional properties to add to the dev stack entry + * @returns {any} + */ +export function with_dev_stack(callback, type, file, line, column, additional) { + /** @type {DevStackEntry} */ + const new_entry = { + type, + file, + line, + column, + parent: dev_stack, + ...additional + }; + const previous_stack = dev_stack; + dev_stack = new_entry; + + try { + return callback(); + } finally { + dev_stack = previous_stack; + } +} + /** * The current component function. Different from current component context: * ```html diff --git a/packages/svelte/src/internal/client/dev/elements.js b/packages/svelte/src/internal/client/dev/elements.js index f70f893d1ef7..8dd54e0a2ac5 100644 --- a/packages/svelte/src/internal/client/dev/elements.js +++ b/packages/svelte/src/internal/client/dev/elements.js @@ -2,6 +2,7 @@ import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants'; import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js'; import { hydrating } from '../dom/hydration.js'; +import { dev_stack } from '../context.js'; /** * @param {any} fn @@ -28,6 +29,7 @@ export function add_locations(fn, filename, locations) { function assign_location(element, filename, location) { // @ts-expect-error element.__svelte_meta = { + parent: dev_stack, loc: { file: filename, line: location[0], column: location[1] } }; diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 47df5fc9a5f8..325224fff237 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -16,9 +16,11 @@ import { queue_micro_task } from '../task.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { component_context, + dev_stack, is_runes, set_component_context, - set_dev_current_component_function + set_dev_current_component_function, + set_dev_stack } from '../../context.js'; const PENDING = 0; @@ -45,6 +47,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { /** @type {any} */ var component_function = DEV ? component_context?.function : null; + var dev_original_stack = DEV ? dev_stack : null; /** @type {V | Promise | typeof UNINITIALIZED} */ var input = UNINITIALIZED; @@ -75,7 +78,10 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { set_active_effect(effect); set_active_reaction(effect); // TODO do we need both? set_component_context(active_component_context); - if (DEV) set_dev_current_component_function(component_function); + if (DEV) { + set_dev_current_component_function(component_function); + set_dev_stack(dev_original_stack); + } } try { @@ -107,7 +113,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { } } finally { if (restore) { - if (DEV) set_dev_current_component_function(null); + if (DEV) { + set_dev_current_component_function(null); + set_dev_stack(null); + } + set_component_context(null); set_active_reaction(null); set_active_effect(null); diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 32d88d4c606a..3a794efe1449 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -5,7 +5,9 @@ import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js'; import { dev_current_component_function, - set_dev_current_component_function + dev_stack, + set_dev_current_component_function, + set_dev_stack } from '../../context.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_fragment_from_html } from '../reconciler.js'; @@ -61,14 +63,18 @@ 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/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index ffa57b2d8ba4..231a3621b1af 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -18,7 +18,7 @@ import { import { set_should_intro } from '../../render.js'; import { current_each_item, set_current_each_item } from './each.js'; import { active_effect } from '../../runtime.js'; -import { component_context } from '../../context.js'; +import { component_context, dev_stack } from '../../context.js'; import { DEV } from 'esm-env'; import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { assign_nodes } from '../template.js'; @@ -107,6 +107,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio if (DEV && location) { // @ts-expect-error element.__svelte_meta = { + parent: dev_stack, loc: { file: filename, line: location[0], diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 60f9af912060..d236914684a8 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -1,6 +1,6 @@ export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; -export { push, pop } from './context.js'; +export { push, pop, with_dev_stack } from './context.js'; export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; export { cleanup_styles } from './dev/css.js'; export { add_locations } from './dev/elements.js'; diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 9703c2aac198..600f0a4a9bf6 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -187,4 +187,13 @@ export type SourceLocation = | [line: number, column: number] | [line: number, column: number, SourceLocation[]]; +export interface DevStackEntry { + file: string; + type: 'component' | 'if' | 'each' | 'await' | 'key'; + line: number; + column: number; + parent: DevStackEntry | null; + componentTag?: string; +} + export * from './reactivity/types'; 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 new file mode 100644 index 000000000000..527976e0f6cb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js @@ -0,0 +1,82 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + compileOptions: { + dev: true + }, + + html: `

no parent

if

each

loading

key

hi

hi

`, + + async test({ target, assert }) { + await tick(); + const [main, if_, each, await_, key, child1, child2] = target.querySelectorAll('p'); + + // @ts-expect-error + assert.deepEqual(main.__svelte_meta.parent, null); + + // @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(each.__svelte_meta.parent, { + file: 'main.svelte', + type: 'each', + line: 14, + 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(key.__svelte_meta.parent, { + file: 'main.svelte', + type: 'key', + line: 24, + 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(child2.__svelte_meta.parent, { + file: 'main.svelte', + type: 'component', + componentTag: 'Child', + line: 31, + column: 1, + parent: { + file: 'main.svelte', + type: 'component', + componentTag: 'Passthrough', + line: 30, + column: 0, + parent: null + } + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte new file mode 100644 index 000000000000..0df6def59374 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte @@ -0,0 +1 @@ +

hi

\ No newline at end of file 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 new file mode 100644 index 000000000000..23f11ab69408 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte @@ -0,0 +1,32 @@ + + +

no parent

+ +{#if true} +

if

+{/if} + +{#each [1]} +

each

+{/each} + +{#await Promise.resolve()} +

loading

+{:then} +

await

+{/await} + +{#key key} +

key

+{/key} + + + + + + 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 new file mode 100644 index 000000000000..bf79ef5d9e9a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte @@ -0,0 +1,5 @@ + + +{@render children()} From 23a4e7c1638db517178b232b1dd4b34908ab72c5 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 27 Jun 2025 23:12:07 +0200 Subject: [PATCH 2/6] oops --- .../src/compiler/phases/3-transform/client/transform-client.js | 3 --- .../phases/3-transform/client/visitors/shared/utils.js | 1 - 2 files changed, 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index f9e11205753c..e2e006c14bec 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -453,9 +453,6 @@ export function client_component(analysis, options) { if (analysis.custom_element) { to_remove.push(b.literal('$$host')); } - if (dev) { - to_remove.push(b.literal('$$dev')); - } component_block.body.unshift( b.const( 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 7619ef29431e..a93fced0d5dc 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 @@ -413,7 +413,6 @@ export function with_dev_stack(call_expression, node, type, additional) { return b.stmt(call_expression); } - // Wrap with dev stack tracking return b.stmt( b.call( '$.with_dev_stack', From 53933619e81853ebb8bc5c5f512f36d065eb9a2e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 30 Jun 2025 21:58:04 +0200 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Rich Harris --- .changeset/hot-buses-end.md | 2 +- .../3-transform/client/visitors/shared/utils.js | 6 ++---- packages/svelte/src/internal/client/context.js | 11 +++++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.changeset/hot-buses-end.md b/.changeset/hot-buses-end.md index d98860c54c4c..dd5a28fca876 100644 --- a/.changeset/hot-buses-end.md +++ b/.changeset/hot-buses-end.md @@ -2,4 +2,4 @@ 'svelte': patch --- -feat: add parent hierarchy to \_\_svelte_meta objects at dev time +feat: add parent hierarchy to `__svelte_meta` objects 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 a93fced0d5dc..8e8e87a755a8 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 @@ -416,14 +416,12 @@ export function with_dev_stack(call_expression, node, type, additional) { return b.stmt( b.call( '$.with_dev_stack', - b.arrow([], b.block([b.stmt(call_expression)])), + b.arrow([], call_expression), b.literal(type), b.literal(filename), b.literal(location.line), b.literal(location.column), - additional - ? b.object(Object.entries(additional).map(([key, value]) => b.init(key, b.literal(value)))) - : b.null + additional && b.object(Object.entries(additional).map(([k, v]) => b.init(k, b.literal(v)))) ) ); } diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index ee90241b7ee0..6eed40432533 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -39,22 +39,21 @@ export function set_dev_stack(stack) { * @returns {any} */ export function with_dev_stack(callback, type, file, line, column, additional) { - /** @type {DevStackEntry} */ - const new_entry = { + const parent = dev_stack; + + dev_stack = { type, file, line, column, - parent: dev_stack, + parent, ...additional }; - const previous_stack = dev_stack; - dev_stack = new_entry; try { return callback(); } finally { - dev_stack = previous_stack; + dev_stack = parent; } } From 3e3d20cc1dd4e39a781f7cf58617188d973537e8 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 30 Jun 2025 22:12:43 +0200 Subject: [PATCH 4/6] tweak --- .../phases/3-transform/client/visitors/AwaitBlock.js | 4 ++-- .../phases/3-transform/client/visitors/EachBlock.js | 4 ++-- .../phases/3-transform/client/visitors/IfBlock.js | 4 ++-- .../phases/3-transform/client/visitors/KeyBlock.js | 4 ++-- .../3-transform/client/visitors/shared/component.js | 4 ++-- .../phases/3-transform/client/visitors/shared/utils.js | 8 ++++---- packages/svelte/src/compiler/state.js | 3 +++ packages/svelte/src/internal/client/context.js | 7 ++++--- packages/svelte/src/internal/client/index.js | 2 +- 9 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index e4b94d1c3a1c..c550c8e17bcb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -5,7 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; -import { build_expression, with_dev_stack } from './shared/utils.js'; +import { build_expression, add_svelte_meta } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -54,7 +54,7 @@ export function AwaitBlock(node, context) { } context.state.init.push( - with_dev_stack( + add_svelte_meta( b.call( '$.await', context.state.node, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index ae8cc5320336..353927b86570 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -13,7 +13,7 @@ import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { get_value } from './shared/declarations.js'; -import { build_expression, with_dev_stack } from './shared/utils.js'; +import { build_expression, add_svelte_meta } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -337,7 +337,7 @@ export function EachBlock(node, context) { ); } - context.state.init.push(with_dev_stack(b.call('$.each', ...args), node, 'each')); + context.state.init.push(add_svelte_meta(b.call('$.each', ...args), node, 'each')); } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index 858f23476bef..cfd2bb7b09f0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_expression, with_dev_stack } from './shared/utils.js'; +import { build_expression, add_svelte_meta } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -74,7 +74,7 @@ export function IfBlock(node, context) { args.push(b.id('$$elseif')); } - statements.push(with_dev_stack(b.call('$.if', ...args), node, 'if')); + statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if')); context.state.init.push(b.block(statements)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index d0f1b107e642..3add1fbe93c8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_expression, with_dev_stack } from './shared/utils.js'; +import { build_expression, add_svelte_meta } from './shared/utils.js'; /** * @param {AST.KeyBlock} node @@ -15,7 +15,7 @@ export function KeyBlock(node, context) { const body = /** @type {Expression} */ (context.visit(node.fragment)); context.state.init.push( - with_dev_stack( + add_svelte_meta( b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)), node, 'key' diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index a638c329f4ef..3a394540a376 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -8,7 +8,7 @@ import { build_bind_this, memoize_expression, validate_binding, - with_dev_stack + add_svelte_meta } from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; @@ -490,7 +490,7 @@ export function build_component(node, component_name, context) { context.state.template.push_comment(); statements.push( - with_dev_stack(fn(anchor), node, 'component', { componentTag: component_name }) + add_svelte_meta(fn(anchor), node, 'component', { componentTag: component_name }) ); } 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 8e8e87a755a8..e5ff8635f8a8 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 @@ -7,7 +7,7 @@ import * as b from '#compiler/builders'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; -import { dev, is_ignored, locator, filename } from '../../../../../state.js'; +import { dev, is_ignored, locator, component_name } from '../../../../../state.js'; import { build_getter, create_derived } from '../../utils.js'; /** @@ -403,7 +403,7 @@ export function build_expression(context, expression, metadata, state = context. * @param {Record} [additional] - Any additional properties to add to the dev stack entry * @returns {Statement} - Statement with or without dev stack wrapping */ -export function with_dev_stack(call_expression, node, type, additional) { +export function add_svelte_meta(call_expression, node, type, additional) { if (!dev) { return b.stmt(call_expression); } @@ -415,10 +415,10 @@ export function with_dev_stack(call_expression, node, type, additional) { return b.stmt( b.call( - '$.with_dev_stack', + '$.add_svelte_meta', b.arrow([], call_expression), b.literal(type), - b.literal(filename), + b.id(component_name), b.literal(location.line), b.literal(location.column), additional && b.object(Object.entries(additional).map(([k, v]) => b.init(k, b.literal(v)))) diff --git a/packages/svelte/src/compiler/state.js b/packages/svelte/src/compiler/state.js index 9095651ced27..5eb25dd6bb15 100644 --- a/packages/svelte/src/compiler/state.js +++ b/packages/svelte/src/compiler/state.js @@ -16,6 +16,9 @@ export let warnings = []; */ export let filename; +/** + * The name of the component that is used in the `export default function ...` statement. + */ export let component_name = ''; /** diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 6eed40432533..e4220149ab2c 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -11,6 +11,7 @@ import { } from './runtime.js'; import { effect, teardown } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; +import { FILENAME } from '../../constants.js'; /** @type {ComponentContext | null} */ export let component_context = null; @@ -32,18 +33,18 @@ export function set_dev_stack(stack) { * Execute a callback with a new dev stack entry * @param {() => any} callback - Function to execute * @param {DevStackEntry['type']} type - Type of block/component - * @param {string} file - Source file + * @param {any} component - Component function * @param {number} line - Line number * @param {number} column - Column number * @param {Record} [additional] - Any additional properties to add to the dev stack entry * @returns {any} */ -export function with_dev_stack(callback, type, file, line, column, additional) { +export function add_svelte_meta(callback, type, component, line, column, additional) { const parent = dev_stack; dev_stack = { type, - file, + file: component[FILENAME], line, column, parent, diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index d236914684a8..576a30fa775c 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -1,6 +1,6 @@ export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; -export { push, pop, with_dev_stack } from './context.js'; +export { push, pop, add_svelte_meta } from './context.js'; export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; export { cleanup_styles } from './dev/css.js'; export { add_locations } from './dev/elements.js'; From 757683fb56213dbacc01fd7e8c9957f5aad29719 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 30 Jun 2025 22:22:13 +0200 Subject: [PATCH 5/6] original component tag --- .../client/visitors/shared/component.js | 4 +--- .../samples/svelte-meta-parent/_config.js | 15 ++++++++++++--- .../samples/svelte-meta-parent/main.svelte | 4 +++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 3a394540a376..aa3704b50b30 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -489,9 +489,7 @@ export function build_component(node, component_name, context) { } else { context.state.template.push_comment(); - statements.push( - add_svelte_meta(fn(anchor), node, 'component', { componentTag: component_name }) - ); + statements.push(add_svelte_meta(fn(anchor), node, 'component', { componentTag: node.name })); } return statements.length > 1 ? b.block(statements) : statements[0]; 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 527976e0f6cb..3b01d9ef3208 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,12 +6,11 @@ export default test({ compileOptions: { dev: true }, - - html: `

no parent

if

each

loading

key

hi

hi

`, + html: `

no parent

if

each

loading

key

hi

hi

hi

`, async test({ target, assert }) { await tick(); - const [main, if_, each, await_, key, child1, child2] = target.querySelectorAll('p'); + const [main, if_, each, await_, key, child1, child2, child3] = target.querySelectorAll('p'); // @ts-expect-error assert.deepEqual(main.__svelte_meta.parent, null); @@ -78,5 +77,15 @@ export default test({ 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 + }); } }); 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 23f11ab69408..e8c8a9163275 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 @@ -1,7 +1,7 @@ @@ -30,3 +30,5 @@ + + From cad332a0670eb974cbcb637bf298aa619ca0e472 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 1 Jul 2025 22:47:15 +0200 Subject: [PATCH 6/6] 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 c3615d9d5085..7f1a4ae7ba72 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 e5ff8635f8a8..9b45768eac27 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 3a794efe1449..32d88d4c606a 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 a2806bde81fd..a2de8940a6df 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 88c84f27fe62..80c4155705af 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 3e70537d7cbb..99193b5d2ddf 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 600f0a4a9bf6..0b7310e1726b 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 3b01d9ef3208..eba85d5098ee 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 e8c8a9163275..b9bd46d8f781 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 bf79ef5d9e9a..70ba81a4c131 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}