Skip to content

Commit 32882a9

Browse files
feat: add parent hierarchy to __svelte_meta objects at dev time (#16255)
* 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 ...}<div>foo</div>{/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 * oops * Apply suggestions from code review Co-authored-by: Rich Harris <[email protected]> * tweak * original component tag * make render appear in tree, keep tree in sync when rerenders occur --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 9dddb31 commit 32882a9

File tree

22 files changed

+408
-28
lines changed

22 files changed

+408
-28
lines changed

.changeset/hot-buses-end.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: add parent hierarchy to `__svelte_meta` objects

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
55
import * as b from '#compiler/builders';
66
import { create_derived } from '../utils.js';
77
import { get_value } from './shared/declarations.js';
8-
import { build_expression } from './shared/utils.js';
8+
import { build_expression, add_svelte_meta } from './shared/utils.js';
99

1010
/**
1111
* @param {AST.AwaitBlock} node
@@ -54,7 +54,7 @@ export function AwaitBlock(node, context) {
5454
}
5555

5656
context.state.init.push(
57-
b.stmt(
57+
add_svelte_meta(
5858
b.call(
5959
'$.await',
6060
context.state.node,
@@ -64,7 +64,9 @@ export function AwaitBlock(node, context) {
6464
: b.null,
6565
then_block,
6666
catch_block
67-
)
67+
),
68+
node,
69+
'await'
6870
)
6971
);
7072
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { dev } from '../../../../state.js';
1313
import { extract_paths, object } from '../../../../utils/ast.js';
1414
import * as b from '#compiler/builders';
1515
import { get_value } from './shared/declarations.js';
16-
import { build_expression } from './shared/utils.js';
16+
import { build_expression, add_svelte_meta } from './shared/utils.js';
1717

1818
/**
1919
* @param {AST.EachBlock} node
@@ -337,7 +337,7 @@ export function EachBlock(node, context) {
337337
);
338338
}
339339

340-
context.state.init.push(b.stmt(b.call('$.each', ...args)));
340+
context.state.init.push(add_svelte_meta(b.call('$.each', ...args), node, 'each'));
341341
}
342342

343343
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '#compiler/builders';
5-
import { build_expression } from './shared/utils.js';
5+
import { build_expression, add_svelte_meta } from './shared/utils.js';
66

77
/**
88
* @param {AST.IfBlock} node
@@ -74,7 +74,7 @@ export function IfBlock(node, context) {
7474
args.push(b.id('$$elseif'));
7575
}
7676

77-
statements.push(b.stmt(b.call('$.if', ...args)));
77+
statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
7878

7979
context.state.init.push(b.block(statements));
8080
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '#compiler/builders';
5-
import { build_expression } from './shared/utils.js';
5+
import { build_expression, add_svelte_meta } from './shared/utils.js';
66

77
/**
88
* @param {AST.KeyBlock} node
@@ -15,6 +15,10 @@ export function KeyBlock(node, context) {
1515
const body = /** @type {Expression} */ (context.visit(node.fragment));
1616

1717
context.state.init.push(
18-
b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)))
18+
add_svelte_meta(
19+
b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)),
20+
node,
21+
'key'
22+
)
1923
);
2024
}

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/** @import { ComponentContext } from '../types' */
44
import { unwrap_optional } from '../../../../utils/ast.js';
55
import * as b from '#compiler/builders';
6-
import { build_expression } from './shared/utils.js';
6+
import { add_svelte_meta, build_expression } from './shared/utils.js';
77

88
/**
99
* @param {AST.RenderTag} node
@@ -48,16 +48,22 @@ export function RenderTag(node, context) {
4848
}
4949

5050
context.state.init.push(
51-
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
51+
add_svelte_meta(
52+
b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args),
53+
node,
54+
'render'
55+
)
5256
);
5357
} else {
5458
context.state.init.push(
55-
b.stmt(
59+
add_svelte_meta(
5660
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
5761
snippet_function,
5862
context.state.node,
5963
...args
60-
)
64+
),
65+
node,
66+
'render'
6167
)
6268
);
6369
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
import { dev, is_ignored } from '../../../../../state.js';
55
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
66
import * as b from '#compiler/builders';
7-
import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
7+
import {
8+
build_bind_this,
9+
memoize_expression,
10+
validate_binding,
11+
add_svelte_meta
12+
} from '../shared/utils.js';
813
import { build_attribute_value } from '../shared/element.js';
914
import { build_event_handler } from './events.js';
1015
import { determine_slot } from '../../../../../utils/slot.js';
@@ -483,7 +488,8 @@ export function build_component(node, component_name, context) {
483488
);
484489
} else {
485490
context.state.template.push_comment();
486-
statements.push(b.stmt(fn(anchor)));
491+
492+
statements.push(add_svelte_meta(fn(anchor), node, 'component', { componentTag: node.name }));
487493
}
488494

489495
return statements.length > 1 ? b.block(statements) : statements[0];

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */
1+
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */
22
/** @import { AST, ExpressionMetadata } from '#compiler' */
33
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
44
import { walk } from 'zimmerframe';
@@ -7,7 +7,7 @@ import * as b from '#compiler/builders';
77
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
88
import { regex_is_valid_identifier } from '../../../../patterns.js';
99
import is_reference from 'is-reference';
10-
import { dev, is_ignored, locator } from '../../../../../state.js';
10+
import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
1111
import { build_getter, create_derived } from '../../utils.js';
1212

