chocola 2.0.0-next.8 → 2.0.0-next.9
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/README.md +4 -1
- package/compiler/component-processor.js +167 -18
- package/compiler/config.js +2 -2
- package/compiler/dom-processor.js +4 -2
- package/compiler/index.js +48 -16
- package/compiler/utils.js +78 -0
- package/dev/index.js +2 -2
- package/package.json +1 -1
- package/parser/component.js +140 -1
- package/parser/context.js +6 -6
- package/parser/css.js +2 -2
- package/parser/index.js +3 -3
- package/parser/template.js +14 -14
- package/parser/utils.js +17 -1
- package/runtime/index.js +25 -25
- package/utils.js +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/banner_dark.jpg">
|
|
3
|
+
<img src="assets/banner.jpg" alt="Chocola • The sweetest way to build the web" />
|
|
4
|
+
</picture>
|
|
2
5
|
|
|
3
6
|
## What is Chocola
|
|
4
7
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { parseHTML } from "linkedom";
|
|
3
3
|
import { protectCurlyBraces } from "../utils.js";
|
|
4
|
-
import { genRandomId, incrementAlfabet, throwError, deterministicHash } from "./utils.js";
|
|
4
|
+
import { genRandomId, incrementAlfabet, throwError, deterministicHash, warnConstantCondition, warnUnusedDeclaration, findElementLine } from "./utils.js";
|
|
5
5
|
import {
|
|
6
|
-
extractPropsDefaults, extractRuntime, extractTopLevelFunctions,
|
|
7
|
-
|
|
6
|
+
extractPropsDefaults, extractRuntime, extractTopLevelFunctions, extractTopLevelVariables,
|
|
7
|
+
extractCtxFromEl, hasMountIf, getMountIf,
|
|
8
8
|
reservedAttrs, validateChainStructure, applyConditionalToElement, interpolateNode,
|
|
9
|
-
scopeCss,
|
|
9
|
+
scopeCss, compileExpr, evaluateConstant,
|
|
10
10
|
} from "../parser/index.js";
|
|
11
11
|
import chalk from "./chalk.js";
|
|
12
12
|
|
|
@@ -24,6 +24,7 @@ class ProcessContext {
|
|
|
24
24
|
this.scopedStyles = scopedStyles;
|
|
25
25
|
this.staticCtxRegistry = staticCtxRegistry;
|
|
26
26
|
this.csrClasses = csrClasses;
|
|
27
|
+
this.unusedWarned = new Set();
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -59,6 +60,7 @@ function generateCSRClass(compName, cx, explicitClassName) {
|
|
|
59
60
|
|
|
60
61
|
const compProps = extractPropsDefaults(script);
|
|
61
62
|
const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
|
|
63
|
+
const topVars = extractTopLevelVariables(script || "");
|
|
62
64
|
let runtime = extractRuntime(script || "", compName);
|
|
63
65
|
|
|
64
66
|
if (!cx.cssScopesMap.has(compName)) {
|
|
@@ -93,15 +95,25 @@ function generateCSRClass(compName, cx, explicitClassName) {
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
let csrRuntimeSource = null;
|
|
98
|
+
const injectedNames = new Set(compProps.map(p => p.name));
|
|
99
|
+
for (const varName of bindVarNames) injectedNames.add(varName);
|
|
100
|
+
const topVarsToInject = topVars.filter(v => !injectedNames.has(v.name));
|
|
96
101
|
if (runtime) {
|
|
97
102
|
let injectCode = "";
|
|
98
103
|
for (const { name, defaultValue } of compProps) {
|
|
99
104
|
if (defaultValue !== undefined) {
|
|
100
|
-
injectCode += `let ${name} = ctx.${name}
|
|
105
|
+
injectCode += `let ${name} = ctx.${name}??(${defaultValue});\n`;
|
|
101
106
|
} else {
|
|
102
107
|
injectCode += `let ${name} = ctx.${name};\n`;
|
|
103
108
|
}
|
|
104
109
|
}
|
|
110
|
+
for (const { keyword, name, value } of topVarsToInject) {
|
|
111
|
+
if (value !== undefined) {
|
|
112
|
+
injectCode += `${keyword} ${name} = ctx.${name}??(${value});\n`;
|
|
113
|
+
} else {
|
|
114
|
+
injectCode += `${keyword} ${name};\n`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
105
117
|
for (const varName of bindVarNames) {
|
|
106
118
|
if (varName !== "self") {
|
|
107
119
|
injectCode += `let ${varName} = ctx.${varName};\n`;
|
|
@@ -116,20 +128,110 @@ function generateCSRClass(compName, cx, explicitClassName) {
|
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
const className = explicitClassName || compName.replace(".html", "").replace(/^\w/, c => c.toUpperCase());
|
|
119
|
-
const
|
|
131
|
+
const propsParts = [];
|
|
120
132
|
for (const { name, defaultValue } of compProps) {
|
|
121
|
-
|
|
133
|
+
propsParts.push(`${JSON.stringify(name)}: ${defaultValue !== undefined ? defaultValue : "null"}`);
|
|
134
|
+
}
|
|
135
|
+
for (const { name, value } of topVarsToInject) {
|
|
136
|
+
if (value !== undefined) {
|
|
137
|
+
propsParts.push(`${JSON.stringify(name)}: ${value}`);
|
|
138
|
+
}
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
const childrenPart = childMappings.length > 0
|
|
125
142
|
? ",\n children: [" + childMappings.map(m => `{tag:"${m.tag}",compClass:${m.compClass}}`).join(",") + "]"
|
|
126
143
|
: "";
|
|
127
144
|
const runtimePart = csrRuntimeSource ? `,\n runtime: ${csrRuntimeSource}` : "";
|
|
128
|
-
const
|
|
145
|
+
const propsPart = propsParts.length > 0 ? `{ ${propsParts.join(", ")} }` : "{}";
|
|
146
|
+
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}`;
|
|
129
147
|
cx.csrClasses.set(compName, classDef);
|
|
130
148
|
}
|
|
131
149
|
|
|
150
|
+
const BARE_IDENTIFIER_RE = /(?<![.\w$])[a-zA-Z_$][0-9a-zA-Z_$]*/g;
|
|
151
|
+
const PROPERTY_IDENTIFIER_RE = /\.([a-zA-Z_$][0-9a-zA-Z_$]*)/g;
|
|
132
152
|
|
|
153
|
+
function countIdentifiers(text, counts) {
|
|
154
|
+
for (const re of [BARE_IDENTIFIER_RE, PROPERTY_IDENTIFIER_RE]) {
|
|
155
|
+
re.lastIndex = 0;
|
|
156
|
+
for (const m of text.matchAll(re)) counts.set(m[1] ?? m[0], (counts.get(m[1] ?? m[0]) || 0) + 1);
|
|
157
|
+
}
|
|
158
|
+
return counts;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function escapeNameForRegex(name) {
|
|
162
|
+
return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function findLineInSource(source, regex) {
|
|
166
|
+
const match = regex.exec(source);
|
|
167
|
+
if (!match) return null;
|
|
168
|
+
return source.substring(0, match.index).split("\n").length;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function warnUnusedDeclarations(cx, compName, instance, script, fragment) {
|
|
172
|
+
if (!script && !fragment.querySelector("[bind\\:]")) return;
|
|
173
|
+
if (cx.unusedWarned.has(compName)) return;
|
|
174
|
+
cx.unusedWarned.add(compName);
|
|
175
|
+
|
|
176
|
+
const props = extractPropsDefaults(script).map(p => p.name);
|
|
177
|
+
const topVars = extractTopLevelVariables(script).map(v => v.name);
|
|
178
|
+
const topFuncs = extractTopLevelFunctions(script, RUNTIME_KW)
|
|
179
|
+
.map(src => src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/)?.[1])
|
|
180
|
+
.filter(Boolean);
|
|
181
|
+
|
|
182
|
+
const bindings = [];
|
|
183
|
+
for (const el of fragment.querySelectorAll("*")) {
|
|
184
|
+
for (const attr of el.attributes) {
|
|
185
|
+
if (attr.name.startsWith("bind:")) bindings.push({ name: attr.value, element: el });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const counts = new Map();
|
|
190
|
+
if (script) countIdentifiers(script, counts);
|
|
191
|
+
|
|
192
|
+
const walk = (node) => {
|
|
193
|
+
if (node.nodeType === 3) {
|
|
194
|
+
for (const m of node.textContent.matchAll(/\{([^}]+)\}/g)) countIdentifiers(m[1], counts);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (node.attributes) {
|
|
198
|
+
for (const attr of node.attributes) {
|
|
199
|
+
if (attr.name.startsWith("bind:")) {
|
|
200
|
+
countIdentifiers(attr.value, counts);
|
|
201
|
+
} else {
|
|
202
|
+
for (const m of attr.value.matchAll(/\{([^}]+)\}/g)) countIdentifiers(m[1], counts);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const child of node.childNodes) walk(child);
|
|
207
|
+
};
|
|
208
|
+
walk(fragment);
|
|
209
|
+
|
|
210
|
+
const isUnused = (name) => (counts.get(name) || 0) <= 1;
|
|
211
|
+
const warn = (kind, name, declRegex) => {
|
|
212
|
+
const lineNum = findLineInSource(instance, declRegex);
|
|
213
|
+
warnUnusedDeclaration(lineNum !== null ? `${compName}:${lineNum}` : compName, kind, name);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
for (const name of props) {
|
|
217
|
+
if (isUnused(name)) warn("prop", name, new RegExp("export\\s+let\\s+" + escapeNameForRegex(name) + "\\b"));
|
|
218
|
+
}
|
|
219
|
+
for (const name of topVars) {
|
|
220
|
+
if (isUnused(name)) warn("variable", name, new RegExp("(?:^|[^\\w])(?:let|const)\\s+" + escapeNameForRegex(name) + "\\b"));
|
|
221
|
+
}
|
|
222
|
+
for (const name of topFuncs) {
|
|
223
|
+
if (isUnused(name)) warn("function", name, new RegExp("(?:async\\s+)?function\\s+" + escapeNameForRegex(name) + "\\b"));
|
|
224
|
+
}
|
|
225
|
+
const seenBindings = new Set();
|
|
226
|
+
for (const { name, element } of bindings) {
|
|
227
|
+
if (seenBindings.has(name)) continue;
|
|
228
|
+
seenBindings.add(name);
|
|
229
|
+
if (isUnused(name)) {
|
|
230
|
+
const lineNum = findElementLine(instance, element.outerHTML);
|
|
231
|
+
warnUnusedDeclaration(lineNum !== null ? `${compName}:${lineNum}` : compName, "binding", name);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
133
235
|
|
|
134
236
|
export function processComponentElement(
|
|
135
237
|
element,
|
|
@@ -174,7 +276,7 @@ export function processComponentElement(
|
|
|
174
276
|
if (cx.staticCtxRegistry && cx.staticCtxRegistry.has(element)) {
|
|
175
277
|
ctx = cx.staticCtxRegistry.get(element);
|
|
176
278
|
} else {
|
|
177
|
-
ctx =
|
|
279
|
+
ctx = extractCtxFromEl(element);
|
|
178
280
|
cx.staticCtxRegistry && cx.staticCtxRegistry.set(element, ctx);
|
|
179
281
|
}
|
|
180
282
|
|
|
@@ -182,7 +284,7 @@ export function processComponentElement(
|
|
|
182
284
|
compProps.forEach(({ name, defaultValue }) => {
|
|
183
285
|
if (defaultValue !== undefined && !(name in ctx)) {
|
|
184
286
|
try {
|
|
185
|
-
ctx[name] =
|
|
287
|
+
ctx[name] = compileExpr(defaultValue, false)();
|
|
186
288
|
} catch {
|
|
187
289
|
ctx[name] = defaultValue;
|
|
188
290
|
}
|
|
@@ -191,6 +293,15 @@ export function processComponentElement(
|
|
|
191
293
|
}
|
|
192
294
|
|
|
193
295
|
const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
|
|
296
|
+
const topVars = extractTopLevelVariables(script || "");
|
|
297
|
+
for (const { name, value } of topVars) {
|
|
298
|
+
if (name in ctx) continue;
|
|
299
|
+
if (value !== undefined) {
|
|
300
|
+
try {
|
|
301
|
+
ctx[name] = compileExpr(value, false)();
|
|
302
|
+
} catch {}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
194
305
|
for (const src of topFuncSrc) {
|
|
195
306
|
try {
|
|
196
307
|
const fn = (0, eval)("(" + src + ")");
|
|
@@ -208,6 +319,8 @@ export function processComponentElement(
|
|
|
208
319
|
|
|
209
320
|
const fragment = parseFragment(template, doc);
|
|
210
321
|
|
|
322
|
+
warnUnusedDeclarations(cx, compName, instance, script, fragment);
|
|
323
|
+
|
|
211
324
|
const slotFragment = parseFragment(elInnerHtml, doc);
|
|
212
325
|
if (sourceFile) {
|
|
213
326
|
validateChainStructure(slotFragment, sourceFile, sourceContent, elInnerHtml);
|
|
@@ -226,6 +339,21 @@ export function processComponentElement(
|
|
|
226
339
|
const elBindIds = new Map();
|
|
227
340
|
let bindCounter = 0;
|
|
228
341
|
|
|
342
|
+
const conditionalLocations = new Map();
|
|
343
|
+
for (const { el } of childEntries) {
|
|
344
|
+
if (!el.hasAttribute("if") && !hasMountIf(el) && !el.hasAttribute("elif")) continue;
|
|
345
|
+
let location = sourceFile;
|
|
346
|
+
if (instance) {
|
|
347
|
+
const lineNum = findElementLine(instance, el.outerHTML);
|
|
348
|
+
if (lineNum !== null) location = `${compName}:${lineNum}`;
|
|
349
|
+
}
|
|
350
|
+
if (location === sourceFile && sourceContent) {
|
|
351
|
+
const lineNum = findElementLine(sourceContent, el.outerHTML);
|
|
352
|
+
if (lineNum !== null) location = `${sourceFile}:${lineNum}`;
|
|
353
|
+
}
|
|
354
|
+
conditionalLocations.set(el, location);
|
|
355
|
+
}
|
|
356
|
+
|
|
229
357
|
childEntries.forEach(({ el: child, parent }) => {
|
|
230
358
|
if (!condChains.has(parent)) {
|
|
231
359
|
condChains.set(parent, { active: false, rendered: false });
|
|
@@ -233,13 +361,26 @@ export function processComponentElement(
|
|
|
233
361
|
const condChain = condChains.get(parent);
|
|
234
362
|
|
|
235
363
|
const hasIf = child.hasAttribute("if");
|
|
236
|
-
const hasDelIf =
|
|
364
|
+
const hasDelIf = hasMountIf(child);
|
|
237
365
|
const hasElif = child.hasAttribute("elif");
|
|
238
366
|
const hasElse = child.hasAttribute("else");
|
|
239
367
|
|
|
368
|
+
if (hasIf || hasDelIf || hasElif) {
|
|
369
|
+
const location = conditionalLocations.get(child) || sourceFile;
|
|
370
|
+
const stripBraces = (raw) => raw.startsWith("{") ? raw.slice(1, -1) : raw;
|
|
371
|
+
const tag = child.tagName.toLowerCase();
|
|
372
|
+
const warnIfConstant = (expr, attr) => {
|
|
373
|
+
const { constant, value } = evaluateConstant(expr);
|
|
374
|
+
if (constant) warnConstantCondition(location, tag, attr, Boolean(value));
|
|
375
|
+
};
|
|
376
|
+
if (hasIf) warnIfConstant(stripBraces(child.getAttribute("if")), "if");
|
|
377
|
+
if (hasDelIf) warnIfConstant(stripBraces(getMountIf(child)), "mount:if");
|
|
378
|
+
if (hasElif) warnIfConstant(stripBraces(child.getAttribute("elif")), "elif");
|
|
379
|
+
}
|
|
380
|
+
|
|
240
381
|
if (hasElif || hasElse) {
|
|
241
382
|
if (!condChain.active) {
|
|
242
|
-
throwError(`${instance.__sourceFile || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/
|
|
383
|
+
throwError(`${instance.__sourceFile || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/mount:if sibling`);
|
|
243
384
|
}
|
|
244
385
|
if (condChain.rendered) {
|
|
245
386
|
child.remove();
|
|
@@ -254,7 +395,7 @@ export function processComponentElement(
|
|
|
254
395
|
if (hasElif || hasElse) {
|
|
255
396
|
if (hasElif) {
|
|
256
397
|
const expr = child.getAttribute("elif").slice(1, -1);
|
|
257
|
-
const fn =
|
|
398
|
+
const fn = compileExpr(expr, true);
|
|
258
399
|
if (!fn(ctxProxy)) {
|
|
259
400
|
child.remove();
|
|
260
401
|
return;
|
|
@@ -266,9 +407,9 @@ export function processComponentElement(
|
|
|
266
407
|
condChain.active = false;
|
|
267
408
|
}
|
|
268
409
|
} else if (hasIf || hasDelIf) {
|
|
269
|
-
const raw = hasIf ? child.getAttribute("if") :
|
|
410
|
+
const raw = hasIf ? child.getAttribute("if") : getMountIf(child);
|
|
270
411
|
const expr = raw.slice(1, -1);
|
|
271
|
-
const fn =
|
|
412
|
+
const fn = compileExpr(expr, true);
|
|
272
413
|
condChain.active = true;
|
|
273
414
|
if (fn(ctxProxy)) {
|
|
274
415
|
child.replaceWith(...child.children);
|
|
@@ -309,7 +450,7 @@ export function processComponentElement(
|
|
|
309
450
|
/\{([^}]+)\}/g,
|
|
310
451
|
(_, expr) => {
|
|
311
452
|
try {
|
|
312
|
-
return
|
|
453
|
+
return compileExpr(expr, true)(ctxProxy);
|
|
313
454
|
} catch {
|
|
314
455
|
return "";
|
|
315
456
|
}
|
|
@@ -320,7 +461,7 @@ export function processComponentElement(
|
|
|
320
461
|
|
|
321
462
|
const condAttrs = {};
|
|
322
463
|
if (hasIf) condAttrs["if"] = child.getAttribute("if");
|
|
323
|
-
if (hasDelIf) condAttrs["mount:if"] =
|
|
464
|
+
if (hasDelIf) condAttrs["mount:if"] = getMountIf(child);
|
|
324
465
|
if (hasElif) condAttrs["elif"] = child.getAttribute("elif");
|
|
325
466
|
if (hasElse) condAttrs["else"] = "";
|
|
326
467
|
|
|
@@ -373,7 +514,7 @@ export function processComponentElement(
|
|
|
373
514
|
for (const { name, defaultValue } of compProps) {
|
|
374
515
|
if (declared.has(name)) continue;
|
|
375
516
|
if (defaultValue !== undefined) {
|
|
376
|
-
ctxDefParts.push(`let ${name} = ctx.${name}
|
|
517
|
+
ctxDefParts.push(`let ${name} = ctx.${name}??(${defaultValue});\n`);
|
|
377
518
|
} else {
|
|
378
519
|
ctxDefParts.push(`let ${name} = ctx.${name};\n`);
|
|
379
520
|
}
|
|
@@ -393,6 +534,14 @@ export function processComponentElement(
|
|
|
393
534
|
const topFuncs = topFuncSrc;
|
|
394
535
|
|
|
395
536
|
let injectCode = ctxDef;
|
|
537
|
+
for (const b of bindings) declared.add(b.varName);
|
|
538
|
+
const undeclaredTopVars = topVars.filter(v => !declared.has(v.name));
|
|
539
|
+
if (undeclaredTopVars.length > 0) {
|
|
540
|
+
injectCode += "\n" + undeclaredTopVars.map(v => v.value !== undefined
|
|
541
|
+
? `${v.keyword} ${v.name} = ctx.${v.name}??(${v.value});`
|
|
542
|
+
: `${v.keyword} ${v.name};`
|
|
543
|
+
).join("\n") + "\n";
|
|
544
|
+
}
|
|
396
545
|
if (bindings.length > 0) {
|
|
397
546
|
injectCode += "\n" + bindings.map(b => {
|
|
398
547
|
const accessor = b.prop === "self" ? "" : "." + b.prop;
|
package/compiler/config.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import {
|
|
2
|
+
import { getConfig } from "../utils.js";
|
|
3
3
|
|
|
4
4
|
export async function loadConfig(rootDir) {
|
|
5
|
-
const config = await
|
|
5
|
+
const config = await getConfig(rootDir);
|
|
6
6
|
const bundleConfig = config.bundle || {};
|
|
7
7
|
|
|
8
8
|
const srcDir = bundleConfig.srcDir || "src";
|
|
@@ -2,12 +2,14 @@ import { parseHTML } from "linkedom";
|
|
|
2
2
|
import { promises as fs } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { protectCurlyBraces, restoreCurlyBraces } from "../utils.js";
|
|
5
|
-
import { extractContextFromElement } from "../parser/context.js";
|
|
6
5
|
import { throwError } from "./utils.js";
|
|
7
6
|
import { readMyFile } from "./fs.js";
|
|
8
7
|
|
|
9
8
|
export function createDOM(srcIndexContent) {
|
|
10
|
-
|
|
9
|
+
const protectedContent = protectCurlyBraces(srcIndexContent);
|
|
10
|
+
const dom = parseHTML(protectedContent);
|
|
11
|
+
dom.protectedContent = protectedContent;
|
|
12
|
+
return dom;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export function validateAppContainer(doc) {
|
package/compiler/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { performance } from "perf_hooks";
|
|
3
4
|
import chalk from "./chalk.js";
|
|
4
5
|
import { loadConfig, resolvePaths } from "./config.js";
|
|
5
6
|
import {
|
|
@@ -16,8 +17,8 @@ import {
|
|
|
16
17
|
} from "./dom-processor.js";
|
|
17
18
|
import { processAllComponents } from "./component-processor.js";
|
|
18
19
|
import { generateRuntimeScript } from "./runtime-generator.js";
|
|
19
|
-
import { genRandomId, throwError } from "./utils.js";
|
|
20
|
-
import {
|
|
20
|
+
import { genRandomId, throwError, warnConstantCondition, findElementLine } from "./utils.js";
|
|
21
|
+
import { compileExpr, evaluateConstant, hasMountIf, getMountIf, removeMountIf } from "../parser/index.js";
|
|
21
22
|
import {
|
|
22
23
|
copyStaticDir,
|
|
23
24
|
getComponents,
|
|
@@ -56,12 +57,17 @@ function logBanner() {
|
|
|
56
57
|
);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function logSuccess(outDirPath) {
|
|
60
|
+
function logSuccess(outDirPath, durationMs) {
|
|
60
61
|
console.log(
|
|
61
62
|
chalk.bold.green(">"),
|
|
62
63
|
"Project bundled succesfully at",
|
|
63
64
|
chalk.green.underline(outDirPath));
|
|
64
|
-
console.log(chalk.bold.green(`\nJOB DONE
|
|
65
|
+
console.log(chalk.bold.green(`\nJOB DONE!`) + chalk.hex(TEXT_FAINT)(` (${formatDuration(durationMs)})\n`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatDuration(ms) {
|
|
69
|
+
if (ms >= 1000) return (ms / 1000).toFixed(2) + "s";
|
|
70
|
+
return Math.round(ms) + "ms";
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
async function setupOutputDirectory(outDirPath, emptyOutDir) {
|
|
@@ -107,26 +113,49 @@ async function processAssets(doc, rootDir, srcDir, outDirPath) {
|
|
|
107
113
|
return cssContents
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
function processPageConditionals(parent) {
|
|
116
|
+
function processPageConditionals(parent, sourceFile, sourceContent) {
|
|
111
117
|
const children = [...parent.children];
|
|
112
118
|
let chainActive = false;
|
|
113
119
|
let chainRendered = false;
|
|
114
120
|
|
|
121
|
+
const getLocation = (child) => {
|
|
122
|
+
if (!sourceContent) return sourceFile;
|
|
123
|
+
const lineNum = findElementLine(sourceContent, child.outerHTML);
|
|
124
|
+
return lineNum !== null ? `${sourceFile}:${lineNum}` : sourceFile;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const warnIfConstant = (expr, child, attr) => {
|
|
128
|
+
const { constant, value } = evaluateConstant(expr);
|
|
129
|
+
if (!constant) return;
|
|
130
|
+
warnConstantCondition(getLocation(child), child.tagName.toLowerCase(), attr, Boolean(value));
|
|
131
|
+
};
|
|
132
|
+
|
|
115
133
|
for (const child of children) {
|
|
116
134
|
const hasIf = child.hasAttribute("if");
|
|
117
|
-
const hasDelIf =
|
|
135
|
+
const hasDelIf = hasMountIf(child);
|
|
118
136
|
const hasElif = child.hasAttribute("elif");
|
|
119
137
|
const hasElse = child.hasAttribute("else");
|
|
120
138
|
|
|
139
|
+
if (hasIf || hasDelIf || hasElif) {
|
|
140
|
+
const stripBraces = (raw) => raw.startsWith("{") ? raw.slice(1, -1) : raw;
|
|
141
|
+
if (hasIf) warnIfConstant(stripBraces(child.getAttribute("if")), child, "if");
|
|
142
|
+
if (hasDelIf) warnIfConstant(stripBraces(getMountIf(child)), child, "mount:if");
|
|
143
|
+
if (hasElif) warnIfConstant(stripBraces(child.getAttribute("elif")), child, "elif");
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
if (hasElif || hasElse) {
|
|
122
|
-
if (!chainActive)
|
|
147
|
+
if (!chainActive) {
|
|
148
|
+
const tag = child.tagName.toLowerCase();
|
|
149
|
+
const attr = hasElif ? "elif" : "else";
|
|
150
|
+
throwError(`${getLocation(child)}\n <${tag}> has ${attr} without a preceding if/mount:if sibling`);
|
|
151
|
+
}
|
|
123
152
|
if (chainRendered) { child.remove(); continue; }
|
|
124
153
|
}
|
|
125
154
|
|
|
126
155
|
if (hasIf) {
|
|
127
156
|
const raw = child.getAttribute("if");
|
|
128
157
|
const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
|
|
129
|
-
const fn =
|
|
158
|
+
const fn = compileExpr(expr, false);
|
|
130
159
|
const result = fn();
|
|
131
160
|
chainActive = true;
|
|
132
161
|
if (result) {
|
|
@@ -137,9 +166,9 @@ function processPageConditionals(parent) {
|
|
|
137
166
|
}
|
|
138
167
|
child.removeAttribute("if");
|
|
139
168
|
} else if (hasDelIf) {
|
|
140
|
-
const raw =
|
|
169
|
+
const raw = getMountIf(child);
|
|
141
170
|
const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
|
|
142
|
-
const fn =
|
|
171
|
+
const fn = compileExpr(expr, false);
|
|
143
172
|
const result = fn();
|
|
144
173
|
chainActive = true;
|
|
145
174
|
if (result) {
|
|
@@ -148,11 +177,11 @@ function processPageConditionals(parent) {
|
|
|
148
177
|
child.remove();
|
|
149
178
|
chainRendered = false;
|
|
150
179
|
}
|
|
151
|
-
|
|
180
|
+
removeMountIf(child);
|
|
152
181
|
} else if (hasElif) {
|
|
153
182
|
const raw = child.getAttribute("elif");
|
|
154
183
|
const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
|
|
155
|
-
const fn =
|
|
184
|
+
const fn = compileExpr(expr, false);
|
|
156
185
|
const result = fn();
|
|
157
186
|
if (result) {
|
|
158
187
|
chainRendered = true;
|
|
@@ -170,13 +199,14 @@ function processPageConditionals(parent) {
|
|
|
170
199
|
}
|
|
171
200
|
|
|
172
201
|
if (child.parentNode) {
|
|
173
|
-
processPageConditionals(child);
|
|
202
|
+
processPageConditionals(child, sourceFile, sourceContent);
|
|
174
203
|
}
|
|
175
204
|
}
|
|
176
205
|
}
|
|
177
206
|
|
|
178
207
|
export default async function compile(rootDir, buildConfig) {
|
|
179
208
|
const isHotReload = buildConfig?.isHotReload || null;
|
|
209
|
+
const startTime = performance.now();
|
|
180
210
|
!isHotReload && logBanner();
|
|
181
211
|
|
|
182
212
|
const config = await loadConfig(rootDir);
|
|
@@ -195,7 +225,7 @@ export default async function compile(rootDir, buildConfig) {
|
|
|
195
225
|
const doc = dom.document;
|
|
196
226
|
const appContainer = validateAppContainer(doc);
|
|
197
227
|
|
|
198
|
-
processPageConditionals(appContainer);
|
|
228
|
+
processPageConditionals(appContainer, pageSourcePath, dom.protectedContent);
|
|
199
229
|
|
|
200
230
|
const appElements = getAppElements(appContainer);
|
|
201
231
|
const { runtimeScript, scopesCss, hashMap, csrClasses } = processAllComponents(appElements, loadedComponents, pageSourcePath, srcIndexContent);
|
|
@@ -225,8 +255,10 @@ export default async function compile(rootDir, buildConfig) {
|
|
|
225
255
|
await fs.mkdir(chocolaDir, { recursive: true });
|
|
226
256
|
await fs.writeFile(path.join(chocolaDir, "hashes.json"), JSON.stringify(hashMap, null, 2) + "\n");
|
|
227
257
|
|
|
228
|
-
|
|
229
|
-
|
|
258
|
+
const durationMs = performance.now() - startTime;
|
|
259
|
+
|
|
260
|
+
!isHotReload && logSuccess(paths.outDir, durationMs);
|
|
261
|
+
isHotReload && console.log("Dev server updated " + chalk.hex(TEXT_FAINT)(`(${formatDuration(durationMs)})`));
|
|
230
262
|
}
|
|
231
263
|
|
|
232
264
|
/**
|
package/compiler/utils.js
CHANGED
|
@@ -6,6 +6,84 @@ export function throwError(err) {
|
|
|
6
6
|
throw new Error(err);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export function warnConstantCondition(location, tag, attr, truthy) {
|
|
10
|
+
const article = /^[aeiou]/.test(attr) ? "an" : "a";
|
|
11
|
+
console.warn(
|
|
12
|
+
chalk.bold.yellow("WARNING!"),
|
|
13
|
+
`${location}: <${tag}> has ${article} \`${attr}\` condition that is always ${truthy ? "truthy" : "falsy"}`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function warnUnusedDeclaration(location, kind, name) {
|
|
18
|
+
console.warn(
|
|
19
|
+
chalk.bold.yellow("WARNING!"),
|
|
20
|
+
`${location}: ${kind} \`${name}\` is never used`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeAttributeQuotes(html) {
|
|
25
|
+
return html
|
|
26
|
+
.replace(/(\s[\w:.-]+)=(['"])([\s\S]*?)\2/g, '$1="$3"')
|
|
27
|
+
.replace(/(\s[\w:.-]+)=([^\s"'<>=]+)/g, '$1="$2"');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getStartTag(outerHTML) {
|
|
31
|
+
let inQuote = null;
|
|
32
|
+
for (let i = 0; i < outerHTML.length; i++) {
|
|
33
|
+
const c = outerHTML[i];
|
|
34
|
+
if (inQuote) {
|
|
35
|
+
if (c === inQuote) inQuote = null;
|
|
36
|
+
} else if (c === '"' || c === "'") {
|
|
37
|
+
inQuote = c;
|
|
38
|
+
} else if (c === ">") {
|
|
39
|
+
return outerHTML.slice(0, i + 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return outerHTML;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function escapeRegex(str) {
|
|
46
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function flexibleStartTagRegex(startTag) {
|
|
50
|
+
return new RegExp(
|
|
51
|
+
startTag
|
|
52
|
+
.split(/\s+/)
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.map(escapeRegex)
|
|
55
|
+
.join("\\s+")
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function findElementLine(sourceContent, outerHTML) {
|
|
60
|
+
const source = normalizeAttributeQuotes(sourceContent).toLowerCase().replace(/\/\s*>/g, ">");
|
|
61
|
+
const startTags = [
|
|
62
|
+
normalizeAttributeQuotes(getStartTag(outerHTML)),
|
|
63
|
+
normalizeAttributeQuotes(
|
|
64
|
+
getStartTag(
|
|
65
|
+
outerHTML
|
|
66
|
+
.replace(/"/g, '"')
|
|
67
|
+
.replace(/&/g, "&")
|
|
68
|
+
.replace(/</g, "<")
|
|
69
|
+
.replace(/>/g, ">")
|
|
70
|
+
)
|
|
71
|
+
),
|
|
72
|
+
].map(tag => tag.toLowerCase());
|
|
73
|
+
const candidates = [
|
|
74
|
+
flexibleStartTagRegex(startTags[0]),
|
|
75
|
+
flexibleStartTagRegex(startTags[0].replace(/=""/g, "")),
|
|
76
|
+
flexibleStartTagRegex(startTags[1]),
|
|
77
|
+
];
|
|
78
|
+
for (const re of candidates) {
|
|
79
|
+
const match = re.exec(source);
|
|
80
|
+
if (match) {
|
|
81
|
+
return source.substring(0, match.index).split("\n").length;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
9
87
|
export function genRandomId(collection = null, length = 10, lettersOnly = false) {
|
|
10
88
|
let id;
|
|
11
89
|
if (lettersOnly) {
|
package/dev/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "path";
|
|
|
4
4
|
import chalk from "../compiler/chalk.js";
|
|
5
5
|
import compile from "../compiler/index.js";
|
|
6
6
|
import { loadConfig, resolvePaths } from "../compiler/config.js";
|
|
7
|
-
import {
|
|
7
|
+
import { getConfig } from "../utils.js";
|
|
8
8
|
|
|
9
9
|
export async function serve(__rootdir) {
|
|
10
10
|
await compile(__rootdir);
|
|
@@ -17,7 +17,7 @@ export async function serve(__rootdir) {
|
|
|
17
17
|
|
|
18
18
|
let lastBuildTime = Date.now();
|
|
19
19
|
|
|
20
|
-
const fullConfig = await
|
|
20
|
+
const fullConfig = await getConfig(__rootdir);
|
|
21
21
|
const config = await loadConfig(__rootdir);
|
|
22
22
|
const paths = resolvePaths(__rootdir, config);
|
|
23
23
|
const devConfig = fullConfig.dev;
|
package/package.json
CHANGED
package/parser/component.js
CHANGED
|
@@ -44,7 +44,146 @@ export function extractRuntime(script, compName) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
throw new Error(`${compName} $runtime function has unclosed curly braces
|
|
47
|
+
throw new Error(`${compName} $runtime function has unclosed curly braces`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function extractTopLevelVariables(script) {
|
|
51
|
+
if (!script) return [];
|
|
52
|
+
const vars = [];
|
|
53
|
+
let i = 0;
|
|
54
|
+
let depth = 0;
|
|
55
|
+
let parenDepth = 0;
|
|
56
|
+
let bracketDepth = 0;
|
|
57
|
+
let inStr = false;
|
|
58
|
+
let strChar = null;
|
|
59
|
+
|
|
60
|
+
while (i < script.length) {
|
|
61
|
+
const ch = script[i];
|
|
62
|
+
|
|
63
|
+
if (inStr) {
|
|
64
|
+
if (ch === "\\") i++;
|
|
65
|
+
else if (ch === strChar) inStr = false;
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
71
|
+
inStr = true;
|
|
72
|
+
strChar = ch;
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (ch === "/" && script[i + 1] === "/") {
|
|
78
|
+
while (i < script.length && script[i] !== "\n") i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === "/" && script[i + 1] === "*") {
|
|
82
|
+
i += 2;
|
|
83
|
+
while (i < script.length && !(script[i] === "*" && script[i + 1] === "/")) i++;
|
|
84
|
+
i += 2;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (ch === "{") { depth++; i++; continue; }
|
|
89
|
+
if (ch === "}") { depth--; i++; continue; }
|
|
90
|
+
if (ch === "(") { parenDepth++; i++; continue; }
|
|
91
|
+
if (ch === ")") { parenDepth--; i++; continue; }
|
|
92
|
+
if (ch === "[") { bracketDepth++; i++; continue; }
|
|
93
|
+
if (ch === "]") { bracketDepth--; i++; continue; }
|
|
94
|
+
|
|
95
|
+
if (depth === 0 && parenDepth === 0 && bracketDepth === 0) {
|
|
96
|
+
const rest = script.slice(i);
|
|
97
|
+
const m = rest.match(/^(let|const)\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*/);
|
|
98
|
+
if (m) {
|
|
99
|
+
const keyword = m[1];
|
|
100
|
+
const before = script.slice(0, i).trimEnd();
|
|
101
|
+
if (!before.endsWith("export")) {
|
|
102
|
+
let name = m[2];
|
|
103
|
+
let p = i + m[0].length;
|
|
104
|
+
while (p < script.length && /\s/.test(script[p])) p++;
|
|
105
|
+
i = p;
|
|
106
|
+
|
|
107
|
+
while (p < script.length) {
|
|
108
|
+
i = p;
|
|
109
|
+
let q = p;
|
|
110
|
+
|
|
111
|
+
if (script[q] === "=") {
|
|
112
|
+
q++;
|
|
113
|
+
while (q < script.length && /\s/.test(script[q])) q++;
|
|
114
|
+
const delim = findDeclaratorEnd(script, q);
|
|
115
|
+
if (name !== "self" && name !== "ctx") {
|
|
116
|
+
vars.push({ keyword, name, value: script.slice(q, delim).trim() });
|
|
117
|
+
}
|
|
118
|
+
if (script[delim] === ";") { i = delim + 1; break; }
|
|
119
|
+
p = delim + 1;
|
|
120
|
+
while (p < script.length && /\s/.test(script[p])) p++;
|
|
121
|
+
} else {
|
|
122
|
+
if (name !== "self" && name !== "ctx") {
|
|
123
|
+
vars.push({ keyword, name, value: undefined });
|
|
124
|
+
}
|
|
125
|
+
if (script[q] === ";") { i = q + 1; break; }
|
|
126
|
+
if (script[q] !== ",") { i = q; break; }
|
|
127
|
+
p = q + 1;
|
|
128
|
+
while (p < script.length && /\s/.test(script[p])) p++;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const nameMatch = script.slice(p).match(/^([a-zA-Z_$][0-9a-zA-Z_$]*)\s*/);
|
|
132
|
+
if (!nameMatch) { i = p; break; }
|
|
133
|
+
name = nameMatch[1];
|
|
134
|
+
p += nameMatch[0].length;
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
i++;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return vars;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function findDeclaratorEnd(script, start) {
|
|
147
|
+
let i = start;
|
|
148
|
+
let parens = 0;
|
|
149
|
+
let brackets = 0;
|
|
150
|
+
let braces = 0;
|
|
151
|
+
let inStr = false;
|
|
152
|
+
let strChar = null;
|
|
153
|
+
|
|
154
|
+
while (i < script.length) {
|
|
155
|
+
const c = script[i];
|
|
156
|
+
|
|
157
|
+
if (inStr) {
|
|
158
|
+
if (c === "\\") i++;
|
|
159
|
+
else if (c === strChar) inStr = false;
|
|
160
|
+
i++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (c === "/" && script[i + 1] === "/") {
|
|
165
|
+
while (i < script.length && script[i] !== "\n") i++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (c === "/" && script[i + 1] === "*") {
|
|
169
|
+
i += 2;
|
|
170
|
+
while (i < script.length && !(script[i] === "*" && script[i + 1] === "/")) i++;
|
|
171
|
+
i += 2;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (c === '"' || c === "'" || c === "`") { inStr = true; strChar = c; i++; continue; }
|
|
176
|
+
if (c === "(") { parens++; i++; continue; }
|
|
177
|
+
if (c === ")") { parens--; i++; continue; }
|
|
178
|
+
if (c === "[") { brackets++; i++; continue; }
|
|
179
|
+
if (c === "]") { brackets--; i++; continue; }
|
|
180
|
+
if (c === "{") { braces++; i++; continue; }
|
|
181
|
+
if (c === "}") { braces--; i++; continue; }
|
|
182
|
+
if ((c === ";" || c === ",") && parens === 0 && brackets === 0 && braces === 0) return i;
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return i;
|
|
48
187
|
}
|
|
49
188
|
|
|
50
189
|
export function extractTopLevelFunctions(script, excludeName) {
|
package/parser/context.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compileExpr } from "./utils.js";
|
|
2
2
|
import { restoreTemplateChars } from "../utils.js";
|
|
3
3
|
|
|
4
|
-
export function
|
|
4
|
+
export function extractCtxFromEl(element) {
|
|
5
5
|
const ctx = {};
|
|
6
6
|
for (const attr of element.attributes) {
|
|
7
7
|
const key = attr.name;
|
|
@@ -10,7 +10,7 @@ export function extractContextFromElement(element) {
|
|
|
10
10
|
const matches = [...val.matchAll(/\{([^}]+)\}/g)];
|
|
11
11
|
if (matches.length === 1 && matches[0][0] === val) {
|
|
12
12
|
try {
|
|
13
|
-
ctx[key] =
|
|
13
|
+
ctx[key] = compileExpr(matches[0][1], false)();
|
|
14
14
|
continue;
|
|
15
15
|
} catch {}
|
|
16
16
|
}
|
|
@@ -19,12 +19,12 @@ export function extractContextFromElement(element) {
|
|
|
19
19
|
return ctx;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function
|
|
22
|
+
export function hasMountIf(el) {
|
|
23
23
|
return el.hasAttribute("mount:if");
|
|
24
24
|
}
|
|
25
|
-
export function
|
|
25
|
+
export function getMountIf(el) {
|
|
26
26
|
return el.getAttribute("mount:if");
|
|
27
27
|
}
|
|
28
|
-
export function
|
|
28
|
+
export function removeMountIf(el) {
|
|
29
29
|
el.removeAttribute("mount:if");
|
|
30
30
|
}
|
package/parser/css.js
CHANGED
|
@@ -38,7 +38,7 @@ export function scopeCss(cssString, cssId) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function
|
|
41
|
+
function findClosure(s, openIndex) {
|
|
42
42
|
let depth = 0;
|
|
43
43
|
for (let i = openIndex; i < s.length; i++) {
|
|
44
44
|
const ch = s[i];
|
|
@@ -65,7 +65,7 @@ export function scopeCss(cssString, cssId) {
|
|
|
65
65
|
break;
|
|
66
66
|
}
|
|
67
67
|
const header = str.substring(i, braceIndex);
|
|
68
|
-
const endBrace =
|
|
68
|
+
const endBrace = findClosure(str, braceIndex);
|
|
69
69
|
const inner = str.substring(braceIndex + 1, endBrace);
|
|
70
70
|
|
|
71
71
|
if (header.trim().startsWith("@")) {
|
package/parser/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { extractPropsDefaults, extractRuntime, extractTopLevelFunctions } from "./component.js";
|
|
2
|
-
export {
|
|
1
|
+
export { extractPropsDefaults, extractRuntime, extractTopLevelFunctions, extractTopLevelVariables } from "./component.js";
|
|
2
|
+
export { extractCtxFromEl, hasMountIf, getMountIf, removeMountIf } from "./context.js";
|
|
3
3
|
export { reservedAttrs, getLineNumber, validateChainStructure, applyConditionalToElement, interpolateNode } from "./template.js";
|
|
4
4
|
export { scopeCss } from "./css.js";
|
|
5
|
-
export {
|
|
5
|
+
export { compileExpr, evaluateConstant } from "./utils.js";
|
package/parser/template.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { compileExpr } from "./utils.js";
|
|
2
|
+
import { hasMountIf, getMountIf, removeMountIf } from "./context.js";
|
|
3
3
|
|
|
4
4
|
export const reservedAttrs = ["if", "mount:if", "elif", "else"];
|
|
5
5
|
|
|
6
|
-
export function getLineNumber(
|
|
7
|
-
if (
|
|
6
|
+
export function getLineNumber(srcContent, parentContent, idxInParent) {
|
|
7
|
+
if (srcContent === parentContent) {
|
|
8
8
|
return parentContent.substring(0, idxInParent).split("\n").length;
|
|
9
9
|
}
|
|
10
|
-
const templateStartMatch =
|
|
10
|
+
const templateStartMatch = srcContent.match(/<template[^>]*>/);
|
|
11
11
|
if (templateStartMatch) {
|
|
12
12
|
const contentStart = templateStartMatch.index + templateStartMatch[0].length;
|
|
13
|
-
const beforeContent =
|
|
13
|
+
const beforeContent = srcContent.substring(0, contentStart);
|
|
14
14
|
const linesInBefore = beforeContent.split("\n").length;
|
|
15
15
|
const linesInTemplate = parentContent.substring(0, idxInParent).split("\n").length;
|
|
16
16
|
return linesInBefore + linesInTemplate - 1;
|
|
@@ -24,7 +24,7 @@ export function validateChainStructure(parent, sourceFile, sourceContent, parent
|
|
|
24
24
|
|
|
25
25
|
for (const child of children) {
|
|
26
26
|
const hasIf = child.hasAttribute("if");
|
|
27
|
-
const hasDelIf =
|
|
27
|
+
const hasDelIf = hasMountIf(child);
|
|
28
28
|
const hasElif = child.hasAttribute("elif");
|
|
29
29
|
const hasElse = child.hasAttribute("else");
|
|
30
30
|
|
|
@@ -40,7 +40,7 @@ export function validateChainStructure(parent, sourceFile, sourceContent, parent
|
|
|
40
40
|
if (lineNum !== null) loc = `${sourceFile}:${lineNum}`;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
throw new Error(`${loc}\n <${tag}> has ${attr} without a preceding if/
|
|
43
|
+
throw new Error(`${loc}\n <${tag}> has ${attr} without a preceding if/mount:if sibling`);
|
|
44
44
|
}
|
|
45
45
|
if (hasElse) {
|
|
46
46
|
chainActive = false;
|
|
@@ -60,7 +60,7 @@ export function validateChainStructure(parent, sourceFile, sourceContent, parent
|
|
|
60
60
|
export function applyConditionalToElement(child, ctxProxy, condChain, hasIf, hasDelIf, hasElif, hasElse) {
|
|
61
61
|
if (hasIf) {
|
|
62
62
|
const expr = child.getAttribute("if").slice(1, -1);
|
|
63
|
-
const fn =
|
|
63
|
+
const fn = compileExpr(expr, true);
|
|
64
64
|
condChain.active = true;
|
|
65
65
|
if (fn(ctxProxy)) {
|
|
66
66
|
condChain.rendered = true;
|
|
@@ -70,8 +70,8 @@ export function applyConditionalToElement(child, ctxProxy, condChain, hasIf, has
|
|
|
70
70
|
}
|
|
71
71
|
child.removeAttribute("if");
|
|
72
72
|
} else if (hasDelIf) {
|
|
73
|
-
const expr =
|
|
74
|
-
const fn =
|
|
73
|
+
const expr = getMountIf(child).slice(1, -1);
|
|
74
|
+
const fn = compileExpr(expr, true);
|
|
75
75
|
condChain.active = true;
|
|
76
76
|
if (fn(ctxProxy)) {
|
|
77
77
|
condChain.rendered = true;
|
|
@@ -79,10 +79,10 @@ export function applyConditionalToElement(child, ctxProxy, condChain, hasIf, has
|
|
|
79
79
|
child.remove();
|
|
80
80
|
condChain.rendered = false;
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
removeMountIf(child);
|
|
83
83
|
} else if (hasElif) {
|
|
84
84
|
const expr = child.getAttribute("elif").slice(1, -1);
|
|
85
|
-
const fn =
|
|
85
|
+
const fn = compileExpr(expr, true);
|
|
86
86
|
if (fn(ctxProxy)) {
|
|
87
87
|
condChain.rendered = true;
|
|
88
88
|
} else {
|
|
@@ -106,7 +106,7 @@ export function interpolateNode(root, ctxProxy) {
|
|
|
106
106
|
if (node.nodeType === 3) {
|
|
107
107
|
node.textContent = node.textContent.replace(/\{([^}]+)\}/g, (_, expr) => {
|
|
108
108
|
try {
|
|
109
|
-
return
|
|
109
|
+
return compileExpr(expr, true)(ctxProxy);
|
|
110
110
|
} catch {
|
|
111
111
|
return "";
|
|
112
112
|
}
|
package/parser/utils.js
CHANGED
|
@@ -2,7 +2,7 @@ import { restoreTemplateChars } from "../utils.js";
|
|
|
2
2
|
|
|
3
3
|
const compiledCache = new Map();
|
|
4
4
|
|
|
5
|
-
export function
|
|
5
|
+
export function compileExpr(expr, useCtx) {
|
|
6
6
|
expr = restoreTemplateChars(expr);
|
|
7
7
|
const key = useCtx ? `ctx:${expr}` : `raw:${expr}`;
|
|
8
8
|
let fn = compiledCache.get(key);
|
|
@@ -14,3 +14,19 @@ export function compileExpression(expr, useCtx) {
|
|
|
14
14
|
}
|
|
15
15
|
return fn;
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
const constantEvalError = Symbol("constant-eval-error");
|
|
19
|
+
|
|
20
|
+
export function evaluateConstant(expr) {
|
|
21
|
+
const trap = new Proxy({}, {
|
|
22
|
+
has() { return true; },
|
|
23
|
+
get() { throw constantEvalError; },
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
const fn = compileExpr(expr, true);
|
|
27
|
+
const value = fn(trap);
|
|
28
|
+
return { constant: true, value };
|
|
29
|
+
} catch {
|
|
30
|
+
return { constant: false };
|
|
31
|
+
}
|
|
32
|
+
}
|
package/runtime/index.js
CHANGED
|
@@ -3,7 +3,7 @@ const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
|
|
|
3
3
|
|
|
4
4
|
const exprCache = new Map();
|
|
5
5
|
|
|
6
|
-
function
|
|
6
|
+
function compileExp(expr, useCtx) {
|
|
7
7
|
expr = expr.replace(/_%%CHOCOLA-(?:AMP|LT|GT)\d+%%_/g, char =>
|
|
8
8
|
char.includes("AMP") ? "&" : char.includes("LT") ? "<" : ">"
|
|
9
9
|
);
|
|
@@ -28,7 +28,7 @@ function interpolateNode(root, ctx) {
|
|
|
28
28
|
.replace(new RegExp(RBRACE_PH, "g"), "}");
|
|
29
29
|
node.textContent = text.replace(/\{([^}]+)\}/g, (_, expr) => {
|
|
30
30
|
try {
|
|
31
|
-
return String(
|
|
31
|
+
return String(compileExp(expr, true)(ctx));
|
|
32
32
|
} catch {
|
|
33
33
|
return "";
|
|
34
34
|
}
|
|
@@ -41,21 +41,21 @@ function interpolateNode(root, ctx) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function
|
|
44
|
+
function applyDirectiveToEl(child, ctx, chain, hasIf, hasMountIf, hasElif, hasElse) {
|
|
45
45
|
if (hasIf) {
|
|
46
46
|
const expr = child.getAttribute("if").slice(1, -1);
|
|
47
47
|
chain.active = true;
|
|
48
|
-
if (
|
|
48
|
+
if (compileExp(expr, true)(ctx)) {
|
|
49
49
|
chain.rendered = true;
|
|
50
50
|
} else {
|
|
51
51
|
child.style.display = "none";
|
|
52
52
|
chain.rendered = false;
|
|
53
53
|
}
|
|
54
54
|
child.removeAttribute("if");
|
|
55
|
-
} else if (
|
|
55
|
+
} else if (hasMountIf) {
|
|
56
56
|
const expr = child.getAttribute("mount:if").slice(1, -1);
|
|
57
57
|
chain.active = true;
|
|
58
|
-
if (
|
|
58
|
+
if (compileExp(expr, true)(ctx)) {
|
|
59
59
|
chain.rendered = true;
|
|
60
60
|
} else {
|
|
61
61
|
child.remove();
|
|
@@ -64,7 +64,7 @@ function applyConditionalToElement(child, ctx, chain, hasIf, hasDelIf, hasElif,
|
|
|
64
64
|
child.removeAttribute("mount:if");
|
|
65
65
|
} else if (hasElif) {
|
|
66
66
|
const expr = child.getAttribute("elif").slice(1, -1);
|
|
67
|
-
if (
|
|
67
|
+
if (compileExp(expr, true)(ctx)) {
|
|
68
68
|
chain.rendered = true;
|
|
69
69
|
} else {
|
|
70
70
|
child.remove();
|
|
@@ -90,7 +90,7 @@ function interpolateAttributes(element, ctx) {
|
|
|
90
90
|
.replace(new RegExp(RBRACE_PH, "g"), "}");
|
|
91
91
|
attr.value = raw.replace(/\{([^}]+)\}/g, (_, expr) => {
|
|
92
92
|
try {
|
|
93
|
-
return String(
|
|
93
|
+
return String(compileExp(expr, true)(ctx));
|
|
94
94
|
} catch {
|
|
95
95
|
return "";
|
|
96
96
|
}
|
|
@@ -99,10 +99,10 @@ function interpolateAttributes(element, ctx) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function
|
|
102
|
+
function processDirectives(element, ctx, chain) {
|
|
103
103
|
for (const child of [...element.children]) {
|
|
104
104
|
const hasIf = child.hasAttribute("if");
|
|
105
|
-
const
|
|
105
|
+
const hasMountIf = child.hasAttribute("mount:if");
|
|
106
106
|
const hasElif = child.hasAttribute("elif");
|
|
107
107
|
const hasElse = child.hasAttribute("else");
|
|
108
108
|
|
|
@@ -112,46 +112,46 @@ function processConditionals(element, ctx, chain) {
|
|
|
112
112
|
continue;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
applyDirectiveToEl(child, ctx, chain, hasIf, hasMountIf, hasElif, hasElse);
|
|
116
116
|
|
|
117
117
|
if (child.parentNode) {
|
|
118
|
-
|
|
118
|
+
processDirectives(child, ctx, chain);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
function processSlots(
|
|
124
|
-
const slots =
|
|
123
|
+
function processSlots(el, projected) {
|
|
124
|
+
const slots = el.querySelectorAll("slot");
|
|
125
125
|
for (const slot of slots) {
|
|
126
|
-
const clone =
|
|
126
|
+
const clone = projected.map(c => c.cloneNode(true));
|
|
127
127
|
slot.replaceWith(...clone);
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
let bindCounter = 0;
|
|
132
132
|
|
|
133
|
-
function
|
|
133
|
+
function processBindings(el) {
|
|
134
134
|
const bindings = [];
|
|
135
|
-
for (const attr of [...
|
|
135
|
+
for (const attr of [...el.attributes]) {
|
|
136
136
|
if (attr.name.startsWith("bind:")) {
|
|
137
137
|
const prop = attr.name.slice(5);
|
|
138
138
|
const varName = attr.value;
|
|
139
139
|
const bindId = "b" + (bindCounter++);
|
|
140
|
-
|
|
140
|
+
el.setAttribute("data-chbind-" + bindId, "");
|
|
141
141
|
bindings.push({ prop, varName, bindId });
|
|
142
|
-
|
|
142
|
+
el.removeAttribute(attr.name);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
return bindings;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
function
|
|
148
|
+
function resolveBindings(el) {
|
|
149
149
|
const ids = [];
|
|
150
|
-
for (const attr of [...
|
|
150
|
+
for (const attr of [...el.attributes]) {
|
|
151
151
|
const match = attr.name.match(/^data-chbind-(b\d+)$/);
|
|
152
152
|
if (match) {
|
|
153
153
|
ids.push(match[1]);
|
|
154
|
-
|
|
154
|
+
el.removeAttribute(attr.name);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
return ids;
|
|
@@ -162,8 +162,8 @@ function collectBindings(root) {
|
|
|
162
162
|
const idToEl = {};
|
|
163
163
|
|
|
164
164
|
for (const el of [root, ...root.querySelectorAll("*")]) {
|
|
165
|
-
bindingDefs.push(...
|
|
166
|
-
for (const id of
|
|
165
|
+
bindingDefs.push(...processBindings(el));
|
|
166
|
+
for (const id of resolveBindings(el)) {
|
|
167
167
|
idToEl[id] = el;
|
|
168
168
|
}
|
|
169
169
|
}
|
|
@@ -225,7 +225,7 @@ class ChocolaComponent {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
processSlots(root, this.#children || []);
|
|
228
|
-
|
|
228
|
+
processDirectives(root, mergedCtx, { active: false, rendered: false });
|
|
229
229
|
interpolateAttributes(root, mergedCtx);
|
|
230
230
|
interpolateNode(root, mergedCtx);
|
|
231
231
|
root.classList.add(this.#hash);
|
package/utils.js
CHANGED
|
@@ -2,7 +2,7 @@ import { promises as fs } from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { throwError } from "./compiler/utils.js";
|
|
4
4
|
|
|
5
|
-
export async function
|
|
5
|
+
export async function getConfig(__rootdir) {
|
|
6
6
|
try {
|
|
7
7
|
const config = await fs.readFile(path.join(__rootdir, "chocola.config.json"), "utf-8");
|
|
8
8
|
return JSON.parse(config);
|