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.
@@ -1,510 +1,879 @@
1
- import { JSDOM } from "jsdom";
2
- import { extractContextFromElement } from "./dom-processor.js";
3
- import { genRandomId, incrementAlfabet, throwError, protectCurlyBraces } from "./utils.js";
4
- import chalk from "chalk";
5
- import beautify from "js-beautify";
6
-
7
- function hasCombinator(sel) {
8
- let depth = 0;
9
- for (const ch of sel) {
10
- if (ch === '[') depth++;
11
- else if (ch === ']') depth--;
12
- else if (depth === 0 && (ch === ' ' || ch === '>' || ch === '+' || ch === '~')) return true;
1
+ import path from "path";
2
+ import { parseHTML } from "linkedom";
3
+ import { protectCurlyBraces } from "../utils.js";
4
+ import { genRandomId, runtimeFunctionId, throwError, deterministicHash, warnConstantCondition, warnUnusedDeclaration, findElementLine } from "./utils.js";
5
+ import {
6
+ extractPropsDefaults, extractRuntime, extractTopLevelFunctions, extractTopLevelVariables, parseScript, computeReachable,
7
+ extractCtxFromEl, hasMountIf, getMountIf,
8
+ reservedAttrs, validateChainStructure, applyConditionalToElement, interpolateNode,
9
+ scopeCss, compileExpr, evaluateConstant,
10
+ } from "../parser/index.js";
11
+ import chalk from "./chalk.js";
12
+
13
+
14
+
15
+ class ProcessContext {
16
+ constructor(loadedComponents, runtimeChunks, compIdColl, runtimeMap, cssScopes, cssScopesMap, scopedStyles, staticCtxRegistry, csrClasses, treeShakeRuntime = true, originalNames = null) {
17
+ this.loadedComponents = loadedComponents;
18
+ this.runtimeChunks = runtimeChunks;
19
+ this.compIdColl = compIdColl;
20
+ this.runtimeMap = runtimeMap;
21
+ this.cssScopes = cssScopes;
22
+ this.cssScopesMap = cssScopesMap;
23
+ this.scopedStyles = scopedStyles;
24
+ this.staticCtxRegistry = staticCtxRegistry;
25
+ this.csrClasses = csrClasses;
26
+ this.unusedWarned = new Set();
27
+ this.treeShakeRuntime = treeShakeRuntime !== false;
28
+ this.originalNames = originalNames || new Map();
13
29
  }
14
- return false;
15
30
  }
16
31
 
17
- function scopeSelector(sel, cssId) {
18
- const s = sel.trim();
19
- if (s.startsWith("." + cssId)) return s;
20
- if (hasCombinator(s)) return `.${cssId} ${s}`;
21
- return `.${cssId}${s}, .${cssId} ${s}`;
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;
22
38
  }
23
39
 
24
- function scopeCss(cssString, cssId) {
25
- const keyframeRegex = /@(-webkit-)?keyframes\s+(\S+)/gi;
26
- const animNames = new Set();
27
- let match;
28
- while ((match = keyframeRegex.exec(cssString))) {
29
- animNames.add(match[2]);
40
+ function escapeForTemplateLiteral(str) {
41
+ return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
42
+ }
43
+
44
+ function parseFragment(html, doc) {
45
+ const fragment = doc.createDocumentFragment();
46
+ const temp = doc.createElement("div");
47
+ temp.innerHTML = html;
48
+ const children = [...temp.childNodes];
49
+ for (const child of children) {
50
+ fragment.appendChild(child);
30
51
  }
52
+ return fragment;
53
+ }
31
54
 
32
- if (animNames.size > 0) {
33
- const sorted = [...animNames].sort((a, b) => b.length - a.length);
34
- const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35
- for (const name of sorted) {
36
- cssString = cssString.replace(
37
- new RegExp(`(@(?:-webkit-)?keyframes\\s+)${esc(name)}`, "gi"),
38
- `$1${name}-${cssId}`
39
- );
40
- cssString = cssString.replace(
41
- new RegExp(`((?:animation|animation-name)\\s*:\\s*[^;{]*?)\\b${esc(name)}\\b`, "gi"),
42
- `$1${name}-${cssId}`
43
- );
44
- }
55
+ function generateCSRClass(compName, cx, explicitClassName) {
56
+ if (cx.csrClasses.has(compName)) return;
57
+
58
+ const displayName = resolveDisplayName(compName, cx);
59
+ let instance = cx.loadedComponents.get(compName);
60
+ if (!instance) return;
61
+
62
+ instance = protectCurlyBraces(instance);
63
+ const dom = parseHTML(instance);
64
+ const doc = dom.document;
65
+ const script = doc.querySelector("script")?.innerHTML;
66
+ const template = doc.querySelector("template")?.innerHTML;
67
+ const styles = doc.querySelector("style")?.innerHTML;
68
+
69
+ if (!template) return;
70
+
71
+ const compProps = extractPropsDefaults(script);
72
+ const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
73
+ const topVars = extractTopLevelVariables(script || "");
74
+ let runtime = extractRuntime(script || "", displayName);
75
+
76
+ if (!cx.cssScopesMap.has(compName)) {
77
+ cx.cssScopesMap.set(compName, deterministicHash(compName, 8));
45
78
  }
79
+ const cssId = cx.cssScopesMap.get(compName);
46
80
 
47
- function findMatchingBrace(s, openIndex) {
48
- let depth = 0;
49
- for (let i = openIndex; i < s.length; i++) {
50
- const ch = s[i];
51
- if (ch === '{') depth++;
52
- else if (ch === '}') {
53
- depth--;
54
- if (depth === 0) return i;
81
+ if (styles) {
82
+ cx.scopedStyles.push(scopeCss(styles, cssId));
83
+ }
84
+
85
+ const childMappings = [];
86
+ const bindVarNames = new Set();
87
+ const templateDoc = parseFragment(template, doc);
88
+ const seenTags = new Set();
89
+ for (const el of templateDoc.querySelectorAll("*")) {
90
+ const tag = el.tagName.toLowerCase();
91
+ if (!seenTags.has(tag)) {
92
+ seenTags.add(tag);
93
+ const childCompName = tag + ".html";
94
+ if (cx.loadedComponents.has(childCompName)) {
95
+ generateCSRClass(childCompName, cx);
96
+ const childClassName = childCompName.replace(".html", "").replace(/^\w/, c => c.toUpperCase());
97
+ childMappings.push({ tag, compClass: childClassName });
98
+ }
99
+ }
100
+ for (const attr of el.attributes) {
101
+ if (attr.name.startsWith("bind:")) {
102
+ bindVarNames.add(attr.value);
55
103
  }
56
104
  }
57
- return s.length - 1;
58
105
  }
59
106
 
60
- const scopeInnerAtRules = new Set(["@media", "@supports", "@container", "@layer"]);
61
-
62
- function processBlock(str) {
63
- str = str.replaceAll(":root", `.${cssId}`);
64
-
65
- let out = "";
66
- let i = 0;
67
- while (i < str.length) {
68
- const braceIndex = str.indexOf('{', i);
69
- if (braceIndex === -1) {
70
- out += str.slice(i);
71
- break;
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
+ }
118
+ const injectedNames = new Set(compProps.map(p => p.name));
119
+ for (const varName of bindVarNames) injectedNames.add(varName);
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
+ }
141
+ if (runtime) {
142
+ let injectCode = "";
143
+ for (const { name, defaultValue } of propsToInject) {
144
+ if (defaultValue !== undefined) {
145
+ injectCode += `let ${name} = ctx.${name}??(${defaultValue});\n`;
146
+ } else {
147
+ injectCode += `let ${name} = ctx.${name};\n`;
72
148
  }
73
- const header = str.substring(i, braceIndex);
74
- const endBrace = findMatchingBrace(str, braceIndex);
75
- const inner = str.substring(braceIndex + 1, endBrace);
76
-
77
- if (header.trim().startsWith("@")) {
78
- const atKeyword = header.trim().split(/\s+/)[0].toLowerCase();
79
- if (scopeInnerAtRules.has(atKeyword)) {
80
- out += header + "{" + processBlock(inner) + "}";
81
- } else {
82
- out += header + "{" + inner + "}";
83
- }
149
+ }
150
+ for (const { keyword, name, value } of topVarsToInject) {
151
+ if (value !== undefined) {
152
+ injectCode += `${keyword} ${name} = ${value};\n`;
84
153
  } else {
85
- const innerScoped = processBlock(inner);
86
- const selectors = header
87
- .split(",")
88
- .map(s => s.trim())
89
- .filter(Boolean);
90
- const scopedHeader = selectors.length > 0
91
- ? selectors.map(sel => scopeSelector(sel, cssId)).join(", ")
92
- : header;
93
- out += scopedHeader + "{" + innerScoped + "}";
154
+ injectCode += `${keyword} ${name};\n`;
155
+ }
156
+ }
157
+ for (const varName of bindingsToInject) {
158
+ if (varName !== "self") {
159
+ injectCode += `let ${varName} = ctx.${varName};\n`;
94
160
  }
95
-
96
- i = endBrace + 1;
97
161
  }
98
- return out;
162
+ if (funcsToInject.length > 0) {
163
+ injectCode += "\n" + funcsToInject.join("\n\n") + "\n";
164
+ }
165
+ runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
166
+ runtime = runtime.replace(`${RUNTIME_KW}()`, `function(self, ctx)`);
167
+ csrRuntimeSource = runtime.replace(/^(async\s+)?function\s+\w+/, "$1function");
168
+ }
169
+
170
+ const className = explicitClassName || compName.replace(".html", "").replace(/^\w/, c => c.toUpperCase());
171
+ const propsParts = [];
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;
176
+ }
177
+ for (const { name, defaultValue } of propsForClass) {
178
+ propsParts.push(`${JSON.stringify(name)}: ${defaultValue !== undefined ? defaultValue : "null"}`);
99
179
  }
100
180
 
101
- return processBlock(cssString);
181
+ const childrenPart = childMappings.length > 0
182
+ ? ",\n children: [" + childMappings.map(m => `{tag:"${m.tag}",compClass:${m.compClass}}`).join(",") + "]"
183
+ : "";
184
+ const runtimePart = csrRuntimeSource ? `,\n runtime: ${csrRuntimeSource}` : "";
185
+ const propsPart = propsParts.length > 0 ? `{ ${propsParts.join(", ")} }` : "{}";
186
+ const classDef = `class ${className} extends ChocolaComponent {\n constructor() {\n super({\n template: \`${escapeForTemplateLiteral(template)}\`,\n hash: "${cssId}",\n props: ${propsPart}${runtimePart}${childrenPart}\n });\n }\n}`;
187
+ cx.csrClasses.set(compName, classDef);
102
188
  }
103
189
 
104
- const warnedDeprecated = new Set();
105
- function hasDelIfAttr(el, sourceLoc) {
106
- if (el.hasAttribute("del-if") && !el.hasAttribute("del:if") && !warnedDeprecated.has(sourceLoc)) {
107
- warnedDeprecated.add(sourceLoc);
108
- const name = sourceLoc ? sourceLoc.split(/[\\/]/).pop() : "unknown";
109
- console.warn(chalk.yellow(`[deprecation] "del-if" is deprecated in ${chalk.cyan(name)} and will be removed in Chocola 2. Use "del:if" instead.`));
190
+ const BARE_IDENTIFIER_RE = /(?<![.\w$])[a-zA-Z_$][0-9a-zA-Z_$]*/g;
191
+ const PROPERTY_IDENTIFIER_RE = /\.([a-zA-Z_$][0-9a-zA-Z_$]*)/g;
192
+
193
+ function countIdentifiers(text, counts) {
194
+ for (const re of [BARE_IDENTIFIER_RE, PROPERTY_IDENTIFIER_RE]) {
195
+ re.lastIndex = 0;
196
+ for (const m of text.matchAll(re)) counts.set(m[1] ?? m[0], (counts.get(m[1] ?? m[0]) || 0) + 1);
110
197
  }
111
- return el.hasAttribute("del-if") || el.hasAttribute("del:if");
112
- }
113
- function getDelIfAttr(el) {
114
- return el.getAttribute("del:if") ?? el.getAttribute("del-if");
198
+ return counts;
115
199
  }
116
- function removeDelIfAttr(el) {
117
- el.removeAttribute("del-if");
118
- el.removeAttribute("del:if");
200
+
201
+ function escapeNameForRegex(name) {
202
+ return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
119
203
  }
120
204
 
121
- function interpolateNode(node, ctxProxy) {
122
- if (node.nodeType === 3) {
123
- node.textContent = node.textContent.replace(/\{([^}]+)\}/g, (_, expr) => {
124
- try {
125
- return Function("ctx", `with(ctx) { return (${expr}); }`)(ctxProxy);
126
- } catch {
127
- return "";
128
- }
129
- });
130
- } else {
131
- node.childNodes.forEach(child => interpolateNode(child, ctxProxy));
132
- }
205
+ function findLineInSource(source, regex) {
206
+ const match = regex.exec(source);
207
+ if (!match) return null;
208
+ return source.substring(0, match.index).split("\n").length;
133
209
  }
134
210
 
135
- function validateChainStructure(parent, sourceFile, sourceContent, parentContent) {
136
- const children = [...parent.children];
137
- let chainActive = false;
138
- const outerCounts = {};
211
+ function warnUnusedDeclarations(cx, compName, instance, script, fragment) {
212
+ const displayName = resolveDisplayName(compName, cx);
213
+ if (!script && !fragment.querySelector("[bind\\:]")) return;
214
+ if (cx.unusedWarned.has(compName)) return;
215
+ cx.unusedWarned.add(compName);
216
+
217
+ const props = extractPropsDefaults(script).map(p => p.name);
218
+ const topVars = extractTopLevelVariables(script).map(v => v.name);
219
+ const topFuncs = extractTopLevelFunctions(script, RUNTIME_KW)
220
+ .map(src => src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/)?.[1])
221
+ .filter(Boolean);
222
+
223
+ const bindings = [];
224
+ for (const el of fragment.querySelectorAll("*")) {
225
+ for (const attr of el.attributes) {
226
+ if (attr.name.startsWith("bind:")) bindings.push({ name: attr.value, element: el });
227
+ }
228
+ }
139
229
 
140
- for (const child of children) {
141
- const hasIf = child.hasAttribute("if");
142
- const hasDelIf = hasDelIfAttr(child, sourceFile);
143
- const hasElif = child.hasAttribute("elif");
144
- const hasElse = child.hasAttribute("else");
230
+ const counts = new Map();
231
+ if (script) countIdentifiers(script, counts);
145
232
 
146
- if (hasElif || hasElse) {
147
- if (!chainActive) {
148
- const tag = child.tagName.toLowerCase();
149
- const attr = hasElif ? "elif" : "else";
150
- let loc = sourceFile;
151
- if (sourceContent && parentContent) {
152
- const outer = child.outerHTML;
153
- outerCounts[outer] = (outerCounts[outer] || 0) + 1;
154
- let searchIdx = 0;
155
- let hits = 0;
156
- while ((searchIdx = parentContent.indexOf(outer, searchIdx)) !== -1) {
157
- hits++;
158
- if (hits === outerCounts[outer]) {
159
- let line = parentContent.substring(0, searchIdx).split("\n").length;
160
- if (sourceContent !== parentContent) {
161
- const anchor = parentContent.trim().slice(0, 80);
162
- const vars = [
163
- anchor,
164
- anchor.replace(/=""/g, ' '),
165
- anchor.replace(/=""/g, ''),
166
- anchor.replace(/="/g, "='").replace(/"/g, "'"),
167
- ];
168
- let anchorIdx = -1;
169
- for (const v of vars) {
170
- anchorIdx = sourceContent.indexOf(v);
171
- if (anchorIdx !== -1) break;
172
- }
173
- if (anchorIdx !== -1) {
174
- const offset = sourceContent.substring(0, anchorIdx).split("\n").length;
175
- line = offset + line - 1;
176
- }
177
- }
178
- loc = `${sourceFile}:${line}`;
179
- break;
180
- }
181
- searchIdx += outer.length;
182
- }
233
+ const walk = (node) => {
234
+ if (node.nodeType === 3) {
235
+ for (const m of node.textContent.matchAll(/\{([^}]+)\}/g)) countIdentifiers(m[1], counts);
236
+ return;
237
+ }
238
+ if (node.attributes) {
239
+ for (const attr of node.attributes) {
240
+ if (attr.name.startsWith("bind:")) {
241
+ countIdentifiers(attr.value, counts);
242
+ } else {
243
+ for (const m of attr.value.matchAll(/\{([^}]+)\}/g)) countIdentifiers(m[1], counts);
183
244
  }
184
- throwError(`${loc}\n <${tag}> has ${attr} without a preceding if/del-if sibling`);
185
- }
186
- if (hasElse) {
187
- chainActive = false;
188
245
  }
189
246
  }
190
-
191
- if (hasIf || hasDelIf) {
192
- chainActive = true;
193
- } else if (!hasElif && !hasElse) {
194
- chainActive = false;
247
+ for (const child of node.childNodes) walk(child);
248
+ };
249
+ walk(fragment);
250
+
251
+ const isUnused = (name) => (counts.get(name) || 0) <= 1;
252
+ const warn = (kind, name, declRegex) => {
253
+ const lineNum = findLineInSource(instance, declRegex);
254
+ warnUnusedDeclaration(lineNum !== null ? `${displayName}:${lineNum}` : displayName, kind, name);
255
+ };
256
+
257
+ for (const name of props) {
258
+ if (isUnused(name)) warn("prop", name, new RegExp("export\\s+let\\s+" + escapeNameForRegex(name) + "\\b"));
259
+ }
260
+ for (const name of topVars) {
261
+ if (isUnused(name)) warn("variable", name, new RegExp("(?:^|[^\\w])(?:let|const|var)\\s+" + escapeNameForRegex(name) + "\\b"));
262
+ }
263
+ for (const name of topFuncs) {
264
+ if (isUnused(name)) warn("function", name, new RegExp("(?:async\\s+)?function\\s+" + escapeNameForRegex(name) + "\\b"));
265
+ }
266
+ const seenBindings = new Set();
267
+ for (const { name, element } of bindings) {
268
+ if (seenBindings.has(name)) continue;
269
+ seenBindings.add(name);
270
+ if (isUnused(name)) {
271
+ const lineNum = findElementLine(instance, element.outerHTML);
272
+ warnUnusedDeclaration(lineNum !== null ? `${displayName}:${lineNum}` : displayName, "binding", name);
195
273
  }
196
-
197
- validateChainStructure(child, sourceFile, sourceContent, parentContent);
198
274
  }
199
275
  }
200
276
 
201
- /**
202
- * Processes a single component element and inserts it into the DOM
203
- * @param {Element} element
204
- * @param {Map} loadedComponents
205
- * @param {Array} runtimeChunks
206
- * @param {Array} compIdColl
207
- * @param {object} letterState - { value: string }
208
- * @returns {boolean} - true if component was processed, false if not found
209
- */
210
277
  export function processComponentElement(
211
278
  element,
212
- loadedComponents,
213
- runtimeChunks,
214
- compIdColl,
215
- letterState,
216
- runtimeMap,
217
- cssScopes,
218
- cssScopesMap,
219
- scopedStyles,
279
+ cx,
220
280
  renderChain = [],
221
- staticCtxRegistry,
222
281
  sourceFile,
223
- sourceContent
282
+ sourceContent,
283
+ globalCtx = {}
224
284
  ) {
225
285
  const tagName = element.tagName.toLowerCase();
226
- const compName = tagName + ".js";
227
- const instance = loadedComponents.get(compName);
286
+ const compName = tagName + ".html";
287
+ const displayName = resolveDisplayName(compName, cx);
288
+ let instance = cx.loadedComponents.get(compName);
228
289
 
229
290
  if (!instance || instance === undefined) return false;
230
291
  if (renderChain && renderChain.includes(compName)) return false;
231
292
 
293
+ instance = protectCurlyBraces(instance);
294
+ const dom = parseHTML(instance);
295
+ const doc = dom.document;
296
+ let script = doc.querySelector("script")?.innerHTML;
297
+ let template = doc.querySelector("template")?.innerHTML;
298
+ let styles = doc.querySelector("style")?.innerHTML;
299
+
300
+ if (!template) {
301
+ console.warn(chalk.yellow(`${displayName} — component is missing a <template>`));
302
+ return false;
303
+ }
304
+
305
+ // Store original script for reachability (before stripping)
306
+ const originalScriptForReach = script;
307
+ if (script) {
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 = [];
323
+ }
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
+ }
355
+ }
356
+
357
+ const compProps = extractPropsDefaults(script);
358
+
232
359
  let ctx;
233
- if (staticCtxRegistry && staticCtxRegistry.has(element)) {
234
- ctx = staticCtxRegistry.get(element);
360
+ if (cx.staticCtxRegistry && cx.staticCtxRegistry.has(element)) {
361
+ ctx = cx.staticCtxRegistry.get(element);
235
362
  } else {
236
- ctx = extractContextFromElement(element);
237
- staticCtxRegistry && staticCtxRegistry.set(element, ctx);
363
+ ctx = extractCtxFromEl(element, globalCtx);
364
+ if (globalCtx && typeof globalCtx === "object") {
365
+ for (const [k, v] of Object.entries(globalCtx)) {
366
+ if (!(k in ctx)) ctx[k] = v;
367
+ }
368
+ }
369
+ cx.staticCtxRegistry && cx.staticCtxRegistry.set(element, ctx);
370
+ }
371
+
372
+ if (compProps.length > 0) {
373
+ compProps.forEach(({ name, defaultValue }) => {
374
+ if (defaultValue !== undefined && !(name in ctx)) {
375
+ try {
376
+ ctx[name] = compileExpr(defaultValue, false)();
377
+ } catch {
378
+ ctx[name] = defaultValue;
379
+ }
380
+ }
381
+ });
238
382
  }
239
- const srcInnerHtml = element.innerHTML;
383
+
384
+ const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
385
+ const topVars = extractTopLevelVariables(script || "");
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 {}
444
+ }
445
+ }
446
+
447
+ const elInnerHtml = element.innerHTML;
240
448
 
241
449
  const ctxProxy = new Proxy(ctx, {
242
450
  has() { return true; },
243
451
  get(target, key) { return target[key]; }
244
452
  });
245
453
 
246
- if (instance.body) {
247
- let body = protectCurlyBraces(instance.body);
454
+ const fragment = parseFragment(template, doc);
455
+
456
+ warnUnusedDeclarations(cx, compName, instance, script, fragment);
248
457
 
249
- const fragment = JSDOM.fragment(body);
458
+ const slotFragment = parseFragment(elInnerHtml, doc);
459
+ if (sourceFile) {
460
+ validateChainStructure(slotFragment, sourceFile, sourceContent, elInnerHtml);
461
+ }
462
+ validateChainStructure(fragment, instance.__sourceFile || displayName, instance, template);
463
+ Array.from(fragment.querySelectorAll("slot")).forEach(slot => {
464
+ slot.replaceWith(slotFragment);
465
+ });
250
466
 
251
- const slotFragment = JSDOM.fragment(srcInnerHtml);
252
- if (sourceFile) {
253
- validateChainStructure(slotFragment, sourceFile, sourceContent, srcInnerHtml);
467
+ const childEntries = Array.from(fragment.querySelectorAll("*")).map(el => ({
468
+ el,
469
+ parent: el.parentNode
470
+ }));
471
+ const condChains = new Map();
472
+ const bindings = [];
473
+ const elBindIds = new Map();
474
+ let bindCounter = 0;
475
+
476
+ const conditionalLocations = new Map();
477
+ for (const { el } of childEntries) {
478
+ if (!el.hasAttribute("if") && !hasMountIf(el) && !el.hasAttribute("elif")) continue;
479
+ let location = sourceFile;
480
+ if (instance) {
481
+ const lineNum = findElementLine(instance, el.outerHTML);
482
+ if (lineNum !== null) location = `${displayName}:${lineNum}`;
254
483
  }
255
- validateChainStructure(fragment, instance.__sourceFile || compName, body, body);
256
- Array.from(fragment.querySelectorAll("slot")).forEach(slot => {
257
- slot.replaceWith(slotFragment);
258
- });
484
+ if (location === sourceFile && sourceContent) {
485
+ const lineNum = findElementLine(sourceContent, el.outerHTML);
486
+ if (lineNum !== null) location = `${sourceFile}:${lineNum}`;
487
+ }
488
+ conditionalLocations.set(el, location);
489
+ }
259
490
 
260
- const childEntries = Array.from(fragment.querySelectorAll("*")).map(el => ({
261
- el,
262
- parent: el.parentNode
263
- }));
264
- const condChains = new Map();
491
+ childEntries.forEach(({ el: child, parent }) => {
492
+ if (!condChains.has(parent)) {
493
+ condChains.set(parent, { active: false, rendered: false });
494
+ }
495
+ const condChain = condChains.get(parent);
265
496
 
266
- childEntries.forEach(({ el: child, parent }) => {
267
- if (!condChains.has(parent)) {
268
- condChains.set(parent, { active: false, rendered: false });
269
- }
270
- const condChain = condChains.get(parent);
497
+ const hasIf = child.hasAttribute("if");
498
+ const hasDelIf = hasMountIf(child);
499
+ const hasElif = child.hasAttribute("elif");
500
+ const hasElse = child.hasAttribute("else");
271
501
 
272
- const hasIf = child.hasAttribute("if");
273
- const hasDelIf = hasDelIfAttr(child, instance.__sourceFile || compName);
274
- const hasElif = child.hasAttribute("elif");
275
- const hasElse = child.hasAttribute("else");
502
+ if (hasIf || hasDelIf || hasElif) {
503
+ const location = conditionalLocations.get(child) || sourceFile;
504
+ const stripBraces = (raw) => raw.startsWith("{") ? raw.slice(1, -1) : raw;
505
+ const tag = child.tagName.toLowerCase();
506
+ const warnIfConstant = (expr, attr) => {
507
+ const { constant, value } = evaluateConstant(expr);
508
+ if (constant) warnConstantCondition(location, tag, attr, expr, Boolean(value));
509
+ };
510
+ if (hasIf) warnIfConstant(stripBraces(child.getAttribute("if")), "if");
511
+ if (hasDelIf) warnIfConstant(stripBraces(getMountIf(child)), "mount:if");
512
+ if (hasElif) warnIfConstant(stripBraces(child.getAttribute("elif")), "elif");
513
+ }
276
514
 
277
- if (hasElif || hasElse) {
278
- if (!condChain.active) {
279
- throwError(`${instance.__sourceFile || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/del-if sibling`);
280
- }
281
- if (condChain.rendered) {
282
- child.remove();
283
- if (hasElse) {
284
- condChain.active = false;
285
- }
286
- return;
515
+ if (hasElif || hasElse) {
516
+ if (!condChain.active) {
517
+ throwError(`${instance.__sourceFile || displayName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/mount:if sibling`);
518
+ }
519
+ if (condChain.rendered) {
520
+ child.remove();
521
+ if (hasElse) {
522
+ condChain.active = false;
287
523
  }
524
+ return;
288
525
  }
526
+ }
289
527
 
290
- if (child.tagName.toLowerCase() === "void") {
291
- if (hasElif || hasElse) {
292
- if (hasElif) {
293
- const expr = child.getAttribute("elif").slice(1, -1);
294
- const fn = new Function("ctx", `with(ctx) { return (${expr}); }`);
295
- if (!fn(ctxProxy)) {
296
- child.remove();
297
- return;
298
- }
528
+ if (child.tagName.toLowerCase() === "void") {
529
+ if (hasElif || hasElse) {
530
+ if (hasElif) {
531
+ const expr = child.getAttribute("elif").slice(1, -1);
532
+ const fn = compileExpr(expr, true);
533
+ if (!fn(ctxProxy)) {
534
+ child.remove();
535
+ return;
299
536
  }
537
+ }
538
+ child.replaceWith(...child.children);
539
+ condChain.rendered = true;
540
+ if (hasElse) {
541
+ condChain.active = false;
542
+ }
543
+ } else if (hasIf || hasDelIf) {
544
+ const raw = hasIf ? child.getAttribute("if") : getMountIf(child);
545
+ const expr = raw.slice(1, -1);
546
+ const fn = compileExpr(expr, true);
547
+ condChain.active = true;
548
+ if (fn(ctxProxy)) {
300
549
  child.replaceWith(...child.children);
301
550
  condChain.rendered = true;
302
- if (hasElse) {
303
- condChain.active = false;
304
- }
305
- } else if (hasIf || hasDelIf) {
306
- const raw = hasIf ? child.getAttribute("if") : getDelIfAttr(child);
307
- const expr = raw.slice(1, -1);
308
- const fn = new Function("ctx", `with(ctx) { return (${expr}); }`);
309
- condChain.active = true;
310
- if (fn(ctxProxy)) {
311
- child.replaceWith(...child.children);
312
- condChain.rendered = true;
313
- } else {
314
- child.remove();
315
- condChain.rendered = false;
316
- }
317
551
  } else {
318
- child.replaceWith(...child.children);
319
- condChain.active = false;
552
+ child.remove();
320
553
  condChain.rendered = false;
321
554
  }
322
- return;
555
+ } else {
556
+ child.replaceWith(...child.children);
557
+ condChain.active = false;
558
+ condChain.rendered = false;
323
559
  }
560
+ return;
561
+ }
324
562
 
325
- const reservedAttrs = ["if", "del-if", "del:if", "elif", "else"];
563
+ Array.from(child.attributes).forEach(attribute => {
564
+ if (!attribute || attribute === undefined) return;
565
+ if (reservedAttrs.includes(attribute.name)) return;
566
+
567
+ if (attribute.name.startsWith("bind:")) {
568
+ const prop = attribute.name.slice(5);
569
+ const varName = attribute.value;
570
+ let bindId = elBindIds.get(child);
571
+ if (!bindId) {
572
+ bindId = "b" + (bindCounter++);
573
+ elBindIds.set(child, bindId);
574
+ child.setAttribute("data-chbind-" + bindId, "");
575
+ }
576
+ bindings.push({ prop, varName, bindId });
577
+ child.removeAttribute(attribute.name);
578
+ return;
579
+ }
326
580
 
327
- Array.from(child.attributes).forEach(attribute => {
328
- if (!attribute || attribute === undefined) return;
329
- if (reservedAttrs.includes(attribute.name)) return;
330
- attribute.value = attribute.value.replace(
581
+ child.setAttribute(
582
+ attribute.name,
583
+ attribute.value.replace(
331
584
  /\{([^}]+)\}/g,
332
585
  (_, expr) => {
333
586
  try {
334
- return Function("ctx", `with(ctx) { return (${expr}); }`)(ctxProxy);
587
+ return compileExpr(expr, true)(ctxProxy);
335
588
  } catch {
336
589
  return "";
337
590
  }
338
591
  }
339
- );
340
- });
341
-
342
- processComponentElement(
343
- child,
344
- loadedComponents,
345
- runtimeChunks,
346
- compIdColl,
347
- letterState,
348
- runtimeMap,
349
- cssScopes,
350
- cssScopesMap,
351
- scopedStyles,
352
- renderChain.concat(compName),
353
- staticCtxRegistry,
354
- instance.__sourceFile || compName,
355
- body
592
+ )
356
593
  );
594
+ });
357
595
 
358
- if (hasIf) {
359
- const expr = child.getAttribute("if").slice(1, -1);
360
- const fn = new Function("ctx", `with(ctx) { return (${expr}); }`);
361
- condChain.active = true;
362
- if (fn(ctxProxy)) {
363
- condChain.rendered = true;
364
- } else {
365
- child.style.display = "none";
366
- condChain.rendered = false;
367
- }
368
- child.removeAttribute("if");
369
- } else if (hasDelIf) {
370
- const expr = getDelIfAttr(child).slice(1, -1);
371
- const fn = new Function("ctx", `with(ctx) { return (${expr}); }`);
372
- condChain.active = true;
373
- if (fn(ctxProxy)) {
374
- condChain.rendered = true;
375
- } else {
376
- child.remove();
377
- condChain.rendered = false;
378
- }
379
- removeDelIfAttr(child);
380
- } else if (hasElif) {
381
- const expr = child.getAttribute("elif").slice(1, -1);
382
- const fn = new Function("ctx", `with(ctx) { return (${expr}); }`);
383
- if (fn(ctxProxy)) {
384
- condChain.rendered = true;
385
- } else {
386
- child.remove();
387
- }
388
- child.removeAttribute("elif");
389
- } else if (hasElse) {
390
- condChain.rendered = true;
391
- condChain.active = false;
392
- child.removeAttribute("else");
393
- } else {
394
- condChain.active = false;
395
- condChain.rendered = false;
596
+ const condAttrs = {};
597
+ if (hasIf) condAttrs["if"] = child.getAttribute("if");
598
+ if (hasDelIf) condAttrs["mount:if"] = getMountIf(child);
599
+ if (hasElif) condAttrs["elif"] = child.getAttribute("elif");
600
+ if (hasElse) condAttrs["else"] = "";
601
+
602
+ const processed = processComponentElement(
603
+ child,
604
+ cx,
605
+ renderChain.concat(compName),
606
+ displayName,
607
+ template,
608
+ globalCtx
609
+ );
610
+
611
+ let condTarget = child;
612
+ if (processed && processed.nodeType === 1) {
613
+ condTarget = processed;
614
+ for (const [name, value] of Object.entries(condAttrs)) {
615
+ condTarget.setAttribute(name, value);
396
616
  }
617
+ }
397
618
 
398
- interpolateNode(child, ctxProxy)
399
- });
619
+ applyConditionalToElement(condTarget, ctxProxy, condChain, hasIf, hasDelIf, hasElif, hasElse);
400
620
 
401
- const firstChild = fragment.firstChild;
621
+ interpolateNode(condTarget, ctxProxy)
622
+ });
402
623
 
403
- if (firstChild && firstChild.nodeType === 1) {
404
- if (instance.script || instance.effects) {
405
- const compId = "chid-" + genRandomId(compIdColl);
406
- firstChild.setAttribute("chid", compId);
624
+ let csrRuntimeSource = null;
625
+ const firstChild = fragment.children[0];
626
+
627
+ if (firstChild && firstChild.nodeType === 1) {
628
+ if (script) {
629
+ const compId = "chid-" + deterministicHash(compName + ":" + cx.compIdColl.length, 8);
630
+ cx.compIdColl.push(compId);
631
+ firstChild.setAttribute("chid", compId);
632
+
633
+ const ctxRegex = /ctx\s*=\s*({.*?})/;
634
+ const ctxMatch = script.match(ctxRegex);
635
+ let runtimeCtx = {};
636
+ if (ctxMatch) {
637
+ try {
638
+ runtimeCtx = JSON.parse(ctxMatch[1].replace(/(\w+):/g, '"$1":'));
639
+ } catch (e) {
640
+ runtimeCtx = {};
641
+ }
642
+ }
643
+ const ctxDefParts = [];
644
+ const declared = new Set();
645
+ for (const [key, value] of Object.entries(runtimeCtx)) {
646
+ ctxDefParts.push(`let ${key} = ctx.${key}??${JSON.stringify(value)};\n`);
647
+ declared.add(key);
648
+ }
649
+
650
+ for (const { name, defaultValue } of compProps) {
651
+ if (declared.has(name)) continue;
652
+ if (defaultValue !== undefined) {
653
+ ctxDefParts.push(`let ${name} = ctx.${name}??(${defaultValue});\n`);
654
+ } else {
655
+ ctxDefParts.push(`let ${name} = ctx.${name};\n`);
656
+ }
657
+ }
407
658
 
408
- let script = instance.script && instance.script.toString();
659
+ const ctxDef = ctxDefParts.join("");
660
+
661
+ script = script.replace(ctxRegex, "ctx");
662
+
663
+ let runtime = extractRuntime(script, displayName);
664
+
665
+ if (runtime) {
666
+ let fnEntry = cx.runtimeMap && cx.runtimeMap.get(compName);
667
+ let fnId;
668
+ if (!fnEntry) {
669
+ fnId = runtimeFunctionId(compName);
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
+ }
409
737
 
410
- const ctxRegex = /ctx\s*=\s*({.*?})/;
411
- const ctxMatch = script.match(ctxRegex);
412
- let runtimeCtx = {};
413
- if (ctxMatch) {
414
- try {
415
- runtimeCtx = JSON.parse(ctxMatch[1].replace(/(\w+):/g, '"$1":'));
416
- } catch (e) {
417
- runtimeCtx = {};
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;
418
755
  }
419
- }
420
- let ctxDef = "";
421
- for (const [key, value] of Object.entries(runtimeCtx)) {
422
- ctxDef += `ctx.${key} = ctx.${key}||${JSON.stringify(value)};\n`;
423
- }
424
- script = script.replace(ctxRegex, "ctx");
425
- script = script.replace(/RUNTIME\([^)]*\)\s*{/, match => match + "\n" + ctxDef);
426
-
427
- let letterEntry = runtimeMap && runtimeMap.get(compName);
428
- let letter;
429
- if (!letterEntry) {
430
- letter = getNextLetter(letterState);
431
- script = script.replace(/RUNTIME/g, `${letter}RUNTIME`);
432
- runtimeChunks.push(script);
433
- runtimeMap && runtimeMap.set(compName, { letter });
756
+ if (effectiveTopVars.length > 0) {
757
+ injectCode += "\n" + effectiveTopVars.map(v => v.value !== undefined
758
+ ? `${v.keyword} ${v.name} = ${v.value};`
759
+ : `${v.keyword} ${v.name};`
760
+ ).join("\n") + "\n";
761
+ }
762
+ if (effectiveBindings.length > 0) {
763
+ injectCode += "\n" + effectiveBindings.map(b => {
764
+ const accessor = b.prop === "self" ? "" : "." + b.prop;
765
+ return "let " + b.varName + " = self.querySelector('[data-chbind-" + b.bindId + "]')" + accessor + ";";
766
+ }).join("\n") + "\n";
767
+ }
768
+ if (effectiveFuncs.length > 0) {
769
+ injectCode += "\n" + effectiveFuncs.join("\n\n") + "\n";
770
+ }
771
+ runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
772
+
773
+ runtime = runtime.replace(`${RUNTIME_KW}()`, `${fnId}(self, ctx)`);
774
+ csrRuntimeSource = runtime.replace(/^function\s+\w+/, "function");
775
+ cx.runtimeChunks.push(runtime);
776
+ cx.runtimeMap && cx.runtimeMap.set(compName, { fnId });
434
777
  } else {
435
- letter = letterEntry.letter;
778
+ fnId = fnEntry.fnId;
436
779
  }
437
780
 
438
- runtimeChunks.push(`${letter}RUNTIME(document.querySelector('[chid="${compId}"]'), ${JSON.stringify(ctx)});`);
781
+ cx.runtimeChunks.push(`${fnId}(document.querySelector('[chid="${compId}"]'), ${JSON.stringify(ctx)});`);
439
782
  }
440
783
  }
784
+ }
441
785
 
442
- let style = instance.styles && instance.styles.toString();
443
- if (style) {
444
- let cssId = cssScopesMap && cssScopesMap.get(compName);
445
- if (!cssId) {
446
- cssId = genRandomId(cssScopes, 8, true);
447
- cssScopesMap.set(compName, cssId);
448
- }
449
- if (fragment.firstChild && fragment.firstChild.nodeType === 1) {
450
- fragment.firstChild.classList.add(cssId);
451
- }
452
- style = scopeCss(style, cssId);
453
- scopedStyles.push(style);
454
- }
786
+ let cssId = cx.cssScopesMap && cx.cssScopesMap.get(compName);
787
+ if (!cssId) {
788
+ cssId = deterministicHash(compName, 8);
789
+ cx.cssScopesMap.set(compName, cssId);
790
+ }
791
+ if (fragment.children.length === 1 && firstChild.nodeType === 1) {
792
+ firstChild.classList.add(cssId);
793
+ }
794
+ if (styles) {
795
+ styles = scopeCss(styles, cssId);
796
+ cx.scopedStyles.push(styles);
797
+ }
455
798
 
456
- if (fragment.firstChild && fragment.firstChild.nodeType === 1) {
457
- fragment.firstChild.setAttribute("data-ch-source", instance.__sourceFile || compName);
458
- }
799
+ if (csrRuntimeSource && !cx.csrClasses.has(compName)) {
800
+ generateCSRClass(compName, cx);
801
+ }
459
802
 
460
- element.replaceWith(fragment);
461
- return true;
803
+ if (element.style.display === "none" && firstChild && firstChild.nodeType === 1) {
804
+ firstChild.style.display = "none";
462
805
  }
463
806
 
464
- console.warn(chalk.yellow(`${compName} — component could not be loaded (missing body or styles export)`));
465
- return false;
807
+ element.replaceWith(fragment);
808
+ return firstChild && firstChild.nodeType === 1 ? firstChild : true;
466
809
  }
467
810
 
468
- /**
469
- * Processes all components in the app container
470
- * @param {Element[]} appElements
471
- * @param {Map} loadedComponents
472
- * @returns {{
473
- * runtimeScript: string,
474
- * hasComponents: boolean
475
- * scopesCss: CSSString
476
- * }}
477
- */
478
- export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent) {
479
- let runtimeChunks = [];
480
- let compIdColl = [];
481
- let letterState = { value: null };
482
- let runtimeMap = new Map();
483
- let cssScopes = [];
484
- let cssScopesMap = new Map();
485
- let scopedStyles = [];
486
- let staticCtxRegistry = new Map();
811
+ export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent, globalCtx = {}, treeShakeRuntime = true, originalNames = null) {
812
+ const cx = new ProcessContext(
813
+ loadedComponents, [], [], new Map(), [], new Map(), [], new Map(), new Map(), treeShakeRuntime, originalNames
814
+ );
815
+
816
+ for (const [compName, instance] of cx.loadedComponents) {
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
+ }
857
+ }
858
+ }
859
+ }
860
+ }
487
861
 
488
862
  appElements.forEach(el => {
489
- processComponentElement(el, loadedComponents, runtimeChunks, compIdColl, letterState, runtimeMap, cssScopes, cssScopesMap, scopedStyles, [], staticCtxRegistry, pageSourceFile, pageSourceContent);
863
+ if (!el.isConnected) return;
864
+ processComponentElement(el, cx, [], pageSourceFile, pageSourceContent, globalCtx);
490
865
  });
491
- const runtimeScript = runtimeChunks.join("\n");
492
- const hasComponents = runtimeChunks.length > 0;
493
- const scopesCss = beautify.css(scopedStyles.join("\n"));
866
+ const runtimeScript = cx.runtimeChunks.join("\n");
867
+ const hasComponents = cx.runtimeChunks.length > 0;
868
+ const scopesCss = cx.scopedStyles.join("\n");
869
+ const csrClasses = [...cx.csrClasses.values()].join("\n\n");
870
+
871
+ const hashMap = {};
872
+ for (const [compName, hash] of cx.cssScopesMap) {
873
+ hashMap[compName] = hash;
874
+ }
494
875
 
495
- return { runtimeScript, hasComponents, scopesCss };
876
+ return { runtimeScript, hasComponents, scopesCss, hashMap, csrClasses };
496
877
  }
497
878
 
498
- /**
499
- * Gets the next letter in sequence or starts with 'a'
500
- * @param {object} letterState - { value: string }
501
- * @returns {string}
502
- */
503
- function getNextLetter(letterState) {
504
- if (!letterState.value) {
505
- letterState.value = "a";
506
- } else {
507
- letterState.value = incrementAlfabet(letterState.value);
508
- }
509
- return letterState.value;
510
- }
879
+ const RUNTIME_KW = "$runtime";