Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/silly-penguins-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: make `$inspect` logs come from the callsite
9 changes: 2 additions & 7 deletions documentation/docs/02-runes/07-$inspect.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<input bind:value={message} />
```

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

## $inspect(...).with

`$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)):
Expand All @@ -36,13 +38,6 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<button onclick={() => count++}>Increment</button>
```

A convenient way to find the origin of some change is to pass `console.trace` to `with`:

```js
// @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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import { should_proxy } from '../utils.js';
import { get_inspect_args } from '../../utils.js';

/**
* @param {CallExpression} node
Expand Down Expand Up @@ -73,7 +73,7 @@ export function CallExpression(node, context) {

case '$inspect':
case '$inspect().with':
return transform_inspect_rune(node, context);
return transform_inspect_rune(rune, node, context);
}

if (
Expand Down Expand Up @@ -104,3 +104,21 @@ export function CallExpression(node, context) {

context.next();
}

/**
* @param {'$inspect' | '$inspect().with'} rune
* @param {CallExpression} node
* @param {Context} context
*/
function transform_inspect_rune(rune, node, context) {
if (!dev) return b.empty;

const { args, inspector } = get_inspect_args(rune, node, context.visit);

// by passing an arrow function, the log appears to come from the `$inspect` callsite
// rather than the `inspect.js` file containing the utility
const id = b.id('$$args');
const fn = b.arrow([b.rest(id)], b.call(inspector, b.spread(id)));

return b.call('$.inspect', b.thunk(b.array(args)), fn, rune === '$inspect' && b.true);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import { is_ignored } from '../../../../state.js';
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import { get_inspect_args } from '../../utils.js';

/**
* @param {CallExpression} node
Expand Down Expand Up @@ -51,7 +51,13 @@ export function CallExpression(node, context) {
}

if (rune === '$inspect' || rune === '$inspect().with') {
return transform_inspect_rune(node, context);
if (!dev) return b.empty;

const { args, inspector } = get_inspect_args(rune, node, context.visit);

return rune === '$inspect'
? b.call(inspector, b.literal('$inspect('), ...args, b.literal(')'))
: b.call(inspector, b.literal('init'), ...args);
}

context.next();
Expand Down
39 changes: 14 additions & 25 deletions packages/svelte/src/compiler/phases/3-transform/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @import { Context } from 'zimmerframe' */
/** @import { TransformState } from './types.js' */
/** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */
/** @import { Node, Expression, CallExpression } from 'estree' */
/** @import { Node, Expression, CallExpression, MemberExpression } from 'estree' */
import {
regex_ends_with_whitespaces,
regex_not_whitespace,
Expand Down Expand Up @@ -452,30 +452,19 @@ export function determine_namespace_for_children(node, namespace) {
}

/**
* @template {TransformState} T
* @param {'$inspect' | '$inspect().with'} rune
* @param {CallExpression} node
* @param {Context<any, T>} context
* @param {(node: AST.SvelteNode) => AST.SvelteNode} visit
*/
export function transform_inspect_rune(node, context) {
const { state, visit } = context;
const as_fn = state.options.generate === 'client';

if (!dev) return b.empty;

if (node.callee.type === 'MemberExpression') {
const raw_inspect_args = /** @type {CallExpression} */ (node.callee.object).arguments;
const inspect_args =
/** @type {Array<Expression>} */
(raw_inspect_args.map((arg) => visit(arg)));
const with_arg = /** @type {Expression} */ (visit(node.arguments[0]));

return b.call(
'$.inspect',
as_fn ? b.thunk(b.array(inspect_args)) : b.array(inspect_args),
with_arg
);
} else {
const arg = node.arguments.map((arg) => /** @type {Expression} */ (visit(arg)));
return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg));
}
export function get_inspect_args(rune, node, visit) {
const call =
rune === '$inspect'
? node
: /** @type {CallExpression} */ (/** @type {MemberExpression} */ (node.callee).object);

return {
args: call.arguments.map((arg) => /** @type {Expression} */ (visit(arg))),
inspector:
rune === '$inspect' ? 'console.log' : /** @type {Expression} */ (visit(node.arguments[0]))
};
}
18 changes: 14 additions & 4 deletions packages/svelte/src/internal/client/dev/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { inspect_effect, render_effect, validate_effect } from '../reactivity/effects.js';
import { untrack } from '../runtime.js';
import { get_stack } from './tracing.js';

/**
* @param {() => any[]} get_value
* @param {Function} [inspector]
* @param {Function} inspector
* @param {boolean} show_stack
*/
// eslint-disable-next-line no-console
export function inspect(get_value, inspector = console.log) {
export function inspect(get_value, inspector, show_stack = false) {
validate_effect('$inspect');

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

var snap = snapshot(value, true, true);
untrack(() => {
inspector(initial ? 'init' : 'update', ...snap);
if (show_stack) {
inspector(...snap);

if (!initial) {
// eslint-disable-next-line no-console
console.log(get_stack('UpdatedAt'));
}
} else {
inspector(initial ? 'init' : 'update', ...snap);
}
});

initial = false;
Expand Down
11 changes: 10 additions & 1 deletion packages/svelte/src/internal/client/dev/tracing.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,16 @@ export function trace(label, fn) {
* @returns {Error & { stack: string } | null}
*/
export function get_stack(label) {
// @ts-ignore stackTraceLimit doesn't exist everywhere
const limit = Error.stackTraceLimit;

// @ts-ignore
Error.stackTraceLimit = Infinity;
let error = Error();

// @ts-ignore
Error.stackTraceLimit = limit;

const stack = error.stack;

if (!stack) return null;
Expand All @@ -151,7 +160,7 @@ export function get_stack(label) {
if (line.includes('validate_each_keys')) {
return null;
}
if (line.includes('svelte/src/internal')) {
if (line.includes('svelte/src/internal') || line.includes('svelte\\src\\internal')) {
continue;
}
new_lines.push(line);
Expand Down
9 changes: 0 additions & 9 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,15 +418,6 @@ export function ensure_array_like(array_like_or_iterator) {
return [];
}

/**
* @param {any[]} args
* @param {Function} [inspect]
*/
// eslint-disable-next-line no-console
export function inspect(args, inspect = console.log) {
inspect('init', ...args);
}

/**
* @template V
* @param {() => V} get_value
Expand Down
20 changes: 20 additions & 0 deletions packages/svelte/tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,26 @@ export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS)

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

/**
* @param {any[]} logs
*/
export function normalise_inspect_logs(logs) {
return logs.map((log) => {
if (log instanceof Error) {
const last_line = log.stack
?.trim()
.split('\n')
.filter((line) => !line.includes('at Module.get_stack'))[1];

const match = last_line && /(at .+) /.exec(last_line);

return match && match[1];
}

return log;
});
}

/**
* @param {any[]} logs
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export default test({
dev: true
},
async test({ assert, logs }) {
assert.deepEqual(logs, ['init', 1, 'init', 1]);
assert.deepEqual(logs, [1, 1]);
}
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { normalise_inspect_logs } from '../../../helpers.js';

export default test({
compileOptions: {
Expand All @@ -13,6 +14,6 @@ export default test({
button?.click();
});

assert.deepEqual(logs, ['init', [1, 2, 3, 7], 'update', [2, 3, 7]]);
assert.deepEqual(normalise_inspect_logs(logs), [[1, 2, 3, 7], [2, 3, 7], 'at Object.doSplice']);
}
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';

export default test({
Expand All @@ -6,6 +7,6 @@ export default test({
},

async test({ assert, logs }) {
assert.deepEqual(logs, ['init', undefined, 'update', [{}]]);
assert.deepEqual(normalise_inspect_logs(logs), [undefined, [{}], 'at $effect']);
}
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { normalise_inspect_logs } from '../../../helpers.js';

export default test({
compileOptions: {
Expand All @@ -14,23 +15,22 @@ export default test({
});

assert.htmlEqual(target.innerHTML, `<button>update</button>\n1`);
assert.deepEqual(logs, [
'init',
assert.deepEqual(normalise_inspect_logs(logs), [
{
data: {
derived: 0,
list: []
},
derived: []
},
'update',
{
data: {
derived: 0,
list: [1]
},
derived: [1]
}
},
'at HTMLButtonElement.Main.button.__click'
]);
}
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { normalise_inspect_logs } from '../../../helpers.js';

export default test({
compileOptions: {
Expand All @@ -13,22 +14,19 @@ export default test({
button?.click();
});

assert.deepEqual(logs, [
'init',
assert.deepEqual(normalise_inspect_logs(logs), [
'0',
true,
'init',
'1',
false,
'init',
'2',
false,
'update',
'0',
false,
'update',
'at $effect',
'1',
true
true,
'at $effect'
]);
}
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { normalise_inspect_logs } from '../../../helpers.js';

export default test({
compileOptions: {
Expand All @@ -12,15 +13,13 @@ export default test({
btn2.click();
flushSync();

assert.deepEqual(logs, [
'init',
assert.deepEqual(normalise_inspect_logs(logs), [
new Map(),
'init',
new Set(),
'update',
new Map([['a', 'a']]),
'update',
new Set(['a'])
'at SvelteMap.set',
new Set(['a']),
'at SvelteSet.add'
]);
}
});
Loading
Loading