Skip to content

Commit b1b2ddd

Browse files
authored
feat: add support for svelte inspector (alternative approach) (#11514)
* mostly working * fix * fix * handle dynamic elements too * add __svelte_meta to prototype * changeset * cheeky fix
1 parent 3c756cf commit b1b2ddd

File tree

13 files changed

+329
-33
lines changed

13 files changed

+329
-33
lines changed

.changeset/cool-jobs-scream.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: add support for svelte inspector

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { javascript_visitors_runes } from './visitors/javascript-runes.js';
88
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
99
import { serialize_get_binding } from './utils.js';
1010
import { render_stylesheet } from '../css/index.js';
11+
import { getLocator } from 'locate-character';
1112

1213
/**
1314
* This function ensures visitor sets don't accidentally clobber each other
@@ -47,6 +48,7 @@ export function client_component(source, analysis, options) {
4748
scopes: analysis.template.scopes,
4849
hoisted: [b.import_all('$', 'svelte/internal/client')],
4950
node: /** @type {any} */ (null), // populated by the root node
51+
source_locator: getLocator(source, { offsetLine: 1 }),
5052
// these should be set by create_block - if they're called outside, it's a bug
5153
get before_init() {
5254
/** @type {any[]} */
@@ -88,6 +90,14 @@ export function client_component(source, analysis, options) {
8890
};
8991
return a;
9092
},
93+
get locations() {
94+
/** @type {any[]} */
95+
const a = [];
96+
a.push = () => {
97+
throw new Error('locations.push should not be called outside create_block');
98+
};
99+
return a;
100+
},
91101
legacy_reactive_statements: new Map(),
92102
metadata: {
93103
context: {
@@ -466,7 +476,7 @@ export function client_component(source, analysis, options) {
466476
}
467477

468478
// add `App.filename = 'App.svelte'` so that we can print useful messages later
469-
body.push(
479+
body.unshift(
470480
b.stmt(
471481
b.assignment('=', b.member(b.id(analysis.name), b.id('filename')), b.literal(filename))
472482
)

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
99
import type { TransformState } from '../types.js';
1010
import type { ComponentAnalysis } from '../../types.js';
11+
import type { Location } from 'locate-character';
1112

1213
export interface ClientTransformState extends TransformState {
1314
readonly private_state: Map<string, StateField>;
@@ -23,11 +24,19 @@ export interface ClientTransformState extends TransformState {
2324
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
2425
}
2526

27+
export type SourceLocation =
28+
| [line: number, column: number]
29+
| [line: number, column: number, SourceLocation[]];
30+
2631
export interface ComponentClientTransformState extends ClientTransformState {
2732
readonly analysis: ComponentAnalysis;
2833
readonly options: ValidatedCompileOptions;
2934
readonly hoisted: Array<Statement | ModuleDeclaration>;
3035
readonly events: Set<string>;
36+
readonly source_locator: (
37+
search: string | number,
38+
index?: number | undefined
39+
) => Location | undefined;
3140

3241
/** Stuff that happens before the render effect(s) */
3342
readonly before_init: Statement[];
@@ -39,6 +48,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
3948
readonly after_update: Statement[];
4049
/** The HTML template string */
4150
readonly template: string[];
51+
readonly locations: SourceLocation[];
4252
readonly metadata: {
4353
namespace: Namespace;
4454
bound_contenteditable: boolean;

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

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,23 @@ function serialize_bind_this(bind_this, context, node) {
959959
return b.call('$.bind_this', ...args);
960960
}
961961

962+
/**
963+
* @param {import('../types.js').SourceLocation[]} locations
964+
*/
965+
function serialize_locations(locations) {
966+
return b.array(
967+
locations.map((loc) => {
968+
const expression = b.array([b.literal(loc[0]), b.literal(loc[1])]);
969+
970+
if (loc.length === 3) {
971+
expression.elements.push(serialize_locations(loc[2]));
972+
}
973+
974+
return expression;
975+
})
976+
);
977+
}
978+
962979
/**
963980
* Creates a new block which looks roughly like this:
964981
* ```js
@@ -1014,6 +1031,7 @@ function create_block(parent, name, nodes, context) {
10141031
update: [],
10151032
after_update: [],
10161033
template: [],
1034+
locations: [],
10171035
metadata: {
10181036
context: {
10191037
template_needs_import_node: false,
@@ -1028,6 +1046,24 @@ function create_block(parent, name, nodes, context) {
10281046
context.visit(node, state);
10291047
}
10301048

1049+
/**
1050+
* @param {import('estree').Identifier} template_name
1051+
* @param {import('estree').Expression[]} args
1052+
*/
1053+
const add_template = (template_name, args) => {
1054+
let call = b.call(get_template_function(namespace, state), ...args);
1055+
if (context.state.options.dev) {
1056+
call = b.call(
1057+
'$.add_locations',
1058+
call,
1059+
b.member(b.id(context.state.analysis.name), b.id('filename')),
1060+
serialize_locations(state.locations)
1061+
);
1062+
}
1063+
1064+
context.state.hoisted.push(b.var(template_name, call));
1065+
};
1066+
10311067
if (is_single_element) {
10321068
const element = /** @type {import('#compiler').RegularElement} */ (trimmed[0]);
10331069

@@ -1045,9 +1081,7 @@ function create_block(parent, name, nodes, context) {
10451081
args.push(b.literal(TEMPLATE_USE_IMPORT_NODE));
10461082
}
10471083

1048-
context.state.hoisted.push(
1049-
b.var(template_name, b.call(get_template_function(namespace, state), ...args))
1050-
);
1084+
add_template(template_name, args);
10511085

10521086
body.push(b.var(id, b.call(template_name)), ...state.before_init, ...state.init);
10531087
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
@@ -1091,16 +1125,10 @@ function create_block(parent, name, nodes, context) {
10911125
flags |= TEMPLATE_USE_IMPORT_NODE;
10921126
}
10931127

1094-
state.hoisted.push(
1095-
b.var(
1096-
template_name,
1097-
b.call(
1098-
get_template_function(namespace, state),
1099-
b.template([b.quasi(state.template.join(''), true)], []),
1100-
b.literal(flags)
1101-
)
1102-
)
1103-
);
1128+
add_template(template_name, [
1129+
b.template([b.quasi(state.template.join(''), true)], []),
1130+
b.literal(flags)
1131+
]);
11041132

11051133
body.push(b.var(id, b.call(template_name)));
11061134
}
@@ -1809,6 +1837,18 @@ export const template_visitors = {
18091837
state.init.push(b.stmt(b.call('$.transition', ...args)));
18101838
},
18111839
RegularElement(node, context) {
1840+
/** @type {import('../types.js').SourceLocation} */
1841+
let location = [-1, -1];
1842+
1843+
if (context.state.options.dev) {
1844+
const loc = context.state.source_locator(node.start);
1845+
if (loc) {
1846+
location[0] = loc.line;
1847+
location[1] = loc.column;
1848+
context.state.locations.push(location);
1849+
}
1850+
}
1851+
18121852
if (node.name === 'noscript') {
18131853
context.state.template.push('<!>');
18141854
return;
@@ -1993,10 +2033,14 @@ export const template_visitors = {
19932033

19942034
context.state.template.push('>');
19952035

2036+
/** @type {import('../types.js').SourceLocation[]} */
2037+
const child_locations = [];
2038+
19962039
/** @type {import('../types').ComponentClientTransformState} */
19972040
const state = {
19982041
...context.state,
19992042
metadata: child_metadata,
2043+
locations: child_locations,
20002044
scope: /** @type {import('../../../scope').Scope} */ (
20012045
context.state.scopes.get(node.fragment)
20022046
),
@@ -2032,6 +2076,11 @@ export const template_visitors = {
20322076
{ ...context, state }
20332077
);
20342078

2079+
if (child_locations.length > 0) {
2080+
// @ts-expect-error
2081+
location.push(child_locations);
2082+
}
2083+
20352084
if (!VoidElements.includes(node.name)) {
20362085
context.state.template.push(`</${node.name}>`);
20372086
}
@@ -2131,19 +2180,21 @@ export const template_visitors = {
21312180
})
21322181
);
21332182

2134-
const args = [
2135-
context.state.node,
2136-
get_tag,
2137-
node.metadata.svg || node.metadata.mathml ? b.true : b.false
2138-
];
2139-
if (inner.length > 0) {
2140-
args.push(b.arrow([element_id, b.id('$$anchor')], b.block(inner)));
2141-
}
2142-
if (dynamic_namespace) {
2143-
if (inner.length === 0) args.push(b.id('undefined'));
2144-
args.push(b.thunk(serialize_attribute_value(dynamic_namespace, context)[1]));
2145-
}
2146-
context.state.init.push(b.stmt(b.call('$.element', ...args)));
2183+
const location = context.state.options.dev && context.state.source_locator(node.start);
2184+
2185+
context.state.init.push(
2186+
b.stmt(
2187+
b.call(
2188+
'$.element',
2189+
context.state.node,
2190+
get_tag,
2191+
node.metadata.svg || node.metadata.mathml ? b.true : b.false,
2192+
inner.length > 0 && b.arrow([element_id, b.id('$$anchor')], b.block(inner)),
2193+
dynamic_namespace && b.thunk(serialize_attribute_value(dynamic_namespace, context)[1]),
2194+
location && b.array([b.literal(location.line), b.literal(location.column)])
2195+
)
2196+
)
2197+
);
21472198
},
21482199
EachBlock(node, context) {
21492200
const each_node_meta = node.metadata;

packages/svelte/src/compiler/utils/builders.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,34 @@ export function labeled(name, body) {
9999

100100
/**
101101
* @param {string | import('estree').Expression} callee
102-
* @param {...(import('estree').Expression | import('estree').SpreadElement)} args
102+
* @param {...(import('estree').Expression | import('estree').SpreadElement | false | undefined)} args
103103
* @returns {import('estree').CallExpression}
104104
*/
105105
export function call(callee, ...args) {
106106
if (typeof callee === 'string') callee = id(callee);
107107
args = args.slice();
108108

109-
while (args.length > 0 && !args.at(-1)) args.pop();
109+
// replacing missing arguments with `undefined`, unless they're at the end in which case remove them
110+
let i = args.length;
111+
let popping = true;
112+
while (i--) {
113+
if (!args[i]) {
114+
if (popping) {
115+
args.pop();
116+
} else {
117+
args[i] = id('undefined');
118+
}
119+
} else {
120+
popping = false;
121+
}
122+
}
110123

111124
return {
112125
type: 'CallExpression',
113126
callee,
114-
arguments: args,
127+
arguments: /** @type {Array<import('estree').Expression | import('estree').SpreadElement>} */ (
128+
args
129+
),
115130
optional: false
116131
};
117132
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { HYDRATION_END, HYDRATION_START } from '../../../constants.js';
2+
import { hydrating } from '../dom/hydration.js';
3+
import { is_array } from '../utils.js';
4+
5+
/**
6+
* @param {any} fn
7+
* @param {string} filename
8+
* @param {import('../../../compiler/phases/3-transform/client/types.js').SourceLocation[]} locations
9+
* @returns {any}
10+
*/
11+
export function add_locations(fn, filename, locations) {
12+
return (/** @type {any[]} */ ...args) => {
13+
const dom = fn(...args);
14+
15+
const nodes = hydrating
16+
? is_array(dom)
17+
? dom
18+
: [dom]
19+
: dom.nodeType === 11
20+
? Array.from(dom.childNodes)
21+
: [dom];
22+
23+
assign_locations(nodes, filename, locations);
24+
25+
return dom;
26+
};
27+
}
28+
29+
/**
30+
* @param {Element} element
31+
* @param {string} filename
32+
* @param {import('../../../compiler/phases/3-transform/client/types.js').SourceLocation} location
33+
*/
34+
function assign_location(element, filename, location) {
35+
// @ts-expect-error
36+
element.__svelte_meta = {
37+
loc: { filename, line: location[0], column: location[1] }
38+
};
39+
40+
if (location[2]) {
41+
assign_locations(
42+
/** @type {import('#client').TemplateNode[]} */ (Array.from(element.childNodes)),
43+
filename,
44+
location[2]
45+
);
46+
}
47+
}
48+
49+
/**
50+
* @param {import('#client').TemplateNode[]} nodes
51+
* @param {string} filename
52+
* @param {import('../../../compiler/phases/3-transform/client/types.js').SourceLocation[]} locations
53+
*/
54+
function assign_locations(nodes, filename, locations) {
55+
var j = 0;
56+
var depth = 0;
57+
58+
for (var i = 0; i < nodes.length; i += 1) {
59+
var node = nodes[i];
60+
61+
if (hydrating && node.nodeType === 8) {
62+
var comment = /** @type {Comment} */ (node);
63+
if (comment.data === HYDRATION_START) depth += 1;
64+
if (comment.data.startsWith(HYDRATION_END)) depth -= 1;
65+
}
66+
67+
if (depth === 0 && node.nodeType === 1) {
68+
assign_location(/** @type {Element} */ (node), filename, locations[j++]);
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)