diff --git a/.changeset/hot-buses-end.md b/.changeset/hot-buses-end.md new file mode 100644 index 0000000000..d98860c54c --- /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 e2e006c14b..f9e1120575 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 7873cf3ddb..e4b94d1c3a 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 201c4b278f..ae8cc53203 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 deab040e50..858f23476b 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 2f17479c7e..d0f1b107e6 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 cb6e4de478..a638c329f4 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 0bd9bfb128..7619ef2943 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 7c7213b7a2..ee90241b7e 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 f70f893d1e..8dd54e0a2a 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 47df5fc9a5..325224fff2 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 32d88d4c60..3a794efe14 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 ffa57b2d8b..231a3621b1 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 60f9af9120..d236914684 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 9703c2aac1..600f0a4a9b 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 0000000000..527976e0f6 --- /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 0000000000..0df6def593 --- /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 0000000000..23f11ab694 --- /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 0000000000..bf79ef5d9e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte @@ -0,0 +1,5 @@ + + +{@render children()}