chocola 2.0.0-next.2 → 2.0.0-next.5

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.
@@ -1,26 +1,13 @@
1
1
  import { promises as fs } from "fs";
2
- import { loadWithAssets, throwError, genRandomId, isWebLink } from "./utils.js";
2
+ import { throwError, genRandomId } from "./utils.js";
3
3
  import { readMyFile, checkFile } from "./fs.js";
4
- import { JSDOM } from "jsdom";
5
4
  import path from "path";
6
5
 
7
- /**
8
- * Discovers and loads all components from a library directory.
9
- * Components are JavaScript files that start with an uppercase letter.
10
- * They must have a default export that is a function
11
- * @param {import("node:fs").PathLike} libDir - Directory containing component files
12
- * @returns {Promise<{
13
- * componentsLib: string[],
14
- * loadedComponents: Map<string, object>,
15
- * notDefComps: string[]
16
- * }>}
17
- * @throws {Error} if libDir cannot be found
18
- */
19
6
  export async function getComponents(libDir) {
20
7
  try {
21
8
  let componentsLib = [];
22
9
  let loadedComponents = new Map();
23
- let notDefComps = [];
10
+ let emptyComps = [];
24
11
 
25
12
  const components = await fs.readdir(libDir);
26
13
 
@@ -28,46 +15,30 @@ export async function getComponents(libDir) {
28
15
  throw Error(`The specified components folder ${libDir} could not be found.`);
29
16
  }
30
17
 
31
- for (const comp of components) {
32
- if (!comp.endsWith(".js") || comp[0] !== comp[0].toUpperCase()) continue;
33
-
34
- componentsLib.push(comp);
18
+ const reads = components
19
+ .filter(comp => comp.endsWith(".html"))
20
+ .map(async (comp) => {
21
+ try {
22
+ const compPath = path.join(libDir, comp);
23
+ const instance = await fs.readFile(compPath, "utf-8");
35
24
 
36
- let module;
37
- try {
38
- module = await loadWithAssets(path.join(libDir, comp));
25
+ if (instance === "" || instance.trim().length === 0) emptyComps.push(comp);
39
26
 
40
- if (typeof module.default !== "function") {
41
- notDefComps.push(comp);
42
- continue;
27
+ loadedComponents.set(comp.toLowerCase(), instance);
28
+ componentsLib.push(comp);
29
+ } catch (err) {
30
+ throwError(`Failed to load component "${comp}": ${err.message || err}`);
43
31
  }
32
+ });
44
33
 
45
- const instance = module.default();
46
- const compPath = path.join(libDir, comp);
47
- instance.__sourceFile = compPath;
34
+ await Promise.all(reads);
48
35
 
49
- loadedComponents.set(comp.toLowerCase(), instance);
50
- } catch (err) {
51
- throwError(`Failed to load component "${comp}": ${err.message || err}`);
52
- }
53
- }
54
-
55
- return { componentsLib, loadedComponents, notDefComps };
36
+ return { componentsLib, loadedComponents, emptyComps };
56
37
  } catch (err) {
57
38
  throwError(`Failed to load components from ${libDir}: ${err.message}`);
58
39
  }
59
40
  }
60
41
 
61
- /**
62
- * Loads the project index file (HTML or .choco)
63
- * If both HTML and .choco files exist, throws an error
64
- * @param {import("fs").PathLike} srcPath - Source directory
65
- * @returns {Promise<{
66
- * srcHtmlFile: string | null,
67
- * srcChocoFile: string | null
68
- * }>}
69
- * @throws {Error} if both index files exist or .choco is used (not yet supported)
70
- */
71
42
  export async function getSrcIndex(srcPath) {
72
43
  const srcHtmlPath = path.join(srcPath, "index.html");
73
44
 
@@ -85,15 +56,6 @@ export async function getSrcIndex(srcPath) {
85
56
  }
86
57
  }
87
58
 
88
- /**
89
- * Processes stylesheet links: copies CSS files to output and updates link href
90
- * @param {HTMLLinkElement} link - The link element to process
91
- * @param {import("fs").PathLike} rootDir - Root project directory
92
- * @param {string} srcDir - Source directory name
93
- * @param {import("fs").PathLike} outDirPath - Output directory path
94
- * @param {Array} fileIds - Array to track used file IDs
95
- * @throws {Error} if stylesheet cannot be read or written
96
- */
97
59
  export async function processStylesheet(link, rootDir, srcDir, outDirPath, fileIds) {
98
60
  try {
99
61
  const href = link.href;
@@ -111,14 +73,6 @@ export async function processStylesheet(link, rootDir, srcDir, outDirPath, fileI
111
73
  }
112
74
  }
113
75
 
114
- /**
115
- * Processes icon links: copies icon files to output directory
116
- * @param {HTMLLinkElement} link - The link element to process
117
- * @param {import("fs").PathLike} rootDir - Root project directory
118
- * @param {string} srcDir - Source directory name
119
- * @param {import("fs").PathLike} outDirPath - Output directory path
120
- * @throws {Error} if icon cannot be copied
121
- */
122
76
  export async function processIcons(link, rootDir, srcDir, outDirPath) {
123
77
  try {
124
78
  const href = link.href;
@@ -130,139 +84,32 @@ export async function processIcons(link, rootDir, srcDir, outDirPath) {
130
84
  }
131
85
  }
132
86
 
133
- export function getCssAssets(css) {
134
- const results = new Set();
135
- const urlRegex = /url\(([^)]+)\)/gi;
136
- const importStringRegex = /@import\s+["']([^"']+)["']/gi;
137
- const importUrlRegex = /@import\s+url\(([^)]+)\)/gi;
138
-
139
- const clean = (raw) => {
140
- let p = raw.trim();
141
- if (
142
- (p.startsWith('"') && p.endsWith('"')) ||
143
- (p.startsWith("'") && p.endsWith("'"))
144
- ) {
145
- p = p.slice(1, -1);
146
- }
147
- if (p.startsWith("data:")) return null;
148
- return p;
149
- };
150
-
151
- let match;
152
- while ((match = urlRegex.exec(css))) {
153
- const p = clean(match[1]);
154
- if (p) results.add(p);
155
- }
156
- while ((match = importStringRegex.exec(css))) {
157
- const p = clean(match[1]);
158
- if (p) results.add(p);
159
- }
160
- while ((match = importUrlRegex.exec(css))) {
161
- const p = clean(match[1]);
162
- if (p) results.add(p);
163
- }
164
- return [...results];
165
- }
166
-
167
- function getComponentSource(el) {
168
- let cur = el;
169
- while (cur) {
170
- const src = cur.getAttribute?.("data-ch-source");
171
- if (src) return src;
172
- cur = cur.parentElement;
173
- }
174
- return null;
175
- }
176
-
177
- /**
178
- * Copies all local resources (images, fonts, etc.) to output directory.
179
- * Excludes web links and script/link tags.
180
- * @param {import("fs").PathLike} rootDir - Root project directory
181
- * @param {string} scopesCss - Scoped CSS content
182
- * @param {string} globalCss - Global CSS content
183
- * @param {string} srcDir - Source directory name
184
- * @param {import("fs").PathLike} outDirPath - Output directory path
185
- */
186
- export async function copyResources(rootDir, scopesCss, globalCss, srcDir, outDirPath) {
87
+ export async function processScript(doc, script, rootDir, srcDir, outDirPath, fileIds) {
187
88
  try {
188
- const newIndex = await fs.readFile(path.join(outDirPath, "index.html"), "utf8");
189
- const newDoc = new JSDOM(newIndex);
190
- const newElements = Array.from(newDoc.window.document.querySelectorAll("*"));
191
- const inlinePathsRegex = /url\((.*?)\)/gi;
192
-
193
- for (const el of newElements) {
194
- if (el.tagName === "LINK" || el.tagName === "SCRIPT") continue;
195
-
196
- const attrName = el.getAttribute("src") ? "src" : "href";
197
- const src = el.getAttribute(attrName);
198
- if (src && !isWebLink(src) && !src.startsWith("#") && !src.startsWith("/")) {
199
- const srcPath = path.join(rootDir, srcDir, src);
200
- const destPath = path.join(outDirPath, src);
201
- try {
202
- await fs.mkdir(path.dirname(destPath), { recursive: true });
203
- await fs.copyFile(srcPath, destPath);
204
- } catch (err) {
205
- throw new Error(
206
- `Failed to copy resource referenced by <${el.tagName.toLowerCase()} ${attrName}="${src}">` +
207
- (getComponentSource(el) ? ` in component "${getComponentSource(el)}"` : "") +
208
- `: ${err.message || err}`
209
- );
210
- }
211
- }
212
-
213
- const styles = el.getAttribute("style");
214
- if (styles) {
215
- let stylesMatch;
216
- while ((stylesMatch = inlinePathsRegex.exec(styles)) !== null) {
217
- let filePath = stylesMatch[1].trim();
218
- if (
219
- (filePath.startsWith('"') && filePath.endsWith('"')) ||
220
- (filePath.startsWith("'") && filePath.endsWith("'"))
221
- ) {
222
- filePath = filePath.slice(1, -1);
223
- }
224
- const srcPath = path.join(rootDir, srcDir, filePath);
225
- const destPath = path.join(outDirPath, filePath);
226
- try {
227
- await fs.mkdir(path.dirname(destPath), { recursive: true });
228
- await fs.copyFile(srcPath, destPath);
229
- } catch (err) {
230
- throw new Error(
231
- `Failed to copy resource from inline style on <${el.tagName.toLowerCase()}> url("${filePath}")` +
232
- (getComponentSource(el) ? ` in component "${getComponentSource(el)}"` : "") +
233
- `: ${err.message || err}`
234
- );
235
- }
236
- }
89
+ const src = script.getAttribute("src");
90
+ if (src.startsWith("http://") || src.startsWith("https://")) return;
91
+ const scriptPath = path.join(rootDir, srcDir, src);
92
+ const content = await fs.readFile(scriptPath, { encoding: "utf8" });
93
+ const jsFileName = "js-" + genRandomId(fileIds, 6) + ".js";
94
+ await fs.writeFile(path.join(outDirPath, jsFileName), content);
95
+
96
+ const newScript = doc.createElement("script");
97
+ for (const attr of script.attributes) {
98
+ if (attr.name === "src") {
99
+ newScript.setAttribute("src", "./" + jsFileName);
100
+ } else {
101
+ newScript.setAttribute(attr.name, attr.value);
237
102
  }
238
103
  }
239
-
240
- const cssAssets = [...getCssAssets(scopesCss || ""), ...getCssAssets(globalCss || "")];
241
- for (const assetPath of cssAssets) {
242
- try {
243
- if (assetPath.startsWith('http:') || assetPath.startsWith('https:')) continue;
244
- const srcPath = path.join(rootDir, srcDir, assetPath);
245
- const destPath = path.join(outDirPath, assetPath);
246
- await fs.mkdir(path.dirname(destPath), { recursive: true });
247
- await fs.copyFile(srcPath, destPath);
248
- } catch (err) {
249
- throw new Error(
250
- `Failed to copy CSS asset "${assetPath}" referenced from stylesheets: ${err.message || err}`
251
- );
252
- }
104
+ if (script.textContent.trim()) {
105
+ newScript.textContent = script.textContent;
253
106
  }
107
+ script.parentNode.replaceChild(newScript, script);
254
108
  } catch (err) {
255
- throw err;
109
+ throwError(`Failed to process script: ${err}`);
256
110
  }
257
111
  }
258
112
 
259
- /**
260
- * Copies the static/ directory from source to output.
261
- * The entire static/ directory tree is copied recursively.
262
- * Silently skips if static/ does not exist.
263
- * @param {import("fs").PathLike} srcPath - Source directory path
264
- * @param {import("fs").PathLike} outDirPath - Output directory path
265
- */
266
113
  export async function copyStaticDir(srcPath, outDirPath) {
267
114
  const staticSrc = path.join(srcPath, "static");
268
115
  const staticDest = path.join(outDirPath, "static");
@@ -270,6 +117,5 @@ export async function copyStaticDir(srcPath, outDirPath) {
270
117
  await fs.access(staticSrc);
271
118
  await fs.cp(staticSrc, staticDest, { recursive: true, force: true });
272
119
  } catch {
273
- // static/ directory doesn't exist — nothing to copy
274
120
  }
275
121
  }
@@ -2,12 +2,6 @@ import { promises as fs } from "fs";
2
2
  import path from "path";
3
3
  import { genRandomId } from "./utils.js";
4
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
5
  export async function generateRuntimeScript(runtimeScript, outDirPath) {
12
6
  const fileIds = [];
13
7
  const runtimeFilename = "run-" + genRandomId(fileIds, 6) + ".js";
package/compiler/utils.js CHANGED
@@ -1,82 +1,20 @@
1
- import fs from "fs/promises";
2
- import path from "path";
3
- import { pathToFileURL } from "url";
4
- import chalk from "chalk";
1
+ import chalk from "./chalk.js";
5
2
 
6
- /**
7
- * Throws a formatted error message and exits
8
- * @param {string|Error} err - Error message or Error object
9
- * @throws {Error}
10
- */
11
3
  export function throwError(err) {
12
4
  console.log(chalk.red.bold("Error!"), "A fatal error has occurred:\n");
13
5
  throw new Error(err);
14
6
  }
15
7
 
16
- /**
17
- * Loads a JavaScript module and inlines asset imports (HTML/CSS files)
18
- * This allows components to import external template files as strings
19
- * @param {import("fs").PathLike} filePath - Path to the JavaScript file
20
- * @returns {Promise<object>} - The imported module
21
- * @example
22
- * // Component with asset import
23
- * import template from "./button.html";
24
- * export default function Button() { return { body: template }; }
25
- */
26
- export async function loadWithAssets(filePath) {
27
- let code;
28
- try {
29
- code = await fs.readFile(filePath, "utf8");
30
- } catch (err) {
31
- throwError(`Failed to read component file "${filePath}": ${err.message || err}`);
32
- }
33
-
34
- const importRegex = /import\s+(\w+)\s+from\s+["'](.+?\.(html|css))["'];?/g;
35
-
36
- const replacements = [];
37
- let match;
38
- while ((match = importRegex.exec(code))) {
39
- const varName = match[1];
40
- const relPath = match[2];
41
- const absPath = path.resolve(path.dirname(filePath), relPath);
42
-
43
- let content;
44
- try {
45
- content = await fs.readFile(absPath, "utf8");
46
- } catch (err) {
47
- throwError(
48
- `Failed to read imported file "${relPath}" (resolved to "${absPath}") ` +
49
- `for variable "${varName}" in component "${filePath}": ${err.message || err}`
50
- );
51
- }
52
-
53
- replacements.push({ from: match[0], to: `const ${varName} = ${JSON.stringify(content)};` });
54
- }
55
-
56
- for (const { from, to } of replacements) {
57
- code = code.replace(from, to);
58
- }
59
-
60
- const dataUrl =
61
- "data:text/javascript;base64," +
62
- Buffer.from(code).toString("base64");
63
-
64
- try {
65
- const mod = await import(dataUrl);
66
- return mod;
67
- } catch (err) {
68
- throwError(
69
- `Failed to evaluate component "${filePath}": ${err.message || err}`
70
- );
71
- }
8
+ export function hasDelIfAttr(el) {
9
+ return el.hasAttribute("del:if");
10
+ }
11
+ export function getDelIfAttr(el) {
12
+ return el.getAttribute("del:if");
13
+ }
14
+ export function removeDelIfAttr(el) {
15
+ el.removeAttribute("del:if");
72
16
  }
73
17
 
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
18
  export function genRandomId(collection = null, length = 10, lettersOnly = false) {
81
19
  let id;
82
20
  if (lettersOnly) {
@@ -93,14 +31,6 @@ export function genRandomId(collection = null, length = 10, lettersOnly = false)
93
31
  }
94
32
  }
95
33
 
96
- /**
97
- * Increments a letter or sequence of letters like Excel columns (a → b, z → aa)
98
- * @param {string} letters - Letter(s) to increment
99
- * @returns {string} - Incremented letter(s)
100
- * @example
101
- * incrementAlfabet("a") // "b"
102
- * incrementAlfabet("z") // "aa"
103
- */
104
34
  export function incrementAlfabet(letters) {
105
35
  let arr = letters.split("");
106
36
  let i = arr.length - 1;
@@ -119,41 +49,15 @@ export function incrementAlfabet(letters) {
119
49
  return "a" + arr.join("");
120
50
  }
121
51
 
122
- /**
123
- * Checks if a string is a valid HTTP/HTTPS URL
124
- * @param {string} str - String to check
125
- * @returns {boolean} - True if valid web link, false otherwise
126
- */
127
- export function isWebLink(str) {
128
- try {
129
- const url = new URL(str);
130
- return url.protocol === "http:" || url.protocol === "https:";
131
- } catch (e) {
132
- return false;
133
- }
134
- }
135
-
136
52
  const LBRACE_PH = "_%%CHOCOLA-LBRACE%%_";
137
53
  const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
138
54
 
139
- /**
140
- * Replaces HTML entities for curly braces with placeholders before JSDOM parsing.
141
- * This prevents JSDOM from decoding them into literal braces before binding processing.
142
- * @param {string} html - Raw HTML content
143
- * @returns {string} - HTML with curly brace entities replaced
144
- */
145
55
  export function protectCurlyBraces(html) {
146
56
  return html
147
57
  .replace(/&(?:lbrace|#123|#x7B);/gi, LBRACE_PH)
148
58
  .replace(/&(?:rcub|#125|#x7D);/gi, RBRACE_PH);
149
59
  }
150
60
 
151
- /**
152
- * Restores curly brace placeholders back to literal characters.
153
- * Called after binding processing is complete and before final output.
154
- * @param {string} html - Serialized HTML with placeholders
155
- * @returns {string} - HTML with literal curly braces restored
156
- */
157
61
  export function restoreCurlyBraces(html) {
158
62
  return html
159
63
  .replace(/_%%CHOCOLA-LBRACE%%_/g, "{")
package/dev/index.js CHANGED
@@ -1,7 +1,7 @@
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
7
  import { getChocolaConfig } from "../utils.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chocola",
3
- "version": "2.0.0-next.2",
3
+ "version": "2.0.0-next.5",
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/sad-gabi/chocola/issues"
16
+ "url": "https://github.com/chocolajs/chocola/issues"
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/sad-gabi/chocola.git"
20
+ "url": "git+https://github.com/chocolajs/chocola.git"
21
21
  },
22
22
  "license": "MIT",
23
23
  "author": "SadGabi",
@@ -25,7 +25,7 @@
25
25
  "main": "index.js",
26
26
  "funding": {
27
27
  "type": "github",
28
- "url": "https://github.com/sponsors/sad-gabi"
28
+ "url": "https://github.com/sponsors/chocolajs"
29
29
  },
30
30
  "scripts": {
31
31
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -35,11 +35,6 @@
35
35
  "./dev": "./dev/index.js"
36
36
  },
37
37
  "dependencies": {
38
- "chalk": "^5.6.2",
39
- "js-beautify": "^1.15.4",
40
38
  "jsdom": "^27.4.0"
41
- },
42
- "devDependencies": {
43
- "js-beautify": "^1.15.4"
44
39
  }
45
40
  }
package/utils.js CHANGED
@@ -2,11 +2,6 @@ import { promises as fs } from "fs";
2
2
  import path from "path";
3
3
  import { throwError } from "./compiler/utils.js";
4
4
 
5
- /**
6
- * Returns `chocola.config.json` as an object.
7
- * @param {import("fs").PathLike} __rootdir
8
- * @returns {Object}
9
- */
10
5
  export async function getChocolaConfig(__rootdir) {
11
6
  try {
12
7
  const config = await fs.readFile(path.join(__rootdir, "chocola.config.json"), "utf-8");