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