diff --git a/.changeset/silly-penguins-sleep.md b/.changeset/silly-penguins-sleep.md
new file mode 100644
index 000000000000..f397f1e8ba17
--- /dev/null
+++ b/.changeset/silly-penguins-sleep.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: make `$inspect` logs come from the callsite
diff --git a/documentation/docs/02-runes/07-$inspect.md b/documentation/docs/02-runes/07-$inspect.md
index 13ac8b79a33a..6d47e30e2735 100644
--- a/documentation/docs/02-runes/07-$inspect.md
+++ b/documentation/docs/02-runes/07-$inspect.md
@@ -18,6 +18,8 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
```
+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)):
@@ -36,13 +38,6 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
```
-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.
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
index bf9a09bb74b1..ae60f3be4070 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
@@ -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
@@ -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 (
@@ -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);
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
index d53b631aa5ec..bba6511eec8d 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
@@ -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
@@ -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();
diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js
index 1445ce3aa6b1..dfc2ab1de140 100644
--- a/packages/svelte/src/compiler/phases/3-transform/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/utils.js
@@ -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,
@@ -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} 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} */
- (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]))
+ };
}
diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js
index f79cf472991a..db7ab0d976b7 100644
--- a/packages/svelte/src/internal/client/dev/inspect.js
+++ b/packages/svelte/src/internal/client/dev/inspect.js
@@ -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;
@@ -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;
diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js
index 673a710faca6..95baefc64aa3 100644
--- a/packages/svelte/src/internal/client/dev/tracing.js
+++ b/packages/svelte/src/internal/client/dev/tracing.js
@@ -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;
@@ -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);
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 50bb629c4d5a..74a90a8600f5 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -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
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 7a9640636c40..bf708878a325 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -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
*/
diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-fields-reassigned-this/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-fields-reassigned-this/_config.js
index 88b806c0f085..0e6bd73220e2 100644
--- a/packages/svelte/tests/runtime-runes/samples/class-private-fields-reassigned-this/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-reassigned-this/_config.js
@@ -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]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js
index 49f1b5de4170..1b331f5b4014 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js
@@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
+import { normalise_inspect_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@@ -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']);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-deep/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-deep/_config.js
index f7480b0e7bbb..89b01da499fb 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-deep/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-deep/_config.js
@@ -1,3 +1,4 @@
+import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@@ -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']);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js
index 9474397f7f45..374238275902 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js
@@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
+import { normalise_inspect_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@@ -14,8 +15,7 @@ export default test({
});
assert.htmlEqual(target.innerHTML, `\n1`);
- assert.deepEqual(logs, [
- 'init',
+ assert.deepEqual(normalise_inspect_logs(logs), [
{
data: {
derived: 0,
@@ -23,14 +23,14 @@ export default test({
},
derived: []
},
- 'update',
{
data: {
derived: 0,
list: [1]
},
derived: [1]
- }
+ },
+ 'at HTMLButtonElement.Main.button.__click'
]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-3/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-derived-3/_config.js
index d2226f433ea0..017de6c0c72d 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-derived-3/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-3/_config.js
@@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
+import { normalise_inspect_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@@ -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'
]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-map-set/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-map-set/_config.js
index 2052cb7f135d..2f91e842880d 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-map-set/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-map-set/_config.js
@@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
+import { normalise_inspect_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@@ -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'
]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js
index fc9a0cda9a74..6886f5e53e6e 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js
@@ -1,3 +1,4 @@
+import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@@ -11,6 +12,15 @@ export default test({
b2.click();
await Promise.resolve();
- assert.deepEqual(logs, ['init', 0, 0, 'update', 1, 0, 'update', 1, 1]);
+ assert.deepEqual(normalise_inspect_logs(logs), [
+ 0,
+ 0,
+ 1,
+ 0,
+ 'at HTMLButtonElement.',
+ 1,
+ 1,
+ 'at HTMLButtonElement.'
+ ]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-nested-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-nested-effect/_config.js
index 82429e5e361b..86e65d50443f 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-nested-effect/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-nested-effect/_config.js
@@ -1,3 +1,4 @@
+import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@@ -6,6 +7,6 @@ export default test({
},
async test({ assert, logs }) {
- assert.deepEqual(logs, ['init', 0, 'update', 1]);
+ assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at $effect']);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js
index e4d9fb501378..34cd74d780b2 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js
@@ -1,3 +1,4 @@
+import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@@ -10,13 +11,12 @@ export default test({
b1.click();
await Promise.resolve();
- assert.deepEqual(logs, [
- 'init',
+ assert.deepEqual(normalise_inspect_logs(logs), [
{ x: { count: 0 } },
[{ count: 0 }],
- 'update',
{ x: { count: 1 } },
- [{ count: 1 }]
+ [{ count: 1 }],
+ 'at HTMLButtonElement.'
]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js
index a85972a0f990..8134044b16fc 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js
@@ -1,3 +1,4 @@
+import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@@ -10,6 +11,13 @@ export default test({
btn.click();
await Promise.resolve();
- assert.deepEqual(logs, ['init', {}, 'init', [], 'update', { x: 'hello' }, 'update', ['hello']]);
+ assert.deepEqual(normalise_inspect_logs(logs), [
+ {},
+ [],
+ { x: 'hello' },
+ 'at HTMLButtonElement.on_click',
+ ['hello'],
+ 'at HTMLButtonElement.on_click'
+ ]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/_config.js
index ab496971955f..1bfc2dc68fc0 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/_config.js
@@ -18,6 +18,6 @@ export default test({
};
b.a.b = b;
- assert.deepEqual(logs, ['init', a, 'init', b]);
+ assert.deepEqual(logs, [a, b]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js
index e35917b1f3c5..9d95956e7d06 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js
@@ -1,3 +1,4 @@
+import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@@ -11,6 +12,12 @@ export default test({
btn.click();
await Promise.resolve();
- assert.deepEqual(logs, ['init', [], 'update', [{}], 'update', [{}, {}]]);
+ assert.deepEqual(normalise_inspect_logs(logs), [
+ [],
+ [{}],
+ 'at HTMLButtonElement.on_click',
+ [{}, {}],
+ 'at HTMLButtonElement.on_click'
+ ]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect/_config.js
index 09a921abee06..c05c4b15c4f1 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect/_config.js
@@ -1,3 +1,4 @@
+import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@@ -11,6 +12,6 @@ export default test({
b2.click();
await Promise.resolve();
- assert.deepEqual(logs, ['init', 0, 'update', 1]);
+ assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at HTMLButtonElement.']);
}
});