Skip to content

Commit 5483495

Browse files
trueadmRich-Harris
andauthored
feat: add $inspect.trace rune (#14290)
* feat: add $trace rune WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP * lint * fix * fix * fix * fix * fix * fix * fix * more tweaks * lint * improve label for derived cached * improve label for derived cached * lint * better stacks * complete redesign * fixes * dead code * dead code * improve change detection * rename rune * lint * lint * fix bug * tweaks * Update packages/svelte/src/internal/client/dev/tracing.js Co-authored-by: Rich Harris <[email protected]> * Update packages/svelte/src/internal/client/dev/tracing.js Co-authored-by: Rich Harris <[email protected]> * Update packages/svelte/src/internal/client/dev/tracing.js Co-authored-by: Rich Harris <[email protected]> * Update packages/svelte/src/internal/client/dev/tracing.js Co-authored-by: Rich Harris <[email protected]> * todos * add test + some docs * changeset * update messages * address feedback * address feedback * limit to first statement of function * remove unreachable trace_rune_duplicate error * tweak message * remove the expression statement, not the expression * revert * make label optional * relax restriction on label - no longer necessary with new design * update errors * newline * tweak * add some docs * fix playground * fix playground * tweak message when function runs outside an effect * unused * tweak * handle async functions * fail on generators * regenerate, update docs * better labelling --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 64a32ce commit 5483495

File tree

32 files changed

+596
-73
lines changed

32 files changed

+596
-73
lines changed

.changeset/tame-ants-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: adds $inspect.trace rune

documentation/docs/02-runes/07-$inspect.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,20 @@ A convenient way to find the origin of some change is to pass `console.trace` to
4242
// @errors: 2304
4343
$inspect(stuff).with(console.trace);
4444
```
45+
46+
## $inspect.trace(...)
47+
48+
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
49+
50+
```svelte
51+
<script>
52+
import { doSomeWork } from './elsewhere';
53+
54+
$effect(() => {
55+
+++$inspect.trace();+++
56+
doSomeWork();
57+
});
58+
</script>
59+
```
60+
61+
`$inspect.trace` takes an optional first argument which will be used as the label.

documentation/docs/98-reference/.generated/compile-errors.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,18 @@ Expected whitespace
442442
Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case
443443
```
444444

445+
### inspect_trace_generator
446+
447+
```
448+
`$inspect.trace(...)` cannot be used inside a generator function
449+
```
450+
451+
### inspect_trace_invalid_placement
452+
453+
```
454+
`$inspect.trace(...)` must be the first statement of a function body
455+
```
456+
445457
### invalid_arguments_usage
446458

447459
```

packages/svelte/messages/compile-errors/script.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@
5454

5555
> Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case
5656
57+
## inspect_trace_generator
58+
59+
> `$inspect.trace(...)` cannot be used inside a generator function
60+
61+
## inspect_trace_invalid_placement
62+
63+
> `$inspect.trace(...)` must be the first statement of a function body
64+
5765
## invalid_arguments_usage
5866

5967
> The arguments keyword cannot be used within the template or at the top level of a component

packages/svelte/src/ambient.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,25 @@ declare function $inspect<T extends any[]>(
371371
...values: T
372372
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };
373373

374+
declare namespace $inspect {
375+
/**
376+
* Tracks which reactive state changes caused an effect to re-run. Must be the first
377+
* statement of a function body. Example:
378+
*
379+
* ```svelte
380+
* <script>
381+
* let count = $state(0);
382+
*
383+
* $effect(() => {
384+
* $inspect.trace('my effect');
385+
*
386+
* count;
387+
* });
388+
* </script>
389+
*/
390+
export function trace(name: string): void;
391+
}
392+
374393
/**
375394
* Retrieves the `this` reference of the custom element that contains this component. Example:
376395
*

packages/svelte/src/compiler/errors.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,24 @@ export function import_svelte_internal_forbidden(node) {
206206
e(node, "import_svelte_internal_forbidden", `Imports of \`svelte/internal/*\` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from \`svelte/internal/*\` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case\nhttps://svelte.dev/e/import_svelte_internal_forbidden`);
207207
}
208208

209+
/**
210+
* `$inspect.trace(...)` cannot be used inside a generator function
211+
* @param {null | number | NodeLike} node
212+
* @returns {never}
213+
*/
214+
export function inspect_trace_generator(node) {
215+
e(node, "inspect_trace_generator", `\`$inspect.trace(...)\` cannot be used inside a generator function\nhttps://svelte.dev/e/inspect_trace_generator`);
216+
}
217+
218+
/**
219+
* `$inspect.trace(...)` must be the first statement of a function body
220+
* @param {null | number | NodeLike} node
221+
* @returns {never}
222+
*/
223+
export function inspect_trace_invalid_placement(node) {
224+
e(node, "inspect_trace_invalid_placement", `\`$inspect.trace(...)\` must be the first statement of a function body\nhttps://svelte.dev/e/inspect_trace_invalid_placement`);
225+
}
226+
209227
/**
210228
* The arguments keyword cannot be used within the template or at the top level of a component
211229
* @param {null | number | NodeLike} node

packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
/** @import { CallExpression, VariableDeclarator } from 'estree' */
1+
/** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, VariableDeclarator } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { Context } from '../types' */
44
import { get_rune } from '../../scope.js';
55
import * as e from '../../../errors.js';
66
import { get_parent, unwrap_optional } from '../../../utils/ast.js';
77
import { is_pure, is_safe_identifier } from './shared/utils.js';
8-
import { mark_subtree_dynamic } from './shared/fragment.js';
8+
import { dev, locate_node, source } from '../../../state.js';
9+
import * as b from '../../../utils/builders.js';
910

