chocola 2.0.0-next.10 → 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 +16 -4
- package/bin/chocola.js +227 -0
- package/compiler/component-processor.js +308 -67
- package/compiler/config.js +42 -12
- package/compiler/index.js +34 -26
- package/compiler/module-graph.js +57 -10
- package/compiler/pipeline.js +3 -1
- package/compiler/render.js +3 -1
- package/dev/index.js +52 -14
- package/package.json +15 -1
- package/parser/component.js +77 -5
- package/parser/index.js +1 -0
- package/parser/script.js +485 -0
- package/parser/script.test.js +103 -0
- package/server/index.js +40 -10
- package/utils.js +73 -3
- package/index.js +0 -0
|
@@ -3,7 +3,7 @@ import { parseHTML } from "linkedom";
|
|
|
3
3
|
import { protectCurlyBraces } from "../utils.js";
|
|
4
4
|
import { genRandomId, runtimeFunctionId, throwError, deterministicHash, warnConstantCondition, warnUnusedDeclaration, findElementLine } from "./utils.js";
|
|
5
5
|
import {
|
|
6
|
-
extractPropsDefaults, extractRuntime, extractTopLevelFunctions, extractTopLevelVariables,
|
|
6
|
+
extractPropsDefaults, extractRuntime, extractTopLevelFunctions, extractTopLevelVariables, parseScript, computeReachable,
|
|
7
7
|
extractCtxFromEl, hasMountIf, getMountIf,
|
|
8
8
|
reservedAttrs, validateChainStructure, applyConditionalToElement, interpolateNode,
|
|
9
9
|
scopeCss, compileExpr, evaluateConstant,
|
|
@@ -13,7 +13,7 @@ import chalk from "./chalk.js";
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class ProcessContext {
|
|
16
|
-
constructor(loadedComponents, runtimeChunks, compIdColl, runtimeMap, cssScopes, cssScopesMap, scopedStyles, staticCtxRegistry, csrClasses) {
|
|
16
|
+
constructor(loadedComponents, runtimeChunks, compIdColl, runtimeMap, cssScopes, cssScopesMap, scopedStyles, staticCtxRegistry, csrClasses, treeShakeRuntime = true, originalNames = null) {
|
|
17
17
|
this.loadedComponents = loadedComponents;
|
|
18
18
|
this.runtimeChunks = runtimeChunks;
|
|
19
19
|
this.compIdColl = compIdColl;
|
|
@@ -24,9 +24,19 @@ class ProcessContext {
|
|
|
24
24
|
this.staticCtxRegistry = staticCtxRegistry;
|
|
25
25
|
this.csrClasses = csrClasses;
|
|
26
26
|
this.unusedWarned = new Set();
|
|
27
|
+
this.treeShakeRuntime = treeShakeRuntime !== false;
|
|
28
|
+
this.originalNames = originalNames || new Map();
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function resolveDisplayName(compName, cx) {
|
|
33
|
+
if (!compName) return compName;
|
|
34
|
+
if (cx?.originalNames?.has(compName)) return cx.originalNames.get(compName);
|
|
35
|
+
const lower = compName.toLowerCase();
|
|
36
|
+
if (cx?.originalNames?.has(lower)) return cx.originalNames.get(lower);
|
|
37
|
+
return compName;
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
function escapeForTemplateLiteral(str) {
|
|
31
41
|
return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
|
|
32
42
|
}
|
|
@@ -45,6 +55,7 @@ function parseFragment(html, doc) {
|
|
|
45
55
|
function generateCSRClass(compName, cx, explicitClassName) {
|
|
46
56
|
if (cx.csrClasses.has(compName)) return;
|
|
47
57
|
|
|
58
|
+
const displayName = resolveDisplayName(compName, cx);
|
|
48
59
|
let instance = cx.loadedComponents.get(compName);
|
|
49
60
|
if (!instance) return;
|
|
50
61
|
|
|
@@ -60,7 +71,7 @@ function generateCSRClass(compName, cx, explicitClassName) {
|
|
|
60
71
|
const compProps = extractPropsDefaults(script);
|
|
61
72
|
const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
|
|
62
73
|
const topVars = extractTopLevelVariables(script || "");
|
|
63
|
-
let runtime = extractRuntime(script || "",
|
|
74
|
+
let runtime = extractRuntime(script || "", displayName);
|
|
64
75
|
|
|
65
76
|
if (!cx.cssScopesMap.has(compName)) {
|
|
66
77
|
cx.cssScopesMap.set(compName, deterministicHash(compName, 8));
|
|
@@ -94,12 +105,42 @@ function generateCSRClass(compName, cx, explicitClassName) {
|
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
let csrRuntimeSource = null;
|
|
108
|
+
// Compute reachable for client bundling (Step B) - Feature flag: treeShakeRuntime
|
|
109
|
+
const parsedForReach = parseScript(script || "");
|
|
110
|
+
let reach = null;
|
|
111
|
+
if (cx.treeShakeRuntime !== false && parsedForReach.ast && runtime) {
|
|
112
|
+
reach = computeReachable(parsedForReach, { bindings: [...bindVarNames] });
|
|
113
|
+
if (reach.fallback) {
|
|
114
|
+
// conservative: include all and warn
|
|
115
|
+
console.warn(chalk.yellow(`WARN ${displayName} — dynamic $runtime, including all declarations`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
97
118
|
const injectedNames = new Set(compProps.map(p => p.name));
|
|
98
119
|
for (const varName of bindVarNames) injectedNames.add(varName);
|
|
99
|
-
|
|
120
|
+
let topVarsToInject = topVars.filter(v => !injectedNames.has(v.name));
|
|
121
|
+
let propsToInject = compProps;
|
|
122
|
+
let funcsToInject = topFuncSrc;
|
|
123
|
+
let bindingsToInject = [...bindVarNames];
|
|
124
|
+
if (cx.treeShakeRuntime !== false && reach && !reach.fallback) {
|
|
125
|
+
const neededVarNames = new Set(reach.neededVars.flatMap(v => v.names));
|
|
126
|
+
const neededPropNames = new Set(reach.neededProps.map(p => p.name));
|
|
127
|
+
const neededFuncNames = new Set(reach.neededFuncs.map(src => {
|
|
128
|
+
const m = src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/);
|
|
129
|
+
return m ? m[1] : null;
|
|
130
|
+
}).filter(Boolean));
|
|
131
|
+
const neededBindingSet = new Set(reach.neededBindings);
|
|
132
|
+
// Filter to reachable only
|
|
133
|
+
propsToInject = compProps.filter(p => neededPropNames.has(p.name));
|
|
134
|
+
topVarsToInject = topVars.filter(v => neededVarNames.has(v.name) && !injectedNames.has(v.name));
|
|
135
|
+
funcsToInject = topFuncSrc.filter(src => {
|
|
136
|
+
const m = src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/);
|
|
137
|
+
return m && neededFuncNames.has(m[1]);
|
|
138
|
+
});
|
|
139
|
+
bindingsToInject = [...bindVarNames].filter(b => neededBindingSet.has(b));
|
|
140
|
+
}
|
|
100
141
|
if (runtime) {
|
|
101
142
|
let injectCode = "";
|
|
102
|
-
for (const { name, defaultValue } of
|
|
143
|
+
for (const { name, defaultValue } of propsToInject) {
|
|
103
144
|
if (defaultValue !== undefined) {
|
|
104
145
|
injectCode += `let ${name} = ctx.${name}??(${defaultValue});\n`;
|
|
105
146
|
} else {
|
|
@@ -108,18 +149,18 @@ function generateCSRClass(compName, cx, explicitClassName) {
|
|
|
108
149
|
}
|
|
109
150
|
for (const { keyword, name, value } of topVarsToInject) {
|
|
110
151
|
if (value !== undefined) {
|
|
111
|
-
injectCode += `${keyword} ${name} =
|
|
152
|
+
injectCode += `${keyword} ${name} = ${value};\n`;
|
|
112
153
|
} else {
|
|
113
154
|
injectCode += `${keyword} ${name};\n`;
|
|
114
155
|
}
|
|
115
156
|
}
|
|
116
|
-
for (const varName of
|
|
157
|
+
for (const varName of bindingsToInject) {
|
|
117
158
|
if (varName !== "self") {
|
|
118
159
|
injectCode += `let ${varName} = ctx.${varName};\n`;
|
|
119
160
|
}
|
|
120
161
|
}
|
|
121
|
-
if (
|
|
122
|
-
injectCode += "\n" +
|
|
162
|
+
if (funcsToInject.length > 0) {
|
|
163
|
+
injectCode += "\n" + funcsToInject.join("\n\n") + "\n";
|
|
123
164
|
}
|
|
124
165
|
runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
|
|
125
166
|
runtime = runtime.replace(`${RUNTIME_KW}()`, `function(self, ctx)`);
|
|
@@ -128,13 +169,13 @@ function generateCSRClass(compName, cx, explicitClassName) {
|
|
|
128
169
|
|
|
129
170
|
const className = explicitClassName || compName.replace(".html", "").replace(/^\w/, c => c.toUpperCase());
|
|
130
171
|
const propsParts = [];
|
|
131
|
-
|
|
132
|
-
|
|
172
|
+
// For CSR class props: when runtime exists, include only reachable; otherwise keep all (fallback for CSR-only)
|
|
173
|
+
let propsForClass = compProps;
|
|
174
|
+
if (cx.treeShakeRuntime !== false && runtime && reach && !reach.fallback) {
|
|
175
|
+
propsForClass = propsToInject;
|
|
133
176
|
}
|
|
134
|
-
for (const { name,
|
|
135
|
-
|
|
136
|
-
propsParts.push(`${JSON.stringify(name)}: ${value}`);
|
|
137
|
-
}
|
|
177
|
+
for (const { name, defaultValue } of propsForClass) {
|
|
178
|
+
propsParts.push(`${JSON.stringify(name)}: ${defaultValue !== undefined ? defaultValue : "null"}`);
|
|
138
179
|
}
|
|
139
180
|
|
|
140
181
|
const childrenPart = childMappings.length > 0
|
|
@@ -168,6 +209,7 @@ function findLineInSource(source, regex) {
|
|
|
168
209
|
}
|
|
169
210
|
|
|
170
211
|
function warnUnusedDeclarations(cx, compName, instance, script, fragment) {
|
|
212
|
+
const displayName = resolveDisplayName(compName, cx);
|
|
171
213
|
if (!script && !fragment.querySelector("[bind\\:]")) return;
|
|
172
214
|
if (cx.unusedWarned.has(compName)) return;
|
|
173
215
|
cx.unusedWarned.add(compName);
|
|
@@ -209,14 +251,14 @@ function warnUnusedDeclarations(cx, compName, instance, script, fragment) {
|
|
|
209
251
|
const isUnused = (name) => (counts.get(name) || 0) <= 1;
|
|
210
252
|
const warn = (kind, name, declRegex) => {
|
|
211
253
|
const lineNum = findLineInSource(instance, declRegex);
|
|
212
|
-
warnUnusedDeclaration(lineNum !== null ? `${
|
|
254
|
+
warnUnusedDeclaration(lineNum !== null ? `${displayName}:${lineNum}` : displayName, kind, name);
|
|
213
255
|
};
|
|
214
256
|
|
|
215
257
|
for (const name of props) {
|
|
216
258
|
if (isUnused(name)) warn("prop", name, new RegExp("export\\s+let\\s+" + escapeNameForRegex(name) + "\\b"));
|
|
217
259
|
}
|
|
218
260
|
for (const name of topVars) {
|
|
219
|
-
if (isUnused(name)) warn("variable", name, new RegExp("(?:^|[^\\w])(?:let|const)\\s+" + escapeNameForRegex(name) + "\\b"));
|
|
261
|
+
if (isUnused(name)) warn("variable", name, new RegExp("(?:^|[^\\w])(?:let|const|var)\\s+" + escapeNameForRegex(name) + "\\b"));
|
|
220
262
|
}
|
|
221
263
|
for (const name of topFuncs) {
|
|
222
264
|
if (isUnused(name)) warn("function", name, new RegExp("(?:async\\s+)?function\\s+" + escapeNameForRegex(name) + "\\b"));
|
|
@@ -227,7 +269,7 @@ function warnUnusedDeclarations(cx, compName, instance, script, fragment) {
|
|
|
227
269
|
seenBindings.add(name);
|
|
228
270
|
if (isUnused(name)) {
|
|
229
271
|
const lineNum = findElementLine(instance, element.outerHTML);
|
|
230
|
-
warnUnusedDeclaration(lineNum !== null ? `${
|
|
272
|
+
warnUnusedDeclaration(lineNum !== null ? `${displayName}:${lineNum}` : displayName, "binding", name);
|
|
231
273
|
}
|
|
232
274
|
}
|
|
233
275
|
}
|
|
@@ -242,6 +284,7 @@ export function processComponentElement(
|
|
|
242
284
|
) {
|
|
243
285
|
const tagName = element.tagName.toLowerCase();
|
|
244
286
|
const compName = tagName + ".html";
|
|
287
|
+
const displayName = resolveDisplayName(compName, cx);
|
|
245
288
|
let instance = cx.loadedComponents.get(compName);
|
|
246
289
|
|
|
247
290
|
if (!instance || instance === undefined) return false;
|
|
@@ -255,19 +298,60 @@ export function processComponentElement(
|
|
|
255
298
|
let styles = doc.querySelector("style")?.innerHTML;
|
|
256
299
|
|
|
257
300
|
if (!template) {
|
|
258
|
-
console.warn(chalk.yellow(`${
|
|
301
|
+
console.warn(chalk.yellow(`${displayName} — component is missing a <template>`));
|
|
259
302
|
return false;
|
|
260
303
|
}
|
|
261
304
|
|
|
305
|
+
// Store original script for reachability (before stripping)
|
|
306
|
+
const originalScriptForReach = script;
|
|
262
307
|
if (script) {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
308
|
+
const parsed = parseScript(script);
|
|
309
|
+
if (parsed.ast && parsed.imports.length > 0) {
|
|
310
|
+
// Determine needed imports via reachability (Step B) - Feature flag
|
|
311
|
+
let importsToGenerate = parsed.imports;
|
|
312
|
+
if (cx.treeShakeRuntime === false) {
|
|
313
|
+
// keep all imports when tree-shaking disabled
|
|
314
|
+
} else if (parsed.runtimeNode) {
|
|
315
|
+
const reachForImports = computeReachable(parsed, { bindings: [] });
|
|
316
|
+
if (!reachForImports.fallback) {
|
|
317
|
+
const neededSet = new Set(reachForImports.neededImports);
|
|
318
|
+
importsToGenerate = parsed.imports.filter(imp => neededSet.has(imp));
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
// No runtime -> no client imports needed (server-only)
|
|
322
|
+
importsToGenerate = [];
|
|
268
323
|
}
|
|
269
|
-
|
|
270
|
-
|
|
324
|
+
for (const imp of importsToGenerate) {
|
|
325
|
+
const isComponent = imp.source.toLowerCase().endsWith(".html");
|
|
326
|
+
if (!isComponent) {
|
|
327
|
+
console.warn(chalk.yellow(`WARN ${displayName} — JS import "${imp.source}" is client-reachable but not bundled (Phase 1: dropping)`));
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const importedCompName = path.basename(imp.source).toLowerCase();
|
|
331
|
+
if (cx.loadedComponents.has(importedCompName)) {
|
|
332
|
+
if (imp.specifiers.length === 0) {
|
|
333
|
+
generateCSRClass(importedCompName, cx);
|
|
334
|
+
} else {
|
|
335
|
+
for (const spec of imp.specifiers) {
|
|
336
|
+
generateCSRClass(importedCompName, cx, spec.local);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const sorted = [...parsed.imports].sort((a, b) => b.start - a.start);
|
|
342
|
+
for (const imp of sorted) {
|
|
343
|
+
script = script.slice(0, imp.start) + script.slice(imp.end);
|
|
344
|
+
}
|
|
345
|
+
} else if (parsed.parseError) {
|
|
346
|
+
const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?\s*/g;
|
|
347
|
+
script = script.replace(importRegex, (_, importedName, importPath) => {
|
|
348
|
+
const importedCompName = path.basename(importPath).toLowerCase();
|
|
349
|
+
if (cx.loadedComponents.has(importedCompName)) {
|
|
350
|
+
generateCSRClass(importedCompName, cx, importedName);
|
|
351
|
+
}
|
|
352
|
+
return "";
|
|
353
|
+
});
|
|
354
|
+
}
|
|
271
355
|
}
|
|
272
356
|
|
|
273
357
|
const compProps = extractPropsDefaults(script);
|
|
@@ -299,20 +383,65 @@ export function processComponentElement(
|
|
|
299
383
|
|
|
300
384
|
const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
|
|
301
385
|
const topVars = extractTopLevelVariables(script || "");
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
386
|
+
// Server resolution: evaluate all top-level declarations into ctx for template rendering
|
|
387
|
+
// Use AST-derived parsed data when available to correctly handle destructuring and comma declarators
|
|
388
|
+
// Use ctx-aware evaluation so initializers can reference earlier ctx vars (e.g., let a = x + y)
|
|
389
|
+
const ctxProxyForServer = new Proxy(ctx, { has() { return true; }, get(t,k){ return t[k]; } });
|
|
390
|
+
if (originalScriptForReach) {
|
|
391
|
+
const parsedForServer = parseScript(originalScriptForReach);
|
|
392
|
+
if (parsedForServer.ast) {
|
|
393
|
+
for (const v of parsedForServer.topVars) {
|
|
394
|
+
if (v.isDestructuring) {
|
|
395
|
+
if (v.names.every(n => n in ctx)) continue;
|
|
396
|
+
if (v.value === undefined) {
|
|
397
|
+
for (const n of v.names) if (!(n in ctx)) ctx[n] = undefined;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const initVal = compileExpr(v.value, true)(ctxProxyForServer);
|
|
402
|
+
const fnBody = `${v.keyword} ${v.name} = initVal; return {${v.names.join(", ")}};`;
|
|
403
|
+
const result = new Function("initVal", fnBody)(initVal);
|
|
404
|
+
for (const n of v.names) if (!(n in ctx)) ctx[n] = result[n];
|
|
405
|
+
} catch {}
|
|
406
|
+
} else {
|
|
407
|
+
const { name, value } = v;
|
|
408
|
+
if (name in ctx) continue;
|
|
409
|
+
if (value !== undefined) {
|
|
410
|
+
try { ctx[name] = compileExpr(value, true)(ctxProxyForServer); } catch {}
|
|
411
|
+
} else {
|
|
412
|
+
if (!(name in ctx)) ctx[name] = undefined;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
for (const src of parsedForServer.topFuncs) {
|
|
417
|
+
try {
|
|
418
|
+
const fn = (0, eval)("(" + src + ")");
|
|
419
|
+
const name = fn.name;
|
|
420
|
+
if (name && !(name in ctx)) ctx[name] = fn;
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
// Fallback to shim evaluation (ctx-aware for inter-var deps)
|
|
425
|
+
for (const { name, value } of topVars) {
|
|
426
|
+
if (name in ctx) continue;
|
|
427
|
+
if (value !== undefined) {
|
|
428
|
+
try { ctx[name] = compileExpr(value, true)(ctxProxyForServer); } catch {}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const src of topFuncSrc) {
|
|
432
|
+
try { const fn = (0, eval)("(" + src + ")"); const name = fn.name; if (name && !(name in ctx)) ctx[name] = fn; } catch {}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
for (const { name, value } of topVars) {
|
|
437
|
+
if (name in ctx) continue;
|
|
438
|
+
if (value !== undefined) {
|
|
439
|
+
try { ctx[name] = compileExpr(value, true)(ctxProxyForServer); } catch {}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
for (const src of topFuncSrc) {
|
|
443
|
+
try { const fn = (0, eval)("(" + src + ")"); const name = fn.name; if (name && !(name in ctx)) ctx[name] = fn; } catch {}
|
|
308
444
|
}
|
|
309
|
-
}
|
|
310
|
-
for (const src of topFuncSrc) {
|
|
311
|
-
try {
|
|
312
|
-
const fn = (0, eval)("(" + src + ")");
|
|
313
|
-
const name = fn.name;
|
|
314
|
-
if (name && !(name in ctx)) ctx[name] = fn;
|
|
315
|
-
} catch {}
|
|
316
445
|
}
|
|
317
446
|
|
|
318
447
|
const elInnerHtml = element.innerHTML;
|
|
@@ -330,7 +459,7 @@ export function processComponentElement(
|
|
|
330
459
|
if (sourceFile) {
|
|
331
460
|
validateChainStructure(slotFragment, sourceFile, sourceContent, elInnerHtml);
|
|
332
461
|
}
|
|
333
|
-
validateChainStructure(fragment, instance.__sourceFile ||
|
|
462
|
+
validateChainStructure(fragment, instance.__sourceFile || displayName, instance, template);
|
|
334
463
|
Array.from(fragment.querySelectorAll("slot")).forEach(slot => {
|
|
335
464
|
slot.replaceWith(slotFragment);
|
|
336
465
|
});
|
|
@@ -350,7 +479,7 @@ export function processComponentElement(
|
|
|
350
479
|
let location = sourceFile;
|
|
351
480
|
if (instance) {
|
|
352
481
|
const lineNum = findElementLine(instance, el.outerHTML);
|
|
353
|
-
if (lineNum !== null) location = `${
|
|
482
|
+
if (lineNum !== null) location = `${displayName}:${lineNum}`;
|
|
354
483
|
}
|
|
355
484
|
if (location === sourceFile && sourceContent) {
|
|
356
485
|
const lineNum = findElementLine(sourceContent, el.outerHTML);
|
|
@@ -385,7 +514,7 @@ export function processComponentElement(
|
|
|
385
514
|
|
|
386
515
|
if (hasElif || hasElse) {
|
|
387
516
|
if (!condChain.active) {
|
|
388
|
-
throwError(`${instance.__sourceFile ||
|
|
517
|
+
throwError(`${instance.__sourceFile || displayName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/mount:if sibling`);
|
|
389
518
|
}
|
|
390
519
|
if (condChain.rendered) {
|
|
391
520
|
child.remove();
|
|
@@ -474,7 +603,7 @@ export function processComponentElement(
|
|
|
474
603
|
child,
|
|
475
604
|
cx,
|
|
476
605
|
renderChain.concat(compName),
|
|
477
|
-
|
|
606
|
+
displayName,
|
|
478
607
|
template,
|
|
479
608
|
globalCtx
|
|
480
609
|
);
|
|
@@ -531,32 +660,113 @@ export function processComponentElement(
|
|
|
531
660
|
|
|
532
661
|
script = script.replace(ctxRegex, "ctx");
|
|
533
662
|
|
|
534
|
-
let runtime = extractRuntime(script,
|
|
663
|
+
let runtime = extractRuntime(script, displayName);
|
|
535
664
|
|
|
536
665
|
if (runtime) {
|
|
537
666
|
let fnEntry = cx.runtimeMap && cx.runtimeMap.get(compName);
|
|
538
667
|
let fnId;
|
|
539
668
|
if (!fnEntry) {
|
|
540
669
|
fnId = runtimeFunctionId(compName);
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
670
|
+
// Step B: filter to client-reachable declarations - Feature flag
|
|
671
|
+
let reachInject = null;
|
|
672
|
+
if (cx.treeShakeRuntime !== false && originalScriptForReach) {
|
|
673
|
+
const parsedReach = parseScript(originalScriptForReach);
|
|
674
|
+
if (parsedReach.ast) {
|
|
675
|
+
const bindNames = bindings.map(b => b.varName);
|
|
676
|
+
reachInject = computeReachable(parsedReach, { bindings: bindNames });
|
|
677
|
+
if (reachInject.fallback) {
|
|
678
|
+
console.warn(chalk.yellow(`WARN ${displayName} — dynamic $runtime, including all declarations`));
|
|
679
|
+
reachInject = null;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// Prepare filtered sets
|
|
684
|
+
let compPropsToInject = compProps;
|
|
685
|
+
let topVarsToInject = topVars;
|
|
686
|
+
let bindingsToInject = bindings;
|
|
687
|
+
let topFuncsToInject = topFuncSrc;
|
|
688
|
+
if (reachInject) {
|
|
689
|
+
const neededPropNames = new Set(reachInject.neededProps.map(p => p.name));
|
|
690
|
+
const neededVarNames = new Set(reachInject.neededVars.flatMap(v => v.names));
|
|
691
|
+
const neededBindingSet = new Set(reachInject.neededBindings);
|
|
692
|
+
const neededFuncNames = new Set(reachInject.neededFuncs.map(src => {
|
|
693
|
+
const m = src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/);
|
|
694
|
+
return m ? m[1] : null;
|
|
695
|
+
}).filter(Boolean));
|
|
696
|
+
// Rebuild ctxDef filtered to needed props (inject only reachable props)
|
|
697
|
+
// Note: declared handling below will be updated to use filtered props
|
|
698
|
+
compPropsToInject = compProps.filter(p => neededPropNames.has(p.name));
|
|
699
|
+
topVarsToInject = topVars.filter(v => neededVarNames.has(v.name));
|
|
700
|
+
bindingsToInject = bindings.filter(b => neededBindingSet.has(b.varName));
|
|
701
|
+
topFuncsToInject = topFuncSrc.filter(src => {
|
|
702
|
+
const m = src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/);
|
|
703
|
+
return m && neededFuncNames.has(m[1]);
|
|
704
|
+
});
|
|
705
|
+
// Rebuild ctxDef to only include reachable props
|
|
706
|
+
// declared already has runtimeCtx keys; now add only needed props
|
|
707
|
+
// Recreate ctxDef parts filtered
|
|
708
|
+
const ctxDefPartsFiltered = [];
|
|
709
|
+
const declaredForCtx = new Set(Object.keys(runtimeCtx));
|
|
710
|
+
for (const { name, defaultValue } of compPropsToInject) {
|
|
711
|
+
if (declaredForCtx.has(name)) continue;
|
|
712
|
+
if (defaultValue !== undefined) {
|
|
713
|
+
ctxDefPartsFiltered.push(`let ${name} = ctx.${name}??(${defaultValue});\n`);
|
|
714
|
+
} else {
|
|
715
|
+
ctxDefPartsFiltered.push(`let ${name} = ctx.${name};\n`);
|
|
716
|
+
}
|
|
717
|
+
declaredForCtx.add(name);
|
|
718
|
+
}
|
|
719
|
+
// Prepend runtimeCtx parts (already in ctxDefParts) but we need to reconstruct
|
|
720
|
+
// Simpler: rebuild ctxDef from scratch filtered
|
|
721
|
+
const runtimeCtxParts = [];
|
|
722
|
+
for (const [k,v] of Object.entries(runtimeCtx)) {
|
|
723
|
+
runtimeCtxParts.push(`let ${k} = ctx.${k}??${JSON.stringify(v)};\n`);
|
|
724
|
+
}
|
|
725
|
+
const filteredCtxDef = runtimeCtxParts.join("") + ctxDefPartsFiltered.join("");
|
|
726
|
+
// Use filtered ctxDef for injection
|
|
727
|
+
// declared set for topVars should include runtimeCtx + needed props + bindingsToInject
|
|
728
|
+
// We'll set injectCode later using filteredCtxDef
|
|
729
|
+
// Override ctxDef and declared handling
|
|
730
|
+
// Store for later use
|
|
731
|
+
reachInject._filteredCtxDef = filteredCtxDef;
|
|
732
|
+
reachInject._compPropsToInject = compPropsToInject;
|
|
733
|
+
reachInject._topVarsToInject = topVarsToInject;
|
|
734
|
+
reachInject._bindingsToInject = bindingsToInject;
|
|
735
|
+
reachInject._topFuncsToInject = topFuncsToInject;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let injectCode;
|
|
739
|
+
let effectiveTopVars;
|
|
740
|
+
let effectiveBindings;
|
|
741
|
+
let effectiveFuncs;
|
|
742
|
+
if (reachInject && reachInject._filteredCtxDef !== undefined) {
|
|
743
|
+
injectCode = reachInject._filteredCtxDef;
|
|
744
|
+
// declared for topVars filtering should include runtimeCtx + needed props + needed bindings
|
|
745
|
+
const filteredDeclared = new Set([...Object.keys(runtimeCtx), ...reachInject._compPropsToInject.map(p=>p.name), ...reachInject._bindingsToInject.map(b=>b.varName)]);
|
|
746
|
+
effectiveTopVars = reachInject._topVarsToInject.filter(v => !filteredDeclared.has(v.name));
|
|
747
|
+
effectiveBindings = reachInject._bindingsToInject;
|
|
748
|
+
effectiveFuncs = reachInject._topFuncsToInject;
|
|
749
|
+
} else {
|
|
750
|
+
injectCode = ctxDef;
|
|
751
|
+
for (const b of bindings) declared.add(b.varName);
|
|
752
|
+
effectiveTopVars = topVars.filter(v => !declared.has(v.name));
|
|
753
|
+
effectiveBindings = bindings;
|
|
754
|
+
effectiveFuncs = topFuncSrc;
|
|
755
|
+
}
|
|
756
|
+
if (effectiveTopVars.length > 0) {
|
|
757
|
+
injectCode += "\n" + effectiveTopVars.map(v => v.value !== undefined
|
|
758
|
+
? `${v.keyword} ${v.name} = ${v.value};`
|
|
549
759
|
: `${v.keyword} ${v.name};`
|
|
550
760
|
).join("\n") + "\n";
|
|
551
761
|
}
|
|
552
|
-
if (
|
|
553
|
-
injectCode += "\n" +
|
|
762
|
+
if (effectiveBindings.length > 0) {
|
|
763
|
+
injectCode += "\n" + effectiveBindings.map(b => {
|
|
554
764
|
const accessor = b.prop === "self" ? "" : "." + b.prop;
|
|
555
765
|
return "let " + b.varName + " = self.querySelector('[data-chbind-" + b.bindId + "]')" + accessor + ";";
|
|
556
766
|
}).join("\n") + "\n";
|
|
557
767
|
}
|
|
558
|
-
if (
|
|
559
|
-
injectCode += "\n" +
|
|
768
|
+
if (effectiveFuncs.length > 0) {
|
|
769
|
+
injectCode += "\n" + effectiveFuncs.join("\n\n") + "\n";
|
|
560
770
|
}
|
|
561
771
|
runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
|
|
562
772
|
|
|
@@ -598,21 +808,52 @@ export function processComponentElement(
|
|
|
598
808
|
return firstChild && firstChild.nodeType === 1 ? firstChild : true;
|
|
599
809
|
}
|
|
600
810
|
|
|
601
|
-
export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent, globalCtx = {}) {
|
|
811
|
+
export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent, globalCtx = {}, treeShakeRuntime = true, originalNames = null) {
|
|
602
812
|
const cx = new ProcessContext(
|
|
603
|
-
loadedComponents, [], [], new Map(), [], new Map(), [], new Map(), new Map()
|
|
813
|
+
loadedComponents, [], [], new Map(), [], new Map(), [], new Map(), new Map(), treeShakeRuntime, originalNames
|
|
604
814
|
);
|
|
605
815
|
|
|
606
816
|
for (const [compName, instance] of cx.loadedComponents) {
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
817
|
+
const scriptMatch = instance.match(/<script>([\s\S]*?)<\/script>/i);
|
|
818
|
+
if (scriptMatch) {
|
|
819
|
+
const scriptContent = scriptMatch[1];
|
|
820
|
+
const parsed = parseScript(scriptContent);
|
|
821
|
+
if (parsed.ast) {
|
|
822
|
+
let importsToGenerate = parsed.imports;
|
|
823
|
+
if (cx.treeShakeRuntime === false) {
|
|
824
|
+
// keep all when disabled
|
|
825
|
+
} else if (parsed.runtimeNode) {
|
|
826
|
+
const reach = computeReachable(parsed, { bindings: [] });
|
|
827
|
+
if (!reach.fallback) {
|
|
828
|
+
const neededSet = new Set(reach.neededImports);
|
|
829
|
+
importsToGenerate = parsed.imports.filter(imp => neededSet.has(imp));
|
|
830
|
+
}
|
|
831
|
+
} else {
|
|
832
|
+
importsToGenerate = [];
|
|
833
|
+
}
|
|
834
|
+
for (const imp of importsToGenerate) {
|
|
835
|
+
const isComponent = imp.source.toLowerCase().endsWith(".html");
|
|
836
|
+
if (!isComponent) continue;
|
|
837
|
+
const importedCompName = path.basename(imp.source).toLowerCase();
|
|
838
|
+
if (cx.loadedComponents.has(importedCompName)) {
|
|
839
|
+
if (imp.specifiers.length === 0) {
|
|
840
|
+
generateCSRClass(importedCompName, cx);
|
|
841
|
+
} else {
|
|
842
|
+
for (const spec of imp.specifiers) {
|
|
843
|
+
generateCSRClass(importedCompName, cx, spec.local);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
} else {
|
|
849
|
+
const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?\s*/g;
|
|
850
|
+
let match;
|
|
851
|
+
while ((match = importRegex.exec(scriptContent)) !== null) {
|
|
852
|
+
const importedName = match[1];
|
|
853
|
+
const importedCompName = path.basename(match[2]).toLowerCase();
|
|
854
|
+
if (cx.loadedComponents.has(importedCompName)) {
|
|
855
|
+
generateCSRClass(importedCompName, cx, importedName);
|
|
856
|
+
}
|
|
616
857
|
}
|
|
617
858
|
}
|
|
618
859
|
}
|
package/compiler/config.js
CHANGED
|
@@ -1,22 +1,52 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import {
|
|
2
|
+
import { promises as fsp } from "fs";
|
|
3
|
+
import chalk from "./chalk.js";
|
|
4
|
+
import { getConfig, isMissingConfigFile, queueConfigWarning } from "../utils.js";
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
const config = await getConfig(rootDir);
|
|
6
|
-
const bundleConfig = config.bundle || {};
|
|
6
|
+
const warnedBundleFields = new Set();
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
export async function loadConfig(rootDir, { silent, customPath, overrides } = {}) {
|
|
9
|
+
const config = customPath
|
|
10
|
+
? await fsp.readFile(customPath, "utf-8").then(r => JSON.parse(r))
|
|
11
|
+
: await getConfig(rootDir, { silent });
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
let srcDir = "src", outDir = "dist", libDir = "lib", emptyOutDir = true, treeShakeRuntime = true;
|
|
14
|
+
|
|
15
|
+
if (!customPath || !isMissingConfigFile(config)) {
|
|
16
|
+
const hasBundle = (config.bundle !== undefined && config.bundle !== null) || (config.build !== undefined && config.build !== null);
|
|
17
|
+
const bundleConfig = config.bundle || config.build || {};
|
|
18
|
+
const compilerConfig = config.compiler || {};
|
|
19
|
+
srcDir = bundleConfig.srcDir || "src";
|
|
20
|
+
outDir = bundleConfig.outDir || "dist";
|
|
21
|
+
libDir = bundleConfig.libDir || "lib";
|
|
22
|
+
emptyOutDir = bundleConfig.emptyOutDir !== false;
|
|
23
|
+
treeShakeRuntime = compilerConfig.treeShakeRuntime !== false;
|
|
24
|
+
|
|
25
|
+
if (!silent && hasBundle) {
|
|
26
|
+
if (bundleConfig.srcDir == null && !warnedBundleFields.has(rootDir + ":srcDir")) {
|
|
27
|
+
warnedBundleFields.add(rootDir + ":srcDir");
|
|
28
|
+
queueConfigWarning(rootDir, chalk.bold.yellow("WARNING!"), `bundle.srcDir not defined in chocola.config.json file: using default "src" bundle.srcDir.`);
|
|
29
|
+
}
|
|
30
|
+
if (bundleConfig.outDir == null && !warnedBundleFields.has(rootDir + ":outDir")) {
|
|
31
|
+
warnedBundleFields.add(rootDir + ":outDir");
|
|
32
|
+
queueConfigWarning(rootDir, chalk.bold.yellow("WARNING!"), `bundle.outDir not defined in chocola.config.json file: using default "dist" bundle.outDir.`);
|
|
33
|
+
}
|
|
34
|
+
if (bundleConfig.libDir == null && !warnedBundleFields.has(rootDir + ":libDir")) {
|
|
35
|
+
warnedBundleFields.add(rootDir + ":libDir");
|
|
36
|
+
queueConfigWarning(rootDir, chalk.bold.yellow("WARNING!"), `bundle.libDir not defined in chocola.config.json file: using default "lib" bundle.libDir.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = { srcDir, outDir, libDir, emptyOutDir, treeShakeRuntime };
|
|
42
|
+
if (overrides) { Object.assign(result, overrides); }
|
|
43
|
+
return result;
|
|
14
44
|
}
|
|
15
45
|
|
|
16
46
|
export function resolvePaths(rootDir, config) {
|
|
17
47
|
return {
|
|
18
|
-
outDir: path.join(rootDir, config.outDir),
|
|
19
|
-
src: path.join(rootDir, config.srcDir),
|
|
20
|
-
components: path.join(rootDir, config.srcDir, config.libDir),
|
|
48
|
+
outDir: path.isAbsolute(config.outDir) ? config.outDir : path.join(rootDir, config.outDir),
|
|
49
|
+
src: path.isAbsolute(config.srcDir) ? config.srcDir : path.join(rootDir, config.srcDir),
|
|
50
|
+
components: path.isAbsolute(config.srcDir) ? path.join(config.srcDir, config.libDir) : path.join(rootDir, config.srcDir, config.libDir),
|
|
21
51
|
};
|
|
22
52
|
}
|