chocola 1.1.19 → 1.1.21
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 +95 -0
- package/compiler/config.js +66 -0
- package/compiler/dom-processor.js +96 -0
- package/compiler/fs.js +23 -12
- package/compiler/index.js +148 -174
- package/compiler/pipeline.js +142 -109
- package/compiler/runtime-generator.js +19 -0
- package/compiler/utils.js +78 -43
- package/dev/index.js +26 -1
- package/package.json +4 -3
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { JSDOM } from "jsdom";
|
|
2
|
+
import { extractContextFromElement } from "./dom-processor.js";
|
|
3
|
+
import { genRandomId, incrementAlfabet } from "./utils.js";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Processes a single component element and inserts it into the DOM
|
|
8
|
+
* @param {Element} element
|
|
9
|
+
* @param {Map} loadedComponents
|
|
10
|
+
* @param {Array} runtimeChunks
|
|
11
|
+
* @param {Array} compIdColl
|
|
12
|
+
* @param {object} letterState - { value: string }
|
|
13
|
+
* @returns {boolean} - true if component was processed, false if not found
|
|
14
|
+
*/
|
|
15
|
+
export function processComponentElement(
|
|
16
|
+
element,
|
|
17
|
+
loadedComponents,
|
|
18
|
+
runtimeChunks,
|
|
19
|
+
compIdColl,
|
|
20
|
+
letterState
|
|
21
|
+
) {
|
|
22
|
+
const tagName = element.tagName.toLowerCase();
|
|
23
|
+
const compName = tagName + ".js";
|
|
24
|
+
const ctx = extractContextFromElement(element);
|
|
25
|
+
|
|
26
|
+
const instance = loadedComponents.get(compName);
|
|
27
|
+
if (!instance || instance === undefined) return false;
|
|
28
|
+
|
|
29
|
+
if (instance && instance.body) {
|
|
30
|
+
let body = instance.body;
|
|
31
|
+
body = body.replace(/\$\{ctx\.(\w+)\}/g, (_, key) => ctx[key] || "");
|
|
32
|
+
const fragment = JSDOM.fragment(body);
|
|
33
|
+
const firstChild = fragment.firstChild;
|
|
34
|
+
|
|
35
|
+
if (firstChild && firstChild.nodeType === 1) {
|
|
36
|
+
if (instance.script || instance.effects) {
|
|
37
|
+
const compId = "chid-" + genRandomId(compIdColl);
|
|
38
|
+
firstChild.setAttribute("chid", compId);
|
|
39
|
+
|
|
40
|
+
let script = instance.script && instance.script.toString();
|
|
41
|
+
const letter = getNextLetter(letterState);
|
|
42
|
+
|
|
43
|
+
script = script.replace(/RUNTIME/g, `${letter}RUNTIME`);
|
|
44
|
+
|
|
45
|
+
runtimeChunks.push(`
|
|
46
|
+
const ${letter} = document.querySelector('[chid="${compId}"]');
|
|
47
|
+
${script}
|
|
48
|
+
${letter}RUNTIME(${letter}, ${JSON.stringify(ctx)});`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
element.replaceWith(fragment);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.warn(chalk.yellow(`${compName} component could not be loaded`));
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Processes all components in the app container
|
|
61
|
+
* @param {Element[]} appElements
|
|
62
|
+
* @param {Map} loadedComponents
|
|
63
|
+
* @returns {{
|
|
64
|
+
* runtimeScript: string,
|
|
65
|
+
* hasComponents: boolean
|
|
66
|
+
* }}
|
|
67
|
+
*/
|
|
68
|
+
export function processAllComponents(appElements, loadedComponents) {
|
|
69
|
+
const runtimeChunks = [];
|
|
70
|
+
const compIdColl = [];
|
|
71
|
+
const letterState = { value: null };
|
|
72
|
+
|
|
73
|
+
appElements.forEach(el => {
|
|
74
|
+
processComponentElement(el, loadedComponents, runtimeChunks, compIdColl, letterState);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const runtimeScript = runtimeChunks.join("\n");
|
|
78
|
+
const hasComponents = runtimeChunks.length > 0;
|
|
79
|
+
|
|
80
|
+
return { runtimeScript, hasComponents };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Gets the next letter in sequence or starts with 'a'
|
|
85
|
+
* @param {object} letterState - { value: string }
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
function getNextLetter(letterState) {
|
|
89
|
+
if (!letterState.value) {
|
|
90
|
+
letterState.value = "a";
|
|
91
|
+
} else {
|
|
92
|
+
letterState.value = incrementAlfabet(letterState.value);
|
|
93
|
+
}
|
|
94
|
+
return letterState.value;
|
|
95
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { getChocolaConfig } from "../utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads and merges Chocola configuration with defaults
|
|
7
|
+
* @param {import("fs").PathLike} rootDir
|
|
8
|
+
* @returns {Promise<{
|
|
9
|
+
* srcDir: string,
|
|
10
|
+
* outDir: string,
|
|
11
|
+
* libDir: string,
|
|
12
|
+
* emptyOutDir: boolean
|
|
13
|
+
* }>}
|
|
14
|
+
*/
|
|
15
|
+
export async function loadConfig(rootDir) {
|
|
16
|
+
const config = await getChocolaConfig(rootDir);
|
|
17
|
+
const bundleConfig = config.bundle || {};
|
|
18
|
+
|
|
19
|
+
const srcDir = bundleConfig.srcDir || "src";
|
|
20
|
+
const outDir = bundleConfig.outDir || "dist";
|
|
21
|
+
const libDir = bundleConfig.libDir || "lib";
|
|
22
|
+
const emptyOutDir = bundleConfig.emptyOutDir !== false;
|
|
23
|
+
|
|
24
|
+
logConfigWarnings(bundleConfig, emptyOutDir);
|
|
25
|
+
|
|
26
|
+
return { srcDir, outDir, libDir, emptyOutDir };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function logConfigWarnings(bundleConfig, emptyOutDir) {
|
|
30
|
+
if (!bundleConfig.srcDir) {
|
|
31
|
+
console.warn(
|
|
32
|
+
chalk.bold.yellow("WARNING!"),
|
|
33
|
+
'srcDir not defined in chocola.config.json file: using default "src" directory.'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!bundleConfig.outDir) {
|
|
38
|
+
console.warn(
|
|
39
|
+
chalk.bold.yellow("WARNING!"),
|
|
40
|
+
'outDir not defined in chocola.config.json file: using default "dist" directory.'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!bundleConfig.libDir) {
|
|
45
|
+
console.warn(
|
|
46
|
+
chalk.bold.yellow("WARNING!"),
|
|
47
|
+
'libDir not defined in chocola.config.json file: using default "lib" directory.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(`> using emptyOutDir = ${emptyOutDir}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolves all path directories based on configuration
|
|
56
|
+
* @param {import("fs").PathLike} rootDir
|
|
57
|
+
* @param {object} config
|
|
58
|
+
* @returns {object}
|
|
59
|
+
*/
|
|
60
|
+
export function resolvePaths(rootDir, config) {
|
|
61
|
+
return {
|
|
62
|
+
outDir: path.join(rootDir, config.outDir),
|
|
63
|
+
src: path.join(rootDir, config.srcDir),
|
|
64
|
+
components: path.join(rootDir, config.srcDir, config.libDir),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { JSDOM } from "jsdom";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { throwError } from "./utils.js";
|
|
5
|
+
import { readMyFile } from "./fs.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a JSDOM instance from source index file
|
|
9
|
+
* @param {string} srcIndexContent
|
|
10
|
+
* @returns {JSDOM}
|
|
11
|
+
*/
|
|
12
|
+
export function createDOM(srcIndexContent) {
|
|
13
|
+
return new JSDOM(srcIndexContent);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validates that the index file has an <app> root element
|
|
18
|
+
* @param {Document} doc
|
|
19
|
+
* @throws {Error} if <app> element not found
|
|
20
|
+
*/
|
|
21
|
+
export function validateAppContainer(doc) {
|
|
22
|
+
const appContainer = doc.querySelector("app");
|
|
23
|
+
if (!appContainer) {
|
|
24
|
+
throwError("Index page must have an <app> element");
|
|
25
|
+
}
|
|
26
|
+
return appContainer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extracts all child elements from app container
|
|
31
|
+
* @param {Element} appContainer
|
|
32
|
+
* @returns {Element[]}
|
|
33
|
+
*/
|
|
34
|
+
export function getAppElements(appContainer) {
|
|
35
|
+
return Array.from(appContainer.querySelectorAll("*"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extracts context attributes from element (ctx.* attributes)
|
|
40
|
+
* @param {Element} element
|
|
41
|
+
* @returns {object}
|
|
42
|
+
*/
|
|
43
|
+
export function extractContextFromElement(element) {
|
|
44
|
+
const ctx = {};
|
|
45
|
+
for (const attr of element.attributes) {
|
|
46
|
+
if (attr.name.startsWith("ctx.")) {
|
|
47
|
+
const key = attr.name.slice(4);
|
|
48
|
+
ctx[key] = attr.value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return ctx;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Serializes and formats DOM to pretty HTML
|
|
56
|
+
* @param {JSDOM} dom
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
export async function serializeDOM(dom) {
|
|
60
|
+
const beautify = (await import("js-beautify")).default;
|
|
61
|
+
const finalHtml = dom.serialize();
|
|
62
|
+
return beautify.html(finalHtml, { indent_size: 2 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Writes the final HTML to output directory
|
|
67
|
+
* @param {string} html
|
|
68
|
+
* @param {import("fs").PathLike} outDirPath
|
|
69
|
+
*/
|
|
70
|
+
export async function writeHTMLOutput(html, outDirPath) {
|
|
71
|
+
await fs.writeFile(path.join(outDirPath, "index.html"), html);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets all stylesheet and icon links from document
|
|
76
|
+
* @param {Document} doc
|
|
77
|
+
* @returns {{stylesheets: HTMLLinkElement[], icons: HTMLLinkElement[]}}
|
|
78
|
+
*/
|
|
79
|
+
export function getAssetLinks(doc) {
|
|
80
|
+
const docLinks = Array.from(doc.querySelectorAll("link"));
|
|
81
|
+
const stylesheets = docLinks.filter(link => link.rel === "stylesheet");
|
|
82
|
+
const icons = docLinks.filter(link => link.rel === "icon");
|
|
83
|
+
return { stylesheets, icons };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Appends a script element to document body
|
|
88
|
+
* @param {Document} doc
|
|
89
|
+
* @param {string} filename
|
|
90
|
+
*/
|
|
91
|
+
export function appendRuntimeScript(doc, filename) {
|
|
92
|
+
const runtimeScriptEl = doc.createElement("script");
|
|
93
|
+
runtimeScriptEl.type = "module";
|
|
94
|
+
runtimeScriptEl.src = "./" + filename;
|
|
95
|
+
doc.body.appendChild(runtimeScriptEl);
|
|
96
|
+
}
|
package/compiler/fs.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
2
|
import { throwError } from "./utils.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Reads a file and returns its contents as a UTF-8 string
|
|
6
|
+
* @param {import("fs").PathLike} filePath - Path to the file
|
|
7
|
+
* @returns {Promise<string>} - File contents
|
|
8
|
+
* @throws {Error} - If file cannot be read
|
|
9
|
+
*/
|
|
4
10
|
export async function readMyFile(filePath) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
try {
|
|
12
|
+
const data = await fs.readFile(filePath, "utf-8");
|
|
13
|
+
return data;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
throwError(`Got an error trying to read the file: ${error.message}`);
|
|
16
|
+
}
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Checks if a file exists
|
|
21
|
+
* @param {import("fs").PathLike} filePath - Path to check
|
|
22
|
+
* @returns {Promise<boolean>} - True if file exists, false otherwise
|
|
23
|
+
*/
|
|
13
24
|
export async function checkFile(filePath) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
try {
|
|
26
|
+
await fs.access(filePath);
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
20
31
|
}
|
package/compiler/index.js
CHANGED
|
@@ -1,27 +1,48 @@
|
|
|
1
|
-
import { throwError, genRandomId, incrementAlfabet, isWebLink } from "./utils.js";
|
|
2
|
-
import { JSDOM } from "jsdom";
|
|
3
1
|
import path from "path";
|
|
4
2
|
import { promises as fs } from "fs";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
|
|
5
|
+
// Configuration
|
|
6
|
+
import { loadConfig, resolvePaths } from "./config.js";
|
|
7
|
+
|
|
8
|
+
// DOM Processing
|
|
9
|
+
import {
|
|
10
|
+
createDOM,
|
|
11
|
+
validateAppContainer,
|
|
12
|
+
getAppElements,
|
|
13
|
+
getAssetLinks,
|
|
14
|
+
appendRuntimeScript,
|
|
15
|
+
serializeDOM,
|
|
16
|
+
writeHTMLOutput,
|
|
17
|
+
} from "./dom-processor.js";
|
|
18
|
+
|
|
19
|
+
// Component & Runtime Processing
|
|
20
|
+
import { processAllComponents } from "./component-processor.js";
|
|
21
|
+
import { generateRuntimeScript } from "./runtime-generator.js";
|
|
22
|
+
|
|
23
|
+
// Utilities & Pipeline
|
|
24
|
+
import { throwError } from "./utils.js";
|
|
25
|
+
import {
|
|
26
|
+
copyResources,
|
|
27
|
+
getComponents,
|
|
28
|
+
getSrcIndex,
|
|
29
|
+
processIcons,
|
|
30
|
+
processStylesheet,
|
|
31
|
+
} from "./pipeline.js";
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// ===== Logging & Banners =====
|
|
10
35
|
|
|
11
36
|
const logSeparation = chalk.yellow(`
|
|
12
37
|
________________________________________________________________________
|
|
13
38
|
========================================================================
|
|
14
39
|
`);
|
|
15
40
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export default async function runtime(__rootdir) {
|
|
22
|
-
console.log(chalk.bold.hex("#945e33")(`\n RUNNING CHOCOLA BUNDLER`))
|
|
23
|
-
console.log(logSeparation);
|
|
24
|
-
console.log(chalk.hex("#945e33")(`
|
|
41
|
+
function logBanner() {
|
|
42
|
+
console.log(chalk.bold.hex("#945e33")(`\n RUNNING CHOCOLA BUNDLER`));
|
|
43
|
+
console.log(logSeparation);
|
|
44
|
+
console.log(
|
|
45
|
+
chalk.hex("#945e33")(`
|
|
25
46
|
|
|
26
47
|
|
|
27
48
|
▄████▄ ██░ ██ ▒█████ ▄████▄ ▒█████ ██▓ ▄▄▄
|
|
@@ -30,171 +51,124 @@ export default async function runtime(__rootdir) {
|
|
|
30
51
|
▒▓▓▄ ▄██▒░▓█ ░██ ▒██ ██░▒▓▓▄ ▄██▒▒██ ██░▒██░ ░██▄▄▄▄██
|
|
31
52
|
▒ ▓███▀ ░░▓█▒░██▓░ ████▓▒░▒ ▓███▀ ░░ ████▓▒░░██████▒▓█ ▓██▒
|
|
32
53
|
░ ░▒ ▒ ░ ▒ ░░▒░▒░ ▒░▒░▒░ ░ ░▒ ▒ ░░ ▒░▒░▒░ ░ ▒░▓ ░▒▒ ▓▒█░
|
|
33
|
-
░ ▒ ▒ ░▒░ ░ ░ ▒ ▒░ ░ ▒ ░
|
|
34
|
-
░ ░ ░░ ░░ ░ ░ ▒ ░ ░ ░ ░ ▒ ░ ░ ░ ▒
|
|
54
|
+
░ ▒ ▒ ░▒░ ░ ░ ░ ▒ ▒░ ░ ▒ ░ ░ ▒░ ░ ░ ▒ ░ ░ ▒▒ ░
|
|
55
|
+
░ ░ ░░ ░░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ▒
|
|
35
56
|
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
|
|
36
57
|
░ ░
|
|
37
58
|
|
|
38
59
|
|
|
39
|
-
`)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const config = await getChocolaConfig(__rootdir);
|
|
46
|
-
const bundleConfig = config.bundle;
|
|
47
|
-
|
|
48
|
-
if (bundleConfig.srcDir) { __srcdir = bundleConfig.srcDir }
|
|
49
|
-
else { console.warn(chalk.bold.yellow("WARNING!"), 'srcDir not defined in chocola.config.json file: using default "src" directory.') }
|
|
50
|
-
|
|
51
|
-
if (bundleConfig.outDir) { __outDir = bundleConfig.outDir }
|
|
52
|
-
else { console.warn(chalk.bold.yellow("WARNING!"), 'outDir not defined in chocola.config.json file: using default "dist" directory.') }
|
|
53
|
-
|
|
54
|
-
if (bundleConfig.libDir) { __libDir = bundleConfig.libDir }
|
|
55
|
-
else { console.warn(chalk.bold.yellow("WARNING!"), 'libDir not defined in chocola.config.json file: using default "lib" directory.') }
|
|
56
|
-
|
|
57
|
-
if (bundleConfig.emptyOutDir) {
|
|
58
|
-
__emptyOutDir = bundleConfig.emptyOutDir;
|
|
59
|
-
console.log(`> using emptyOutDir = ${__emptyOutDir}`);
|
|
60
|
-
} else {
|
|
61
|
-
console.log(`> using default emptyOutDir = ${__emptyOutDir}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
console.log(logSeparation);
|
|
65
|
-
|
|
66
|
-
const outDirPath = path.join(__rootdir, __outDir);
|
|
67
|
-
const srcPath = path.join(__rootdir, __srcdir);
|
|
68
|
-
const srcComponents = path.join(srcPath, __libDir);
|
|
69
|
-
|
|
70
|
-
if (__emptyOutDir) {
|
|
71
|
-
await fs.rm(path.join(outDirPath), { recursive: true, force: true });
|
|
72
|
-
await fs.mkdir(path.join(outDirPath));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let indexFiles = await getSrcIndex(srcPath);
|
|
76
|
-
let srcHtmlFile = indexFiles.srcHtmlFile;
|
|
77
|
-
let srcChocoFile = indexFiles.srcChocoFile;
|
|
78
|
-
|
|
79
|
-
console.log(` LOADING COMPONENTS`)
|
|
80
|
-
|
|
81
|
-
const foundComponents = await getComponents(srcComponents);
|
|
82
|
-
|
|
83
|
-
let loadedComponents = foundComponents.loadedComponents;
|
|
84
|
-
let notDefComps = foundComponents.notDefComps;
|
|
85
|
-
|
|
86
|
-
const srcDirData = {
|
|
87
|
-
index: srcHtmlFile || srcChocoFile,
|
|
88
|
-
components: foundComponents.componentsLib,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
console.log(chalk.bold.green(">"), "Components found in", chalk.green.underline(srcComponents) + ":");
|
|
92
|
-
console.log(" ", srcDirData.components, "\n");
|
|
93
|
-
|
|
94
|
-
if (notDefComps.length > 0) {
|
|
95
|
-
console.warn(chalk.bold.yellow("WARNING!"), "The following components don't include a default export:");
|
|
96
|
-
console.log(" ", notDefComps);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
console.log(logSeparation);
|
|
100
|
-
console.log(` BUNDLING STATIC BUILD`);
|
|
101
|
-
console.log(chalk.bold.green(">"), "Creating Chocola static build in directory", chalk.green.underline(outDirPath) + "\n");
|
|
102
|
-
console.log(logSeparation);
|
|
103
|
-
|
|
104
|
-
const dom = new JSDOM(srcDirData.index);
|
|
105
|
-
const doc = dom.window.document;
|
|
106
|
-
const appContainer = doc.querySelector("app");
|
|
107
|
-
|
|
108
|
-
if (!appContainer) {
|
|
109
|
-
throwError("Index page must have an " + chalk.blue("<app>") + " element");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const appElements = Array.from(appContainer.querySelectorAll('*'));
|
|
113
|
-
|
|
114
|
-
let runtimeChunks = [];
|
|
115
|
-
let runtimeScript = "";
|
|
116
|
-
let compIdColl = [];
|
|
117
|
-
let letter;
|
|
118
|
-
|
|
119
|
-
appElements.forEach(el => {
|
|
120
|
-
const tagName = el.tagName.toLowerCase();
|
|
121
|
-
const compName = tagName + ".js";
|
|
122
|
-
|
|
123
|
-
const ctx = {};
|
|
124
|
-
for (const attr of el.attributes) {
|
|
125
|
-
if (attr.name.startsWith("ctx.")) {
|
|
126
|
-
const key = attr.name.slice(4);
|
|
127
|
-
ctx[key] = attr.value;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const instance = loadedComponents.get(compName);
|
|
132
|
-
if (!instance || instance === undefined) return;
|
|
133
|
-
if (instance && instance.body) {
|
|
134
|
-
let body = instance.body;
|
|
135
|
-
body = body.replace(/\$\{ctx\.(\w+)\}/g, (_, key) => ctx[key] || "");
|
|
136
|
-
const fragment = JSDOM.fragment(body);
|
|
137
|
-
const firstChild = fragment.firstChild;
|
|
138
|
-
if (firstChild && firstChild.nodeType === 1) {
|
|
139
|
-
if (instance.script || instance.effects) {
|
|
140
|
-
let script;
|
|
141
|
-
let effects;
|
|
142
|
-
const compId = "chid-" + genRandomId(compIdColl);
|
|
143
|
-
firstChild.setAttribute("chid", compId);
|
|
144
|
-
instance.script && (script = instance.script.toString())
|
|
145
|
-
|
|
146
|
-
if (!letter) {
|
|
147
|
-
letter = "a";
|
|
148
|
-
} else {
|
|
149
|
-
letter = incrementAlfabet(letter);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
script = script.replace(/RUNTIME/g, `${letter}RUNTIME`);
|
|
153
|
-
|
|
154
|
-
runtimeChunks.push(`
|
|
155
|
-
const ${letter} = document.querySelector('[chid="${compId}"]');
|
|
156
|
-
${script}
|
|
157
|
-
${letter}RUNTIME(${letter}, ${JSON.stringify(ctx)});`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
el.replaceWith(fragment);
|
|
161
|
-
} else {
|
|
162
|
-
console.warn(chalk.yellow(`${compName} component could not be loaded`));
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
runtimeScript = runtimeChunks.join("\n");
|
|
167
|
-
|
|
168
|
-
let fileIds = [];
|
|
169
|
-
|
|
170
|
-
const docLinks = Array.from(doc.querySelectorAll("link"));
|
|
171
|
-
|
|
172
|
-
for (const link of docLinks) {
|
|
173
|
-
const rel = link.rel;
|
|
174
|
-
if (rel === "stylesheet") await processStylesheet(link, __rootdir, __srcdir, outDirPath, fileIds);
|
|
175
|
-
if (rel === "icon") await processIcons(link, __rootdir, __srcdir, outDirPath, fileIds)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const runtimeFilename = "run-" + genRandomId(fileIds, 6) + ".js";
|
|
180
|
-
const runtimeFileContents = `document.addEventListener("DOMContentLoaded", () => {${runtimeScript}})`;
|
|
181
|
-
await fs.writeFile(path.join(outDirPath, runtimeFilename), runtimeFileContents);
|
|
182
|
-
const runtimeScriptEl = doc.createElement("script");
|
|
183
|
-
runtimeScriptEl.type = "module";
|
|
184
|
-
runtimeScriptEl.src = "./" + runtimeFilename;
|
|
185
|
-
doc.body.appendChild(runtimeScriptEl);
|
|
186
|
-
const finalHtml = dom.serialize();
|
|
187
|
-
const prettyHtml = beautify.html(finalHtml, { indent_size: 2 });
|
|
188
|
-
|
|
189
|
-
await fs.writeFile(path.join(outDirPath, "index.html"), prettyHtml);
|
|
190
|
-
|
|
191
|
-
await copyResources(__rootdir, __srcdir, outDirPath);
|
|
192
|
-
|
|
193
|
-
console.log(`
|
|
60
|
+
`)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function logSuccess(outDirPath) {
|
|
65
|
+
console.log(`
|
|
194
66
|
▄▄ ▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄▄▄ ██
|
|
195
67
|
██ ██▀██ ██▄██ ██▀██ ██▀██ ███▄██ ██▄▄ ██
|
|
196
68
|
▄▄█▀ ▀███▀ ██▄█▀ ████▀ ▀███▀ ██ ▀██ ██▄▄▄ ▄▄
|
|
197
69
|
|
|
198
70
|
`);
|
|
199
|
-
|
|
71
|
+
console.log(
|
|
72
|
+
chalk.bold.green(">"),
|
|
73
|
+
"Project bundled succesfully at",
|
|
74
|
+
chalk.green.underline(outDirPath) + "\n\n"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ===== Directory Setup =====
|
|
79
|
+
|
|
80
|
+
async function setupOutputDirectory(outDirPath, emptyOutDir) {
|
|
81
|
+
if (emptyOutDir) {
|
|
82
|
+
await fs.rm(outDirPath, { recursive: true, force: true });
|
|
83
|
+
await fs.mkdir(outDirPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ===== Component Loading =====
|
|
88
|
+
|
|
89
|
+
async function loadAndDisplayComponents(srcComponentsPath) {
|
|
90
|
+
const foundComponents = await getComponents(srcComponentsPath);
|
|
91
|
+
const { loadedComponents, notDefComps, componentsLib } = foundComponents;
|
|
92
|
+
|
|
93
|
+
console.log(` LOADING COMPONENTS`);
|
|
94
|
+
console.log(chalk.bold.green(">"), "Components found in", chalk.green.underline(srcComponentsPath) + ":");
|
|
95
|
+
console.log(" ", componentsLib, "\n");
|
|
96
|
+
|
|
97
|
+
if (notDefComps.length > 0) {
|
|
98
|
+
console.warn(chalk.bold.yellow("WARNING!"), "The following components don't include a default export:");
|
|
99
|
+
console.log(" ", notDefComps);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return loadedComponents;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ===== Asset Processing =====
|
|
106
|
+
|
|
107
|
+
async function processAssets(doc, rootDir, srcDir, outDirPath) {
|
|
108
|
+
const { stylesheets, icons } = getAssetLinks(doc);
|
|
109
|
+
const fileIds = [];
|
|
110
|
+
|
|
111
|
+
for (const link of stylesheets) {
|
|
112
|
+
await processStylesheet(link, rootDir, srcDir, outDirPath, fileIds);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const link of icons) {
|
|
116
|
+
await processIcons(link, rootDir, srcDir, outDirPath, fileIds);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ===== Main Compilation Function =====
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Compiles a static build of your Chocola project from the directory provided.
|
|
124
|
+
* @param {import("fs").PathLike} rootDir
|
|
125
|
+
*/
|
|
126
|
+
export default async function runtime(rootDir) {
|
|
127
|
+
logBanner();
|
|
128
|
+
|
|
129
|
+
// Load Configuration
|
|
130
|
+
const config = await loadConfig(rootDir);
|
|
131
|
+
const paths = resolvePaths(rootDir, config);
|
|
132
|
+
console.log(logSeparation);
|
|
133
|
+
|
|
134
|
+
// Setup Output Directory
|
|
135
|
+
await setupOutputDirectory(paths.outDir, config.emptyOutDir);
|
|
136
|
+
|
|
137
|
+
// Load Index File
|
|
138
|
+
const indexFiles = await getSrcIndex(paths.src);
|
|
139
|
+
const srcIndexContent = indexFiles.srcHtmlFile || indexFiles.srcChocoFile;
|
|
140
|
+
|
|
141
|
+
// Load Components
|
|
142
|
+
const loadedComponents = await loadAndDisplayComponents(paths.components);
|
|
143
|
+
console.log(logSeparation);
|
|
144
|
+
|
|
145
|
+
// Create and Validate DOM
|
|
146
|
+
console.log(` BUNDLING STATIC BUILD`);
|
|
147
|
+
console.log(chalk.bold.green(">"), "Creating Chocola static build in directory", chalk.green.underline(paths.outDir) + "\n");
|
|
148
|
+
console.log(logSeparation);
|
|
149
|
+
|
|
150
|
+
const dom = createDOM(srcIndexContent);
|
|
151
|
+
const doc = dom.window.document;
|
|
152
|
+
const appContainer = validateAppContainer(doc);
|
|
153
|
+
const appElements = getAppElements(appContainer);
|
|
154
|
+
|
|
155
|
+
// Process Components
|
|
156
|
+
const { runtimeScript } = processAllComponents(appElements, loadedComponents);
|
|
157
|
+
|
|
158
|
+
// Generate Runtime File
|
|
159
|
+
const runtimeFilename = await generateRuntimeScript(runtimeScript, paths.outDir);
|
|
160
|
+
|
|
161
|
+
// Process Assets (stylesheets, icons)
|
|
162
|
+
await processAssets(doc, rootDir, config.srcDir, paths.outDir);
|
|
163
|
+
|
|
164
|
+
// Finalize HTML
|
|
165
|
+
appendRuntimeScript(doc, runtimeFilename);
|
|
166
|
+
const html = await serializeDOM(dom);
|
|
167
|
+
await writeHTMLOutput(html, paths.outDir);
|
|
168
|
+
|
|
169
|
+
// Copy Resources
|
|
170
|
+
await copyResources(rootDir, config.srcDir, paths.outDir);
|
|
171
|
+
|
|
172
|
+
// Success Message
|
|
173
|
+
logSuccess(paths.outDir);
|
|
200
174
|
}
|
package/compiler/pipeline.js
CHANGED
|
@@ -4,146 +4,179 @@ import { readMyFile, checkFile } from "./fs.js";
|
|
|
4
4
|
import { JSDOM } from "jsdom";
|
|
5
5
|
import path from "path";
|
|
6
6
|
|
|
7
|
+
// ===== Component Loading =====
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* Discovers and loads all components from a library directory
|
|
11
|
+
* Components are JavaScript files that start with an uppercase letter
|
|
12
|
+
* They must have a default export that is a function
|
|
13
|
+
* @param {import("node:fs").PathLike} libDir - Directory containing component files
|
|
14
|
+
* @returns {Promise<{
|
|
15
|
+
* componentsLib: string[],
|
|
16
|
+
* loadedComponents: Map<string, object>,
|
|
17
|
+
* notDefComps: string[]
|
|
18
|
+
* }>}
|
|
19
|
+
* @throws {Error} if libDir cannot be found
|
|
15
20
|
*/
|
|
16
21
|
export async function getComponents(libDir) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
try {
|
|
23
|
+
let componentsLib = [];
|
|
24
|
+
let loadedComponents = new Map();
|
|
25
|
+
let notDefComps = [];
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
const components = await fs.readdir(libDir);
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (!comp.endsWith(".js") || comp[0] !== comp[0].toUpperCase()) continue;
|
|
29
|
+
if (!components) {
|
|
30
|
+
throw Error(`The specified components folder ${libDir} could not be found.`);
|
|
31
|
+
}
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
for (const comp of components) {
|
|
34
|
+
// Only load .js files that start with uppercase (component convention)
|
|
35
|
+
if (!comp.endsWith(".js") || comp[0] !== comp[0].toUpperCase()) continue;
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
componentsLib.push(comp);
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
notDefComps.push(comp);
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
39
|
+
const module = await loadWithAssets(path.join(libDir, comp));
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
if (typeof module.default !== "function") {
|
|
42
|
+
notDefComps.push(comp);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
instance.body = await fs.readFile(
|
|
42
|
-
path.resolve(libDir, instance.bodyPath),
|
|
43
|
-
"utf8"
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
+
const instance = module.default();
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Load external body template if specified
|
|
49
|
+
if (instance.bodyPath) {
|
|
50
|
+
instance.body = await fs.readFile(
|
|
51
|
+
path.resolve(libDir, instance.bodyPath),
|
|
52
|
+
"utf8"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
} catch (err) {
|
|
52
|
-
throwError(err)
|
|
56
|
+
loadedComponents.set(comp.toLowerCase(), instance);
|
|
53
57
|
}
|
|
58
|
+
|
|
59
|
+
return { componentsLib, loadedComponents, notDefComps };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
throwError(err);
|
|
62
|
+
}
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
// ===== Index File Loading =====
|
|
66
|
+
|
|
56
67
|
/**
|
|
57
|
-
*
|
|
58
|
-
* If both HTML and .choco
|
|
59
|
-
*
|
|
60
|
-
* @
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* }
|
|
68
|
+
* Loads the project index file (HTML or .choco)
|
|
69
|
+
* If both HTML and .choco files exist, throws an error
|
|
70
|
+
* @param {import("fs").PathLike} srcPath - Source directory
|
|
71
|
+
* @returns {Promise<{
|
|
72
|
+
* srcHtmlFile: string | null,
|
|
73
|
+
* srcChocoFile: string | null
|
|
74
|
+
* }>}
|
|
75
|
+
* @throws {Error} if both index files exist or .choco is used (not yet supported)
|
|
65
76
|
*/
|
|
66
77
|
export async function getSrcIndex(srcPath) {
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
const srcHtmlPath = path.join(srcPath, "index.html");
|
|
79
|
+
const srcChocoPath = path.join(srcPath, "index.choco");
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
const htmlExists = await checkFile(srcHtmlPath);
|
|
82
|
+
const chocoExists = await checkFile(srcChocoPath);
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
let srcHtmlFile = null;
|
|
85
|
+
let srcChocoFile = null;
|
|
75
86
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
if (htmlExists && chocoExists) {
|
|
88
|
+
throwError(
|
|
89
|
+
"Can't have both .choco and .html source index files at a time: please remove one of the two"
|
|
90
|
+
);
|
|
91
|
+
}
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
93
|
+
if (htmlExists) {
|
|
94
|
+
try {
|
|
95
|
+
srcHtmlFile = await readMyFile(srcHtmlPath);
|
|
96
|
+
return { srcHtmlFile, srcChocoFile };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throwError(err);
|
|
87
99
|
}
|
|
100
|
+
}
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
if (chocoExists) {
|
|
103
|
+
throwError(".choco files are not supported yet");
|
|
104
|
+
}
|
|
92
105
|
}
|
|
93
106
|
|
|
107
|
+
// ===== Asset Processing =====
|
|
108
|
+
|
|
94
109
|
/**
|
|
95
|
-
*
|
|
96
|
-
* @param {HTMLLinkElement} link
|
|
97
|
-
* @param {PathLike}
|
|
98
|
-
* @param {
|
|
99
|
-
* @param {PathLike} outDirPath
|
|
100
|
-
* @param {Array} fileIds
|
|
110
|
+
* Processes stylesheet links: copies CSS files to output and updates link href
|
|
111
|
+
* @param {HTMLLinkElement} link - The link element to process
|
|
112
|
+
* @param {import("fs").PathLike} rootDir - Root project directory
|
|
113
|
+
* @param {string} srcDir - Source directory name
|
|
114
|
+
* @param {import("fs").PathLike} outDirPath - Output directory path
|
|
115
|
+
* @param {Array} fileIds - Array to track used file IDs
|
|
116
|
+
* @throws {Error} if stylesheet cannot be read or written
|
|
101
117
|
*/
|
|
102
|
-
export async function processStylesheet(link,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
118
|
+
export async function processStylesheet(link, rootDir, srcDir, outDirPath, fileIds) {
|
|
119
|
+
try {
|
|
120
|
+
const href = link.href;
|
|
121
|
+
const stylesheetPath = path.join(rootDir, srcDir, href);
|
|
122
|
+
const css = await fs.readFile(stylesheetPath, { encoding: "utf8" });
|
|
123
|
+
const cssFileName = "css-" + genRandomId(fileIds, 6) + ".css";
|
|
124
|
+
|
|
125
|
+
await fs.writeFile(path.join(outDirPath, cssFileName), css);
|
|
126
|
+
link.setAttribute("href", "./" + cssFileName);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
throwError(err);
|
|
129
|
+
}
|
|
115
130
|
}
|
|
116
131
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Processes icon links: copies icon files to output directory
|
|
134
|
+
* @param {HTMLLinkElement} link - The link element to process
|
|
135
|
+
* @param {import("fs").PathLike} rootDir - Root project directory
|
|
136
|
+
* @param {string} srcDir - Source directory name
|
|
137
|
+
* @param {import("fs").PathLike} outDirPath - Output directory path
|
|
138
|
+
* @throws {Error} if icon cannot be copied
|
|
139
|
+
*/
|
|
140
|
+
export async function processIcons(link, rootDir, srcDir, outDirPath) {
|
|
141
|
+
try {
|
|
142
|
+
const href = link.href;
|
|
143
|
+
const iconPath = path.join(rootDir, srcDir, href);
|
|
144
|
+
await fs.copyFile(iconPath, path.join(outDirPath, href));
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throwError(err);
|
|
147
|
+
}
|
|
125
148
|
}
|
|
126
149
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Copies all local resources (images, fonts, etc.) to output directory
|
|
152
|
+
* Excludes web links and script/link tags
|
|
153
|
+
* @param {import("fs").PathLike} rootDir - Root project directory
|
|
154
|
+
* @param {string} srcDir - Source directory name
|
|
155
|
+
* @param {import("fs").PathLike} outDirPath - Output directory path
|
|
156
|
+
* @throws {Error} if resources cannot be copied
|
|
157
|
+
*/
|
|
158
|
+
export async function copyResources(rootDir, srcDir, outDirPath) {
|
|
159
|
+
try {
|
|
160
|
+
const newIndex = await fs.readFile(path.join(outDirPath, "index.html"), "utf8");
|
|
161
|
+
const newDoc = new JSDOM(newIndex);
|
|
162
|
+
const newElements = Array.from(newDoc.window.document.querySelectorAll("*"));
|
|
163
|
+
|
|
164
|
+
for (const el of newElements) {
|
|
165
|
+
// Skip LINK and SCRIPT tags as they're already processed
|
|
166
|
+
if (el.tagName === "LINK" || el.tagName === "SCRIPT") continue;
|
|
167
|
+
|
|
168
|
+
const src = el.getAttribute("src") || el.getAttribute("href");
|
|
169
|
+
|
|
170
|
+
// Only copy local resources, not external web links
|
|
171
|
+
if (src && !isWebLink(src)) {
|
|
172
|
+
const srcPath = path.join(rootDir, srcDir, src);
|
|
173
|
+
const destPath = path.join(outDirPath, src);
|
|
174
|
+
|
|
175
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
176
|
+
await fs.copyFile(srcPath, destPath);
|
|
177
|
+
}
|
|
148
178
|
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
throwError(err);
|
|
181
|
+
}
|
|
149
182
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { genRandomId } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates a runtime script file and returns its filename
|
|
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}})`;
|
|
15
|
+
|
|
16
|
+
await fs.writeFile(path.join(outDirPath, runtimeFilename), runtimeFileContents);
|
|
17
|
+
|
|
18
|
+
return runtimeFilename;
|
|
19
|
+
}
|
package/compiler/utils.js
CHANGED
|
@@ -3,68 +3,103 @@ 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
|
+
*/
|
|
6
11
|
export function throwError(err) {
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
console.log(chalk.red.bold("Error!"), "A fatal error has occurred:\n");
|
|
13
|
+
throw new Error(err);
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* @
|
|
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 }; }
|
|
15
25
|
*/
|
|
16
26
|
export async function loadWithAssets(filePath) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const importRegex = /import\s+(\w+)\s+from\s+["'](.+?\.(html|css))["'];?/g;
|
|
27
|
+
let code = await fs.readFile(filePath, "utf8");
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const varName = match[1];
|
|
24
|
-
const relPath = match[2];
|
|
29
|
+
// Find all `import varName from "*.html"` or `import varName from "*.css"`
|
|
30
|
+
const importRegex = /import\s+(\w+)\s+from\s+["'](.+?\.(html|css))["'];?/g;
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = importRegex.exec(code))) {
|
|
34
|
+
const varName = match[1];
|
|
35
|
+
const relPath = match[2];
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
// Resolve relative path and read the asset file
|
|
38
|
+
const absPath = path.resolve(path.dirname(filePath), relPath);
|
|
39
|
+
let content = await fs.readFile(absPath, "utf8");
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
}
|
|
41
|
+
// Replace the import with a const assignment of the file contents
|
|
42
|
+
const replacement = `const ${varName} = ${JSON.stringify(content)};`;
|
|
43
|
+
code = code.replace(match[0], replacement);
|
|
44
|
+
}
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
// Convert to data URL and import dynamically to avoid filesystem restrictions
|
|
47
|
+
const dataUrl =
|
|
48
|
+
"data:text/javascript;base64," +
|
|
49
|
+
Buffer.from(code).toString("base64");
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
const mod = await import(dataUrl);
|
|
52
|
+
return mod;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Generates a random ID string
|
|
57
|
+
* @param {Array} collection - Array to track used IDs (prevents duplicates)
|
|
58
|
+
* @param {number} length - Desired length of the ID (default: 10)
|
|
59
|
+
* @returns {string} - Unique random ID
|
|
60
|
+
*/
|
|
61
|
+
export function genRandomId(collection, length = 10) {
|
|
62
|
+
const id = Math.random().toString(36).substring(2, length + 2);
|
|
63
|
+
if (collection.includes(id)) {
|
|
64
|
+
return genRandomId(collection, length);
|
|
65
|
+
} else {
|
|
66
|
+
collection.push(id);
|
|
67
|
+
return id;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
50
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Increments a letter or sequence of letters like Excel columns (a → b, z → aa)
|
|
73
|
+
* @param {string} letters - Letter(s) to increment
|
|
74
|
+
* @returns {string} - Incremented letter(s)
|
|
75
|
+
* @example
|
|
76
|
+
* incrementAlfabet("a") // "b"
|
|
77
|
+
* incrementAlfabet("z") // "aa"
|
|
78
|
+
*/
|
|
51
79
|
export function incrementAlfabet(letters) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
80
|
+
const alfabet = "abcdefghijklmnopqrstuvwxyz";
|
|
81
|
+
let arr = letters.split("");
|
|
82
|
+
let i = arr.length - 1;
|
|
83
|
+
|
|
84
|
+
while (i >= 0) {
|
|
85
|
+
let pos = alfabet.indexOf(arr[i]);
|
|
86
|
+
if (pos < 25) {
|
|
87
|
+
arr[i] = alfabet[pos + 1];
|
|
88
|
+
return arr.join("");
|
|
89
|
+
} else {
|
|
90
|
+
arr[i] = "a";
|
|
91
|
+
i--;
|
|
64
92
|
}
|
|
65
|
-
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return "a" + arr.join("");
|
|
66
96
|
}
|
|
67
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Checks if a string is a valid HTTP/HTTPS URL
|
|
100
|
+
* @param {string} str - String to check
|
|
101
|
+
* @returns {boolean} - True if valid web link, false otherwise
|
|
102
|
+
*/
|
|
68
103
|
export function isWebLink(str) {
|
|
69
104
|
try {
|
|
70
105
|
const url = new URL(str);
|
package/dev/index.js
CHANGED
|
@@ -32,13 +32,38 @@ export async function serve(__rootdir) {
|
|
|
32
32
|
const extname = String(path.extname(filePath)).toLowerCase();
|
|
33
33
|
const mimeTypes = {
|
|
34
34
|
".html": "text/html",
|
|
35
|
+
".htm": "text/html",
|
|
35
36
|
".js": "text/javascript",
|
|
36
37
|
".css": "text/css",
|
|
38
|
+
".csv": "text/csv",
|
|
37
39
|
".json": "application/json",
|
|
38
40
|
".png": "image/png",
|
|
39
41
|
".jpg": "image/jpg",
|
|
42
|
+
".jpeg": "image/jpeg",
|
|
43
|
+
".bmp": "image/bmp",
|
|
40
44
|
".gif": "image/gif",
|
|
41
|
-
".svg": "svg+xml"
|
|
45
|
+
".svg": "image/svg+xml",
|
|
46
|
+
".icon": "image/vnd.microsoft.icon",
|
|
47
|
+
".pdf": "application/pdf",
|
|
48
|
+
".mp3": "audio/mpeg",
|
|
49
|
+
".md": "text/markdown",
|
|
50
|
+
".mp4": "video/mp4",
|
|
51
|
+
".mpeg": "video/mpeg",
|
|
52
|
+
".oga": "audio/ogg",
|
|
53
|
+
".ogv": "video/ogg",
|
|
54
|
+
".php": "application/x-httpd-php",
|
|
55
|
+
".rar": "application/vnd.rar",
|
|
56
|
+
".tar": "application/x-tar",
|
|
57
|
+
".ttf": "font/ttf",
|
|
58
|
+
".txt": "text/plain",
|
|
59
|
+
".wav": "audio/wav",
|
|
60
|
+
".weba": "audio/webm",
|
|
61
|
+
".webm": "video/webm",
|
|
62
|
+
".webmanifest": "application/manifest+json",
|
|
63
|
+
".webp": "image/webp",
|
|
64
|
+
".xhtml": "application/xhtml+xml",
|
|
65
|
+
".xml": "application/xml",
|
|
66
|
+
".zip": "application/zip"
|
|
42
67
|
};
|
|
43
68
|
|
|
44
69
|
const contentType = mimeTypes[extname] || "application/octet-stream";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chocola",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.21",
|
|
4
4
|
"description": "Chocola pipeline for web apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web",
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"chalk": "^5.6.2",
|
|
23
|
-
"
|
|
24
|
-
"js-beautify": "^1.15.4"
|
|
23
|
+
"chocola": "^1.1.20",
|
|
24
|
+
"js-beautify": "^1.15.4",
|
|
25
|
+
"jsdom": "^27.4.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"js-beautify": "^1.15.4"
|