feat: add $log rune

pull/9670/head
Dominic Gannaway 3 years ago
parent b7af2ffabd
commit 5280e00cc1

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: adds $log rune

@ -292,12 +292,36 @@ export const javascript_visitors_runes = {
context.next(); context.next();
}, },
CallExpression(node, { state, next }) { CallExpression(node, { state, next, visit }) {
const rune = get_rune(node, state.scope); const rune = get_rune(node, state.scope);
if (rune === '$effect.active') { if (rune === '$effect.active') {
return b.call('$.effect_active'); return b.call('$.effect_active');
} }
if (rune === '$log') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => visit(arg))
);
return b.call('$.log', b.thunk(b.array(args)));
}
if (rune === '$log.trace') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => visit(arg))
);
return b.call('$.log_trace', b.thunk(b.array(args)));
}
if (rune === '$log.break') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => visit(arg))
);
return b.call('$.log_break', b.thunk(b.array(args)));
}
if (rune === '$log.table') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => visit(arg))
);
return b.call('$.log_table', b.thunk(b.array(args)));
}
next(); next();
} }

@ -624,12 +624,24 @@ const javascript_visitors_runes = {
} }
context.next(); context.next();
}, },
CallExpression(node, { state, next }) { CallExpression(node, { state, next, visit }) {
const rune = get_rune(node, state.scope); const rune = get_rune(node, state.scope);
if (rune === '$effect.active') { if (rune === '$effect.active') {
return b.literal(false); return b.literal(false);
} }
if (rune === '$log' || rune === '$log.break' || rune === '$log.trace') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => visit(arg))
);
return b.call('console.log', ...args);
}
if (rune === '$log.table') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => visit(arg))
);
return b.call('console.table', ...args);
}
next(); next();
} }

