Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add $inspect.trace rune #14290

Merged
merged 59 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
8e320fe
feat: add $trace rune
trueadm Nov 12, 2024
863ee8f
lint
trueadm Nov 13, 2024
3084911
fix
trueadm Nov 13, 2024
0c584a0
fix
trueadm Nov 13, 2024
1e8fe64
fix
trueadm Nov 13, 2024
b7f485d
fix
trueadm Nov 13, 2024
0983978
fix
trueadm Nov 13, 2024
7322128
fix
trueadm Nov 13, 2024
0c8575f
fix
trueadm Nov 13, 2024
3470d4d
more tweaks
trueadm Nov 13, 2024
602a919
lint
trueadm Nov 13, 2024
e35334c
improve label for derived cached
trueadm Nov 13, 2024
4735d64
improve label for derived cached
trueadm Nov 13, 2024
e88150f
lint
trueadm Nov 13, 2024
87d5bc2
better stacks
trueadm Nov 13, 2024
554cd74
complete redesign
trueadm Nov 14, 2024
e8997c3
fixes
trueadm Nov 14, 2024
6ac0779
dead code
trueadm Nov 14, 2024
ffab346
dead code
trueadm Nov 14, 2024
0c97306
improve change detection
trueadm Nov 14, 2024
f6ebd00
rename rune
trueadm Nov 14, 2024
66482c4
lint
trueadm Nov 14, 2024
9589f53
lint
trueadm Nov 14, 2024
8e33cd9
Merge branch 'main' into trace-rune
trueadm Nov 16, 2024
dad777a
fix bug
trueadm Nov 16, 2024
3393409
tweaks
Rich-Harris Nov 21, 2024
55b7549
Update packages/svelte/src/internal/client/dev/tracing.js
trueadm Nov 21, 2024
8c103b2
Update packages/svelte/src/internal/client/dev/tracing.js
trueadm Nov 21, 2024
3da9dac
Update packages/svelte/src/internal/client/dev/tracing.js
trueadm Nov 21, 2024
a078e25
Update packages/svelte/src/internal/client/dev/tracing.js
trueadm Nov 21, 2024
82e6cb9
add labels, remove grouping
Rich-Harris Nov 21, 2024
4d1bd54
conflicts
trueadm Dec 5, 2024
f8eee24
todos
trueadm Dec 5, 2024
74ceb6d
add test + some docs
trueadm Dec 5, 2024
c3428fc
changeset
trueadm Dec 5, 2024
e69c992
merge main
Rich-Harris Dec 14, 2024
9b69a3b
update messages
Rich-Harris Dec 14, 2024
1cc6bc8
address feedback
trueadm Dec 14, 2024
fa941f7
address feedback
trueadm Dec 14, 2024
c02bab8
limit to first statement of function
trueadm Dec 14, 2024
8461ca9
remove unreachable trace_rune_duplicate error
Rich-Harris Dec 14, 2024
464b22b
tweak message
Rich-Harris Dec 14, 2024
58ee498
remove the expression statement, not the expression
Rich-Harris Dec 14, 2024
457619f
revert
Rich-Harris Dec 15, 2024
7778e99
make label optional
Rich-Harris Dec 15, 2024
28e05ed
relax restriction on label - no longer necessary with new design
Rich-Harris Dec 15, 2024
b423b5c
update errors
Rich-Harris Dec 15, 2024
fd49190
newline
Rich-Harris Dec 15, 2024
d42cade
tweak
Rich-Harris Dec 15, 2024
3900db0
add some docs
Rich-Harris Dec 15, 2024
9c3db38
fix playground
trueadm Dec 15, 2024
17c223f
fix playground
trueadm Dec 15, 2024
702c704
tweak message when function runs outside an effect
Rich-Harris Dec 15, 2024
77f90e5
unused
Rich-Harris Dec 15, 2024
e6bb46a
tweak
Rich-Harris Dec 15, 2024
d92bd8a
handle async functions
Rich-Harris Dec 15, 2024
4cb3335
fail on generators
Rich-Harris Dec 15, 2024
86eab1c
regenerate, update docs
Rich-Harris Dec 15, 2024
ae3d8a1
better labelling
Rich-Harris Dec 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tame-ants-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: adds $inspect.trace rune
17 changes: 17 additions & 0 deletions documentation/docs/02-runes/07-$inspect.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,20 @@ A convenient way to find the origin of some change is to pass `console.trace` to
// @errors: 2304
$inspect(stuff).with(console.trace);
```

## $inspect.trace(...)

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.

```svelte
<script>
import { doSomeWork } from './elsewhere';