1313
/**
@@ -424,3 +424,34 @@ export function build_expression(context, expression, metadata, state = context.
424424

425425
return sequence;
426426
}
427+
428+
/**
429+
* Wraps a statement/expression with dev stack tracking in dev mode
430+
* @param {Expression} expression - The function call to wrap (e.g., $.if, $.each, etc.)
431+
* @param {{ start?: number }} node - AST node for location info
432+
* @param {'component' | 'if' | 'each' | 'await' | 'key' | 'render'} type - Type of block/component
433+
* @param {Record<string, number | string>} [additional] - Any additional properties to add to the dev stack entry
434+
* @returns {ExpressionStatement} - Statement with or without dev stack wrapping
435+
*/
436+
export function add_svelte_meta(expression, node, type, additional) {
437+
if (!dev) {
438+
return b.stmt(expression);
439+
}
440+
441+
const location = node.start !== undefined && locator(node.start);
442+
if (!location) {
443+
return b.stmt(expression);
444+
}
445+
446+
return b.stmt(
447+
b.call(
448+
'$.add_svelte_meta',
449+
b.arrow([], expression),
450+
b.literal(type),
451+
b.id(component_name),
452+
b.literal(location.line),
453+
b.literal(location.column),
454+
additional && b.object(Object.entries(additional).map(([k, v]) => b.init(k, b.literal(v))))
455+
)
456+
);
457+
}

packages/svelte/src/compiler/state.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export let warnings = [];
1616
*/
1717
export let filename;
1818

19+
/**
20+
* The name of the component that is used in the `export default function ...` statement.
21+
*/
1922
export let component_name = '<unknown>';
2023

2124
/**

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { ComponentContext } from '#client' */
1+
/** @import { ComponentContext, DevStackEntry } from '#client' */
22

33
import { DEV } from 'esm-env';
44
import { lifecycle_outside_component } from '../shared/errors.js';
@@ -11,6 +11,7 @@ import {
1111
} from './runtime.js';
1212
import { effect, teardown } from './reactivity/effects.js';
1313
import { legacy_mode_flag } from '../flags/index.js';
14+
import { FILENAME } from '../../constants.js';
1415

1516
/** @type {ComponentContext | null} */
1617
export let component_context = null;
@@ -20,6 +21,43 @@ export function set_component_context(context) {
2021
component_context = context;
2122
}
2223

24+
/** @type {DevStackEntry | null} */
25+
export let dev_stack = null;
26+
27+
/** @param {DevStackEntry | null} stack */
28+
export function set_dev_stack(stack) {
29+
dev_stack = stack;
30+
}
31+
32+
/**
33+
* Execute a callback with a new dev stack entry
34+
* @param {() => any} callback - Function to execute
35+
* @param {DevStackEntry['type']} type - Type of block/component
36+
* @param {any} component - Component function
37+
* @param {number} line - Line number
38+
* @param {number} column - Column number
39+
* @param {Record<string, any>} [additional] - Any additional properties to add to the dev stack entry
40+
* @returns {any}
41+
*/
42+
export function add_svelte_meta(callback, type, component, line, column, additional) {
43+
const parent = dev_stack;
44+
45+
dev_stack = {
46+
type,
47+
file: component[FILENAME],
48+
line,
49+
column,
50+
parent,
51+
...additional
52+
};
53+
54+
try {
55+
return callback();
56+
} finally {
57+
dev_stack = parent;
58+
}
59+
}
60+
2361
/**
2462
* The current component function. Different from current component context:
2563
* ```html

0 commit comments

Comments
 (0)