chocola 2.0.0-next.1 → 2.0.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +45 -35
- package/bin/chocola.js +227 -0
- package/compiler/chalk.js +34 -0
- package/compiler/component-processor.js +771 -402
- package/compiler/config.js +43 -66
- package/compiler/dom-processor.js +12 -75
- package/compiler/fs.js +0 -11
- package/compiler/index.js +87 -230
- package/compiler/module-graph.js +257 -0
- package/compiler/pipeline.js +56 -196
- package/compiler/render.js +183 -0
- package/compiler/runtime-generator.js +20 -15
- package/compiler/utils.js +79 -126
- package/dev/index.js +54 -22
- package/package.json +23 -12
- package/parser/component.js +340 -0
- package/parser/context.js +59 -0
- package/parser/css.js +96 -0
- package/parser/index.js +6 -0
- package/parser/script.js +485 -0
- package/parser/script.test.js +103 -0
- package/parser/template.js +120 -0
- package/parser/utils.js +32 -0
- package/runtime/index.js +312 -0
- package/server/index.js +432 -0
- package/utils.js +113 -8
- package/index.js +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import { parseHTML } from "linkedom";
|
|
4
|
+
import chalk from "./chalk.js";
|
|
5
|
+
import { loadConfig, resolvePaths } from "./config.js";
|
|
6
|
+
import { getComponents, getSrcIndex } from "./pipeline.js";
|
|
7
|
+
import { protectCurlyBraces } from "../utils.js";
|
|
8
|
+
import { deterministicHash, runtimeFunctionId } from "./utils.js";
|
|
9
|
+
import { createDOM, getAssetLinks, getScriptElements } from "./dom-processor.js";
|
|
10
|
+
import {
|
|
11
|
+
extractPropsDefaults, extractTopLevelFunctions, extractTopLevelVariables, parseScript, computeReachable,
|
|
12
|
+
} from "../parser/index.js";
|
|
13
|
+
|
|
14
|
+
const RUNTIME_KW = "$runtime";
|
|
15
|
+
const PAGE_ID = "index.html";
|
|
16
|
+
|
|
17
|
+
export class ChocolaModule {
|
|
18
|
+
constructor({ id, kind, sourcePath, source, mtimeMs }) {
|
|
19
|
+
this.id = id;
|
|
20
|
+
this.kind = kind;
|
|
21
|
+
this.sourcePath = sourcePath;
|
|
22
|
+
this.source = source;
|
|
23
|
+
this.mtimeMs = mtimeMs;
|
|
24
|
+
this.compName = kind === "component" ? path.basename(id) : null;
|
|
25
|
+
this.deps = new Set();
|
|
26
|
+
this.parsed = false;
|
|
27
|
+
this.script = null;
|
|
28
|
+
this.template = null;
|
|
29
|
+
this.styles = null;
|
|
30
|
+
this.compProps = [];
|
|
31
|
+
this.topFuncs = [];
|
|
32
|
+
this.topVars = [];
|
|
33
|
+
this.imports = [];
|
|
34
|
+
this.neededClientImports = [];
|
|
35
|
+
this.cssId = null;
|
|
36
|
+
this.fnId = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ModuleGraph {
|
|
41
|
+
constructor(rootDir, config, paths) {
|
|
42
|
+
this.rootDir = rootDir;
|
|
43
|
+
this.config = config;
|
|
44
|
+
this.paths = paths;
|
|
45
|
+
this.modules = new Map();
|
|
46
|
+
this.components = new Map();
|
|
47
|
+
this.loadedComponents = new Map();
|
|
48
|
+
this.originalComponentNames = new Map();
|
|
49
|
+
this.page = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
addModule(module) {
|
|
53
|
+
this.modules.set(module.id, module);
|
|
54
|
+
if (module.kind === "component") {
|
|
55
|
+
this.components.set(module.compName, module);
|
|
56
|
+
this.loadedComponents.set(module.compName, module.source);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
component(compName) {
|
|
61
|
+
return this.components.get(compName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
moduleById(id) {
|
|
65
|
+
return this.modules.get(id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function getFileMtime(filePath) {
|
|
70
|
+
try {
|
|
71
|
+
return (await fs.stat(filePath)).mtimeMs;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isExternalUrl(url) {
|
|
78
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function compileComponentModule(module, graph) {
|
|
82
|
+
if (module.parsed) return module;
|
|
83
|
+
|
|
84
|
+
const dom = parseHTML(protectCurlyBraces(module.source));
|
|
85
|
+
const doc = dom.document;
|
|
86
|
+
module.script = doc.querySelector("script")?.innerHTML ?? null;
|
|
87
|
+
module.template = doc.querySelector("template")?.innerHTML ?? null;
|
|
88
|
+
module.styles = doc.querySelector("style")?.innerHTML ?? null;
|
|
89
|
+
module.compProps = extractPropsDefaults(module.script);
|
|
90
|
+
module.topFuncs = extractTopLevelFunctions(module.script || "", RUNTIME_KW);
|
|
91
|
+
module.topVars = extractTopLevelVariables(module.script || "");
|
|
92
|
+
module.cssId = deterministicHash(module.compName, 8);
|
|
93
|
+
module.fnId = runtimeFunctionId(module.compName);
|
|
94
|
+
|
|
95
|
+
const deps = new Set();
|
|
96
|
+
if (module.script) {
|
|
97
|
+
const parsed = parseScript(module.script);
|
|
98
|
+
if (parsed.ast) {
|
|
99
|
+
module.imports = parsed.imports;
|
|
100
|
+
let importsForDeps = parsed.imports;
|
|
101
|
+
const treeShake = graph.config.treeShakeRuntime !== false;
|
|
102
|
+
if (treeShake && parsed.runtimeNode) {
|
|
103
|
+
const reach = computeReachable(parsed, { bindings: [] });
|
|
104
|
+
if (!reach.fallback) {
|
|
105
|
+
const neededSet = new Set(reach.neededImports);
|
|
106
|
+
importsForDeps = parsed.imports.filter(imp => neededSet.has(imp));
|
|
107
|
+
}
|
|
108
|
+
module.neededClientImports = importsForDeps;
|
|
109
|
+
} else if (!treeShake) {
|
|
110
|
+
// Feature flag disabled: include all imports for deps
|
|
111
|
+
importsForDeps = parsed.imports;
|
|
112
|
+
module.neededClientImports = importsForDeps;
|
|
113
|
+
} else {
|
|
114
|
+
importsForDeps = [];
|
|
115
|
+
module.neededClientImports = [];
|
|
116
|
+
}
|
|
117
|
+
for (const imp of importsForDeps) {
|
|
118
|
+
const isComponent = imp.source.toLowerCase().endsWith(".html");
|
|
119
|
+
if (!isComponent) {
|
|
120
|
+
// Phase 1: JS imports that are client-reachable are warned in component-processor (render-time)
|
|
121
|
+
// to avoid duplicate warnings at graph build time. Silently skip for deps.
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const importedCompName = path.basename(imp.source).toLowerCase();
|
|
125
|
+
const importedModule = graph.component(importedCompName);
|
|
126
|
+
if (importedModule) deps.add(importedModule.id);
|
|
127
|
+
}
|
|
128
|
+
// Template deps added below will cover component usage in template even when
|
|
129
|
+
// script import is not client-reachable (server-only).
|
|
130
|
+
// Keep full import list in module.imports for reference.
|
|
131
|
+
} else {
|
|
132
|
+
// Fallback to regex for scripts that failed to parse (e.g., syntax errors)
|
|
133
|
+
module.imports = [];
|
|
134
|
+
module.neededClientImports = [];
|
|
135
|
+
const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?\s*/g;
|
|
136
|
+
let match;
|
|
137
|
+
while ((match = importRegex.exec(module.script)) !== null) {
|
|
138
|
+
const importedCompName = path.basename(match[2]).toLowerCase();
|
|
139
|
+
const importedModule = graph.component(importedCompName);
|
|
140
|
+
if (importedModule) deps.add(importedModule.id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
module.imports = [];
|
|
145
|
+
module.neededClientImports = [];
|
|
146
|
+
}
|
|
147
|
+
if (module.template) {
|
|
148
|
+
const frag = parseHTML(module.template).document;
|
|
149
|
+
for (const el of frag.querySelectorAll("*")) {
|
|
150
|
+
const childModule = graph.component(el.tagName.toLowerCase() + ".html");
|
|
151
|
+
if (childModule) deps.add(childModule.id);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
module.deps = deps;
|
|
155
|
+
module.parsed = true;
|
|
156
|
+
return module;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function scanComponentDeps(root, graph) {
|
|
160
|
+
const deps = new Set();
|
|
161
|
+
for (const el of root.querySelectorAll("*")) {
|
|
162
|
+
const childModule = graph.component(el.tagName.toLowerCase() + ".html");
|
|
163
|
+
if (childModule) deps.add(childModule.id);
|
|
164
|
+
}
|
|
165
|
+
return deps;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function scanAssetModules(doc, graph) {
|
|
169
|
+
const deps = new Set();
|
|
170
|
+
const { stylesheets, icons } = getAssetLinks(doc);
|
|
171
|
+
const scripts = getScriptElements(doc);
|
|
172
|
+
|
|
173
|
+
const collect = (href) => {
|
|
174
|
+
if (isExternalUrl(href)) return;
|
|
175
|
+
const id = href.split("\\").join("/");
|
|
176
|
+
const existing = graph.moduleById(id);
|
|
177
|
+
if (existing) {
|
|
178
|
+
deps.add(id);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const asset = new ChocolaModule({
|
|
182
|
+
id,
|
|
183
|
+
kind: "asset",
|
|
184
|
+
sourcePath: path.join(graph.paths.src, href),
|
|
185
|
+
source: null,
|
|
186
|
+
mtimeMs: null,
|
|
187
|
+
});
|
|
188
|
+
graph.addModule(asset);
|
|
189
|
+
deps.add(id);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
for (const link of stylesheets) collect(link.href);
|
|
193
|
+
for (const link of icons) collect(link.href);
|
|
194
|
+
for (const script of scripts) collect(script.getAttribute("src"));
|
|
195
|
+
|
|
196
|
+
return deps;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function buildModuleGraph(rootDir, { overrides } = {}) {
|
|
200
|
+
const config = overrides ? await loadConfig(rootDir, { silent: true, overrides }) : await loadConfig(rootDir);
|
|
201
|
+
const paths = resolvePaths(rootDir, config);
|
|
202
|
+
|
|
203
|
+
const graph = new ModuleGraph(rootDir, config, paths);
|
|
204
|
+
|
|
205
|
+
const indexFiles = await getSrcIndex(paths.src);
|
|
206
|
+
const pageModule = new ChocolaModule({
|
|
207
|
+
id: PAGE_ID,
|
|
208
|
+
kind: "page",
|
|
209
|
+
sourcePath: indexFiles.srcPath,
|
|
210
|
+
source: indexFiles.srcHtmlFile,
|
|
211
|
+
mtimeMs: await getFileMtime(indexFiles.srcPath),
|
|
212
|
+
});
|
|
213
|
+
graph.addModule(pageModule);
|
|
214
|
+
graph.page = pageModule;
|
|
215
|
+
|
|
216
|
+
const foundComponents = await getComponents(paths.components);
|
|
217
|
+
const { loadedComponents, originalNames, componentsLib, emptyComps } = foundComponents;
|
|
218
|
+
|
|
219
|
+
console.log(chalk.bold.green(">"), "Components found in", chalk.green.underline(paths.components) + ":");
|
|
220
|
+
console.log(" ", componentsLib, "\n\n");
|
|
221
|
+
|
|
222
|
+
if (emptyComps?.length > 0) {
|
|
223
|
+
console.warn(chalk.bold.yellow("WARNING!"), "The following component files are empty:");
|
|
224
|
+
console.log(" ", emptyComps);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const filename of componentsLib) {
|
|
228
|
+
const compName = filename.toLowerCase();
|
|
229
|
+
const source = loadedComponents.get(compName);
|
|
230
|
+
if (source === undefined) continue;
|
|
231
|
+
const id = path.posix.join(config.libDir, compName);
|
|
232
|
+
const module = new ChocolaModule({
|
|
233
|
+
id,
|
|
234
|
+
kind: "component",
|
|
235
|
+
sourcePath: path.join(paths.components, filename),
|
|
236
|
+
source,
|
|
237
|
+
mtimeMs: await getFileMtime(path.join(paths.components, filename)),
|
|
238
|
+
});
|
|
239
|
+
graph.addModule(module);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Track original filename casing for log display (fallback to lowercase tag)
|
|
243
|
+
graph.originalComponentNames = originalNames;
|
|
244
|
+
|
|
245
|
+
for (const module of graph.modules.values()) {
|
|
246
|
+
if (module.kind === "component") compileComponentModule(module, graph);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const pageDom = createDOM(pageModule.source);
|
|
250
|
+
const pageDeps = scanComponentDeps(pageDom.document, graph);
|
|
251
|
+
for (const dep of scanAssetModules(pageDom.document, graph)) {
|
|
252
|
+
pageDeps.add(dep);
|
|
253
|
+
}
|
|
254
|
+
pageModule.deps = pageDeps;
|
|
255
|
+
|
|
256
|
+
return graph;
|
|
257
|
+
}
|
package/compiler/pipeline.js
CHANGED
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
|
-
import {
|
|
2
|
+
import { throwError, deterministicHash } 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
|
|
10
|
+
let originalNames = new Map();
|
|
11
|
+
let emptyComps = [];
|
|
24
12
|
|
|
25
13
|
const components = await fs.readdir(libDir);
|
|
26
14
|
|
|
@@ -28,46 +16,31 @@ export async function getComponents(libDir) {
|
|
|
28
16
|
throw Error(`The specified components folder ${libDir} could not be found.`);
|
|
29
17
|
}
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
const reads = components
|
|
20
|
+
.filter(comp => comp.endsWith(".html"))
|
|
21
|
+
.map(async (comp) => {
|
|
22
|
+
try {
|
|
23
|
+
const compPath = path.join(libDir, comp);
|
|
24
|
+
const instance = await fs.readFile(compPath, "utf-8");
|
|
35
25
|
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
module = await loadWithAssets(path.join(libDir, comp));
|
|
26
|
+
if (instance === "" || instance.trim().length === 0) emptyComps.push(comp);
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
loadedComponents.set(comp.toLowerCase(), instance);
|
|
29
|
+
originalNames.set(comp.toLowerCase(), comp);
|
|
30
|
+
componentsLib.push(comp);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throwError(`Failed to load component "${comp}": ${err.message || err}`);
|
|
43
33
|
}
|
|
34
|
+
});
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
const compPath = path.join(libDir, comp);
|
|
47
|
-
instance.__sourceFile = compPath;
|
|
48
|
-
|
|
49
|
-
loadedComponents.set(comp.toLowerCase(), instance);
|
|
50
|
-
} catch (err) {
|
|
51
|
-
throwError(`Failed to load component "${comp}": ${err.message || err}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
36
|
+
await Promise.all(reads);
|
|
54
37
|
|
|
55
|
-
return { componentsLib, loadedComponents,
|
|
38
|
+
return { componentsLib, loadedComponents, originalNames, emptyComps };
|
|
56
39
|
} catch (err) {
|
|
57
40
|
throwError(`Failed to load components from ${libDir}: ${err.message}`);
|
|
58
41
|
}
|
|
59
42
|
}
|
|
60
43
|
|
|
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
44
|
export async function getSrcIndex(srcPath) {
|
|
72
45
|
const srcHtmlPath = path.join(srcPath, "index.html");
|
|
73
46
|
|
|
@@ -85,24 +58,21 @@ export async function getSrcIndex(srcPath) {
|
|
|
85
58
|
}
|
|
86
59
|
}
|
|
87
60
|
|
|
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
|
-
export async function processStylesheet(link, rootDir, srcDir, outDirPath, fileIds) {
|
|
61
|
+
export async function processStylesheet(link, rootDir, srcDir, out) {
|
|
98
62
|
try {
|
|
99
63
|
const href = link.href;
|
|
100
64
|
if (href.startsWith("http://") || href.startsWith("https://")) return;
|
|
101
65
|
const stylesheetPath = path.join(rootDir, srcDir, href);
|
|
102
66
|
const css = await fs.readFile(stylesheetPath, { encoding: "utf8" });
|
|
103
|
-
const
|
|
67
|
+
const base = deterministicHash(css + href, 6);
|
|
68
|
+
let cssFileName = "css-" + base + ".css";
|
|
69
|
+
let counter = 0;
|
|
70
|
+
while (out.ids.includes(cssFileName)) {
|
|
71
|
+
cssFileName = "css-" + deterministicHash(css + href + counter++, 6) + ".css";
|
|
72
|
+
}
|
|
73
|
+
out.ids.push(cssFileName);
|
|
104
74
|
|
|
105
|
-
|
|
75
|
+
out.files.push({ path: cssFileName, content: css });
|
|
106
76
|
link.setAttribute("href", "./" + cssFileName);
|
|
107
77
|
|
|
108
78
|
return css;
|
|
@@ -111,165 +81,55 @@ export async function processStylesheet(link, rootDir, srcDir, outDirPath, fileI
|
|
|
111
81
|
}
|
|
112
82
|
}
|
|
113
83
|
|
|
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
|
-
export async function processIcons(link, rootDir, srcDir, outDirPath) {
|
|
84
|
+
export async function processIcons(link, rootDir, srcDir, out) {
|
|
123
85
|
try {
|
|
124
86
|
const href = link.href;
|
|
125
87
|
if (href.startsWith("http://") || href.startsWith("https://")) return;
|
|
126
88
|
const iconPath = path.join(rootDir, srcDir, href);
|
|
127
|
-
|
|
89
|
+
out.copies.push({ from: iconPath, to: path.join(out.outDir, href) });
|
|
128
90
|
} catch (err) {
|
|
129
91
|
throwError(`Failed to copy icon: ${err}`);
|
|
130
92
|
}
|
|
131
93
|
}
|
|
132
94
|
|
|
133
|
-
export function
|
|
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) {
|
|
95
|
+
export async function processScript(doc, script, rootDir, srcDir, out) {
|
|
187
96
|
try {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
}
|
|
237
|
-
}
|
|
97
|
+
const src = script.getAttribute("src");
|
|
98
|
+
if (src.startsWith("http://") || src.startsWith("https://")) return;
|
|
99
|
+
const scriptPath = path.join(rootDir, srcDir, src);
|
|
100
|
+
const content = await fs.readFile(scriptPath, { encoding: "utf8" });
|
|
101
|
+
const base = deterministicHash(content + src, 6);
|
|
102
|
+
let jsFileName = "js-" + base + ".js";
|
|
103
|
+
let counter = 0;
|
|
104
|
+
while (out.ids.includes(jsFileName)) {
|
|
105
|
+
jsFileName = "js-" + deterministicHash(content + src + counter++, 6) + ".js";
|
|
238
106
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
} catch (err) {
|
|
249
|
-
throw new Error(
|
|
250
|
-
`Failed to copy CSS asset "${assetPath}" referenced from stylesheets: ${err.message || err}`
|
|
251
|
-
);
|
|
107
|
+
out.ids.push(jsFileName);
|
|
108
|
+
out.files.push({ path: jsFileName, content });
|
|
109
|
+
|
|
110
|
+
const newScript = doc.createElement("script");
|
|
111
|
+
for (const attr of script.attributes) {
|
|
112
|
+
if (attr.name === "src") {
|
|
113
|
+
newScript.setAttribute("src", "./" + jsFileName);
|
|
114
|
+
} else {
|
|
115
|
+
newScript.setAttribute(attr.name, attr.value);
|
|
252
116
|
}
|
|
253
117
|
}
|
|
118
|
+
if (script.textContent.trim()) {
|
|
119
|
+
newScript.textContent = script.textContent;
|
|
120
|
+
}
|
|
121
|
+
script.parentNode.replaceChild(newScript, script);
|
|
254
122
|
} catch (err) {
|
|
255
|
-
|
|
123
|
+
throwError(`Failed to process script: ${err}`);
|
|
256
124
|
}
|
|
257
125
|
}
|
|
258
126
|
|
|
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
|
-
export async function copyStaticDir(srcPath, outDirPath) {
|
|
127
|
+
export async function copyStaticDir(srcPath, out) {
|
|
267
128
|
const staticSrc = path.join(srcPath, "static");
|
|
268
|
-
const staticDest = path.join(
|
|
129
|
+
const staticDest = path.join(out.outDir, "static");
|
|
269
130
|
try {
|
|
270
131
|
await fs.access(staticSrc);
|
|
271
|
-
|
|
132
|
+
out.copies.push({ from: staticSrc, to: staticDest, recursive: true });
|
|
272
133
|
} catch {
|
|
273
|
-
// static/ directory doesn't exist — nothing to copy
|
|
274
134
|
}
|
|
275
135
|
}
|