chocola 1.4.2 → 1.5.1
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/compiler/component-processor.js +206 -26
- package/compiler/config.js +12 -3
- package/compiler/dom-processor.js +3 -3
- package/compiler/index.js +13 -4
- package/compiler/pipeline.js +78 -40
- package/compiler/utils.js +57 -7
- package/dev/index.js +2 -2
- package/package.json +1 -1
- package/utils.js +1 -1
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { JSDOM } from "jsdom";
|
|
2
2
|
import { extractContextFromElement } from "./dom-processor.js";
|
|
3
|
-
import { genRandomId, incrementAlfabet, throwError } from "./utils.js";
|
|
3
|
+
import { genRandomId, incrementAlfabet, throwError, protectCurlyBraces } from "./utils.js";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import beautify from "js-beautify";
|
|
6
6
|
|
|
7
|
+
function hasCombinator(sel) {
|
|
8
|
+
let depth = 0;
|
|
9
|
+
for (const ch of sel) {
|
|
10
|
+
if (ch === '[') depth++;
|
|
11
|
+
else if (ch === ']') depth--;
|
|
12
|
+
else if (depth === 0 && (ch === ' ' || ch === '>' || ch === '+' || ch === '~')) return true;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function scopeSelector(sel, cssId) {
|
|
18
|
+
const s = sel.trim();
|
|
19
|
+
if (s.startsWith("." + cssId)) return s;
|
|
20
|
+
if (hasCombinator(s)) return `.${cssId} ${s}`;
|
|
21
|
+
return `.${cssId}${s}, .${cssId} ${s}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
function scopeCss(cssString, cssId) {
|
|
8
25
|
function findMatchingBrace(s, openIndex) {
|
|
9
26
|
let depth = 0;
|
|
@@ -42,13 +59,7 @@ function scopeCss(cssString, cssId) {
|
|
|
42
59
|
.map(s => s.trim())
|
|
43
60
|
.filter(Boolean);
|
|
44
61
|
const scopedHeader = selectors.length > 0
|
|
45
|
-
? selectors.map(sel =>
|
|
46
|
-
const s = sel.trim();
|
|
47
|
-
if (s.startsWith("." + cssId)) {
|
|
48
|
-
return s;
|
|
49
|
-
}
|
|
50
|
-
return `.${cssId} ${s}`;
|
|
51
|
-
}).join(", ")
|
|
62
|
+
? selectors.map(sel => scopeSelector(sel, cssId)).join(", ")
|
|
52
63
|
: header;
|
|
53
64
|
out += scopedHeader + "{" + innerScoped + "}";
|
|
54
65
|
}
|
|
@@ -75,6 +86,72 @@ function interpolateNode(node, ctxProxy) {
|
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
function validateChainStructure(parent, sourceFile, sourceContent, parentContent) {
|
|
90
|
+
const children = [...parent.children];
|
|
91
|
+
let chainActive = false;
|
|
92
|
+
const outerCounts = {};
|
|
93
|
+
|
|
94
|
+
for (const child of children) {
|
|
95
|
+
const hasIf = child.hasAttribute("if");
|
|
96
|
+
const hasDelIf = child.hasAttribute("del-if");
|
|
97
|
+
const hasElif = child.hasAttribute("elif");
|
|
98
|
+
const hasElse = child.hasAttribute("else");
|
|
99
|
+
|
|
100
|
+
if (hasElif || hasElse) {
|
|
101
|
+
if (!chainActive) {
|
|
102
|
+
const tag = child.tagName.toLowerCase();
|
|
103
|
+
const attr = hasElif ? "elif" : "else";
|
|
104
|
+
let loc = sourceFile;
|
|
105
|
+
if (sourceContent && parentContent) {
|
|
106
|
+
const outer = child.outerHTML;
|
|
107
|
+
outerCounts[outer] = (outerCounts[outer] || 0) + 1;
|
|
108
|
+
let searchIdx = 0;
|
|
109
|
+
let hits = 0;
|
|
110
|
+
while ((searchIdx = parentContent.indexOf(outer, searchIdx)) !== -1) {
|
|
111
|
+
hits++;
|
|
112
|
+
if (hits === outerCounts[outer]) {
|
|
113
|
+
let line = parentContent.substring(0, searchIdx).split("\n").length;
|
|
114
|
+
if (sourceContent !== parentContent) {
|
|
115
|
+
const anchor = parentContent.trim().slice(0, 80);
|
|
116
|
+
const vars = [
|
|
117
|
+
anchor,
|
|
118
|
+
anchor.replace(/=""/g, ' '),
|
|
119
|
+
anchor.replace(/=""/g, ''),
|
|
120
|
+
anchor.replace(/="/g, "='").replace(/"/g, "'"),
|
|
121
|
+
];
|
|
122
|
+
let anchorIdx = -1;
|
|
123
|
+
for (const v of vars) {
|
|
124
|
+
anchorIdx = sourceContent.indexOf(v);
|
|
125
|
+
if (anchorIdx !== -1) break;
|
|
126
|
+
}
|
|
127
|
+
if (anchorIdx !== -1) {
|
|
128
|
+
const offset = sourceContent.substring(0, anchorIdx).split("\n").length;
|
|
129
|
+
line = offset + line - 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
loc = `${sourceFile}:${line}`;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
searchIdx += outer.length;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throwError(`${loc}\n <${tag}> has ${attr} without a preceding if/del-if sibling`);
|
|
139
|
+
}
|
|
140
|
+
if (hasElse) {
|
|
141
|
+
chainActive = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (hasIf || hasDelIf) {
|
|
146
|
+
chainActive = true;
|
|
147
|
+
} else if (!hasElif && !hasElse) {
|
|
148
|
+
chainActive = false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
validateChainStructure(child, sourceFile, sourceContent, parentContent);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
78
155
|
/**
|
|
79
156
|
* Processes a single component element and inserts it into the DOM
|
|
80
157
|
* @param {Element} element
|
|
@@ -95,7 +172,9 @@ export function processComponentElement(
|
|
|
95
172
|
cssScopesMap,
|
|
96
173
|
scopedStyles,
|
|
97
174
|
renderChain = [],
|
|
98
|
-
staticCtxRegistry
|
|
175
|
+
staticCtxRegistry,
|
|
176
|
+
sourceFile,
|
|
177
|
+
sourceContent
|
|
99
178
|
) {
|
|
100
179
|
const tagName = element.tagName.toLowerCase();
|
|
101
180
|
const compName = tagName + ".js";
|
|
@@ -119,19 +198,85 @@ export function processComponentElement(
|
|
|
119
198
|
});
|
|
120
199
|
|
|
121
200
|
if (instance.body) {
|
|
122
|
-
let body = instance.body;
|
|
201
|
+
let body = protectCurlyBraces(instance.body);
|
|
123
202
|
|
|
124
203
|
const fragment = JSDOM.fragment(body);
|
|
125
204
|
|
|
126
205
|
const slotFragment = JSDOM.fragment(srcInnerHtml);
|
|
206
|
+
if (sourceFile) {
|
|
207
|
+
validateChainStructure(slotFragment, sourceFile, sourceContent, srcInnerHtml);
|
|
208
|
+
}
|
|
209
|
+
validateChainStructure(fragment, instance.__sourceFile || compName, body, body);
|
|
127
210
|
Array.from(fragment.querySelectorAll("slot")).forEach(slot => {
|
|
128
211
|
slot.replaceWith(slotFragment);
|
|
129
212
|
});
|
|
130
213
|
|
|
131
|
-
const
|
|
214
|
+
const childEntries = Array.from(fragment.querySelectorAll("*")).map(el => ({
|
|
215
|
+
el,
|
|
216
|
+
parent: el.parentNode
|
|
217
|
+
}));
|
|
218
|
+
const condChains = new Map();
|
|
219
|
+
|
|
220
|
+
childEntries.forEach(({ el: child, parent }) => {
|
|
221
|
+
if (!condChains.has(parent)) {
|
|
222
|
+
condChains.set(parent, { active: false, rendered: false });
|
|
223
|
+
}
|
|
224
|
+
const condChain = condChains.get(parent);
|
|
132
225
|
|
|
133
|
-
|
|
134
|
-
const
|
|
226
|
+
const hasIf = child.hasAttribute("if");
|
|
227
|
+
const hasDelIf = child.hasAttribute("del-if");
|
|
228
|
+
const hasElif = child.hasAttribute("elif");
|
|
229
|
+
const hasElse = child.hasAttribute("else");
|
|
230
|
+
|
|
231
|
+
if (hasElif || hasElse) {
|
|
232
|
+
if (!condChain.active) {
|
|
233
|
+
throwError(`${instance.__sourceFile || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/del-if sibling`);
|
|
234
|
+
}
|
|
235
|
+
if (condChain.rendered) {
|
|
236
|
+
child.remove();
|
|
237
|
+
if (hasElse) {
|
|
238
|
+
condChain.active = false;
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (child.tagName.toLowerCase() === "void") {
|
|
245
|
+
if (hasElif || hasElse) {
|
|
246
|
+
if (hasElif) {
|
|
247
|
+
const expr = child.getAttribute("elif").slice(1, -1);
|
|
248
|
+
const fn = new Function("ctx", `with(ctx) { return (${expr}); }`);
|
|
249
|
+
if (!fn(ctxProxy)) {
|
|
250
|
+
child.remove();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
child.replaceWith(...child.children);
|
|
255
|
+
condChain.rendered = true;
|
|
256
|
+
if (hasElse) {
|
|
257
|
+
condChain.active = false;
|
|
258
|
+
}
|
|
259
|
+
} else if (hasIf || hasDelIf) {
|
|
260
|
+
const attr = hasIf ? "if" : "del-if";
|
|
261
|
+
const expr = child.getAttribute(attr).slice(1, -1);
|
|
262
|
+
const fn = new Function("ctx", `with(ctx) { return (${expr}); }`);
|
|
263
|
+
condChain.active = true;
|
|
264
|
+
if (fn(ctxProxy)) {
|
|
265
|
+
child.replaceWith(...child.children);
|
|
266
|
+
condChain.rendered = true;
|
|
267
|
+
} else {
|
|
268
|
+
child.remove();
|
|
269
|
+
condChain.rendered = false;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
child.replaceWith(...child.children);
|
|
273
|
+
condChain.active = false;
|
|
274
|
+
condChain.rendered = false;
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const reservedAttrs = ["if", "del-if", "elif", "else"];
|
|
135
280
|
|
|
136
281
|
Array.from(child.attributes).forEach(attribute => {
|
|
137
282
|
if (!attribute || attribute === undefined) return;
|
|
@@ -159,20 +304,50 @@ export function processComponentElement(
|
|
|
159
304
|
cssScopesMap,
|
|
160
305
|
scopedStyles,
|
|
161
306
|
renderChain.concat(compName),
|
|
162
|
-
staticCtxRegistry
|
|
307
|
+
staticCtxRegistry,
|
|
308
|
+
instance.__sourceFile || compName,
|
|
309
|
+
body
|
|
163
310
|
);
|
|
164
311
|
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
312
|
+
if (hasIf) {
|
|
313
|
+
const expr = child.getAttribute("if").slice(1, -1);
|
|
314
|
+
const fn = new Function("ctx", `with(ctx) { return (${expr}); }`);
|
|
315
|
+
condChain.active = true;
|
|
316
|
+
if (fn(ctxProxy)) {
|
|
317
|
+
condChain.rendered = true;
|
|
318
|
+
} else {
|
|
319
|
+
child.style.display = "none";
|
|
320
|
+
condChain.rendered = false;
|
|
321
|
+
}
|
|
322
|
+
child.removeAttribute("if");
|
|
323
|
+
} else if (hasDelIf) {
|
|
324
|
+
const expr = child.getAttribute("del-if").slice(1, -1);
|
|
169
325
|
const fn = new Function("ctx", `with(ctx) { return (${expr}); }`);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
326
|
+
condChain.active = true;
|
|
327
|
+
if (fn(ctxProxy)) {
|
|
328
|
+
condChain.rendered = true;
|
|
329
|
+
} else {
|
|
330
|
+
child.remove();
|
|
331
|
+
condChain.rendered = false;
|
|
173
332
|
}
|
|
174
|
-
child.removeAttribute(
|
|
175
|
-
})
|
|
333
|
+
child.removeAttribute("del-if");
|
|
334
|
+
} else if (hasElif) {
|
|
335
|
+
const expr = child.getAttribute("elif").slice(1, -1);
|
|
336
|
+
const fn = new Function("ctx", `with(ctx) { return (${expr}); }`);
|
|
337
|
+
if (fn(ctxProxy)) {
|
|
338
|
+
condChain.rendered = true;
|
|
339
|
+
} else {
|
|
340
|
+
child.remove();
|
|
341
|
+
}
|
|
342
|
+
child.removeAttribute("elif");
|
|
343
|
+
} else if (hasElse) {
|
|
344
|
+
condChain.rendered = true;
|
|
345
|
+
condChain.active = false;
|
|
346
|
+
child.removeAttribute("else");
|
|
347
|
+
} else {
|
|
348
|
+
condChain.active = false;
|
|
349
|
+
condChain.rendered = false;
|
|
350
|
+
}
|
|
176
351
|
|
|
177
352
|
interpolateNode(child, ctxProxy)
|
|
178
353
|
});
|
|
@@ -231,11 +406,16 @@ export function processComponentElement(
|
|
|
231
406
|
style = scopeCss(style, cssId);
|
|
232
407
|
scopedStyles.push(style);
|
|
233
408
|
}
|
|
409
|
+
|
|
410
|
+
if (fragment.firstChild && fragment.firstChild.nodeType === 1) {
|
|
411
|
+
fragment.firstChild.setAttribute("data-ch-source", instance.__sourceFile || compName);
|
|
412
|
+
}
|
|
413
|
+
|
|
234
414
|
element.replaceWith(fragment);
|
|
235
415
|
return true;
|
|
236
416
|
}
|
|
237
417
|
|
|
238
|
-
console.warn(chalk.yellow(`${compName} component could not be loaded`));
|
|
418
|
+
console.warn(chalk.yellow(`${compName} — component could not be loaded (missing body or styles export)`));
|
|
239
419
|
return false;
|
|
240
420
|
}
|
|
241
421
|
|
|
@@ -249,7 +429,7 @@ export function processComponentElement(
|
|
|
249
429
|
* scopesCss: CSSString
|
|
250
430
|
* }}
|
|
251
431
|
*/
|
|
252
|
-
export function processAllComponents(appElements, loadedComponents) {
|
|
432
|
+
export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent) {
|
|
253
433
|
let runtimeChunks = [];
|
|
254
434
|
let compIdColl = [];
|
|
255
435
|
let letterState = { value: null };
|
|
@@ -260,7 +440,7 @@ export function processAllComponents(appElements, loadedComponents) {
|
|
|
260
440
|
let staticCtxRegistry = new Map();
|
|
261
441
|
|
|
262
442
|
appElements.forEach(el => {
|
|
263
|
-
processComponentElement(el, loadedComponents, runtimeChunks, compIdColl, letterState, runtimeMap, cssScopes, cssScopesMap, scopedStyles, [], staticCtxRegistry);
|
|
443
|
+
processComponentElement(el, loadedComponents, runtimeChunks, compIdColl, letterState, runtimeMap, cssScopes, cssScopesMap, scopedStyles, [], staticCtxRegistry, pageSourceFile, pageSourceContent);
|
|
264
444
|
});
|
|
265
445
|
const runtimeScript = runtimeChunks.join("\n");
|
|
266
446
|
const hasComponents = runtimeChunks.length > 0;
|
package/compiler/config.js
CHANGED
|
@@ -20,13 +20,14 @@ export async function loadConfig(rootDir) {
|
|
|
20
20
|
const outDir = bundleConfig.outDir || "dist";
|
|
21
21
|
const libDir = bundleConfig.libDir || "lib";
|
|
22
22
|
const emptyOutDir = bundleConfig.emptyOutDir !== false;
|
|
23
|
+
const assetImport = bundleConfig.assetImport || "legacy";
|
|
23
24
|
|
|
24
|
-
logConfigWarnings(bundleConfig, emptyOutDir);
|
|
25
|
+
logConfigWarnings(bundleConfig, emptyOutDir, assetImport);
|
|
25
26
|
|
|
26
|
-
return { srcDir, outDir, libDir, emptyOutDir };
|
|
27
|
+
return { srcDir, outDir, libDir, emptyOutDir, assetImport };
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function logConfigWarnings(bundleConfig, emptyOutDir) {
|
|
30
|
+
function logConfigWarnings(bundleConfig, emptyOutDir, assetImport) {
|
|
30
31
|
if (!bundleConfig.srcDir) {
|
|
31
32
|
console.warn(
|
|
32
33
|
chalk.bold.yellow("WARNING!"),
|
|
@@ -49,6 +50,14 @@ function logConfigWarnings(bundleConfig, emptyOutDir) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
console.log(`> using emptyOutDir = ${emptyOutDir}`);
|
|
53
|
+
|
|
54
|
+
if (assetImport !== "static") {
|
|
55
|
+
console.warn(
|
|
56
|
+
chalk.bold.yellow("DEPRECATION WARNING!"),
|
|
57
|
+
'The legacy asset import system ("bundle.assetImport") will be removed in Chocola 2. ' +
|
|
58
|
+
'Set "bundle.assetImport": "static" in chocola.config.json and migrate your assets to src/static/.'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { JSDOM } from "jsdom";
|
|
2
2
|
import { promises as fs } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { throwError } from "./utils.js";
|
|
4
|
+
import { throwError, protectCurlyBraces, restoreCurlyBraces } from "./utils.js";
|
|
5
5
|
import { readMyFile } from "./fs.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -10,7 +10,7 @@ import { readMyFile } from "./fs.js";
|
|
|
10
10
|
* @returns {JSDOM}
|
|
11
11
|
*/
|
|
12
12
|
export function createDOM(srcIndexContent) {
|
|
13
|
-
return new JSDOM(srcIndexContent);
|
|
13
|
+
return new JSDOM(protectCurlyBraces(srcIndexContent));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -56,7 +56,7 @@ export function extractContextFromElement(element) {
|
|
|
56
56
|
*/
|
|
57
57
|
export async function serializeDOM(dom) {
|
|
58
58
|
const beautify = (await import("js-beautify")).default;
|
|
59
|
-
const finalHtml = dom.serialize();
|
|
59
|
+
const finalHtml = restoreCurlyBraces(dom.serialize());
|
|
60
60
|
return beautify.html(finalHtml, { indent_size: 2 });
|
|
61
61
|
}
|
|
62
62
|
|
package/compiler/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from "path";
|
|
2
1
|
import { promises as fs } from "fs";
|
|
3
2
|
import chalk from "chalk";
|
|
4
3
|
import { loadConfig, resolvePaths } from "./config.js";
|
|
@@ -18,6 +17,7 @@ import { generateRuntimeScript } from "./runtime-generator.js";
|
|
|
18
17
|
import { genRandomId, throwError } from "./utils.js";
|
|
19
18
|
import {
|
|
20
19
|
copyResources,
|
|
20
|
+
copyStaticDir,
|
|
21
21
|
getComponents,
|
|
22
22
|
getSrcIndex,
|
|
23
23
|
processIcons,
|
|
@@ -101,7 +101,7 @@ async function processAssets(doc, rootDir, srcDir, outDirPath) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
for (const link of icons) {
|
|
104
|
-
await processIcons(link, rootDir, srcDir, outDirPath
|
|
104
|
+
await processIcons(link, rootDir, srcDir, outDirPath);
|
|
105
105
|
}
|
|
106
106
|
return cssContents
|
|
107
107
|
}
|
|
@@ -126,6 +126,7 @@ export default async function runtime(rootDir, buildConfig) {
|
|
|
126
126
|
|
|
127
127
|
const indexFiles = await getSrcIndex(paths.src);
|
|
128
128
|
const srcIndexContent = indexFiles.srcHtmlFile || indexFiles.srcChocoFile;
|
|
129
|
+
const pageSourcePath = indexFiles.srcPath;
|
|
129
130
|
|
|
130
131
|
const loadedComponents = await loadAndDisplayComponents(paths.components);
|
|
131
132
|
!isHotReload && console.log(logSeparation);
|
|
@@ -138,7 +139,7 @@ export default async function runtime(rootDir, buildConfig) {
|
|
|
138
139
|
const appContainer = validateAppContainer(doc);
|
|
139
140
|
const appElements = getAppElements(appContainer);
|
|
140
141
|
|
|
141
|
-
const { runtimeScript, scopesCss } = processAllComponents(appElements, loadedComponents);
|
|
142
|
+
const { runtimeScript, scopesCss } = processAllComponents(appElements, loadedComponents, pageSourcePath, srcIndexContent);
|
|
142
143
|
const runtimeFilename = await generateRuntimeScript(runtimeScript, paths.outDir);
|
|
143
144
|
const globalCss = (await processAssets(doc, rootDir, config.srcDir, paths.outDir)).join("\n");
|
|
144
145
|
|
|
@@ -152,7 +153,15 @@ export default async function runtime(rootDir, buildConfig) {
|
|
|
152
153
|
const html = await serializeDOM(dom);
|
|
153
154
|
await writeHTMLOutput(html, paths.outDir);
|
|
154
155
|
|
|
155
|
-
|
|
156
|
+
try {
|
|
157
|
+
if (config.assetImport === "static") {
|
|
158
|
+
await copyStaticDir(paths.src, paths.outDir);
|
|
159
|
+
} else {
|
|
160
|
+
await copyResources(rootDir, scopesCss, globalCss, config.srcDir, paths.outDir);
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
throwError(err.message || err);
|
|
164
|
+
}
|
|
156
165
|
|
|
157
166
|
!isHotReload && logSuccess(paths.outDir);
|
|
158
167
|
isHotReload && console.log("Dev server updated");
|
package/compiler/pipeline.js
CHANGED
|
@@ -33,21 +33,28 @@ export async function getComponents(libDir) {
|
|
|
33
33
|
|
|
34
34
|
componentsLib.push(comp);
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
let module;
|
|
37
|
+
try {
|
|
38
|
+
module = await loadWithAssets(path.join(libDir, comp));
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
if (typeof module.default !== "function") {
|
|
41
|
+
notDefComps.push(comp);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
const instance = module.default();
|
|
46
|
+
const compPath = path.join(libDir, comp);
|
|
47
|
+
instance.__sourceFile = compPath;
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
loadedComponents.set(comp.toLowerCase(), instance);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
throwError(`Failed to load component "${comp}": ${err.message || err}`);
|
|
52
|
+
}
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
return { componentsLib, loadedComponents, notDefComps };
|
|
49
56
|
} catch (err) {
|
|
50
|
-
throwError(err);
|
|
57
|
+
throwError(`Failed to load components from ${libDir}: ${err.message}`);
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -80,7 +87,7 @@ export async function getSrcIndex(srcPath) {
|
|
|
80
87
|
if (htmlExists) {
|
|
81
88
|
try {
|
|
82
89
|
srcHtmlFile = await readMyFile(srcHtmlPath);
|
|
83
|
-
return { srcHtmlFile, srcChocoFile };
|
|
90
|
+
return { srcHtmlFile, srcChocoFile, srcPath: srcHtmlPath };
|
|
84
91
|
} catch (err) {
|
|
85
92
|
throwError(err);
|
|
86
93
|
}
|
|
@@ -113,7 +120,7 @@ export async function processStylesheet(link, rootDir, srcDir, outDirPath, fileI
|
|
|
113
120
|
|
|
114
121
|
return css;
|
|
115
122
|
} catch (err) {
|
|
116
|
-
throwError(err);
|
|
123
|
+
throwError(`Failed to process stylesheet: ${err}`);
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
|
|
@@ -128,65 +135,66 @@ export async function processStylesheet(link, rootDir, srcDir, outDirPath, fileI
|
|
|
128
135
|
export async function processIcons(link, rootDir, srcDir, outDirPath) {
|
|
129
136
|
try {
|
|
130
137
|
const href = link.href;
|
|
138
|
+
if (href.startsWith("http://") || href.startsWith("https://")) return;
|
|
131
139
|
const iconPath = path.join(rootDir, srcDir, href);
|
|
132
140
|
await fs.copyFile(iconPath, path.join(outDirPath, href));
|
|
133
141
|
} catch (err) {
|
|
134
|
-
throwError(err);
|
|
142
|
+
throwError(`Failed to copy icon: ${err}`);
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
export function getCssAssets(css) {
|
|
139
147
|
const results = new Set();
|
|
140
|
-
|
|
141
148
|
const urlRegex = /url\(([^)]+)\)/gi;
|
|
142
|
-
|
|
143
149
|
const importStringRegex = /@import\s+["']([^"']+)["']/gi;
|
|
144
|
-
|
|
145
150
|
const importUrlRegex = /@import\s+url\(([^)]+)\)/gi;
|
|
146
151
|
|
|
147
152
|
const clean = (raw) => {
|
|
148
153
|
let p = raw.trim();
|
|
149
|
-
|
|
150
154
|
if (
|
|
151
155
|
(p.startsWith('"') && p.endsWith('"')) ||
|
|
152
156
|
(p.startsWith("'") && p.endsWith("'"))
|
|
153
157
|
) {
|
|
154
158
|
p = p.slice(1, -1);
|
|
155
159
|
}
|
|
156
|
-
|
|
157
160
|
if (p.startsWith("data:")) return null;
|
|
158
|
-
|
|
159
161
|
return p;
|
|
160
162
|
};
|
|
161
163
|
|
|
162
164
|
let match;
|
|
163
|
-
|
|
164
165
|
while ((match = urlRegex.exec(css))) {
|
|
165
166
|
const p = clean(match[1]);
|
|
166
167
|
if (p) results.add(p);
|
|
167
168
|
}
|
|
168
|
-
|
|
169
169
|
while ((match = importStringRegex.exec(css))) {
|
|
170
170
|
const p = clean(match[1]);
|
|
171
171
|
if (p) results.add(p);
|
|
172
172
|
}
|
|
173
|
-
|
|
174
173
|
while ((match = importUrlRegex.exec(css))) {
|
|
175
174
|
const p = clean(match[1]);
|
|
176
175
|
if (p) results.add(p);
|
|
177
176
|
}
|
|
178
|
-
|
|
179
177
|
return [...results];
|
|
180
178
|
}
|
|
181
179
|
|
|
180
|
+
function getComponentSource(el) {
|
|
181
|
+
let cur = el;
|
|
182
|
+
while (cur) {
|
|
183
|
+
const src = cur.getAttribute?.("data-ch-source");
|
|
184
|
+
if (src) return src;
|
|
185
|
+
cur = cur.parentElement;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
182
189
|
|
|
183
190
|
/**
|
|
184
|
-
* Copies all local resources (images, fonts, etc.) to output directory
|
|
185
|
-
* Excludes web links and script/link tags
|
|
191
|
+
* Copies all local resources (images, fonts, etc.) to output directory.
|
|
192
|
+
* Excludes web links and script/link tags.
|
|
186
193
|
* @param {import("fs").PathLike} rootDir - Root project directory
|
|
194
|
+
* @param {string} scopesCss - Scoped CSS content
|
|
195
|
+
* @param {string} globalCss - Global CSS content
|
|
187
196
|
* @param {string} srcDir - Source directory name
|
|
188
197
|
* @param {import("fs").PathLike} outDirPath - Output directory path
|
|
189
|
-
* @throws {Error} if resources cannot be copied
|
|
190
198
|
*/
|
|
191
199
|
export async function copyResources(rootDir, scopesCss, globalCss, srcDir, outDirPath) {
|
|
192
200
|
try {
|
|
@@ -198,53 +206,83 @@ export async function copyResources(rootDir, scopesCss, globalCss, srcDir, outDi
|
|
|
198
206
|
for (const el of newElements) {
|
|
199
207
|
if (el.tagName === "LINK" || el.tagName === "SCRIPT") continue;
|
|
200
208
|
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
if (src && !isWebLink(src)) {
|
|
209
|
+
const attrName = el.getAttribute("src") ? "src" : "href";
|
|
210
|
+
const src = el.getAttribute(attrName);
|
|
211
|
+
if (src && !isWebLink(src) && !src.startsWith("#") && !src.startsWith("/")) {
|
|
204
212
|
const srcPath = path.join(rootDir, srcDir, src);
|
|
205
213
|
const destPath = path.join(outDirPath, src);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
214
|
+
try {
|
|
215
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
216
|
+
await fs.copyFile(srcPath, destPath);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Failed to copy resource referenced by <${el.tagName.toLowerCase()} ${attrName}="${src}">` +
|
|
220
|
+
(getComponentSource(el) ? ` in component "${getComponentSource(el)}"` : "") +
|
|
221
|
+
`: ${err.message || err}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
209
224
|
}
|
|
210
225
|
|
|
211
226
|
const styles = el.getAttribute("style");
|
|
212
|
-
|
|
213
227
|
if (styles) {
|
|
214
228
|
let stylesMatch;
|
|
215
229
|
while ((stylesMatch = inlinePathsRegex.exec(styles)) !== null) {
|
|
216
230
|
let filePath = stylesMatch[1].trim();
|
|
217
|
-
|
|
218
231
|
if (
|
|
219
232
|
(filePath.startsWith('"') && filePath.endsWith('"')) ||
|
|
220
233
|
(filePath.startsWith("'") && filePath.endsWith("'"))
|
|
221
234
|
) {
|
|
222
235
|
filePath = filePath.slice(1, -1);
|
|
223
236
|
}
|
|
224
|
-
|
|
225
237
|
const srcPath = path.join(rootDir, srcDir, filePath);
|
|
226
238
|
const destPath = path.join(outDirPath, filePath);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
239
|
+
try {
|
|
240
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
241
|
+
await fs.copyFile(srcPath, destPath);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Failed to copy resource from inline style on <${el.tagName.toLowerCase()}> url("${filePath}")` +
|
|
245
|
+
(getComponentSource(el) ? ` in component "${getComponentSource(el)}"` : "") +
|
|
246
|
+
`: ${err.message || err}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
230
249
|
}
|
|
231
250
|
}
|
|
232
251
|
}
|
|
233
252
|
|
|
234
|
-
const cssAssets = [...getCssAssets(scopesCss), ...getCssAssets(globalCss)];
|
|
253
|
+
const cssAssets = [...getCssAssets(scopesCss || ""), ...getCssAssets(globalCss || "")];
|
|
235
254
|
for (const assetPath of cssAssets) {
|
|
236
255
|
try {
|
|
237
256
|
if (assetPath.startsWith('http:') || assetPath.startsWith('https:')) continue;
|
|
238
257
|
const srcPath = path.join(rootDir, srcDir, assetPath);
|
|
239
258
|
const destPath = path.join(outDirPath, assetPath);
|
|
240
|
-
|
|
241
259
|
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
242
260
|
await fs.copyFile(srcPath, destPath);
|
|
243
261
|
} catch (err) {
|
|
244
|
-
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Failed to copy CSS asset "${assetPath}" referenced from stylesheets: ${err.message || err}`
|
|
264
|
+
);
|
|
245
265
|
}
|
|
246
266
|
}
|
|
247
267
|
} catch (err) {
|
|
248
|
-
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Copies the static/ directory from source to output.
|
|
274
|
+
* The entire static/ directory tree is copied recursively.
|
|
275
|
+
* Silently skips if static/ does not exist.
|
|
276
|
+
* @param {import("fs").PathLike} srcPath - Source directory path
|
|
277
|
+
* @param {import("fs").PathLike} outDirPath - Output directory path
|
|
278
|
+
*/
|
|
279
|
+
export async function copyStaticDir(srcPath, outDirPath) {
|
|
280
|
+
const staticSrc = path.join(srcPath, "static");
|
|
281
|
+
const staticDest = path.join(outDirPath, "static");
|
|
282
|
+
try {
|
|
283
|
+
await fs.access(staticSrc);
|
|
284
|
+
await fs.cp(staticSrc, staticDest, { recursive: true, force: true });
|
|
285
|
+
} catch {
|
|
286
|
+
// static/ directory doesn't exist — nothing to copy
|
|
249
287
|
}
|
|
250
288
|
}
|
package/compiler/utils.js
CHANGED
|
@@ -24,28 +24,51 @@ export function throwError(err) {
|
|
|
24
24
|
* export default function Button() { return { body: template }; }
|
|
25
25
|
*/
|
|
26
26
|
export async function loadWithAssets(filePath) {
|
|
27
|
-
let code
|
|
27
|
+
let code;
|
|
28
|
+
try {
|
|
29
|
+
code = await fs.readFile(filePath, "utf8");
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throwError(`Failed to read component file "${filePath}": ${err.message || err}`);
|
|
32
|
+
}
|
|
28
33
|
|
|
29
34
|
const importRegex = /import\s+(\w+)\s+from\s+["'](.+?\.(html|css))["'];?/g;
|
|
30
35
|
|
|
36
|
+
const replacements = [];
|
|
31
37
|
let match;
|
|
32
38
|
while ((match = importRegex.exec(code))) {
|
|
33
39
|
const varName = match[1];
|
|
34
40
|
const relPath = match[2];
|
|
35
|
-
|
|
36
41
|
const absPath = path.resolve(path.dirname(filePath), relPath);
|
|
37
|
-
let content = await fs.readFile(absPath, "utf8");
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
let content;
|
|
44
|
+
try {
|
|
45
|
+
content = await fs.readFile(absPath, "utf8");
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throwError(
|
|
48
|
+
`Failed to read imported file "${relPath}" (resolved to "${absPath}") ` +
|
|
49
|
+
`for variable "${varName}" in component "${filePath}": ${err.message || err}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
replacements.push({ from: match[0], to: `const ${varName} = ${JSON.stringify(content)};` });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const { from, to } of replacements) {
|
|
57
|
+
code = code.replace(from, to);
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
const dataUrl =
|
|
44
61
|
"data:text/javascript;base64," +
|
|
45
62
|
Buffer.from(code).toString("base64");
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
try {
|
|
65
|
+
const mod = await import(dataUrl);
|
|
66
|
+
return mod;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
throwError(
|
|
69
|
+
`Failed to evaluate component "${filePath}": ${err.message || err}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
49
72
|
}
|
|
50
73
|
|
|
51
74
|
/**
|
|
@@ -111,3 +134,30 @@ export function isWebLink(str) {
|
|
|
111
134
|
return false;
|
|
112
135
|
}
|
|
113
136
|
}
|
|
137
|
+
|
|
138
|
+
const LBRACE_PH = "_%%CHOCOLA-LBRACE%%_";
|
|
139
|
+
const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Replaces HTML entities for curly braces with placeholders before JSDOM parsing.
|
|
143
|
+
* This prevents JSDOM from decoding them into literal braces before binding processing.
|
|
144
|
+
* @param {string} html - Raw HTML content
|
|
145
|
+
* @returns {string} - HTML with curly brace entities replaced
|
|
146
|
+
*/
|
|
147
|
+
export function protectCurlyBraces(html) {
|
|
148
|
+
return html
|
|
149
|
+
.replace(/&(?:lbrace|#123|#x7B);/gi, LBRACE_PH)
|
|
150
|
+
.replace(/&(?:rcub|#125|#x7D);/gi, RBRACE_PH);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Restores curly brace placeholders back to literal characters.
|
|
155
|
+
* Called after binding processing is complete and before final output.
|
|
156
|
+
* @param {string} html - Serialized HTML with placeholders
|
|
157
|
+
* @returns {string} - HTML with literal curly braces restored
|
|
158
|
+
*/
|
|
159
|
+
export function restoreCurlyBraces(html) {
|
|
160
|
+
return html
|
|
161
|
+
.replace(/_%%CHOCOLA-LBRACE%%_/g, "{")
|
|
162
|
+
.replace(/_%%CHOCOLA-RBRACE%%_/g, "}");
|
|
163
|
+
}
|
package/dev/index.js
CHANGED
|
@@ -112,10 +112,10 @@ export async function serve(__rootdir) {
|
|
|
112
112
|
if (error) {
|
|
113
113
|
if (error.code === "ENOENT") {
|
|
114
114
|
res.writeHead(404, { "Content-Type": "text/html" });
|
|
115
|
-
res.end("<h1>404 Not Found</h1>", "utf-8");
|
|
115
|
+
res.end("<h1>404 Not Found</h1><p>The requested resource was not found on this server.</p>", "utf-8");
|
|
116
116
|
} else {
|
|
117
117
|
res.writeHead(500);
|
|
118
|
-
res.end("
|
|
118
|
+
res.end("Internal server error: " + error.code);
|
|
119
119
|
}
|
|
120
120
|
} else {
|
|
121
121
|
if (extname === ".html" || extname === ".htm") {
|
package/package.json
CHANGED
package/utils.js
CHANGED
|
@@ -12,6 +12,6 @@ export async function getChocolaConfig(__rootdir) {
|
|
|
12
12
|
const config = await fs.readFile(path.join(__rootdir, "chocola.config.json"), "utf-8");
|
|
13
13
|
return JSON.parse(config);
|
|
14
14
|
} catch(err) {
|
|
15
|
-
throwError("An error
|
|
15
|
+
throwError("An error occurred while fetching the Chocola config file:\n" + err);
|
|
16
16
|
}
|
|
17
17
|
}
|