@ -70,7 +70,18 @@ export const ElementBindings = [
'indeterminate' 'indeterminate'
]; ];
export const Runes = ['$state', '$props', '$derived', '$effect', '$effect.pre', '$effect.active']; export const Runes = [
'$state',
'$props',
'$derived',
'$effect',
'$effect.pre',
'$effect.active',
'$log',
'$log.break',
'$log.trace',
'$log.table'
];
/** /**
* Whitespace inside one of these elements will not result in * Whitespace inside one of these elements will not result in

@ -72,7 +72,7 @@ export function labeled(name, body) {
/** /**
* @param {string | import('estree').Expression} callee * @param {string | import('estree').Expression} callee
* @param {...import('estree').Expression} args * @param {...(import('estree').Expression | import('estree').SpreadElement)} args
* @returns {import('estree').CallExpression} * @returns {import('estree').CallExpression}
*/ */
export function call(callee, ...args) { export function call(callee, ...args) {

@ -2,7 +2,7 @@ import { DEV } from 'esm-env';
import { subscribe_to_store } from '../../store/utils.js'; import { subscribe_to_store } from '../../store/utils.js';
import { EMPTY_FUNC, run_all } from '../common.js'; import { EMPTY_FUNC, run_all } from '../common.js';
import { unwrap } from './render.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 SOURCE = 1;
export const DERIVED = 1 << 1; export const DERIVED = 1 << 1;
@ -69,8 +69,9 @@ let current_skip_consumer = false;
// Handle collecting all signals which are read during a specific time frame // Handle collecting all signals which are read during a specific time frame
let is_signals_recorded = false; let is_signals_recorded = false;
let captured_signals = new Set(); let captured_signals = new Set();
// Handle trace logging
let is_tracing_signals = false;
// Handle rendering tree blocks and anchors // Handle rendering tree blocks and anchors
/** @type {null | import('./types.js').Block} */ /** @type {null | import('./types.js').Block} */
export let current_block = null; export let current_block = null;
// Handling runtime component context // Handling runtime component context
@ -145,10 +146,26 @@ function default_equals(a, b) {
* @template V * @template V
* @param {import('./types.js').SignalFlags} flags * @param {import('./types.js').SignalFlags} flags
* @param {V} value * @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) { 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,
// debug: this is for DEV only
d: null
};
}
return {
// consumers // consumers
c: null, c: null,
// equals // equals
@ -160,7 +177,6 @@ function create_source_signal(flags, value) {
// context: We can remove this if we get rid of beforeUpdate/afterUpdate // context: We can remove this if we get rid of beforeUpdate/afterUpdate
x: null x: null
}; };
return source;
} }
/** /**
@ -688,7 +704,7 @@ export function store_get(store, store_name, stores) {
/** /**
* @template V * @template V
* @param {import('./types.js').Store<V> | null | undefined} store * @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) { function connect_store_to_signal(store, source) {
if (store == null) { if (store == null) {
@ -790,16 +806,47 @@ export function get(signal) {
if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) { if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) {
update_derived(/** @type {import('./types.js').ComputationSignal<V>} **/ (signal), false); update_derived(/** @type {import('./types.js').ComputationSignal<V>} **/ (signal), false);
} }
if (DEV && is_tracing_signals && (flags & SOURCE) !== 0) {
const debug_source =
/** @type {import('./types.js').SourceSignal<V> & import('./types.js').SourceSignalDebug} */ (
signal
);
if (debug_source.d !== null) {
console.log('$log.trace: ' + debug_source.d);
queueMicrotask(() => {
debug_source.d = null;
});
}
}
return signal.v; return signal.v;
} }
/** /**
* @template V * @template V
* @param {import('./types.js').Signal<V>} signal * @param {import('./types.js').SourceSignal<V>} signal
* @param {V} value * @param {V} value
* @returns {V} * @returns {V}
*/ */
export function set(signal, value) { export function set(signal, value) {
if (DEV) {
let stack_debug = null;
try {
const error = new Error();
stack_debug = error.stack
?.split('\n')
?.at(2)
?.replace(/\s+at\s+/, '');
} catch {
// Do nothing
}
if (stack_debug) {
const debug_source =
/** @type {import('./types.js').SourceSignal<V> & import('./types.js').SourceSignalDebug} */ (
signal
);
debug_source.d = stack_debug;
}
}
set_signal_value(signal, value); set_signal_value(signal, value);
return value; return value;
} }
@ -1716,3 +1763,93 @@ export function pop(accessors) {
context_stack_item.m = true; context_stack_item.m = true;
} }
} }
/**
* @param {any} value
* @returns {void}
*/
function deep_read(value) {
if (typeof value === 'object' && value !== null) {
for (let key in value) {
deep_read(value[key]);
}
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_values
* @returns {void}
*/
export function log(get_values) {
if (DEV) {
pre_effect(() => {
const values = get_values();
deep_read(values);
console.log(...values);
});
}
}
/**
* @param {() => import('./types.js').MaybeSignal<>[]} get_values
* @returns {void}
*/
export function log_table(get_values) {
if (DEV) {
pre_effect(() => {
const values = get_values();
deep_read(values);
console.table(...values);
});
}
}
/**
* @param {() => import('./types.js').MaybeSignal<>[]} get_values
* @returns {void}
*/
export function log_break(get_values) {
if (DEV) {
pre_effect(() => {
const values = get_values();
deep_read(values);
console.log(...values);
debugger;
});
}
}
/**
* @param {() => import('./types.js').MaybeSignal<>[]} get_values
* @returns {void}
*/
export function log_trace(get_values) {
if (DEV) {
pre_effect(() => {
is_tracing_signals = true;
try {
const values = get_values();
deep_read(values);
console.log(...values);
} finally {
is_tracing_signals = false;
}
});
}
}

@ -80,6 +80,11 @@ export type SourceSignal<V = unknown> = {
v: V; v: V;
}; };
export type SourceSignalDebug = {
/** debug: This is DEV only */
d: null | string;
};
export type ComputationSignal<V = unknown> = { export type ComputationSignal<V = unknown> = {
/** block: The block associated with this effect/computed */ /** block: The block associated with this effect/computed */
b: null | Block; b: null | Block;

@ -36,7 +36,11 @@ export {
pop, pop,
push, push,
reactive_import, reactive_import,
effect_active effect_active,
log,
log_trace,
log_break,
log_table
} from './client/runtime.js'; } from './client/runtime.js';
export * from './client/validate.js'; export * from './client/validate.js';

@ -102,3 +102,53 @@ declare namespace $effect {
* https://svelte-5-preview.vercel.app/docs/runes#$props * https://svelte-5-preview.vercel.app/docs/runes#$props
*/ */
declare function $props<T>(): T; declare function $props<T>(): T;
/**
* Deeply tracks and `console.log`s any values passed to the rune. Example:
*
* ```ts
* $log(someValue, someOtherValue)
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$log
*/
declare function $log(): void;
declare namespace $log {
/**
* Deeply tracks and `console.log`s any values passed to the rune and pauses execution with
* a debugger break point.
*
* ```ts
* $log.break(someValue, someOtherValue)
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$log-break
*/
function break_fn(): void;
export { break_fn as break };
/**
* Deeply tracks and `console.log`s any values passed to the rune and also traces the call-sites
* where state gets mutated. Example:
*
* ```ts
* $log.trace(someValue, someOtherValue)
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$log-trace
*/
export function trace(): void;
/**
* Deeply tracks and `console.table`s any values passed to the rune. Example:
*
* ```ts
* $log.table(someDataCollection)
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$log-table
*/
export function table(): void;
}

@ -243,3 +243,65 @@ export default {
} }
}; };
``` ```
## `$log`
The `$log` rune is roughly equivalent to `console.log`, with the exception that when anything passed to the
rune changes, the latest values will be logged out. `$log` tracks reactive state deeply, meaning that mutating
something from with an object or array using fine-grain reactivity will be tracked.
```svelte
<script>
let count = $state(0);
$log({ count }); // will console.log when count changes
</script>
<button onclick={() => count++}>Increment</button>
```
> `$log` only works during development.
## `$log.break`
This works just like `$log`, except runtime execution will be paused via a `debugger` statement.
```svelte
<script>
let count = $state(0);
$log.break({ count });
</script>
<button onclick={() => count++}>Increment</button>
```
## `$log.break`
This works just like `$log`, but triggers `console.table` instead of `console.log`.
```svelte
<script>
let count = $state(0);
$log.table({ count });
</script>
<button onclick={() => count++}>Increment</button>
```
## `$log.trace`
This works just like `$log`, but in addition to logging the values, it will also trace
any mutations to fine-grain reactive state and log the sourcecode location to help find
the cause for something changing.
```svelte
<script>
let count = $state(0);
$log.trace({ count });
</script>
<button onclick={() => count++}>Increment</button>
```

Loading…
Cancel
Save