From 8e320fe17860ae464e8afd8c20f7cd35eb4ac89f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 12 Nov 2024 12:54:25 +0000 Subject: [PATCH] feat: add $trace rune WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP --- packages/svelte/src/ambient.d.ts | 2 + .../2-analyze/visitors/CallExpression.js | 23 ++ .../3-transform/client/transform-client.js | 14 +- .../phases/3-transform/client/types.d.ts | 4 + .../phases/3-transform/client/utils.js | 47 +++++ .../client/visitors/BlockStatement.js | 26 ++- .../client/visitors/CallExpression.js | 16 ++ .../3-transform/client/visitors/Identifier.js | 16 +- .../client/visitors/MemberExpression.js | 31 ++- .../client/visitors/NewExpression.js | 24 +++ .../client/visitors/VariableDeclaration.js | 3 +- .../src/compiler/phases/3-transform/index.js | 4 +- .../server/visitors/CallExpression.js | 4 + packages/svelte/src/compiler/phases/scope.js | 6 + .../svelte/src/internal/client/dev/tracing.js | 197 ++++++++++++++++++ packages/svelte/src/internal/client/index.js | 1 + .../src/internal/client/reactivity/sources.js | 14 +- .../src/internal/client/reactivity/types.d.ts | 2 + .../svelte/src/internal/client/runtime.js | 27 ++- packages/svelte/src/utils.js | 3 +- packages/svelte/types/index.d.ts | 2 + playgrounds/sandbox/run.js | 12 +- 22 files changed, 457 insertions(+), 21 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/NewExpression.js create mode 100644 packages/svelte/src/internal/client/dev/tracing.js diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 8a20a884ff..9d37d9a3f7 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -399,3 +399,5 @@ declare function $inspect( * https://svelte.dev/docs/svelte/$host */ declare function $host(): El; + +declare function $trace(name: string): void; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 957b27ae9b..27e9964d93 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -5,6 +5,7 @@ import { get_rune } from '../../scope.js'; import * as e from '../../../errors.js'; import { get_parent, unwrap_optional } from '../../../utils/ast.js'; import { is_pure, is_safe_identifier } from './shared/utils.js'; +import { dev } from '../../../state.js'; /** * @param {CallExpression} node @@ -135,6 +136,28 @@ export function CallExpression(node, context) { break; + case '$trace': + if (node.arguments.length !== 1) { + e.rune_invalid_arguments_length(node, rune, 'exactly one argument'); + } + if (node.arguments[0].type !== 'Literal' || typeof node.arguments[0].value !== 'string') { + throw new Error('TODO: $track requires a string argument'); + } + if (parent.type !== 'ExpressionStatement' || context.path.at(-2)?.type !== 'BlockStatement') { + throw new Error('TODO: $track must be inside a block statement'); + } + + if (context.state.scope.tracing) { + throw new Error('TODO: $track must only be used once within the same block statement'); + } + + if (dev) { + // TODO should we validate if tracing is already enabled in this or a parent scope? + context.state.scope.tracing = node.arguments[0].value; + } + + break; + case '$state.snapshot': if (node.arguments.length !== 1) { e.rune_invalid_arguments_length(node, rune, 'exactly one argument'); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 5349f60255..809ae0851a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -17,6 +17,7 @@ import { BindDirective } from './visitors/BindDirective.js'; import { BlockStatement } from './visitors/BlockStatement.js'; import { BreakStatement } from './visitors/BreakStatement.js'; import { CallExpression } from './visitors/CallExpression.js'; +import { NewExpression } from './visitors/NewExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; import { Comment } from './visitors/Comment.js'; import { Component } from './visitors/Component.js'; @@ -91,6 +92,7 @@ const visitors = { BlockStatement, BreakStatement, CallExpression, + NewExpression, ClassBody, Comment, Component, @@ -134,14 +136,16 @@ const visitors = { /** * @param {ComponentAnalysis} analysis + * @param {string} source * @param {ValidatedCompileOptions} options * @returns {ESTree.Program} */ -export function client_component(analysis, options) { +export function client_component(analysis, source, options) { /** @type {ComponentClientTransformState} */ const state = { analysis, options, + source: source.split('\n'), scope: analysis.module.scope, scopes: analysis.module.scopes, is_instance: false, @@ -163,6 +167,7 @@ export function client_component(analysis, options) { private_state: new Map(), transform: {}, in_constructor: false, + trace_dependencies: false, // these are set inside the `Fragment` visitor, and cannot be used until then before_init: /** @type {any} */ (null), @@ -643,20 +648,23 @@ export function client_component(analysis, options) { /** * @param {Analysis} analysis + * @param {string} source * @param {ValidatedModuleCompileOptions} options * @returns {ESTree.Program} */ -export function client_module(analysis, options) { +export function client_module(analysis, source, options) { /** @type {ClientTransformState} */ const state = { analysis, options, + source: source.split('\n'), scope: analysis.module.scope, scopes: analysis.module.scopes, public_state: new Map(), private_state: new Map(), transform: {}, - in_constructor: false + in_constructor: false, + trace_dependencies: false }; const module = /** @type {ESTree.Program} */ ( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 0b54de5384..37beea6b54 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -23,6 +23,10 @@ export interface ClientTransformState extends TransformState { */ readonly in_constructor: boolean; + readonly source: string[]; + + readonly trace_dependencies: boolean; + readonly transform: Record< string, { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index c460905977..60e939373b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -351,3 +351,50 @@ export function is_inlinable_expression(node_or_nodes, state) { } return has_expression_tag; } + +/** + * @param {Expression} node + * @param {Expression} expression + * @param {ClientTransformState} state + */ +export function trace(node, expression, state) { + const loc = node.loc; + const source = state.source; + let code = ''; + + if (loc) { + const start = loc.start; + const end = loc.end; + + if (start.line === end.line) { + code = source[start.line - 1].slice(start.column, end.column); + } else { + for (let i = start.line; i < end.line + 1; i++) { + const loc = source[i - 1]; + + if (i === start.line) { + code += loc.slice(start.column) + '\n'; + } else if (i === end.line) { + code += loc.slice(0, end.column); + } else { + code += loc + '\n'; + } + } + } + } else if (node.start !== undefined && node.end !== undefined) { + code = source.join('\n').slice(node.start, node.end); + } else { + return expression; + } + + return b.call( + '$.trace', + b.thunk(expression), + b.literal(code), + node.type === 'CallExpression' || + node.type === 'MemberExpression' || + node.type === 'NewExpression' + ? b.literal(true) + : undefined + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js index 502fbd471e..e5bbe4cde5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js @@ -1,6 +1,7 @@ -/** @import { BlockStatement } from 'estree' */ +/** @import { BlockStatement, Statement } from 'estree' */ /** @import { ComponentContext } from '../types' */ import { add_state_transformers } from './shared/declarations.js'; +import * as b from '../../../../utils/builders.js'; /** * @param {BlockStatement} node @@ -8,5 +9,28 @@ import { add_state_transformers } from './shared/declarations.js'; */ export function BlockStatement(node, context) { add_state_transformers(context); + const tracing = context.state.scope.tracing; + + if (tracing !== null) { + return b.block([ + b.return( + b.call( + '$.log_trace', + b.thunk( + b.block( + node.body.map( + (n) => + /** @type {Statement} */ ( + context.visit(n, { ...context.state, trace_dependencies: true }) + ) + ) + ) + ), + b.literal(tracing) + ) + ) + ]); + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 7a3057451a..1e90c69a99 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -4,6 +4,7 @@ import { dev, is_ignored } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; import { get_rune } from '../../../scope.js'; import { transform_inspect_rune } from '../../utils.js'; +import { trace } from '../utils.js'; /** * @param {CallExpression} node @@ -33,6 +34,9 @@ export function CallExpression(node, context) { case '$inspect': case '$inspect().with': return transform_inspect_rune(node, context); + + case '$trace': + return b.empty; } if ( @@ -58,5 +62,17 @@ export function CallExpression(node, context) { ); } + if (dev) { + return trace( + node, + { + ...node, + callee: /** @type {Expression} */ (context.visit(node.callee)), + arguments: node.arguments.map((arg) => /** @type {Expression} */ (context.visit(arg))) + }, + context.state + ); + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js index ae62909eff..53ae7a21ff 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js @@ -2,7 +2,8 @@ /** @import { Context } from '../types' */ import is_reference from 'is-reference'; import * as b from '../../../../utils/builders.js'; -import { build_getter } from '../utils.js'; +import { build_getter, trace } from '../utils.js'; +import { dev } from '../../../../state.js'; /** * @param {Identifier} node @@ -10,6 +11,7 @@ import { build_getter } from '../utils.js'; */ export function Identifier(node, context) { const parent = /** @type {Node} */ (context.path.at(-1)); + let transformed; if (is_reference(node, parent)) { if (node.name === '$$props') { @@ -32,10 +34,18 @@ export function Identifier(node, context) { grand_parent?.type !== 'AssignmentExpression' && grand_parent?.type !== 'UpdateExpression' ) { - return b.id('$$props'); + transformed = b.id('$$props'); } } - return build_getter(node, context.state); + if (!transformed) { + transformed = build_getter(node, context.state); + } + } + + if (transformed && transformed !== node && dev) { + return trace(node, transformed, context.state); } + + return transformed; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js index 501ecda555..ebfe61424b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js @@ -1,19 +1,46 @@ -/** @import { MemberExpression } from 'estree' */ +/** @import { MemberExpression, Expression, Super, PrivateIdentifier } from 'estree' */ /** @import { Context } from '../types' */ +import { dev } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; +import { trace } from '../utils.js'; /** * @param {MemberExpression} node * @param {Context} context */ export function MemberExpression(node, context) { + let transformed; // rewrite `this.#foo` as `this.#foo.v` inside a constructor if (node.property.type === 'PrivateIdentifier') { const field = context.state.private_state.get(node.property.name); if (field) { - return context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node); + transformed = context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node); } } + const parent = context.path.at(-1); + + if ( + dev && + // Bail out of tracing members if they're used as calees to avoid context issues + (parent?.type !== 'CallExpression' || parent.callee !== node) && + parent?.type !== 'BindDirective' && + parent?.type !== 'AssignmentExpression' && + parent?.type !== 'UpdateExpression' && + parent?.type !== 'Component' + ) { + return trace( + node, + transformed || { + ...node, + object: /** @type {Expression | Super} */ (context.visit(node.object)), + property: /** @type {Expression | PrivateIdentifier} */ (context.visit(node.property)) + }, + context.state + ); + } else if (transformed) { + return transformed; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/NewExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/NewExpression.js new file mode 100644 index 0000000000..1a3ad56488 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/NewExpression.js @@ -0,0 +1,24 @@ +/** @import { NewExpression, Expression } from 'estree' */ +/** @import { Context } from '../types' */ +import { dev } from '../../../../state.js'; +import { trace } from '../utils.js'; + +/** + * @param {NewExpression} node + * @param {Context} context + */ +export function NewExpression(node, context) { + if (dev) { + return trace( + node, + { + ...node, + callee: /** @type {Expression} */ (context.visit(node.callee)), + arguments: node.arguments.map((arg) => /** @type {Expression} */ (context.visit(arg))) + }, + context.state + ); + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 04685b66bd..d045073729 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -28,7 +28,8 @@ export function VariableDeclaration(node, context) { rune === '$effect.root' || rune === '$inspect' || rune === '$state.snapshot' || - rune === '$host' + rune === '$host' || + rune === '$trace' ) { if (init != null && is_hoisted_function(init)) { context.state.hoisted.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index 8f6597ee12..b1a59e6f15 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -30,7 +30,7 @@ export function transform_component(analysis, source, options) { const program = options.generate === 'server' ? server_component(analysis, options) - : client_component(analysis, options); + : client_component(analysis, source, options); const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte'); const js = print(program, { @@ -79,7 +79,7 @@ export function transform_module(analysis, source, options) { const program = options.generate === 'server' ? server_module(analysis, options) - : client_module(analysis, options); + : client_module(analysis, source, options); const basename = options.filename.split(/[/\\]/).at(-1); if (program.body.length > 0) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 386c6b6ff3..308aec5625 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -37,5 +37,9 @@ export function CallExpression(node, context) { return transform_inspect_rune(node, context); } + if (rune === '$trace') { + return b.empty; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 0542a20708..cf999675d0 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -58,6 +58,12 @@ export class Scope { */ function_depth = 0; + /** + * If tracing of reactive dependencies is enabled for this scope + * @type {null | string} + */ + tracing = null; + /** * * @param {ScopeRoot} root diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js new file mode 100644 index 0000000000..beccfae54a --- /dev/null +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -0,0 +1,197 @@ +import { snapshot } from '../../shared/clone.js'; +import { define_property } from '../../shared/utils.js'; +import { STATE_SYMBOL } from '../constants.js'; +import { captured_signals, set_captured_signals } from '../runtime.js'; + +export const NOT_REACTIVE = 0; +export const REACTIVE_UNCHANGED = 1; +export const REACTIVE_CHANGED = 2; + +/** @type { { changed: boolean, label: string, time: number, sub: any, stacks: any[], value: any }[] | null } */ +export let tracing_expressions = null; +/** @type { 0 | 1 | 2 } */ +export let tracing_expression_reactive = NOT_REACTIVE; + +/** + * @param {any} expressions + */ +function log_expressions(expressions) { + for (let expression of expressions) { + const val = expression.value; + const label = expression.label; + const time = expression.time; + const changed = expression.changed; + + if (time) { + // eslint-disable-next-line no-console + console.groupCollapsed( + `%c${label} %c(${time.toFixed(2)}ms)`, + changed ? 'color: CornflowerBlue; font-weight: bold' : 'color: grey; font-weight: bold', + 'color: grey', + val && typeof val === 'object' && STATE_SYMBOL in val ? snapshot(val, true) : val + ); + } else { + // eslint-disable-next-line no-console + console.groupCollapsed( + `%c${label}`, + changed ? 'color: CornflowerBlue; font-weight: bold' : 'color: grey; font-weight: bold', + val && typeof val === 'object' && STATE_SYMBOL in val ? snapshot(val, true) : val + ); + } + + if (expression.sub) { + log_expressions(expression.sub); + } + + for (var [name, stack] of expression.stacks) { + // eslint-disable-next-line no-console + console.groupCollapsed('%c' + name + ' stack', 'color: white; font-weight: normal;'); + // eslint-disable-next-line no-console + console.log(stack); + // eslint-disable-next-line no-console + console.groupEnd(); + } + + // eslint-disable-next-line no-console + console.groupEnd(); + } +} + +/** + * @template T + * @param {() => T} fn + * @param {string} label + */ +export function log_trace(fn, label) { + var previously_tracing_expressions = tracing_expressions; + try { + tracing_expressions = []; + + var start = performance.now(); + var value = fn(); + var time = (performance.now() - start).toFixed(2); + if (tracing_expressions.length > 0) { + // eslint-disable-next-line no-console + console.group(`${label} %c(${time}ms)`, 'color: grey'); + log_expressions(tracing_expressions); + // eslint-disable-next-line no-console + console.groupEnd(); + } else { + // eslint-disable-next-line no-console + console.log(`${label} %cno reactive dependencies (${time}ms)`, 'color: grey'); + } + + if (previously_tracing_expressions !== null) { + previously_tracing_expressions.push(...tracing_expressions); + } + + return value; + } finally { + tracing_expressions = previously_tracing_expressions; + } +} + +/** + * @template T + * @param {() => T} fn + * @param {boolean} [computed] + * @param {string} label + */ +export function trace(fn, label, computed) { + // If we aren't capturing the trace, just return the value + if (tracing_expressions === null) { + return fn(); + } + var previously_tracing_expressions = tracing_expressions; + var previously_tracing_expression_reactive = tracing_expression_reactive; + var previous_captured_signals = captured_signals; + var signals = new Set(); + set_captured_signals(signals); + + try { + tracing_expression_reactive = NOT_REACTIVE; + tracing_expressions = []; + var value, + time = 0; + + if (computed) { + var start = performance.now(); + value = fn(); + time = performance.now() - start; + } else { + value = fn(); + } + + if (tracing_expressions !== null) { + var read_stack = ['read', get_stack()]; + + if (tracing_expression_reactive !== NOT_REACTIVE) { + var write_stack; + if (signals.size === 1) { + write_stack = Array.from(signals)[0].stack; + } + tracing_expressions.push({ + changed: tracing_expression_reactive === REACTIVE_CHANGED, + label, + value, + time, + stacks: [read_stack, write_stack], + sub: null + }); + + if (previously_tracing_expressions !== null) { + previously_tracing_expressions.push(...tracing_expressions); + } + } else if (tracing_expressions.length !== 0) { + previously_tracing_expressions.push({ + changed: tracing_expressions.some((e) => e.changed), + label, + value, + time, + stacks: [read_stack], + sub: tracing_expressions + }); + } + } + + return value; + } finally { + tracing_expressions = previously_tracing_expressions; + tracing_expression_reactive = previously_tracing_expression_reactive; + set_captured_signals(previous_captured_signals); + } +} + +/** + * @param {0 | 1 | 2} value + */ +export function set_tracing_expression_reactive(value) { + tracing_expression_reactive = value; +} + +export function get_stack() { + let error = Error() + const stack = error.stack; + + if (stack) { + const lines = stack.split('\n'); + const new_lines = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.includes('svelte/src/internal')) { + continue; + } + new_lines.push(line); + } + + define_property(error, 'stack', { + value: new_lines.join('\n') + }); + + define_property(error, 'name', { + value: 'TraceInvokedError' + }); + } + return error; +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c401867a0f..b300fd31ff 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -11,6 +11,7 @@ export { skip_ownership_validation } from './dev/ownership.js'; export { check_target, legacy_api } from './dev/legacy.js'; +export { log_trace, trace } from './dev/tracing.js'; export { inspect } from './dev/inspect.js'; export { await_block as await } from './dom/blocks/await.js'; export { if_block as if } from './dom/blocks/if.js'; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 4bbd470d08..333ea4fceb 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -33,6 +33,7 @@ import { } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag } from '../../flags/index.js'; +import { get_stack } from '../dev/tracing.js'; export let inspect_effects = new Set(); @@ -49,13 +50,20 @@ export function set_inspect_effects(v) { * @returns {Source} */ export function source(v) { - return { + /** @type {Value} */ + var signal = { f: 0, // TODO ideally we could skip this altogether, but it causes type errors v, reactions: null, equals, version: 0 }; + + if (DEV) { + signal.stack = ['created', get_stack()]; + } + + return signal; } /** @@ -160,6 +168,10 @@ export function internal_set(source, value) { source.v = value; source.version = increment_version(); + if (DEV) { + source.stack = ['updated', get_stack()]; + } + mark_reactions(source, DIRTY); // If the current signal is running for the first time, it won't have any diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 2cef49eb2d..71cf8952c3 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -14,6 +14,8 @@ export interface Value extends Signal { equals: Equals; /** The latest value for this signal */ v: V; + /** Dev only */ + stack?: [string, Error]; } export interface Reaction extends Signal { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5b5b488824..a03de203d6 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -34,6 +34,13 @@ import * as e from './errors.js'; import { lifecycle_outside_component } from '../shared/errors.js'; import { FILENAME } from '../../constants.js'; import { legacy_mode_flag } from '../flags/index.js'; +import { + tracing_expressions, + set_tracing_expression_reactive, + REACTIVE_UNCHANGED, + REACTIVE_CHANGED, + tracing_expression_reactive +} from './dev/tracing.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; @@ -131,6 +138,11 @@ export let skip_reaction = false; /** @type {Set | null} */ export let captured_signals = null; +/** @param {Set | null} value */ +export function set_captured_signals(value) { + captured_signals = value; +} + // Handling runtime component context /** @type {ComponentContext | null} */ export let component_context = null; @@ -283,7 +295,7 @@ function handle_error(error, effect, component_context) { new_lines.push(line); } define_property(error, 'stack', { - value: error.stack + new_lines.join('\n') + value: new_lines.join('\n') }); } @@ -781,6 +793,19 @@ export function get(signal) { } } + if ( + DEV && + active_reaction !== null && + tracing_expressions !== null && + tracing_expression_reactive !== REACTIVE_UNCHANGED + ) { + set_tracing_expression_reactive( + signal.version > active_reaction.version || active_reaction.version === current_version + ? REACTIVE_CHANGED + : REACTIVE_UNCHANGED + ); + } + return signal.v; } diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index 60ec364e6f..d3c6bcb6f1 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -414,7 +414,8 @@ const RUNES = /** @type {const} */ ([ '$effect.root', '$inspect', '$inspect().with', - '$host' + '$host', + '$trace' ]); /** diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 00ba2556d9..0e29e3e725 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2707,4 +2707,6 @@ declare function $inspect( */ declare function $host(): El; +declare function $trace(name: string): void; + //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 4531e7182d..975613b5f4 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -48,12 +48,12 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { fs.writeFileSync(`${cwd}/output/${file}.json`, JSON.stringify(ast, null, '\t')); - try { - const migrated = migrate(source); - fs.writeFileSync(`${cwd}/output/${file}.migrated.svelte`, migrated.code); - } catch (e) { - console.warn(`Error migrating ${file}`, e); - } + // try { + // const migrated = migrate(source); + // fs.writeFileSync(`${cwd}/output/${file}.migrated.svelte`, migrated.code); + // } catch (e) { + // console.warn(`Error migrating ${file}`, e); + // } } const compiled = compile(source, {