Skip to content

$inspect rune #9705

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 51 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
5280e00
feat: add $log rune
trueadm Nov 24, 2023
68068b5
fix issues
trueadm Nov 24, 2023
8805366
fix issues
trueadm Nov 24, 2023
c69353c
tune
trueadm Nov 24, 2023
7e347c6
avoid static state reference validation
trueadm Nov 27, 2023
df25640
work around unfortunate browser behavior
dummdidumm Nov 27, 2023
ae3ef2c
call it ExpectedError
dummdidumm Nov 27, 2023
5205bac
cleanup
dummdidumm Nov 27, 2023
9100430
Merge branch 'main' into log-rune
trueadm Nov 27, 2023
22b5289
Fix docs
trueadm Nov 27, 2023
f2162c0
tweaks
trueadm Nov 27, 2023
9d7e915
tweaks
trueadm Nov 27, 2023
aa6ef0f
lint
trueadm Nov 27, 2023
aac227d
merge conflicts
trueadm Nov 27, 2023
dac4fc4
repl, dev: true
trueadm Nov 27, 2023
801bfee
repl dev mode
trueadm Nov 27, 2023
e1299a6
Merge branch 'main' into log-rune
trueadm Nov 27, 2023
3b1e812
Update sites/svelte-5-preview/src/lib/Repl.svelte
Rich-Harris Nov 27, 2023
8964d1a
squelch static-state-reference warning
Rich-Harris Nov 27, 2023
fb813c2
simplify
Rich-Harris Nov 27, 2023
098ce67
remove redundant code
Rich-Harris Nov 27, 2023
902c168
Update packages/svelte/src/main/ambient.d.ts
trueadm Nov 27, 2023
be762fe
Update packages/svelte/src/main/ambient.d.ts
trueadm Nov 27, 2023
be07df0
Update packages/svelte/src/main/ambient.d.ts
trueadm Nov 27, 2023
1c10e14
only pause/trace on change
Rich-Harris Nov 27, 2023
f181547
Merge branch 'log-rune' of github.com:sveltejs/svelte into log-rune
Rich-Harris Nov 27, 2023
765c432
Update packages/svelte/src/main/ambient.d.ts
Rich-Harris Nov 27, 2023
376fc50
Update .changeset/chatty-hotels-grin.md
Rich-Harris Nov 27, 2023
93a2607
Update sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md
trueadm Nov 27, 2023
6daa422
$log.break and $log.trace no-op during SSR
Rich-Harris Nov 27, 2023
6b2d267
Update sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md
trueadm Nov 27, 2023
21985a3
update test
Rich-Harris Nov 27, 2023
e7d5cd6
Merge branch 'log-rune' of github.com:sveltejs/svelte into log-rune
Rich-Harris Nov 27, 2023
8122453
improve break experience
trueadm Nov 28, 2023
cd2fc8f
fix ts
trueadm Nov 28, 2023
2666e6d
remove unnecessary if (DEV) checks - log runes are removed in prod
Rich-Harris Nov 28, 2023
c6d7fca
ensure hoisting doesnt mess up source maps
dummdidumm Nov 28, 2023
e1cfa14
check visited for cyclical values
trueadm Nov 28, 2023
d89d54d
merge conflict
trueadm Nov 29, 2023
536bbe3
rename $log to $inspect, remove children
Rich-Harris Nov 29, 2023
a7734b4
custom inspect function
Rich-Harris Nov 29, 2023
01308c5
implement custom inspect functions
Rich-Harris Nov 29, 2023
4290a01
changeset
Rich-Harris Nov 29, 2023
a998cb3
update docs
Rich-Harris Nov 29, 2023
5bdbd81
only fire on change
Rich-Harris Nov 29, 2023
2fb380f
lint
Rich-Harris Nov 29, 2023
743465e
make inspect take a single argument
Rich-Harris Nov 29, 2023
1c781a3
ugh eslint
Rich-Harris Nov 29, 2023
af017da
document console.trace trick
Rich-Harris Nov 29, 2023
2016320
demos
Rich-Harris Nov 29, 2023
4d7acbd
fix site
Rich-Harris Nov 29, 2023
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/large-clouds-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: $inspect rune
18 changes: 15 additions & 3 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export function analyze_component(root, options) {
// is referencing a rune and not a global store.
if (
options.runes === false ||
!Runes.includes(name) ||
!Runes.includes(/** @type {any} */ (name)) ||
(declaration !== null &&
// const state = $state(0) is valid
get_rune(declaration.initial, instance.scope) === null &&
Expand All @@ -279,7 +279,7 @@ export function analyze_component(root, options) {
if (options.runes !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) {
error(references[0].node, 'illegal-global', name);
} else if (declaration !== null && Runes.includes(name)) {
} else if (declaration !== null && Runes.includes(/** @type {any} */ (name))) {
for (const { node, path } of references) {
if (path.at(-1)?.type === 'CallExpression') {
warn(warnings, node, [], 'store-with-rune-name', store_name);
Expand Down Expand Up @@ -326,7 +326,10 @@ export function analyze_component(root, options) {
get_css_hash: options.cssHash
}),
runes:
options.runes ?? Array.from(module.scope.references).some(([name]) => Runes.includes(name)),
options.runes ??
Array.from(module.scope.references).some(([name]) =>
Runes.includes(/** @type {any} */ (name))
),
exports: [],
uses_props: false,
uses_rest_props: false,
Expand Down Expand Up @@ -660,6 +663,14 @@ const runes_scope_js_tweaker = {

/** @type {import('./types').Visitors} */
const runes_scope_tweaker = {
CallExpression(node, { state, next }) {
const rune = get_rune(node, state.scope);

// `$inspect(foo)` should not trigger the `static-state-reference` warning
if (rune === '$inspect') {
next({ ...state, function_depth: state.function_depth + 1 });
}
},
VariableDeclarator(node, { state }) {
const init = unwrap_ts_expression(node.init);
if (!init || init.type !== 'CallExpression') return;
Expand Down Expand Up @@ -880,6 +891,7 @@ const common_visitors = {
Identifier(node, context) {
const parent = /** @type {import('estree').Node} */ (context.path.at(-1));
if (!is_reference(node, parent)) return;

const binding = context.state.scope.get(node.name);

// if no binding, means some global variable
Expand Down
10 changes: 8 additions & 2 deletions packages/svelte/src/compiler/phases/2-analyze/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,13 +521,19 @@ function validate_call_expression(node, scope, path) {

if (rune === '$effect.active') {
if (node.arguments.length !== 0) {
error(node, 'invalid-rune-args-length', '$effect.active', [0]);
error(node, 'invalid-rune-args-length', rune, [0]);
}
}

if (rune === '$effect.root') {
if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', '$effect.root', [1]);
error(node, 'invalid-rune-args-length', rune, [1]);
}
}

if (rune === '$inspect') {
if (node.arguments.length < 1 || node.arguments.length > 2) {
error(node, 'invalid-rune-args-length', rune, [1, 2]);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ function get_hoistable_params(node, context) {
params.push(b.id(binding.node.name.slice(1)));
params.push(b.id(binding.node.name));
} else {
params.push(binding.node);
// create a copy to remove start/end tags which would mess up source maps
params.push(b.id(binding.node.name));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export const javascript_visitors_runes = {
for (const declarator of node.declarations) {
const init = unwrap_ts_expression(declarator.init);
const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.active' || rune === '$effect.root') {
if (!rune || rune === '$effect.active' || rune === '$effect.root' || rune === '$inspect') {
if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init);
state.hoisted.push(
Expand Down Expand Up @@ -307,6 +307,19 @@ export const javascript_visitors_runes = {
return b.call('$.user_root_effect', ...args);
}

if (rune === '$inspect') {
if (state.options.dev) {
const arg = /** @type {import('estree').Expression} */ (visit(node.arguments[0]));
const fn =
node.arguments[1] &&
/** @type {import('estree').Expression} */ (visit(node.arguments[1]));

return b.call('$.inspect', b.thunk(arg), fn);
}

return b.unary('void', b.literal(0));
}

next();
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ const javascript_visitors_runes = {
for (const declarator of node.declarations) {
const init = unwrap_ts_expression(declarator.init);
const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.active') {
if (!rune || rune === '$effect.active' || rune === '$inspect') {
declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator)));
continue;
}
Expand Down Expand Up @@ -630,13 +630,25 @@ const javascript_visitors_runes = {
}
context.next();
},
CallExpression(node, { state, next }) {
CallExpression(node, { state, next, visit }) {
const rune = get_rune(node, state.scope);

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

if (rune === '$inspect') {
if (state.options.dev) {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => visit(arg))
);

return b.call('console.log', ...args);
}

return b.unary('void', b.literal(0));
}

next();
}
};
Expand Down
7 changes: 4 additions & 3 deletions packages/svelte/src/compiler/phases/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,16 @@ export const ElementBindings = [
'indeterminate'
];

export const Runes = [
export const Runes = /** @type {const} */ ([
'$state',
'$props',
'$derived',
'$effect',
'$effect.pre',
'$effect.active',
'$effect.root'
];
'$effect.root',
'$inspect'
]);

/**
* Whitespace inside one of these elements will not result in
Expand Down
5 changes: 3 additions & 2 deletions packages/svelte/src/compiler/phases/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ export function set_scope(scopes) {
* Returns the name of the rune if the given expression is a `CallExpression` using a rune.
* @param {import('estree').Node | null | undefined} node
* @param {Scope} scope
* @returns {Runes[number] | null}
*/
export function get_rune(node, scope) {
if (!node) return null;
Expand All @@ -691,10 +692,10 @@ export function get_rune(node, scope) {
if (n.type !== 'Identifier') return null;

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

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

return joined;
return /** @type {Runes[number] | null} */ (joined);
}
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/utils/builders.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function labeled(name, body) {

/**
* @param {string | import('estree').Expression} callee
* @param {...import('estree').Expression} args
* @param {...(import('estree').Expression | import('estree').SpreadElement)} args
* @returns {import('estree').CallExpression}
*/
export function call(callee, ...args) {
Expand Down
115 changes: 108 additions & 7 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DEV } from 'esm-env';
import { subscribe_to_store } from '../../store/utils.js';
import { EMPTY_FUNC, run_all } from '../common.js';
import { unwrap } from './render.js';
import { is_array } from './utils.js';
import { get_descriptors, is_array } from './utils.js';

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

/** @type {Function | null} */
let inspect_fn = null;

/** @type {Array<import('./types.js').SourceSignal & import('./types.js').SourceSignalDebug>} */
let inspect_captured_signals = [];

// Handle rendering tree blocks and anchors
/** @type {null | import('./types.js').Block} */
export let current_block = null;
// Handling runtime component context
Expand Down Expand Up @@ -145,10 +151,26 @@ function default_equals(a, b) {
* @template V
* @param {import('./types.js').SignalFlags} flags
* @param {V} value
* @returns {import('./types.js').SourceSignal<V>}
* @returns {import('./types.js').SourceSignal<V> | import('./types.js').SourceSignal<V> & import('./types.js').SourceSignalDebug}
*/
function create_source_signal(flags, value) {
const source = {
if (DEV) {
return {
// consumers
c: null,
// equals
e: null,
// flags
f: flags,
// value
v: value,
// context: We can remove this if we get rid of beforeUpdate/afterUpdate
x: null,
// this is for DEV only
inspect: new Set()
};
}
return {
// consumers
c: null,
// equals
Expand All @@ -160,7 +182,6 @@ function create_source_signal(flags, value) {
// context: We can remove this if we get rid of beforeUpdate/afterUpdate
x: null
};
return source;
}

/**
Expand Down Expand Up @@ -688,7 +709,7 @@ export function store_get(store, store_name, stores) {
/**
* @template V
* @param {import('./types.js').Store<V> | null | undefined} store
* @param {import('./types.js').Signal<V>} source
* @param {import('./types.js').SourceSignal<V>} source
*/
function connect_store_to_signal(store, source) {
if (store == null) {
Expand Down Expand Up @@ -756,6 +777,14 @@ export function exposable(fn) {
* @returns {V}
*/
export function get(signal) {
// @ts-expect-error
if (DEV && signal.inspect && inspect_fn) {
// @ts-expect-error
signal.inspect.add(inspect_fn);
// @ts-expect-error
inspect_captured_signals.push(signal);
}

const flags = signal.f;
if ((flags & DESTROYED) !== 0) {
return signal.v;
Expand Down Expand Up @@ -811,7 +840,7 @@ export function set(signal, value) {
* @returns {void}
*/
export function set_sync(signal, value) {
flushSync(() => set_signal_value(signal, value));
flushSync(() => set(signal, value));
}

/**
Expand Down Expand Up @@ -1016,6 +1045,12 @@ export function set_signal_value(signal, value) {
});
}
}

// @ts-expect-error
if (DEV && signal.inspect) {
// @ts-expect-error
for (const fn of signal.inspect) fn();
}
}
}

Expand Down Expand Up @@ -1727,3 +1762,69 @@ export function pop(accessors) {
context_stack_item.m = true;
}
}

/**
* @param {any} value
* @param {Set<any>} visited
* @returns {void}
*/
function deep_read(value, visited = new Set()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
visited.add(value);
for (let key in value) {
deep_read(value[key], visited);
}
const proto = Object.getPrototypeOf(value);
if (
proto !== Object.prototype &&
proto !== Array.prototype &&
proto !== Map.prototype &&
proto !== Set.prototype &&
proto !== Date.prototype
) {
const descriptors = get_descriptors(proto);
for (let key in descriptors) {
const get = descriptors[key].get;
if (get) {
get.call(value);
}
}
}
}
}

/**
* @param {() => import('./types.js').MaybeSignal<>} get_value
* @param {Function} inspect
* @returns {void}
*/
// eslint-disable-next-line no-console
export function inspect(get_value, inspect = console.log) {
let initial = true;

pre_effect(() => {
const fn = () => {
const value = get_value();
inspect(value, initial ? 'init' : 'update');
};

inspect_fn = fn;
const value = get_value();
deep_read(value);
inspect_fn = null;

const signals = inspect_captured_signals.slice();
inspect_captured_signals = [];

if (initial) {
fn();
initial = false;
}

return () => {
for (const s of signals) {
s.inspect.delete(fn);
}
};
});
}
5 changes: 5 additions & 0 deletions packages/svelte/src/internal/client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ export type SourceSignal<V = unknown> = {
v: V;
};

export type SourceSignalDebug = {
/** This is DEV only */
inspect: Set<Function>;
};

export type ComputationSignal<V = unknown> = {
/** block: The block associated with this effect/computed */
b: null | Block;
Expand Down
Loading