chocola 2.0.0-next.1 → 2.0.0-next.11
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/LICENSE +1 -1
- package/README.md +45 -35
- package/bin/chocola.js +227 -0
- package/compiler/chalk.js +34 -0
- package/compiler/component-processor.js +771 -402
- package/compiler/config.js +43 -66
- package/compiler/dom-processor.js +12 -75
- package/compiler/fs.js +0 -11
- package/compiler/index.js +87 -230
- package/compiler/module-graph.js +257 -0
- package/compiler/pipeline.js +56 -196
- package/compiler/render.js +183 -0
- package/compiler/runtime-generator.js +20 -15
- package/compiler/utils.js +79 -126
- package/dev/index.js +54 -22
- package/package.json +23 -12
- package/parser/component.js +340 -0
- package/parser/context.js +59 -0
- package/parser/css.js +96 -0
- package/parser/index.js +6 -0
- package/parser/script.js +485 -0
- package/parser/script.test.js +103 -0
- package/parser/template.js +120 -0
- package/parser/utils.js +32 -0
- package/runtime/index.js +312 -0
- package/server/index.js +432 -0
- package/utils.js +113 -8
- package/index.js +0 -0
package/parser/script.js
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import * as acorn from "acorn";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract binding names from a pattern node (Identifier, ObjectPattern, ArrayPattern, RestElement, AssignmentPattern)
|
|
5
|
+
* Returns flat list of identifier names.
|
|
6
|
+
*/
|
|
7
|
+
function extractBindingNames(node) {
|
|
8
|
+
const names = [];
|
|
9
|
+
function walk(n) {
|
|
10
|
+
if (!n) return;
|
|
11
|
+
switch (n.type) {
|
|
12
|
+
case "Identifier":
|
|
13
|
+
names.push(n.name);
|
|
14
|
+
break;
|
|
15
|
+
case "ObjectPattern":
|
|
16
|
+
for (const prop of n.properties) {
|
|
17
|
+
if (prop.type === "RestElement") walk(prop.argument);
|
|
18
|
+
else walk(prop.value);
|
|
19
|
+
}
|
|
20
|
+
break;
|
|
21
|
+
case "ArrayPattern":
|
|
22
|
+
for (const elem of n.elements) {
|
|
23
|
+
if (!elem) continue;
|
|
24
|
+
walk(elem);
|
|
25
|
+
}
|
|
26
|
+
break;
|
|
27
|
+
case "RestElement":
|
|
28
|
+
walk(n.argument);
|
|
29
|
+
break;
|
|
30
|
+
case "AssignmentPattern":
|
|
31
|
+
walk(n.left);
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
walk(node);
|
|
38
|
+
return names;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a <script> block into an ESTree AST and classify top-level declarations.
|
|
43
|
+
*
|
|
44
|
+
* Handles:
|
|
45
|
+
* - export let props (including comma declarators)
|
|
46
|
+
* - let/const with destructuring, comma declarators, array rest
|
|
47
|
+
* - import default/named/namespace/side-effect
|
|
48
|
+
* - function declarations (including async) and $runtime extraction
|
|
49
|
+
*
|
|
50
|
+
* @param {string|null} script - raw script innerHTML or null
|
|
51
|
+
* @returns {{
|
|
52
|
+
* ast: import("acorn").Program|null,
|
|
53
|
+
* props: Array<{name:string, defaultValue:string|undefined, raw:string, initNode: any, declaratorNode:any}>,
|
|
54
|
+
* topVars: Array<{keyword:string, name:string, value:string|undefined, raw:string, names:string[], isDestructuring:boolean, start:number, end:number, initNode:any, declaratorNode:any}>,
|
|
55
|
+
* topFuncs: string[],
|
|
56
|
+
* topFuncNodes: import("acorn").FunctionDeclaration[],
|
|
57
|
+
* imports: Array<{source:string, specifiers:Array<{type:string, local:string, imported?:string}>, raw:string, start:number, end:number}>,
|
|
58
|
+
* runtimeNode: import("acorn").FunctionDeclaration|null,
|
|
59
|
+
* runtime: string|null
|
|
60
|
+
* }}
|
|
61
|
+
*/
|
|
62
|
+
export function parseScript(script) {
|
|
63
|
+
if (!script) {
|
|
64
|
+
return {
|
|
65
|
+
ast: null,
|
|
66
|
+
props: [],
|
|
67
|
+
topVars: [],
|
|
68
|
+
topFuncs: [],
|
|
69
|
+
topFuncNodes: [],
|
|
70
|
+
imports: [],
|
|
71
|
+
runtimeNode: null,
|
|
72
|
+
runtime: null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let ast = null;
|
|
77
|
+
try {
|
|
78
|
+
ast = acorn.parse(script, {
|
|
79
|
+
ecmaVersion: 2023,
|
|
80
|
+
sourceType: "module",
|
|
81
|
+
ranges: false,
|
|
82
|
+
});
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return {
|
|
85
|
+
ast: null,
|
|
86
|
+
props: [],
|
|
87
|
+
topVars: [],
|
|
88
|
+
topFuncs: [],
|
|
89
|
+
topFuncNodes: [],
|
|
90
|
+
imports: [],
|
|
91
|
+
runtimeNode: null,
|
|
92
|
+
runtime: null,
|
|
93
|
+
parseError: e.message,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const props = [];
|
|
98
|
+
const topVars = [];
|
|
99
|
+
const topFuncs = [];
|
|
100
|
+
const topFuncNodes = [];
|
|
101
|
+
const imports = [];
|
|
102
|
+
let runtimeNode = null;
|
|
103
|
+
let runtime = null;
|
|
104
|
+
|
|
105
|
+
for (const node of ast.body) {
|
|
106
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
107
|
+
const decl = node.declaration;
|
|
108
|
+
if (decl && decl.type === "VariableDeclaration" && decl.kind === "let") {
|
|
109
|
+
for (const d of decl.declarations) {
|
|
110
|
+
const names = extractBindingNames(d.id);
|
|
111
|
+
const defaultValue = d.init ? script.slice(d.init.start, d.init.end).trim() : undefined;
|
|
112
|
+
const raw = script.slice(d.start, d.end);
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
props.push({ name, defaultValue, raw, initNode: d.init || null, declaratorNode: d });
|
|
115
|
+
}
|
|
116
|
+
if (names.length === 0) {
|
|
117
|
+
const patternRaw = script.slice(d.id.start, d.id.end);
|
|
118
|
+
props.push({ name: patternRaw, defaultValue, raw, initNode: d.init || null, declaratorNode: d });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} else if (!decl) {
|
|
122
|
+
// e.g., export { x } — ignore
|
|
123
|
+
}
|
|
124
|
+
} else if (node.type === "VariableDeclaration") {
|
|
125
|
+
const keyword = node.kind;
|
|
126
|
+
for (const d of node.declarations) {
|
|
127
|
+
const names = extractBindingNames(d.id);
|
|
128
|
+
const value = d.init ? script.slice(d.init.start, d.init.end).trim() : undefined;
|
|
129
|
+
const raw = script.slice(d.start, d.end);
|
|
130
|
+
const idRaw = script.slice(d.id.start, d.id.end);
|
|
131
|
+
const isDestructuring = d.id.type !== "Identifier";
|
|
132
|
+
if (names.length === 0) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (isDestructuring) {
|
|
136
|
+
const filtered = names.filter((n) => n !== "self" && n !== "ctx");
|
|
137
|
+
if (filtered.length === 0) continue;
|
|
138
|
+
topVars.push({
|
|
139
|
+
keyword,
|
|
140
|
+
name: idRaw,
|
|
141
|
+
value,
|
|
142
|
+
raw,
|
|
143
|
+
names: filtered,
|
|
144
|
+
isDestructuring: true,
|
|
145
|
+
start: d.start,
|
|
146
|
+
end: d.end,
|
|
147
|
+
initNode: d.init || null,
|
|
148
|
+
declaratorNode: d,
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
const name = names[0];
|
|
152
|
+
if (name === "self" || name === "ctx") continue;
|
|
153
|
+
topVars.push({
|
|
154
|
+
keyword,
|
|
155
|
+
name,
|
|
156
|
+
value,
|
|
157
|
+
raw,
|
|
158
|
+
names,
|
|
159
|
+
isDestructuring: false,
|
|
160
|
+
start: d.start,
|
|
161
|
+
end: d.end,
|
|
162
|
+
initNode: d.init || null,
|
|
163
|
+
declaratorNode: d,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else if (node.type === "FunctionDeclaration") {
|
|
168
|
+
const name = node.id ? node.id.name : null;
|
|
169
|
+
if (name === "$runtime") {
|
|
170
|
+
runtimeNode = node;
|
|
171
|
+
runtime = script.slice(node.start, node.end);
|
|
172
|
+
} else if (name) {
|
|
173
|
+
topFuncs.push(script.slice(node.start, node.end));
|
|
174
|
+
topFuncNodes.push(node);
|
|
175
|
+
}
|
|
176
|
+
} else if (node.type === "ImportDeclaration") {
|
|
177
|
+
const source = node.source.value;
|
|
178
|
+
const specifiers = node.specifiers.map((s) => {
|
|
179
|
+
if (s.type === "ImportDefaultSpecifier") return { type: "default", local: s.local.name };
|
|
180
|
+
if (s.type === "ImportNamespaceSpecifier") return { type: "namespace", local: s.local.name };
|
|
181
|
+
if (s.type === "ImportSpecifier") return { type: "named", imported: s.imported.name, local: s.local.name };
|
|
182
|
+
return { type: "unknown", local: s.local?.name };
|
|
183
|
+
});
|
|
184
|
+
imports.push({
|
|
185
|
+
source,
|
|
186
|
+
specifiers,
|
|
187
|
+
raw: script.slice(node.start, node.end),
|
|
188
|
+
start: node.start,
|
|
189
|
+
end: node.end,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
ast,
|
|
196
|
+
props,
|
|
197
|
+
topVars,
|
|
198
|
+
topFuncs,
|
|
199
|
+
topFuncNodes,
|
|
200
|
+
imports,
|
|
201
|
+
runtimeNode,
|
|
202
|
+
runtime,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Reachability (Step B)
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
const GLOBALS = new Set([
|
|
211
|
+
"self", "ctx",
|
|
212
|
+
"document", "window", "globalThis",
|
|
213
|
+
"console", "Math", "Date", "JSON", "Object", "Array", "String", "Number", "Boolean",
|
|
214
|
+
"Promise", "Set", "Map", "WeakMap", "WeakSet", "RegExp", "Error", "Intl", "URL", "URLSearchParams",
|
|
215
|
+
"parseInt", "parseFloat", "isNaN", "isFinite", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent",
|
|
216
|
+
"eval", "Function",
|
|
217
|
+
"location", "navigator", "history", "localStorage", "sessionStorage", "fetch",
|
|
218
|
+
"alert", "confirm", "prompt",
|
|
219
|
+
"setTimeout", "setInterval", "clearTimeout", "clearInterval", "requestAnimationFrame", "cancelAnimationFrame",
|
|
220
|
+
"queueMicrotask", "structuredClone",
|
|
221
|
+
"undefined", "NaN", "Infinity",
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
function isDynamicRuntime(runtimeNode, script) {
|
|
225
|
+
// Detect eval("..."), new Function(...), with(...), or computed ctx access with non-literal
|
|
226
|
+
let dynamic = false;
|
|
227
|
+
function walk(n) {
|
|
228
|
+
if (!n || dynamic) return;
|
|
229
|
+
if (n.type === "CallExpression") {
|
|
230
|
+
const callee = n.callee;
|
|
231
|
+
if (callee.type === "Identifier" && (callee.name === "eval" || callee.name === "Function")) dynamic = true;
|
|
232
|
+
if (callee.type === "MemberExpression" && callee.object?.name === "ctx" && callee.computed) {
|
|
233
|
+
// will be handled below as MemberExpression case
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (n.type === "WithStatement") dynamic = true;
|
|
237
|
+
if (n.type === "MemberExpression" && n.object?.type === "Identifier" && n.object.name === "ctx" && n.computed) {
|
|
238
|
+
// ctx["dyn"+x] or ctx[variable] -> dynamic. If property is Literal string, it's static.
|
|
239
|
+
if (n.property.type !== "Literal" || typeof n.property.value !== "string") dynamic = true;
|
|
240
|
+
}
|
|
241
|
+
for (const key in n) {
|
|
242
|
+
if (["type","start","end","loc","range"].includes(key)) continue;
|
|
243
|
+
const child = n[key];
|
|
244
|
+
if (Array.isArray(child)) child.forEach(c => c && typeof c.type === "string" && walk(c));
|
|
245
|
+
else if (child && typeof child.type === "string") walk(child);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
walk(runtimeNode);
|
|
249
|
+
return dynamic;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function collectReferencedIdentifiers(node, outSet) {
|
|
253
|
+
// Walk and collect Identifier references, skipping declarations and non-reference positions
|
|
254
|
+
function walk(n, parent) {
|
|
255
|
+
if (!n || typeof n.type !== "string") return;
|
|
256
|
+
if (n.type === "Identifier") {
|
|
257
|
+
// check if this Identifier is a reference (not declaration)
|
|
258
|
+
let isRef = true;
|
|
259
|
+
if (parent) {
|
|
260
|
+
if (parent.type === "VariableDeclarator" && parent.id === n) isRef = false;
|
|
261
|
+
else if ((parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression") && parent.params.includes(n)) isRef = false;
|
|
262
|
+
else if ((parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression") && parent.id === n) isRef = false;
|
|
263
|
+
else if (parent.type === "MemberExpression" && parent.property === n && !parent.computed) isRef = false;
|
|
264
|
+
else if (parent.type === "Property" && parent.key === n && !parent.computed) isRef = false;
|
|
265
|
+
else if (parent.type === "ImportSpecifier" || parent.type === "ImportDefaultSpecifier" || parent.type === "ImportNamespaceSpecifier") isRef = false;
|
|
266
|
+
else if (parent.type === "ExportSpecifier") isRef = false;
|
|
267
|
+
else if (parent.type === "LabeledStatement" && parent.label === n) isRef = false;
|
|
268
|
+
else if (parent.type === "CatchClause" && parent.param === n) isRef = false;
|
|
269
|
+
else if (parent.type === "AssignmentPattern" && parent.left === n) isRef = false; // param default pattern left is declaration
|
|
270
|
+
}
|
|
271
|
+
if (isRef) outSet.add(n.name);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Skip traversing into declaration patterns where identifiers are not references
|
|
275
|
+
switch (n.type) {
|
|
276
|
+
case "VariableDeclarator":
|
|
277
|
+
// skip id (pattern), only walk init
|
|
278
|
+
if (n.init) walk(n.init, n);
|
|
279
|
+
return;
|
|
280
|
+
case "FunctionDeclaration":
|
|
281
|
+
case "FunctionExpression":
|
|
282
|
+
case "ArrowFunctionExpression":
|
|
283
|
+
// skip params (declarations)
|
|
284
|
+
walk(n.body, n);
|
|
285
|
+
return;
|
|
286
|
+
case "ImportDeclaration":
|
|
287
|
+
return; // skip all
|
|
288
|
+
case "ExportNamedDeclaration":
|
|
289
|
+
if (n.declaration) walk(n.declaration, n);
|
|
290
|
+
return;
|
|
291
|
+
case "MemberExpression":
|
|
292
|
+
walk(n.object, n);
|
|
293
|
+
if (n.computed) walk(n.property, n);
|
|
294
|
+
return;
|
|
295
|
+
case "Property":
|
|
296
|
+
if (n.computed) walk(n.key, n);
|
|
297
|
+
walk(n.value, n);
|
|
298
|
+
return;
|
|
299
|
+
case "MethodDefinition":
|
|
300
|
+
if (n.computed) walk(n.key, n);
|
|
301
|
+
// skip params via FunctionExpression handling already
|
|
302
|
+
walk(n.value, n);
|
|
303
|
+
return;
|
|
304
|
+
default:
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
for (const key in n) {
|
|
308
|
+
if (["type","start","end","loc","range"].includes(key)) continue;
|
|
309
|
+
const child = n[key];
|
|
310
|
+
if (Array.isArray(child)) {
|
|
311
|
+
for (const c of child) if (c && typeof c.type === "string") walk(c, n);
|
|
312
|
+
} else if (child && typeof child.type === "string") {
|
|
313
|
+
walk(child, n);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
walk(node, null);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Compute client-reachable declarations from $runtime.
|
|
322
|
+
* Transitive closure over vars/funcs/props/imports.
|
|
323
|
+
*
|
|
324
|
+
* @param {ReturnType<typeof parseScript>} parsed
|
|
325
|
+
* @param {{bindings?: string[]}} options
|
|
326
|
+
* @returns {{
|
|
327
|
+
* neededProps: any[],
|
|
328
|
+
* neededVars: any[],
|
|
329
|
+
* neededFuncs: string[],
|
|
330
|
+
* neededFuncNodes: any[],
|
|
331
|
+
* neededImports: any[],
|
|
332
|
+
* neededBindings: string[],
|
|
333
|
+
* fallback: boolean
|
|
334
|
+
* }}
|
|
335
|
+
*/
|
|
336
|
+
export function computeReachable(parsed, options = {}) {
|
|
337
|
+
const bindings = options.bindings || [];
|
|
338
|
+
const bindingSet = new Set(bindings);
|
|
339
|
+
|
|
340
|
+
// No runtime -> empty (server-only). Fallback not needed.
|
|
341
|
+
if (!parsed.ast || !parsed.runtimeNode) {
|
|
342
|
+
return {
|
|
343
|
+
neededProps: [],
|
|
344
|
+
neededVars: [],
|
|
345
|
+
neededFuncs: [],
|
|
346
|
+
neededFuncNodes: [],
|
|
347
|
+
neededImports: [],
|
|
348
|
+
neededBindings: [],
|
|
349
|
+
fallback: false,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Parse error fallback -> include all
|
|
354
|
+
if (parsed.parseError) {
|
|
355
|
+
return {
|
|
356
|
+
neededProps: parsed.props.slice(),
|
|
357
|
+
neededVars: parsed.topVars.slice(),
|
|
358
|
+
neededFuncs: parsed.topFuncs.slice(),
|
|
359
|
+
neededFuncNodes: parsed.topFuncNodes.slice(),
|
|
360
|
+
neededImports: parsed.imports.slice(),
|
|
361
|
+
neededBindings: bindings.slice(),
|
|
362
|
+
fallback: true,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Dynamic fallback detection
|
|
367
|
+
if (isDynamicRuntime(parsed.runtimeNode, "")) {
|
|
368
|
+
// Warn and include all
|
|
369
|
+
// console.warn will be done by caller; here just mark fallback
|
|
370
|
+
return {
|
|
371
|
+
neededProps: parsed.props.slice(),
|
|
372
|
+
neededVars: parsed.topVars.slice(),
|
|
373
|
+
neededFuncs: parsed.topFuncs.slice(),
|
|
374
|
+
neededFuncNodes: parsed.topFuncNodes.slice(),
|
|
375
|
+
neededImports: parsed.imports.slice(),
|
|
376
|
+
neededBindings: bindings.slice(),
|
|
377
|
+
fallback: true,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Build maps
|
|
382
|
+
const varMap = new Map(); // name -> var entry (for destructuring, multiple names point to same entry)
|
|
383
|
+
for (const v of parsed.topVars) {
|
|
384
|
+
for (const n of v.names) varMap.set(n, v);
|
|
385
|
+
}
|
|
386
|
+
const funcMap = new Map(); // name -> {node, src, idx}
|
|
387
|
+
for (let i = 0; i < parsed.topFuncNodes.length; i++) {
|
|
388
|
+
const node = parsed.topFuncNodes[i];
|
|
389
|
+
const src = parsed.topFuncs[i];
|
|
390
|
+
funcMap.set(node.id.name, { node, src, idx: i });
|
|
391
|
+
}
|
|
392
|
+
const propMap = new Map();
|
|
393
|
+
for (const p of parsed.props) propMap.set(p.name, p);
|
|
394
|
+
const importMap = new Map();
|
|
395
|
+
for (const imp of parsed.imports) {
|
|
396
|
+
for (const s of imp.specifiers) importMap.set(s.local, imp);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const neededVarNames = new Set();
|
|
400
|
+
const neededFuncNames = new Set();
|
|
401
|
+
const neededPropNames = new Set();
|
|
402
|
+
const neededImportLocals = new Set();
|
|
403
|
+
const neededBindingNames = new Set();
|
|
404
|
+
const worklist = [];
|
|
405
|
+
|
|
406
|
+
const runtimeIds = new Set();
|
|
407
|
+
collectReferencedIdentifiers(parsed.runtimeNode.body || parsed.runtimeNode, runtimeIds);
|
|
408
|
+
// Also collect from runtime params? Params are self/ctx globals, ignore
|
|
409
|
+
|
|
410
|
+
for (const id of runtimeIds) {
|
|
411
|
+
if (GLOBALS.has(id)) continue;
|
|
412
|
+
if (bindingSet.has(id)) { neededBindingNames.add(id); }
|
|
413
|
+
else if (varMap.has(id)) { if (!neededVarNames.has(id)) { neededVarNames.add(id); worklist.push(id); } }
|
|
414
|
+
else if (funcMap.has(id)) { if (!neededFuncNames.has(id)) { neededFuncNames.add(id); worklist.push(id); } }
|
|
415
|
+
else if (propMap.has(id)) { if (!neededPropNames.has(id)) { neededPropNames.add(id); worklist.push(id); } }
|
|
416
|
+
else if (importMap.has(id)) { if (!neededImportLocals.has(id)) { neededImportLocals.add(id); worklist.push(id); } }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// BFS transitive
|
|
420
|
+
while (worklist.length) {
|
|
421
|
+
const cur = worklist.shift();
|
|
422
|
+
let deps = new Set();
|
|
423
|
+
if (varMap.has(cur)) {
|
|
424
|
+
const entry = varMap.get(cur);
|
|
425
|
+
if (entry.initNode) collectReferencedIdentifiers(entry.initNode, deps);
|
|
426
|
+
} else if (funcMap.has(cur)) {
|
|
427
|
+
const entry = funcMap.get(cur);
|
|
428
|
+
collectReferencedIdentifiers(entry.node, deps);
|
|
429
|
+
// Remove self-reference to avoid trivial loop (func name inside its own body is not a dep)
|
|
430
|
+
deps.delete(cur);
|
|
431
|
+
} else if (propMap.has(cur)) {
|
|
432
|
+
const entry = propMap.get(cur);
|
|
433
|
+
if (entry.initNode) collectReferencedIdentifiers(entry.initNode, deps);
|
|
434
|
+
} else if (importMap.has(cur)) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
for (const dep of deps) {
|
|
438
|
+
if (GLOBALS.has(dep)) continue;
|
|
439
|
+
if (bindingSet.has(dep)) { neededBindingNames.add(dep); continue; }
|
|
440
|
+
if (varMap.has(dep) && !neededVarNames.has(dep)) { neededVarNames.add(dep); worklist.push(dep); }
|
|
441
|
+
else if (funcMap.has(dep) && !neededFuncNames.has(dep)) { neededFuncNames.add(dep); worklist.push(dep); }
|
|
442
|
+
else if (propMap.has(dep) && !neededPropNames.has(dep)) { neededPropNames.add(dep); worklist.push(dep); }
|
|
443
|
+
else if (importMap.has(dep) && !neededImportLocals.has(dep)) { neededImportLocals.add(dep); worklist.push(dep); }
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Deduplicate imports/vars that may have multiple names pointing to same entry
|
|
448
|
+
const neededVars = [];
|
|
449
|
+
const seenVarKeys = new Set();
|
|
450
|
+
for (const v of parsed.topVars) {
|
|
451
|
+
const key = v.isDestructuring ? v.raw : v.name;
|
|
452
|
+
if (seenVarKeys.has(key)) continue;
|
|
453
|
+
const isNeeded = v.names.some(n => neededVarNames.has(n));
|
|
454
|
+
if (isNeeded) { neededVars.push(v); seenVarKeys.add(key); }
|
|
455
|
+
}
|
|
456
|
+
const neededFuncs = [];
|
|
457
|
+
const neededFuncNodes = [];
|
|
458
|
+
for (const [name, entry] of funcMap.entries()) {
|
|
459
|
+
if (neededFuncNames.has(name)) {
|
|
460
|
+
neededFuncs.push(entry.src);
|
|
461
|
+
neededFuncNodes.push(entry.node);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const neededProps = parsed.props.filter(p => neededPropNames.has(p.name));
|
|
465
|
+
const neededImportsSet = new Set();
|
|
466
|
+
for (const local of neededImportLocals) {
|
|
467
|
+
const imp = importMap.get(local);
|
|
468
|
+
if (imp) neededImportsSet.add(imp);
|
|
469
|
+
}
|
|
470
|
+
const neededImports = [...neededImportsSet];
|
|
471
|
+
const neededBindings = bindings.filter(b => neededBindingNames.has(b));
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
neededProps,
|
|
475
|
+
neededVars,
|
|
476
|
+
neededFuncs,
|
|
477
|
+
neededFuncNodes,
|
|
478
|
+
neededImports,
|
|
479
|
+
neededBindings,
|
|
480
|
+
fallback: false,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Re-export helpers for convenience
|
|
485
|
+
export { extractBindingNames };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { parseScript, computeReachable } from "./script.js";
|
|
4
|
+
|
|
5
|
+
test("parseScript: simple let/const and props", () => {
|
|
6
|
+
const { props, topVars } = parseScript(`export let title = "Card"; let a = 1, b = 2; const c = 3;`);
|
|
7
|
+
assert.deepEqual(props.map(p=>p.name), ["title"]);
|
|
8
|
+
assert.equal(props[0].defaultValue, '"Card"');
|
|
9
|
+
assert.deepEqual(topVars.map(v=>v.name), ["a","b","c"]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("parseScript: destructuring object and array", () => {
|
|
13
|
+
const { topVars } = parseScript(`const {x,y} = obj; let [p, ...rest] = arr; const {a: aa, b: bb} = obj2;`);
|
|
14
|
+
assert.equal(topVars.length, 3);
|
|
15
|
+
assert.deepEqual(topVars[0].names, ["x","y"]);
|
|
16
|
+
assert.equal(topVars[0].isDestructuring, true);
|
|
17
|
+
assert.deepEqual(topVars[1].names, ["p","rest"]);
|
|
18
|
+
assert.deepEqual(topVars[2].names, ["aa","bb"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("parseScript: comma declarators", () => {
|
|
22
|
+
const { topVars } = parseScript(`let a = 1, b = 2, c;`);
|
|
23
|
+
assert.equal(topVars.length, 3);
|
|
24
|
+
assert.equal(topVars[0].name, "a");
|
|
25
|
+
assert.equal(topVars[0].value, "1");
|
|
26
|
+
assert.equal(topVars[1].name, "b");
|
|
27
|
+
assert.equal(topVars[2].name, "c");
|
|
28
|
+
assert.equal(topVars[2].value, undefined);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("parseScript: imports default/named/namespace/side-effect", () => {
|
|
32
|
+
const { imports } = parseScript(`
|
|
33
|
+
import Action from "./Action.html";
|
|
34
|
+
import {x} from "./lib.js";
|
|
35
|
+
import * as ns from "./ns.js";
|
|
36
|
+
import "./polyfill.js";
|
|
37
|
+
import {y as z} from "./lib.js";
|
|
38
|
+
`);
|
|
39
|
+
assert.equal(imports.length, 5);
|
|
40
|
+
assert.equal(imports[0].specifiers[0].type, "default");
|
|
41
|
+
assert.equal(imports[0].specifiers[0].local, "Action");
|
|
42
|
+
assert.equal(imports[1].specifiers[0].type, "named");
|
|
43
|
+
assert.equal(imports[2].specifiers[0].type, "namespace");
|
|
44
|
+
assert.equal(imports[3].specifiers.length, 0);
|
|
45
|
+
assert.equal(imports[4].specifiers[0].imported, "y");
|
|
46
|
+
assert.equal(imports[4].specifiers[0].local, "z");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("parseScript: comments containing function $runtime are ignored", () => {
|
|
50
|
+
const { runtime, topVars } = parseScript(`let a = 1; // function $runtime(){ fake }
|
|
51
|
+
/* function $runtime(){ fake } */
|
|
52
|
+
function $runtime(){ a++ }`);
|
|
53
|
+
assert.ok(runtime && runtime.includes("$runtime"));
|
|
54
|
+
assert.equal(topVars.length, 1);
|
|
55
|
+
assert.equal(topVars[0].name, "a");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("parseScript: nested braces in template literals not confused", () => {
|
|
59
|
+
const { topFuncs } = parseScript("function foo(){ return `a {b} c`; } function $runtime(){ foo(); }");
|
|
60
|
+
assert.equal(topFuncs.length, 1);
|
|
61
|
+
assert.ok(topFuncs[0].includes("foo"));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("parseScript: async $runtime", () => {
|
|
65
|
+
const { runtime } = parseScript(`async function $runtime(){ await fetch(); }`);
|
|
66
|
+
assert.ok(runtime && runtime.startsWith("async function $runtime"));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("computeReachable: transitive closure", () => {
|
|
70
|
+
const parsed = parseScript(`let a = b + 1; let b = 2; let unused = 99; function foo(){ return a; } function bar(){ return unused; } function $runtime(){ foo(); }`);
|
|
71
|
+
const reach = computeReachable(parsed, {bindings: []});
|
|
72
|
+
assert.ok(reach.neededVars.some(v=> v.names.includes("a")));
|
|
73
|
+
assert.ok(reach.neededVars.some(v=> v.names.includes("b")));
|
|
74
|
+
assert.equal(reach.neededVars.some(v=> v.names.includes("unused")), false);
|
|
75
|
+
assert.ok(reach.neededFuncs.some(s=> s.includes("foo")));
|
|
76
|
+
assert.equal(reach.neededFuncs.some(s=> s.includes("bar")), false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("computeReachable: bindings only if referenced", () => {
|
|
80
|
+
const parsed = parseScript(`let count = 0; function inc(){ count++; } function $runtime(){ inc(); btn.addEventListener("click", inc); }`);
|
|
81
|
+
const reach = computeReachable(parsed, {bindings: ["btn","span"]});
|
|
82
|
+
assert.deepEqual(reach.neededBindings, ["btn"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("computeReachable: props only if referenced", () => {
|
|
86
|
+
const parsed = parseScript(`export let title = "Card"; export let unused = "hi"; function $runtime(){ console.log(title); }`);
|
|
87
|
+
const reach = computeReachable(parsed, {bindings: []});
|
|
88
|
+
assert.deepEqual(reach.neededProps.map(p=>p.name), ["title"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("computeReachable: no runtime -> empty", () => {
|
|
92
|
+
const parsed = parseScript(`let a = 1; function foo(){}`);
|
|
93
|
+
const reach = computeReachable(parsed, {bindings: []});
|
|
94
|
+
assert.equal(reach.neededVars.length, 0);
|
|
95
|
+
assert.equal(reach.neededFuncs.length, 0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("computeReachable: dynamic fallback", () => {
|
|
99
|
+
const parsed = parseScript(`let a = 1; function $runtime(){ eval("a"); }`);
|
|
100
|
+
const reach = computeReachable(parsed, {bindings: []});
|
|
101
|
+
assert.equal(reach.fallback, true);
|
|
102
|
+
assert.ok(reach.neededVars.length > 0);
|
|
103
|
+
});
|