Skip to content

Commit 4b32d6d

Browse files
authored
fix: make $inspect logs come from the callsite (#17001)
* fix: make `$inspect` logs come from the callsite * default to showing stack trace * reuse id * DRY out, improve server-side inspect * update docs * ugh * fix the dang tests * ugh windows is there no punch bowl you won't poop in? * argh * how about this * alright finally
1 parent 5bb28c4 commit 4b32d6d

File tree

22 files changed

+152
-83
lines changed

22 files changed

+152
-83
lines changed

.changeset/silly-penguins-sleep.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+
fix: make `$inspect` logs come from the callsite

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
1818
<input bind:value={message} />
1919
```
2020

21+
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
22+
2123
## $inspect(...).with
2224

2325
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` ([demo](/playground/untitled#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA)):
@@ -36,13 +38,6 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
3638
<button onclick={() => count++}>Increment</button>
3739
```
3840

39-
A convenient way to find the origin of some change is to pass `console.trace` to `with`:
40-
41-
```js
42-
// @errors: 2304
43-
$inspect(stuff).with(console.trace);
44-
```
45-
4641
## $inspect.trace(...)
4742

4843
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.

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
/** @import { CallExpression, Expression } from 'estree' */
1+
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
22
/** @import { Context } from '../types' */
33
import { dev, is_ignored } from '../../../../state.js';
44
import * as b from '#compiler/builders';
55
import { get_rune } from '../../../scope.js';
6-
import { transform_inspect_rune } from '../../utils.js';
76
import { should_proxy } from '../utils.js';
7+
import { get_inspect_args } from '../../utils.js';
88

99
/**
1010
* @param {CallExpression} node
@@ -73,7 +73,7 @@ export function CallExpression(node, context) {
7373

7474
case '$inspect':
7575
case '$inspect().with':
76-
return transform_inspect_rune(node, context);
76+
return transform_inspect_rune(rune, node, context);
7777
}
7878

7979
if (
@@ -104,3 +104,21 @@ export function CallExpression(node, context) {
104104

105105
context.next();
106106
}
107+
108+
/**
109+
* @param {'$inspect' | '$inspect().with'} rune
110+
* @param {CallExpression} node
111+
* @param {Context} context
112+
*/
113+
function transform_inspect_rune(rune, node, context) {
114+
if (!dev) return b.empty;
115+
116+
const { args, inspector } = get_inspect_args(rune, node, context.visit);
117+
118+
// by passing an arrow function, the log appears to come from the `$inspect` callsite
119+
// rather than the `inspect.js` file containing the utility
120+
const id = b.id('$$args');
121+
const fn = b.arrow([b.rest(id)], b.call(inspector, b.spread(id)));
122+
123+
return b.call('$.inspect', b.thunk(b.array(args)), fn, rune === '$inspect' && b.true);
124+
}

packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
/** @import { CallExpression, Expression } from 'estree' */
1+
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
22
/** @import { Context } from '../types.js' */
3-
import { is_ignored } from '../../../../state.js';
3+
import { dev, is_ignored } from '../../../../state.js';
44
import * as b from '#compiler/builders';
55
import { get_rune } from '../../../scope.js';
6-
import { transform_inspect_rune } from '../../utils.js';
6+
import { get_inspect_args } from '../../utils.js';
77

88
/**
99
* @param {CallExpression} node
@@ -51,7 +51,13 @@ export function CallExpression(node, context) {
5151
}
5252

5353
if (rune === '$inspect' || rune === '$inspect().with') {
54-
return transform_inspect_rune(node, context);
54+
if (!dev) return b.empty;
55+
56+
const { args, inspector } = get_inspect_args(rune, node, context.visit);
57+
58+
return rune === '$inspect'
59+
? b.call(inspector, b.literal('$inspect('), ...args, b.literal(')'))
60+
: b.call(inspector, b.literal('init'), ...args);
5561
}
5662

5763
context.next();

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

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { Context } from 'zimmerframe' */
22
/** @import { TransformState } from './types.js' */
33
/** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */
4-
/** @import { Node, Expression, CallExpression } from 'estree' */
4+
/** @import { Node, Expression, CallExpression, MemberExpression } from 'estree' */
55
import {
66
regex_ends_with_whitespaces,
77
regex_not_whitespace,
@@ -452,30 +452,19 @@ export function determine_namespace_for_children(node, namespace) {
452452
}
453453

454454
/**
455-
* @template {TransformState} T
455+
* @param {'$inspect' | '$inspect().with'} rune
456456
* @param {CallExpression} node
457-
* @param {Context<any, T>} context
457+
* @param {(node: AST.SvelteNode) => AST.SvelteNode} visit
458458
*/
459-
export function transform_inspect_rune(node, context) {
460-
const { state, visit } = context;
461-
const as_fn = state.options.generate === 'client';
462-
463-
if (!dev) return b.empty;
464-
465-
if (node.callee.type === 'MemberExpression') {
466-
const raw_inspect_args = /** @type {CallExpression} */ (node.callee.object).arguments;
467-
const inspect_args =
468-
/** @type {Array<Expression>} */
469-
(raw_inspect_args.map((arg) => visit(arg)));
470-
const with_arg = /** @type {Expression} */ (visit(node.arguments[0]));
471-
472-
return b.call(
473-
'$.inspect',
474-
as_fn ? b.thunk(b.array(inspect_args)) : b.array(inspect_args),
475-
with_arg
476-
);
477-
} else {
478-
const arg = node.arguments.map((arg) => /** @type {Expression} */ (visit(arg)));
479-
return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg));
480-
}
459+
export function get_inspect_args(rune, node, visit) {
460+
const call =
461+
rune === '$inspect'
462+
? node
463+
: /** @type {CallExpression} */ (/** @type {MemberExpression} */ (node.callee).object);
464+
465+
return {
466+
args: call.arguments.map((arg) => /** @type {Expression} */ (visit(arg))),
467+
inspector:
468+
rune === '$inspect' ? 'console.log' : /** @type {Expression} */ (visit(node.arguments[0]))
469+
};
481470
}

packages/svelte/src/internal/client/dev/inspect.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { UNINITIALIZED } from '../../../constants.js';
22
import { snapshot } from '../../shared/clone.js';
33
import { inspect_effect, render_effect, validate_effect } from '../reactivity/effects.js';
44
import { untrack } from '../runtime.js';
5+
import { get_stack } from './tracing.js';
56

67
/**
78
* @param {() => any[]} get_value
8-
* @param {Function} [inspector]
9+
* @param {Function} inspector
10+
* @param {boolean} show_stack
911
*/
10-
// eslint-disable-next-line no-console
11-
export function inspect(get_value, inspector = console.log) {
12+
export function inspect(get_value, inspector, show_stack = false) {
1213
validate_effect('$inspect');
1314

1415
let initial = true;
@@ -28,7 +29,16 @@ export function inspect(get_value, inspector = console.log) {
2829

2930
var snap = snapshot(value, true, true);
3031
untrack(() => {
31-
inspector(initial ? 'init' : 'update', ...snap);
32+
if (show_stack) {
33+
inspector(...snap);
34+
35+
if (!initial) {
36+
// eslint-disable-next-line no-console
37+
console.log(get_stack('UpdatedAt'));
38+
}
39+
} else {
40+
inspector(initial ? 'init' : 'update', ...snap);
41+
}
3242
});
3343

3444
initial = false;

packages/svelte/src/internal/client/dev/tracing.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,16 @@ export function trace(label, fn) {
134134
* @returns {Error & { stack: string } | null}
135135
*/
136136
export function get_stack(label) {
137+
// @ts-ignore stackTraceLimit doesn't exist everywhere
138+
const limit = Error.stackTraceLimit;
139+
140+
// @ts-ignore
141+
Error.stackTraceLimit = Infinity;
137142
let error = Error();
143+
144+
// @ts-ignore
145+
Error.stackTraceLimit = limit;
146+
138147
const stack = error.stack;
139148

140149
if (!stack) return null;
@@ -151,7 +160,7 @@ export function get_stack(label) {
151160
if (line.includes('validate_each_keys')) {
152161
return null;
153162
}
154-
if (line.includes('svelte/src/internal')) {
163+
if (line.includes('svelte/src/internal') || line.includes('svelte\\src\\internal')) {
155164
continue;
156165
}
157166
new_lines.push(line);

packages/svelte/src/internal/server/index.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -418,15 +418,6 @@ export function ensure_array_like(array_like_or_iterator) {
418418
return [];
419419
}
420420

421-
/**
422-
* @param {any[]} args
423-
* @param {Function} [inspect]
424-
*/
425-
// eslint-disable-next-line no-console
426-
export function inspect(args, inspect = console.log) {
427-
inspect('init', ...args);
428-
}
429-
430421
/**
431422
* @template V
432423
* @param {() => V} get_value

packages/svelte/tests/helpers.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,26 @@ export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS)
197197

198198
export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true';
199199

200+
/**
201+
* @param {any[]} logs
202+
*/
203+
export function normalise_inspect_logs(logs) {
204+
return logs.map((log) => {
205+
if (log instanceof Error) {
206+
const last_line = log.stack
207+
?.trim()
208+
.split('\n')
209+
.filter((line) => !line.includes('at Module.get_stack'))[1];
210+
211+
const match = last_line && /(at .+) /.exec(last_line);
212+
213+
return match && match[1];
214+
}
215+
216+
return log;
217+
});
218+
}
219+
200220
/**
201221
* @param {any[]} logs
202222
*/

packages/svelte/tests/runtime-runes/samples/class-private-fields-reassigned-this/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ export default test({
55
dev: true
66
},
77
async test({ assert, logs }) {
8-
assert.deepEqual(logs, ['init', 1, 'init', 1]);
8+
assert.deepEqual(logs, [1, 1]);
99
}
1010
});

0 commit comments

Comments
 (0)