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.
Files changed (72) hide show
  1. package/README.md +39 -29
  2. package/cli/commands/build.js +113 -4
  3. package/cli/commands/bundle.js +392 -29
  4. package/cli/commands/create.js +1 -1
  5. package/cli/commands/dev/devtools/index.js +56 -0
  6. package/cli/commands/dev/devtools/js/components.js +49 -0
  7. package/cli/commands/dev/devtools/js/core.js +409 -0
  8. package/cli/commands/dev/devtools/js/elements.js +413 -0
  9. package/cli/commands/dev/devtools/js/network.js +166 -0
  10. package/cli/commands/dev/devtools/js/performance.js +73 -0
  11. package/cli/commands/dev/devtools/js/router.js +105 -0
  12. package/cli/commands/dev/devtools/js/source.js +132 -0
  13. package/cli/commands/dev/devtools/js/stats.js +35 -0
  14. package/cli/commands/dev/devtools/js/tabs.js +79 -0
  15. package/cli/commands/dev/devtools/panel.html +95 -0
  16. package/cli/commands/dev/devtools/styles.css +244 -0
  17. package/cli/commands/dev/index.js +29 -4
  18. package/cli/commands/dev/logger.js +6 -1
  19. package/cli/commands/dev/overlay.js +428 -2
  20. package/cli/commands/dev/server.js +42 -5
  21. package/cli/commands/dev/watcher.js +59 -1
  22. package/cli/help.js +8 -5
  23. package/cli/scaffold/{scripts → app}/app.js +16 -23
  24. package/cli/scaffold/{scripts → app}/components/about.js +4 -4
  25. package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
  26. package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -7
  27. package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +3 -3
  28. package/cli/scaffold/app/components/home.js +137 -0
  29. package/cli/scaffold/{scripts → app}/routes.js +1 -1
  30. package/cli/scaffold/{scripts → app}/store.js +6 -6
  31. package/cli/scaffold/assets/.gitkeep +0 -0
  32. package/cli/scaffold/{styles/styles.css → global.css} +4 -2
  33. package/cli/scaffold/index.html +12 -11
  34. package/cli/utils.js +111 -6
  35. package/dist/zquery.dist.zip +0 -0
  36. package/dist/zquery.js +1122 -158
  37. package/dist/zquery.min.js +3 -16
  38. package/index.d.ts +129 -1290
  39. package/index.js +15 -10
  40. package/package.json +7 -6
  41. package/src/component.js +172 -49
  42. package/src/core.js +359 -18
  43. package/src/diff.js +256 -58
  44. package/src/expression.js +33 -3
  45. package/src/reactive.js +37 -5
  46. package/src/router.js +243 -7
  47. package/tests/component.test.js +886 -0
  48. package/tests/core.test.js +977 -0
  49. package/tests/diff.test.js +525 -0
  50. package/tests/errors.test.js +162 -0
  51. package/tests/expression.test.js +482 -0
  52. package/tests/http.test.js +289 -0
  53. package/tests/reactive.test.js +339 -0
  54. package/tests/router.test.js +649 -0
  55. package/tests/store.test.js +379 -0
  56. package/tests/utils.test.js +512 -0
  57. package/types/collection.d.ts +383 -0
  58. package/types/component.d.ts +217 -0
  59. package/types/errors.d.ts +103 -0
  60. package/types/http.d.ts +81 -0
  61. package/types/misc.d.ts +179 -0
  62. package/types/reactive.d.ts +76 -0
  63. package/types/router.d.ts +161 -0
  64. package/types/ssr.d.ts +49 -0
  65. package/types/store.d.ts +107 -0
  66. package/types/utils.d.ts +142 -0
  67. package/cli/commands/dev.old.js +0 -520
  68. package/cli/scaffold/scripts/components/home.js +0 -137
  69. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
  70. /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
  71. /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
  72. /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
@@ -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 styleMatch = code.match(/styleUrl\s*:\s*['"]([^'"]+)['"]/);
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 and tooling dirs. This ensures all static
391
- * assets (icons/, images/, fonts/, styles/, scripts/, manifests, etc.)
392
- * are available in the built output without maintaining a fragile whitelist.
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', 'scripts']);
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
- if (SKIP_DIRS.has(path.basename(srcPath))) return;
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 + bundle only)`);
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
- if (!fs.existsSync(serverDir)) fs.mkdirSync(serverDir, { recursive: true });
566
- if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
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} ${''.repeat(Math.max(1, 60 - rel.length))}\n${code.trim()}`;
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) ${''.repeat(34)}\n${fs.readFileSync(libPath, 'utf-8').trim()}\n\n`
616
- + `// --- Build-time metadata ————————————————————————————\nif(typeof $!=="undefined"){$.meta=Object.assign($.meta||{},{libSize:${libBytes}});}\n\n`;
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/vendor/, vendor/, lib/, or dist/`);
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 built by zQuery CLI\n * Entry: ${entryRel}\n * ${new Date().toISOString()}\n */`;
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) ${''.repeat(20)}\nwindow.__zqInline = {\n${entries.join(',\n')}\n};\n\n`;
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 bundleBase = `z-${entryName}.${contentHash}.js`;
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 bundles
661
- const bundleFile = path.join(serverDir, bundleBase);
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 ✓ ${bundleBase} (${sizeKB(fs.readFileSync(bundleFile))} KB)`);
669
- console.log(` ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
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 (use full bundle minified version mangles template literal whitespace)
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, bundleFile, true, bundledFileSet, serverDir, localDir);
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.)
@@ -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', 'scripts'].filter(f =>
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>`;