chocola 2.0.0-next.1 → 2.0.0-next.4
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 +32 -45
- package/compiler/component-processor.js +292 -249
- package/compiler/config.js +1 -54
- package/compiler/dom-processor.js +3 -53
- package/compiler/fs.js +0 -11
- package/compiler/index.js +16 -36
- package/compiler/pipeline.js +7 -188
- package/compiler/runtime-generator.js +0 -6
- package/compiler/utils.js +17 -99
- package/dev/index.js +1 -7
- package/package.json +2 -3
- package/utils.js +0 -5
package/compiler/utils.js
CHANGED
|
@@ -3,85 +3,15 @@ import path from "path";
|
|
|
3
3
|
import { pathToFileURL } from "url";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Throws a formatted error message and exits
|
|
8
|
-
* @param {string|Error} err - Error message or Error object
|
|
9
|
-
* @throws {Error}
|
|
10
|
-
*/
|
|
11
6
|
export function throwError(err) {
|
|
12
7
|
console.log(chalk.red.bold("Error!"), "A fatal error has occurred:\n");
|
|
13
8
|
throw new Error(err);
|
|
14
9
|
}
|
|
15
10
|
|
|
16
|
-
/**
|
|
17
|
-
* Loads a JavaScript module and inlines asset imports (HTML/CSS files)
|
|
18
|
-
* This allows components to import external template files as strings
|
|
19
|
-
* @param {import("fs").PathLike} filePath - Path to the JavaScript file
|
|
20
|
-
* @returns {Promise<object>} - The imported module
|
|
21
|
-
* @example
|
|
22
|
-
* // Component with asset import
|
|
23
|
-
* import template from "./button.html";
|
|
24
|
-
* export default function Button() { return { body: template }; }
|
|
25
|
-
*/
|
|
26
|
-
export async function loadWithAssets(filePath) {
|
|
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
|
-
}
|
|
33
|
-
|
|
34
|
-
const importRegex = /import\s+(\w+)\s+from\s+["'](.+?\.(html|css))["'];?/g;
|
|
35
|
-
|
|
36
|
-
const replacements = [];
|
|
37
|
-
let match;
|
|
38
|
-
while ((match = importRegex.exec(code))) {
|
|
39
|
-
const varName = match[1];
|
|
40
|
-
const relPath = match[2];
|
|
41
|
-
const absPath = path.resolve(path.dirname(filePath), relPath);
|
|
42
|
-
|
|
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);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const dataUrl =
|
|
61
|
-
"data:text/javascript;base64," +
|
|
62
|
-
Buffer.from(code).toString("base64");
|
|
63
|
-
|
|
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
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Generates a random ID string
|
|
76
|
-
* @param {Array} collection - Array to track used IDs (prevents duplicates)
|
|
77
|
-
* @param {number} length - Desired length of the ID (default: 10)
|
|
78
|
-
* @returns {string} - Unique random ID
|
|
79
|
-
*/
|
|
80
11
|
export function genRandomId(collection = null, length = 10, lettersOnly = false) {
|
|
81
12
|
let id;
|
|
82
13
|
if (lettersOnly) {
|
|
83
|
-
|
|
84
|
-
id = Array.from({ length }, () => letters[Math.floor(Math.random() * letters.length)]).join("");
|
|
14
|
+
id = Array.from({ length }, () => ID_LETTERS[Math.floor(Math.random() * ID_LETTERS.length)]).join("");
|
|
85
15
|
} else {
|
|
86
16
|
id = Math.random().toString(36).substring(2, length + 2);
|
|
87
17
|
}
|
|
@@ -94,23 +24,14 @@ export function genRandomId(collection = null, length = 10, lettersOnly = false)
|
|
|
94
24
|
}
|
|
95
25
|
}
|
|
96
26
|
|
|
97
|
-
/**
|
|
98
|
-
* Increments a letter or sequence of letters like Excel columns (a → b, z → aa)
|
|
99
|
-
* @param {string} letters - Letter(s) to increment
|
|
100
|
-
* @returns {string} - Incremented letter(s)
|
|
101
|
-
* @example
|
|
102
|
-
* incrementAlfabet("a") // "b"
|
|
103
|
-
* incrementAlfabet("z") // "aa"
|
|
104
|
-
*/
|
|
105
27
|
export function incrementAlfabet(letters) {
|
|
106
|
-
const alfabet = "abcdefghijklmnopqrstuvwxyz";
|
|
107
28
|
let arr = letters.split("");
|
|
108
29
|
let i = arr.length - 1;
|
|
109
30
|
|
|
110
31
|
while (i >= 0) {
|
|
111
|
-
let pos =
|
|
32
|
+
let pos = arr[i].charCodeAt(0) - 97;
|
|
112
33
|
if (pos < 25) {
|
|
113
|
-
arr[i] =
|
|
34
|
+
arr[i] = String.fromCharCode(pos + 97 + 1);
|
|
114
35
|
return arr.join("");
|
|
115
36
|
} else {
|
|
116
37
|
arr[i] = "a";
|
|
@@ -121,11 +42,6 @@ export function incrementAlfabet(letters) {
|
|
|
121
42
|
return "a" + arr.join("");
|
|
122
43
|
}
|
|
123
44
|
|
|
124
|
-
/**
|
|
125
|
-
* Checks if a string is a valid HTTP/HTTPS URL
|
|
126
|
-
* @param {string} str - String to check
|
|
127
|
-
* @returns {boolean} - True if valid web link, false otherwise
|
|
128
|
-
*/
|
|
129
45
|
export function isWebLink(str) {
|
|
130
46
|
try {
|
|
131
47
|
const url = new URL(str);
|
|
@@ -138,26 +54,28 @@ export function isWebLink(str) {
|
|
|
138
54
|
const LBRACE_PH = "_%%CHOCOLA-LBRACE%%_";
|
|
139
55
|
const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
|
|
140
56
|
|
|
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
57
|
export function protectCurlyBraces(html) {
|
|
148
58
|
return html
|
|
149
59
|
.replace(/&(?:lbrace|#123|#x7B);/gi, LBRACE_PH)
|
|
150
60
|
.replace(/&(?:rcub|#125|#x7D);/gi, RBRACE_PH);
|
|
151
61
|
}
|
|
152
62
|
|
|
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
63
|
export function restoreCurlyBraces(html) {
|
|
160
64
|
return html
|
|
161
65
|
.replace(/_%%CHOCOLA-LBRACE%%_/g, "{")
|
|
162
66
|
.replace(/_%%CHOCOLA-RBRACE%%_/g, "}");
|
|
163
67
|
}
|
|
68
|
+
|
|
69
|
+
const ID_LETTERS = "abcdefghijklmnopqrstuvwxyz";
|
|
70
|
+
const compiledCache = new Map();
|
|
71
|
+
export function compileExpression(expr, useCtx) {
|
|
72
|
+
const key = useCtx ? `ctx:${expr}` : `raw:${expr}`;
|
|
73
|
+
let fn = compiledCache.get(key);
|
|
74
|
+
if (!fn) {
|
|
75
|
+
fn = useCtx
|
|
76
|
+
? new Function("ctx", `with(ctx) { return (${expr}); }`)
|
|
77
|
+
: new Function(`"use strict"; return (${expr})`);
|
|
78
|
+
compiledCache.set(key, fn);
|
|
79
|
+
}
|
|
80
|
+
return fn;
|
|
81
|
+
}
|
package/dev/index.js
CHANGED
|
@@ -29,7 +29,6 @@ export async function serve(__rootdir) {
|
|
|
29
29
|
else { console.warn(chalk.bold.yellow("WARNING!"), `port not defined in chocola.config.json file: using default ${__config.port} port.`) }
|
|
30
30
|
|
|
31
31
|
const srcDir = paths.src;
|
|
32
|
-
const componentsDir = paths.components;
|
|
33
32
|
|
|
34
33
|
let compileTimeout;
|
|
35
34
|
const scheduleRecompile = () => {
|
|
@@ -51,11 +50,6 @@ export async function serve(__rootdir) {
|
|
|
51
50
|
scheduleRecompile();
|
|
52
51
|
}
|
|
53
52
|
});
|
|
54
|
-
fs.watch(componentsDir, { recursive: true }, (eventType, filename) => {
|
|
55
|
-
if (filename && !filename.includes("node_modules")) {
|
|
56
|
-
scheduleRecompile();
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
53
|
|
|
60
54
|
const server = http.createServer((req, res) => {
|
|
61
55
|
if (req.url === "/api/hot-reload") {
|
|
@@ -174,5 +168,5 @@ app.build(__dirname);
|
|
|
174
168
|
* @example
|
|
175
169
|
* @param {PathLike} __rootdir the directory where your Chocola App is
|
|
176
170
|
*/
|
|
177
|
-
server(__rootdir) { serve(__rootdir) }
|
|
171
|
+
async server(__rootdir) { return serve(__rootdir) }
|
|
178
172
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chocola",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.4",
|
|
4
4
|
"description": "The sweetest way to build the web",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web",
|
|
@@ -32,8 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"exports": {
|
|
34
34
|
"./compiler": "./compiler/index.js",
|
|
35
|
-
"./dev": "./dev/index.js"
|
|
36
|
-
"./types": "./types/index.js"
|
|
35
|
+
"./dev": "./dev/index.js"
|
|
37
36
|
},
|
|
38
37
|
"dependencies": {
|
|
39
38
|
"chalk": "^5.6.2",
|
package/utils.js
CHANGED
|
@@ -2,11 +2,6 @@ import { promises as fs } from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { throwError } from "./compiler/utils.js";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Returns `chocola.config.json` as an object.
|
|
7
|
-
* @param {import("fs").PathLike} __rootdir
|
|
8
|
-
* @returns {Object}
|
|
9
|
-
*/
|
|
10
5
|
export async function getChocolaConfig(__rootdir) {
|
|
11
6
|
try {
|
|
12
7
|
const config = await fs.readFile(path.join(__rootdir, "chocola.config.json"), "utf-8");
|