chocola 1.6.0 → 2.0.0-next.10

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,638 @@
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,
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) {
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();
13
27
  }
14
- return false;
15
28
  }
16
29
 
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}`;
30
+ function escapeForTemplateLiteral(str) {
31
+ return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
22
32
  }
23
33
 
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]);
34
+ function parseFragment(html, doc) {
35
+ const fragment = doc.createDocumentFragment();
36
+ const temp = doc.createElement("div");
37
+ temp.innerHTML = html;
38
+ const children = [...temp.childNodes];
39
+ for (const child of children) {
40
+ fragment.appendChild(child);
30
41
  }
42
+ return fragment;
43
+ }
31
44
 
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
- }
45
+ function generateCSRClass(compName, cx, explicitClassName) {
46
+ if (cx.csrClasses.has(compName)) return;
47
+
48
+ let instance = cx.loadedComponents.get(compName);
49
+ if (!instance) return;
50
+
51
+ instance = protectCurlyBraces(instance);
52
+ const dom = parseHTML(instance);
53
+ const doc = dom.document;
54
+ const script = doc.querySelector("script")?.innerHTML;
55
+ const template = doc.querySelector("template")?.innerHTML;
56
+ const styles = doc.querySelector("style")?.innerHTML;
57
+
58
+ if (!template) return;
59
+
60
+ const compProps = extractPropsDefaults(script);
61
+ const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
62
+ const topVars = extractTopLevelVariables(script || "");
63
+ let runtime = extractRuntime(script || "", compName);
64
+
65
+ if (!cx.cssScopesMap.has(compName)) {
66
+ cx.cssScopesMap.set(compName, deterministicHash(compName, 8));
67
+ }
68
+ const cssId = cx.cssScopesMap.get(compName);
69
+
70
+ if (styles) {
71
+ cx.scopedStyles.push(scopeCss(styles, cssId));
45
72
  }
46
73
 
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;
74
+ const childMappings = [];
75
+ const bindVarNames = new Set();
76
+ const templateDoc = parseFragment(template, doc);
77
+ const seenTags = new Set();
78
+ for (const el of templateDoc.querySelectorAll("*")) {
79
+ const tag = el.tagName.toLowerCase();
80
+ if (!seenTags.has(tag)) {
81
+ seenTags.add(tag);
82
+ const childCompName = tag + ".html";
83
+ if (cx.loadedComponents.has(childCompName)) {
84
+ generateCSRClass(childCompName, cx);
85
+ const childClassName = childCompName.replace(".html", "").replace(/^\w/, c => c.toUpperCase());
86
+ childMappings.push({ tag, compClass: childClassName });
87
+ }
88
+ }
89
+ for (const attr of el.attributes) {
90
+ if (attr.name.startsWith("bind:")) {
91
+ bindVarNames.add(attr.value);
55
92
  }
56
93
  }
57
- return s.length - 1;
58
94
  }
59
95
 
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;
96
+ let csrRuntimeSource = null;
97
+ const injectedNames = new Set(compProps.map(p => p.name));
98
+ for (const varName of bindVarNames) injectedNames.add(varName);
99
+ const topVarsToInject = topVars.filter(v => !injectedNames.has(v.name));
100
+ if (runtime) {
101
+ let injectCode = "";
102
+ for (const { name, defaultValue } of compProps) {
103
+ if (defaultValue !== undefined) {
104
+ injectCode += `let ${name} = ctx.${name}??(${defaultValue});\n`;
105
+ } else {
106
+ injectCode += `let ${name} = ctx.${name};\n`;
72
107
  }
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
- }
108
+ }
109
+ for (const { keyword, name, value } of topVarsToInject) {
110
+ if (value !== undefined) {
111
+ injectCode += `${keyword} ${name} = ctx.${name}??(${value});\n`;
84
112
  } 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 + "}";
113
+ injectCode += `${keyword} ${name};\n`;
114
+ }
115
+ }
116
+ for (const varName of bindVarNames) {
117
+ if (varName !== "self") {
118
+ injectCode += `let ${varName} = ctx.${varName};\n`;
94
119
  }
120
+ }
121
+ if (topFuncSrc.length > 0) {
122
+ injectCode += "\n" + topFuncSrc.join("\n\n") + "\n";
123
+ }
124
+ runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
125
+ runtime = runtime.replace(`${RUNTIME_KW}()`, `function(self, ctx)`);
126
+ csrRuntimeSource = runtime.replace(/^(async\s+)?function\s+\w+/, "$1function");
127
+ }
95
128
 
96
- i = endBrace + 1;
129
+ const className = explicitClassName || compName.replace(".html", "").replace(/^\w/, c => c.toUpperCase());
130
+ const propsParts = [];
131
+ for (const { name, defaultValue } of compProps) {
132
+ propsParts.push(`${JSON.stringify(name)}: ${defaultValue !== undefined ? defaultValue : "null"}`);
133
+ }
134
+ for (const { name, value } of topVarsToInject) {
135
+ if (value !== undefined) {
136
+ propsParts.push(`${JSON.stringify(name)}: ${value}`);
97
137
  }
98
- return out;
99
138
  }
100
139
 
101
- return processBlock(cssString);
140
+ const childrenPart = childMappings.length > 0
141
+ ? ",\n children: [" + childMappings.map(m => `{tag:"${m.tag}",compClass:${m.compClass}}`).join(",") + "]"
142
+ : "";
143
+ const runtimePart = csrRuntimeSource ? `,\n runtime: ${csrRuntimeSource}` : "";
144
+ const propsPart = propsParts.length > 0 ? `{ ${propsParts.join(", ")} }` : "{}";
145
+ 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}`;
146
+ cx.csrClasses.set(compName, classDef);
102
147
  }
