chocola 2.0.0-next.10 → 2.0.0-next.12

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 CHANGED
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Chocola is a new and sweeter way to build your web apps.
9
9
 
10
- No bundler config. No virtual DOM. No hydration ceremony. Just `.html` files with `<template>`, `<script>`, and `<style>` compiled to HTML with scoped CSS the browser already understands, and optional runtime when you need it.
10
+ Just `.html` files with `<template>`, `<script>`, and `<style>` compiled to HTML with scoped CSS the browser already understands, and optional runtime when you need it.
11
11
 
12
12
  Import components. Instantiate them. Mount, update, remove. Client-side or server-side. Same file, minimal overhead.
13
13
 
@@ -15,11 +15,10 @@ Import components. Instantiate them. Mount, update, remove. Client-side or serve
15
15
  <script>
16
16
  import CoolButton from './CoolButton.html';
17
17
 
18
- let self;
19
- let input;
20
-
21
18
  export let title = "Hello";
22
19
 
20
+ let input;
21
+
23
22
  function $runtime() {
24
23
  input.focus();
25
24
  }
@@ -38,6 +37,26 @@ Import components. Instantiate them. Mount, update, remove. Client-side or serve
38
37
  </style>
39
38
  ```
40
39
 
40
+ ## Supporting Chocola
41
+
42
+ As of now, Chocola is a project driven by one developer (me, [@sadgabi20](https://github.com/sadgabi20)), but made to be community driven in the future. Therefore, the only current way to support Chocola is via my [GitHub Sponsors](https://github.com/sponsors/sadgabi20).
43
+
44
+ If this project achieves a wider support, it will have its own official support channels.
45
+
41
46
  ## Documentation
42
47
 
43
- https://github.com/chocolajs/chocola/tree/main/documentation
48
+ - [Getting started](documentation/01-introduction/02-getting-started.md)
49
+ - [Project structure](documentation/01-introduction/03-project-structure.md)
50
+ - [Build API reference](documentation/07-reference/04-cli.md)
51
+
52
+ ## Roadmap
53
+
54
+ If you want to know what is being worked on right now, see the [Roadmap](ROADMAP.md).
55
+
56
+ ## Contributing
57
+
58
+ Please follow the [Contribution Guidelines](CONTRIBUTING.md) to know how to contribute to Chocola.
59
+
60
+ ## License
61
+
62
+ [MIT](LICENSE)
@@ -3,17 +3,14 @@ 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,
10
10
  } from "../parser/index.js";
11
- import chalk from "./chalk.js";
12
-
13
-
14
11
 
15
12
  class ProcessContext {
16
- constructor(loadedComponents, runtimeChunks, compIdColl, runtimeMap, cssScopes, cssScopesMap, scopedStyles, staticCtxRegistry, csrClasses) {
13
+ constructor(loadedComponents, runtimeChunks, compIdColl, runtimeMap, cssScopes, cssScopesMap, scopedStyles, staticCtxRegistry, csrClasses, treeShakeRuntime = true, originalNames = null) {
17
14
  this.loadedComponents = loadedComponents;
18
15
  this.runtimeChunks = runtimeChunks;
19
16
  this.compIdColl = compIdColl;
@@ -24,9 +21,19 @@ class ProcessContext {
24
21
  this.staticCtxRegistry = staticCtxRegistry;
25
22
  this.csrClasses = csrClasses;
26
23
  this.unusedWarned = new Set();
24
+ this.treeShakeRuntime = treeShakeRuntime !== false;
25
+ this.originalNames = originalNames || new Map();
27
26
  }
28
27
  }
29
28
 
29
+ function resolveDisplayName(compName, cx) {
30
+ if (!compName) return compName;
31
+ if (cx?.originalNames?.has(compName)) return cx.originalNames.get(compName);
32
+ const lower = compName.toLowerCase();
33
+ if (cx?.originalNames?.has(lower)) return cx.originalNames.get(lower);
34
+ return compName;
35
+ }
36
+
30
37
  function escapeForTemplateLiteral(str) {
31
38
  return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
32
39
  }
@@ -45,6 +52,7 @@ function parseFragment(html, doc) {
45
52
  function generateCSRClass(compName, cx, explicitClassName) {
46
53
  if (cx.csrClasses.has(compName)) return;
47
54
 
55
+ const displayName = resolveDisplayName(compName, cx);
48
56
  let instance = cx.loadedComponents.get(compName);
49
57
  if (!instance) return;
50
58
 
@@ -60,7 +68,7 @@ function generateCSRClass(compName, cx, explicitClassName) {
60
68
  const compProps = extractPropsDefaults(script);
61
69
  const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
62
70
  const topVars = extractTopLevelVariables(script || "");
63
- let runtime = extractRuntime(script || "", compName);
71
+ let runtime = extractRuntime(script || "", displayName);
64
72
 
65
73
  if (!cx.cssScopesMap.has(compName)) {
66
74
  cx.cssScopesMap.set(compName, deterministicHash(compName, 8));
@@ -94,12 +102,41 @@ function generateCSRClass(compName, cx, explicitClassName) {
94
102
  }
95
103
 
96
104
  let csrRuntimeSource = null;
105
+ // Compute reachable for client bundling (Step B) - Feature flag: treeShakeRuntime
106
+ const parsedForReach = parseScript(script || "");
107
+ let reach = null;
108
+ if (cx.treeShakeRuntime !== false && parsedForReach.ast && runtime) {
109
+ reach = computeReachable(parsedForReach, { bindings: [...bindVarNames] });
110
+ if (reach.fallback) {
111
+ // conservative: include all and warn
112
+ }
113
+ }
97
114
  const injectedNames = new Set(compProps.map(p => p.name));
98
115
  for (const varName of bindVarNames) injectedNames.add(varName);
99
- const topVarsToInject = topVars.filter(v => !injectedNames.has(v.name));
116
+ let topVarsToInject = topVars.filter(v => !injectedNames.has(v.name));
117
+ let propsToInject = compProps;
118
+ let funcsToInject = topFuncSrc;
119
+ let bindingsToInject = [...bindVarNames];
120
+ if (cx.treeShakeRuntime !== false && reach && !reach.fallback) {
121
+ const neededVarNames = new Set(reach.neededVars.flatMap(v => v.names));
122
+ const neededPropNames = new Set(reach.neededProps.map(p => p.name));
123
+ const neededFuncNames = new Set(reach.neededFuncs.map(src => {
124
+ const m = src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/);
125
+ return m ? m[1] : null;
126
+ }).filter(Boolean));
127
+ const neededBindingSet = new Set(reach.neededBindings);
128
+ // Filter to reachable only
129
+ propsToInject = compProps.filter(p => neededPropNames.has(p.name));
130
+ topVarsToInject = topVars.filter(v => neededVarNames.has(v.name) && !injectedNames.has(v.name));
131
+ funcsToInject = topFuncSrc.filter(src => {
132
+ const m = src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/);
133
+ return m && neededFuncNames.has(m[1]);
134
+ });
135
+ bindingsToInject = [...bindVarNames].filter(b => neededBindingSet.has(b));
136
+ }
100
137
  if (runtime) {
101
138
  let injectCode = "";
102
- for (const { name, defaultValue } of compProps) {
139
+ for (const { name, defaultValue } of propsToInject) {
103
140
  if (defaultValue !== undefined) {
104
141
  injectCode += `let ${name} = ctx.${name}??(${defaultValue});\n`;
105
142
  } else {
@@ -108,18 +145,18 @@ function generateCSRClass(compName, cx, explicitClassName) {
108
145
  }
109
146
  for (const { keyword, name, value } of topVarsToInject) {
110
147
  if (value !== undefined) {
111
- injectCode += `${keyword} ${name} = ctx.${name}??(${value});\n`;
148
+ injectCode += `${keyword} ${name} = ${value};\n`;
112
149
  } else {
113
150
  injectCode += `${keyword} ${name};\n`;
114
151
  }
115
152
  }
116
- for (const varName of bindVarNames) {
153
+ for (const varName of bindingsToInject) {
117
154
  if (varName !== "self") {
118
155
  injectCode += `let ${varName} = ctx.${varName};\n`;
119
156
  }
120
157
  }
121
- if (topFuncSrc.length > 0) {
122
- injectCode += "\n" + topFuncSrc.join("\n\n") + "\n";
158
+ if (funcsToInject.length > 0) {
159
+ injectCode += "\n" + funcsToInject.join("\n\n") + "\n";
123
160
  }
124
161
  runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
125
162
  runtime = runtime.replace(`${RUNTIME_KW}()`, `function(self, ctx)`);
@@ -128,13 +165,13 @@ function generateCSRClass(compName, cx, explicitClassName) {
128
165
 
129
166
  const className = explicitClassName || compName.replace(".html", "").replace(/^\w/, c => c.toUpperCase());
130
167
  const propsParts = [];
131
- for (const { name, defaultValue } of compProps) {
132
- propsParts.push(`${JSON.stringify(name)}: ${defaultValue !== undefined ? defaultValue : "null"}`);
168
+ // For CSR class props: when runtime exists, include only reachable; otherwise keep all (fallback for CSR-only)
169
+ let propsForClass = compProps;
170
+ if (cx.treeShakeRuntime !== false && runtime && reach && !reach.fallback) {
171
+ propsForClass = propsToInject;
133
172
  }
134
- for (const { name, value } of topVarsToInject) {
135
- if (value !== undefined) {
136
- propsParts.push(`${JSON.stringify(name)}: ${value}`);
137
- }
173
+ for (const { name, defaultValue } of propsForClass) {
174
+ propsParts.push(`${JSON.stringify(name)}: ${defaultValue !== undefined ? defaultValue : "null"}`);
138
175
  }
139
176
 
140
177
  const childrenPart = childMappings.length > 0
@@ -168,6 +205,7 @@ function findLineInSource(source, regex) {
168
205
  }
169
206
 
170
207
  function warnUnusedDeclarations(cx, compName, instance, script, fragment) {
208
+ const displayName = resolveDisplayName(compName, cx);
171
209
  if (!script && !fragment.querySelector("[bind\\:]")) return;
172
210
  if (cx.unusedWarned.has(compName)) return;
173
211
  cx.unusedWarned.add(compName);
@@ -209,14 +247,14 @@ function warnUnusedDeclarations(cx, compName, instance, script, fragment) {
209
247
  const isUnused = (name) => (counts.get(name) || 0) <= 1;
210
248
  const warn = (kind, name, declRegex) => {
211
249
  const lineNum = findLineInSource(instance, declRegex);
212
- warnUnusedDeclaration(lineNum !== null ? `${compName}:${lineNum}` : compName, kind, name);
250
+ warnUnusedDeclaration(lineNum !== null ? `${displayName}:${lineNum}` : displayName, kind, name);
213
251
  };
214
252
 
215
253
  for (const name of props) {
216
254
  if (isUnused(name)) warn("prop", name, new RegExp("export\\s+let\\s+" + escapeNameForRegex(name) + "\\b"));
217
255
  }
218
256
  for (const name of topVars) {
219
- if (isUnused(name)) warn("variable", name, new RegExp("(?:^|[^\\w])(?:let|const)\\s+" + escapeNameForRegex(name) + "\\b"));
257
+ if (isUnused(name)) warn("variable", name, new RegExp("(?:^|[^\\w])(?:let|const|var)\\s+" + escapeNameForRegex(name) + "\\b"));
220
258
  }
221
259
  for (const name of topFuncs) {
222
260
  if (isUnused(name)) warn("function", name, new RegExp("(?:async\\s+)?function\\s+" + escapeNameForRegex(name) + "\\b"));
@@ -227,7 +265,7 @@ function warnUnusedDeclarations(cx, compName, instance, script, fragment) {
227
265
  seenBindings.add(name);
228
266
  if (isUnused(name)) {
229
267
  const lineNum = findElementLine(instance, element.outerHTML);
230
- warnUnusedDeclaration(lineNum !== null ? `${compName}:${lineNum}` : compName, "binding", name);
268
+ warnUnusedDeclaration(lineNum !== null ? `${displayName}:${lineNum}` : displayName, "binding", name);
231
269
  }
232
270
  }
233
271
  }
@@ -242,6 +280,7 @@ export function processComponentElement(
242
280
  ) {
243
281
  const tagName = element.tagName.toLowerCase();
244
282
  const compName = tagName + ".html";
283
+ const displayName = resolveDisplayName(compName, cx);
245
284
  let instance = cx.loadedComponents.get(compName);
246
285
 
247
286
  if (!instance || instance === undefined) return false;
@@ -254,20 +293,59 @@ export function processComponentElement(
254
293
  let template = doc.querySelector("template")?.innerHTML;
255
294
  let styles = doc.querySelector("style")?.innerHTML;
256
295
 
257
- if (!template) {
258
- console.warn(chalk.yellow(`${compName} — component is missing a <template>`));
259
- return false;
296
+ if (!template) {
297
+ return false;
260
298
  }
261
299
 
300
+ // Store original script for reachability (before stripping)
301
+ const originalScriptForReach = script;
262
302
  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);
303
+ const parsed = parseScript(script);
304
+ if (parsed.ast && parsed.imports.length > 0) {
305
+ // Determine needed imports via reachability (Step B) - Feature flag
306
+ let importsToGenerate = parsed.imports;
307
+ if (cx.treeShakeRuntime === false) {
308
+ // keep all imports when tree-shaking disabled
309
+ } else if (parsed.runtimeNode) {
310
+ const reachForImports = computeReachable(parsed, { bindings: [] });
311
+ if (!reachForImports.fallback) {
312
+ const neededSet = new Set(reachForImports.neededImports);
313
+ importsToGenerate = parsed.imports.filter(imp => neededSet.has(imp));
314
+ }
315
+ } else {
316
+ // No runtime -> no client imports needed (server-only)
317
+ importsToGenerate = [];
268
318
  }
269
- return "";
270
- });
319
+ for (const imp of importsToGenerate) {
320
+ const isComponent = imp.source.toLowerCase().endsWith(".html");
321
+ if (!isComponent) {
322
+ continue;
323
+ }
324
+ const importedCompName = path.basename(imp.source).toLowerCase();
325
+ if (cx.loadedComponents.has(importedCompName)) {
326
+ if (imp.specifiers.length === 0) {
327
+ generateCSRClass(importedCompName, cx);
328
+ } else {
329
+ for (const spec of imp.specifiers) {
330
+ generateCSRClass(importedCompName, cx, spec.local);
331
+ }
332
+ }
333
+ }
334
+ }
335
+ const sorted = [...parsed.imports].sort((a, b) => b.start - a.start);
336
+ for (const imp of sorted) {
337
+ script = script.slice(0, imp.start) + script.slice(imp.end);
338
+ }
339
+ } else if (parsed.parseError) {
340
+ const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?\s*/g;
341
+ script = script.replace(importRegex, (_, importedName, importPath) => {
342
+ const importedCompName = path.basename(importPath).toLowerCase();
343
+ if (cx.loadedComponents.has(importedCompName)) {
344
+ generateCSRClass(importedCompName, cx, importedName);
345
+ }
346
+ return "";
347
+ });
348
+ }
271
349
  }
272
350
 
273
351
  const compProps = extractPropsDefaults(script);
@@ -299,20 +377,65 @@ export function processComponentElement(
299
377
 
300
378
  const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
301
379
  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 {}
380
+ // Server resolution: evaluate all top-level declarations into ctx for template rendering
381
+ // Use AST-derived parsed data when available to correctly handle destructuring and comma declarators
382
+ // Use ctx-aware evaluation so initializers can reference earlier ctx vars (e.g., let a = x + y)
383
+ const ctxProxyForServer = new Proxy(ctx, { has() { return true; }, get(t,k){ return t[k]; } });
384
+ if (originalScriptForReach) {
385
+ const parsedForServer = parseScript(originalScriptForReach);
386
+ if (parsedForServer.ast) {
387
+ for (const v of parsedForServer.topVars) {
388
+ if (v.isDestructuring) {
389
+ if (v.names.every(n => n in ctx)) continue;
390
+ if (v.value === undefined) {
391
+ for (const n of v.names) if (!(n in ctx)) ctx[n] = undefined;
392
+ continue;
393
+ }
394
+ try {
395
+ const initVal = compileExpr(v.value, true)(ctxProxyForServer);
396
+ const fnBody = `${v.keyword} ${v.name} = initVal; return {${v.names.join(", ")}};`;
397
+ const result = new Function("initVal", fnBody)(initVal);
398
+ for (const n of v.names) if (!(n in ctx)) ctx[n] = result[n];
399
+ } catch {}
400
+ } else {
401
+ const { name, value } = v;
402
+ if (name in ctx) continue;
403
+ if (value !== undefined) {
404
+ try { ctx[name] = compileExpr(value, true)(ctxProxyForServer); } catch {}
405
+ } else {
406
+ if (!(name in ctx)) ctx[name] = undefined;
407
+ }
408
+ }
409
+ }
410
+ for (const src of parsedForServer.topFuncs) {
411
+ try {
412
+ const fn = (0, eval)("(" + src + ")");
413
+ const name = fn.name;
414
+ if (name && !(name in ctx)) ctx[name] = fn;
415
+ } catch {}
416
+ }
417
+ } else {
418
+ // Fallback to shim evaluation (ctx-aware for inter-var deps)
419
+ for (const { name, value } of topVars) {
420
+ if (name in ctx) continue;
421
+ if (value !== undefined) {
422
+ try { ctx[name] = compileExpr(value, true)(ctxProxyForServer); } catch {}
423
+ }
424
+ }
425
+ for (const src of topFuncSrc) {
426
+ try { const fn = (0, eval)("(" + src + ")"); const name = fn.name; if (name && !(name in ctx)) ctx[name] = fn; } catch {}
427
+ }
428
+ }
429
+ } else {
430
+ for (const { name, value } of topVars) {
431
+ if (name in ctx) continue;
432
+ if (value !== undefined) {
433
+ try { ctx[name] = compileExpr(value, true)(ctxProxyForServer); } catch {}
434
+ }
435
+ }
436
+ for (const src of topFuncSrc) {
437
+ try { const fn = (0, eval)("(" + src + ")"); const name = fn.name; if (name && !(name in ctx)) ctx[name] = fn; } catch {}
308
438
  }
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
439
  }
317
440
 
318
441
  const elInnerHtml = element.innerHTML;
@@ -330,7 +453,7 @@ export function processComponentElement(
330
453
  if (sourceFile) {
331
454
  validateChainStructure(slotFragment, sourceFile, sourceContent, elInnerHtml);
332
455
  }
333
- validateChainStructure(fragment, instance.__sourceFile || compName, instance, template);
456
+ validateChainStructure(fragment, instance.__sourceFile || displayName, instance, template);
334
457
  Array.from(fragment.querySelectorAll("slot")).forEach(slot => {
335
458
  slot.replaceWith(slotFragment);
336
459
  });
@@ -350,7 +473,7 @@ export function processComponentElement(
350
473
  let location = sourceFile;
351
474
  if (instance) {
352
475
  const lineNum = findElementLine(instance, el.outerHTML);
353
- if (lineNum !== null) location = `${compName}:${lineNum}`;
476
+ if (lineNum !== null) location = `${displayName}:${lineNum}`;
354
477
  }
355
478
  if (location === sourceFile && sourceContent) {
356
479
  const lineNum = findElementLine(sourceContent, el.outerHTML);
@@ -385,7 +508,7 @@ export function processComponentElement(
385
508
 
386
509
  if (hasElif || hasElse) {
387
510
  if (!condChain.active) {
388
- throwError(`${instance.__sourceFile || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/mount:if sibling`);
511
+ throwError(`${instance.__sourceFile || displayName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/mount:if sibling`);
389
512
  }
390
513
  if (condChain.rendered) {
391
514
  child.remove();
@@ -474,7 +597,7 @@ export function processComponentElement(
474
597
  child,
475
598
  cx,
476
599
  renderChain.concat(compName),
477
- compName,
600
+ displayName,
478
601
  template,
479
602
  globalCtx
480
603
  );
@@ -531,32 +654,112 @@ export function processComponentElement(
531
654
 
532
655
  script = script.replace(ctxRegex, "ctx");
533
656
 
534
- let runtime = extractRuntime(script, compName);
657
+ let runtime = extractRuntime(script, displayName);
535
658
 
536
659
  if (runtime) {
537
660
  let fnEntry = cx.runtimeMap && cx.runtimeMap.get(compName);
538
661
  let fnId;
539
662
  if (!fnEntry) {
540
663
  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});`
664
+ // Step B: filter to client-reachable declarations - Feature flag
665
+ let reachInject = null;
666
+ if (cx.treeShakeRuntime !== false && originalScriptForReach) {
667
+ const parsedReach = parseScript(originalScriptForReach);
668
+ if (parsedReach.ast) {
669
+ const bindNames = bindings.map(b => b.varName);
670
+ reachInject = computeReachable(parsedReach, { bindings: bindNames });
671
+ if (reachInject.fallback) {
672
+ reachInject = null;
673
+ }
674
+ }
675
+ }
676
+ // Prepare filtered sets
677
+ let compPropsToInject = compProps;
678
+ let topVarsToInject = topVars;
679
+ let bindingsToInject = bindings;
680
+ let topFuncsToInject = topFuncSrc;
681
+ if (reachInject) {
682
+ const neededPropNames = new Set(reachInject.neededProps.map(p => p.name));
683
+ const neededVarNames = new Set(reachInject.neededVars.flatMap(v => v.names));
684
+ const neededBindingSet = new Set(reachInject.neededBindings);
685
+ const neededFuncNames = new Set(reachInject.neededFuncs.map(src => {
686
+ const m = src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/);
687
+ return m ? m[1] : null;
688
+ }).filter(Boolean));
689
+ // Rebuild ctxDef filtered to needed props (inject only reachable props)
690
+ // Note: declared handling below will be updated to use filtered props
691
+ compPropsToInject = compProps.filter(p => neededPropNames.has(p.name));
692
+ topVarsToInject = topVars.filter(v => neededVarNames.has(v.name));
693
+ bindingsToInject = bindings.filter(b => neededBindingSet.has(b.varName));
694
+ topFuncsToInject = topFuncSrc.filter(src => {
695
+ const m = src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/);
696
+ return m && neededFuncNames.has(m[1]);
697
+ });
698
+ // Rebuild ctxDef to only include reachable props
699
+ // declared already has runtimeCtx keys; now add only needed props
700
+ // Recreate ctxDef parts filtered
701
+ const ctxDefPartsFiltered = [];
702
+ const declaredForCtx = new Set(Object.keys(runtimeCtx));
703
+ for (const { name, defaultValue } of compPropsToInject) {
704
+ if (declaredForCtx.has(name)) continue;
705
+ if (defaultValue !== undefined) {
706
+ ctxDefPartsFiltered.push(`let ${name} = ctx.${name}??(${defaultValue});\n`);
707
+ } else {
708
+ ctxDefPartsFiltered.push(`let ${name} = ctx.${name};\n`);
709
+ }
710
+ declaredForCtx.add(name);
711
+ }
712
+ // Prepend runtimeCtx parts (already in ctxDefParts) but we need to reconstruct
713
+ // Simpler: rebuild ctxDef from scratch filtered
714
+ const runtimeCtxParts = [];
715
+ for (const [k,v] of Object.entries(runtimeCtx)) {
716
+ runtimeCtxParts.push(`let ${k} = ctx.${k}??${JSON.stringify(v)};\n`);
717
+ }
718
+ const filteredCtxDef = runtimeCtxParts.join("") + ctxDefPartsFiltered.join("");
719
+ // Use filtered ctxDef for injection
720
+ // declared set for topVars should include runtimeCtx + needed props + bindingsToInject
721
+ // We'll set injectCode later using filteredCtxDef
722
+ // Override ctxDef and declared handling
723
+ // Store for later use
724
+ reachInject._filteredCtxDef = filteredCtxDef;
725
+ reachInject._compPropsToInject = compPropsToInject;
726
+ reachInject._topVarsToInject = topVarsToInject;
727
+ reachInject._bindingsToInject = bindingsToInject;
728
+ reachInject._topFuncsToInject = topFuncsToInject;
729
+ }
730
+
731
+ let injectCode;
732
+ let effectiveTopVars;
733
+ let effectiveBindings;
734
+ let effectiveFuncs;
735
+ if (reachInject && reachInject._filteredCtxDef !== undefined) {
736
+ injectCode = reachInject._filteredCtxDef;
737
+ // declared for topVars filtering should include runtimeCtx + needed props + needed bindings
738
+ const filteredDeclared = new Set([...Object.keys(runtimeCtx), ...reachInject._compPropsToInject.map(p=>p.name), ...reachInject._bindingsToInject.map(b=>b.varName)]);
739
+ effectiveTopVars = reachInject._topVarsToInject.filter(v => !filteredDeclared.has(v.name));
740
+ effectiveBindings = reachInject._bindingsToInject;
741
+ effectiveFuncs = reachInject._topFuncsToInject;
742
+ } else {
743
+ injectCode = ctxDef;
744
+ for (const b of bindings) declared.add(b.varName);
745
+ effectiveTopVars = topVars.filter(v => !declared.has(v.name));
746
+ effectiveBindings = bindings;
747
+ effectiveFuncs = topFuncSrc;
748
+ }
749
+ if (effectiveTopVars.length > 0) {
750
+ injectCode += "\n" + effectiveTopVars.map(v => v.value !== undefined
751
+ ? `${v.keyword} ${v.name} = ${v.value};`
549
752
  : `${v.keyword} ${v.name};`
550
753
  ).join("\n") + "\n";
551
754
  }