$effect(() => {
+++$inspect.trace();+++
doSomeWork();
});
</script>
```

`$inspect.trace` takes an optional first argument which will be used as the label.
12 changes: 12 additions & 0 deletions documentation/docs/98-reference/.generated/compile-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,18 @@ Expected whitespace
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
```

### inspect_trace_generator

```
`$inspect.trace(...)` cannot be used inside a generator function
```

### inspect_trace_invalid_placement

```
`$inspect.trace(...)` must be the first statement of a function body
```

### invalid_arguments_usage

```
Expand Down
8 changes: 8 additions & 0 deletions packages/svelte/messages/compile-errors/script.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@

> 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

## inspect_trace_generator

> `$inspect.trace(...)` cannot be used inside a generator function

## inspect_trace_invalid_placement

> `$inspect.trace(...)` must be the first statement of a function body

## invalid_arguments_usage

> The arguments keyword cannot be used within the template or at the top level of a component
Expand Down
19 changes: 19 additions & 0 deletions packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,25 @@ declare function $inspect<T extends any[]>(
...values: T
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };

declare namespace $inspect {
/**
* Tracks which reactive state changes caused an effect to re-run. Must be the first
* statement of a function body. Example:
*
* ```svelte
* <script>
* let count = $state(0);
*
* $effect(() => {
* $inspect.trace('my effect');
*
* count;
* });
* </script>
*/
export function trace(name: string): void;
}

/**
* Retrieves the `this` reference of the custom element that contains this component. Example:
*
Expand Down
18 changes: 18 additions & 0 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,24 @@ export function import_svelte_internal_forbidden(node) {
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`);
}

/**
* `$inspect.trace(...)` cannot be used inside a generator function
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function inspect_trace_generator(node) {
e(node, "inspect_trace_generator", `\`$inspect.trace(...)\` cannot be used inside a generator function\nhttps://svelte.dev/e/inspect_trace_generator`);
}

/**
* `$inspect.trace(...)` must be the first statement of a function body
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function inspect_trace_invalid_placement(node) {
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`);
}

/**
* The arguments keyword cannot be used within the template or at the top level of a component
* @param {null | number | NodeLike} node
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/** @import { CallExpression, VariableDeclarator } from 'estree' */
/** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, VariableDeclarator } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
import { get_parent, unwrap_optional } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { dev, locate_node, source } from '../../../state.js';
import * as b from '../../../utils/builders.js';

/**
* @param {CallExpression} node
Expand Down Expand Up @@ -136,6 +137,45 @@ export function CallExpression(node, context) {

break;

case '$inspect.trace': {
if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}

const grand_parent = context.path.at(-2);
const fn = context.path.at(-3);

if (
parent.type !== 'ExpressionStatement' ||
grand_parent?.type !== 'BlockStatement' ||
!(
fn?.type === 'FunctionDeclaration' ||
fn?.type === 'FunctionExpression' ||
fn?.type === 'ArrowFunctionExpression'
) ||
grand_parent.body[0] !== parent
) {
e.inspect_trace_invalid_placement(node);
}

if (fn.generator) {
e.inspect_trace_generator(node);
}

if (dev) {
if (node.arguments[0]) {
context.state.scope.tracing = b.thunk(/** @type {Expression} */ (node.arguments[0]));
} else {
const label = get_function_label(context.path.slice(0, -2)) ?? 'trace';
const loc = `(${locate_node(fn)})`;

context.state.scope.tracing = b.thunk(b.literal(label + ' ' + loc));
}
}

break;
}

case '$state.snapshot':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
Expand Down Expand Up @@ -182,3 +222,31 @@ export function CallExpression(node, context) {
}
}
}

