chocola 2.0.0-next.1 → 2.0.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +45 -35
- package/bin/chocola.js +227 -0
- package/compiler/chalk.js +34 -0
- package/compiler/component-processor.js +771 -402
- package/compiler/config.js +43 -66
- package/compiler/dom-processor.js +12 -75
- package/compiler/fs.js +0 -11
- package/compiler/index.js +87 -230
- package/compiler/module-graph.js +257 -0
- package/compiler/pipeline.js +56 -196
- package/compiler/render.js +183 -0
- package/compiler/runtime-generator.js +20 -15
- package/compiler/utils.js +79 -126
- package/dev/index.js +54 -22
- package/package.json +23 -12
- package/parser/component.js +340 -0
- package/parser/context.js +59 -0
- package/parser/css.js +96 -0
- package/parser/index.js +6 -0
- package/parser/script.js +485 -0
- package/parser/script.test.js +103 -0
- package/parser/template.js +120 -0
- package/parser/utils.js +32 -0
- package/runtime/index.js +312 -0
- package/server/index.js +432 -0
- package/utils.js +113 -8
- package/index.js +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import {
|
|
4
|
+
createDOM,
|
|
5
|
+
validateAppContainer,
|
|
6
|
+
getAppElements,
|
|
7
|
+
getAssetLinks,
|
|
8
|
+
getScriptElements,
|
|
9
|
+
appendRuntimeScript,
|
|
10
|
+
appendStylesheetLink,
|
|
11
|
+
serializeDOM,
|
|
12
|
+
} from "./dom-processor.js";
|
|
13
|
+
import { processAllComponents } from "./component-processor.js";
|
|
14
|
+
import { generateRuntimeScript } from "./runtime-generator.js";
|
|
15
|
+
import { deterministicHash, throwError, warnConstantCondition, findElementLine } from "./utils.js";
|
|
16
|
+
import { compileExpr, evaluateConstant, hasMountIf, getMountIf, removeMountIf } from "../parser/index.js";
|
|
17
|
+
import {
|
|
18
|
+
processStylesheet,
|
|
19
|
+
processIcons,
|
|
20
|
+
processScript,
|
|
21
|
+
copyStaticDir,
|
|
22
|
+
} from "./pipeline.js";
|
|
23
|
+
|
|
24
|
+
function processPageConditionals(parent, sourceFile, sourceContent, ctx = {}) {
|
|
25
|
+
const ctxProxy = new Proxy(ctx, {
|
|
26
|
+
has() { return true; },
|
|
27
|
+
get(target, key) { return target[key]; },
|
|
28
|
+
});
|
|
29
|
+
const children = [...parent.children];
|
|
30
|
+
let chainActive = false;
|
|
31
|
+
let chainRendered = false;
|
|
32
|
+
|
|
33
|
+
const getLocation = (child) => {
|
|
34
|
+
if (!sourceContent) return sourceFile;
|
|
35
|
+
const lineNum = findElementLine(sourceContent, child.outerHTML);
|
|
36
|
+
return lineNum !== null ? `${sourceFile}:${lineNum}` : sourceFile;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const warnIfConstant = (expr, child, attr) => {
|
|
40
|
+
const { constant, value } = evaluateConstant(expr);
|
|
41
|
+
if (!constant) return;
|
|
42
|
+
warnConstantCondition(getLocation(child), child.tagName.toLowerCase(), attr, Boolean(value));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (const child of children) {
|
|
46
|
+
const hasIf = child.hasAttribute("if");
|
|
47
|
+
const hasDelIf = hasMountIf(child);
|
|
48
|
+
const hasElif = child.hasAttribute("elif");
|
|
49
|
+
const hasElse = child.hasAttribute("else");
|
|
50
|
+
|
|
51
|
+
if (hasIf || hasDelIf || hasElif) {
|
|
52
|
+
const stripBraces = (raw) => raw.startsWith("{") ? raw.slice(1, -1) : raw;
|
|
53
|
+
if (hasIf) warnIfConstant(stripBraces(child.getAttribute("if")), child, "if");
|
|
54
|
+
if (hasDelIf) warnIfConstant(stripBraces(getMountIf(child)), child, "mount:if");
|
|
55
|
+
if (hasElif) warnIfConstant(stripBraces(child.getAttribute("elif")), child, "elif");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (hasElif || hasElse) {
|
|
59
|
+
if (!chainActive) {
|
|
60
|
+
const tag = child.tagName.toLowerCase();
|
|
61
|
+
const attr = hasElif ? "elif" : "else";
|
|
62
|
+
throwError(`${getLocation(child)}\n <${tag}> has ${attr} without a preceding if/mount:if sibling`);
|
|
63
|
+
}
|
|
64
|
+
if (chainRendered) { child.remove(); continue; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (hasIf) {
|
|
68
|
+
const raw = child.getAttribute("if");
|
|
69
|
+
const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
|
|
70
|
+
const fn = compileExpr(expr, true);
|
|
71
|
+
const result = fn(ctxProxy);
|
|
72
|
+
chainActive = true;
|
|
73
|
+
if (result) {
|
|
74
|
+
chainRendered = true;
|
|
75
|
+
} else {
|
|
76
|
+
child.style.display = "none";
|
|
77
|
+
chainRendered = false;
|
|
78
|
+
}
|
|
79
|
+
child.removeAttribute("if");
|
|
80
|
+
} else if (hasDelIf) {
|
|
81
|
+
const raw = getMountIf(child);
|
|
82
|
+
const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
|
|
83
|
+
const fn = compileExpr(expr, true);
|
|
84
|
+
const result = fn(ctxProxy);
|
|
85
|
+
chainActive = true;
|
|
86
|
+
if (result) {
|
|
87
|
+
chainRendered = true;
|
|
88
|
+
} else {
|
|
89
|
+
child.remove();
|
|
90
|
+
chainRendered = false;
|
|
91
|
+
}
|
|
92
|
+
removeMountIf(child);
|
|
93
|
+
} else if (hasElif) {
|
|
94
|
+
const raw = child.getAttribute("elif");
|
|
95
|
+
const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
|
|
96
|
+
const fn = compileExpr(expr, true);
|
|
97
|
+
const result = fn(ctxProxy);
|
|
98
|
+
if (result) {
|
|
99
|
+
chainRendered = true;
|
|
100
|
+
} else {
|
|
101
|
+
child.remove();
|
|
102
|
+
}
|
|
103
|
+
child.removeAttribute("elif");
|
|
104
|
+
} else if (hasElse) {
|
|
105
|
+
chainRendered = true;
|
|
106
|
+
chainActive = false;
|
|
107
|
+
child.removeAttribute("else");
|
|
108
|
+
} else {
|
|
109
|
+
chainActive = false;
|
|
110
|
+
chainRendered = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (child.parentNode) {
|
|
114
|
+
processPageConditionals(child, sourceFile, sourceContent, ctx);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function processAssets(doc, graph, out) {
|
|
120
|
+
const { stylesheets, icons } = getAssetLinks(doc);
|
|
121
|
+
const scripts = getScriptElements(doc);
|
|
122
|
+
|
|
123
|
+
for (const link of stylesheets) {
|
|
124
|
+
await processStylesheet(link, graph.rootDir, graph.config.srcDir, out);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const link of icons) {
|
|
128
|
+
await processIcons(link, graph.rootDir, graph.config.srcDir, out);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const script of scripts) {
|
|
132
|
+
await processScript(doc, script, graph.rootDir, graph.config.srcDir, out);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function renderPage(graph, ctx = {}) {
|
|
137
|
+
const out = {
|
|
138
|
+
files: [],
|
|
139
|
+
copies: [],
|
|
140
|
+
ids: [],
|
|
141
|
+
outDir: graph.paths.outDir,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const page = graph.page;
|
|
145
|
+
const dom = createDOM(page.source);
|
|
146
|
+
const doc = dom.document;
|
|
147
|
+
const appContainer = validateAppContainer(doc);
|
|
148
|
+
|
|
149
|
+
processPageConditionals(appContainer, page.sourcePath, dom.protectedContent, ctx);
|
|
150
|
+
|
|
151
|
+
const appElements = getAppElements(appContainer);
|
|
152
|
+
const { runtimeScript, scopesCss, hashMap, csrClasses } = processAllComponents(
|
|
153
|
+
appElements,
|
|
154
|
+
graph.loadedComponents,
|
|
155
|
+
page.sourcePath,
|
|
156
|
+
page.source,
|
|
157
|
+
ctx,
|
|
158
|
+
graph.config.treeShakeRuntime,
|
|
159
|
+
graph.originalComponentNames
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const csrSource = await fs.readFile(new URL("../runtime/index.js", import.meta.url), "utf-8");
|
|
163
|
+
const runtimeFiles = generateRuntimeScript(runtimeScript, csrSource, csrClasses);
|
|
164
|
+
|
|
165
|
+
await processAssets(doc, graph, out);
|
|
166
|
+
|
|
167
|
+
if (scopesCss) {
|
|
168
|
+
const fileName = "sc-" + deterministicHash(scopesCss, 6) + ".css";
|
|
169
|
+
out.files.push({ path: fileName, content: scopesCss });
|
|
170
|
+
appendStylesheetLink(doc, fileName);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const file of runtimeFiles) {
|
|
174
|
+
out.files.push(file);
|
|
175
|
+
appendRuntimeScript(doc, file.path);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const html = await serializeDOM(dom);
|
|
179
|
+
|
|
180
|
+
await copyStaticDir(graph.paths.src, out);
|
|
181
|
+
|
|
182
|
+
return { html, hashMap, files: out.files, copies: out.copies };
|
|
183
|
+
}
|
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { genRandomId } from "./utils.js";
|
|
1
|
+
import { deterministicHash } from "./utils.js";
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* @param {string} runtimeScript - The JavaScript code for runtime
|
|
8
|
-
* @param {import("fs").PathLike} outDirPath
|
|
9
|
-
* @returns {Promise<string>} - filename of the generated runtime script
|
|
10
|
-
*/
|
|
11
|
-
export async function generateRuntimeScript(runtimeScript, outDirPath) {
|
|
12
|
-
const fileIds = [];
|
|
13
|
-
const runtimeFilename = "run-" + genRandomId(fileIds, 6) + ".js";
|
|
14
|
-
const runtimeFileContents = `document.addEventListener("DOMContentLoaded", () => {${runtimeScript}})`;
|
|
3
|
+
export function generateRuntimeScript(runtimeScript, csrSource, csrClasses) {
|
|
4
|
+
const files = [];
|
|
15
5
|
|
|
16
|
-
|
|
6
|
+
if (csrSource) {
|
|
7
|
+
const source = csrSource.replace(/^export\s+\{?\s*[^}]+\s*}?\s*;?\s*$/m, "");
|
|
8
|
+
const name = "run-" + deterministicHash(source, 6) + ".js";
|
|
9
|
+
files.push({ path: name, content: source });
|
|
10
|
+
}
|
|
17
11
|
|
|
18
|
-
|
|
12
|
+
if (csrClasses) {
|
|
13
|
+
const name = "run-" + deterministicHash(csrClasses, 6) + ".js";
|
|
14
|
+
files.push({ path: name, content: csrClasses });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (runtimeScript) {
|
|
18
|
+
const content = `document.addEventListener("DOMContentLoaded", () => {${runtimeScript}})`;
|
|
19
|
+
const name = "run-" + deterministicHash(content, 6) + ".js";
|
|
20
|
+
files.push({ path: name, content });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return files;
|
|
19
24
|
}
|
package/compiler/utils.js
CHANGED
|
@@ -1,87 +1,93 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { pathToFileURL } from "url";
|
|
4
|
-
import chalk from "chalk";
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import chalk from "./chalk.js";
|
|
5
3
|
|
|
6
|
-
/**
|
|
7
|
-
* Throws a formatted error message and exits
|
|
8
|
-
* @param {string|Error} err - Error message or Error object
|
|
9
|
-
* @throws {Error}
|
|
10
|
-
*/
|
|
11
4
|
export function throwError(err) {
|
|
12
5
|
console.log(chalk.red.bold("Error!"), "A fatal error has occurred:\n");
|
|
13
6
|
throw new Error(err);
|
|
14
7
|
}
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
}
|
|
9
|
+
export function warnConstantCondition(location, tag, attr, expr, truthy) {
|
|
10
|
+
const article = /^[aeiou]/.test(attr) ? "an" : "a";
|
|
11
|
+
console.warn(
|
|
12
|
+
chalk.bold.yellow("WARN "),
|
|
13
|
+
`${chalk.bold.green(location)} ${chalk.dim('—')} ${chalk.blue(`<${tag}>`)} \`${chalk.bold(attr + "={" + expr + "}")}\` is always ${chalk.green(truthy ? "truthy" : "falsy")}`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
33
16
|
|
|
34
|
-
|
|
17
|
+
export function warnUnusedDeclaration(location, kind, name) {
|
|
18
|
+
console.warn(
|
|
19
|
+
chalk.bold.yellow("WARN "),
|
|
20
|
+
`${chalk.bold.green(location)} ${chalk.dim('—')} ${kind} \`${chalk.blue(name)}\` is never used`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const absPath = path.resolve(path.dirname(filePath), relPath);
|
|
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
|
+
}
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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);
|
|
51
40
|
}
|
|
52
|
-
|
|
53
|
-
replacements.push({ from: match[0], to: `const ${varName} = ${JSON.stringify(content)};` });
|
|
54
41
|
}
|
|
42
|
+
return outerHTML;
|
|
43
|
+
}
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
function escapeRegex(str) {
|
|
46
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
+
}
|
|
59
48
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
function flexibleStartTagRegex(startTag) {
|
|
50
|
+
return new RegExp(
|
|
51
|
+
startTag
|
|
52
|
+
.split(/\s+/)
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.map(escapeRegex)
|
|
55
|
+
.join("\\s+")
|
|
56
|
+
);
|
|
57
|
+
}
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
}
|
|
71
83
|
}
|
|
84
|
+
return null;
|
|
72
85
|
}
|
|
73
86
|
|
|
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
87
|
export function genRandomId(collection = null, length = 10, lettersOnly = false) {
|
|
81
88
|
let id;
|
|
82
89
|
if (lettersOnly) {
|
|
83
|
-
|
|
84
|
-
id = Array.from({ length }, () => letters[Math.floor(Math.random() * letters.length)]).join("");
|
|
90
|
+
id = Array.from({ length }, () => ID_LETTERS[Math.floor(Math.random() * ID_LETTERS.length)]).join("");
|
|
85
91
|
} else {
|
|
86
92
|
id = Math.random().toString(36).substring(2, length + 2);
|
|
87
93
|
}
|
|
@@ -94,70 +100,17 @@ export function genRandomId(collection = null, length = 10, lettersOnly = false)
|
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
* incrementAlfabet("a") // "b"
|
|
103
|
-
* incrementAlfabet("z") // "aa"
|
|
104
|
-
*/
|
|
105
|
-
export function incrementAlfabet(letters) {
|
|
106
|
-
const alfabet = "abcdefghijklmnopqrstuvwxyz";
|
|
107
|
-
let arr = letters.split("");
|
|
108
|
-
let i = arr.length - 1;
|
|
109
|
-
|
|
110
|
-
while (i >= 0) {
|
|
111
|
-
let pos = alfabet.indexOf(arr[i]);
|
|
112
|
-
if (pos < 25) {
|
|
113
|
-
arr[i] = alfabet[pos + 1];
|
|
114
|
-
return arr.join("");
|
|
115
|
-
} else {
|
|
116
|
-
arr[i] = "a";
|
|
117
|
-
i--;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return "a" + arr.join("");
|
|
122
|
-
}
|
|
123
|
-
|
|
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
|
-
export function isWebLink(str) {
|
|
130
|
-
try {
|
|
131
|
-
const url = new URL(str);
|
|
132
|
-
return url.protocol === "http:" || url.protocol === "https:";
|
|
133
|
-
} catch (e) {
|
|
134
|
-
return false;
|
|
103
|
+
export function deterministicHash(content, length = 8) {
|
|
104
|
+
const hash = createHash("sha256").update(content).digest();
|
|
105
|
+
let result = "";
|
|
106
|
+
for (let i = 0; i < length; i++) {
|
|
107
|
+
result += String.fromCharCode(97 + (hash[i % hash.length] % 26));
|
|
135
108
|
}
|
|
109
|
+
return result;
|
|
136
110
|
}
|
|
137
111
|
|
|
138
|
-
|
|
139
|
-
|
|
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);
|
|
112
|
+
export function runtimeFunctionId(compName) {
|
|
113
|
+
return "r_" + deterministicHash(compName, 8);
|
|
151
114
|
}
|
|
152
115
|
|
|
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
|
-
}
|
|
116
|
+
const ID_LETTERS = "abcdefghijklmnopqrstuvwxyz";
|
package/dev/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import http from "http";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import chalk from "chalk";
|
|
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, isMissingConfigFile, queueConfigWarning } from "../utils.js";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const warnedDevHostname = new Set();
|
|
10
|
+
const warnedDevPort = new Set();
|
|
11
11
|
|
|
12
|
+
export async function serve(__rootdir, { silent = false, port: cliPort, hostname: cliHostname, open } = {}) {
|
|
12
13
|
let __outdir = "dist";
|
|
13
14
|
let __config = {
|
|
14
15
|
hostname: "localhost",
|
|
@@ -17,19 +18,43 @@ export async function serve(__rootdir) {
|
|
|
17
18
|
|
|
18
19
|
let lastBuildTime = Date.now();
|
|
19
20
|
|
|
20
|
-
const fullConfig = await
|
|
21
|
-
const config = await loadConfig(__rootdir);
|
|
21
|
+
const fullConfig = await getConfig(__rootdir, { silent });
|
|
22
|
+
const config = await loadConfig(__rootdir, { silent });
|
|
22
23
|
const paths = resolvePaths(__rootdir, config);
|
|
23
|
-
const devConfig = fullConfig.dev;
|
|
24
24
|
|
|
25
|
-
if (
|
|
26
|
-
|
|
25
|
+
if (cliPort) {
|
|
26
|
+
__config.port = cliPort;
|
|
27
|
+
} else if (!silent) {
|
|
28
|
+
if (isMissingConfigFile(fullConfig)) {
|
|
29
|
+
// top-level already warned; use defaults silently
|
|
30
|
+
} else if (fullConfig.dev != null) {
|
|
31
|
+
const devConfig = fullConfig.dev;
|
|
32
|
+
if (devConfig.port) { __config.port = devConfig.port }
|
|
33
|
+
else if (!warnedDevPort.has(__rootdir)) {
|
|
34
|
+
warnedDevPort.add(__rootdir);
|
|
35
|
+
queueConfigWarning(__rootdir, chalk.bold.yellow("WARNING!"), `dev.port not defined in chocola.config.json file: using default ${__config.port} dev.port.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
27
39
|
|
|
28
|
-
if (
|
|
29
|
-
|
|
40
|
+
if (cliHostname) {
|
|
41
|
+
__config.hostname = cliHostname;
|
|
42
|
+
} else if (!silent) {
|
|
43
|
+
if (!cliPort && isMissingConfigFile(fullConfig)) {
|
|
44
|
+
// top-level already warned
|
|
45
|
+
} else if (fullConfig.dev != null) {
|
|
46
|
+
const devConfig = fullConfig.dev;
|
|
47
|
+
if (devConfig.hostname) { __config.hostname = devConfig.hostname }
|
|
48
|
+
else if (!warnedDevHostname.has(__rootdir)) {
|
|
49
|
+
warnedDevHostname.add(__rootdir);
|
|
50
|
+
queueConfigWarning(__rootdir, chalk.bold.yellow("WARNING!"), `dev.hostname not defined in chocola.config.json file: using default ${__config.hostname} dev.hostname.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await compile(__rootdir);
|
|
30
56
|
|
|
31
57
|
const srcDir = paths.src;
|
|
32
|
-
const componentsDir = paths.components;
|
|
33
58
|
|
|
34
59
|
let compileTimeout;
|
|
35
60
|
const scheduleRecompile = () => {
|
|
@@ -51,11 +76,6 @@ export async function serve(__rootdir) {
|
|
|
51
76
|
scheduleRecompile();
|
|
52
77
|
}
|
|
53
78
|
});
|
|
54
|
-
fs.watch(componentsDir, { recursive: true }, (eventType, filename) => {
|
|
55
|
-
if (filename && !filename.includes("node_modules")) {
|
|
56
|
-
scheduleRecompile();
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
79
|
|
|
60
80
|
const server = http.createServer((req, res) => {
|
|
61
81
|
if (req.url === "/api/hot-reload") {
|
|
@@ -149,10 +169,22 @@ export async function serve(__rootdir) {
|
|
|
149
169
|
});
|
|
150
170
|
});
|
|
151
171
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
172
|
+
server.listen(__config.port, __config.hostname, async () => {
|
|
173
|
+
console.log('Live server running at', chalk.hex("#68C4EE").underline(`http://${__config.hostname}:${__config.port}/`));
|
|
174
|
+
if (open) {
|
|
175
|
+
const url = __config.hostname === "0.0.0.0" ? `http://localhost:${__config.port}` : `http://${__config.hostname}:${__config.port}`;
|
|
176
|
+
try {
|
|
177
|
+
const openPkg = await import("open");
|
|
178
|
+
openPkg.default(url);
|
|
179
|
+
} catch {
|
|
180
|
+
const { exec } = await import("child_process");
|
|
181
|
+
const cmd = process.platform === "win32" ? `start ${url}` : process.platform === "darwin" ? `open ${url}` : `xdg-open ${url}`;
|
|
182
|
+
exec(cmd);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return server;
|
|
187
|
+
}
|
|
156
188
|
|
|
157
189
|
/**
|
|
158
190
|
* An intrinsic object that contains the Chocola Dev methods.
|
|
@@ -174,5 +206,5 @@ app.build(__dirname);
|
|
|
174
206
|
* @example
|
|
175
207
|
* @param {PathLike} __rootdir the directory where your Chocola App is
|
|
176
208
|
*/
|
|
177
|
-
server(__rootdir) { serve(__rootdir) }
|
|
209
|
+
async server(__rootdir) { return serve(__rootdir) }
|
|
178
210
|
};
|
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.11",
|
|
4
4
|
"description": "The sweetest way to build the web",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web",
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
],
|
|
14
14
|
"homepage": "https://chocolaframework.web.app/",
|
|
15
15
|
"bugs": {
|
|
16
|
-
"url": "https://github.com/
|
|
16
|
+
"url": "https://github.com/chocolajs/chocola/issues"
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/
|
|
20
|
+
"url": "git+https://github.com/chocolajs/chocola.git"
|
|
21
21
|
},
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"author": "SadGabi",
|
|
@@ -25,22 +25,33 @@
|
|
|
25
25
|
"main": "index.js",
|
|
26
26
|
"funding": {
|
|
27
27
|
"type": "github",
|
|
28
|
-
"url": "https://github.com/sponsors/
|
|
28
|
+
"url": "https://github.com/sponsors/chocolajs"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"test": "
|
|
31
|
+
"test": "node --test \"tests/**/*.test.js\"",
|
|
32
|
+
"bench": "node bench/index.js",
|
|
33
|
+
"bench:csv": "node bench/index.js --csv"
|
|
32
34
|
},
|
|
33
35
|
"exports": {
|
|
34
36
|
"./compiler": "./compiler/index.js",
|
|
35
37
|
"./dev": "./dev/index.js",
|
|
36
|
-
"./
|
|
38
|
+
"./server": "./server/index.js"
|
|
37
39
|
},
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"jsdom": "^27.4.0"
|
|
40
|
+
"bin": {
|
|
41
|
+
"chocola": "./bin/chocola.js",
|
|
42
|
+
"chjs": "./bin/chocola.js"
|
|
42
43
|
},
|
|
43
|
-
"
|
|
44
|
-
"
|
|
44
|
+
"files": [
|
|
45
|
+
"bin/",
|
|
46
|
+
"compiler/",
|
|
47
|
+
"dev/",
|
|
48
|
+
"server/",
|
|
49
|
+
"runtime/",
|
|
50
|
+
"parser/",
|
|
51
|
+
"utils.js"
|
|
52
|
+
],
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"acorn": "^8.18.0",
|
|
55
|
+
"linkedom": "^0.18.13"
|
|
45
56
|
}
|
|
46
57
|
}
|