552
- if (bindings.length > 0) {
553
- injectCode += "\n" + bindings.map(b => {
755
+ if (effectiveBindings.length > 0) {
756
+ injectCode += "\n" + effectiveBindings.map(b => {
554
757
  const accessor = b.prop === "self" ? "" : "." + b.prop;
555
758
  return "let " + b.varName + " = self.querySelector('[data-chbind-" + b.bindId + "]')" + accessor + ";";
556
759
  }).join("\n") + "\n";
557
760
  }
558
- if (topFuncs.length > 0) {
559
- injectCode += "\n" + topFuncs.join("\n\n") + "\n";
761
+ if (effectiveFuncs.length > 0) {
762
+ injectCode += "\n" + effectiveFuncs.join("\n\n") + "\n";
560
763
  }
561
764
  runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
562
765
 
@@ -598,21 +801,52 @@ export function processComponentElement(
598
801
  return firstChild && firstChild.nodeType === 1 ? firstChild : true;
599
802
  }
600
803
 
601
- export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent, globalCtx = {}) {
804
+ export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent, globalCtx = {}, treeShakeRuntime = true, originalNames = null) {
602
805
  const cx = new ProcessContext(
603
- loadedComponents, [], [], new Map(), [], new Map(), [], new Map(), new Map()
806
+ loadedComponents, [], [], new Map(), [], new Map(), [], new Map(), new Map(), treeShakeRuntime, originalNames
604
807
  );
605
808
 
606
809
  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);
810
+ const scriptMatch = instance.match(/<script>([\s\S]*?)<\/script>/i);
811
+ if (scriptMatch) {
812
+ const scriptContent = scriptMatch[1];
813
+ const parsed = parseScript(scriptContent);
814
+ if (parsed.ast) {
815
+ let importsToGenerate = parsed.imports;
816
+ if (cx.treeShakeRuntime === false) {
817
+ // keep all when disabled
818
+ } else if (parsed.runtimeNode) {
819
+ const reach = computeReachable(parsed, { bindings: [] });
820
+ if (!reach.fallback) {
821
+ const neededSet = new Set(reach.neededImports);
822
+ importsToGenerate = parsed.imports.filter(imp => neededSet.has(imp));
823
+ }
824
+ } else {
825
+ importsToGenerate = [];
826
+ }
827
+ for (const imp of importsToGenerate) {
828
+ const isComponent = imp.source.toLowerCase().endsWith(".html");
829
+ if (!isComponent) continue;
830
+ const importedCompName = path.basename(imp.source).toLowerCase();
831
+ if (cx.loadedComponents.has(importedCompName)) {
832
+ if (imp.specifiers.length === 0) {
833
+ generateCSRClass(importedCompName, cx);
834
+ } else {
835
+ for (const spec of imp.specifiers) {
836
+ generateCSRClass(importedCompName, cx, spec.local);
837
+ }
838
+ }
839
+ }
840
+ }
841
+ } else {
842
+ const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?\s*/g;
843
+ let match;
844
+ while ((match = importRegex.exec(scriptContent)) !== null) {
845
+ const importedName = match[1];
846
+ const importedCompName = path.basename(match[2]).toLowerCase();
847
+ if (cx.loadedComponents.has(importedCompName)) {
848
+ generateCSRClass(importedCompName, cx, importedName);
849
+ }
616
850
  }
617
851
  }
618
852
  }