chocola 1.6.0 → 2.0.0-next.10

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,181 @@
1
+ import path from "path";
2
+ import { promises as fs } from "fs";
3
+ import {
4
+ createDOM,
5
+ validateAppContainer,
6
+ getAppElements,
7
+ getAssetLinks,
8
+ getScriptElements,
9
+ appendRuntimeScript,
10
+ appendStylesheetLink,
11
+ serializeDOM,
12
+ } from "./dom-processor.js";
13
+ import { processAllComponents } from "./component-processor.js";
14
+ import { generateRuntimeScript } from "./runtime-generator.js";
15
+ import { deterministicHash, throwError, warnConstantCondition, findElementLine } from "./utils.js";
16
+ import { compileExpr, evaluateConstant, hasMountIf, getMountIf, removeMountIf } from "../parser/index.js";
17
+ import {
18
+ processStylesheet,
19
+ processIcons,
20
+ processScript,
21
+ copyStaticDir,
22
+ } from "./pipeline.js";
23
+
24
+ function processPageConditionals(parent, sourceFile, sourceContent, ctx = {}) {
25
+ const ctxProxy = new Proxy(ctx, {
26
+ has() { return true; },
27
+ get(target, key) { return target[key]; },
28
+ });
29
+ const children = [...parent.children];
30
+ let chainActive = false;
31
+ let chainRendered = false;
32
+
33
+ const getLocation = (child) => {
34
+ if (!sourceContent) return sourceFile;
35
+ const lineNum = findElementLine(sourceContent, child.outerHTML);
36
+ return lineNum !== null ? `${sourceFile}:${lineNum}` : sourceFile;
37
+ };
38
+
39
+ const warnIfConstant = (expr, child, attr) => {
40
+ const { constant, value } = evaluateConstant(expr);
41
+ if (!constant) return;
42
+ warnConstantCondition(getLocation(child), child.tagName.toLowerCase(), attr, Boolean(value));
43
+ };
44
+
45
+ for (const child of children) {
46
+ const hasIf = child.hasAttribute("if");
47
+ const hasDelIf = hasMountIf(child);
48
+ const hasElif = child.hasAttribute("elif");
49
+ const hasElse = child.hasAttribute("else");
50
+
51
+ if (hasIf || hasDelIf || hasElif) {
52
+ const stripBraces = (raw) => raw.startsWith("{") ? raw.slice(1, -1) : raw;
53
+ if (hasIf) warnIfConstant(stripBraces(child.getAttribute("if")), child, "if");
54
+ if (hasDelIf) warnIfConstant(stripBraces(getMountIf(child)), child, "mount:if");
55
+ if (hasElif) warnIfConstant(stripBraces(child.getAttribute("elif")), child, "elif");
56
+ }
57
+
58
+ if (hasElif || hasElse) {
59
+ if (!chainActive) {
60
+ const tag = child.tagName.toLowerCase();
61
+ const attr = hasElif ? "elif" : "else";
62
+ throwError(`${getLocation(child)}\n <${tag}> has ${attr} without a preceding if/mount:if sibling`);
63
+ }
64
+ if (chainRendered) { child.remove(); continue; }
65
+ }
66
+
67
+ if (hasIf) {
68
+ const raw = child.getAttribute("if");
69
+ const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
70
+ const fn = compileExpr(expr, true);
71
+ const result = fn(ctxProxy);
72
+ chainActive = true;
73
+ if (result) {
74
+ chainRendered = true;
75
+ } else {
76
+ child.style.display = "none";
77
+ chainRendered = false;
78
+ }
79
+ child.removeAttribute("if");
80
+ } else if (hasDelIf) {
81
+ const raw = getMountIf(child);
82
+ const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
83
+ const fn = compileExpr(expr, true);
84
+ const result = fn(ctxProxy);
85
+ chainActive = true;
86
+ if (result) {
87
+ chainRendered = true;
88
+ } else {
89
+ child.remove();
90
+ chainRendered = false;
91
+ }
92
+ removeMountIf(child);
93
+ } else if (hasElif) {
94
+ const raw = child.getAttribute("elif");
95
+ const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
96
+ const fn = compileExpr(expr, true);
97
+ const result = fn(ctxProxy);
98
+ if (result) {
99
+ chainRendered = true;
100
+ } else {
101
+ child.remove();
102
+ }
103
+ child.removeAttribute("elif");
104
+ } else if (hasElse) {
105
+ chainRendered = true;
106
+ chainActive = false;
107
+ child.removeAttribute("else");
108
+ } else {
109
+ chainActive = false;
110
+ chainRendered = false;
111
+ }
112
+
113
+ if (child.parentNode) {
114
+ processPageConditionals(child, sourceFile, sourceContent, ctx);
115
+ }
116
+ }
117
+ }
118
+
119
+ async function processAssets(doc, graph, out) {
120
+ const { stylesheets, icons } = getAssetLinks(doc);
121
+ const scripts = getScriptElements(doc);
122
+
123
+ for (const link of stylesheets) {
124
+ await processStylesheet(link, graph.rootDir, graph.config.srcDir, out);
125
+ }
126
+
127
+ for (const link of icons) {
128
+ await processIcons(link, graph.rootDir, graph.config.srcDir, out);
129
+ }
130
+
131
+ for (const script of scripts) {
132
+ await processScript(doc, script, graph.rootDir, graph.config.srcDir, out);
133
+ }
134
+ }
135
+
136
+ export async function renderPage(graph, ctx = {}) {
137
+ const out = {
138
+ files: [],
139
+ copies: [],
140
+ ids: [],
141
+ outDir: graph.paths.outDir,
142
+ };
143
+
144
+ const page = graph.page;
145
+ const dom = createDOM(page.source);
146
+ const doc = dom.document;
147
+ const appContainer = validateAppContainer(doc);
148
+
149
+ processPageConditionals(appContainer, page.sourcePath, dom.protectedContent, ctx);
150
+
151
+ const appElements = getAppElements(appContainer);
152
+ const { runtimeScript, scopesCss, hashMap, csrClasses } = processAllComponents(
153
+ appElements,
154
+ graph.loadedComponents,
155
+ page.sourcePath,
156
+ page.source,
157
+ ctx
158
+ );
159
+
160
+ const csrSource = await fs.readFile(new URL("../runtime/index.js", import.meta.url), "utf-8");
161
+ const runtimeFiles = generateRuntimeScript(runtimeScript, csrSource, csrClasses);
162
+
163
+ await processAssets(doc, graph, out);
164
+
165
+ if (scopesCss) {
166
+ const fileName = "sc-" + deterministicHash(scopesCss, 6) + ".css";
167
+ out.files.push({ path: fileName, content: scopesCss });
168
+ appendStylesheetLink(doc, fileName);
169
+ }
170
+
171
+ for (const file of runtimeFiles) {
172
+ out.files.push(file);
173
+ appendRuntimeScript(doc, file.path);
174
+ }
175
+
176
+ const html = await serializeDOM(dom);
177
+
178
+ await copyStaticDir(graph.paths.src, out);
179
+
180
+ return { html, hashMap, files: out.files, copies: out.copies };
181
+ }
@@ -1,19 +1,24 @@
1
- import { promises as fs } from "fs";
2
- import path from "path";
3
- import { genRandomId } from "./utils.js";
1
+ import { deterministicHash } from "./utils.js";
4
2
 
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}})`;
3
+ export function generateRuntimeScript(runtimeScript, csrSource, csrClasses) {
4
+ const files = [];
15
5
 
16
- await fs.writeFile(path.join(outDirPath, runtimeFilename), runtimeFileContents);
6
+ if (csrSource) {
7
+ const source = csrSource.replace(/^export\s+\{?\s*[^}]+\s*}?\s*;?\s*$/m, "");
8
+ const name = "run-" + deterministicHash(source, 6) + ".js";
9
+ files.push({ path: name, content: source });
10
+ }
17
11
 
18
- return runtimeFilename;
12
+ if (csrClasses) {
13
+ const name = "run-" + deterministicHash(csrClasses, 6) + ".js";
14
+ files.push({ path: name, content: csrClasses });
15
+ }
16
+
17
+ if (runtimeScript) {
18
+ const content = `document.addEventListener("DOMContentLoaded", () => {${runtimeScript}})`;
19
+ const name = "run-" + deterministicHash(content, 6) + ".js";
20
+ files.push({ path: name, content });
21
+ }
22
+
23
+ return files;
19
24
  }
package/compiler/utils.js CHANGED
@@ -1,87 +1,93 @@
1
- import fs from "fs/promises";
2
- import path from "path";
3
- import { pathToFileURL } from "url";
4
- import chalk from "chalk";
1
+ import { createHash } from "crypto";
2
+ import chalk from "./chalk.js";
5
3
 
6
- /**
7
- * Throws a formatted error message and exits
8
- * @param {string|Error} err - Error message or Error object
9
- * @throws {Error}
10
- */
11
4
  export function throwError(err) {
12
5
  console.log(chalk.red.bold("Error!"), "A fatal error has occurred:\n");
13
6
  throw new Error(err);
14
7
  }
15
8
 
16
- /**
17
- * 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
- }
9
+ export function warnConstantCondition(location, tag, attr, expr, truthy) {
10
+ const article = /^[aeiou]/.test(attr) ? "an" : "a";
11
+ console.warn(
12
+ chalk.bold.yellow("WARN "),
13
+ `${chalk.bold.green(location)} ${chalk.dim('—')} ${chalk.blue(`<${tag}>`)} \`${chalk.bold(attr + "={" + expr + "}")}\` is always ${chalk.green(truthy ? "truthy" : "falsy")}`
14
+ );
15
+ }
33
16
 