/**
* @param {AST.SvelteNode[]} nodes
*/
function get_function_label(nodes) {
const fn = /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ (
nodes.at(-1)
);

if ((fn.type === 'FunctionDeclaration' || fn.type === 'FunctionExpression') && fn.id != null) {
return fn.id.name;
}

const parent = nodes.at(-2);
if (!parent) return;

if (parent.type === 'CallExpression') {
return source.slice(parent.callee.start, parent.callee.end) + '(...)';
}

if (parent.type === 'Property' && !parent.computed) {
return /** @type {Identifier} */ (parent.key).name;
}

if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
return parent.id.name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
get_attribute_expression,
is_event_attribute
} from '../../../../utils/ast.js';
import { dev, filename, is_ignored, locator } from '../../../../state.js';
import { dev, filename, is_ignored, locate_node, locator } from '../../../../state.js';
import { build_proxy_reassignment, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';

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

const loc = /** @type {Location} */ (locator(/** @type {number} */ (left.start)));
const location = `${filename}:${loc.line}:${loc.column}`;

return /** @type {Expression} */ (
context.visit(
b.call(
Expand All @@ -197,7 +194,7 @@ function build_assignment(operator, left, right, context) {
: b.literal(/** @type {Identifier} */ (left.property).name)
),
right,
b.literal(location)
b.literal(locate_node(left))
)
)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
/** @import { BlockStatement } from 'estree' */
/** @import { ArrowFunctionExpression, BlockStatement, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { add_state_transformers } from './shared/declarations.js';
import * as b from '../../../../utils/builders.js';

/**
* @param {BlockStatement} node
* @param {ComponentContext} context
*/
export function BlockStatement(node, context) {
add_state_transformers(context);
const tracing = context.state.scope.tracing;

if (tracing !== null) {
const parent =
/** @type {ArrowFunctionExpression | FunctionDeclaration | FunctionExpression} */ (
context.path.at(-1)
);

const is_async = parent.async;

const call = b.call(
'$.trace',
/** @type {Expression} */ (tracing),
b.thunk(b.block(node.body.map((n) => /** @type {Statement} */ (context.visit(n)))), is_async)
);

return b.block([b.return(is_async ? b.await(call) : call)]);
}

context.next();
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export function ExpressionStatement(node, context) {

return b.stmt(expr);
}

if (rune === '$inspect.trace') {
return b.empty;
}
}

context.next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function VariableDeclaration(node, context) {
rune === '$effect.tracking' ||
rune === '$effect.root' ||
rune === '$inspect' ||
rune === '$inspect.trace' ||
rune === '$state.snapshot' ||
rune === '$host'
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { get_rune } from '../../../scope.js';
export function ExpressionStatement(node, context) {
const rune = get_rune(node.expression, context.state.scope);

if (rune === '$effect' || rune === '$effect.pre' || rune === '$effect.root') {
if (
rune === '$effect' ||
rune === '$effect.pre' ||
rune === '$effect.root' ||
rune === '$inspect.trace'
) {
return b.empty;
}

Expand Down
6 changes: 6 additions & 0 deletions packages/svelte/src/compiler/phases/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export class Scope {
*/
function_depth = 0;

/**
* If tracing of reactive dependencies is enabled for this scope
* @type {null | Expression}
*/
tracing = null;

/**
*
* @param {ScopeRoot} root
Expand Down
10 changes: 10 additions & 0 deletions packages/svelte/src/compiler/state.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** @import { Location } from 'locate-character' */
/** @import { CompileOptions } from './types' */
/** @import { AST, Warning } from '#compiler' */
import { getLocator } from 'locate-character';
import { sanitize_location } from '../utils.js';

/** @typedef {{ start?: number, end?: number }} NodeLike */

Expand Down Expand Up @@ -28,6 +30,14 @@ export let dev;

export let locator = getLocator('', { offsetLine: 1 });

/**
* @param {AST.SvelteNode & { start?: number | undefined }} node
*/
export function locate_node(node) {
const loc = /** @type {Location} */ (locator(/** @type {number} */ (node.start)));
return `${sanitize_location(filename)}:${loc?.line}:${loc.column}`;
}

/** @type {NonNullable<CompileOptions['warningFilter']>} */
export let warning_filter;

Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/dev/assign.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sanitize_location } from '../../../utils.js';
import { untrack } from '../runtime.js';
import * as w from '../warnings.js';
import { sanitize_location } from './location.js';

/**
*
Expand Down
25 changes: 0 additions & 25 deletions packages/svelte/src/internal/client/dev/location.js

This file was deleted.

Loading
Loading