chocola 1.1.20 → 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.
@@ -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
- try {
6
- const data = await fs.readFile(filePath, "utf-8");
7
- return data;
8
- } catch (error) {
9
- throwError(`Got an error trying to read the file: ${error.message}`);
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
- try {
15
- await fs.access(filePath);
16
- return true;
17
- } catch {
18
- return false;
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 { copyResources, getComponents, getSrcIndex, processIcons, processStylesheet } from "./pipeline.js";
6
- import chalk from 'chalk';
7
- import beautify from "js-beautify";
8
- import { getChocolaConfig } from "../utils.js";
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
- * Compiles a static build of your Chocola project from the directory provided.
18
- * @param {import("fs").PathLike} __rootdir
19
- * @param {string} __srcdir
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
- let __srcdir = "src";
41
- let __outDir = "dist";
42
- let __libDir = "lib";
43
- let __emptyOutDir = true;
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
- console.log(chalk.bold.green(">"), "Project bundled succesfully at", chalk.green.underline(outDirPath) + "\n\n");
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
  }
@@ -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
- * Returns a collection with the found components, the loaded ones (unpacked) and the undefined ones.
9
- * @param {import("node:fs").PathLike} libDir
10
- * @returns {{
11
- * componentsLib: PathLike[],
12
- * loadedComponents: Object[],
13
- * notDefComps: PathLike[]
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
- try {
18
- let componentsLib = [];
19
- let loadedComponents = new Map();
20
- let notDefComps = [];
22
+ try {
23
+ let componentsLib = [];
24
+ let loadedComponents = new Map();
25
+ let notDefComps = [];
21
26
 
22
- const components = await fs.readdir(libDir);
27
+ const components = await fs.readdir(libDir);
23
28
 
24
- if (!components) throw Error(`The specified components folder ${libDir} could not be found.`)
25
-
26
- for (const comp of components) {
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
- componentsLib.push(comp);
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
- const module = await loadWithAssets(path.join(libDir, comp));
37
+ componentsLib.push(comp);
32
38
 
33
- if (typeof module.default !== "function") {
34
- notDefComps.push(comp);
35
- continue;
36
- }
39
+ const module = await loadWithAssets(path.join(libDir, comp));
37
40
 
38
- const instance = module.default();
41
+ if (typeof module.default !== "function") {
42
+ notDefComps.push(comp);
43
+ continue;
44
+ }
39
45
 
40
- if (instance.bodyPath) {
41
- instance.body = await fs.readFile(
42
- path.resolve(libDir, instance.bodyPath),
43
- "utf8"
44
- );
45
- }
46
+ const instance = module.default();
46
47
 
47
- loadedComponents.set(comp.toLowerCase(), instance);
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
- return { componentsLib, loadedComponents, notDefComps };
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
- * Returns the Chocola project index HTML or .chocola file contents.
58
- * If both HTML and .choco index file exist, it will throw an error.
59
- * Note: It throws an error when using .choco files since it is not supported yet.
60
- * @param {PathLike} srcPath
61
- * @returns {{
62
- * srcHtmlFile: string | null,
63
- * srcChocoFile: string | null
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
- const srcHtmlPath = path.join(srcPath, "index.html");
68
- const srcChocoPath = path.join(srcPath, "index.choco");
78
+ const srcHtmlPath = path.join(srcPath, "index.html");
79
+ const srcChocoPath = path.join(srcPath, "index.choco");
69
80
 
70
- const htmlExists = await checkFile(srcHtmlPath);
71
- const chocoExists = await checkFile(srcChocoPath);
81
+ const htmlExists = await checkFile(srcHtmlPath);
82
+ const chocoExists = await checkFile(srcChocoPath);
72
83
 
73
- let srcHtmlFile = null;
74
- let srcChocoFile = null;
84
+ let srcHtmlFile = null;
85
+ let srcChocoFile = null;
75
86
 
76
- if (htmlExists && chocoExists) {
77
- throwError("Can't have both .choco and .html source index files at a time: please remove one of the two");
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
- if (htmlExists) {
81
- try {
82
- srcHtmlFile = await readMyFile(srcHtmlPath);
83
- return { srcHtmlFile: srcHtmlFile, srcChocoFile: srcChocoFile }
84
- } catch (err) {
85
- throwError(err);
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
- if (chocoExists) {
90
- throwError(".choco files are not supported yet")
91
- }
102
+ if (chocoExists) {
103
+ throwError(".choco files are not supported yet");
104
+ }
92
105
  }
93
106
 
107
+ // ===== Asset Processing =====
108
+
94
109
  /**
95
- * Generates the new CSS files attached to the build index.html.
96
- * @param {HTMLLinkElement} link
97
- * @param {PathLike} __rootdir
98
- * @param {PathLike} __srcdir
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, __rootdir, __srcdir, outDirPath, fileIds) {
103
- try {
104
- const href = link.href;
105
- const stylesheetPath = path.join(__rootdir, __srcdir, href);
106
- const css = await fs.readFile(stylesheetPath, { encoding: "utf8" });
107
- const cssFileName = "css-" + genRandomId(fileIds, 6) + ".css";
108
-
109
- await fs.writeFile(path.join(outDirPath, cssFileName), css);
110
-
111
- link.setAttribute("href", "./" + cssFileName);
112
- } catch (err) {
113
- throwError(err)
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
- export async function processIcons(link, __rootdir, __srcdir, outDirPath) {
118
- try {
119
- const href = link.href;
120
- const iconPath = path.join(__rootdir, __srcdir, href);
121
- await fs.copyFile(iconPath, path.join(outDirPath, href));
122
- } catch (err) {
123
- throwError(err)
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
- export async function copyResources(__rootdir, __srcdir, outDirPath) {
128
- try {
129
- const newIndex = await fs.readFile(path.join(outDirPath, "index.html"), "utf8");
130
- const newDoc = new JSDOM(newIndex);
131
- const newElements = Array.from(newDoc.window.document.querySelectorAll("*"));
132
-
133
- for (const el of newElements) {
134
- if (el.tagName === "LINK" || el.tagName === "SCRIPT") continue;
135
-
136
- const src = el.getAttribute("src") || el.getAttribute("href");
137
-
138
- if (src && !isWebLink(src)) {
139
- const srcPath = path.join(__rootdir, __srcdir, src);
140
- const destPath = path.join(outDirPath, src);
141
-
142
- await fs.mkdir(path.dirname(destPath), { recursive: true });
143
- await fs.copyFile(srcPath, destPath);
144
- }
145
- }
146
- } catch (err) {
147
- throwError(err)
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
- console.log(chalk.red.bold("Error!"), "A fatal error has ocurred:\n");
8
- throw new Error(err)
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
- * Returns the given module function adjusted to work with NPM without any dependency.
13
- * @param {import("fs").PathLike} filePath
14
- * @returns {Function}
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
- let code = await fs.readFile(filePath, "utf8");
18
-
19
- const importRegex = /import\s+(\w+)\s+from\s+["'](.+?\.(html|css))["'];?/g;
27
+ let code = await fs.readFile(filePath, "utf8");
20
28
 
21
- let match;
22
- while ((match = importRegex.exec(code))) {
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
- const absPath = path.resolve(path.dirname(filePath), relPath);
27
- let content = await fs.readFile(absPath, "utf8");
32
+ let match;
33
+ while ((match = importRegex.exec(code))) {
34
+ const varName = match[1];
35
+ const relPath = match[2];
28
36
 
29
- const replacement = `const ${varName} = ${JSON.stringify(content)};`;
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
- code = code.replace(match[0], replacement);
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
- const dataUrl =
35
- "data:text/javascript;base64," +
36
- Buffer.from(code).toString("base64");
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
- const mod = await import(dataUrl);
39
- return mod;
51
+ const mod = await import(dataUrl);
52
+ return mod;
40
53
  }
41
54
 
42
- export function genRandomId(compIdColl, length = 10) {
43
- const id = Math.random().toString(36).substring(2, length + 2);
44
- if (compIdColl.includes(id)) { return genRandomCompId() }
45
- else {
46
- compIdColl.push(id);
47
- return id
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
- const alfabet = "abcdefghijklmnopqrstuvwxyz";
53
- let arr = letters.split("");
54
- let i = arr.length - 1;
55
- while (i >= 0) {
56
- let pos = alfabet.indexOf(arr[i]);
57
- if (pos < 25) {
58
- arr[i] = alfabet[pos + 1];
59
- return arr.join("");
60
- } else {
61
- arr[i] = "a";
62
- i--;
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
- return "a" + arr.join("");
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.20",
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
- "jsdom": "^27.4.0",
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"