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.
- package/LICENSE +1 -1
- package/README.md +45 -35
- package/bin/chocola.js +227 -0
- package/compiler/chalk.js +34 -0
- package/compiler/component-processor.js +771 -402
- package/compiler/config.js +43 -66
- package/compiler/dom-processor.js +12 -75
- package/compiler/fs.js +0 -11
- package/compiler/index.js +87 -230
- package/compiler/module-graph.js +257 -0
- package/compiler/pipeline.js +56 -196
- package/compiler/render.js +183 -0
- package/compiler/runtime-generator.js +20 -15
- package/compiler/utils.js +79 -126
- package/dev/index.js +54 -22
- package/package.json +23 -12
- package/parser/component.js +340 -0
- package/parser/context.js +59 -0
- package/parser/css.js +96 -0
- package/parser/index.js +6 -0
- package/parser/script.js +485 -0
- package/parser/script.test.js +103 -0
- package/parser/template.js +120 -0
- package/parser/utils.js +32 -0
- package/runtime/index.js +312 -0
- package/server/index.js +432 -0
- package/utils.js +113 -8
- package/index.js +0 -0
|
@@ -1,510 +1,879 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
112
|
-
}
|
|
113
|
-
function getDelIfAttr(el) {
|
|
114
|
-
return el.getAttribute("del:if") ?? el.getAttribute("del-if");
|
|
198
|
+
return counts;
|
|
115
199
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
200
|
+
|
|
201
|
+
function escapeNameForRegex(name) {
|
|
202
|
+
return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
119
203
|
}
|
|
120
204
|
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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 + ".
|
|
227
|
-
const
|
|
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 =
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
454
|
+
const fragment = parseFragment(template, doc);
|
|
455
|
+
|
|
456
|
+
warnUnusedDeclarations(cx, compName, instance, script, fragment);
|
|
248
457
|
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
parent:
|
|
263
|
-
}
|
|
264
|
-
const
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
const
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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.
|
|
319
|
-
condChain.active = false;
|
|
552
|
+
child.remove();
|
|
320
553
|
condChain.rendered = false;
|
|
321
554
|
}
|
|
322
|
-
|
|
555
|
+
} else {
|
|
556
|
+
child.replaceWith(...child.children);
|
|
557
|
+
condChain.active = false;
|
|
558
|
+
condChain.rendered = false;
|
|
323
559
|
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
324
562
|
|
|
325
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
399
|
-
});
|
|
619
|
+
applyConditionalToElement(condTarget, ctxProxy, condChain, hasIf, hasDelIf, hasElif, hasElse);
|
|
400
620
|
|
|
401
|
-
|
|
621
|
+
interpolateNode(condTarget, ctxProxy)
|
|
622
|
+
});
|
|
402
623
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
778
|
+
fnId = fnEntry.fnId;
|
|
436
779
|
}
|
|
437
780
|
|
|
438
|
-
runtimeChunks.push(`${
|
|
781
|
+
cx.runtimeChunks.push(`${fnId}(document.querySelector('[chid="${compId}"]'), ${JSON.stringify(ctx)});`);
|
|
439
782
|
}
|
|
440
783
|
}
|
|
784
|
+
}
|
|
441
785
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
799
|
+
if (csrRuntimeSource && !cx.csrClasses.has(compName)) {
|
|
800
|
+
generateCSRClass(compName, cx);
|
|
801
|
+
}
|
|
459
802
|
|
|
460
|
-
|
|
461
|
-
|
|
803
|
+
if (element.style.display === "none" && firstChild && firstChild.nodeType === 1) {
|
|
804
|
+
firstChild.style.display = "none";
|
|
462
805
|
}
|
|
463
806
|
|
|
464
|
-
|
|
465
|
-
return
|
|
807
|
+
element.replaceWith(fragment);
|
|
808
|
+
return firstChild && firstChild.nodeType === 1 ? firstChild : true;
|
|
466
809
|
}
|
|
467
810
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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 =
|
|
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";
|