1011
/**
1112
* @param {CallExpression} node
@@ -136,6 +137,45 @@ export function CallExpression(node, context) {
136137

137138
break;
138139

140+
case '$inspect.trace': {
141+
if (node.arguments.length > 1) {
142+
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
143+
}
144+
145+
const grand_parent = context.path.at(-2);
146+
const fn = context.path.at(-3);
147+
148+
if (
149+
parent.type !== 'ExpressionStatement' ||
150+
grand_parent?.type !== 'BlockStatement' ||
151+
!(
152+
fn?.type === 'FunctionDeclaration' ||
153+
fn?.type === 'FunctionExpression' ||
154+
fn?.type === 'ArrowFunctionExpression'
155+
) ||
156+
grand_parent.body[0] !== parent
157+
) {
158+
e.inspect_trace_invalid_placement(node);
159+
}
160+
161+
if (fn.generator) {
162+
e.inspect_trace_generator(node);
163+
}
164+
165+
if (dev) {
166+
if (node.arguments[0]) {
167+
context.state.scope.tracing = b.thunk(/** @type {Expression} */ (node.arguments[0]));
168+
} else {
169+
const label = get_function_label(context.path.slice(0, -2)) ?? 'trace';
170+
const loc = `(${locate_node(fn)})`;
171+
172+
context.state.scope.tracing = b.thunk(b.literal(label + ' ' + loc));
173+
}
174+
}
175+
176+
break;
177+
}
178+
139179
case '$state.snapshot':
140180
if (node.arguments.length !== 1) {
141181
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
@@ -182,3 +222,31 @@ export function CallExpression(node, context) {
182222
}
183223
}
184224
}
225+
226+
/**
227+
* @param {AST.SvelteNode[]} nodes
228+
*/
229+
function get_function_label(nodes) {
230+
const fn = /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ (
231+
nodes.at(-1)
232+
);
233+
234+
if ((fn.type === 'FunctionDeclaration' || fn.type === 'FunctionExpression') && fn.id != null) {
235+
return fn.id.name;
236+
}
237+
238+
const parent = nodes.at(-2);
239+
if (!parent) return;
240+
241+
if (parent.type === 'CallExpression') {
242+
return source.slice(parent.callee.start, parent.callee.end) + '(...)';
243+
}
244+
245+
if (parent.type === 'Property' && !parent.computed) {
246+
return /** @type {Identifier} */ (parent.key).name;
247+
}
248+
249+
if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
250+
return parent.id.name;
251+
}
252+
}

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
get_attribute_expression,
99
is_event_attribute
1010
} from '../../../../utils/ast.js';
11-
import { dev, filename, is_ignored, locator } from '../../../../state.js';
11+
import { dev, filename, is_ignored, locate_node, locator } from '../../../../state.js';
1212
import { build_proxy_reassignment, should_proxy } from '../utils.js';
1313
import { visit_assignment_expression } from '../../shared/assignments.js';
1414

@@ -183,9 +183,6 @@ function build_assignment(operator, left, right, context) {
183183
if (left.type === 'MemberExpression' && should_transform) {
184184
const callee = callees[operator];
185185

186-
const loc = /** @type {Location} */ (locator(/** @type {number} */ (left.start)));
187-
const location = `${filename}:${loc.line}:${loc.column}`;
188-
189186
return /** @type {Expression} */ (
190187
context.visit(
191188
b.call(
@@ -197,7 +194,7 @@ function build_assignment(operator, left, right, context) {
197194
: b.literal(/** @type {Identifier} */ (left.property).name)
198195
),
199196
right,
200-
b.literal(location)
197+
b.literal(locate_node(left))
201198
)
202199
)
203200
);
Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
1-
/** @import { BlockStatement } from 'estree' */
1+
/** @import { ArrowFunctionExpression, BlockStatement, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */
22
/** @import { ComponentContext } from '../types' */
33
import { add_state_transformers } from './shared/declarations.js';
4+
import * as b from '../../../../utils/builders.js';
45

56
/**
67
* @param {BlockStatement} node
78
* @param {ComponentContext} context
89
*/
910
export function BlockStatement(node, context) {
1011
add_state_transformers(context);
12+
const tracing = context.state.scope.tracing;
13+
14+
if (tracing !== null) {
15+
const parent =
16+
/** @type {ArrowFunctionExpression | FunctionDeclaration | FunctionExpression} */ (
17+
context.path.at(-1)
18+
);
19+
20+
const is_async = parent.async;
21+
22+
const call = b.call(
23+
'$.trace',
24+
/** @type {Expression} */ (tracing),
25+
b.thunk(b.block(node.body.map((n) => /** @type {Statement} */ (context.visit(n)))), is_async)
26+
);
27+
28+
return b.block([b.return(is_async ? b.await(call) : call)]);
29+
}
30+
1131
context.next();
1232
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export function ExpressionStatement(node, context) {
2020

2121
return b.stmt(expr);
2222
}
23+
24+
if (rune === '$inspect.trace') {
25+
return b.empty;
26+
}
2327
}
2428

2529
context.next();

0 commit comments

Comments
 (0)