@fluixi/compiler 1.0.0-alpha.82 → 1.0.0-alpha.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyze/static-graph.cjs +1 -0
- package/dist/analyze/static-graph.d.ts +103 -0
- package/dist/analyze/static-graph.d.ts.map +1 -0
- package/dist/analyze/static-graph.js +869 -0
- package/dist/analyze/static-graph.mjs +1 -0
- package/dist/{babel-4JUDAR2G.mjs → babel-VXKH6MRQ.mjs} +1 -1
- package/dist/chunk-5KMMN5QP.mjs +1 -0
- package/dist/chunk-YKZ54NVE.mjs +1 -0
- package/dist/codegen/backends/imperative.cjs +1 -1
- package/dist/codegen/backends/imperative.d.ts.map +1 -1
- package/dist/codegen/backends/imperative.js +62 -11
- package/dist/codegen/backends/imperative.mjs +1 -1
- package/dist/codegen/partial-template.cjs +1 -1
- package/dist/codegen/partial-template.mjs +1 -1
- package/dist/codegen/serialize-static.cjs +1 -1
- package/dist/codegen/serialize-static.d.ts +16 -0
- package/dist/codegen/serialize-static.d.ts.map +1 -1
- package/dist/codegen/serialize-static.js +25 -0
- package/dist/codegen/serialize-static.mjs +1 -1
- package/dist/frontend/babel/build-ir-lit.cjs +1 -1
- package/dist/frontend/babel/build-ir-lit.mjs +1 -1
- package/dist/frontend/babel/build-ir.cjs +1 -1
- package/dist/frontend/babel/build-ir.mjs +1 -1
- package/dist/frontend/babel/index.cjs +1 -1
- package/dist/frontend/babel/index.mjs +1 -1
- package/dist/frontend/babel/lower-template.cjs +1 -1
- package/dist/frontend/babel/lower-template.mjs +1 -1
- package/dist/frontend/babel/plugin.cjs +1 -1
- package/dist/frontend/babel/plugin.mjs +1 -1
- package/dist/index.cjs +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.mjs +8 -8
- package/dist/integrations.cjs +18 -18
- package/dist/integrations.d.ts +16 -0
- package/dist/integrations.d.ts.map +1 -1
- package/dist/integrations.js +21 -2
- package/dist/integrations.mjs +5 -5
- package/dist/ir/nodes.cjs +1 -1
- package/dist/ir/nodes.d.ts +46 -0
- package/dist/ir/nodes.d.ts.map +1 -1
- package/dist/lower/compile-template.cjs +1 -1
- package/dist/lower/compile-template.d.ts +12 -0
- package/dist/lower/compile-template.d.ts.map +1 -1
- package/dist/lower/compile-template.js +5 -1
- package/dist/lower/compile-template.mjs +1 -1
- package/dist/lower/jsx.cjs +1 -1
- package/dist/lower/jsx.d.ts +2 -0
- package/dist/lower/jsx.d.ts.map +1 -1
- package/dist/lower/jsx.js +25 -3
- package/dist/lower/jsx.mjs +1 -1
- package/dist/lower/template.cjs +1 -1
- package/dist/lower/template.d.ts +14 -1
- package/dist/lower/template.d.ts.map +1 -1
- package/dist/lower/template.js +101 -15
- package/dist/lower/template.mjs +1 -1
- package/dist/transform/index.cjs +5 -5
- package/dist/transform/index.mjs +12 -12
- package/dist/transform/source-locations.cjs +1 -0
- package/dist/transform/source-locations.d.ts +44 -0
- package/dist/transform/source-locations.d.ts.map +1 -0
- package/dist/transform/source-locations.js +238 -0
- package/dist/transform/source-locations.mjs +1 -0
- package/dist/transform/templates.cjs +4 -4
- package/dist/transform/templates.d.ts +12 -4
- package/dist/transform/templates.d.ts.map +1 -1
- package/dist/transform/templates.js +72 -7
- package/dist/transform/templates.mjs +11 -11
- package/dist/transform-C4ZXRVGP.mjs +8 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/dist/chunk-PVR2SN3B.mjs +0 -1
- package/dist/chunk-QUIYULS2.mjs +0 -1
- package/dist/chunk-YR3SAEO7.mjs +0 -1
- package/dist/transform-5WC4FWJE.mjs +0 -8
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reactive graph of a project, read out of the source instead of run.
|
|
3
|
+
*
|
|
4
|
+
* An approximation: a dependency means "this memo's body names that signal". It cannot know
|
|
5
|
+
* values, run counts or which branch ran, but it answers with nothing started.
|
|
6
|
+
*
|
|
7
|
+
* Shaped like the snapshot @fluixi/devtools puts on the wire so the same renderer draws both.
|
|
8
|
+
* Nothing here imports that package.
|
|
9
|
+
*/
|
|
10
|
+
import { parse } from '@babel/parser';
|
|
11
|
+
import { parseTemplate, } from '@fluixi/template-parser';
|
|
12
|
+
import { isIntrinsicName } from './intrinsics.js';
|
|
13
|
+
import { resolveReactiveBindings } from './reactive-bindings.js';
|
|
14
|
+
import { CORE_CONTROL_FLOW, DOM_CONTROL_FLOW, ROUTER_COMPONENTS } from '../resolve/builtins.js';
|
|
15
|
+
/** What each runtime primitive produces. */
|
|
16
|
+
const PRIMITIVE_KIND = {
|
|
17
|
+
signal: 'state',
|
|
18
|
+
createSignal: 'state',
|
|
19
|
+
memo: 'memo',
|
|
20
|
+
createMemo: 'memo',
|
|
21
|
+
effect: 'effect',
|
|
22
|
+
createEffect: 'effect',
|
|
23
|
+
store: 'store',
|
|
24
|
+
createStore: 'store',
|
|
25
|
+
resource: 'resource',
|
|
26
|
+
createResource: 'resource',
|
|
27
|
+
};
|
|
28
|
+
/** A kind that reads something, and so can have dependencies. */
|
|
29
|
+
const READS = new Set(['memo', 'effect', 'resource']);
|
|
30
|
+
/**
|
|
31
|
+
* `<Show when={ready()}>` is a memo over `ready` at runtime, so it is a node here too.
|
|
32
|
+
* Nothing declares these names locally, so without the set every branch and list is invisible.
|
|
33
|
+
* Taken from the resolver rather than copied, so the two cannot drift.
|
|
34
|
+
*/
|
|
35
|
+
const DIRECTIVES = new Set([
|
|
36
|
+
...DOM_CONTROL_FLOW,
|
|
37
|
+
...CORE_CONTROL_FLOW,
|
|
38
|
+
// The router is supplied without an import too. Without these, an app whose root renders
|
|
39
|
+
// `<Router>` has a root component connected to nothing at all.
|
|
40
|
+
...ROUTER_COMPONENTS,
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* The one prop each tracks. `fallback` and `children` are content: counting them would make a
|
|
44
|
+
* branch depend on everything inside it. A name absent here reads nothing of its own.
|
|
45
|
+
*/
|
|
46
|
+
const TRACKED_PROP = {
|
|
47
|
+
Router: 'routes',
|
|
48
|
+
Link: 'href',
|
|
49
|
+
Redirect: 'to',
|
|
50
|
+
Show: 'when',
|
|
51
|
+
Match: 'when',
|
|
52
|
+
For: 'each',
|
|
53
|
+
Index: 'each',
|
|
54
|
+
Dynamic: 'component',
|
|
55
|
+
Portal: 'mount',
|
|
56
|
+
Await: 'value',
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* A framework tag is never one of the author's components.
|
|
60
|
+
*
|
|
61
|
+
* The two groups come from the resolver, so a name added there is drawn as framework here
|
|
62
|
+
* without this list having to be remembered.
|
|
63
|
+
*/
|
|
64
|
+
const ROUTER = new Set(ROUTER_COMPONENTS);
|
|
65
|
+
const directiveKind = (tag) => (ROUTER.has(tag) ? 'router' : 'control');
|
|
66
|
+
/**
|
|
67
|
+
* An http method name is a route handler, not a component.
|
|
68
|
+
*
|
|
69
|
+
* They are capitalised, which is the only thing telling a component from a helper, so an api
|
|
70
|
+
* file arrived in the graph as two components called GET and POST.
|
|
71
|
+
*/
|
|
72
|
+
const HTTP_METHODS = new Set([
|
|
73
|
+
'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS',
|
|
74
|
+
]);
|
|
75
|
+
/** Windows separators and a leading `./`, so a path compares against the configured one. */
|
|
76
|
+
function normalizePath(path) {
|
|
77
|
+
return path.replace(/\\/g, '/').replace(/^\.?\/+/, '');
|
|
78
|
+
}
|
|
79
|
+
function walk(node, visit, stack = []) {
|
|
80
|
+
if (!node || typeof node.type !== 'string')
|
|
81
|
+
return;
|
|
82
|
+
visit(node, stack);
|
|
83
|
+
stack.push(node);
|
|
84
|
+
for (const key of Object.keys(node)) {
|
|
85
|
+
if (key === 'loc' || key === 'parent')
|
|
86
|
+
continue;
|
|
87
|
+
const value = node[key];
|
|
88
|
+
if (Array.isArray(value))
|
|
89
|
+
for (const child of value)
|
|
90
|
+
walk(child, visit, stack);
|
|
91
|
+
else if (value && typeof value.type === 'string')
|
|
92
|
+
walk(value, visit, stack);
|
|
93
|
+
}
|
|
94
|
+
stack.pop();
|
|
95
|
+
}
|
|
96
|
+
/** `const count = signal(0)` → count; `const [count] = createSignal(0)` → count. */
|
|
97
|
+
function declaredName(stack) {
|
|
98
|
+
const declarator = [...stack].reverse().find((n) => n.type === 'VariableDeclarator');
|
|
99
|
+
const id = declarator?.id;
|
|
100
|
+
if (id?.type === 'Identifier')
|
|
101
|
+
return id.name;
|
|
102
|
+
if (id?.type === 'ArrayPattern') {
|
|
103
|
+
const first = id.elements?.[0];
|
|
104
|
+
if (first?.type === 'Identifier')
|
|
105
|
+
return first.name;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
/** A capitalised function is what tells a component from a helper. */
|
|
110
|
+
function componentName(node) {
|
|
111
|
+
if (node.type === 'FunctionDeclaration') {
|
|
112
|
+
const name = node.id?.name;
|
|
113
|
+
return name && name[0] === name[0]?.toUpperCase() ? name : undefined;
|
|
114
|
+
}
|
|
115
|
+
if (node.type === 'VariableDeclarator') {
|
|
116
|
+
const id = node.id;
|
|
117
|
+
const init = node.init;
|
|
118
|
+
if (id?.type !== 'Identifier' || !init)
|
|
119
|
+
return undefined;
|
|
120
|
+
if (init.type !== 'ArrowFunctionExpression' && init.type !== 'FunctionExpression')
|
|
121
|
+
return undefined;
|
|
122
|
+
const name = id.name;
|
|
123
|
+
return name[0] === name[0]?.toUpperCase() ? name : undefined;
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
/** Every identifier a subtree mentions, which is as close to "reads" as reading gets. */
|
|
128
|
+
function mentions(node) {
|
|
129
|
+
const names = new Set();
|
|
130
|
+
walk(node, (n, stack) => {
|
|
131
|
+
if (n.type !== 'Identifier')
|
|
132
|
+
return;
|
|
133
|
+
const parent = stack[stack.length - 1];
|
|
134
|
+
// `props.count` names props, not count; `{ count: 1 }` names neither.
|
|
135
|
+
if (parent?.type === 'MemberExpression' && parent.property === n && !parent.computed)
|
|
136
|
+
return;
|
|
137
|
+
if (parent?.type === 'ObjectProperty' && parent.key === n && !parent.computed)
|
|
138
|
+
return;
|
|
139
|
+
names.add(n.name);
|
|
140
|
+
});
|
|
141
|
+
return names;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A template literal as the parser wants it: static runs and hole slots, in order.
|
|
145
|
+
*
|
|
146
|
+
* The same shape the Babel front-end builds, so what the analyzer sees inside `` html`…` ``
|
|
147
|
+
* is what the compiler lowers.
|
|
148
|
+
*/
|
|
149
|
+
function templatePieces(quasi) {
|
|
150
|
+
const pieces = [];
|
|
151
|
+
const quasis = (quasi.quasis ?? []);
|
|
152
|
+
const expressions = (quasi.expressions ?? []);
|
|
153
|
+
quasis.forEach((q, i) => {
|
|
154
|
+
const value = q.value;
|
|
155
|
+
pieces.push({ kind: 'static', text: value?.cooked ?? value?.raw ?? '', start: q.start ?? 0 });
|
|
156
|
+
if (i < expressions.length) {
|
|
157
|
+
const e = expressions[i];
|
|
158
|
+
pieces.push({ kind: 'hole', index: i, start: e.start ?? 0, end: e.end ?? 0 });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return pieces;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Offset → line and column.
|
|
165
|
+
*
|
|
166
|
+
* The template parser reports spans as offsets, and they are offsets into the file: the
|
|
167
|
+
* pieces it is given carry the positions Babel recorded. So a tag written inside a template
|
|
168
|
+
* can be placed as exactly as one written as JSX.
|
|
169
|
+
*/
|
|
170
|
+
function positionsIn(code) {
|
|
171
|
+
const starts = [0];
|
|
172
|
+
for (let i = 0; i < code.length; i++)
|
|
173
|
+
if (code.charCodeAt(i) === 10)
|
|
174
|
+
starts.push(i + 1);
|
|
175
|
+
return (offset) => {
|
|
176
|
+
let low = 0;
|
|
177
|
+
let high = starts.length - 1;
|
|
178
|
+
while (low < high) {
|
|
179
|
+
const mid = (low + high + 1) >> 1;
|
|
180
|
+
if (starts[mid] <= offset)
|
|
181
|
+
low = mid;
|
|
182
|
+
else
|
|
183
|
+
high = mid - 1;
|
|
184
|
+
}
|
|
185
|
+
return { line: low + 1, column: offset - starts[low] };
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** `span.box` — a template element as written, static attributes only, like describeJsx. */
|
|
189
|
+
function describeTemplateElement(el) {
|
|
190
|
+
let id = '';
|
|
191
|
+
const classes = [];
|
|
192
|
+
for (const attr of el.attributes) {
|
|
193
|
+
if (attr.kind !== 'Attribute' || attr.value?.kind !== 'static')
|
|
194
|
+
continue;
|
|
195
|
+
if (attr.name === 'id')
|
|
196
|
+
id = `#${attr.value.value}`;
|
|
197
|
+
else if (attr.name === 'class') {
|
|
198
|
+
classes.push(...String(attr.value.value).split(/\s+/).filter(Boolean).slice(0, 2));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return `${el.tag}${id}${classes.map((c) => `.${c}`).join('')}`;
|
|
202
|
+
}
|
|
203
|
+
/** Which holes an attribute value reads — one for `=${x}`, several for `"a-${x}-${y}"`. */
|
|
204
|
+
function valueHoles(value) {
|
|
205
|
+
if (!value)
|
|
206
|
+
return [];
|
|
207
|
+
if (value.kind === 'hole')
|
|
208
|
+
return [value.hole];
|
|
209
|
+
if (value.kind === 'mixed')
|
|
210
|
+
return value.parts.flatMap((p) => ('hole' in p ? [p.hole] : []));
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* `span.num`, `div#root` — the element as written.
|
|
215
|
+
*
|
|
216
|
+
* Static attributes only: a class computed at runtime is not something the source can name,
|
|
217
|
+
* and guessing would put a lie on the card.
|
|
218
|
+
*/
|
|
219
|
+
function describeJsx(opening) {
|
|
220
|
+
const tag = opening.name?.name ?? 'element';
|
|
221
|
+
let id = '';
|
|
222
|
+
const classes = [];
|
|
223
|
+
for (const attr of opening.attributes ?? []) {
|
|
224
|
+
if (attr.type !== 'JSXAttribute')
|
|
225
|
+
continue;
|
|
226
|
+
const name = attr.name?.name;
|
|
227
|
+
const value = attr.value;
|
|
228
|
+
if (value?.type !== 'StringLiteral')
|
|
229
|
+
continue;
|
|
230
|
+
if (name === 'id')
|
|
231
|
+
id = `#${value.value}`;
|
|
232
|
+
else if (name === 'class' || name === 'className') {
|
|
233
|
+
classes.push(...String(value.value).split(/\s+/).filter(Boolean).slice(0, 2));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return `${tag}${id}${classes.map((c) => `.${c}`).join('')}`;
|
|
237
|
+
}
|
|
238
|
+
function trackedExpression(opening, prop) {
|
|
239
|
+
for (const attr of opening.attributes ?? []) {
|
|
240
|
+
if (attr.type !== 'JSXAttribute' || attr.name?.name !== prop)
|
|
241
|
+
continue;
|
|
242
|
+
const value = attr.value;
|
|
243
|
+
return value?.type === 'JSXExpressionContainer' ? value.expression : undefined;
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Files are analysed independently, then joined: an imported name resolves to the node the
|
|
249
|
+
* exporting file declared under it, which connects a store to the memo reading it elsewhere.
|
|
250
|
+
*/
|
|
251
|
+
export function analyzeReactiveGraph(files, options = {}) {
|
|
252
|
+
const routesDir = (options.routesDir ?? 'src/routes').replace(/^\.?\/+|\/+$/g, '');
|
|
253
|
+
const apiDir = (options.apiDir ?? 'src/api').replace(/^\.?\/+|\/+$/g, '');
|
|
254
|
+
/** Ids declared in a file-route, to be given an owner once the whole project is read. */
|
|
255
|
+
const fileRoutes = [];
|
|
256
|
+
const pending = [];
|
|
257
|
+
/** file → exported name → id, for resolving an import. */
|
|
258
|
+
const exported = new Map();
|
|
259
|
+
/** file → local name → id, for resolving a reference in the same file. */
|
|
260
|
+
const locals = new Map();
|
|
261
|
+
/** file → local name → the file it was imported from. */
|
|
262
|
+
const imports = new Map();
|
|
263
|
+
/** Component tags, resolved after every file: a tag may name a component read later. */
|
|
264
|
+
const usages = [];
|
|
265
|
+
const routeRefs = [];
|
|
266
|
+
/** Holes, resolved to the nodes they name once every file has been read. */
|
|
267
|
+
const holes = [];
|
|
268
|
+
/** The elements each component writes, by component id. */
|
|
269
|
+
const domFor = new Map();
|
|
270
|
+
/** Tags standing in a dom tree whose component is declared in some other file. */
|
|
271
|
+
const unresolved = [];
|
|
272
|
+
/**
|
|
273
|
+
* What each component's body names. Kept apart from `reads`, which becomes dependency
|
|
274
|
+
* edges — a component mentioning a route table does not depend on it reactively.
|
|
275
|
+
*/
|
|
276
|
+
const componentMentions = new Map();
|
|
277
|
+
let seq = 0;
|
|
278
|
+
for (const file of files) {
|
|
279
|
+
if (!/\.(tsx?|jsx?)$/.test(file.path))
|
|
280
|
+
continue;
|
|
281
|
+
let program;
|
|
282
|
+
try {
|
|
283
|
+
program = parse(file.code, {
|
|
284
|
+
sourceType: 'module',
|
|
285
|
+
plugins: ['jsx', 'typescript'],
|
|
286
|
+
errorRecovery: true,
|
|
287
|
+
}).program;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// A file that does not parse is one being typed in.
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const { primitives, shadowed } = resolveReactiveBindings(program);
|
|
294
|
+
const here = new Map();
|
|
295
|
+
const sold = new Map();
|
|
296
|
+
const from = new Map();
|
|
297
|
+
locals.set(file.path, here);
|
|
298
|
+
exported.set(file.path, sold);
|
|
299
|
+
imports.set(file.path, from);
|
|
300
|
+
/** A page the router is handed by the directory rather than by name. */
|
|
301
|
+
const inRoutesDir = normalizePath(file.path).startsWith(`${routesDir}/`);
|
|
302
|
+
/** The component a node sits inside, by the id of its declaration. */
|
|
303
|
+
const components = new Map();
|
|
304
|
+
/** Built once per file, and only for a file that has a template in it. */
|
|
305
|
+
let positionAt = null;
|
|
306
|
+
const positionOf = () => (positionAt ??= positionsIn(file.code));
|
|
307
|
+
// Components first: a primitive needs the id of the body it sits in.
|
|
308
|
+
walk(program, (node, stack) => {
|
|
309
|
+
const name = componentName(node);
|
|
310
|
+
const at = node.loc?.start;
|
|
311
|
+
if (!name || !at)
|
|
312
|
+
return;
|
|
313
|
+
const id = seq++;
|
|
314
|
+
components.set(node, id);
|
|
315
|
+
here.set(name, id);
|
|
316
|
+
componentMentions.set(id, mentions(node));
|
|
317
|
+
if (inRoutesDir)
|
|
318
|
+
fileRoutes.push(id);
|
|
319
|
+
pending.push({
|
|
320
|
+
node: {
|
|
321
|
+
id,
|
|
322
|
+
label: name,
|
|
323
|
+
kind: HTTP_METHODS.has(name) ? 'handler' : 'component',
|
|
324
|
+
deps: [],
|
|
325
|
+
writes: [],
|
|
326
|
+
runs: 0,
|
|
327
|
+
lastPulse: 0,
|
|
328
|
+
source: { file: file.path, line: at.line, column: at.column },
|
|
329
|
+
},
|
|
330
|
+
reads: new Set(),
|
|
331
|
+
file: file.path,
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
walk(program, (node, stack) => {
|
|
335
|
+
if (node.type === 'ImportDeclaration') {
|
|
336
|
+
const source = node.source?.value;
|
|
337
|
+
for (const spec of node.specifiers ?? []) {
|
|
338
|
+
const local = spec.local?.name;
|
|
339
|
+
if (local && source?.startsWith('.'))
|
|
340
|
+
from.set(local, source);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (node.type !== 'CallExpression')
|
|
345
|
+
return;
|
|
346
|
+
const callee = node.callee;
|
|
347
|
+
if (callee?.type !== 'Identifier')
|
|
348
|
+
return;
|
|
349
|
+
const name = callee.name;
|
|
350
|
+
// A local function called `effect` is not one of ours.
|
|
351
|
+
const intrinsic = name.startsWith('$') && isIntrinsicName(name) && !shadowed.has(name);
|
|
352
|
+
if (!intrinsic && !primitives.has(name))
|
|
353
|
+
return;
|
|
354
|
+
const kind = PRIMITIVE_KIND[intrinsic ? name.slice(1) : name];
|
|
355
|
+
const at = node.loc?.start;
|
|
356
|
+
if (!kind || !at)
|
|
357
|
+
return;
|
|
358
|
+
const id = seq++;
|
|
359
|
+
const label = declaredName(stack) ?? `${kind}${id}`;
|
|
360
|
+
here.set(label, id);
|
|
361
|
+
// The nearest enclosing component owns it, the way the runtime's does.
|
|
362
|
+
let owner;
|
|
363
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
364
|
+
const found = components.get(stack[i]);
|
|
365
|
+
if (found !== undefined) {
|
|
366
|
+
owner = found;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
pending.push({
|
|
371
|
+
node: {
|
|
372
|
+
id,
|
|
373
|
+
label,
|
|
374
|
+
kind,
|
|
375
|
+
deps: [],
|
|
376
|
+
writes: [],
|
|
377
|
+
runs: 0,
|
|
378
|
+
lastPulse: 0,
|
|
379
|
+
source: { file: file.path, line: at.line, column: at.column },
|
|
380
|
+
...(owner !== undefined ? { createdIn: owner } : {}),
|
|
381
|
+
},
|
|
382
|
+
// Only what reads can read: a signal's initial value is not a dependency.
|
|
383
|
+
reads: READS.has(kind) ? mentions(node.arguments?.[0]) : new Set(),
|
|
384
|
+
file: file.path,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
// A component used inside another belongs to it, as the running graph shows it.
|
|
388
|
+
walk(program, (node, stack) => {
|
|
389
|
+
if (node.type !== 'JSXOpeningElement')
|
|
390
|
+
return;
|
|
391
|
+
const tag = node.name?.name;
|
|
392
|
+
if (!tag || tag[0] !== tag[0]?.toUpperCase())
|
|
393
|
+
return;
|
|
394
|
+
// Nearest enclosing body, so a <Match> sits under its <Switch>.
|
|
395
|
+
let owner;
|
|
396
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
397
|
+
const found = components.get(stack[i]);
|
|
398
|
+
if (found !== undefined) {
|
|
399
|
+
owner = found;
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// A locally declared or relatively imported name is that component, not control flow.
|
|
404
|
+
const own = here.has(tag) || from.has(tag);
|
|
405
|
+
if (!own && DIRECTIVES.has(tag)) {
|
|
406
|
+
const at = node.loc?.start;
|
|
407
|
+
if (!at)
|
|
408
|
+
return;
|
|
409
|
+
const id = seq++;
|
|
410
|
+
const prop = TRACKED_PROP[tag];
|
|
411
|
+
const tracked = prop ? trackedExpression(node, prop) : undefined;
|
|
412
|
+
// Nested directives look this up by the element that contains this tag.
|
|
413
|
+
const element = stack[stack.length - 1];
|
|
414
|
+
if (element?.type === 'JSXElement')
|
|
415
|
+
components.set(element, id);
|
|
416
|
+
pending.push({
|
|
417
|
+
node: {
|
|
418
|
+
id,
|
|
419
|
+
label: tag,
|
|
420
|
+
kind: directiveKind(tag),
|
|
421
|
+
deps: [],
|
|
422
|
+
writes: [],
|
|
423
|
+
runs: 0,
|
|
424
|
+
lastPulse: 0,
|
|
425
|
+
source: { file: file.path, line: at.line, column: at.column },
|
|
426
|
+
...(owner !== undefined ? { createdIn: owner } : {}),
|
|
427
|
+
},
|
|
428
|
+
reads: tracked ? mentions(tracked) : new Set(),
|
|
429
|
+
file: file.path,
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (owner !== undefined)
|
|
434
|
+
usages.push({ tag, file: file.path, owner });
|
|
435
|
+
});
|
|
436
|
+
// Where each value reaches the document. The live graph gets this from the dom layer as
|
|
437
|
+
// it wires the binding; the source has it written down.
|
|
438
|
+
walk(program, (node, stack) => {
|
|
439
|
+
if (node.type !== 'JSXExpressionContainer')
|
|
440
|
+
return;
|
|
441
|
+
const expr = node.expression;
|
|
442
|
+
if (!expr || expr.type === 'JSXEmptyExpression')
|
|
443
|
+
return;
|
|
444
|
+
const at = expr.loc?.start;
|
|
445
|
+
if (!at)
|
|
446
|
+
return;
|
|
447
|
+
const parent = stack[stack.length - 1];
|
|
448
|
+
const attribute = parent?.type === 'JSXAttribute' ? parent.name?.name : undefined;
|
|
449
|
+
// The tag this sits in: its own for an attribute, the enclosing one for a hole.
|
|
450
|
+
const holder = [...stack]
|
|
451
|
+
.reverse()
|
|
452
|
+
.find((n) => n.type === 'JSXElement' || n.type === 'JSXOpeningElement');
|
|
453
|
+
const opening = holder?.type === 'JSXOpeningElement' ? holder : holder?.openingElement;
|
|
454
|
+
if (!opening)
|
|
455
|
+
return;
|
|
456
|
+
const target = {
|
|
457
|
+
kind: attribute ? 'attribute' : 'content',
|
|
458
|
+
...(attribute ? { name: attribute } : {}),
|
|
459
|
+
element: describeJsx(opening),
|
|
460
|
+
source: { file: file.path, line: at.line, column: at.column },
|
|
461
|
+
};
|
|
462
|
+
for (const name of mentions(expr))
|
|
463
|
+
holes.push({ file: file.path, name, target });
|
|
464
|
+
});
|
|
465
|
+
/**
|
|
466
|
+
* The elements each component writes, as a tree.
|
|
467
|
+
*
|
|
468
|
+
* Nothing has run, so this is the structure the source states outright. A hole —
|
|
469
|
+
* `{items().map(...)}`, a `<Show>` body — is decided at runtime and has no elements
|
|
470
|
+
* here; the tag stands in its place and fills in once the application is up.
|
|
471
|
+
*/
|
|
472
|
+
const describeTree = (el) => {
|
|
473
|
+
const opening = el.openingElement ?? undefined;
|
|
474
|
+
const tag = opening?.name?.name;
|
|
475
|
+
if (!opening || !tag)
|
|
476
|
+
return undefined;
|
|
477
|
+
const at = el.loc?.start;
|
|
478
|
+
const where = at ? { file: file.path, line: at.line, column: at.column } : undefined;
|
|
479
|
+
const children = [];
|
|
480
|
+
for (const child of (el.children ?? [])) {
|
|
481
|
+
if (child?.type !== 'JSXElement')
|
|
482
|
+
continue;
|
|
483
|
+
const sub = describeTree(child);
|
|
484
|
+
if (sub)
|
|
485
|
+
children.push(sub);
|
|
486
|
+
}
|
|
487
|
+
// A capitalised tag renders its own dom, so it stands here as itself and the panel
|
|
488
|
+
// pulls that component's row in. `components` has the directives, `here` the names
|
|
489
|
+
// this file declares; anything else is a name another file exports, which is only
|
|
490
|
+
// resolvable once they have all been read. The children stay either way — they are
|
|
491
|
+
// written here, and a row that resolves replaces them with the component's own.
|
|
492
|
+
if (tag[0] === tag[0]?.toUpperCase()) {
|
|
493
|
+
const node = { label: tag, children, ...(where ? { at: where } : {}) };
|
|
494
|
+
const id = components.get(el) ?? here.get(tag);
|
|
495
|
+
if (id !== undefined)
|
|
496
|
+
node.owner = id;
|
|
497
|
+
else
|
|
498
|
+
unresolved.push({ node, tag, file: file.path });
|
|
499
|
+
return node;
|
|
500
|
+
}
|
|
501
|
+
return { label: describeJsx(opening), children, ...(where ? { at: where } : {}) };
|
|
502
|
+
};
|
|
503
|
+
walk(program, (node, stack) => {
|
|
504
|
+
if (node.type !== 'JSXElement')
|
|
505
|
+
return;
|
|
506
|
+
// Only the outermost element of a body: the rest are reached by recursing into it.
|
|
507
|
+
let owner;
|
|
508
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
509
|
+
const found = stack[i];
|
|
510
|
+
if (found.type === 'JSXElement')
|
|
511
|
+
return;
|
|
512
|
+
const id = components.get(found);
|
|
513
|
+
if (id !== undefined) {
|
|
514
|
+
owner = id;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (owner === undefined)
|
|
519
|
+
return;
|
|
520
|
+
const tree = describeTree(node);
|
|
521
|
+
if (!tree)
|
|
522
|
+
return;
|
|
523
|
+
const held = domFor.get(owner);
|
|
524
|
+
if (held)
|
|
525
|
+
held.push(tree);
|
|
526
|
+
else
|
|
527
|
+
domFor.set(owner, [tree]);
|
|
528
|
+
});
|
|
529
|
+
/**
|
|
530
|
+
* The same again, for `` html`…` ``.
|
|
531
|
+
*
|
|
532
|
+
* The tags and holes are inside a string, so none of the walks above can see them: a
|
|
533
|
+
* `<Show>` in a template is not a JSXElement and a `${count()}` is not an expression
|
|
534
|
+
* container. The template parser reads the string into the same shape the compiler
|
|
535
|
+
* lowers, and each hole maps back by index to the expression Babel already parsed —
|
|
536
|
+
* which is where the position and the names read come from.
|
|
537
|
+
*/
|
|
538
|
+
walk(program, (node, stack) => {
|
|
539
|
+
if (node.type !== 'TaggedTemplateExpression')
|
|
540
|
+
return;
|
|
541
|
+
const tagName = node.tag?.name;
|
|
542
|
+
if (tagName !== 'html' && tagName !== 'svg')
|
|
543
|
+
return;
|
|
544
|
+
const quasi = node.quasi;
|
|
545
|
+
const expressions = (quasi?.expressions ?? []);
|
|
546
|
+
const positionAt = positionOf();
|
|
547
|
+
let children;
|
|
548
|
+
try {
|
|
549
|
+
children = parseTemplate(templatePieces(quasi), { svg: tagName === 'svg' }).root.children;
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// A template being typed in is not a template that means nothing.
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
/** The expression a hole index stands for, as Babel parsed it. */
|
|
556
|
+
const holeExpr = (index) => index == null ? undefined : expressions[index];
|
|
557
|
+
const readsOf = (index) => {
|
|
558
|
+
const expr = holeExpr(index);
|
|
559
|
+
return expr ? mentions(expr) : new Set();
|
|
560
|
+
};
|
|
561
|
+
/** A directive or tag becomes a node of its own, owned by whatever encloses it. */
|
|
562
|
+
const addControl = (tag, offset, hole, owner) => {
|
|
563
|
+
// Where the tag itself is written, which is what the JSX walk records too. The hole
|
|
564
|
+
// stands in for a directive that has no tag of its own.
|
|
565
|
+
const at = offset !== undefined ? positionAt(offset) : (holeExpr(hole)?.loc?.start ?? node.loc?.start);
|
|
566
|
+
if (!at)
|
|
567
|
+
return undefined;
|
|
568
|
+
const id = seq++;
|
|
569
|
+
pending.push({
|
|
570
|
+
node: {
|
|
571
|
+
id,
|
|
572
|
+
label: tag,
|
|
573
|
+
kind: directiveKind(tag),
|
|
574
|
+
deps: [],
|
|
575
|
+
writes: [],
|
|
576
|
+
runs: 0,
|
|
577
|
+
lastPulse: 0,
|
|
578
|
+
source: { file: file.path, line: at.line, column: at.column },
|
|
579
|
+
...(owner !== undefined ? { createdIn: owner } : {}),
|
|
580
|
+
},
|
|
581
|
+
reads: readsOf(hole),
|
|
582
|
+
file: file.path,
|
|
583
|
+
});
|
|
584
|
+
return id;
|
|
585
|
+
};
|
|
586
|
+
const addTarget = (index, element, attribute) => {
|
|
587
|
+
const expr = holeExpr(index);
|
|
588
|
+
const at = expr?.loc?.start;
|
|
589
|
+
if (!expr || !at)
|
|
590
|
+
return;
|
|
591
|
+
const target = {
|
|
592
|
+
kind: attribute ? 'attribute' : 'content',
|
|
593
|
+
...(attribute ? { name: attribute } : {}),
|
|
594
|
+
element,
|
|
595
|
+
source: { file: file.path, line: at.line, column: at.column },
|
|
596
|
+
};
|
|
597
|
+
for (const name of mentions(expr))
|
|
598
|
+
holes.push({ file: file.path, name, target });
|
|
599
|
+
};
|
|
600
|
+
/** Every hole an element's attributes read, and the name each reaches the dom under. */
|
|
601
|
+
const elementTargets = (el) => {
|
|
602
|
+
const element = describeTemplateElement(el);
|
|
603
|
+
for (const attr of el.attributes) {
|
|
604
|
+
switch (attr.kind) {
|
|
605
|
+
case 'Attribute':
|
|
606
|
+
for (const hole of valueHoles(attr.value))
|
|
607
|
+
addTarget(hole, element, attr.name);
|
|
608
|
+
break;
|
|
609
|
+
case 'PropertyBinding':
|
|
610
|
+
case 'BindDirective':
|
|
611
|
+
addTarget(attr.hole, element, attr.name);
|
|
612
|
+
break;
|
|
613
|
+
case 'ClassDirective':
|
|
614
|
+
addTarget(attr.hole, element, 'class');
|
|
615
|
+
break;
|
|
616
|
+
case 'StyleDirective':
|
|
617
|
+
addTarget(attr.hole, element, 'style');
|
|
618
|
+
break;
|
|
619
|
+
// A handler is not a read, and neither is a ref, a spread or a use.
|
|
620
|
+
default:
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
const visit = (nodes, owner, holder) => {
|
|
626
|
+
for (const child of nodes) {
|
|
627
|
+
switch (child.kind) {
|
|
628
|
+
case 'Element': {
|
|
629
|
+
// `if=`/`each=` are `<Show>`/`<For>` once lowered, so they are nodes here too.
|
|
630
|
+
const ifDir = child.attributes.find((a) => a.kind === 'IfDirective');
|
|
631
|
+
const eachDir = child.attributes.find((a) => a.kind === 'EachDirective');
|
|
632
|
+
let inner = owner;
|
|
633
|
+
if (ifDir?.kind === 'IfDirective')
|
|
634
|
+
inner = addControl('Show', child.loc?.start, ifDir.hole, owner) ?? owner;
|
|
635
|
+
else if (eachDir?.kind === 'EachDirective')
|
|
636
|
+
inner = addControl('For', child.loc?.start, eachDir.hole, owner) ?? owner;
|
|
637
|
+
elementTargets(child);
|
|
638
|
+
visit(child.children, inner, describeTemplateElement(child));
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case 'Component': {
|
|
642
|
+
const tag = child.tag;
|
|
643
|
+
// A name the file declares or imports is that component, not control flow.
|
|
644
|
+
const own = here.has(tag) || from.has(tag);
|
|
645
|
+
if (tag && !own && DIRECTIVES.has(tag)) {
|
|
646
|
+
const prop = TRACKED_PROP[tag];
|
|
647
|
+
const tracked = prop
|
|
648
|
+
? child.attributes.find((a) => 'name' in a && a.name === prop)
|
|
649
|
+
: undefined;
|
|
650
|
+
const hole = tracked && 'hole' in tracked
|
|
651
|
+
? tracked.hole
|
|
652
|
+
: tracked && 'value' in tracked
|
|
653
|
+
? valueHoles(tracked.value)[0]
|
|
654
|
+
: undefined;
|
|
655
|
+
const id = addControl(tag, child.loc?.start, hole, owner);
|
|
656
|
+
visit(child.children, id ?? owner, holder);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
if (tag && owner !== undefined)
|
|
660
|
+
usages.push({ tag, file: file.path, owner });
|
|
661
|
+
visit(child.children, owner, holder);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
case 'Fragment':
|
|
665
|
+
visit(child.children, owner, holder);
|
|
666
|
+
break;
|
|
667
|
+
case 'Expression':
|
|
668
|
+
addTarget(child.hole, holder, undefined);
|
|
669
|
+
break;
|
|
670
|
+
default:
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
// Whatever body the template is written in — the same lookup the tag walk uses.
|
|
676
|
+
let owner;
|
|
677
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
678
|
+
const found = components.get(stack[i]);
|
|
679
|
+
if (found !== undefined) {
|
|
680
|
+
owner = found;
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
visit(children, owner, 'template');
|
|
685
|
+
});
|
|
686
|
+
// A route table names its components rather than writing them as tags.
|
|
687
|
+
walk(program, (node, stack) => {
|
|
688
|
+
if (node.type !== 'ObjectProperty' || node.computed)
|
|
689
|
+
return;
|
|
690
|
+
const key = node.key?.name ?? node.key?.value;
|
|
691
|
+
if (key !== 'component' && key !== 'Component')
|
|
692
|
+
return;
|
|
693
|
+
const value = node.value;
|
|
694
|
+
if (value?.type !== 'Identifier')
|
|
695
|
+
return;
|
|
696
|
+
const holder = [...stack].reverse().find((n) => n.type === 'VariableDeclarator');
|
|
697
|
+
const via = holder?.id?.name;
|
|
698
|
+
if (via)
|
|
699
|
+
routeRefs.push({ via, tag: value.name, file: file.path });
|
|
700
|
+
});
|
|
701
|
+
// What a module exports under a name is what another module gets by importing it.
|
|
702
|
+
walk(program, (node) => {
|
|
703
|
+
if (node.type !== 'ExportNamedDeclaration' && node.type !== 'ExportDefaultDeclaration')
|
|
704
|
+
return;
|
|
705
|
+
for (const name of here.keys())
|
|
706
|
+
sold.set(name, here.get(name));
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
// A name into an id, in the file that used it and then in the file it imported it from.
|
|
710
|
+
const resolve = (file, name) => {
|
|
711
|
+
const local = locals.get(file)?.get(name);
|
|
712
|
+
if (local !== undefined)
|
|
713
|
+
return local;
|
|
714
|
+
const source = imports.get(file)?.get(name);
|
|
715
|
+
if (!source)
|
|
716
|
+
return undefined;
|
|
717
|
+
for (const [path, names] of exported) {
|
|
718
|
+
if (!sameModule(file, source, path))
|
|
719
|
+
continue;
|
|
720
|
+
const id = names.get(name);
|
|
721
|
+
if (id !== undefined)
|
|
722
|
+
return id;
|
|
723
|
+
}
|
|
724
|
+
return undefined;
|
|
725
|
+
};
|
|
726
|
+
for (const entry of pending) {
|
|
727
|
+
const deps = new Set();
|
|
728
|
+
for (const name of entry.reads) {
|
|
729
|
+
const id = resolve(entry.file, name);
|
|
730
|
+
if (id !== undefined && id !== entry.node.id)
|
|
731
|
+
deps.add(id);
|
|
732
|
+
}
|
|
733
|
+
entry.node.deps = [...deps];
|
|
734
|
+
}
|
|
735
|
+
// Last, so a component used before its own file was read still finds it.
|
|
736
|
+
// `resolve` config and auto-import mean a tag can have no import to follow. One declaration
|
|
737
|
+
// of the name is unambiguous; two is a guess, and a wrong owner is worse than none.
|
|
738
|
+
const unique = new Map();
|
|
739
|
+
for (const names of exported.values()) {
|
|
740
|
+
for (const [name, id] of names)
|
|
741
|
+
unique.set(name, unique.has(name) ? null : id);
|
|
742
|
+
}
|
|
743
|
+
const byId = new Map(pending.map((p) => [p.node.id, p.node]));
|
|
744
|
+
// A tag naming a component another file declares, now that they have all been read.
|
|
745
|
+
for (const entry of unresolved) {
|
|
746
|
+
const id = resolve(entry.file, entry.tag) ?? unique.get(entry.tag) ?? undefined;
|
|
747
|
+
if (id !== undefined && byId.get(id)?.kind === 'component')
|
|
748
|
+
entry.node.owner = id;
|
|
749
|
+
}
|
|
750
|
+
// What each component writes. Attached here because the walks that read it run per file,
|
|
751
|
+
// while a node is only final once every file has been read.
|
|
752
|
+
for (const [id, dom] of domFor) {
|
|
753
|
+
const node = byId.get(id);
|
|
754
|
+
if (node && dom.length)
|
|
755
|
+
node.dom = dom;
|
|
756
|
+
}
|
|
757
|
+
for (const hole of holes) {
|
|
758
|
+
const id = resolve(hole.file, hole.name);
|
|
759
|
+
const node = id !== undefined ? byId.get(id) : undefined;
|
|
760
|
+
// Only what the graph draws a value for: a tag is not a binding.
|
|
761
|
+
if (!node || node.kind === 'component')
|
|
762
|
+
continue;
|
|
763
|
+
(node.targets ??= []).push(hole.target);
|
|
764
|
+
}
|
|
765
|
+
// A component that mentions the table is the one that renders what the table names.
|
|
766
|
+
for (const ref of routeRefs) {
|
|
767
|
+
const named = resolve(ref.file, ref.tag) ?? unique.get(ref.tag) ?? undefined;
|
|
768
|
+
if (named === undefined)
|
|
769
|
+
continue;
|
|
770
|
+
// The router the table was handed to, which is what renders the components it names.
|
|
771
|
+
// Without this they belong to the component that merely mentions the table, and a
|
|
772
|
+
// `<Router routes={appRoutes}>` ends up beside the layout it renders rather than above.
|
|
773
|
+
const router = pending.find((p) => p.file === ref.file &&
|
|
774
|
+
p.node.kind === 'router' &&
|
|
775
|
+
p.node.id !== named &&
|
|
776
|
+
p.reads.has(ref.via))?.node.id;
|
|
777
|
+
const mentions = pending.find((p) => p.file === ref.file &&
|
|
778
|
+
(p.node.kind === 'component' || p.node.kind === 'handler') &&
|
|
779
|
+
p.node.id !== named &&
|
|
780
|
+
componentMentions.get(p.node.id)?.has(ref.via))?.node.id;
|
|
781
|
+
const holder = router ?? mentions;
|
|
782
|
+
const node = byId.get(named);
|
|
783
|
+
if (node && node.createdIn === undefined && holder !== undefined)
|
|
784
|
+
node.createdIn = holder;
|
|
785
|
+
}
|
|
786
|
+
for (const use of usages) {
|
|
787
|
+
const id = resolve(use.file, use.tag) ?? unique.get(use.tag) ?? undefined;
|
|
788
|
+
if (id === undefined || id === use.owner)
|
|
789
|
+
continue;
|
|
790
|
+
const node = byId.get(id);
|
|
791
|
+
// First use wins: a component used twice is one declaration either way.
|
|
792
|
+
if (node && node.kind === 'component' && node.createdIn === undefined)
|
|
793
|
+
node.createdIn = use.owner;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* A file-route belongs to the outlet it renders into.
|
|
797
|
+
*
|
|
798
|
+
* Nothing names these components — the table comes from the directory — so a page that
|
|
799
|
+
* declares no signals is connected to nothing at all and sits on its own. `<Outlet />` is
|
|
800
|
+
* the hole a matched route fills, so it is the honest owner; a router with no outlet
|
|
801
|
+
* written out renders into itself. Only one of either is a definite answer: with two
|
|
802
|
+
* outlets, which one a given page lands in is a runtime question.
|
|
803
|
+
*/
|
|
804
|
+
const only = (kind, label) => {
|
|
805
|
+
const found = pending.filter((p) => p.node.kind === kind && p.node.label === label);
|
|
806
|
+
return found.length === 1 ? found[0].node.id : undefined;
|
|
807
|
+
};
|
|
808
|
+
const routeOwner = only('router', 'Outlet') ?? only('router', 'Router');
|
|
809
|
+
if (routeOwner !== undefined) {
|
|
810
|
+
for (const id of fileRoutes) {
|
|
811
|
+
const node = byId.get(id);
|
|
812
|
+
// A helper declared beside the page is already owned by the page that uses it.
|
|
813
|
+
if (node && node.kind === 'component' && node.createdIn === undefined && id !== routeOwner) {
|
|
814
|
+
node.createdIn = routeOwner;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* The api routes hang off one card of their own.
|
|
820
|
+
*
|
|
821
|
+
* A handler is served because of the directory it sits in, so like a page nothing names it
|
|
822
|
+
* — but unlike a page nothing renders it either, and hanging it off the router would say
|
|
823
|
+
* something false. A root of its own is what it is: one card, the handlers under it.
|
|
824
|
+
*/
|
|
825
|
+
const loose = pending.filter((p) => p.node.kind === 'handler' && p.node.createdIn === undefined);
|
|
826
|
+
if (loose.length > 0) {
|
|
827
|
+
const id = seq++;
|
|
828
|
+
pending.push({
|
|
829
|
+
node: {
|
|
830
|
+
id,
|
|
831
|
+
label: 'api',
|
|
832
|
+
kind: 'handler',
|
|
833
|
+
deps: [],
|
|
834
|
+
writes: [],
|
|
835
|
+
runs: 0,
|
|
836
|
+
lastPulse: 0,
|
|
837
|
+
// The directory, since there is no line of anyone's source to point at.
|
|
838
|
+
source: { file: apiDir, line: 1, column: 0 },
|
|
839
|
+
},
|
|
840
|
+
reads: new Set(),
|
|
841
|
+
file: apiDir,
|
|
842
|
+
});
|
|
843
|
+
for (const p of loose)
|
|
844
|
+
p.node.createdIn = id;
|
|
845
|
+
}
|
|
846
|
+
return { t: Date.now(), nodes: pending.map((p) => p.node), static: true };
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* `./cart` from `src/app.tsx` is `src/cart.ts`. Both sides lose their extension: ESM wants
|
|
850
|
+
* `./cart.js` for a file written `cart.ts`, so as written they never match.
|
|
851
|
+
*/
|
|
852
|
+
const withoutExtension = (path) => path.replace(/\.(tsx?|jsx?|mts|cts|mjs|cjs)$/, '');
|
|
853
|
+
function sameModule(importer, specifier, candidate) {
|
|
854
|
+
const base = importer.includes('/') ? importer.slice(0, importer.lastIndexOf('/')) : '';
|
|
855
|
+
const parts = (base ? base.split('/') : []).concat(specifier.split('/'));
|
|
856
|
+
const stack = [];
|
|
857
|
+
for (const part of parts) {
|
|
858
|
+
if (part === '.' || part === '')
|
|
859
|
+
continue;
|
|
860
|
+
if (part === '..')
|
|
861
|
+
stack.pop();
|
|
862
|
+
else
|
|
863
|
+
stack.push(part);
|
|
864
|
+
}
|
|
865
|
+
const resolved = withoutExtension(stack.join('/'));
|
|
866
|
+
const target = withoutExtension(candidate);
|
|
867
|
+
// A directory import: `./cart` is `src/cart/index.ts`.
|
|
868
|
+
return target === resolved || target === `${resolved}/index`;
|
|
869
|
+
}
|