Skip to content

Commit df5105e

Browse files
authored
breaking: change $inspect API (#9838)
* breaking: change `$inspect` API `$inspect` now takes 1-n arguments, and inspections modification happens through `.with(..)` closes #9737 * lint
1 parent 26c6d6f commit df5105e

File tree

18 files changed

+155
-62
lines changed

18 files changed

+155
-62
lines changed

.changeset/wise-donkeys-marry.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+
breaking: change `$inspect` API

packages/svelte/src/compiler/errors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ const runes = {
190190
'invalid-derived-binding': () => `Invalid binding to derived state`,
191191
/**
192192
* @param {string} rune
193-
* @param {number[]} args
193+
* @param {Array<number | string>} args
194194
*/
195195
'invalid-rune-args-length': (rune, args) =>
196196
`${rune} can only be called with ${list(args, 'or')} ${

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -711,8 +711,14 @@ function validate_call_expression(node, scope, path) {
711711
}
712712

713713
if (rune === '$inspect') {
714-
if (node.arguments.length < 1 || node.arguments.length > 2) {
715-
error(node, 'invalid-rune-args-length', rune, [1, 2]);
714+
if (node.arguments.length < 1) {
715+
error(node, 'invalid-rune-args-length', rune, [1, 'more']);
716+
}
717+
}
718+
719+
if (rune === '$inspect().with') {
720+
if (node.arguments.length !== 1) {
721+
error(node, 'invalid-rune-args-length', rune, [1]);
716722
}
717723
}
718724
}

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

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { get_rune } from '../../../scope.js';
2-
import { is_hoistable_function } from '../../utils.js';
2+
import { is_hoistable_function, transform_inspect_rune } from '../../utils.js';
33
import * as b from '../../../../utils/builders.js';
44
import * as assert from '../../../../utils/assert.js';
55
import { create_state_declarators, get_prop_source, should_proxy } from '../utils.js';
@@ -301,33 +301,24 @@ export const javascript_visitors_runes = {
301301

302302
context.next();
303303
},
304-
CallExpression(node, { state, next, visit }) {
305-
const rune = get_rune(node, state.scope);
304+
CallExpression(node, context) {
305+
const rune = get_rune(node, context.state.scope);
306306

307307
if (rune === '$effect.active') {
308308
return b.call('$.effect_active');
309309
}
310310

311311
if (rune === '$effect.root') {
312312
const args = /** @type {import('estree').Expression[]} */ (
313-
node.arguments.map((arg) => visit(arg))
313+
node.arguments.map((arg) => context.visit(arg))
314314
);
315315
return b.call('$.user_root_effect', ...args);
316316
}
317317

318-
if (rune === '$inspect') {
319-
if (state.options.dev) {
320-
const arg = /** @type {import('estree').Expression} */ (visit(node.arguments[0]));
321-
const fn =
322-
node.arguments[1] &&
323-
/** @type {import('estree').Expression} */ (visit(node.arguments[1]));
324-
325-
return b.call('$.inspect', b.thunk(arg), fn);
326-
}
327-
328-
return b.unary('void', b.literal(0));
318+
if (rune === '$inspect' || rune === '$inspect().with') {
319+
return transform_inspect_rune(node, context);
329320
}
330321

331-
next();
322+
context.next();
332323
}
333324
};

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

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
clean_nodes,
1818
determine_element_namespace,
1919
escape_html,
20-
infer_namespace
20+
infer_namespace,
21+
transform_inspect_rune
2122
} from '../utils.js';
2223
import { create_attribute, is_custom_element_node, is_element_node } from '../../nodes.js';
2324
import { error } from '../../../errors.js';
@@ -630,26 +631,18 @@ const javascript_visitors_runes = {
630631
}
631632
context.next();
632633
},
633-
CallExpression(node, { state, next, visit }) {
634-
const rune = get_rune(node, state.scope);
634+
CallExpression(node, context) {
635+
const rune = get_rune(node, context.state.scope);
635636

636637
if (rune === '$effect.active') {
637638
return b.literal(false);
638639
}
639640

640-
if (rune === '$inspect') {
641-
if (state.options.dev) {
642-
const args = /** @type {import('estree').Expression[]} */ (
643-
node.arguments.map((arg) => visit(arg))
644-
);
645-
646-
return b.call('console.log', ...args);
647-
}
648-
649-
return b.unary('void', b.literal(0));
641+
if (rune === '$inspect' || rune === '$inspect().with') {
642+
return transform_inspect_rune(node, context);
650643
}
651644

652-
next();
645+
context.next();
653646
}
654647
};
655648

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,35 @@ export function determine_element_namespace(node, namespace, path) {
271271

272272
return namespace;
273273
}
274+
275+
/**
276+
* @template {import('./types.js').TransformState} T
277+
* @param {import('estree').CallExpression} node
278+
* @param {import('zimmerframe').Context<any, T>} context
279+
*/
280+
export function transform_inspect_rune(node, context) {
281+
const { state, visit } = context;
282+
const as_fn = state.options.generate === 'client';
283+
284+
if (!state.options.dev) return b.unary('void', b.literal(0));
285+
286+
if (node.callee.type === 'MemberExpression') {
287+
const raw_inspect_args = /** @type {import('estree').CallExpression} */ (node.callee.object)
288+
.arguments;
289+
const inspect_args =
290+
/** @type {Array<import('estree').Expression>} */
291+
(raw_inspect_args.map((arg) => visit(arg)));
292+
const with_arg = /** @type {import('estree').Expression} */ (visit(node.arguments[0]));
293+
294+
return b.call(
295+
'$.inspect',
296+
as_fn ? b.thunk(b.array(inspect_args)) : b.array(inspect_args),
297+
with_arg
298+
);
299+
} else {
300+
const arg = node.arguments.map(
301+
(arg) => /** @type {import('estree').Expression} */ (visit(arg))
302+
);
303+
return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg));
304+
}
305+
}

packages/svelte/src/compiler/phases/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ export const Runes = /** @type {const} */ ([
7878
'$effect.pre',
7979
'$effect.active',
8080
'$effect.root',
81-
'$inspect'
81+
'$inspect',
82+
'$inspect().with'
8283
]);
8384

8485
/**

packages/svelte/src/compiler/phases/scope.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,11 @@ export function get_rune(node, scope) {
699699
n = n.object;
700700
}
701701

702+
if (n.type === 'CallExpression' && n.callee.type === 'Identifier') {
703+
joined = '()' + joined;
704+
n = n.callee;
705+
}
706+
702707
if (n.type !== 'Identifier') return null;
703708

704709
joined = n.name + joined;

packages/svelte/src/internal/client/runtime.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
PROPS_IS_UPDATED
1010
} from '../../constants.js';
1111
import { readonly } from './proxy/readonly.js';
12-
import { proxy } from './proxy/proxy.js';
12+
import { proxy, unstate } from './proxy/proxy.js';
1313

1414
export const SOURCE = 1;
1515
export const DERIVED = 1 << 1;
@@ -1775,19 +1775,28 @@ function deep_read(value, visited = new Set()) {
17751775
}
17761776
}
17771777

1778+
// TODO remove in a few versions, before 5.0 at the latest
1779+
let warned_inspect_changed = false;
1780+
17781781
/**
1779-
* @param {() => any} get_value
1780-
* @param {Function} inspect
1781-
* @returns {void}
1782+
* @param {() => any[]} get_value
1783+
* @param {Function} [inspect]
17821784
*/
17831785
// eslint-disable-next-line no-console
17841786
export function inspect(get_value, inspect = console.log) {
17851787
let initial = true;
17861788

17871789
pre_effect(() => {
17881790
const fn = () => {
1789-
const value = get_value();
1790-
inspect(value, initial ? 'init' : 'update');
1791+
const value = get_value().map(unstate);
1792+
if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) {
1793+
// eslint-disable-next-line no-console
1794+
console.warn(
1795+
'$inspect() API has changed. See https://svelte-5-preview.vercel.app/docs/runes#$inspect for more information.'
1796+
);
1797+
warned_inspect_changed = true;
1798+
}
1799+
inspect(initial ? 'init' : 'update', ...value);
17911800
};
17921801

17931802
inspect_fn = fn;

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,3 +552,12 @@ export function loop_guard(timeout) {
552552
}
553553
};
554554
}
555+
556+
/**
557+
* @param {any[]} args
558+
* @param {Function} [inspect]
559+
*/
560+
// eslint-disable-next-line no-console
561+
export function inspect(args, inspect = console.log) {
562+
inspect('init', ...args);
563+
}

packages/svelte/src/main/ambient.d.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,23 +132,23 @@ declare namespace $effect {
132132
declare function $props<T>(): T;
133133

134134
/**
135-
* Inspects a value whenever it, or the properties it contains, change. Example:
135+
* Inspects one or more values whenever they, or the properties they contain, change. Example:
136136
*
137137
* ```ts
138-
* $inspect({ someValue, someOtherValue })
138+
* $inspect(someValue, someOtherValue)
139139
* ```
140140
*
141-
* If a second argument is provided, it will be called with the value and the event type
142-
* (`'init'` or `'update'`), otherwise the value will be logged to the console.
141+
* `$inspect` returns a `with` function, which you can invoke with a callback function that
142+
* will be called with the value and the event type (`'init'` or `'update'`) on every change.
143+
* By default, the values will be logged to the console.
143144
*
144145
* ```ts
145-
* $inspect(x, console.trace);
146-
* $inspect(y, (y) => { debugger; });
146+
* $inspect(x).with(console.trace);
147+
* $inspect(x, y).with(() => { debugger; });
147148
* ```
148149
*
149150
* https://svelte-5-preview.vercel.app/docs/runes#$inspect
150151
*/
151-
declare function $inspect<T>(
152-
value: T,
153-
callback?: (value: T, type: 'init' | 'update') => void
154-
): void;
152+
declare function $inspect<T extends any[]>(
153+
...values: T
154+
): { with: (type: 'init' | 'update', ...values: T) => void };

packages/svelte/tests/runtime-runes/samples/inspect-derived/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ export default test({
2626
button?.click();
2727
await Promise.resolve();
2828

29-
assert.deepEqual(log, ['X', 'init', 'XX', 'update', 'XXX', 'update']);
29+
assert.deepEqual(log, ['init', 'X', 'update', 'XX', 'update', 'XXX']);
3030
}
3131
});

packages/svelte/tests/runtime-runes/samples/inspect-derived/main.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
let x = $state('x');
66
let y = $derived(x.toUpperCase());
77
8-
$inspect(y, push);
8+
$inspect(y).with(push);
99
</script>
1010

1111
<button on:click={() => x += 'x'}>{x}</button>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { test } from '../../test';
2+
3+
/**
4+
* @type {any[]}
5+
*/
6+
let log;
7+
/**
8+
* @type {typeof console.log}}
9+
*/
10+
let original_log;
11+
12+
export default test({
13+
compileOptions: {
14+
dev: true
15+
},
16+
before_test() {
17+
log = [];
18+
original_log = console.log;
19+
console.log = (...v) => {
20+
log.push(...v);
21+
};
22+
},
23+
after_test() {
24+
console.log = original_log;
25+
},
26+
async test({ assert, target }) {
27+
const [b1, b2] = target.querySelectorAll('button');
28+
b1.click();
29+
b2.click();
30+
await Promise.resolve();
31+
32+
assert.deepEqual(log, ['init', 0, 0, 'update', 1, 0, 'update', 1, 1]);
33+
}
34+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
let x = $state(0);
3+
let y = $state(0);
4+
5+
$inspect(x, y);
6+
</script>
7+
8+
<button on:click={() => x++}>{x}</button>
9+
<button on:click={() => y++}>{y}</button>

packages/svelte/tests/runtime-runes/samples/inspect-trace/main.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
let x = $state(0);
33
let y = $state(0);
44
5-
$inspect(x, (x, type) => {
5+
$inspect(x).with((type, x) => {
66
if (type === 'update') console.log(new Error(), x);
77
});
88
</script>

packages/svelte/tests/runtime-runes/samples/inspect/_config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ export default test({
2323
after_test() {
2424
console.log = original_log;
2525
},
26-
async test({ assert, target, component }) {
26+
async test({ assert, target }) {
2727
const [b1, b2] = target.querySelectorAll('button');
2828
b1.click();
2929
b2.click();
3030
await Promise.resolve();
3131

32-
assert.deepEqual(log, [0, 'init', 1, 'update']);
32+
assert.deepEqual(log, ['init', 0, 'update', 1]);
3333
}
3434
});

0 commit comments

Comments
 (0)