103
148
 
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.`));
149
+ const BARE_IDENTIFIER_RE = /(?<![.\w$])[a-zA-Z_$][0-9a-zA-Z_$]*/g;
150
+ const PROPERTY_IDENTIFIER_RE = /\.([a-zA-Z_$][0-9a-zA-Z_$]*)/g;
151
+
152
+ function countIdentifiers(text, counts) {
153
+ for (const re of [BARE_IDENTIFIER_RE, PROPERTY_IDENTIFIER_RE]) {
154
+ re.lastIndex = 0;
155
+ for (const m of text.matchAll(re)) counts.set(m[1] ?? m[0], (counts.get(m[1] ?? m[0]) || 0) + 1);
110
156
  }
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");
157
+ return counts;
115
158
  }
116
- function removeDelIfAttr(el) {
117
- el.removeAttribute("del-if");
118
- el.removeAttribute("del:if");
159
+
160
+ function escapeNameForRegex(name) {
161
+ return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
119
162
  }
120
163
 
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
- }
164
+ function findLineInSource(source, regex) {
165
+ const match = regex.exec(source);
166
+ if (!match) return null;
167
+ return source.substring(0, match.index).split("\n").length;
133
168
  }
134
169
 
135
- function validateChainStructure(parent, sourceFile, sourceContent, parentContent) {
136
- const children = [...parent.children];
137
- let chainActive = false;
138
- const outerCounts = {};
170
+ function warnUnusedDeclarations(cx, compName, instance, script, fragment) {
171
+ if (!script && !fragment.querySelector("[bind\\:]")) return;
172
+ if (cx.unusedWarned.has(compName)) return;
173
+ cx.unusedWarned.add(compName);
174
+
175
+ const props = extractPropsDefaults(script).map(p => p.name);
176
+ const topVars = extractTopLevelVariables(script).map(v => v.name);
177
+ const topFuncs = extractTopLevelFunctions(script, RUNTIME_KW)
178
+ .map(src => src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/)?.[1])
179
+ .filter(Boolean);
180
+
181
+ const bindings = [];
182
+ for (const el of fragment.querySelectorAll("*")) {
183
+ for (const attr of el.attributes) {
184
+ if (attr.name.startsWith("bind:")) bindings.push({ name: attr.value, element: el });
185
+ }
186
+ }
139
187
 
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");
188
+ const counts = new Map();
189
+ if (script) countIdentifiers(script, counts);
145
190
 
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
- }
191
+ const walk = (node) => {
192
+ if (node.nodeType === 3) {
193
+ for (const m of node.textContent.matchAll(/\{([^}]+)\}/g)) countIdentifiers(m[1], counts);
194
+ return;
195
+ }
196
+ if (node.attributes) {
197
+ for (const attr of node.attributes) {
198
+ if (attr.name.startsWith("bind:")) {
199
+ countIdentifiers(attr.value, counts);
200
+ } else {
201
+ for (const m of attr.value.matchAll(/\{([^}]+)\}/g)) countIdentifiers(m[1], counts);
183
202
  }
184
- throwError(`${loc}\n <${tag}> has ${attr} without a preceding if/del-if sibling`);
185
- }
186
- if (hasElse) {
187
- chainActive = false;
188
203
  }
189
204
  }
190
-
191
- if (hasIf || hasDelIf) {
192
- chainActive = true;
193
- } else if (!hasElif && !hasElse) {
194
- chainActive = false;
205
+ for (const child of node.childNodes) walk(child);
206
+ };
207
+ walk(fragment);
208
+
209
+ const isUnused = (name) => (counts.get(name) || 0) <= 1;
210
+ const warn = (kind, name, declRegex) => {
211
+ const lineNum = findLineInSource(instance, declRegex);
212
+ warnUnusedDeclaration(lineNum !== null ? `${compName}:${lineNum}` : compName, kind, name);
213
+ };
214
+
215
+ for (const name of props) {
216
+ if (isUnused(name)) warn("prop", name, new RegExp("export\\s+let\\s+" + escapeNameForRegex(name) + "\\b"));
217
+ }
218
+ for (const name of topVars) {
219
+ if (isUnused(name)) warn("variable", name, new RegExp("(?:^|[^\\w])(?:let|const)\\s+" + escapeNameForRegex(name) + "\\b"));
220
+ }
221
+ for (const name of topFuncs) {
222
+ if (isUnused(name)) warn("function", name, new RegExp("(?:async\\s+)?function\\s+" + escapeNameForRegex(name) + "\\b"));
223
+ }
224
+ const seenBindings = new Set();
225
+ for (const { name, element } of bindings) {
226
+ if (seenBindings.has(name)) continue;
227
+ seenBindings.add(name);
228
+ if (isUnused(name)) {
229
+ const lineNum = findElementLine(instance, element.outerHTML);
230
+ warnUnusedDeclaration(lineNum !== null ? `${compName}:${lineNum}` : compName, "binding", name);
195
231
  }
196
-
197
- validateChainStructure(child, sourceFile, sourceContent, parentContent);
198
232
  }
199
233
  }
200
234
 
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
235
  export function processComponentElement(
211
236
  element,
212
- loadedComponents,
213
- runtimeChunks,
214
- compIdColl,
215
- letterState,
216
- runtimeMap,
217
- cssScopes,
218
- cssScopesMap,
219
- scopedStyles,
237
+ cx,
220
238
  renderChain = [],
221
- staticCtxRegistry,
222
239
  sourceFile,
223
- sourceContent
240
+ sourceContent,
241
+ globalCtx = {}
224
242
  ) {
225
243
  const tagName = element.tagName.toLowerCase();
226
- const compName = tagName + ".js";
227
- const instance = loadedComponents.get(compName);
244
+ const compName = tagName + ".html";
245
+ let instance = cx.loadedComponents.get(compName);
228
246
 
229
247
  if (!instance || instance === undefined) return false;
230
248
  if (renderChain && renderChain.includes(compName)) return false;
231
249
 
250
+ instance = protectCurlyBraces(instance);
251
+ const dom = parseHTML(instance);
252
+ const doc = dom.document;
253
+ let script = doc.querySelector("script")?.innerHTML;
254
+ let template = doc.querySelector("template")?.innerHTML;
255
+ let styles = doc.querySelector("style")?.innerHTML;
256
+
257
+ if (!template) {
258
+ console.warn(chalk.yellow(`${compName} — component is missing a <template>`));
259
+ return false;
260
+ }
261
+
262
+ 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);
268
+ }
269
+ return "";
270
+ });
271
+ }
272
+
273
+ const compProps = extractPropsDefaults(script);
274
+
232
275
  let ctx;
233
- if (staticCtxRegistry && staticCtxRegistry.has(element)) {
234
- ctx = staticCtxRegistry.get(element);
276
+ if (cx.staticCtxRegistry && cx.staticCtxRegistry.has(element)) {
277
+ ctx = cx.staticCtxRegistry.get(element);
235
278
  } else {
236
- ctx = extractContextFromElement(element);
237
- staticCtxRegistry && staticCtxRegistry.set(element, ctx);
279
+ ctx = extractCtxFromEl(element, globalCtx);
280
+ if (globalCtx && typeof globalCtx === "object") {
281
+ for (const [k, v] of Object.entries(globalCtx)) {
282
+ if (!(k in ctx)) ctx[k] = v;
283
+ }
284
+ }
285
+ cx.staticCtxRegistry && cx.staticCtxRegistry.set(element, ctx);
286
+ }
287
+
288
+ if (compProps.length > 0) {
289
+ compProps.forEach(({ name, defaultValue }) => {
290
+ if (defaultValue !== undefined && !(name in ctx)) {
291
+ try {
292
+ ctx[name] = compileExpr(defaultValue, false)();
293
+ } catch {
294
+ ctx[name] = defaultValue;
295
+ }
296
+ }
297
+ });
238
298
  }
239
- const srcInnerHtml = element.innerHTML;
299
+
300
+ const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
301
+ 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 {}
308
+ }
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
+ }
317
+
318
+ const elInnerHtml = element.innerHTML;
240
319
 
241
320
  const ctxProxy = new Proxy(ctx, {
242
321
  has() { return true; },
243
322
  get(target, key) { return target[key]; }
244
323
  });
245
324
 
246
- if (instance.body) {
247
- let body = protectCurlyBraces(instance.body);
325
+ const fragment = parseFragment(template, doc);
326
+
327
+ warnUnusedDeclarations(cx, compName, instance, script, fragment);
248
328
 
249
- const fragment = JSDOM.fragment(body);
329
+ const slotFragment = parseFragment(elInnerHtml, doc);
330
+ if (sourceFile) {
331
+ validateChainStructure(slotFragment, sourceFile, sourceContent, elInnerHtml);
332
+ }
333
+ validateChainStructure(fragment, instance.__sourceFile || compName, instance, template);
334
+ Array.from(fragment.querySelectorAll("slot")).forEach(slot => {
335
+ slot.replaceWith(slotFragment);
336
+ });
250
337
 
251
- const slotFragment = JSDOM.fragment(srcInnerHtml);
252
- if (sourceFile) {
253
- validateChainStructure(slotFragment, sourceFile, sourceContent, srcInnerHtml);
338
+ const childEntries = Array.from(fragment.querySelectorAll("*")).map(el => ({
339
+ el,
340
+ parent: el.parentNode
341
+ }));
342
+ const condChains = new Map();
343
+ const bindings = [];
344
+ const elBindIds = new Map();
345
+ let bindCounter = 0;
346
+
347
+ const conditionalLocations = new Map();
348
+ for (const { el } of childEntries) {
349
+ if (!el.hasAttribute("if") && !hasMountIf(el) && !el.hasAttribute("elif")) continue;
350
+ let location = sourceFile;
351
+ if (instance) {
352
+ const lineNum = findElementLine(instance, el.outerHTML);
353
+ if (lineNum !== null) location = `${compName}:${lineNum}`;
254
354
  }
255
- validateChainStructure(fragment, instance.__sourceFile || compName, body, body);
256
- Array.from(fragment.querySelectorAll("slot")).forEach(slot => {
257
- slot.replaceWith(slotFragment);
258
- });
355
+ if (location === sourceFile && sourceContent) {
356
+ const lineNum = findElementLine(sourceContent, el.outerHTML);
357
+ if (lineNum !== null) location = `${sourceFile}:${lineNum}`;
358
+ }
359
+ conditionalLocations.set(el, location);
360
+ }
259
361
 
260
- const childEntries = Array.from(fragment.querySelectorAll("*")).map(el => ({
261
- el,
262
- parent: el.parentNode
263
- }));
264
- const condChains = new Map();
362
+ childEntries.forEach(({ el: child, parent }) => {
363
+ if (!condChains.has(parent)) {
364
+ condChains.set(parent, { active: false, rendered: false });
365
+ }
366
+ const condChain = condChains.get(parent);
265
367
 
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);
368
+ const hasIf = child.hasAttribute("if");
369
+ const hasDelIf = hasMountIf(child);
370
+ const hasElif = child.hasAttribute("elif");
371
+ const hasElse = child.hasAttribute("else");
271
372
 
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");
373
+ if (hasIf || hasDelIf || hasElif) {
374
+ const location = conditionalLocations.get(child) || sourceFile;
375
+ const stripBraces = (raw) => raw.startsWith("{") ? raw.slice(1, -1) : raw;
376
+ const tag = child.tagName.toLowerCase();
377
+ const warnIfConstant = (expr, attr) => {
378
+ const { constant, value } = evaluateConstant(expr);
379
+ if (constant) warnConstantCondition(location, tag, attr, expr, Boolean(value));
380
+ };
381
+ if (hasIf) warnIfConstant(stripBraces(child.getAttribute("if")), "if");
382
+ if (hasDelIf) warnIfConstant(stripBraces(getMountIf(child)), "mount:if");
383
+ if (hasElif) warnIfConstant(stripBraces(child.getAttribute("elif")), "elif");
384
+ }
276
385
 
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;
386
+ if (hasElif || hasElse) {
387
+ if (!condChain.active) {
388
+ throwError(`${instance.__sourceFile || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/mount:if sibling`);
389
+ }
390
+ if (condChain.rendered) {
391
+ child.remove();
392
+ if (hasElse) {
393
+ condChain.active = false;
287
394
  }
395
+ return;
288
396
  }
397
+ }
289
398
 
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
- }
399
+ if (child.tagName.toLowerCase() === "void") {
400
+ if (hasElif || hasElse) {
401
+ if (hasElif) {
402
+ const expr = child.getAttribute("elif").slice(1, -1);
403
+ const fn = compileExpr(expr, true);
404
+ if (!fn(ctxProxy)) {
405
+ child.remove();
406
+ return;
299
407
  }
408
+ }
409
+ child.replaceWith(...child.children);
410
+ condChain.rendered = true;
411
+ if (hasElse) {
412
+ condChain.active = false;
413
+ }
414
+ } else if (hasIf || hasDelIf) {
415
+ const raw = hasIf ? child.getAttribute("if") : getMountIf(child);
416
+ const expr = raw.slice(1, -1);
417
+ const fn = compileExpr(expr, true);
418
+ condChain.active = true;
419
+ if (fn(ctxProxy)) {
300
420
  child.replaceWith(...child.children);
301
421
  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
422
  } else {
318
- child.replaceWith(...child.children);
319
- condChain.active = false;
423
+ child.remove();
320
424
  condChain.rendered = false;
321
425
  }
322
- return;
426
+ } else {
427
+ child.replaceWith(...child.children);
428
+ condChain.active = false;
429
+ condChain.rendered = false;
323
430
  }
431
+ return;
432
+ }
324
433
 
325
- const reservedAttrs = ["if", "del-if", "del:if", "elif", "else"];
434
+ Array.from(child.attributes).forEach(attribute => {
435
+ if (!attribute || attribute === undefined) return;
436
+ if (reservedAttrs.includes(attribute.name)) return;
437
+
438
+ if (attribute.name.startsWith("bind:")) {
439
+ const prop = attribute.name.slice(5);
440
+ const varName = attribute.value;
441
+ let bindId = elBindIds.get(child);
442
+ if (!bindId) {
443
+ bindId = "b" + (bindCounter++);
444
+ elBindIds.set(child, bindId);
445
+ child.setAttribute("data-chbind-" + bindId, "");
446
+ }
447
+ bindings.push({ prop, varName, bindId });
448
+ child.removeAttribute(attribute.name);
449
+ return;
450
+ }
326
451
 
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(
452
+ child.setAttribute(
453
+ attribute.name,
454
+ attribute.value.replace(
331
455
  /\{([^}]+)\}/g,
332
456
  (_, expr) => {
333
457
  try {
334
- return Function("ctx", `with(ctx) { return (${expr}); }`)(ctxProxy);
458
+ return compileExpr(expr, true)(ctxProxy);
335
459
  } catch {
336
460
  return "";
337
461
  }
338
462
  }
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
463
+ )
356
464
  );
465
+ });
357
466
 
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;
467
+ const condAttrs = {};
468
+ if (hasIf) condAttrs["if"] = child.getAttribute("if");
469
+ if (hasDelIf) condAttrs["mount:if"] = getMountIf(child);
470
+ if (hasElif) condAttrs["elif"] = child.getAttribute("elif");
471
+ if (hasElse) condAttrs["else"] = "";
472
+
473
+ const processed = processComponentElement(
474
+ child,
475
+ cx,
476
+ renderChain.concat(compName),
477
+ compName,
478
+ template,
479
+ globalCtx
480
+ );
481
+
482
+ let condTarget = child;
483
+ if (processed && processed.nodeType === 1) {
484
+ condTarget = processed;
485
+ for (const [name, value] of Object.entries(condAttrs)) {
486
+ condTarget.setAttribute(name, value);
487
+ }
488
+ }
489
+
490
+ applyConditionalToElement(condTarget, ctxProxy, condChain, hasIf, hasDelIf, hasElif, hasElse);
491
+
492
+ interpolateNode(condTarget, ctxProxy)
493
+ });
494
+
495
+ let csrRuntimeSource = null;
496
+ const firstChild = fragment.children[0];
497
+
498
+ if (firstChild && firstChild.nodeType === 1) {
499
+ if (script) {
500
+ const compId = "chid-" + deterministicHash(compName + ":" + cx.compIdColl.length, 8);
501
+ cx.compIdColl.push(compId);
502
+ firstChild.setAttribute("chid", compId);
503
+
504
+ const ctxRegex = /ctx\s*=\s*({.*?})/;
505
+ const ctxMatch = script.match(ctxRegex);
506
+ let runtimeCtx = {};
507
+ if (ctxMatch) {
508
+ try {
509
+ runtimeCtx = JSON.parse(ctxMatch[1].replace(/(\w+):/g, '"$1":'));
510
+ } catch (e) {
511
+ runtimeCtx = {};
378
512
  }
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;
513
+ }
514
+ const ctxDefParts = [];
515
+ const declared = new Set();
516
+ for (const [key, value] of Object.entries(runtimeCtx)) {
517
+ ctxDefParts.push(`let ${key} = ctx.${key}??${JSON.stringify(value)};\n`);
518
+ declared.add(key);
519
+ }
520
+
521
+ for (const { name, defaultValue } of compProps) {
522
+ if (declared.has(name)) continue;
523
+ if (defaultValue !== undefined) {
524
+ ctxDefParts.push(`let ${name} = ctx.${name}??(${defaultValue});\n`);
385
525
  } else {
386
- child.remove();
526
+ ctxDefParts.push(`let ${name} = ctx.${name};\n`);
387
527
  }
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;
396
528
  }
397
529
 
398
- interpolateNode(child, ctxProxy)
399
- });
530
+ const ctxDef = ctxDefParts.join("");
400
531
 
401
- const firstChild = fragment.firstChild;
532
+ script = script.replace(ctxRegex, "ctx");
402
533
 
403
- if (firstChild && firstChild.nodeType === 1) {
404
- if (instance.script || instance.effects) {
405
- const compId = "chid-" + genRandomId(compIdColl);
406
- firstChild.setAttribute("chid", compId);
534
+ let runtime = extractRuntime(script, compName);
407
535
 
408
- let script = instance.script && instance.script.toString();
536
+ if (runtime) {
537
+ let fnEntry = cx.runtimeMap && cx.runtimeMap.get(compName);
538
+ let fnId;
539
+ if (!fnEntry) {
540
+ fnId = runtimeFunctionId(compName);
541
+ const topFuncs = topFuncSrc;
409
542
 
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 = {};
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});`
549
+ : `${v.keyword} ${v.name};`
550
+ ).join("\n") + "\n";
418
551
  }
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 });
552
+ if (bindings.length > 0) {
553
+ injectCode += "\n" + bindings.map(b => {
554
+ const accessor = b.prop === "self" ? "" : "." + b.prop;
555
+ return "let " + b.varName + " = self.querySelector('[data-chbind-" + b.bindId + "]')" + accessor + ";";
556
+ }).join("\n") + "\n";
557
+ }
558
+ if (topFuncs.length > 0) {
559
+ injectCode += "\n" + topFuncs.join("\n\n") + "\n";
560
+ }
561
+ runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
562
+
563
+ runtime = runtime.replace(`${RUNTIME_KW}()`, `${fnId}(self, ctx)`);
564
+ csrRuntimeSource = runtime.replace(/^function\s+\w+/, "function");
565
+ cx.runtimeChunks.push(runtime);
566
+ cx.runtimeMap && cx.runtimeMap.set(compName, { fnId });
434
567
  } else {
435
- letter = letterEntry.letter;
568
+ fnId = fnEntry.fnId;
436
569
  }
