Skip to content

Commit 4657664

Browse files
committed
feat: add $trace rune
WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP
1 parent 4a85c41 commit 4657664

File tree

23 files changed

+464
-31
lines changed

23 files changed

+464
-31
lines changed

packages/svelte/src/ambient.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,3 +399,5 @@ declare function $inspect<T extends any[]>(
399399
* https://svelte.dev/docs/svelte/$host
400400
*/
401401
declare function $host<El extends HTMLElement = HTMLElement>(): El;
402+
403+
declare function $trace(name: string): void;

packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { get_rune } from '../../scope.js';
55
import * as e from '../../../errors.js';
66
import { get_parent, unwrap_optional } from '../../../utils/ast.js';
77
import { is_pure, is_safe_identifier } from './shared/utils.js';
8+
import { dev } from '../../../state.js';
89

910
/**
1011
* @param {CallExpression} node
@@ -135,6 +136,28 @@ export function CallExpression(node, context) {
135136

136137
break;
137138

139+
case '$trace':
140+
if (node.arguments.length !== 1) {
141+
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
142+
}
143+
if (node.arguments[0].type !== 'Literal' || typeof node.arguments[0].value !== 'string') {
144+
throw new Error('TODO: $track requires a string argument');
145+
}
146+
if (parent.type !== 'ExpressionStatement' || context.path.at(-2)?.type !== 'BlockStatement') {
147+
throw new Error('TODO: $track must be inside a block statement');
148+
}
149+
150+
if (context.state.scope.tracing) {
151+
throw new Error('TODO: $track must only be used once within the same block statement');
152+
}
153+
154+
if (dev) {
155+
// TODO should we validate if tracing is already enabled in this or a parent scope?
156+
context.state.scope.tracing = node.arguments[0].value;
157+
}
158+
159+
break;
160+
138161
case '$state.snapshot':
139162
if (node.arguments.length !== 1) {
140163
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { BindDirective } from './visitors/BindDirective.js';
1717
import { BlockStatement } from './visitors/BlockStatement.js';
1818
import { BreakStatement } from './visitors/BreakStatement.js';
1919
import { CallExpression } from './visitors/CallExpression.js';
20+
import { NewExpression } from './visitors/NewExpression.js';
2021
import { ClassBody } from './visitors/ClassBody.js';
2122
import { Comment } from './visitors/Comment.js';
2223
import { Component } from './visitors/Component.js';
@@ -91,6 +92,7 @@ const visitors = {
9192
BlockStatement,
9293
BreakStatement,
9394
CallExpression,
95+
NewExpression,
9496
ClassBody,
9597
Comment,
9698
Component,
@@ -134,14 +136,16 @@ const visitors = {
134136

135137
/**
136138
* @param {ComponentAnalysis} analysis
139+
* @param {string} source
137140
* @param {ValidatedCompileOptions} options
138141
* @returns {ESTree.Program}
139142
*/
140-
export function client_component(analysis, options) {
143+
export function client_component(analysis, source, options) {
141144
/** @type {ComponentClientTransformState} */
142145
const state = {
143146
analysis,
144147
options,
148+
source: source.split('\n'),
145149
scope: analysis.module.scope,
146150
scopes: analysis.module.scopes,
147151
is_instance: false,
@@ -163,6 +167,7 @@ export function client_component(analysis, options) {
163167
private_state: new Map(),
164168
transform: {},
165169
in_constructor: false,
170+
trace_dependencies: false,
166171

167172
// these are set inside the `Fragment` visitor, and cannot be used until then
168173
before_init: /** @type {any} */ (null),
@@ -643,20 +648,23 @@ export function client_component(analysis, options) {
643648

644649
/**
645650
* @param {Analysis} analysis
651+
* @param {string} source
646652
* @param {ValidatedModuleCompileOptions} options
647653
* @returns {ESTree.Program}
648654
*/
649-
export function client_module(analysis, options) {
655+
export function client_module(analysis, source, options) {
650656
/** @type {ClientTransformState} */
651657
const state = {
652658
analysis,
653659
options,
660+
source: source.split('\n'),
654661
scope: analysis.module.scope,
655662
scopes: analysis.module.scopes,
656663
public_state: new Map(),
657664
private_state: new Map(),
658665
transform: {},
659-
in_constructor: false
666+
in_constructor: false,
667+
trace_dependencies: false
660668
};
661669

662670
const module = /** @type {ESTree.Program} */ (

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export interface ClientTransformState extends TransformState {
2323
*/
2424
readonly in_constructor: boolean;
2525

26+
readonly source: string[];
27+
28+
readonly trace_dependencies: boolean;
29+
2630
readonly transform: Record<
2731
string,
2832
{

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,50 @@ export function is_inlinable_expression(node_or_nodes, state) {
351351
}
352352
return has_expression_tag;
353353
}
354+
355+
/**
356+
* @param {Expression} node
357+
* @param {Expression} expression
358+
* @param {ClientTransformState} state
359+
*/
360+
export function trace(node, expression, state) {
361+
const loc = node.loc;
362+
const source = state.source;
363+
let code = '';
364+
365+
if (loc) {
366+
const start = loc.start;
367+
const end = loc.end;
368+
369+
if (start.line === end.line) {
370+
code = source[start.line - 1].slice(start.column, end.column);
371+
} else {
372+
for (let i = start.line; i < end.line + 1; i++) {
373+
const loc = source[i - 1];
374+
375+
if (i === start.line) {
376+
code += loc.slice(start.column) + '\n';
377+
} else if (i === end.line) {
378+
code += loc.slice(0, end.column);
379+
} else {
380+
code += loc + '\n';
381+
}
382+
}
383+
}
384+
} else if (node.start !== undefined && node.end !== undefined) {
385+
code = source.join('\n').slice(node.start, node.end);
386+
} else {
387+
return expression;
388+
}
389+
390+
return b.call(
391+
'$.trace',
392+
b.thunk(expression),
393+
b.literal(code),
394+
node.type === 'CallExpression' ||
395+
node.type === 'MemberExpression' ||
396+
node.type === 'NewExpression'
397+
? b.literal(true)
398+
: undefined
399+
);
400+
}
Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
1-
/** @import { BlockStatement } from 'estree' */
1+
/** @import { BlockStatement, Statement } from 'estree' */
22
/** @import { ComponentContext } from '../types' */
33
import { add_state_transformers } from './shared/declarations.js';
4+
import * as b from '../../../../utils/builders.js';
45

56
/**
67
* @param {BlockStatement} node
78
* @param {ComponentContext} context
89
*/
910
export function BlockStatement(node, context) {
1011
add_state_transformers(context);
12+
const tracing = context.state.scope.tracing;
13+
14+
if (tracing !== null) {
15+
return b.block([
16+
b.return(
17+
b.call(
18+
'$.log_trace',
19+
b.thunk(
20+
b.block(
21+
node.body.map(
22+
(n) =>
23+
/** @type {Statement} */ (
24+
context.visit(n, { ...context.state, trace_dependencies: true })
25+
)
26+
)
27+
)
28+
),
29+
b.literal(tracing)
30+
)
31+
)
32+
]);
33+
}
34+
1135
context.next();
1236
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { dev, is_ignored } from '../../../../state.js';
44
import * as b from '../../../../utils/builders.js';
55
import { get_rune } from '../../../scope.js';
66
import { transform_inspect_rune } from '../../utils.js';
7+
import { trace } from '../utils.js';
78

89
/**
910
* @param {CallExpression} node
@@ -33,6 +34,9 @@ export function CallExpression(node, context) {
3334
case '$inspect':
3435
case '$inspect().with':
3536
return transform_inspect_rune(node, context);
37+
38+
case '$trace':
39+
return b.empty;
3640
}
3741

3842
if (
@@ -58,5 +62,17 @@ export function CallExpression(node, context) {
5862
);
5963
}
6064

65+
if (dev) {
66+
return trace(
67+
node,
68+
{
69+
...node,
70+
callee: /** @type {Expression} */ (context.visit(node.callee)),
71+
arguments: node.arguments.map((arg) => /** @type {Expression} */ (context.visit(arg)))
72+
},
73+
context.state
74+
);
75+
}
76+
6177
context.next();
6278
}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
/** @import { Context } from '../types' */
33
import is_reference from 'is-reference';
44
import * as b from '../../../../utils/builders.js';
5-
import { build_getter } from '../utils.js';
5+
import { build_getter, trace } from '../utils.js';
6+
import { dev } from '../../../../state.js';
67

78
/**
89
* @param {Identifier} node
910
* @param {Context} context
1011
*/
1112
export function Identifier(node, context) {
1213
const parent = /** @type {Node} */ (context.path.at(-1));
14+
let transformed;
1315

1416
if (is_reference(node, parent)) {
1517
if (node.name === '$$props') {
@@ -32,10 +34,18 @@ export function Identifier(node, context) {
3234
grand_parent?.type !== 'AssignmentExpression' &&
3335
grand_parent?.type !== 'UpdateExpression'
3436
) {
35-
return b.id('$$props');
37+
transformed = b.id('$$props');
3638
}
3739
}
3840

39-
return build_getter(node, context.state);
41+
if (!transformed) {
42+
transformed = build_getter(node, context.state);
43+
}
44+
}
45+
46+
if (transformed && transformed !== node && dev) {
47+
return trace(node, transformed, context.state);
4048
}
49+
50+
return transformed;
4151
}
Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
1-
/** @import { MemberExpression } from 'estree' */
1+
/** @import { MemberExpression, Expression, Super, PrivateIdentifier } from 'estree' */
22
/** @import { Context } from '../types' */
3+
import { dev } from '../../../../state.js';
34
import * as b from '../../../../utils/builders.js';
5+
import { trace } from '../utils.js';
46

57
/**
68
* @param {MemberExpression} node
79
* @param {Context} context
810
*/
911
export function MemberExpression(node, context) {
12+
let transformed;
1013
// rewrite `this.#foo` as `this.#foo.v` inside a constructor
1114
if (node.property.type === 'PrivateIdentifier') {
1215
const field = context.state.private_state.get(node.property.name);
1316
if (field) {
14-
return context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node);
17+
transformed = context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node);
1518
}
1619
}
1720

21+
const parent = context.path.at(-1);
22+
23+
if (
24+
dev &&
25+
// Bail out of tracing members if they're used as calees to avoid context issues
26+
(parent?.type !== 'CallExpression' || parent.callee !== node) &&
27+
parent?.type !== 'BindDirective' &&
28+
parent?.type !== 'AssignmentExpression' &&
29+
parent?.type !== 'UpdateExpression' &&
30+
parent?.type !== 'Component'
31+
) {
32+
return trace(
33+
node,
34+
transformed || {
35+
...node,
36+
object: /** @type {Expression | Super} */ (context.visit(node.object)),
37+
property: /** @type {Expression | PrivateIdentifier} */ (context.visit(node.property))
38+
},
39+
context.state
40+
);
41+
} else if (transformed) {
42+
return transformed;
43+
}
44+
1845
context.next();
1946
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/** @import { NewExpression, Expression } from 'estree' */
2+
/** @import { Context } from '../types' */
3+
import { dev } from '../../../../state.js';
4+
import { trace } from '../utils.js';
5+
6+
/**
7+
* @param {NewExpression} node
8+
* @param {Context} context
9+
*/
10+
export function NewExpression(node, context) {
11+
if (dev) {
12+
return trace(
13+
node,
14+
{
15+
...node,
16+
callee: /** @type {Expression} */ (context.visit(node.callee)),
17+
arguments: node.arguments.map((arg) => /** @type {Expression} */ (context.visit(arg)))
18+
},
19+
context.state
20+
);
21+
}
22+
23+
context.next();
24+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export function VariableDeclaration(node, context) {
2828
rune === '$effect.root' ||
2929
rune === '$inspect' ||
3030
rune === '$state.snapshot' ||
31-
rune === '$host'
31+
rune === '$host' ||
32+
rune === '$trace'
3233
) {
3334
if (init != null && is_hoisted_function(init)) {
3435
context.state.hoisted.push(

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function transform_component(analysis, source, options) {
3030
const program =
3131
options.generate === 'server'
3232
? server_component(analysis, options)
33-
: client_component(analysis, options);
33+
: client_component(analysis, source, options);
3434

3535
const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte');
3636
const js = print(program, {
@@ -79,7 +79,7 @@ export function transform_module(analysis, source, options) {
7979
const program =
8080
options.generate === 'server'
8181
? server_module(analysis, options)
82-
: client_module(analysis, options);
82+
: client_module(analysis, source, options);
8383

8484
const basename = options.filename.split(/[/\\]/).at(-1);
8585
if (program.body.length > 0) {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,9 @@ export function CallExpression(node, context) {
3737
return transform_inspect_rune(node, context);
3838
}
3939

40+
if (rune === '$trace') {
41+
return b.empty;
42+
}
43+
4044
context.next();
4145
}

0 commit comments

Comments
 (0)