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.']); } });