34
- const importRegex = /import\s+(\w+)\s+from\s+["'](.+?\.(html|css))["'];?/g;
17
+ export function warnUnusedDeclaration(location, kind, name) {
18
+ console.warn(
19
+ chalk.bold.yellow("WARN "),
20
+ `${chalk.bold.green(location)} ${chalk.dim('—')} ${kind} \`${chalk.blue(name)}\` is never used`
21
+ );
22
+ }
35
23
 
36
- 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);
24
+ function normalizeAttributeQuotes(html) {
25
+ return html
26
+ .replace(/(\s[\w:.-]+)=(['"])([\s\S]*?)\2/g, '$1="$3"')
27
+ .replace(/(\s[\w:.-]+)=([^\s"'<>=]+)/g, '$1="$2"');
28
+ }
42
29
 
43
- 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
- );
30
+ function getStartTag(outerHTML) {
31
+ let inQuote = null;
32
+ for (let i = 0; i < outerHTML.length; i++) {
33
+ const c = outerHTML[i];
34
+ if (inQuote) {
35
+ if (c === inQuote) inQuote = null;
36
+ } else if (c === '"' || c === "'") {
37
+ inQuote = c;
38
+ } else if (c === ">") {
39
+ return outerHTML.slice(0, i + 1);
51
40
  }
52
-
53
- replacements.push({ from: match[0], to: `const ${varName} = ${JSON.stringify(content)};` });
54
41
  }
42
+ return outerHTML;
43
+ }
55
44
 
56
- for (const { from, to } of replacements) {
57
- code = code.replace(from, to);
58
- }
45
+ function escapeRegex(str) {
46
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
+ }
59
48
 
60
- const dataUrl =
61
- "data:text/javascript;base64," +
62
- Buffer.from(code).toString("base64");
49
+ function flexibleStartTagRegex(startTag) {
50
+ return new RegExp(
51
+ startTag
52
+ .split(/\s+/)
53
+ .filter(Boolean)
54
+ .map(escapeRegex)
55
+ .join("\\s+")
56
+ );
57
+ }
63
58
 
64
- 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
- );
59
+ export function findElementLine(sourceContent, outerHTML) {
60
+ const source = normalizeAttributeQuotes(sourceContent).toLowerCase().replace(/\/\s*>/g, ">");
61
+ const startTags = [
62
+ normalizeAttributeQuotes(getStartTag(outerHTML)),
63
+ normalizeAttributeQuotes(
64
+ getStartTag(
65
+ outerHTML
66
+ .replace(/&quot;/g, '"')
67
+ .replace(/&amp;/g, "&")
68
+ .replace(/&lt;/g, "<")
69
+ .replace(/&gt;/g, ">")
70
+ )
71
+ ),
72
+ ].map(tag => tag.toLowerCase());
73
+ const candidates = [
74
+ flexibleStartTagRegex(startTags[0]),
75
+ flexibleStartTagRegex(startTags[0].replace(/=""/g, "")),
76
+ flexibleStartTagRegex(startTags[1]),
77
+ ];
78
+ for (const re of candidates) {
79
+ const match = re.exec(source);
80
+ if (match) {
81
+ return source.substring(0, match.index).split("\n").length;
82
+ }
71
83
  }
84
+ return null;
72
85
  }
73
86
 
74
- /**
75
- * Generates a random ID string
76
- * @param {Array} collection - Array to track used IDs (prevents duplicates)
77
- * @param {number} length - Desired length of the ID (default: 10)
78
- * @returns {string} - Unique random ID
79
- */
80
87
  export function genRandomId(collection = null, length = 10, lettersOnly = false) {
81
88
  let id;
82
89
  if (lettersOnly) {
83
- const letters = "abcdefghijklmnopqrstuvwxyz";
84
- id = Array.from({ length }, () => letters[Math.floor(Math.random() * letters.length)]).join("");
90
+ id = Array.from({ length }, () => ID_LETTERS[Math.floor(Math.random() * ID_LETTERS.length)]).join("");
85
91
  } else {
86
92
  id = Math.random().toString(36).substring(2, length + 2);
87
93
  }
@@ -94,70 +100,17 @@ export function genRandomId(collection = null, length = 10, lettersOnly = false)
94
100
  }
95
101
  }
96
102
 
97
- /**
98
- * Increments a letter or sequence of letters like Excel columns (a → b, z → aa)
99
- * @param {string} letters - Letter(s) to increment
100
- * @returns {string} - Incremented letter(s)
101
- * @example
102
- * incrementAlfabet("a") // "b"
103
- * incrementAlfabet("z") // "aa"
104
- */
105
- export function incrementAlfabet(letters) {
106
- const alfabet = "abcdefghijklmnopqrstuvwxyz";
107
- let arr = letters.split("");
108
- let i = arr.length - 1;
109
-
110
- while (i >= 0) {
111
- let pos = alfabet.indexOf(arr[i]);
112
- if (pos < 25) {
113
- arr[i] = alfabet[pos + 1];
114
- return arr.join("");
115
- } else {
116
- arr[i] = "a";
117
- i--;
118
- }
119
- }
120
-
121
- return "a" + arr.join("");
122
- }
123
-
124
- /**
125
- * Checks if a string is a valid HTTP/HTTPS URL
126
- * @param {string} str - String to check
127
- * @returns {boolean} - True if valid web link, false otherwise
128
- */
129
- export function isWebLink(str) {
130
- try {
131
- const url = new URL(str);
132
- return url.protocol === "http:" || url.protocol === "https:";
133
- } catch (e) {
134
- return false;
103
+ export function deterministicHash(content, length = 8) {
104
+ const hash = createHash("sha256").update(content).digest();
105
+ let result = "";
106
+ for (let i = 0; i < length; i++) {
107
+ result += String.fromCharCode(97 + (hash[i % hash.length] % 26));
135
108
  }
109
+ return result;
136
110
  }
137
111
 
138
- const LBRACE_PH = "_%%CHOCOLA-LBRACE%%_";
139
- const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
140
-
141
- /**
142
- * Replaces HTML entities for curly braces with placeholders before JSDOM parsing.
143
- * This prevents JSDOM from decoding them into literal braces before binding processing.
144
- * @param {string} html - Raw HTML content
145
- * @returns {string} - HTML with curly brace entities replaced
146
- */
147
- export function protectCurlyBraces(html) {
148
- return html
149
- .replace(/&(?:lbrace|#123|#x7B);/gi, LBRACE_PH)
150
- .replace(/&(?:rcub|#125|#x7D);/gi, RBRACE_PH);
112
+ export function runtimeFunctionId(compName) {
113
+ return "r_" + deterministicHash(compName, 8);
151
114
  }
152
115
 
153
- /**
154
- * Restores curly brace placeholders back to literal characters.
155
- * Called after binding processing is complete and before final output.
156
- * @param {string} html - Serialized HTML with placeholders
157
- * @returns {string} - HTML with literal curly braces restored
158
- */
159
- export function restoreCurlyBraces(html) {
160
- return html
161
- .replace(/_%%CHOCOLA-LBRACE%%_/g, "{")
162
- .replace(/_%%CHOCOLA-RBRACE%%_/g, "}");
163
- }
116
+ const ID_LETTERS = "abcdefghijklmnopqrstuvwxyz";
package/dev/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import http from "http";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import chalk from "chalk";
4
+ import chalk from "../compiler/chalk.js";
5
5
  import compile from "../compiler/index.js";
6
6
  import { loadConfig, resolvePaths } from "../compiler/config.js";
7
- import { getChocolaConfig } from "../utils.js";
7
+ import { getConfig } from "../utils.js";
8
8
 
9
9
  export async function serve(__rootdir) {
10
10
  await compile(__rootdir);
@@ -17,7 +17,7 @@ export async function serve(__rootdir) {
17
17
 
18
18
  let lastBuildTime = Date.now();
19
19
 
20
- const fullConfig = await getChocolaConfig(__rootdir);
20
+ const fullConfig = await getConfig(__rootdir);
21
21
  const config = await loadConfig(__rootdir);
22
22
  const paths = resolvePaths(__rootdir, config);
23
23
  const devConfig = fullConfig.dev;
@@ -29,7 +29,6 @@ export async function serve(__rootdir) {
29
29
  else { console.warn(chalk.bold.yellow("WARNING!"), `port not defined in chocola.config.json file: using default ${__config.port} port.`) }
30
30
 
31
31
  const srcDir = paths.src;
32
- const componentsDir = paths.components;
33
32
 
34
33
  let compileTimeout;
35
34
  const scheduleRecompile = () => {
@@ -51,11 +50,6 @@ export async function serve(__rootdir) {
51
50
  scheduleRecompile();
52
51
  }
53
52
  });
54
- fs.watch(componentsDir, { recursive: true }, (eventType, filename) => {
55
- if (filename && !filename.includes("node_modules")) {
56
- scheduleRecompile();
57
- }
58
- });
59
53
 
60
54
  const server = http.createServer((req, res) => {
61
55
  if (req.url === "/api/hot-reload") {
@@ -150,6 +144,29 @@ export async function serve(__rootdir) {
150
144
  });
151
145
 
152
146
  server.listen(__config.port, __config.hostname, () => {
153
- console.log(`Chocola App running at http://${__config.hostname}:${__config.port}/`);
147
+ console.log('Live server running at', chalk.hex("#68C4EE").underline(`http://${__config.hostname}:${__config.port}/`));
154
148
  });
155
- }
149
+ }
150
+
151
+ /**
152
+ * An intrinsic object that contains the Chocola Dev methods.
153
+ */
154
+ export const dev = {
155
+ /**
156
+ * Initializes your Chocola dev server using a root directory.
157
+ *
158
+ * ```js
159
+ * import { app } from "chocola/compiler"
160
+ import path from "path";
161
+ import { fileURLToPath } from "url";
162
+
163
+ const __filename = fileURLToPath(import.meta.url);
164
+ const __dirname = path.dirname(__filename);
165
+
166
+ app.build(__dirname);
167
+ ```
168
+ * @example
169
+ * @param {PathLike} __rootdir the directory where your Chocola App is
170
+ */
171
+ async server(__rootdir) { return serve(__rootdir) }
172
+ };
package/index.js CHANGED
@@ -1,31 +0,0 @@
1
- import compile from "./compiler/index.js";
2
- import * as development from "./dev/index.js";
3
-
4
- /**
5
- * An intrinsic object that contains the Chocola App methods.
6
- */
7
- export const app = {
8
- /**
9
- * Initializes your Chocola App using a root directory and a source directory.
10
- *
11
- * ```js
12
- * import { app } from "chocola"
13
- import path from "path";
14
- import { fileURLToPath } from "url";
15
-
16
- const __filename = fileURLToPath(import.meta.url);
17
- const __dirname = path.dirname(__filename);
18
-
19
- app.build(__dirname, "src");
20
- ```
21
- * @example
22
- * @param {PathLike} __rootdir the directory where your Chocola App is
23
- * @param {PathLike} __srcdir the directory where your app sources files are
24
- * @returns {Object}
25
- */
26
- build(__rootdir, __srcdir) { compile(__rootdir, __srcdir) }
27
- }
28
-
29
- export const dev = {
30
- server(__rootdir, __srcdir, port) { development.serve(__rootdir, __srcdir, port) }
31
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chocola",
3
- "version": "1.6.0",
3
+ "version": "2.0.0-next.10",
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,21 +25,19 @@
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
- "test": "echo \"Error: no test specified\" && exit 1"
31
+ "test": "node --test \"tests/**/*.test.js\"",
32
+ "bench": "node bench/index.js",
33
+ "bench:csv": "node bench/index.js --csv"
32
34
  },
33
35
  "exports": {
34
- ".": "./index.js",
35
- "./types": "./types/index.js"
36
+ "./compiler": "./compiler/index.js",
37
+ "./dev": "./dev/index.js",
38
+ "./server": "./server/index.js"
36
39
  },
37
40
  "dependencies": {
38
- "chalk": "^5.6.2",
39
- "js-beautify": "^1.15.4",
40
- "jsdom": "^27.4.0"
41
- },
42
- "devDependencies": {
43
- "js-beautify": "^1.15.4"
41
+ "linkedom": "^0.18.13"
44
42
  }
45
43
  }