437
570
 
438
- runtimeChunks.push(`${letter}RUNTIME(document.querySelector('[chid="${compId}"]'), ${JSON.stringify(ctx)});`);
571
+ cx.runtimeChunks.push(`${fnId}(document.querySelector('[chid="${compId}"]'), ${JSON.stringify(ctx)});`);
439
572
  }
440
573
  }
574
+ }
441
575
 
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
- }
576
+ let cssId = cx.cssScopesMap && cx.cssScopesMap.get(compName);
577
+ if (!cssId) {
578
+ cssId = deterministicHash(compName, 8);
579
+ cx.cssScopesMap.set(compName, cssId);
580
+ }
581
+ if (fragment.children.length === 1 && firstChild.nodeType === 1) {
582
+ firstChild.classList.add(cssId);
583
+ }
584
+ if (styles) {
585
+ styles = scopeCss(styles, cssId);
586
+ cx.scopedStyles.push(styles);
587
+ }
455
588
 
456
- if (fragment.firstChild && fragment.firstChild.nodeType === 1) {
457
- fragment.firstChild.setAttribute("data-ch-source", instance.__sourceFile || compName);
458
- }
589
+ if (csrRuntimeSource && !cx.csrClasses.has(compName)) {
590
+ generateCSRClass(compName, cx);
591
+ }
459
592
 
