zero-query 0.6.3 → 0.8.6
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/README.md +39 -29
- package/cli/commands/build.js +113 -4
- package/cli/commands/bundle.js +392 -29
- package/cli/commands/create.js +1 -1
- package/cli/commands/dev/devtools/index.js +56 -0
- package/cli/commands/dev/devtools/js/components.js +49 -0
- package/cli/commands/dev/devtools/js/core.js +409 -0
- package/cli/commands/dev/devtools/js/elements.js +413 -0
- package/cli/commands/dev/devtools/js/network.js +166 -0
- package/cli/commands/dev/devtools/js/performance.js +73 -0
- package/cli/commands/dev/devtools/js/router.js +105 -0
- package/cli/commands/dev/devtools/js/source.js +132 -0
- package/cli/commands/dev/devtools/js/stats.js +35 -0
- package/cli/commands/dev/devtools/js/tabs.js +79 -0
- package/cli/commands/dev/devtools/panel.html +95 -0
- package/cli/commands/dev/devtools/styles.css +244 -0
- package/cli/commands/dev/index.js +29 -4
- package/cli/commands/dev/logger.js +6 -1
- package/cli/commands/dev/overlay.js +428 -2
- package/cli/commands/dev/server.js +42 -5
- package/cli/commands/dev/watcher.js +59 -1
- package/cli/help.js +8 -5
- package/cli/scaffold/{scripts → app}/app.js +16 -23
- package/cli/scaffold/{scripts → app}/components/about.js +4 -4
- package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
- package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -7
- package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +3 -3
- package/cli/scaffold/app/components/home.js +137 -0
- package/cli/scaffold/{scripts → app}/routes.js +1 -1
- package/cli/scaffold/{scripts → app}/store.js +6 -6
- package/cli/scaffold/assets/.gitkeep +0 -0
- package/cli/scaffold/{styles/styles.css → global.css} +4 -2
- package/cli/scaffold/index.html +12 -11
- package/cli/utils.js +111 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +1122 -158
- package/dist/zquery.min.js +3 -16
- package/index.d.ts +129 -1290
- package/index.js +15 -10
- package/package.json +7 -6
- package/src/component.js +172 -49
- package/src/core.js +359 -18
- package/src/diff.js +256 -58
- package/src/expression.js +33 -3
- package/src/reactive.js +37 -5
- package/src/router.js +243 -7
- package/tests/component.test.js +886 -0
- package/tests/core.test.js +977 -0
- package/tests/diff.test.js +525 -0
- package/tests/errors.test.js +162 -0
- package/tests/expression.test.js +482 -0
- package/tests/http.test.js +289 -0
- package/tests/reactive.test.js +339 -0
- package/tests/router.test.js +649 -0
- package/tests/store.test.js +379 -0
- package/tests/utils.test.js +512 -0
- package/types/collection.d.ts +383 -0
- package/types/component.d.ts +217 -0
- package/types/errors.d.ts +103 -0
- package/types/http.d.ts +81 -0
- package/types/misc.d.ts +179 -0
- package/types/reactive.d.ts +76 -0
- package/types/router.d.ts +161 -0
- package/types/ssr.d.ts +49 -0
- package/types/store.d.ts +107 -0
- package/types/utils.d.ts +142 -0
- package/cli/commands/dev.old.js +0 -520
- package/cli/scaffold/scripts/components/home.js +0 -137
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
package/cli/commands/bundle.js
CHANGED
|
@@ -24,6 +24,8 @@ const buildLibrary = require('./build');
|
|
|
24
24
|
/** Resolve an import specifier relative to the importing file. */
|
|
25
25
|
function resolveImport(specifier, fromFile) {
|
|
26
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;
|
|
27
29
|
let resolved = path.resolve(path.dirname(fromFile), specifier);
|
|
28
30
|
if (!path.extname(resolved) && fs.existsSync(resolved + '.js')) {
|
|
29
31
|
resolved += '.js';
|
|
@@ -112,6 +114,243 @@ function rewriteResourceUrls(code, filePath, projectRoot) {
|
|
|
112
114
|
);
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Minify HTML for inlining — strips indentation and collapses whitespace
|
|
119
|
+
* between tags. Preserves content inside <pre>, <code>, and <textarea>
|
|
120
|
+
* blocks verbatim so syntax-highlighted code samples survive.
|
|
121
|
+
*/
|
|
122
|
+
function minifyHTML(html) {
|
|
123
|
+
const preserved = [];
|
|
124
|
+
// Protect <pre>…</pre> and <textarea>…</textarea> (multiline code blocks)
|
|
125
|
+
html = html.replace(/<(pre|textarea)(\b[^>]*)>[\s\S]*?<\/\1>/gi, m => {
|
|
126
|
+
preserved.push(m);
|
|
127
|
+
return `\x00P${preserved.length - 1}\x00`;
|
|
128
|
+
});
|
|
129
|
+
// Strip HTML comments
|
|
130
|
+
html = html.replace(/<!--[\s\S]*?-->/g, '');
|
|
131
|
+
// Collapse runs of whitespace (newlines + indentation) to a single space
|
|
132
|
+
html = html.replace(/\s{2,}/g, ' ');
|
|
133
|
+
// Remove space between tags: "> <" → "><"
|
|
134
|
+
// but preserve a space when an inline element is involved (a, span, strong, em, b, i, code, small, sub, sup, abbr, label)
|
|
135
|
+
html = html.replace(/>\s+</g, (m, offset) => {
|
|
136
|
+
const before = html.slice(Math.max(0, offset - 80), offset + 1);
|
|
137
|
+
const after = html.slice(offset + m.length - 1, offset + m.length + 40);
|
|
138
|
+
const inlineTags = /\b(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\b/i;
|
|
139
|
+
const closingInline = /<\/\s*(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\s*>$/i.test(before);
|
|
140
|
+
const openingInline = /^<(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)[\s>]/i.test(after);
|
|
141
|
+
return (closingInline || openingInline) ? '> <' : '><';
|
|
142
|
+
});
|
|
143
|
+
// Remove spaces inside opening tags: <tag attr = "val" > → <tag attr="val">
|
|
144
|
+
html = html.replace(/ *\/ *>/g, '/>');
|
|
145
|
+
html = html.replace(/ *= */g, '=');
|
|
146
|
+
// Trim
|
|
147
|
+
html = html.trim();
|
|
148
|
+
// Restore preserved blocks
|
|
149
|
+
html = html.replace(/\x00P(\d+)\x00/g, (_, i) => preserved[+i]);
|
|
150
|
+
return html;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Minify CSS for inlining — strips comments, collapses whitespace,
|
|
155
|
+
* removes unnecessary spaces around punctuation.
|
|
156
|
+
*/
|
|
157
|
+
function minifyCSS(css) {
|
|
158
|
+
// Strip block comments
|
|
159
|
+
css = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
160
|
+
// Collapse whitespace
|
|
161
|
+
css = css.replace(/\s{2,}/g, ' ');
|
|
162
|
+
// Remove spaces around { } : ; ,
|
|
163
|
+
css = css.replace(/\s*([{};:,])\s*/g, '$1');
|
|
164
|
+
// Remove trailing semicolons before }
|
|
165
|
+
css = css.replace(/;}/g, '}');
|
|
166
|
+
return css.trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Walk JS source and minify the HTML/CSS inside template literals.
|
|
171
|
+
* Handles ${…} interpolations (with nesting) and preserves <pre> blocks.
|
|
172
|
+
* Only trims whitespace sequences that appear between/around HTML tags.
|
|
173
|
+
*/
|
|
174
|
+
function minifyTemplateLiterals(code) {
|
|
175
|
+
let out = '';
|
|
176
|
+
let i = 0;
|
|
177
|
+
while (i < code.length) {
|
|
178
|
+
const ch = code[i];
|
|
179
|
+
|
|
180
|
+
// Regular string: copy verbatim
|
|
181
|
+
if (ch === '"' || ch === "'") {
|
|
182
|
+
const q = ch; out += ch; i++;
|
|
183
|
+
while (i < code.length) {
|
|
184
|
+
if (code[i] === '\\') { out += code[i] + (code[i + 1] || ''); i += 2; continue; }
|
|
185
|
+
out += code[i];
|
|
186
|
+
if (code[i] === q) { i++; break; }
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Line comment: copy verbatim
|
|
193
|
+
if (ch === '/' && code[i + 1] === '/') {
|
|
194
|
+
while (i < code.length && code[i] !== '\n') out += code[i++];
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// Block comment: copy verbatim
|
|
198
|
+
if (ch === '/' && code[i + 1] === '*') {
|
|
199
|
+
out += '/*'; i += 2;
|
|
200
|
+
while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) out += code[i++];
|
|
201
|
+
out += '*/'; i += 2;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Template literal: extract, minify HTML, and emit
|
|
206
|
+
if (ch === '`') {
|
|
207
|
+
out += _minifyTemplate(code, i);
|
|
208
|
+
// Advance past the template
|
|
209
|
+
i = _skipTemplateLiteral(code, i);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
out += ch; i++;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Extract a full template literal (handling nested ${…}) and return it minified. */
|
|
219
|
+
function _minifyTemplate(code, start) {
|
|
220
|
+
const end = _skipTemplateLiteral(code, start);
|
|
221
|
+
const raw = code.substring(start, end);
|
|
222
|
+
// Only minify templates that contain HTML tags or CSS rules
|
|
223
|
+
if (/<\w/.test(raw)) return _collapseTemplateWS(raw);
|
|
224
|
+
if (/[{};]\s/.test(raw) && /[.#\w-]+\s*\{/.test(raw)) return _collapseTemplateCSS(raw);
|
|
225
|
+
return raw;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Return index past the closing backtick of a template starting at `start`. */
|
|
229
|
+
function _skipTemplateLiteral(code, start) {
|
|
230
|
+
let i = start + 1; // skip opening backtick
|
|
231
|
+
let depth = 0;
|
|
232
|
+
while (i < code.length) {
|
|
233
|
+
if (code[i] === '\\') { i += 2; continue; }
|
|
234
|
+
if (code[i] === '$' && code[i + 1] === '{') { depth++; i += 2; continue; }
|
|
235
|
+
if (depth > 0) {
|
|
236
|
+
if (code[i] === '{') { depth++; i++; continue; }
|
|
237
|
+
if (code[i] === '}') { depth--; i++; continue; }
|
|
238
|
+
if (code[i] === '`') { i = _skipTemplateLiteral(code, i); continue; }
|
|
239
|
+
if (code[i] === '"' || code[i] === "'") {
|
|
240
|
+
const q = code[i]; i++;
|
|
241
|
+
while (i < code.length) {
|
|
242
|
+
if (code[i] === '\\') { i += 2; continue; }
|
|
243
|
+
if (code[i] === q) { i++; break; }
|
|
244
|
+
i++;
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
i++; continue;
|
|
249
|
+
}
|
|
250
|
+
if (code[i] === '`') { i++; return i; } // closing backtick
|
|
251
|
+
i++;
|
|
252
|
+
}
|
|
253
|
+
return i;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Collapse whitespace in the text portions of an HTML template literal,
|
|
258
|
+
* preserving ${…} expressions, <pre> blocks, and inline text spacing.
|
|
259
|
+
*/
|
|
260
|
+
function _collapseTemplateWS(tpl) {
|
|
261
|
+
// Build array of segments: text portions vs ${…} expressions
|
|
262
|
+
const segments = [];
|
|
263
|
+
let i = 1; // skip opening backtick
|
|
264
|
+
let text = '';
|
|
265
|
+
while (i < tpl.length - 1) { // stop before closing backtick
|
|
266
|
+
if (tpl[i] === '\\') { text += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
267
|
+
if (tpl[i] === '$' && tpl[i + 1] === '{') {
|
|
268
|
+
if (text) { segments.push({ type: 'text', val: text }); text = ''; }
|
|
269
|
+
// Collect the full expression
|
|
270
|
+
let depth = 1; let expr = '${'; i += 2;
|
|
271
|
+
while (i < tpl.length - 1 && depth > 0) {
|
|
272
|
+
if (tpl[i] === '\\') { expr += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
273
|
+
if (tpl[i] === '{') depth++;
|
|
274
|
+
if (tpl[i] === '}') depth--;
|
|
275
|
+
if (depth > 0) { expr += tpl[i]; i++; } else { expr += '}'; i++; }
|
|
276
|
+
}
|
|
277
|
+
segments.push({ type: 'expr', val: expr });
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
text += tpl[i]; i++;
|
|
281
|
+
}
|
|
282
|
+
if (text) segments.push({ type: 'text', val: text });
|
|
283
|
+
|
|
284
|
+
// Minify text segments (collapse whitespace between/around tags)
|
|
285
|
+
for (let s = 0; s < segments.length; s++) {
|
|
286
|
+
if (segments[s].type !== 'text') continue;
|
|
287
|
+
let t = segments[s].val;
|
|
288
|
+
// Protect <pre>…</pre> regions
|
|
289
|
+
const preserved = [];
|
|
290
|
+
t = t.replace(/<pre(\b[^>]*)>[\s\S]*?<\/pre>/gi, m => {
|
|
291
|
+
preserved.push(m);
|
|
292
|
+
return `\x00P${preserved.length - 1}\x00`;
|
|
293
|
+
});
|
|
294
|
+
// Collapse whitespace runs that touch a < or > but preserve a space
|
|
295
|
+
// when an inline element boundary is involved
|
|
296
|
+
t = t.replace(/>\s{2,}/g, (m, offset) => {
|
|
297
|
+
const before = t.slice(Math.max(0, offset - 80), offset + 1);
|
|
298
|
+
if (/<\/\s*(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\s*>$/i.test(before)) return '> ';
|
|
299
|
+
return '>';
|
|
300
|
+
});
|
|
301
|
+
t = t.replace(/\s{2,}</g, (m, offset) => {
|
|
302
|
+
const after = t.slice(offset + m.length - 1, offset + m.length + 40);
|
|
303
|
+
if (/^<(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)[\s>]/i.test(after)) return ' <';
|
|
304
|
+
return '<';
|
|
305
|
+
});
|
|
306
|
+
// Collapse other multi-whitespace runs to a single space
|
|
307
|
+
t = t.replace(/\s{2,}/g, ' ');
|
|
308
|
+
// Restore <pre> blocks
|
|
309
|
+
t = t.replace(/\x00P(\d+)\x00/g, (_, idx) => preserved[+idx]);
|
|
310
|
+
segments[s].val = t;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return '`' + segments.map(s => s.val).join('') + '`';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Collapse CSS whitespace inside a template literal (styles: `…`).
|
|
318
|
+
* Uses the same segment-splitting approach as _collapseTemplateWS so
|
|
319
|
+
* ${…} expressions are preserved untouched.
|
|
320
|
+
*/
|
|
321
|
+
function _collapseTemplateCSS(tpl) {
|
|
322
|
+
const segments = [];
|
|
323
|
+
let i = 1; let text = '';
|
|
324
|
+
while (i < tpl.length - 1) {
|
|
325
|
+
if (tpl[i] === '\\') { text += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
326
|
+
if (tpl[i] === '$' && tpl[i + 1] === '{') {
|
|
327
|
+
if (text) { segments.push({ type: 'text', val: text }); text = ''; }
|
|
328
|
+
let depth = 1; let expr = '${'; i += 2;
|
|
329
|
+
while (i < tpl.length - 1 && depth > 0) {
|
|
330
|
+
if (tpl[i] === '\\') { expr += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
331
|
+
if (tpl[i] === '{') depth++;
|
|
332
|
+
if (tpl[i] === '}') depth--;
|
|
333
|
+
if (depth > 0) { expr += tpl[i]; i++; } else { expr += '}'; i++; }
|
|
334
|
+
}
|
|
335
|
+
segments.push({ type: 'expr', val: expr });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
text += tpl[i]; i++;
|
|
339
|
+
}
|
|
340
|
+
if (text) segments.push({ type: 'text', val: text });
|
|
341
|
+
|
|
342
|
+
for (let s = 0; s < segments.length; s++) {
|
|
343
|
+
if (segments[s].type !== 'text') continue;
|
|
344
|
+
let t = segments[s].val;
|
|
345
|
+
t = t.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
346
|
+
t = t.replace(/\s{2,}/g, ' ');
|
|
347
|
+
t = t.replace(/\s*([{};:,])\s*/g, '$1');
|
|
348
|
+
t = t.replace(/;}/g, '}');
|
|
349
|
+
segments[s].val = t;
|
|
350
|
+
}
|
|
351
|
+
return '`' + segments.map(s => s.val).join('') + '`';
|
|
352
|
+
}
|
|
353
|
+
|
|
115
354
|
/**
|
|
116
355
|
* Scan bundled source files for external resource references
|
|
117
356
|
* (pages config, templateUrl, styleUrl) and return a map of
|
|
@@ -152,7 +391,8 @@ function collectInlineResources(files, projectRoot) {
|
|
|
152
391
|
}
|
|
153
392
|
|
|
154
393
|
// styleUrl:
|
|
155
|
-
const
|
|
394
|
+
const styleUrlRe = /styleUrl\s*:\s*['"]([^'"]+)['"]/g;
|
|
395
|
+
const styleMatch = styleUrlRe.exec(code);
|
|
156
396
|
if (styleMatch) {
|
|
157
397
|
const stylePath = path.join(fileDir, styleMatch[1]);
|
|
158
398
|
if (fs.existsSync(stylePath)) {
|
|
@@ -172,6 +412,12 @@ function collectInlineResources(files, projectRoot) {
|
|
|
172
412
|
}
|
|
173
413
|
}
|
|
174
414
|
|
|
415
|
+
// Minify inlined resources by type
|
|
416
|
+
for (const key of Object.keys(inlineMap)) {
|
|
417
|
+
if (key.endsWith('.html')) inlineMap[key] = minifyHTML(inlineMap[key]);
|
|
418
|
+
else if (key.endsWith('.css')) inlineMap[key] = minifyCSS(inlineMap[key]);
|
|
419
|
+
}
|
|
420
|
+
|
|
175
421
|
return inlineMap;
|
|
176
422
|
}
|
|
177
423
|
|
|
@@ -275,7 +521,7 @@ function detectEntry(projectRoot) {
|
|
|
275
521
|
}
|
|
276
522
|
|
|
277
523
|
// 3. Convention fallbacks
|
|
278
|
-
const fallbacks = ['scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
|
|
524
|
+
const fallbacks = ['app/app.js', 'scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
|
|
279
525
|
for (const f of fallbacks) {
|
|
280
526
|
const fp = path.join(projectRoot, f);
|
|
281
527
|
if (fs.existsSync(fp)) return fp;
|
|
@@ -293,7 +539,7 @@ function detectEntry(projectRoot) {
|
|
|
293
539
|
* server/index.html — <base href="/"> for SPA deep routes
|
|
294
540
|
* local/index.html — relative paths for file:// access
|
|
295
541
|
*/
|
|
296
|
-
function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir) {
|
|
542
|
+
function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir, globalCssOrigHref, globalCssHash) {
|
|
297
543
|
const htmlPath = path.resolve(projectRoot, htmlRelPath);
|
|
298
544
|
if (!fs.existsSync(htmlPath)) {
|
|
299
545
|
console.warn(` ⚠ HTML file not found: ${htmlRelPath}`);
|
|
@@ -316,6 +562,37 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
|
|
|
316
562
|
assets.add(ref);
|
|
317
563
|
}
|
|
318
564
|
|
|
565
|
+
// Also scan the bundled JS for src/href references to local assets
|
|
566
|
+
const bundleContent = fs.existsSync(bundleFile) ? fs.readFileSync(bundleFile, 'utf-8') : '';
|
|
567
|
+
const jsAssetRe = /\b(?:src|href)\s*=\s*["\\]*["']([^"']+\.(?:svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|eot|mp4|webm))["']/gi;
|
|
568
|
+
while ((m = jsAssetRe.exec(bundleContent)) !== null) {
|
|
569
|
+
const ref = m[1];
|
|
570
|
+
if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:')) continue;
|
|
571
|
+
if (!assets.has(ref)) {
|
|
572
|
+
const refAbs = path.resolve(htmlDir, ref);
|
|
573
|
+
if (fs.existsSync(refAbs)) assets.add(ref);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// For any referenced asset directories, copy all sibling files too
|
|
578
|
+
const assetDirs = new Set();
|
|
579
|
+
for (const asset of assets) {
|
|
580
|
+
const dir = path.dirname(asset);
|
|
581
|
+
if (dir && dir !== '.') assetDirs.add(dir);
|
|
582
|
+
}
|
|
583
|
+
for (const dir of assetDirs) {
|
|
584
|
+
const absDirPath = path.resolve(htmlDir, dir);
|
|
585
|
+
if (fs.existsSync(absDirPath) && fs.statSync(absDirPath).isDirectory()) {
|
|
586
|
+
for (const child of fs.readdirSync(absDirPath)) {
|
|
587
|
+
const childRel = path.join(dir, child).replace(/\\/g, '/');
|
|
588
|
+
const childAbs = path.join(absDirPath, child);
|
|
589
|
+
if (fs.statSync(childAbs).isFile() && !assets.has(childRel)) {
|
|
590
|
+
assets.add(childRel);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
319
596
|
// Copy assets into both dist dirs
|
|
320
597
|
let copiedCount = 0;
|
|
321
598
|
for (const asset of assets) {
|
|
@@ -370,6 +647,15 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
|
|
|
370
647
|
);
|
|
371
648
|
}
|
|
372
649
|
|
|
650
|
+
// Rewrite global CSS link to hashed version
|
|
651
|
+
if (globalCssOrigHref && globalCssHash) {
|
|
652
|
+
const escapedHref = globalCssOrigHref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
653
|
+
const cssLinkRe = new RegExp(
|
|
654
|
+
`(<link[^>]+href\\s*=\\s*["'])${escapedHref}(["'][^>]*>)`, 'i'
|
|
655
|
+
);
|
|
656
|
+
html = html.replace(cssLinkRe, `$1${globalCssHash}$2`);
|
|
657
|
+
}
|
|
658
|
+
|
|
373
659
|
const serverHtml = html;
|
|
374
660
|
const localHtml = html.replace(/<base\s+href\s*=\s*["']\/["'][^>]*>\s*\n?\s*/i, '');
|
|
375
661
|
|
|
@@ -387,19 +673,26 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
|
|
|
387
673
|
|
|
388
674
|
/**
|
|
389
675
|
* Copy the entire app directory into both dist/server and dist/local,
|
|
390
|
-
* skipping only build outputs
|
|
391
|
-
* assets (icons/, images/, fonts/,
|
|
392
|
-
* are available in the built output without maintaining
|
|
676
|
+
* skipping only build outputs, tooling dirs, and the app/ source dir.
|
|
677
|
+
* This ensures all static assets (assets/, icons/, images/, fonts/,
|
|
678
|
+
* manifests, etc.) are available in the built output without maintaining
|
|
679
|
+
* a fragile whitelist.
|
|
393
680
|
*/
|
|
394
681
|
function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
|
|
395
|
-
const SKIP_DIRS = new Set(['dist', 'node_modules', '.git', '.vscode'
|
|
682
|
+
const SKIP_DIRS = new Set(['dist', 'node_modules', '.git', '.vscode']);
|
|
683
|
+
|
|
684
|
+
// Case-insensitive match for App/ directory (contains bundled source)
|
|
685
|
+
function isAppDir(name) {
|
|
686
|
+
return name.toLowerCase() === 'app';
|
|
687
|
+
}
|
|
396
688
|
|
|
397
689
|
let copiedCount = 0;
|
|
398
690
|
|
|
399
691
|
function copyEntry(srcPath, relPath) {
|
|
400
692
|
const stat = fs.statSync(srcPath);
|
|
401
693
|
if (stat.isDirectory()) {
|
|
402
|
-
|
|
694
|
+
const dirName = path.basename(srcPath);
|
|
695
|
+
if (SKIP_DIRS.has(dirName) || isAppDir(dirName)) return;
|
|
403
696
|
for (const child of fs.readdirSync(srcPath)) {
|
|
404
697
|
copyEntry(path.join(srcPath, child), path.join(relPath, child));
|
|
405
698
|
}
|
|
@@ -418,7 +711,7 @@ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
|
|
|
418
711
|
|
|
419
712
|
for (const entry of fs.readdirSync(appRoot, { withFileTypes: true })) {
|
|
420
713
|
if (entry.isDirectory()) {
|
|
421
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
714
|
+
if (SKIP_DIRS.has(entry.name) || isAppDir(entry.name)) continue;
|
|
422
715
|
copyEntry(path.join(appRoot, entry.name), entry.name);
|
|
423
716
|
} else {
|
|
424
717
|
copyEntry(path.join(appRoot, entry.name), entry.name);
|
|
@@ -437,6 +730,7 @@ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
|
|
|
437
730
|
function bundleApp() {
|
|
438
731
|
const projectRoot = process.cwd();
|
|
439
732
|
const minimal = flag('minimal', 'm');
|
|
733
|
+
const globalCssOverride = option('global-css', null, null);
|
|
440
734
|
|
|
441
735
|
// Entry point — positional arg (directory or file) or auto-detection
|
|
442
736
|
let entry = null;
|
|
@@ -556,14 +850,18 @@ function bundleApp() {
|
|
|
556
850
|
console.log(` Output: ${path.relative(projectRoot, baseDistDir)}/server/ & local/`);
|
|
557
851
|
console.log(` Library: embedded`);
|
|
558
852
|
console.log(` HTML: ${htmlFile || 'not found (no HTML detected)'}`);
|
|
559
|
-
if (minimal) console.log(` Mode: minimal (HTML +
|
|
853
|
+
if (minimal) console.log(` Mode: minimal (HTML + JS + global CSS only)`);
|
|
560
854
|
console.log('');
|
|
561
855
|
|
|
562
856
|
// ------ doBuild (inlined) ------
|
|
563
857
|
const start = Date.now();
|
|
564
858
|
|
|
565
|
-
|
|
566
|
-
|
|
859
|
+
// Clean previous dist outputs for a fresh build
|
|
860
|
+
for (const dir of [serverDir, localDir]) {
|
|
861
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true });
|
|
862
|
+
}
|
|
863
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
864
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
567
865
|
|
|
568
866
|
const files = walkImportGraph(entry);
|
|
569
867
|
console.log(` Resolved ${files.length} module(s):`);
|
|
@@ -574,8 +872,9 @@ function bundleApp() {
|
|
|
574
872
|
code = stripModuleSyntax(code);
|
|
575
873
|
code = replaceImportMeta(code, file, projectRoot);
|
|
576
874
|
code = rewriteResourceUrls(code, file, projectRoot);
|
|
875
|
+
code = minifyTemplateLiterals(code);
|
|
577
876
|
const rel = path.relative(projectRoot, file);
|
|
578
|
-
return `// --- ${rel} ${'
|
|
877
|
+
return `// --- ${rel} ${'-'.repeat(Math.max(1, 60 - rel.length))}\n${code.trim()}`;
|
|
579
878
|
});
|
|
580
879
|
|
|
581
880
|
// Embed zquery.min.js
|
|
@@ -598,8 +897,24 @@ function bundleApp() {
|
|
|
598
897
|
}
|
|
599
898
|
|
|
600
899
|
const htmlDir = htmlAbs ? path.dirname(htmlAbs) : null;
|
|
900
|
+
|
|
901
|
+
// Case-insensitive search for Assets/ directory
|
|
902
|
+
function findAssetsDir(root) {
|
|
903
|
+
try {
|
|
904
|
+
for (const e of fs.readdirSync(root, { withFileTypes: true })) {
|
|
905
|
+
if (e.isDirectory() && e.name.toLowerCase() === 'assets') return path.join(root, e.name);
|
|
906
|
+
}
|
|
907
|
+
} catch { /* skip */ }
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const assetsDir = findAssetsDir(htmlDir || projectRoot);
|
|
912
|
+
const altAssetsDir = htmlDir ? findAssetsDir(projectRoot) : null;
|
|
913
|
+
|
|
601
914
|
const libCandidates = [
|
|
602
915
|
path.join(pkgRoot, 'dist/zquery.min.js'),
|
|
916
|
+
assetsDir && path.join(assetsDir, 'scripts/zquery.min.js'),
|
|
917
|
+
altAssetsDir && path.join(altAssetsDir, 'scripts/zquery.min.js'),
|
|
603
918
|
htmlDir && path.join(htmlDir, 'scripts/vendor/zquery.min.js'),
|
|
604
919
|
htmlDir && path.join(htmlDir, 'vendor/zquery.min.js'),
|
|
605
920
|
path.join(projectRoot, 'scripts/vendor/zquery.min.js'),
|
|
@@ -612,16 +927,16 @@ function bundleApp() {
|
|
|
612
927
|
|
|
613
928
|
if (libPath) {
|
|
614
929
|
const libBytes = fs.statSync(libPath).size;
|
|
615
|
-
libSection = `// --- zquery.min.js (library) ${'
|
|
616
|
-
+ `// --- Build-time metadata
|
|
930
|
+
libSection = `// --- zquery.min.js (library) ${'-'.repeat(34)}\n${fs.readFileSync(libPath, 'utf-8').trim()}\n\n`
|
|
931
|
+
+ `// --- Build-time metadata ${'-'.repeat(50)}\nif(typeof $!=="undefined"){$.meta=Object.assign($.meta||{},{libSize:${libBytes}});}\n\n`;
|
|
617
932
|
console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
|
|
618
933
|
} else {
|
|
619
934
|
console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
|
|
620
|
-
console.warn(` Place zquery.min.js in scripts
|
|
935
|
+
console.warn(` Place zquery.min.js in assets/scripts/, vendor/, lib/, or dist/`);
|
|
621
936
|
}
|
|
622
937
|
}
|
|
623
938
|
|
|
624
|
-
const banner = `/**\n * App bundle
|
|
939
|
+
const banner = `/**\n * App bundle - built by zQuery CLI\n * Entry: ${entryRel}\n * ${new Date().toISOString()}\n */`;
|
|
625
940
|
|
|
626
941
|
// Inline resources
|
|
627
942
|
const inlineMap = collectInlineResources(files, projectRoot);
|
|
@@ -635,7 +950,7 @@ function bundleApp() {
|
|
|
635
950
|
.replace(/\r/g, '');
|
|
636
951
|
return ` '${key}': '${escaped}'`;
|
|
637
952
|
});
|
|
638
|
-
inlineSection = `// --- Inlined resources (file:// support) ${'
|
|
953
|
+
inlineSection = `// --- Inlined resources (file:// support) ${'-'.repeat(20)}\nwindow.__zqInline = {\n${entries.join(',\n')}\n};\n\n`;
|
|
639
954
|
console.log(`\n Inlined ${Object.keys(inlineMap).length} external resource(s)`);
|
|
640
955
|
}
|
|
641
956
|
|
|
@@ -643,8 +958,7 @@ function bundleApp() {
|
|
|
643
958
|
|
|
644
959
|
// Content-hashed filenames
|
|
645
960
|
const contentHash = crypto.createHash('sha256').update(bundle).digest('hex').slice(0, 8);
|
|
646
|
-
const
|
|
647
|
-
const minBase = `z-${entryName}.${contentHash}.min.js`;
|
|
961
|
+
const minBase = `z-${entryName}.${contentHash}.min.js`;
|
|
648
962
|
|
|
649
963
|
// Clean previous builds
|
|
650
964
|
const escName = entryName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -657,21 +971,70 @@ function bundleApp() {
|
|
|
657
971
|
}
|
|
658
972
|
}
|
|
659
973
|
|
|
660
|
-
// Write
|
|
661
|
-
const
|
|
662
|
-
const minFile = path.join(serverDir, minBase);
|
|
663
|
-
fs.writeFileSync(bundleFile, bundle, 'utf-8');
|
|
974
|
+
// Write minified bundle
|
|
975
|
+
const minFile = path.join(serverDir, minBase);
|
|
664
976
|
fs.writeFileSync(minFile, minify(bundle, banner), 'utf-8');
|
|
665
|
-
fs.copyFileSync(bundleFile, path.join(localDir, bundleBase));
|
|
666
977
|
fs.copyFileSync(minFile, path.join(localDir, minBase));
|
|
667
978
|
|
|
668
|
-
console.log(`\n ✓ ${
|
|
669
|
-
|
|
979
|
+
console.log(`\n ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
|
|
980
|
+
|
|
981
|
+
// ------------------------------------------------------------------
|
|
982
|
+
// Global CSS bundling — extract from index.html <link> or --global-css
|
|
983
|
+
// ------------------------------------------------------------------
|
|
984
|
+
let globalCssHash = null;
|
|
985
|
+
let globalCssOrigHref = null;
|
|
986
|
+
let globalCssPath = null;
|
|
987
|
+
if (htmlAbs) {
|
|
988
|
+
const htmlContent = fs.readFileSync(htmlAbs, 'utf-8');
|
|
989
|
+
const htmlDir = path.dirname(htmlAbs);
|
|
990
|
+
|
|
991
|
+
// Determine global CSS path: --global-css flag overrides, else first <link rel="stylesheet"> in HTML
|
|
992
|
+
globalCssPath = null;
|
|
993
|
+
if (globalCssOverride) {
|
|
994
|
+
globalCssPath = path.resolve(projectRoot, globalCssOverride);
|
|
995
|
+
// Reconstruct relative href for HTML rewriting
|
|
996
|
+
globalCssOrigHref = path.relative(htmlDir, globalCssPath).replace(/\\/g, '/');
|
|
997
|
+
} else {
|
|
998
|
+
const linkRe = /<link[^>]+rel\s*=\s*["']stylesheet["'][^>]+href\s*=\s*["']([^"']+)["']/gi;
|
|
999
|
+
const altRe = /<link[^>]+href\s*=\s*["']([^"']+)["'][^>]+rel\s*=\s*["']stylesheet["']/gi;
|
|
1000
|
+
let linkMatch = linkRe.exec(htmlContent) || altRe.exec(htmlContent);
|
|
1001
|
+
if (linkMatch) {
|
|
1002
|
+
globalCssOrigHref = linkMatch[1];
|
|
1003
|
+
// Strip query string / fragment so the path resolves to the actual file
|
|
1004
|
+
const cleanHref = linkMatch[1].split('?')[0].split('#')[0];
|
|
1005
|
+
globalCssPath = path.resolve(htmlDir, cleanHref);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (globalCssPath && fs.existsSync(globalCssPath)) {
|
|
1010
|
+
let cssContent = fs.readFileSync(globalCssPath, 'utf-8');
|
|
1011
|
+
const cssMin = minifyCSS(cssContent);
|
|
1012
|
+
const cssHash = crypto.createHash('sha256').update(cssMin).digest('hex').slice(0, 8);
|
|
1013
|
+
const cssOutName = `global.${cssHash}.min.css`;
|
|
1014
|
+
|
|
1015
|
+
// Clean previous global CSS builds
|
|
1016
|
+
const cssCleanRe = /^global\.[a-f0-9]{8}\.min\.css$/;
|
|
1017
|
+
for (const dir of [serverDir, localDir]) {
|
|
1018
|
+
if (fs.existsSync(dir)) {
|
|
1019
|
+
for (const f of fs.readdirSync(dir)) {
|
|
1020
|
+
if (cssCleanRe.test(f)) fs.unlinkSync(path.join(dir, f));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
fs.writeFileSync(path.join(serverDir, cssOutName), cssMin, 'utf-8');
|
|
1026
|
+
fs.writeFileSync(path.join(localDir, cssOutName), cssMin, 'utf-8');
|
|
1027
|
+
globalCssHash = cssOutName;
|
|
1028
|
+
console.log(` ✓ ${cssOutName} (${sizeKB(Buffer.from(cssMin))} KB)`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
670
1031
|
|
|
671
|
-
// Rewrite HTML
|
|
1032
|
+
// Rewrite HTML to reference the minified bundle
|
|
672
1033
|
const bundledFileSet = new Set(files);
|
|
1034
|
+
// Skip the original unminified global CSS from static asset copying
|
|
1035
|
+
if (globalCssPath) bundledFileSet.add(path.resolve(globalCssPath));
|
|
673
1036
|
if (htmlFile) {
|
|
674
|
-
rewriteHtml(projectRoot, htmlFile,
|
|
1037
|
+
rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir, globalCssOrigHref, globalCssHash);
|
|
675
1038
|
}
|
|
676
1039
|
|
|
677
1040
|
// Copy static asset directories (icons/, images/, fonts/, etc.)
|
package/cli/commands/create.js
CHANGED
|
@@ -26,7 +26,7 @@ function createProject(args) {
|
|
|
26
26
|
const name = path.basename(target);
|
|
27
27
|
|
|
28
28
|
// Guard: refuse to overwrite existing files
|
|
29
|
-
const conflicts = ['index.html', '
|
|
29
|
+
const conflicts = ['index.html', 'global.css', 'app', 'assets'].filter(f =>
|
|
30
30
|
fs.existsSync(path.join(target, f))
|
|
31
31
|
);
|
|
32
32
|
if (conflicts.length) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/devtools/index.js — DevTools HTML assembler
|
|
3
|
+
*
|
|
4
|
+
* Reads CSS, HTML, and JS partials from this folder and concatenates them
|
|
5
|
+
* into a single self-contained HTML page served at /_devtools.
|
|
6
|
+
*
|
|
7
|
+
* Communication:
|
|
8
|
+
* - window.opener: direct DOM access (same-origin popup)
|
|
9
|
+
* - BroadcastChannel('__zq_devtools'): cross-tab fallback
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { readFileSync } = require('fs');
|
|
15
|
+
const { join } = require('path');
|
|
16
|
+
|
|
17
|
+
const dir = __dirname;
|
|
18
|
+
const read = (f) => readFileSync(join(dir, f), 'utf8');
|
|
19
|
+
|
|
20
|
+
const css = read('styles.css');
|
|
21
|
+
const html = read('panel.html');
|
|
22
|
+
|
|
23
|
+
const jsFiles = [
|
|
24
|
+
'js/core.js',
|
|
25
|
+
'js/tabs.js',
|
|
26
|
+
'js/source.js',
|
|
27
|
+
'js/elements.js',
|
|
28
|
+
'js/network.js',
|
|
29
|
+
'js/components.js',
|
|
30
|
+
'js/performance.js',
|
|
31
|
+
'js/router.js',
|
|
32
|
+
'js/stats.js'
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const js = jsFiles.map(read).join('\n\n');
|
|
36
|
+
|
|
37
|
+
module.exports = `<!DOCTYPE html>
|
|
38
|
+
<html lang="en">
|
|
39
|
+
<head>
|
|
40
|
+
<meta charset="UTF-8">
|
|
41
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
42
|
+
<title>zQuery DevTools</title>
|
|
43
|
+
<style>
|
|
44
|
+
${css}
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
${html}
|
|
49
|
+
<script>
|
|
50
|
+
(function() {
|
|
51
|
+
'use strict';
|
|
52
|
+
${js}
|
|
53
|
+
})();
|
|
54
|
+
</script>
|
|
55
|
+
</body>
|
|
56
|
+
</html>`;
|