zero-query 1.0.9 → 1.2.0
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 +21 -21
- package/README.md +2 -0
- package/cli/args.js +33 -33
- package/cli/commands/build-api.js +443 -0
- package/cli/commands/build.js +254 -216
- package/cli/commands/bundle.js +1228 -1183
- package/cli/commands/create.js +137 -121
- package/cli/commands/dev/devtools/index.js +56 -56
- package/cli/commands/dev/devtools/js/components.js +49 -49
- package/cli/commands/dev/devtools/js/core.js +423 -423
- package/cli/commands/dev/devtools/js/elements.js +421 -421
- package/cli/commands/dev/devtools/js/network.js +166 -166
- package/cli/commands/dev/devtools/js/performance.js +73 -73
- package/cli/commands/dev/devtools/js/router.js +105 -105
- package/cli/commands/dev/devtools/js/source.js +132 -132
- package/cli/commands/dev/devtools/js/stats.js +35 -35
- package/cli/commands/dev/devtools/js/tabs.js +79 -79
- package/cli/commands/dev/devtools/panel.html +95 -95
- package/cli/commands/dev/devtools/styles.css +244 -244
- package/cli/commands/dev/index.js +107 -107
- package/cli/commands/dev/logger.js +75 -75
- package/cli/commands/dev/overlay.js +858 -858
- package/cli/commands/dev/server.js +220 -167
- package/cli/commands/dev/validator.js +94 -94
- package/cli/commands/dev/watcher.js +172 -172
- package/cli/help.js +114 -112
- package/cli/index.js +52 -52
- package/cli/scaffold/default/LICENSE +21 -21
- package/cli/scaffold/default/app/app.js +207 -207
- package/cli/scaffold/default/app/components/about.js +201 -201
- package/cli/scaffold/default/app/components/api-demo.js +143 -143
- package/cli/scaffold/default/app/components/contact-card.js +231 -231
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
- package/cli/scaffold/default/app/components/counter.js +127 -127
- package/cli/scaffold/default/app/components/home.js +249 -249
- package/cli/scaffold/default/app/components/not-found.js +16 -16
- package/cli/scaffold/default/app/components/playground/playground.css +115 -115
- package/cli/scaffold/default/app/components/playground/playground.html +161 -161
- package/cli/scaffold/default/app/components/playground/playground.js +116 -116
- package/cli/scaffold/default/app/components/todos.js +225 -225
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
- package/cli/scaffold/default/app/routes.js +15 -15
- package/cli/scaffold/default/app/store.js +101 -101
- package/cli/scaffold/default/global.css +552 -552
- package/cli/scaffold/default/index.html +99 -99
- package/cli/scaffold/minimal/app/app.js +85 -85
- package/cli/scaffold/minimal/app/components/about.js +68 -68
- package/cli/scaffold/minimal/app/components/counter.js +122 -122
- package/cli/scaffold/minimal/app/components/home.js +68 -68
- package/cli/scaffold/minimal/app/components/not-found.js +16 -16
- package/cli/scaffold/minimal/app/routes.js +9 -9
- package/cli/scaffold/minimal/app/store.js +36 -36
- package/cli/scaffold/minimal/global.css +300 -300
- package/cli/scaffold/minimal/index.html +44 -44
- package/cli/scaffold/ssr/app/app.js +41 -41
- package/cli/scaffold/ssr/app/components/about.js +55 -55
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
- package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
- package/cli/scaffold/ssr/app/components/home.js +37 -37
- package/cli/scaffold/ssr/app/components/not-found.js +15 -15
- package/cli/scaffold/ssr/app/routes.js +8 -8
- package/cli/scaffold/ssr/global.css +228 -228
- package/cli/scaffold/ssr/index.html +37 -37
- package/cli/scaffold/ssr/package.json +8 -8
- package/cli/scaffold/ssr/server/data/posts.js +144 -144
- package/cli/scaffold/ssr/server/index.js +213 -213
- package/cli/scaffold/webrtc/app/app.js +11 -0
- package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
- package/cli/scaffold/webrtc/app/lib/room.js +252 -0
- package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
- package/cli/scaffold/webrtc/global.css +250 -0
- package/cli/scaffold/webrtc/index.html +21 -0
- package/cli/utils.js +305 -287
- package/dist/API.md +7264 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +10313 -6252
- package/dist/zquery.min.js +8 -601
- package/index.d.ts +570 -365
- package/index.js +311 -232
- package/package.json +76 -69
- package/src/component.js +1709 -1454
- package/src/core.js +921 -921
- package/src/diff.js +497 -497
- package/src/errors.js +209 -209
- package/src/expression.js +922 -922
- package/src/http.js +242 -242
- package/src/package.json +1 -1
- package/src/reactive.js +255 -254
- package/src/router.js +843 -773
- package/src/ssr.js +418 -418
- package/src/store.js +318 -272
- package/src/utils.js +515 -515
- package/src/webrtc/e2ee.js +351 -0
- package/src/webrtc/errors.js +116 -0
- package/src/webrtc/ice.js +301 -0
- package/src/webrtc/index.js +131 -0
- package/src/webrtc/joinToken.js +119 -0
- package/src/webrtc/observe.js +172 -0
- package/src/webrtc/peer.js +351 -0
- package/src/webrtc/reactive.js +268 -0
- package/src/webrtc/room.js +625 -0
- package/src/webrtc/sdp.js +302 -0
- package/src/webrtc/sfu/index.js +43 -0
- package/src/webrtc/sfu/livekit.js +131 -0
- package/src/webrtc/sfu/mediasoup.js +150 -0
- package/src/webrtc/signaling.js +373 -0
- package/src/webrtc/turn.js +237 -0
- package/tests/_helpers/webrtcFakes.js +289 -0
- package/tests/audit.test.js +4158 -4158
- package/tests/cli.test.js +1136 -1023
- package/tests/compare.test.js +497 -0
- package/tests/component.test.js +3969 -3938
- package/tests/core.test.js +1910 -1910
- package/tests/dev-server.test.js +489 -0
- package/tests/diff.test.js +1416 -1416
- package/tests/docs.test.js +1664 -0
- package/tests/electron-features.test.js +864 -0
- package/tests/errors.test.js +619 -619
- package/tests/expression.test.js +1056 -1056
- package/tests/http.test.js +648 -648
- package/tests/reactive.test.js +819 -819
- package/tests/router.test.js +2327 -2327
- package/tests/ssr.test.js +870 -870
- package/tests/store.test.js +830 -830
- package/tests/test-minifier.js +153 -153
- package/tests/test-ssr.js +27 -27
- package/tests/utils.test.js +1377 -1377
- package/tests/webrtc/e2ee.test.js +283 -0
- package/tests/webrtc/ice.test.js +202 -0
- package/tests/webrtc/joinToken.test.js +89 -0
- package/tests/webrtc/observe.test.js +111 -0
- package/tests/webrtc/peer.test.js +373 -0
- package/tests/webrtc/reactive.test.js +235 -0
- package/tests/webrtc/room.test.js +406 -0
- package/tests/webrtc/sdp.test.js +151 -0
- package/tests/webrtc/sfu-livekit.test.js +119 -0
- package/tests/webrtc/sfu.test.js +160 -0
- package/tests/webrtc/signaling.test.js +251 -0
- package/tests/webrtc/turn.test.js +256 -0
- package/types/collection.d.ts +383 -383
- package/types/component.d.ts +186 -186
- package/types/errors.d.ts +135 -135
- package/types/http.d.ts +92 -92
- package/types/misc.d.ts +201 -201
- package/types/reactive.d.ts +98 -98
- package/types/router.d.ts +190 -190
- package/types/ssr.d.ts +102 -102
- package/types/store.d.ts +146 -145
- package/types/utils.d.ts +245 -245
- package/types/webrtc.d.ts +653 -0
package/cli/commands/bundle.js
CHANGED
|
@@ -1,1183 +1,1228 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/commands/bundle.js - app bundler command
|
|
3
|
-
*
|
|
4
|
-
* Walks the ES module import graph starting from an entry file,
|
|
5
|
-
* strips import/export syntax, concatenates everything into a single
|
|
6
|
-
* IIFE with content-hashed filenames, and rewrites index.html for
|
|
7
|
-
* both server and local (file://) deployment.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const crypto = require('crypto');
|
|
15
|
-
|
|
16
|
-
const { args, flag, option } = require('../args');
|
|
17
|
-
const { minify, sizeKB } = require('../utils');
|
|
18
|
-
const buildLibrary = require('./build');
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Module graph helpers
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
/** Resolve an import specifier relative to the importing file. */
|
|
25
|
-
function resolveImport(specifier, fromFile) {
|
|
26
|
-
if (!specifier.startsWith('.') && !specifier.startsWith('/')) return null;
|
|
27
|
-
// Skip non-JS assets (CSS, images, etc.) referenced in code examples
|
|
28
|
-
if (/\.(?:css|json|svg|png|jpg|gif|woff2?|ttf|eot)$/i.test(specifier)) return null;
|
|
29
|
-
let resolved = path.resolve(path.dirname(fromFile), specifier);
|
|
30
|
-
if (!path.extname(resolved) && fs.existsSync(resolved + '.js')) {
|
|
31
|
-
resolved += '.js';
|
|
32
|
-
}
|
|
33
|
-
return resolved;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Extract import specifiers from a source file. */
|
|
37
|
-
function extractImports(code) {
|
|
38
|
-
// Only scan the import preamble (before the first top-level `export`)
|
|
39
|
-
// so that code examples inside exported template strings are not
|
|
40
|
-
// mistaken for real imports.
|
|
41
|
-
const exportStart = code.search(/^export\b/m);
|
|
42
|
-
const preamble = exportStart > -1 ? code.slice(0, exportStart) : code;
|
|
43
|
-
|
|
44
|
-
const specifiers = [];
|
|
45
|
-
let m;
|
|
46
|
-
const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g;
|
|
47
|
-
while ((m = fromRe.exec(preamble)) !== null) specifiers.push(m[1]);
|
|
48
|
-
const sideRe = /^\s*import\s+['"]([^'"]+)['"]\s*;?\s*$/gm;
|
|
49
|
-
while ((m = sideRe.exec(preamble)) !== null) {
|
|
50
|
-
if (!specifiers.includes(m[1])) specifiers.push(m[1]);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Also capture re-exports anywhere in the file:
|
|
54
|
-
// export { x } from '...' export * from '...'
|
|
55
|
-
const reExportRe = /^\s*export\s+(?:\{[^}]*\}|\*)\s*from\s+['"]([^'"]+)['"]/gm;
|
|
56
|
-
while ((m = reExportRe.exec(code)) !== null) {
|
|
57
|
-
if (!specifiers.includes(m[1])) specifiers.push(m[1]);
|
|
58
|
-
}
|
|
59
|
-
return specifiers;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Walk the import graph - topological sort (leaves first). */
|
|
63
|
-
function walkImportGraph(entry) {
|
|
64
|
-
const visited = new Set();
|
|
65
|
-
const order = [];
|
|
66
|
-
|
|
67
|
-
function visit(file) {
|
|
68
|
-
const abs = path.resolve(file);
|
|
69
|
-
if (visited.has(abs)) return;
|
|
70
|
-
visited.add(abs);
|
|
71
|
-
|
|
72
|
-
if (!fs.existsSync(abs)) {
|
|
73
|
-
console.warn(` ⚠ Missing file: ${abs}`);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const code = fs.readFileSync(abs, 'utf-8');
|
|
78
|
-
const imports = extractImports(code);
|
|
79
|
-
for (const spec of imports) {
|
|
80
|
-
const resolved = resolveImport(spec, abs);
|
|
81
|
-
if (resolved) visit(resolved);
|
|
82
|
-
}
|
|
83
|
-
order.push(abs);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
visit(entry);
|
|
87
|
-
return order;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Strip ES module import/export syntax, keeping declarations.
|
|
92
|
-
* Exported const/let are converted to var so they hoist past the per-module
|
|
93
|
-
* block scope and remain accessible to downstream modules.
|
|
94
|
-
* Exported function/class are converted to var assignments for the same reason.
|
|
95
|
-
* Non-exported declarations stay as-is and remain block-scoped (private).
|
|
96
|
-
*
|
|
97
|
-
* Template literals are temporarily hidden via a character-level scanner
|
|
98
|
-
* (handles nested backtick expressions) so that code examples inside
|
|
99
|
-
* backtick strings aren't accidentally rewritten.
|
|
100
|
-
*/
|
|
101
|
-
function stripModuleSyntax(code) {
|
|
102
|
-
// -- Hide template literals (supports nesting) -------------------------
|
|
103
|
-
const templates = [];
|
|
104
|
-
|
|
105
|
-
function scanTemplateLiteral(str, start) {
|
|
106
|
-
let i = start + 1; // skip opening backtick
|
|
107
|
-
while (i < str.length) {
|
|
108
|
-
const ch = str[i];
|
|
109
|
-
if (ch === '\\') { i += 2; continue; }
|
|
110
|
-
if (ch === '`') { return i + 1; } // end of template
|
|
111
|
-
if (ch === '$' && str[i + 1] === '{') {
|
|
112
|
-
i += 2; // skip ${
|
|
113
|
-
let depth = 1;
|
|
114
|
-
while (i < str.length && depth > 0) {
|
|
115
|
-
const c = str[i];
|
|
116
|
-
if (c === '{') { depth++; i++; }
|
|
117
|
-
else if (c === '}') { depth--; i++; }
|
|
118
|
-
else if (c === '`') { i = scanTemplateLiteral(str, i); }
|
|
119
|
-
else if (c === "'" || c === '"') { i = skipString(str, i); }
|
|
120
|
-
else if (c === '/' && str[i + 1] === '/') { while (i < str.length && str[i] !== '\n') i++; }
|
|
121
|
-
else if (c === '/' && str[i + 1] === '*') { i += 2; while (i < str.length - 1 && !(str[i] === '*' && str[i + 1] === '/')) i++; i += 2; }
|
|
122
|
-
else { i++; }
|
|
123
|
-
}
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
i++;
|
|
127
|
-
}
|
|
128
|
-
return i;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function skipString(str, start) {
|
|
132
|
-
const q = str[start];
|
|
133
|
-
let i = start + 1;
|
|
134
|
-
while (i < str.length) {
|
|
135
|
-
if (str[i] === '\\') { i += 2; continue; }
|
|
136
|
-
if (str[i] === q) { return i + 1; }
|
|
137
|
-
i++;
|
|
138
|
-
}
|
|
139
|
-
return i;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
let hidden = '';
|
|
143
|
-
let pos = 0;
|
|
144
|
-
while (pos < code.length) {
|
|
145
|
-
const ch = code[pos];
|
|
146
|
-
if (ch === "'" || ch === '"') {
|
|
147
|
-
const end = skipString(code, pos);
|
|
148
|
-
hidden += code.substring(pos, end);
|
|
149
|
-
pos = end;
|
|
150
|
-
} else if (ch === '/' && code[pos + 1] === '/') {
|
|
151
|
-
let end = pos;
|
|
152
|
-
while (end < code.length && code[end] !== '\n') end++;
|
|
153
|
-
hidden += code.substring(pos, end);
|
|
154
|
-
pos = end;
|
|
155
|
-
} else if (ch === '/' && code[pos + 1] === '*') {
|
|
156
|
-
let end = pos + 2;
|
|
157
|
-
while (end < code.length - 1 && !(code[end] === '*' && code[end + 1] === '/')) end++;
|
|
158
|
-
end += 2;
|
|
159
|
-
hidden += code.substring(pos, end);
|
|
160
|
-
pos = end;
|
|
161
|
-
} else if (ch === '`') {
|
|
162
|
-
const end = scanTemplateLiteral(code, pos);
|
|
163
|
-
templates.push(code.substring(pos, end));
|
|
164
|
-
hidden += `__TPL_${templates.length - 1}__`;
|
|
165
|
-
pos = end;
|
|
166
|
-
} else {
|
|
167
|
-
hidden += ch;
|
|
168
|
-
pos++;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// -- Apply import / export transforms -----------------------------------
|
|
173
|
-
code = hidden;
|
|
174
|
-
code = code.replace(/^\s*import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
175
|
-
code = code.replace(/^\s*import\s+['"].*?['"];?\s*$/gm, '');
|
|
176
|
-
code = code.replace(/^(\s*)export\s+default\s+/gm, '$1');
|
|
177
|
-
// Convert exported const/let/var to var (hoists past block scope)
|
|
178
|
-
code = code.replace(/^(\s*)export\s+(const|let|var)\s/gm, '$1var ');
|
|
179
|
-
// Convert exported function/async function to var assignment (hoists past block scope)
|
|
180
|
-
code = code.replace(/^(\s*)export\s+async\s+function\s+(\w+)/gm, '$1var $2 = async function $2');
|
|
181
|
-
code = code.replace(/^(\s*)export\s+function\s+(\w+)/gm, '$1var $2 = function $2');
|
|
182
|
-
// Convert exported class to var assignment
|
|
183
|
-
code = code.replace(/^(\s*)export\s+class\s+(\w+)/gm, '$1var $2 = class $2');
|
|
184
|
-
// Collect names from bare export blocks: export { a, b, c };
|
|
185
|
-
// These names are declared elsewhere (function/const/let) and need to be
|
|
186
|
-
// hoisted past the per-module block scope. We collect them here and
|
|
187
|
-
// the caller converts their declarations to var-based forms.
|
|
188
|
-
const bareExportNames = [];
|
|
189
|
-
code = code.replace(/^\s*export\s*\{([^}]+)\};?\s*$/gm, (_, names) => {
|
|
190
|
-
for (const n of names.split(',')) {
|
|
191
|
-
const parts = n.trim().split(/\s+as\s+/);
|
|
192
|
-
if (parts[0]) bareExportNames.push({ local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() });
|
|
193
|
-
}
|
|
194
|
-
return '';
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// For every bare-exported name, convert its block-scoped declaration to a
|
|
198
|
-
// var-based form so it hoists past the per-module { } wrapper:
|
|
199
|
-
// function foo(…) { … } → var foo = function foo(…) { … }
|
|
200
|
-
// async function foo(…) → var foo = async function foo(…)
|
|
201
|
-
// const/let foo = … → var foo = …
|
|
202
|
-
for (const { local } of bareExportNames) {
|
|
203
|
-
const fnRe = new RegExp(`^(\\s*)function\\s+${local}\\s*\\(`, 'gm');
|
|
204
|
-
code = code.replace(fnRe, `$1var ${local} = function ${local}(`);
|
|
205
|
-
const asyncFnRe = new RegExp(`^(\\s*)async\\s+function\\s+${local}\\s*\\(`, 'gm');
|
|
206
|
-
code = code.replace(asyncFnRe, `$1var ${local} = async function ${local}(`);
|
|
207
|
-
const constLetRe = new RegExp(`^(\\s*)(const|let)\\s+${local}\\b`, 'gm');
|
|
208
|
-
code = code.replace(constLetRe, `$1var ${local}`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Create aliases for "export { local as exported }" where the names differ
|
|
212
|
-
for (const { local, exported } of bareExportNames) {
|
|
213
|
-
if (exported !== local) {
|
|
214
|
-
code += `\nvar ${exported} = ${local};`;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// -- Restore template literals ------------------------------------------
|
|
219
|
-
code = code.replace(/__TPL_(\d+)__/g, (_, i) => templates[i]);
|
|
220
|
-
return { code, bareExportNames };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Replace import.meta.url with a runtime equivalent. */
|
|
224
|
-
function replaceImportMeta(code, filePath, projectRoot) {
|
|
225
|
-
if (!code.includes('import.meta')) return code;
|
|
226
|
-
const rel = path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
227
|
-
return code.replace(/import\.meta\.url/g, `(new URL('${rel}', document.baseURI).href)`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Rewrite templateUrl / styleUrl relative paths to project-relative paths.
|
|
232
|
-
* In a bundled IIFE, caller-base detection returns undefined (all stack
|
|
233
|
-
* frames point to the single bundle file), so relative URLs like
|
|
234
|
-
* 'contacts.html' never resolve to their original directory. By rewriting
|
|
235
|
-
* them to the same project-relative keys used in window.__zqInline, the
|
|
236
|
-
* runtime inline-resource lookup succeeds without a network fetch.
|
|
237
|
-
*/
|
|
238
|
-
function rewriteResourceUrls(code, filePath, projectRoot) {
|
|
239
|
-
const fileDir = path.dirname(filePath);
|
|
240
|
-
return code.replace(
|
|
241
|
-
/((?:templateUrl|styleUrl)\s*:\s*)(['"])([^'"]+)\2/g,
|
|
242
|
-
(match, prefix, quote, url) => {
|
|
243
|
-
if (url.startsWith('/') || url.includes('://')) return match;
|
|
244
|
-
const abs = path.resolve(fileDir, url);
|
|
245
|
-
// Only rewrite if the file actually exists - avoids mangling code examples
|
|
246
|
-
if (!fs.existsSync(abs)) return match;
|
|
247
|
-
const rel = path.relative(projectRoot, abs).replace(/\\/g, '/');
|
|
248
|
-
return `${prefix}${quote}${rel}${quote}`;
|
|
249
|
-
}
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Minify HTML for inlining - strips indentation and collapses whitespace
|
|
255
|
-
* between tags. Preserves content inside <pre>, <code>, and <textarea>
|
|
256
|
-
* blocks verbatim so syntax-highlighted code samples survive.
|
|
257
|
-
*/
|
|
258
|
-
function minifyHTML(html) {
|
|
259
|
-
const preserved = [];
|
|
260
|
-
// Protect <pre>…</pre> and <textarea>…</textarea> (multiline code blocks)
|
|
261
|
-
html = html.replace(/<(pre|textarea)(\b[^>]*)>[\s\S]*?<\/\1>/gi, m => {
|
|
262
|
-
preserved.push(m);
|
|
263
|
-
return `\x00P${preserved.length - 1}\x00`;
|
|
264
|
-
});
|
|
265
|
-
// Strip HTML comments
|
|
266
|
-
html = html.replace(/<!--[\s\S]*?-->/g, '');
|
|
267
|
-
// Collapse runs of whitespace (newlines + indentation) to a single space
|
|
268
|
-
html = html.replace(/\s{2,}/g, ' ');
|
|
269
|
-
// Remove space between tags: "> <" → "><"
|
|
270
|
-
// but preserve a space when an inline element is involved (a, span, strong, em, b, i, code, small, sub, sup, abbr, label)
|
|
271
|
-
html = html.replace(/>\s+</g, (m, offset) => {
|
|
272
|
-
const before = html.slice(Math.max(0, offset - 80), offset + 1);
|
|
273
|
-
const after = html.slice(offset + m.length - 1, offset + m.length + 40);
|
|
274
|
-
const inlineTags = /\b(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\b/i;
|
|
275
|
-
const closingInline = /<\/\s*(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\s*>$/i.test(before);
|
|
276
|
-
const openingInline = /^<(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)[\s>]/i.test(after);
|
|
277
|
-
return (closingInline || openingInline) ? '> <' : '><';
|
|
278
|
-
});
|
|
279
|
-
// Remove spaces inside opening tags: <tag attr = "val" > → <tag attr="val">
|
|
280
|
-
html = html.replace(/ *\/ *>/g, '/>');
|
|
281
|
-
html = html.replace(/ *= */g, '=');
|
|
282
|
-
// Trim
|
|
283
|
-
html = html.trim();
|
|
284
|
-
// Restore preserved blocks
|
|
285
|
-
html = html.replace(/\x00P(\d+)\x00/g, (_, i) => preserved[+i]);
|
|
286
|
-
return html;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Minify CSS for inlining - strips comments, collapses whitespace,
|
|
291
|
-
* removes unnecessary spaces around punctuation.
|
|
292
|
-
*/
|
|
293
|
-
function minifyCSS(css) {
|
|
294
|
-
// Strip block comments
|
|
295
|
-
css = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
296
|
-
// Collapse whitespace
|
|
297
|
-
css = css.replace(/\s{2,}/g, ' ');
|
|
298
|
-
// Remove spaces around { } ; , (but NOT : — pseudo-selectors like :not() need the preceding space)
|
|
299
|
-
css = css.replace(/\s*([{};,])\s*/g, '$1');
|
|
300
|
-
// Remove space after : (safe in both selectors and declarations)
|
|
301
|
-
css = css.replace(/:\s+/g, ':');
|
|
302
|
-
// Remove trailing semicolons before }
|
|
303
|
-
css = css.replace(/;}/g, '}');
|
|
304
|
-
return css.trim();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*/
|
|
312
|
-
function
|
|
313
|
-
let
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
continue;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
t = t.replace(/\s
|
|
486
|
-
|
|
487
|
-
t = t.replace(
|
|
488
|
-
segments[s].val = t;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
*
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
function
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
//
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
if (
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
);
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
})
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
.
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/bundle.js - app bundler command
|
|
3
|
+
*
|
|
4
|
+
* Walks the ES module import graph starting from an entry file,
|
|
5
|
+
* strips import/export syntax, concatenates everything into a single
|
|
6
|
+
* IIFE with content-hashed filenames, and rewrites index.html for
|
|
7
|
+
* both server and local (file://) deployment.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
const { args, flag, option } = require('../args');
|
|
17
|
+
const { minify, sizeKB } = require('../utils');
|
|
18
|
+
const buildLibrary = require('./build');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Module graph helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** Resolve an import specifier relative to the importing file. */
|
|
25
|
+
function resolveImport(specifier, fromFile) {
|
|
26
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) return null;
|
|
27
|
+
// Skip non-JS assets (CSS, images, etc.) referenced in code examples
|
|
28
|
+
if (/\.(?:css|json|svg|png|jpg|gif|woff2?|ttf|eot)$/i.test(specifier)) return null;
|
|
29
|
+
let resolved = path.resolve(path.dirname(fromFile), specifier);
|
|
30
|
+
if (!path.extname(resolved) && fs.existsSync(resolved + '.js')) {
|
|
31
|
+
resolved += '.js';
|
|
32
|
+
}
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Extract import specifiers from a source file. */
|
|
37
|
+
function extractImports(code) {
|
|
38
|
+
// Only scan the import preamble (before the first top-level `export`)
|
|
39
|
+
// so that code examples inside exported template strings are not
|
|
40
|
+
// mistaken for real imports.
|
|
41
|
+
const exportStart = code.search(/^export\b/m);
|
|
42
|
+
const preamble = exportStart > -1 ? code.slice(0, exportStart) : code;
|
|
43
|
+
|
|
44
|
+
const specifiers = [];
|
|
45
|
+
let m;
|
|
46
|
+
const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g;
|
|
47
|
+
while ((m = fromRe.exec(preamble)) !== null) specifiers.push(m[1]);
|
|
48
|
+
const sideRe = /^\s*import\s+['"]([^'"]+)['"]\s*;?\s*$/gm;
|
|
49
|
+
while ((m = sideRe.exec(preamble)) !== null) {
|
|
50
|
+
if (!specifiers.includes(m[1])) specifiers.push(m[1]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Also capture re-exports anywhere in the file:
|
|
54
|
+
// export { x } from '...' export * from '...'
|
|
55
|
+
const reExportRe = /^\s*export\s+(?:\{[^}]*\}|\*)\s*from\s+['"]([^'"]+)['"]/gm;
|
|
56
|
+
while ((m = reExportRe.exec(code)) !== null) {
|
|
57
|
+
if (!specifiers.includes(m[1])) specifiers.push(m[1]);
|
|
58
|
+
}
|
|
59
|
+
return specifiers;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Walk the import graph - topological sort (leaves first). */
|
|
63
|
+
function walkImportGraph(entry) {
|
|
64
|
+
const visited = new Set();
|
|
65
|
+
const order = [];
|
|
66
|
+
|
|
67
|
+
function visit(file) {
|
|
68
|
+
const abs = path.resolve(file);
|
|
69
|
+
if (visited.has(abs)) return;
|
|
70
|
+
visited.add(abs);
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(abs)) {
|
|
73
|
+
console.warn(` ⚠ Missing file: ${abs}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const code = fs.readFileSync(abs, 'utf-8');
|
|
78
|
+
const imports = extractImports(code);
|
|
79
|
+
for (const spec of imports) {
|
|
80
|
+
const resolved = resolveImport(spec, abs);
|
|
81
|
+
if (resolved) visit(resolved);
|
|
82
|
+
}
|
|
83
|
+
order.push(abs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
visit(entry);
|
|
87
|
+
return order;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Strip ES module import/export syntax, keeping declarations.
|
|
92
|
+
* Exported const/let are converted to var so they hoist past the per-module
|
|
93
|
+
* block scope and remain accessible to downstream modules.
|
|
94
|
+
* Exported function/class are converted to var assignments for the same reason.
|
|
95
|
+
* Non-exported declarations stay as-is and remain block-scoped (private).
|
|
96
|
+
*
|
|
97
|
+
* Template literals are temporarily hidden via a character-level scanner
|
|
98
|
+
* (handles nested backtick expressions) so that code examples inside
|
|
99
|
+
* backtick strings aren't accidentally rewritten.
|
|
100
|
+
*/
|
|
101
|
+
function stripModuleSyntax(code) {
|
|
102
|
+
// -- Hide template literals (supports nesting) -------------------------
|
|
103
|
+
const templates = [];
|
|
104
|
+
|
|
105
|
+
function scanTemplateLiteral(str, start) {
|
|
106
|
+
let i = start + 1; // skip opening backtick
|
|
107
|
+
while (i < str.length) {
|
|
108
|
+
const ch = str[i];
|
|
109
|
+
if (ch === '\\') { i += 2; continue; }
|
|
110
|
+
if (ch === '`') { return i + 1; } // end of template
|
|
111
|
+
if (ch === '$' && str[i + 1] === '{') {
|
|
112
|
+
i += 2; // skip ${
|
|
113
|
+
let depth = 1;
|
|
114
|
+
while (i < str.length && depth > 0) {
|
|
115
|
+
const c = str[i];
|
|
116
|
+
if (c === '{') { depth++; i++; }
|
|
117
|
+
else if (c === '}') { depth--; i++; }
|
|
118
|
+
else if (c === '`') { i = scanTemplateLiteral(str, i); }
|
|
119
|
+
else if (c === "'" || c === '"') { i = skipString(str, i); }
|
|
120
|
+
else if (c === '/' && str[i + 1] === '/') { while (i < str.length && str[i] !== '\n') i++; }
|
|
121
|
+
else if (c === '/' && str[i + 1] === '*') { i += 2; while (i < str.length - 1 && !(str[i] === '*' && str[i + 1] === '/')) i++; i += 2; }
|
|
122
|
+
else { i++; }
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
return i;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function skipString(str, start) {
|
|
132
|
+
const q = str[start];
|
|
133
|
+
let i = start + 1;
|
|
134
|
+
while (i < str.length) {
|
|
135
|
+
if (str[i] === '\\') { i += 2; continue; }
|
|
136
|
+
if (str[i] === q) { return i + 1; }
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
return i;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let hidden = '';
|
|
143
|
+
let pos = 0;
|
|
144
|
+
while (pos < code.length) {
|
|
145
|
+
const ch = code[pos];
|
|
146
|
+
if (ch === "'" || ch === '"') {
|
|
147
|
+
const end = skipString(code, pos);
|
|
148
|
+
hidden += code.substring(pos, end);
|
|
149
|
+
pos = end;
|
|
150
|
+
} else if (ch === '/' && code[pos + 1] === '/') {
|
|
151
|
+
let end = pos;
|
|
152
|
+
while (end < code.length && code[end] !== '\n') end++;
|
|
153
|
+
hidden += code.substring(pos, end);
|
|
154
|
+
pos = end;
|
|
155
|
+
} else if (ch === '/' && code[pos + 1] === '*') {
|
|
156
|
+
let end = pos + 2;
|
|
157
|
+
while (end < code.length - 1 && !(code[end] === '*' && code[end + 1] === '/')) end++;
|
|
158
|
+
end += 2;
|
|
159
|
+
hidden += code.substring(pos, end);
|
|
160
|
+
pos = end;
|
|
161
|
+
} else if (ch === '`') {
|
|
162
|
+
const end = scanTemplateLiteral(code, pos);
|
|
163
|
+
templates.push(code.substring(pos, end));
|
|
164
|
+
hidden += `__TPL_${templates.length - 1}__`;
|
|
165
|
+
pos = end;
|
|
166
|
+
} else {
|
|
167
|
+
hidden += ch;
|
|
168
|
+
pos++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// -- Apply import / export transforms -----------------------------------
|
|
173
|
+
code = hidden;
|
|
174
|
+
code = code.replace(/^\s*import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
175
|
+
code = code.replace(/^\s*import\s+['"].*?['"];?\s*$/gm, '');
|
|
176
|
+
code = code.replace(/^(\s*)export\s+default\s+/gm, '$1');
|
|
177
|
+
// Convert exported const/let/var to var (hoists past block scope)
|
|
178
|
+
code = code.replace(/^(\s*)export\s+(const|let|var)\s/gm, '$1var ');
|
|
179
|
+
// Convert exported function/async function to var assignment (hoists past block scope)
|
|
180
|
+
code = code.replace(/^(\s*)export\s+async\s+function\s+(\w+)/gm, '$1var $2 = async function $2');
|
|
181
|
+
code = code.replace(/^(\s*)export\s+function\s+(\w+)/gm, '$1var $2 = function $2');
|
|
182
|
+
// Convert exported class to var assignment
|
|
183
|
+
code = code.replace(/^(\s*)export\s+class\s+(\w+)/gm, '$1var $2 = class $2');
|
|
184
|
+
// Collect names from bare export blocks: export { a, b, c };
|
|
185
|
+
// These names are declared elsewhere (function/const/let) and need to be
|
|
186
|
+
// hoisted past the per-module block scope. We collect them here and
|
|
187
|
+
// the caller converts their declarations to var-based forms.
|
|
188
|
+
const bareExportNames = [];
|
|
189
|
+
code = code.replace(/^\s*export\s*\{([^}]+)\};?\s*$/gm, (_, names) => {
|
|
190
|
+
for (const n of names.split(',')) {
|
|
191
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
192
|
+
if (parts[0]) bareExportNames.push({ local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() });
|
|
193
|
+
}
|
|
194
|
+
return '';
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// For every bare-exported name, convert its block-scoped declaration to a
|
|
198
|
+
// var-based form so it hoists past the per-module { } wrapper:
|
|
199
|
+
// function foo(…) { … } → var foo = function foo(…) { … }
|
|
200
|
+
// async function foo(…) → var foo = async function foo(…)
|
|
201
|
+
// const/let foo = … → var foo = …
|
|
202
|
+
for (const { local } of bareExportNames) {
|
|
203
|
+
const fnRe = new RegExp(`^(\\s*)function\\s+${local}\\s*\\(`, 'gm');
|
|
204
|
+
code = code.replace(fnRe, `$1var ${local} = function ${local}(`);
|
|
205
|
+
const asyncFnRe = new RegExp(`^(\\s*)async\\s+function\\s+${local}\\s*\\(`, 'gm');
|
|
206
|
+
code = code.replace(asyncFnRe, `$1var ${local} = async function ${local}(`);
|
|
207
|
+
const constLetRe = new RegExp(`^(\\s*)(const|let)\\s+${local}\\b`, 'gm');
|
|
208
|
+
code = code.replace(constLetRe, `$1var ${local}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create aliases for "export { local as exported }" where the names differ
|
|
212
|
+
for (const { local, exported } of bareExportNames) {
|
|
213
|
+
if (exported !== local) {
|
|
214
|
+
code += `\nvar ${exported} = ${local};`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// -- Restore template literals ------------------------------------------
|
|
219
|
+
code = code.replace(/__TPL_(\d+)__/g, (_, i) => templates[i]);
|
|
220
|
+
return { code, bareExportNames };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Replace import.meta.url with a runtime equivalent. */
|
|
224
|
+
function replaceImportMeta(code, filePath, projectRoot) {
|
|
225
|
+
if (!code.includes('import.meta')) return code;
|
|
226
|
+
const rel = path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
227
|
+
return code.replace(/import\.meta\.url/g, `(new URL('${rel}', document.baseURI).href)`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Rewrite templateUrl / styleUrl relative paths to project-relative paths.
|
|
232
|
+
* In a bundled IIFE, caller-base detection returns undefined (all stack
|
|
233
|
+
* frames point to the single bundle file), so relative URLs like
|
|
234
|
+
* 'contacts.html' never resolve to their original directory. By rewriting
|
|
235
|
+
* them to the same project-relative keys used in window.__zqInline, the
|
|
236
|
+
* runtime inline-resource lookup succeeds without a network fetch.
|
|
237
|
+
*/
|
|
238
|
+
function rewriteResourceUrls(code, filePath, projectRoot) {
|
|
239
|
+
const fileDir = path.dirname(filePath);
|
|
240
|
+
return code.replace(
|
|
241
|
+
/((?:templateUrl|styleUrl)\s*:\s*)(['"])([^'"]+)\2/g,
|
|
242
|
+
(match, prefix, quote, url) => {
|
|
243
|
+
if (url.startsWith('/') || url.includes('://')) return match;
|
|
244
|
+
const abs = path.resolve(fileDir, url);
|
|
245
|
+
// Only rewrite if the file actually exists - avoids mangling code examples
|
|
246
|
+
if (!fs.existsSync(abs)) return match;
|
|
247
|
+
const rel = path.relative(projectRoot, abs).replace(/\\/g, '/');
|
|
248
|
+
return `${prefix}${quote}${rel}${quote}`;
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Minify HTML for inlining - strips indentation and collapses whitespace
|
|
255
|
+
* between tags. Preserves content inside <pre>, <code>, and <textarea>
|
|
256
|
+
* blocks verbatim so syntax-highlighted code samples survive.
|
|
257
|
+
*/
|
|
258
|
+
function minifyHTML(html) {
|
|
259
|
+
const preserved = [];
|
|
260
|
+
// Protect <pre>…</pre> and <textarea>…</textarea> (multiline code blocks)
|
|
261
|
+
html = html.replace(/<(pre|textarea)(\b[^>]*)>[\s\S]*?<\/\1>/gi, m => {
|
|
262
|
+
preserved.push(m);
|
|
263
|
+
return `\x00P${preserved.length - 1}\x00`;
|
|
264
|
+
});
|
|
265
|
+
// Strip HTML comments
|
|
266
|
+
html = html.replace(/<!--[\s\S]*?-->/g, '');
|
|
267
|
+
// Collapse runs of whitespace (newlines + indentation) to a single space
|
|
268
|
+
html = html.replace(/\s{2,}/g, ' ');
|
|
269
|
+
// Remove space between tags: "> <" → "><"
|
|
270
|
+
// but preserve a space when an inline element is involved (a, span, strong, em, b, i, code, small, sub, sup, abbr, label)
|
|
271
|
+
html = html.replace(/>\s+</g, (m, offset) => {
|
|
272
|
+
const before = html.slice(Math.max(0, offset - 80), offset + 1);
|
|
273
|
+
const after = html.slice(offset + m.length - 1, offset + m.length + 40);
|
|
274
|
+
const inlineTags = /\b(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\b/i;
|
|
275
|
+
const closingInline = /<\/\s*(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\s*>$/i.test(before);
|
|
276
|
+
const openingInline = /^<(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)[\s>]/i.test(after);
|
|
277
|
+
return (closingInline || openingInline) ? '> <' : '><';
|
|
278
|
+
});
|
|
279
|
+
// Remove spaces inside opening tags: <tag attr = "val" > → <tag attr="val">
|
|
280
|
+
html = html.replace(/ *\/ *>/g, '/>');
|
|
281
|
+
html = html.replace(/ *= */g, '=');
|
|
282
|
+
// Trim
|
|
283
|
+
html = html.trim();
|
|
284
|
+
// Restore preserved blocks
|
|
285
|
+
html = html.replace(/\x00P(\d+)\x00/g, (_, i) => preserved[+i]);
|
|
286
|
+
return html;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Minify CSS for inlining - strips comments, collapses whitespace,
|
|
291
|
+
* removes unnecessary spaces around punctuation.
|
|
292
|
+
*/
|
|
293
|
+
function minifyCSS(css) {
|
|
294
|
+
// Strip block comments
|
|
295
|
+
css = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
296
|
+
// Collapse whitespace
|
|
297
|
+
css = css.replace(/\s{2,}/g, ' ');
|
|
298
|
+
// Remove spaces around { } ; , (but NOT : — pseudo-selectors like :not() need the preceding space)
|
|
299
|
+
css = css.replace(/\s*([{};,])\s*/g, '$1');
|
|
300
|
+
// Remove space after : (safe in both selectors and declarations)
|
|
301
|
+
css = css.replace(/:\s+/g, ':');
|
|
302
|
+
// Remove trailing semicolons before }
|
|
303
|
+
css = css.replace(/;}/g, '}');
|
|
304
|
+
return css.trim();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Heuristic: is the next '/' the start of a regex literal (vs division)?
|
|
309
|
+
* Used by minifyTemplateLiterals to avoid misinterpreting backticks
|
|
310
|
+
* inside regex patterns as template literal delimiters.
|
|
311
|
+
*/
|
|
312
|
+
function _isRegexCtxTpl(out) {
|
|
313
|
+
let end = out.length - 1;
|
|
314
|
+
while (end >= 0 && (out[end] === ' ' || out[end] === '\t' || out[end] === '\n' || out[end] === '\r')) end--;
|
|
315
|
+
if (end < 0) return true;
|
|
316
|
+
const last = out[end];
|
|
317
|
+
if ('=({[,;:!&|?~+-*/%^>'.includes(last)) return true;
|
|
318
|
+
const tail = out.substring(Math.max(0, end - 7), end + 1);
|
|
319
|
+
for (const kw of ['return', 'typeof', 'case', 'in', 'delete', 'void', 'throw', 'new']) {
|
|
320
|
+
if (tail.endsWith(kw)) {
|
|
321
|
+
const pos = end - kw.length;
|
|
322
|
+
if (pos < 0 || !/[a-zA-Z0-9_$]/.test(out[pos])) return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Walk JS source and minify the HTML/CSS inside template literals.
|
|
330
|
+
* Handles ${…} interpolations (with nesting) and preserves <pre> blocks.
|
|
331
|
+
* Only trims whitespace sequences that appear between/around HTML tags.
|
|
332
|
+
*/
|
|
333
|
+
function minifyTemplateLiterals(code) {
|
|
334
|
+
let out = '';
|
|
335
|
+
let i = 0;
|
|
336
|
+
while (i < code.length) {
|
|
337
|
+
const ch = code[i];
|
|
338
|
+
|
|
339
|
+
// Regular string: copy verbatim
|
|
340
|
+
if (ch === '"' || ch === "'") {
|
|
341
|
+
const q = ch; out += ch; i++;
|
|
342
|
+
while (i < code.length) {
|
|
343
|
+
if (code[i] === '\\') { out += code[i] + (code[i + 1] || ''); i += 2; continue; }
|
|
344
|
+
out += code[i];
|
|
345
|
+
if (code[i] === q) { i++; break; }
|
|
346
|
+
i++;
|
|
347
|
+
}
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Line comment: copy verbatim
|
|
352
|
+
if (ch === '/' && code[i + 1] === '/') {
|
|
353
|
+
while (i < code.length && code[i] !== '\n') out += code[i++];
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// Block comment: copy verbatim
|
|
357
|
+
if (ch === '/' && code[i + 1] === '*') {
|
|
358
|
+
out += '/*'; i += 2;
|
|
359
|
+
while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) out += code[i++];
|
|
360
|
+
out += '*/'; i += 2;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Regex literal: copy verbatim (prevents backticks inside regex from
|
|
365
|
+
// being mistaken for template literals, e.g. /`([^`]+)`/g)
|
|
366
|
+
if (ch === '/' && _isRegexCtxTpl(out)) {
|
|
367
|
+
out += ch; i++;
|
|
368
|
+
let inCharClass = false;
|
|
369
|
+
while (i < code.length) {
|
|
370
|
+
const rc = code[i];
|
|
371
|
+
if (rc === '\\') { out += rc + (code[i + 1] || ''); i += 2; continue; }
|
|
372
|
+
if (rc === '[') inCharClass = true;
|
|
373
|
+
if (rc === ']') inCharClass = false;
|
|
374
|
+
out += rc; i++;
|
|
375
|
+
if (rc === '/' && !inCharClass) {
|
|
376
|
+
while (i < code.length && /[gimsuy]/.test(code[i])) { out += code[i]; i++; }
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Template literal: extract, minify HTML, and emit
|
|
384
|
+
if (ch === '`') {
|
|
385
|
+
out += _minifyTemplate(code, i);
|
|
386
|
+
// Advance past the template
|
|
387
|
+
i = _skipTemplateLiteral(code, i);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
out += ch; i++;
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Extract a full template literal (handling nested ${…}) and return it minified. */
|
|
397
|
+
function _minifyTemplate(code, start) {
|
|
398
|
+
const end = _skipTemplateLiteral(code, start);
|
|
399
|
+
const raw = code.substring(start, end);
|
|
400
|
+
// Only minify templates that contain HTML tags or CSS rules
|
|
401
|
+
if (/<\w/.test(raw)) return _collapseTemplateWS(raw);
|
|
402
|
+
if (/[{};]\s/.test(raw) && /[.#\w-]+\s*\{/.test(raw)) return _collapseTemplateCSS(raw);
|
|
403
|
+
return raw;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Return index past the closing backtick of a template starting at `start`. */
|
|
407
|
+
function _skipTemplateLiteral(code, start) {
|
|
408
|
+
let i = start + 1; // skip opening backtick
|
|
409
|
+
let depth = 0;
|
|
410
|
+
while (i < code.length) {
|
|
411
|
+
if (code[i] === '\\') { i += 2; continue; }
|
|
412
|
+
if (code[i] === '$' && code[i + 1] === '{') { depth++; i += 2; continue; }
|
|
413
|
+
if (depth > 0) {
|
|
414
|
+
if (code[i] === '{') { depth++; i++; continue; }
|
|
415
|
+
if (code[i] === '}') { depth--; i++; continue; }
|
|
416
|
+
if (code[i] === '`') { i = _skipTemplateLiteral(code, i); continue; }
|
|
417
|
+
if (code[i] === '"' || code[i] === "'") {
|
|
418
|
+
const q = code[i]; i++;
|
|
419
|
+
while (i < code.length) {
|
|
420
|
+
if (code[i] === '\\') { i += 2; continue; }
|
|
421
|
+
if (code[i] === q) { i++; break; }
|
|
422
|
+
i++;
|
|
423
|
+
}
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
i++; continue;
|
|
427
|
+
}
|
|
428
|
+
if (code[i] === '`') { i++; return i; } // closing backtick
|
|
429
|
+
i++;
|
|
430
|
+
}
|
|
431
|
+
return i;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Collapse whitespace in the text portions of an HTML template literal,
|
|
436
|
+
* preserving ${…} expressions, <pre> blocks, and inline text spacing.
|
|
437
|
+
*/
|
|
438
|
+
function _collapseTemplateWS(tpl) {
|
|
439
|
+
// Build array of segments: text portions vs ${…} expressions
|
|
440
|
+
const segments = [];
|
|
441
|
+
let i = 1; // skip opening backtick
|
|
442
|
+
let text = '';
|
|
443
|
+
while (i < tpl.length - 1) { // stop before closing backtick
|
|
444
|
+
if (tpl[i] === '\\') { text += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
445
|
+
if (tpl[i] === '$' && tpl[i + 1] === '{') {
|
|
446
|
+
if (text) { segments.push({ type: 'text', val: text }); text = ''; }
|
|
447
|
+
// Collect the full expression
|
|
448
|
+
let depth = 1; let expr = '${'; i += 2;
|
|
449
|
+
while (i < tpl.length - 1 && depth > 0) {
|
|
450
|
+
if (tpl[i] === '\\') { expr += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
451
|
+
if (tpl[i] === '{') depth++;
|
|
452
|
+
if (tpl[i] === '}') depth--;
|
|
453
|
+
if (depth > 0) { expr += tpl[i]; i++; } else { expr += '}'; i++; }
|
|
454
|
+
}
|
|
455
|
+
segments.push({ type: 'expr', val: expr });
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
text += tpl[i]; i++;
|
|
459
|
+
}
|
|
460
|
+
if (text) segments.push({ type: 'text', val: text });
|
|
461
|
+
|
|
462
|
+
// Minify text segments (collapse whitespace between/around tags)
|
|
463
|
+
for (let s = 0; s < segments.length; s++) {
|
|
464
|
+
if (segments[s].type !== 'text') continue;
|
|
465
|
+
let t = segments[s].val;
|
|
466
|
+
// Protect <pre>…</pre> regions
|
|
467
|
+
const preserved = [];
|
|
468
|
+
t = t.replace(/<pre(\b[^>]*)>[\s\S]*?<\/pre>/gi, m => {
|
|
469
|
+
preserved.push(m);
|
|
470
|
+
return `\x00P${preserved.length - 1}\x00`;
|
|
471
|
+
});
|
|
472
|
+
// Collapse whitespace runs that touch a < or > but preserve a space
|
|
473
|
+
// when an inline element boundary is involved
|
|
474
|
+
t = t.replace(/>\s{2,}/g, (m, offset) => {
|
|
475
|
+
const before = t.slice(Math.max(0, offset - 80), offset + 1);
|
|
476
|
+
if (/<\/\s*(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\s*>$/i.test(before)) return '> ';
|
|
477
|
+
return '>';
|
|
478
|
+
});
|
|
479
|
+
t = t.replace(/\s{2,}</g, (m, offset) => {
|
|
480
|
+
const after = t.slice(offset + m.length - 1, offset + m.length + 40);
|
|
481
|
+
if (/^<(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)[\s>]/i.test(after)) return ' <';
|
|
482
|
+
return '<';
|
|
483
|
+
});
|
|
484
|
+
// Collapse other multi-whitespace runs to a single space
|
|
485
|
+
t = t.replace(/\s{2,}/g, ' ');
|
|
486
|
+
// Restore <pre> blocks
|
|
487
|
+
t = t.replace(/\x00P(\d+)\x00/g, (_, idx) => preserved[+idx]);
|
|
488
|
+
segments[s].val = t;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return '`' + segments.map(s => s.val).join('') + '`';
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Collapse CSS whitespace inside a template literal (styles: `…`).
|
|
496
|
+
* Uses the same segment-splitting approach as _collapseTemplateWS so
|
|
497
|
+
* ${…} expressions are preserved untouched.
|
|
498
|
+
*/
|
|
499
|
+
function _collapseTemplateCSS(tpl) {
|
|
500
|
+
const segments = [];
|
|
501
|
+
let i = 1; let text = '';
|
|
502
|
+
while (i < tpl.length - 1) {
|
|
503
|
+
if (tpl[i] === '\\') { text += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
504
|
+
if (tpl[i] === '$' && tpl[i + 1] === '{') {
|
|
505
|
+
if (text) { segments.push({ type: 'text', val: text }); text = ''; }
|
|
506
|
+
let depth = 1; let expr = '${'; i += 2;
|
|
507
|
+
while (i < tpl.length - 1 && depth > 0) {
|
|
508
|
+
if (tpl[i] === '\\') { expr += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
509
|
+
if (tpl[i] === '{') depth++;
|
|
510
|
+
if (tpl[i] === '}') depth--;
|
|
511
|
+
if (depth > 0) { expr += tpl[i]; i++; } else { expr += '}'; i++; }
|
|
512
|
+
}
|
|
513
|
+
segments.push({ type: 'expr', val: expr });
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
text += tpl[i]; i++;
|
|
517
|
+
}
|
|
518
|
+
if (text) segments.push({ type: 'text', val: text });
|
|
519
|
+
|
|
520
|
+
for (let s = 0; s < segments.length; s++) {
|
|
521
|
+
if (segments[s].type !== 'text') continue;
|
|
522
|
+
let t = segments[s].val;
|
|
523
|
+
t = t.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
524
|
+
t = t.replace(/\s{2,}/g, ' ');
|
|
525
|
+
t = t.replace(/\s*([{};,])\s*/g, '$1');
|
|
526
|
+
t = t.replace(/:\s+/g, ':');
|
|
527
|
+
t = t.replace(/;}/g, '}');
|
|
528
|
+
segments[s].val = t;
|
|
529
|
+
}
|
|
530
|
+
return '`' + segments.map(s => s.val).join('') + '`';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Scan bundled source files for external resource references
|
|
535
|
+
* (templateUrl, styleUrl) and return a map of
|
|
536
|
+
* { relativePath: fileContent } for inlining.
|
|
537
|
+
*/
|
|
538
|
+
function collectInlineResources(files, projectRoot) {
|
|
539
|
+
const inlineMap = {};
|
|
540
|
+
|
|
541
|
+
for (const file of files) {
|
|
542
|
+
const code = fs.readFileSync(file, 'utf-8');
|
|
543
|
+
const fileDir = path.dirname(file);
|
|
544
|
+
|
|
545
|
+
// styleUrl:
|
|
546
|
+
const styleUrlRe = /styleUrl\s*:\s*['"]([^'"]+)['"]/g;
|
|
547
|
+
let styleMatch;
|
|
548
|
+
while ((styleMatch = styleUrlRe.exec(code)) !== null) {
|
|
549
|
+
const stylePath = path.join(fileDir, styleMatch[1]);
|
|
550
|
+
if (fs.existsSync(stylePath)) {
|
|
551
|
+
const relKey = path.relative(projectRoot, stylePath).replace(/\\/g, '/');
|
|
552
|
+
inlineMap[relKey] = fs.readFileSync(stylePath, 'utf-8');
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// templateUrl:
|
|
557
|
+
const tmplMatch = code.match(/templateUrl\s*:\s*['"]([^'"]+)['"]/);
|
|
558
|
+
if (tmplMatch) {
|
|
559
|
+
const tmplPath = path.join(fileDir, tmplMatch[1]);
|
|
560
|
+
if (fs.existsSync(tmplPath)) {
|
|
561
|
+
const relKey = path.relative(projectRoot, tmplPath).replace(/\\/g, '/');
|
|
562
|
+
inlineMap[relKey] = fs.readFileSync(tmplPath, 'utf-8');
|
|
563
|
+
}
|
|
564
|
+
} else if (/templateUrl\s*:/.test(code)) {
|
|
565
|
+
// Dynamic templateUrl (e.g. Object.fromEntries, computed map) -
|
|
566
|
+
// inline all .html files in the component's directory tree so
|
|
567
|
+
// the runtime __zqInline lookup can resolve them by suffix.
|
|
568
|
+
(function scanHtml(dir) {
|
|
569
|
+
try {
|
|
570
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
571
|
+
const full = path.join(dir, entry.name);
|
|
572
|
+
if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
573
|
+
const relKey = path.relative(projectRoot, full).replace(/\\/g, '/');
|
|
574
|
+
if (!inlineMap[relKey]) {
|
|
575
|
+
inlineMap[relKey] = fs.readFileSync(full, 'utf-8');
|
|
576
|
+
}
|
|
577
|
+
} else if (entry.isDirectory()) {
|
|
578
|
+
scanHtml(full);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} catch { /* permission error - skip */ }
|
|
582
|
+
})(fileDir);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Minify inlined resources by type
|
|
587
|
+
for (const key of Object.keys(inlineMap)) {
|
|
588
|
+
if (key.endsWith('.html')) inlineMap[key] = minifyHTML(inlineMap[key]);
|
|
589
|
+
else if (key.endsWith('.css')) inlineMap[key] = minifyCSS(inlineMap[key]);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return inlineMap;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Auto-detect the app entry point.
|
|
597
|
+
*
|
|
598
|
+
* Strategy - ordered by precedence (first match wins):
|
|
599
|
+
* 1. HTML discovery: index.html first, then other .html files
|
|
600
|
+
* (root level + one directory deep).
|
|
601
|
+
* 2. Within each HTML file, prefer a module <script> whose src
|
|
602
|
+
* resolves to app.js, then fall back to the first module
|
|
603
|
+
* <script> tag regardless of name.
|
|
604
|
+
* 3. JS file scan: look for $.router first (entry point by design),
|
|
605
|
+
* then $.mount / $.store / mountAll.
|
|
606
|
+
* 4. Convention fallback paths.
|
|
607
|
+
*/
|
|
608
|
+
function detectEntry(projectRoot) {
|
|
609
|
+
// Matches all <script type="module" src="…"> tags (global)
|
|
610
|
+
const moduleScriptReG = /<script[^>]+type\s*=\s*["']module["'][^>]+src\s*=\s*["']([^"']+)["']/g;
|
|
611
|
+
|
|
612
|
+
// Collect HTML files: root level + one directory deep
|
|
613
|
+
const htmlFiles = [];
|
|
614
|
+
for (const entry of fs.readdirSync(projectRoot, { withFileTypes: true })) {
|
|
615
|
+
if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
616
|
+
htmlFiles.push(path.join(projectRoot, entry.name));
|
|
617
|
+
} else if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'dist') {
|
|
618
|
+
const sub = path.join(projectRoot, entry.name);
|
|
619
|
+
try {
|
|
620
|
+
for (const child of fs.readdirSync(sub, { withFileTypes: true })) {
|
|
621
|
+
if (child.isFile() && child.name.endsWith('.html')) {
|
|
622
|
+
htmlFiles.push(path.join(sub, child.name));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
} catch { /* permission error - skip */ }
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Sort: index.html first (at any depth), then alphabetical
|
|
630
|
+
htmlFiles.sort((a, b) => {
|
|
631
|
+
const aIsIndex = path.basename(a) === 'index.html' ? 0 : 1;
|
|
632
|
+
const bIsIndex = path.basename(b) === 'index.html' ? 0 : 1;
|
|
633
|
+
if (aIsIndex !== bIsIndex) return aIsIndex - bIsIndex;
|
|
634
|
+
return a.localeCompare(b);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// 1. Parse module <script> tags from HTML files (index.html evaluated first).
|
|
638
|
+
// Within each file prefer a src ending in app.js, else the first module tag.
|
|
639
|
+
for (const htmlPath of htmlFiles) {
|
|
640
|
+
const html = fs.readFileSync(htmlPath, 'utf-8');
|
|
641
|
+
const htmlDir = path.dirname(htmlPath);
|
|
642
|
+
|
|
643
|
+
let appJsEntry = null; // src that resolves to app.js
|
|
644
|
+
let firstEntry = null; // first module script src
|
|
645
|
+
let m;
|
|
646
|
+
moduleScriptReG.lastIndex = 0;
|
|
647
|
+
while ((m = moduleScriptReG.exec(html)) !== null) {
|
|
648
|
+
const resolved = path.resolve(htmlDir, m[1]);
|
|
649
|
+
if (!fs.existsSync(resolved)) continue;
|
|
650
|
+
if (!firstEntry) firstEntry = resolved;
|
|
651
|
+
if (path.basename(resolved) === 'app.js') { appJsEntry = resolved; break; }
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (appJsEntry) return appJsEntry;
|
|
655
|
+
if (firstEntry) return firstEntry;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// 2. Search JS files for entry-point patterns.
|
|
659
|
+
// Pass 1 - $.router (the canonical entry point).
|
|
660
|
+
// Pass 2 - $.mount, $.store, mountAll (component-level, lower confidence).
|
|
661
|
+
const routerRe = /\$\.router\s*\(/;
|
|
662
|
+
const otherRe = /\$\.(mount|store)\s*\(|mountAll\s*\(/;
|
|
663
|
+
|
|
664
|
+
function collectJS(dir, depth = 0) {
|
|
665
|
+
const results = [];
|
|
666
|
+
if (depth > 2) return results;
|
|
667
|
+
try {
|
|
668
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
669
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist') continue;
|
|
670
|
+
const full = path.join(dir, entry.name);
|
|
671
|
+
if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
672
|
+
results.push(full);
|
|
673
|
+
} else if (entry.isDirectory()) {
|
|
674
|
+
results.push(...collectJS(full, depth + 1));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
} catch { /* skip */ }
|
|
678
|
+
return results;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const jsFiles = collectJS(projectRoot);
|
|
682
|
+
|
|
683
|
+
// Pass 1: $.router
|
|
684
|
+
for (const file of jsFiles) {
|
|
685
|
+
const code = fs.readFileSync(file, 'utf-8');
|
|
686
|
+
if (routerRe.test(code)) return file;
|
|
687
|
+
}
|
|
688
|
+
// Pass 2: other entry-point signals
|
|
689
|
+
for (const file of jsFiles) {
|
|
690
|
+
const code = fs.readFileSync(file, 'utf-8');
|
|
691
|
+
if (otherRe.test(code)) return file;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// 3. Convention fallbacks
|
|
695
|
+
const fallbacks = ['app/app.js', 'scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
|
|
696
|
+
for (const f of fallbacks) {
|
|
697
|
+
const fp = path.join(projectRoot, f);
|
|
698
|
+
if (fs.existsSync(fp)) return fp;
|
|
699
|
+
}
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
// HTML rewriting
|
|
705
|
+
// ---------------------------------------------------------------------------
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Rewrite an HTML file to replace the module <script> with the bundle.
|
|
709
|
+
* Produces two variants:
|
|
710
|
+
* server/index.html - <base href="/"> for SPA deep routes
|
|
711
|
+
* local/index.html - relative paths for file:// access
|
|
712
|
+
*/
|
|
713
|
+
function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir, globalCssOrigHref, globalCssHash) {
|
|
714
|
+
const htmlPath = path.resolve(projectRoot, htmlRelPath);
|
|
715
|
+
if (!fs.existsSync(htmlPath)) {
|
|
716
|
+
console.warn(` ⚠ HTML file not found: ${htmlRelPath}`);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const htmlDir = path.dirname(htmlPath);
|
|
721
|
+
let html = fs.readFileSync(htmlPath, 'utf-8');
|
|
722
|
+
|
|
723
|
+
// Collect asset references from HTML
|
|
724
|
+
const assetRe = /(?:<(?:link|script|img)[^>]*?\s(?:src|href)\s*=\s*["'])([^"']+)["']/gi;
|
|
725
|
+
const assets = new Set();
|
|
726
|
+
let m;
|
|
727
|
+
while ((m = assetRe.exec(html)) !== null) {
|
|
728
|
+
const ref = m[1];
|
|
729
|
+
if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:') || ref.startsWith('#')) continue;
|
|
730
|
+
const refAbs = path.resolve(htmlDir, ref);
|
|
731
|
+
if (bundledFiles && bundledFiles.has(refAbs)) continue;
|
|
732
|
+
if (includeLib && /zquery(?:\.min)?\.js$/i.test(ref)) continue;
|
|
733
|
+
assets.add(ref);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Also scan the bundled JS for src/href references to local assets
|
|
737
|
+
const bundleContent = fs.existsSync(bundleFile) ? fs.readFileSync(bundleFile, 'utf-8') : '';
|
|
738
|
+
const jsAssetRe = /\b(?:src|href)\s*=\s*["\\]*["']([^"']+\.(?:svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|eot|mp4|webm))["']/gi;
|
|
739
|
+
while ((m = jsAssetRe.exec(bundleContent)) !== null) {
|
|
740
|
+
const ref = m[1];
|
|
741
|
+
if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:')) continue;
|
|
742
|
+
if (!assets.has(ref)) {
|
|
743
|
+
const refAbs = path.resolve(htmlDir, ref);
|
|
744
|
+
if (fs.existsSync(refAbs)) assets.add(ref);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// For any referenced asset directories, copy all sibling files too
|
|
749
|
+
const assetDirs = new Set();
|
|
750
|
+
for (const asset of assets) {
|
|
751
|
+
const dir = path.dirname(asset);
|
|
752
|
+
if (dir && dir !== '.') assetDirs.add(dir);
|
|
753
|
+
}
|
|
754
|
+
for (const dir of assetDirs) {
|
|
755
|
+
const absDirPath = path.resolve(htmlDir, dir);
|
|
756
|
+
if (fs.existsSync(absDirPath) && fs.statSync(absDirPath).isDirectory()) {
|
|
757
|
+
for (const child of fs.readdirSync(absDirPath)) {
|
|
758
|
+
const childRel = path.join(dir, child).replace(/\\/g, '/');
|
|
759
|
+
const childAbs = path.join(absDirPath, child);
|
|
760
|
+
if (fs.statSync(childAbs).isFile() && !assets.has(childRel)) {
|
|
761
|
+
assets.add(childRel);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Copy assets into both dist dirs
|
|
768
|
+
let copiedCount = 0;
|
|
769
|
+
for (const asset of assets) {
|
|
770
|
+
const srcFile = path.resolve(htmlDir, asset);
|
|
771
|
+
if (!fs.existsSync(srcFile)) continue;
|
|
772
|
+
for (const distDir of [serverDir, localDir]) {
|
|
773
|
+
const destFile = path.join(distDir, asset);
|
|
774
|
+
const destDir = path.dirname(destFile);
|
|
775
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
776
|
+
fs.copyFileSync(srcFile, destFile);
|
|
777
|
+
}
|
|
778
|
+
copiedCount++;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Copy CSS-referenced assets (fonts, images in url())
|
|
782
|
+
for (const asset of assets) {
|
|
783
|
+
const srcFile = path.resolve(htmlDir, asset);
|
|
784
|
+
if (!fs.existsSync(srcFile) || !asset.endsWith('.css')) continue;
|
|
785
|
+
const cssContent = fs.readFileSync(srcFile, 'utf-8');
|
|
786
|
+
const urlRe = /url\(\s*["']?([^"')]+?)["']?\s*\)/g;
|
|
787
|
+
let cm;
|
|
788
|
+
while ((cm = urlRe.exec(cssContent)) !== null) {
|
|
789
|
+
const ref = cm[1];
|
|
790
|
+
if (ref.startsWith('data:') || ref.startsWith('http') || ref.startsWith('//')) continue;
|
|
791
|
+
const cssSrcDir = path.dirname(srcFile);
|
|
792
|
+
const assetSrc = path.resolve(cssSrcDir, ref);
|
|
793
|
+
if (!fs.existsSync(assetSrc)) continue;
|
|
794
|
+
for (const distDir of [serverDir, localDir]) {
|
|
795
|
+
const cssDestDir = path.dirname(path.join(distDir, asset));
|
|
796
|
+
const assetDest = path.resolve(cssDestDir, ref);
|
|
797
|
+
if (!fs.existsSync(assetDest)) {
|
|
798
|
+
const dir = path.dirname(assetDest);
|
|
799
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
800
|
+
fs.copyFileSync(assetSrc, assetDest);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
copiedCount++;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const bundleRel = path.relative(serverDir, bundleFile).replace(/\\/g, '/');
|
|
808
|
+
|
|
809
|
+
html = html.replace(
|
|
810
|
+
/<script\s+type\s*=\s*["']module["']\s+src\s*=\s*["'][^"']+["']\s*>\s*<\/script>/gi,
|
|
811
|
+
`<script defer src="${bundleRel}"></script>`
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
if (includeLib) {
|
|
815
|
+
html = html.replace(
|
|
816
|
+
/\s*<script\s+src\s*=\s*["'][^"']*zquery(?:\.min)?\.js["']\s*>\s*<\/script>/gi,
|
|
817
|
+
''
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Rewrite global CSS link to hashed version
|
|
822
|
+
if (globalCssOrigHref && globalCssHash) {
|
|
823
|
+
const escapedHref = globalCssOrigHref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
824
|
+
const cssLinkRe = new RegExp(
|
|
825
|
+
`(<link[^>]+href\\s*=\\s*["'])${escapedHref}(["'][^>]*>)`, 'i'
|
|
826
|
+
);
|
|
827
|
+
html = html.replace(cssLinkRe, `$1${globalCssHash}$2`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const serverHtml = html;
|
|
831
|
+
const localHtml = html.replace(/<base\s+href\s*=\s*["']\/["'][^>]*>\s*\n?\s*/i, '');
|
|
832
|
+
|
|
833
|
+
const htmlName = path.basename(htmlRelPath);
|
|
834
|
+
fs.writeFileSync(path.join(serverDir, htmlName), serverHtml, 'utf-8');
|
|
835
|
+
fs.writeFileSync(path.join(localDir, htmlName), localHtml, 'utf-8');
|
|
836
|
+
console.log(` ✓ server/${htmlName} (with <base href="/">)`);
|
|
837
|
+
console.log(` ✓ local/${htmlName} (relative paths, file:// ready)`);
|
|
838
|
+
console.log(` ✓ Copied ${copiedCount} asset(s) into both dist dirs`);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ---------------------------------------------------------------------------
|
|
842
|
+
// Static asset copying
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Copy the entire app directory into both dist/server and dist/local,
|
|
847
|
+
* skipping only build outputs, tooling dirs, and the app/ source dir.
|
|
848
|
+
* This ensures all static assets (assets/, icons/, images/, fonts/,
|
|
849
|
+
* manifests, etc.) are available in the built output without maintaining
|
|
850
|
+
* a fragile whitelist.
|
|
851
|
+
*/
|
|
852
|
+
function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
|
|
853
|
+
const SKIP_DIRS = new Set(['dist', 'node_modules', '.git', '.vscode']);
|
|
854
|
+
|
|
855
|
+
// Case-insensitive match for App/ directory (contains bundled source)
|
|
856
|
+
function isAppDir(name) {
|
|
857
|
+
return name.toLowerCase() === 'app';
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
let copiedCount = 0;
|
|
861
|
+
|
|
862
|
+
function copyEntry(srcPath, relPath) {
|
|
863
|
+
const stat = fs.statSync(srcPath);
|
|
864
|
+
if (stat.isDirectory()) {
|
|
865
|
+
const dirName = path.basename(srcPath);
|
|
866
|
+
if (SKIP_DIRS.has(dirName) || isAppDir(dirName)) return;
|
|
867
|
+
for (const child of fs.readdirSync(srcPath)) {
|
|
868
|
+
copyEntry(path.join(srcPath, child), path.join(relPath, child));
|
|
869
|
+
}
|
|
870
|
+
} else {
|
|
871
|
+
if (bundledFiles && bundledFiles.has(path.resolve(srcPath))) return;
|
|
872
|
+
for (const distDir of [serverDir, localDir]) {
|
|
873
|
+
const dest = path.join(distDir, relPath);
|
|
874
|
+
if (fs.existsSync(dest)) continue; // already copied by rewriteHtml
|
|
875
|
+
const dir = path.dirname(dest);
|
|
876
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
877
|
+
fs.copyFileSync(srcPath, dest);
|
|
878
|
+
}
|
|
879
|
+
copiedCount++;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
for (const entry of fs.readdirSync(appRoot, { withFileTypes: true })) {
|
|
884
|
+
if (entry.isDirectory()) {
|
|
885
|
+
if (SKIP_DIRS.has(entry.name) || isAppDir(entry.name)) continue;
|
|
886
|
+
copyEntry(path.join(appRoot, entry.name), entry.name);
|
|
887
|
+
} else {
|
|
888
|
+
copyEntry(path.join(appRoot, entry.name), entry.name);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (copiedCount > 0) {
|
|
893
|
+
console.log(` \u2713 Copied ${copiedCount} additional static asset(s) into both dist dirs`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ---------------------------------------------------------------------------
|
|
898
|
+
// Main bundleApp function
|
|
899
|
+
// ---------------------------------------------------------------------------
|
|
900
|
+
|
|
901
|
+
function bundleApp() {
|
|
902
|
+
const projectRoot = process.cwd();
|
|
903
|
+
const minimal = flag('minimal', 'm');
|
|
904
|
+
const globalCssOverride = option('global-css', null, null);
|
|
905
|
+
|
|
906
|
+
// Entry point - positional arg (directory or file) or auto-detection
|
|
907
|
+
let entry = null;
|
|
908
|
+
let targetDir = null;
|
|
909
|
+
for (let i = 1; i < args.length; i++) {
|
|
910
|
+
if (!args[i].startsWith('-') && args[i - 1] !== '-o' && args[i - 1] !== '--out' && args[i - 1] !== '--index') {
|
|
911
|
+
const resolved = path.resolve(projectRoot, args[i]);
|
|
912
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
913
|
+
targetDir = resolved;
|
|
914
|
+
entry = detectEntry(resolved);
|
|
915
|
+
} else {
|
|
916
|
+
entry = resolved;
|
|
917
|
+
}
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (!entry) entry = detectEntry(projectRoot);
|
|
922
|
+
|
|
923
|
+
if (!entry || !fs.existsSync(entry)) {
|
|
924
|
+
console.error(`\n \u2717 Could not find entry file.`);
|
|
925
|
+
console.error(` Provide an app directory: zquery bundle my-app/`);
|
|
926
|
+
console.error(` Or pass a direct entry file: zquery bundle my-app/scripts/main.js\n`);
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const outPath = option('out', 'o', null);
|
|
931
|
+
|
|
932
|
+
// Auto-detect HTML file
|
|
933
|
+
let htmlFile = option('index', 'i', null);
|
|
934
|
+
let htmlAbs = htmlFile ? path.resolve(projectRoot, htmlFile) : null;
|
|
935
|
+
if (!htmlFile) {
|
|
936
|
+
// Strategy: first look for index.html walking up from entry, then
|
|
937
|
+
// scan for any .html that references the entry via a module script tag.
|
|
938
|
+
const htmlCandidates = [];
|
|
939
|
+
let entryDir = path.dirname(entry);
|
|
940
|
+
while (entryDir.length >= projectRoot.length) {
|
|
941
|
+
htmlCandidates.push(path.join(entryDir, 'index.html'));
|
|
942
|
+
const parent = path.dirname(entryDir);
|
|
943
|
+
if (parent === entryDir) break;
|
|
944
|
+
entryDir = parent;
|
|
945
|
+
}
|
|
946
|
+
htmlCandidates.push(path.join(projectRoot, 'index.html'));
|
|
947
|
+
htmlCandidates.push(path.join(projectRoot, 'public/index.html'));
|
|
948
|
+
for (const candidate of htmlCandidates) {
|
|
949
|
+
if (fs.existsSync(candidate)) {
|
|
950
|
+
htmlAbs = candidate;
|
|
951
|
+
htmlFile = path.relative(projectRoot, candidate);
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// If no index.html found, scan for any .html file that references
|
|
957
|
+
// the entry point (supports home.html, app.html, etc.)
|
|
958
|
+
if (!htmlAbs) {
|
|
959
|
+
const searchRoot = targetDir || projectRoot;
|
|
960
|
+
const htmlScan = [];
|
|
961
|
+
for (const e of fs.readdirSync(searchRoot, { withFileTypes: true })) {
|
|
962
|
+
if (e.isFile() && e.name.endsWith('.html')) {
|
|
963
|
+
htmlScan.push(path.join(searchRoot, e.name));
|
|
964
|
+
} else if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist') {
|
|
965
|
+
try {
|
|
966
|
+
for (const child of fs.readdirSync(path.join(searchRoot, e.name), { withFileTypes: true })) {
|
|
967
|
+
if (child.isFile() && child.name.endsWith('.html')) {
|
|
968
|
+
htmlScan.push(path.join(searchRoot, e.name, child.name));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} catch { /* skip */ }
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
// Prefer the HTML file that references our entry via a module script
|
|
975
|
+
const moduleScriptRe = /<script[^>]+type\s*=\s*["']module["'][^>]+src\s*=\s*["']([^"']+)["']/g;
|
|
976
|
+
for (const hp of htmlScan) {
|
|
977
|
+
const content = fs.readFileSync(hp, 'utf-8');
|
|
978
|
+
let m;
|
|
979
|
+
moduleScriptRe.lastIndex = 0;
|
|
980
|
+
while ((m = moduleScriptRe.exec(content)) !== null) {
|
|
981
|
+
const resolved = path.resolve(path.dirname(hp), m[1]);
|
|
982
|
+
if (resolved === path.resolve(entry)) {
|
|
983
|
+
htmlAbs = hp;
|
|
984
|
+
htmlFile = path.relative(projectRoot, hp);
|
|
985
|
+
break;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (htmlAbs) break;
|
|
989
|
+
}
|
|
990
|
+
// Last resort: use the first .html found
|
|
991
|
+
if (!htmlAbs && htmlScan.length > 0) {
|
|
992
|
+
htmlAbs = htmlScan[0];
|
|
993
|
+
htmlFile = path.relative(projectRoot, htmlScan[0]);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Output directory
|
|
999
|
+
const entryRel = path.relative(projectRoot, entry);
|
|
1000
|
+
const entryName = path.basename(entry, '.js');
|
|
1001
|
+
let baseDistDir;
|
|
1002
|
+
if (outPath) {
|
|
1003
|
+
const resolved = path.resolve(projectRoot, outPath);
|
|
1004
|
+
if (outPath.endsWith('/') || outPath.endsWith('\\') || !path.extname(outPath) ||
|
|
1005
|
+
(fs.existsSync(resolved) && fs.statSync(resolved).isDirectory())) {
|
|
1006
|
+
baseDistDir = resolved;
|
|
1007
|
+
} else {
|
|
1008
|
+
baseDistDir = path.dirname(resolved);
|
|
1009
|
+
}
|
|
1010
|
+
} else if (htmlAbs) {
|
|
1011
|
+
baseDistDir = path.join(path.dirname(htmlAbs), 'dist');
|
|
1012
|
+
} else {
|
|
1013
|
+
baseDistDir = path.join(projectRoot, 'dist');
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const serverDir = path.join(baseDistDir, 'server');
|
|
1017
|
+
const localDir = path.join(baseDistDir, 'local');
|
|
1018
|
+
|
|
1019
|
+
console.log(`\n zQuery App Bundler`);
|
|
1020
|
+
console.log(` Entry: ${entryRel}`);
|
|
1021
|
+
console.log(` Output: ${path.relative(projectRoot, baseDistDir)}/server/ & local/`);
|
|
1022
|
+
console.log(` Library: embedded`);
|
|
1023
|
+
console.log(` HTML: ${htmlFile || 'not found (no HTML detected)'}`);
|
|
1024
|
+
if (minimal) console.log(` Mode: minimal (HTML + JS + global CSS only)`);
|
|
1025
|
+
console.log('');
|
|
1026
|
+
|
|
1027
|
+
// ------ doBuild (inlined) ------
|
|
1028
|
+
const start = Date.now();
|
|
1029
|
+
|
|
1030
|
+
// Clean previous dist outputs for a fresh build
|
|
1031
|
+
for (const dir of [serverDir, localDir]) {
|
|
1032
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true });
|
|
1033
|
+
}
|
|
1034
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
1035
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
1036
|
+
|
|
1037
|
+
const files = walkImportGraph(entry);
|
|
1038
|
+
console.log(` Resolved ${files.length} module(s):`);
|
|
1039
|
+
files.forEach(f => console.log(` • ${path.relative(projectRoot, f)}`));
|
|
1040
|
+
|
|
1041
|
+
const sections = files.map(file => {
|
|
1042
|
+
let code = fs.readFileSync(file, 'utf-8');
|
|
1043
|
+
const stripped = stripModuleSyntax(code);
|
|
1044
|
+
code = stripped.code;
|
|
1045
|
+
code = replaceImportMeta(code, file, projectRoot);
|
|
1046
|
+
code = rewriteResourceUrls(code, file, projectRoot);
|
|
1047
|
+
code = minifyTemplateLiterals(code);
|
|
1048
|
+
const rel = path.relative(projectRoot, file);
|
|
1049
|
+
return `// --- ${rel} ${'-'.repeat(Math.max(1, 60 - rel.length))}\n{\n${code.trim()}\n}`;
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
// Embed zquery.min.js
|
|
1053
|
+
let libSection = '';
|
|
1054
|
+
{
|
|
1055
|
+
// __dirname is cli/commands/, so the package root is two levels up.
|
|
1056
|
+
// When installed via `npm install zero-query`, pkgRoot lives inside the
|
|
1057
|
+
// user's node_modules/ — in that case we never rebuild and just embed the
|
|
1058
|
+
// pre-built dist/zquery.min.js that ships with the package.
|
|
1059
|
+
const pkgRoot = path.resolve(__dirname, '..', '..');
|
|
1060
|
+
const pkgSrcDir = path.join(pkgRoot, 'src');
|
|
1061
|
+
const pkgMinFile = path.join(pkgRoot, 'dist', 'zquery.min.js');
|
|
1062
|
+
const isInstalled = /[\\/]node_modules[\\/]/.test(pkgRoot);
|
|
1063
|
+
|
|
1064
|
+
if (!isInstalled && fs.existsSync(pkgSrcDir) && fs.existsSync(path.join(pkgRoot, 'index.js'))) {
|
|
1065
|
+
console.log(`\n Building library from source...`);
|
|
1066
|
+
const prevCwd = process.cwd();
|
|
1067
|
+
try {
|
|
1068
|
+
process.chdir(pkgRoot);
|
|
1069
|
+
buildLibrary();
|
|
1070
|
+
} finally {
|
|
1071
|
+
process.chdir(prevCwd);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const htmlDir = htmlAbs ? path.dirname(htmlAbs) : null;
|
|
1076
|
+
|
|
1077
|
+
// Case-insensitive search for Assets/ directory
|
|
1078
|
+
function findAssetsDir(root) {
|
|
1079
|
+
try {
|
|
1080
|
+
for (const e of fs.readdirSync(root, { withFileTypes: true })) {
|
|
1081
|
+
if (e.isDirectory() && e.name.toLowerCase() === 'assets') return path.join(root, e.name);
|
|
1082
|
+
}
|
|
1083
|
+
} catch { /* skip */ }
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const assetsDir = findAssetsDir(htmlDir || projectRoot);
|
|
1088
|
+
const altAssetsDir = htmlDir ? findAssetsDir(projectRoot) : null;
|
|
1089
|
+
|
|
1090
|
+
const libCandidates = [
|
|
1091
|
+
path.join(pkgRoot, 'dist/zquery.min.js'),
|
|
1092
|
+
assetsDir && path.join(assetsDir, 'scripts/zquery.min.js'),
|
|
1093
|
+
altAssetsDir && path.join(altAssetsDir, 'scripts/zquery.min.js'),
|
|
1094
|
+
htmlDir && path.join(htmlDir, 'scripts/vendor/zquery.min.js'),
|
|
1095
|
+
htmlDir && path.join(htmlDir, 'vendor/zquery.min.js'),
|
|
1096
|
+
path.join(projectRoot, 'scripts/vendor/zquery.min.js'),
|
|
1097
|
+
path.join(projectRoot, 'vendor/zquery.min.js'),
|
|
1098
|
+
path.join(projectRoot, 'lib/zquery.min.js'),
|
|
1099
|
+
path.join(projectRoot, 'dist/zquery.min.js'),
|
|
1100
|
+
path.join(projectRoot, 'zquery.min.js'),
|
|
1101
|
+
].filter(Boolean);
|
|
1102
|
+
const libPath = libCandidates.find(p => fs.existsSync(p));
|
|
1103
|
+
|
|
1104
|
+
if (libPath) {
|
|
1105
|
+
const libBytes = fs.statSync(libPath).size;
|
|
1106
|
+
libSection = `// --- zquery.min.js (library) ${'-'.repeat(34)}\n${fs.readFileSync(libPath, 'utf-8').trim()}\n\n`
|
|
1107
|
+
+ `// --- Build-time metadata ${'-'.repeat(50)}\nif(typeof $!=="undefined"){$.meta=Object.assign($.meta||{},{libSize:${libBytes}});}\n\n`;
|
|
1108
|
+
console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
|
|
1109
|
+
} else {
|
|
1110
|
+
console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
|
|
1111
|
+
console.warn(` Place zquery.min.js in assets/scripts/, vendor/, lib/, or dist/`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const banner = `/**\n * App bundle - built by zQuery CLI\n * Entry: ${entryRel}\n * ${new Date().toISOString()}\n */`;
|
|
1116
|
+
|
|
1117
|
+
// Inline resources
|
|
1118
|
+
const inlineMap = collectInlineResources(files, projectRoot);
|
|
1119
|
+
let inlineSection = '';
|
|
1120
|
+
if (Object.keys(inlineMap).length > 0) {
|
|
1121
|
+
const entries = Object.entries(inlineMap).map(([key, content]) => {
|
|
1122
|
+
const escaped = content
|
|
1123
|
+
.replace(/\\/g, '\\\\')
|
|
1124
|
+
.replace(/'/g, "\\'")
|
|
1125
|
+
.replace(/\n/g, '\\n')
|
|
1126
|
+
.replace(/\r/g, '');
|
|
1127
|
+
return ` '${key}': '${escaped}'`;
|
|
1128
|
+
});
|
|
1129
|
+
inlineSection = `// --- Inlined resources (file:// support) ${'-'.repeat(20)}\nwindow.__zqInline = {\n${entries.join(',\n')}\n};\n\n`;
|
|
1130
|
+
console.log(`\n Inlined ${Object.keys(inlineMap).length} external resource(s)`);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const bundle = `${banner}\n(function() {\n 'use strict';\n\n${libSection}${inlineSection}${sections.join('\n\n')}\n\n})();\n`;
|
|
1134
|
+
|
|
1135
|
+
// Content-hashed filenames
|
|
1136
|
+
const contentHash = crypto.createHash('sha256').update(bundle).digest('hex').slice(0, 8);
|
|
1137
|
+
const minBase = `z-${entryName}.${contentHash}.min.js`;
|
|
1138
|
+
|
|
1139
|
+
// Clean previous builds
|
|
1140
|
+
const escName = entryName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1141
|
+
const cleanRe = new RegExp(`^z-${escName}\\.[a-f0-9]{8}\\.(?:min\\.)?js$`);
|
|
1142
|
+
for (const dir of [serverDir, localDir]) {
|
|
1143
|
+
if (fs.existsSync(dir)) {
|
|
1144
|
+
for (const f of fs.readdirSync(dir)) {
|
|
1145
|
+
if (cleanRe.test(f)) fs.unlinkSync(path.join(dir, f));
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Write minified bundle
|
|
1151
|
+
const minFile = path.join(serverDir, minBase);
|
|
1152
|
+
fs.writeFileSync(minFile, minify(bundle, banner), 'utf-8');
|
|
1153
|
+
fs.copyFileSync(minFile, path.join(localDir, minBase));
|
|
1154
|
+
|
|
1155
|
+
console.log(`\n ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
|
|
1156
|
+
|
|
1157
|
+
// ------------------------------------------------------------------
|
|
1158
|
+
// Global CSS bundling - extract from index.html <link> or --global-css
|
|
1159
|
+
// ------------------------------------------------------------------
|
|
1160
|
+
let globalCssHash = null;
|
|
1161
|
+
let globalCssOrigHref = null;
|
|
1162
|
+
let globalCssPath = null;
|
|
1163
|
+
if (htmlAbs) {
|
|
1164
|
+
const htmlContent = fs.readFileSync(htmlAbs, 'utf-8');
|
|
1165
|
+
const htmlDir = path.dirname(htmlAbs);
|
|
1166
|
+
|
|
1167
|
+
// Determine global CSS path: --global-css flag overrides, else first <link rel="stylesheet"> in HTML
|
|
1168
|
+
globalCssPath = null;
|
|
1169
|
+
if (globalCssOverride) {
|
|
1170
|
+
globalCssPath = path.resolve(projectRoot, globalCssOverride);
|
|
1171
|
+
// Reconstruct relative href for HTML rewriting
|
|
1172
|
+
globalCssOrigHref = path.relative(htmlDir, globalCssPath).replace(/\\/g, '/');
|
|
1173
|
+
} else {
|
|
1174
|
+
const linkRe = /<link[^>]+rel\s*=\s*["']stylesheet["'][^>]+href\s*=\s*["']([^"']+)["']/gi;
|
|
1175
|
+
const altRe = /<link[^>]+href\s*=\s*["']([^"']+)["'][^>]+rel\s*=\s*["']stylesheet["']/gi;
|
|
1176
|
+
let linkMatch = linkRe.exec(htmlContent) || altRe.exec(htmlContent);
|
|
1177
|
+
if (linkMatch) {
|
|
1178
|
+
globalCssOrigHref = linkMatch[1];
|
|
1179
|
+
// Strip query string / fragment so the path resolves to the actual file
|
|
1180
|
+
const cleanHref = linkMatch[1].split('?')[0].split('#')[0];
|
|
1181
|
+
globalCssPath = path.resolve(htmlDir, cleanHref);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (globalCssPath && fs.existsSync(globalCssPath)) {
|
|
1186
|
+
let cssContent = fs.readFileSync(globalCssPath, 'utf-8');
|
|
1187
|
+
const cssMin = minifyCSS(cssContent);
|
|
1188
|
+
const cssHash = crypto.createHash('sha256').update(cssMin).digest('hex').slice(0, 8);
|
|
1189
|
+
const cssOutName = `global.${cssHash}.min.css`;
|
|
1190
|
+
|
|
1191
|
+
// Clean previous global CSS builds
|
|
1192
|
+
const cssCleanRe = /^global\.[a-f0-9]{8}\.min\.css$/;
|
|
1193
|
+
for (const dir of [serverDir, localDir]) {
|
|
1194
|
+
if (fs.existsSync(dir)) {
|
|
1195
|
+
for (const f of fs.readdirSync(dir)) {
|
|
1196
|
+
if (cssCleanRe.test(f)) fs.unlinkSync(path.join(dir, f));
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
fs.writeFileSync(path.join(serverDir, cssOutName), cssMin, 'utf-8');
|
|
1202
|
+
fs.writeFileSync(path.join(localDir, cssOutName), cssMin, 'utf-8');
|
|
1203
|
+
globalCssHash = cssOutName;
|
|
1204
|
+
console.log(` ✓ ${cssOutName} (${sizeKB(Buffer.from(cssMin))} KB)`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Rewrite HTML to reference the minified bundle
|
|
1209
|
+
const bundledFileSet = new Set(files);
|
|
1210
|
+
// Skip the original unminified global CSS from static asset copying
|
|
1211
|
+
if (globalCssPath) bundledFileSet.add(path.resolve(globalCssPath));
|
|
1212
|
+
if (htmlFile) {
|
|
1213
|
+
rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir, globalCssOrigHref, globalCssHash);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Copy static asset directories (icons/, images/, fonts/, etc.)
|
|
1217
|
+
if (!minimal) {
|
|
1218
|
+
const appRoot = htmlAbs ? path.dirname(htmlAbs) : (targetDir || projectRoot);
|
|
1219
|
+
copyStaticAssets(appRoot, serverDir, localDir, bundledFileSet);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const elapsed = Date.now() - start;
|
|
1223
|
+
console.log(` Done in ${elapsed}ms\n`);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
module.exports = bundleApp;
|
|
1227
|
+
module.exports.stripModuleSyntax = stripModuleSyntax;
|
|
1228
|
+
module.exports.minifyTemplateLiterals = minifyTemplateLiterals;
|