460
- element.replaceWith(fragment);
461
- return true;
593
+ if (element.style.display === "none" && firstChild && firstChild.nodeType === 1) {
594
+ firstChild.style.display = "none";
462
595
  }
463
596
 
464
- console.warn(chalk.yellow(`${compName} — component could not be loaded (missing body or styles export)`));
465
- return false;
597
+ element.replaceWith(fragment);
598
+ return firstChild && firstChild.nodeType === 1 ? firstChild : true;
466
599
  }
467
600
 
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();
601
+ export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent, globalCtx = {}) {
602
+ const cx = new ProcessContext(
603
+ loadedComponents, [], [], new Map(), [], new Map(), [], new Map(), new Map()
604
+ );
605
+
606
+ 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);
616
+ }
617
+ }
618
+ }
619
+ }
487
620
 
488
621
  appElements.forEach(el => {
489
- processComponentElement(el, loadedComponents, runtimeChunks, compIdColl, letterState, runtimeMap, cssScopes, cssScopesMap, scopedStyles, [], staticCtxRegistry, pageSourceFile, pageSourceContent);
622
+ if (!el.isConnected) return;
623
+ processComponentElement(el, cx, [], pageSourceFile, pageSourceContent, globalCtx);
490
624
  });
491
- const runtimeScript = runtimeChunks.join("\n");
492
- const hasComponents = runtimeChunks.length > 0;
493
- const scopesCss = beautify.css(scopedStyles.join("\n"));
625
+ const runtimeScript = cx.runtimeChunks.join("\n");
626
+ const hasComponents = cx.runtimeChunks.length > 0;
627
+ const scopesCss = cx.scopedStyles.join("\n");
628
+ const csrClasses = [...cx.csrClasses.values()].join("\n\n");
629
+
630
+ const hashMap = {};
631
+ for (const [compName, hash] of cx.cssScopesMap) {
632
+ hashMap[compName] = hash;
633
+ }
494
634
 
495
- return { runtimeScript, hasComponents, scopesCss };
635
+ return { runtimeScript, hasComponents, scopesCss, hashMap, csrClasses };
496
636
  }
497
637
 
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
- }
638
+ const RUNTIME_KW = "$runtime";