diff --git a/.changeset/chatty-hotels-grin.md b/.changeset/chatty-hotels-grin.md new file mode 100644 index 0000000000..f72fd33beb --- /dev/null +++ b/.changeset/chatty-hotels-grin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: adds $log rune diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index 1aa78b14af..53e257412c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -292,12 +292,36 @@ export 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.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(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 505ddeaed1..ceb9033c43 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -624,12 +624,24 @@ 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 === '$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(); } diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 0b06c5eda0..649205938d 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -70,7 +70,18 @@ export const ElementBindings = [ '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 diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 8971532182..dd6cff6f91 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -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) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index db5cf7be30..e4a7dcc239 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -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; @@ -69,8 +69,9 @@ 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 trace logging +let is_tracing_signals = false; // Handle rendering tree blocks and anchors - /** @type {null | import('./types.js').Block} */ export let current_block = null; // Handling runtime component context @@ -145,10 +146,26 @@ function default_equals(a, b) { * @template V * @param {import('./types.js').SignalFlags} flags * @param {V} value - * @returns {import('./types.js').SourceSignal} + * @returns {import('./types.js').SourceSignal | import('./types.js').SourceSignal & 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, + // debug: this is for DEV only + d: null + }; + } + return { // consumers c: null, // equals @@ -160,7 +177,6 @@ function create_source_signal(flags, value) { // context: We can remove this if we get rid of beforeUpdate/afterUpdate x: null }; - return source; } /** @@ -688,7 +704,7 @@ export function store_get(store, store_name, stores) { /** * @template V * @param {import('./types.js').Store | null | undefined} store - * @param {import('./types.js').Signal} source + * @param {import('./types.js').SourceSignal} source */ function connect_store_to_signal(store, source) { if (store == null) { @@ -790,16 +806,47 @@ export function get(signal) { if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) { update_derived(/** @type {import('./types.js').ComputationSignal} **/ (signal), false); } + if (DEV && is_tracing_signals && (flags & SOURCE) !== 0) { + const debug_source = + /** @type {import('./types.js').SourceSignal & 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; } /** * @template V - * @param {import('./types.js').Signal} signal + * @param {import('./types.js').SourceSignal} signal * @param {V} value * @returns {V} */ 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 & import('./types.js').SourceSignalDebug} */ ( + signal + ); + debug_source.d = stack_debug; + } + } set_signal_value(signal, value); return value; } @@ -1716,3 +1763,93 @@ export function pop(accessors) { 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; + } + }); + } +} diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 1b41f6e0a4..09e0e94c33 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -80,6 +80,11 @@ export type SourceSignal = { v: V; }; +export type SourceSignalDebug = { + /** debug: This is DEV only */ + d: null | string; +}; + export type ComputationSignal = { /** block: The block associated with this effect/computed */ b: null | Block; diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index be6459b7df..8b85a7c77a 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -36,7 +36,11 @@ export { pop, push, reactive_import, - effect_active + effect_active, + log, + log_trace, + log_break, + log_table } from './client/runtime.js'; export * from './client/validate.js'; diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index f32f1863e9..21c2358028 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -102,3 +102,53 @@ declare namespace $effect { * https://svelte-5-preview.vercel.app/docs/runes#$props */ declare function $props(): 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; +} diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index 7c3ee0953a..cf4932cc48 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -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 + + + +``` + +> `$log` only works during development. + +## `$log.break` + +This works just like `$log`, except runtime execution will be paused via a `debugger` statement. + +```svelte + + + +``` + +## `$log.break` + +This works just like `$log`, but triggers `console.table` instead of `console.log`. + +```svelte + + + +``` + +## `$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 + + + +```