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.
@@ -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 || "", compName);
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
- const topVarsToInject = topVars.filter(v => !injectedNames.has(v.name));
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 compProps) {
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} = ctx.${name}??(${value});\n`;
152
+ injectCode += `${keyword} ${name} = ${value};\n`;
112
153
  } else {
113
154
  injectCode += `${keyword} ${name};\n`;
114
155
  }
115
156
  }
116
- for (const varName of bindVarNames) {
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 (topFuncSrc.length > 0) {
122
- injectCode += "\n" + topFuncSrc.join("\n\n") + "\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
- for (const { name, defaultValue } of compProps) {
132
- propsParts.push(`${JSON.stringify(name)}: ${defaultValue !== undefined ? defaultValue : "null"}`);
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, value } of topVarsToInject) {
135
- if (value !== undefined) {
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 ? `${compName}:${lineNum}` : compName, kind, name);
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 ? `${compName}:${lineNum}` : compName, "binding", name);
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(`${compName} — component is missing a <template>`));
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 importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?\s*/g;
264
- script = script.replace(importRegex, (_, importedName, importPath) => {
265
- const importedCompName = path.basename(importPath).toLowerCase();
266
- if (cx.loadedComponents.has(importedCompName)) {
267
- generateCSRClass(importedCompName, cx, importedName);
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
- return "";
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
- for (const { name, value } of topVars) {
303
- if (name in ctx) continue;
304
- if (value !== undefined) {
305
- try {
306
- ctx[name] = compileExpr(value, false)();
307
- } catch {}
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 || compName, instance, template);
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 = `${compName}:${lineNum}`;
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 || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/mount:if sibling`);
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
- compName,
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, compName);
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
- const topFuncs = topFuncSrc;
542
-
543
- let injectCode = ctxDef;
544
- for (const b of bindings) declared.add(b.varName);
545
- const undeclaredTopVars = topVars.filter(v => !declared.has(v.name));
546
- if (undeclaredTopVars.length > 0) {
547
- injectCode += "\n" + undeclaredTopVars.map(v => v.value !== undefined
548
- ? `${v.keyword} ${v.name} = ctx.${v.name}??(${v.value});`
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 (bindings.length > 0) {
553
- injectCode += "\n" + bindings.map(b => {
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 (topFuncs.length > 0) {
559
- injectCode += "\n" + topFuncs.join("\n\n") + "\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 script = instance.match(/<script>([\s\S]*?)<\/script>/i);
608
- if (script) {
609
- const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?\s*/g;
610
- let match;
611
- while ((match = importRegex.exec(script[1])) !== null) {
612
- const importedName = match[1];
613
- const importedCompName = path.basename(match[2]).toLowerCase();
614
- if (cx.loadedComponents.has(importedCompName)) {
615
- generateCSRClass(importedCompName, cx, importedName);
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
  }
@@ -1,22 +1,52 @@
1
1
  import path from "path";
2
- import { getConfig } from "../utils.js";
2
+ import { promises as fsp } from "fs";
3
+ import chalk from "./chalk.js";
4
+ import { getConfig, isMissingConfigFile, queueConfigWarning } from "../utils.js";
3
5
 
4
- export async function loadConfig(rootDir) {
5
- const config = await getConfig(rootDir);
6
- const bundleConfig = config.bundle || {};
6
+ const warnedBundleFields = new Set();
7
7
 
8
- const srcDir = bundleConfig.srcDir || "src";
9
- const outDir = bundleConfig.outDir || "dist";
10
- const libDir = bundleConfig.libDir || "lib";
11
- const emptyOutDir = bundleConfig.emptyOutDir !== false;
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
- return { srcDir, outDir, libDir, emptyOutDir };
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
  }