Skip to content

Commit bde42d5

Browse files
Rich-Harristrueadmdummdidumm
authored
$inspect rune (#9705)
* feat: add $log rune * fix issues * fix issues * tune * avoid static state reference validation * work around unfortunate browser behavior * call it ExpectedError * cleanup * Fix docs * tweaks * tweaks * lint * repl, dev: true * repl dev mode * Update sites/svelte-5-preview/src/lib/Repl.svelte * squelch static-state-reference warning * simplify * remove redundant code * Update packages/svelte/src/main/ambient.d.ts Co-authored-by: Rich Harris <[email protected]> * Update packages/svelte/src/main/ambient.d.ts Co-authored-by: Rich Harris <[email protected]> * Update packages/svelte/src/main/ambient.d.ts Co-authored-by: Rich Harris <[email protected]> * only pause/trace on change * Update packages/svelte/src/main/ambient.d.ts * Update .changeset/chatty-hotels-grin.md * Update sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md Co-authored-by: Rich Harris <[email protected]> * $log.break and $log.trace no-op during SSR * Update sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md Co-authored-by: Rich Harris <[email protected]> * update test * improve break experience * fix ts * remove unnecessary if (DEV) checks - log runes are removed in prod * ensure hoisting doesnt mess up source maps * check visited for cyclical values * rename $log to $inspect, remove children * custom inspect function * implement custom inspect functions * changeset * update docs * only fire on change * lint * make inspect take a single argument * ugh eslint * document console.trace trick * demos * fix site --------- Co-authored-by: Dominic Gannaway <[email protected]> Co-authored-by: Simon Holthausen <[email protected]> Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Dominic Gannaway <[email protected]>
1 parent 3e3ae92 commit bde42d5

File tree

21 files changed

+345
-31
lines changed

21 files changed

+345
-31
lines changed

.changeset/large-clouds-carry.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+
feat: $inspect rune

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ export function analyze_component(root, options) {
265265
// is referencing a rune and not a global store.
266266
if (
267267
options.runes === false ||
268-
!Runes.includes(name) ||
268+
!Runes.includes(/** @type {any} */ (name)) ||
269269
(declaration !== null &&
270270
// const state = $state(0) is valid
271271
get_rune(declaration.initial, instance.scope) === null &&
@@ -279,7 +279,7 @@ export function analyze_component(root, options) {
279279
if (options.runes !== false) {
280280
if (declaration === null && /[a-z]/.test(store_name[0])) {
281281
error(references[0].node, 'illegal-global', name);
282-
} else if (declaration !== null && Runes.includes(name)) {
282+
} else if (declaration !== null && Runes.includes(/** @type {any} */ (name))) {
283283
for (const { node, path } of references) {
284284
if (path.at(-1)?.type === 'CallExpression') {
285285
warn(warnings, node, [], 'store-with-rune-name', store_name);
@@ -326,7 +326,10 @@ export function analyze_component(root, options) {
326326
get_css_hash: options.cssHash
327327
}),
328328
runes:
329-
options.runes ?? Array.from(module.scope.references).some(([name]) => Runes.includes(name)),
329+
options.runes ??
330+
Array.from(module.scope.references).some(([name]) =>
331+
Runes.includes(/** @type {any} */ (name))
332+
),
330333
exports: [],
331334
uses_props: false,
332335
uses_rest_props: false,
@@ -660,6 +663,14 @@ const runes_scope_js_tweaker = {
660663

661664
/** @type {import('./types').Visitors} */
662665
const runes_scope_tweaker = {
666+
CallExpression(node, { state, next }) {
667+
const rune = get_rune(node, state.scope);
668+
669+
// `$inspect(foo)` should not trigger the `static-state-reference` warning
670+
if (rune === '$inspect') {
671+
next({ ...state, function_depth: state.function_depth + 1 });
672+
}
673+
},
663674
VariableDeclarator(node, { state }) {
664675
const init = unwrap_ts_expression(node.init);
665676
if (!init || init.type !== 'CallExpression') return;
@@ -880,6 +891,7 @@ const common_visitors = {
880891
Identifier(node, context) {
881892
const parent = /** @type {import('estree').Node} */ (context.path.at(-1));
882893
if (!is_reference(node, parent)) return;
894+
883895
const binding = context.state.scope.get(node.name);
884896

885897
// if no binding, means some global variable

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,13 +521,19 @@ function validate_call_expression(node, scope, path) {
521521

522522
if (rune === '$effect.active') {
523523
if (node.arguments.length !== 0) {
524-
error(node, 'invalid-rune-args-length', '$effect.active', [0]);
524+
error(node, 'invalid-rune-args-length', rune, [0]);
525525
}
526526
}
527527

528528
if (rune === '$effect.root') {
529529
if (node.arguments.length !== 1) {
530-
error(node, 'invalid-rune-args-length', '$effect.root', [1]);
530+
error(node, 'invalid-rune-args-length', rune, [1]);
531+
}
532+
}
533+
534+
if (rune === '$inspect') {
535+
if (node.arguments.length < 1 || node.arguments.length > 2) {
536+
error(node, 'invalid-rune-args-length', rune, [1, 2]);
531537
}
532538
}
533539
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,8 @@ function get_hoistable_params(node, context) {
288288
params.push(b.id(binding.node.name.slice(1)));
289289
params.push(b.id(binding.node.name));
290290
} else {
291-
params.push(binding.node);
291+
// create a copy to remove start/end tags which would mess up source maps
292+
params.push(b.id(binding.node.name));
292293
}
293294
}
294295
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export const javascript_visitors_runes = {
136136
for (const declarator of node.declarations) {
137137
const init = unwrap_ts_expression(declarator.init);
138138
const rune = get_rune(init, state.scope);
139-
if (!rune || rune === '$effect.active' || rune === '$effect.root') {
139+
if (!rune || rune === '$effect.active' || rune === '$effect.root' || rune === '$inspect') {
140140
if (init != null && is_hoistable_function(init)) {
141141
const hoistable_function = visit(init);
142142
state.hoisted.push(
@@ -307,6 +307,19 @@ export const javascript_visitors_runes = {
307307
return b.call('$.user_root_effect', ...args);
308308
}
309309

310+
if (rune === '$inspect') {
311+
if (state.options.dev) {
312+
const arg = /** @type {import('estree').Expression} */ (visit(node.arguments[0]));
313+
const fn =
314+
node.arguments[1] &&
315+
/** @type {import('estree').Expression} */ (visit(node.arguments[1]));
316+
317+
return b.call('$.inspect', b.thunk(arg), fn);
318+
}
319+
320+
return b.unary('void', b.literal(0));
321+
}
322+
310323
next();
311324
}
312325
};

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ const javascript_visitors_runes = {
575575
for (const declarator of node.declarations) {
576576
const init = unwrap_ts_expression(declarator.init);
577577
const rune = get_rune(init, state.scope);
578-
if (!rune || rune === '$effect.active') {
578+
if (!rune || rune === '$effect.active' || rune === '$inspect') {
579579
declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator)));
580580
continue;
581581
}
@@ -630,13 +630,25 @@ const javascript_visitors_runes = {
630630
}
631631
context.next();
632632
},
633-
CallExpression(node, { state, next }) {
633+
CallExpression(node, { state, next, visit }) {
634634
const rune = get_rune(node, state.scope);
635635

636636
if (rune === '$effect.active') {
637637
return b.literal(false);
638638
}
639639

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));
650+
}
651+
640652
next();
641653
}
642654
};

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,16 @@ export const ElementBindings = [
7070
'indeterminate'
7171
];
7272

73-
export const Runes = [
73+
export const Runes = /** @type {const} */ ([
7474
'$state',
7575
'$props',
7676
'$derived',
7777
'$effect',
7878
'$effect.pre',
7979
'$effect.active',
80-
'$effect.root'
81-
];
80+
'$effect.root',
81+
'$inspect'
82+
]);
8283

8384
/**
8485
* Whitespace inside one of these elements will not result in

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,7 @@ export function set_scope(scopes) {
672672
* Returns the name of the rune if the given expression is a `CallExpression` using a rune.
673673
* @param {import('estree').Node | null | undefined} node
674674
* @param {Scope} scope
675+
* @returns {Runes[number] | null}
675676
*/
676677
export function get_rune(node, scope) {
677678
if (!node) return null;
@@ -691,10 +692,10 @@ export function get_rune(node, scope) {
691692
if (n.type !== 'Identifier') return null;
692693

693694
joined = n.name + joined;
694-
if (!Runes.includes(joined)) return null;
695+
if (!Runes.includes(/** @type {any} */ (joined))) return null;
695696

696697
const binding = scope.get(n.name);
697698
if (binding !== null) return null; // rune name, but references a variable or store
698699

699-
return joined;
700+
return /** @type {Runes[number] | null} */ (joined);
700701
}

packages/svelte/src/compiler/utils/builders.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function labeled(name, body) {
7272

7373
/**
7474
* @param {string | import('estree').Expression} callee
75-
* @param {...import('estree').Expression} args
75+
* @param {...(import('estree').Expression | import('estree').SpreadElement)} args
7676
* @returns {import('estree').CallExpression}
7777
*/
7878
export function call(callee, ...args) {

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

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { DEV } from 'esm-env';
22
import { subscribe_to_store } from '../../store/utils.js';
33
import { EMPTY_FUNC, run_all } from '../common.js';
44
import { unwrap } from './render.js';
5-
import { is_array } from './utils.js';
5+
import { get_descriptors, is_array } from './utils.js';
66

77
export const SOURCE = 1;
88
export const DERIVED = 1 << 1;
@@ -69,8 +69,14 @@ let current_skip_consumer = false;
6969
// Handle collecting all signals which are read during a specific time frame
7070
let is_signals_recorded = false;
7171
let captured_signals = new Set();
72-
// Handle rendering tree blocks and anchors
7372

73+
/** @type {Function | null} */
74+
let inspect_fn = null;
75+
76+
/** @type {Array<import('./types.js').SourceSignal & import('./types.js').SourceSignalDebug>} */
77+
let inspect_captured_signals = [];
78+
79+
// Handle rendering tree blocks and anchors
7480
/** @type {null | import('./types.js').Block} */
7581
export let current_block = null;
7682
// Handling runtime component context
@@ -145,10 +151,26 @@ function default_equals(a, b) {
145151
* @template V
146152
* @param {import('./types.js').SignalFlags} flags
147153
* @param {V} value
148-
* @returns {import('./types.js').SourceSignal<V>}
154+
* @returns {import('./types.js').SourceSignal<V> | import('./types.js').SourceSignal<V> & import('./types.js').SourceSignalDebug}
149155
*/
150156
function create_source_signal(flags, value) {
151-
const source = {
157+
if (DEV) {
158+
return {
159+
// consumers
160+
c: null,
161+
// equals
162+
e: null,
163+
// flags
164+
f: flags,
165+
// value
166+
v: value,
167+
// context: We can remove this if we get rid of beforeUpdate/afterUpdate
168+
x: null,
169+
// this is for DEV only
170+
inspect: new Set()
171+
};
172+
}
173+
return {
152174
// consumers
153175
c: null,
154176
// equals
@@ -160,7 +182,6 @@ function create_source_signal(flags, value) {
160182
// context: We can remove this if we get rid of beforeUpdate/afterUpdate
161183
x: null
162184
};
163-
return source;
164185
}
165186

166187
/**
@@ -688,7 +709,7 @@ export function store_get(store, store_name, stores) {
688709
/**
689710
* @template V
690711
* @param {import('./types.js').Store<V> | null | undefined} store
691-
* @param {import('./types.js').Signal<V>} source
712+
* @param {import('./types.js').SourceSignal<V>} source
692713
*/
693714
function connect_store_to_signal(store, source) {
694715
if (store == null) {
@@ -756,6 +777,14 @@ export function exposable(fn) {
756777
* @returns {V}
757778
*/
758779
export function get(signal) {
780+
// @ts-expect-error
781+
if (DEV && signal.inspect && inspect_fn) {
782+
// @ts-expect-error
783+
signal.inspect.add(inspect_fn);
784+
// @ts-expect-error
785+
inspect_captured_signals.push(signal);
786+
}
787+
759788
const flags = signal.f;
760789
if ((flags & DESTROYED) !== 0) {
761790
return signal.v;
@@ -811,7 +840,7 @@ export function set(signal, value) {
811840
* @returns {void}
812841
*/
813842
export function set_sync(signal, value) {
814-
flushSync(() => set_signal_value(signal, value));
843+
flushSync(() => set(signal, value));
815844
}
816845

817846
/**
@@ -1016,6 +1045,12 @@ export function set_signal_value(signal, value) {
10161045
});
10171046
}
10181047
}
1048+
1049+
// @ts-expect-error
1050+
if (DEV && signal.inspect) {
1051+
// @ts-expect-error
1052+
for (const fn of signal.inspect) fn();
1053+
}
10191054
}
10201055
}
10211056

@@ -1727,3 +1762,69 @@ export function pop(accessors) {
17271762
context_stack_item.m = true;
17281763
}
17291764
}
1765+
1766+
/**
1767+
* @param {any} value
1768+
* @param {Set<any>} visited
1769+
* @returns {void}
1770+
*/
1771+
function deep_read(value, visited = new Set()) {
1772+
if (typeof value === 'object' && value !== null && !visited.has(value)) {
1773+
visited.add(value);
1774+
for (let key in value) {
1775+
deep_read(value[key], visited);
1776+
}
1777+
const proto = Object.getPrototypeOf(value);
1778+
if (
1779+
proto !== Object.prototype &&
1780+
proto !== Array.prototype &&
1781+
proto !== Map.prototype &&
1782+
proto !== Set.prototype &&
1783+
proto !== Date.prototype
1784+
) {
1785+
const descriptors = get_descriptors(proto);
1786+
for (let key in descriptors) {
1787+
const get = descriptors[key].get;
1788+
if (get) {
1789+
get.call(value);
1790+
}
1791+
}
1792+
}
1793+
}
1794+
}
1795+
1796+
/**
1797+
* @param {() => import('./types.js').MaybeSignal<>} get_value
1798+
* @param {Function} inspect
1799+
* @returns {void}
1800+
*/
1801+
// eslint-disable-next-line no-console
1802+
export function inspect(get_value, inspect = console.log) {
1803+
let initial = true;
1804+
1805+
pre_effect(() => {
1806+
const fn = () => {
1807+
const value = get_value();
1808+
inspect(value, initial ? 'init' : 'update');
1809+
};
1810+
1811+
inspect_fn = fn;
1812+
const value = get_value();
1813+
deep_read(value);
1814+
inspect_fn = null;
1815+
1816+
const signals = inspect_captured_signals.slice();
1817+
inspect_captured_signals = [];
1818+
1819+
if (initial) {
1820+
fn();
1821+
initial = false;
1822+
}
1823+
1824+
return () => {
1825+
for (const s of signals) {
1826+
s.inspect.delete(fn);
1827+
}
1828+
};
1829+
});
1830+
}

packages/svelte/src/internal/client/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ export type SourceSignal<V = unknown> = {
8080
v: V;
8181
};
8282

83+
export type SourceSignalDebug = {
84+
/** This is DEV only */
85+
inspect: Set<Function>;
86+
};
87+
8388
export type ComputationSignal<V = unknown> = {
8489
/** block: The block associated with this effect/computed */
8590
b: null | Block;

0 commit comments

Comments
 (0)