zero-query 0.7.5 → 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 (64) hide show
  1. package/README.md +37 -27
  2. package/cli/commands/build.js +110 -1
  3. package/cli/commands/bundle.js +107 -22
  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 +28 -3
  18. package/cli/commands/dev/logger.js +6 -1
  19. package/cli/commands/dev/overlay.js +377 -0
  20. package/cli/commands/dev/server.js +8 -0
  21. package/cli/commands/dev/watcher.js +26 -1
  22. package/cli/help.js +8 -5
  23. package/cli/scaffold/{scripts → app}/app.js +1 -1
  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/app/components/home.js +137 -0
  27. package/cli/scaffold/{scripts → app}/routes.js +1 -1
  28. package/cli/scaffold/{scripts → app}/store.js +6 -6
  29. package/cli/scaffold/assets/.gitkeep +0 -0
  30. package/cli/scaffold/{styles/styles.css → global.css} +3 -2
  31. package/cli/scaffold/index.html +11 -11
  32. package/dist/zquery.dist.zip +0 -0
  33. package/dist/zquery.js +746 -134
  34. package/dist/zquery.min.js +2 -2
  35. package/index.d.ts +11 -9
  36. package/index.js +15 -10
  37. package/package.json +3 -2
  38. package/src/component.js +161 -48
  39. package/src/core.js +57 -11
  40. package/src/diff.js +256 -58
  41. package/src/expression.js +33 -3
  42. package/src/reactive.js +37 -5
  43. package/src/router.js +195 -6
  44. package/tests/component.test.js +582 -0
  45. package/tests/core.test.js +251 -0
  46. package/tests/diff.test.js +333 -2
  47. package/tests/expression.test.js +148 -0
  48. package/tests/http.test.js +108 -0
  49. package/tests/reactive.test.js +148 -0
  50. package/tests/router.test.js +317 -0
  51. package/tests/store.test.js +126 -0
  52. package/tests/utils.test.js +161 -2
  53. package/types/collection.d.ts +17 -2
  54. package/types/component.d.ts +7 -0
  55. package/types/misc.d.ts +13 -0
  56. package/types/router.d.ts +30 -1
  57. package/cli/commands/dev.old.js +0 -520
  58. package/cli/scaffold/scripts/components/home.js +0 -137
  59. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -0
  60. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +0 -0
  61. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
  62. /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
  63. /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
  64. /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
package/README.md CHANGED
@@ -15,19 +15,21 @@
15
15
 
16
16
  </p>
17
17
 
18
- > **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~80 KB minified browser bundle. Works out of the box with ES modules. An optional CLI bundler is available for single-file production builds.**
18
+ > **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~91 KB minified browser bundle. Works out of the box with ES modules. An optional CLI bundler is available for single-file production builds.**
19
19
 
20
20
  ## Features
21
21
 
22
22
  | Module | Highlights |
23
23
  | --- | --- |
24
- | **Router** | History & hash mode, route params (`:id`), guards, lazy loading, `z-link` navigation, `z-to-top` scroll modifier (`instant`/`smooth`) |
25
- | **Components** | Reactive state, template literals, `@event` delegation (8 modifiers), `z-model` two-way binding, computed properties, watch callbacks, slot-based content projection, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`), DOM morphing engine (no innerHTML rebuild), CSP-safe expression evaluation, scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles |
26
- | **Store** | Reactive global state, named actions, computed getters, middleware, subscriptions |
27
- | **HTTP** | Fetch wrapper with auto-JSON, interceptors, timeout/abort, base URL |
28
- | **Reactive** | Deep proxy reactivity, Signals, computed values, effects |
24
+ | **Components** | Reactive state, template literals, `@event` delegation (8 modifiers), `z-model` two-way binding, computed properties, watch callbacks, slot-based content projection, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`, `z-skip`), DOM morphing engine with LIS-based keyed reconciliation (no innerHTML rebuild), CSP-safe expression evaluation with AST caching, scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles |
25
+ | **Router** | History & hash mode, route params (`:id`), wildcards, guards (`beforeEach`/`afterEach`), lazy loading, `z-link` navigation, `z-to-top` scroll modifier (`instant`/`smooth`), sub-route history substates (`pushSubstate`/`onSubstate`) |
26
+ | **Directives** | `z-if`, `z-for`, `z-model`, `z-show`, `z-bind`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`, `z-skip` &mdash; 17 built-in template directives |
27
+ | **Reactive** | Deep proxy reactivity, Signals (`.value`, `.peek()`), computed values, effects (auto-tracked with dispose) |
28
+ | **Store** | Reactive global state, named actions, computed getters, middleware, subscriptions, time-travel (undo/redo/history) |
29
29
  | **Selectors & DOM** | jQuery-like chainable selectors, traversal, DOM manipulation, events, animation |
30
+ | **HTTP** | Fetch wrapper with auto-JSON, interceptors, timeout/abort, base URL |
30
31
  | **Utils** | debounce, throttle, pipe, once, sleep, escapeHtml, uuid, deepClone, deepMerge, storage/session wrappers, event bus |
32
+ | **Dev Tools** | CLI dev server with live-reload, CSS hot-swap, full-screen error overlay, floating toolbar, dark-themed inspector panel (Router view, DOM tree, network log, component viewer, performance dashboard), fetch interceptor, render instrumentation, CLI bundler for single-file production builds |
31
33
 
32
34
  ---
33
35
 
@@ -61,15 +63,19 @@ The dev server includes a **full-screen error overlay** that surfaces errors dir
61
63
  - **Runtime errors** — uncaught exceptions and unhandled promise rejections are captured and displayed in the same overlay with a cleaned-up stack trace.
62
64
  - The overlay **auto-clears** when you fix the error and save. Press `Esc` or click `×` to dismiss manually.
63
65
 
66
+ #### Floating Toolbar & Inspector
67
+
68
+ A compact toolbar appears in the bottom-right corner showing live request/render counters and a DOM button. Click any counter to open a **dark-themed DevTools inspector** as a popup — or visit `http://localhost:<port>/_devtools` for a standalone split-view panel with four tabs: **Elements** (live DOM tree with component badges, source viewer, expand/collapse), **Network** (fetch log with JSON viewer), **Components** (live state cards), and **Performance** (render timeline with timing metrics).
69
+
64
70
  ### Alternative: Manual Setup (No npm)
65
71
 
66
- If you prefer **zero tooling**, download `dist/zQuery.min.js` from the [GitHub releases](https://github.com/tonywied17/zero-query/releases/tag/RELEASE) and drop it into `scripts/vendor/`. Then open `index.html` directly in a browser — no Node.js required.
72
+ If you prefer **zero tooling**, download `dist/zquery.min.js` from the [dist/ folder](https://github.com/tonywied17/zero-query/tree/main/dist) and drop it into your project root or `assets/scripts/`. Then open `index.html` directly in a browser — no Node.js required.
67
73
 
68
74
  ```bash
69
75
  git clone https://github.com/tonywied17/zero-query.git
70
76
  cd zero-query
71
77
  npx zquery build
72
- # → dist/zQuery.min.js (~80 KB)
78
+ # → dist/zquery.min.js (~91 KB)
73
79
  ```
74
80
 
75
81
  ### Include in HTML
@@ -80,9 +86,9 @@ npx zquery build
80
86
  <head>
81
87
  <meta charset="UTF-8">
82
88
  <title>My App</title>
83
- <link rel="stylesheet" href="styles/styles.css">
84
- <script src="scripts/vendor/zQuery.min.js"></script>
85
- <script type="module" src="scripts/app.js"></script>
89
+ <link rel="stylesheet" href="global.css">
90
+ <script src="zquery.min.js"></script>
91
+ <script type="module" src="app/app.js"></script>
86
92
  </head>
87
93
  <body>
88
94
  <nav>
@@ -97,7 +103,7 @@ npx zquery build
97
103
  ### Boot Your App
98
104
 
99
105
  ```js
100
- // scripts/app.js
106
+ // app/app.js
101
107
  import './components/home.js';
102
108
  import './components/about.js';
103
109
  import './components/contacts/contacts.js';
@@ -109,7 +115,7 @@ $.router({ el: '#app', routes, fallback: 'not-found' });
109
115
  ### Define a Component
110
116
 
111
117
  ```js
112
- // scripts/components/home.js
118
+ // app/components/home.js
113
119
  $.component('home-page', {
114
120
  state: () => ({ count: 0 }),
115
121
  increment() { this.state.count++; },
@@ -132,9 +138,8 @@ That's it — a fully working SPA with the dev server's live-reload.
132
138
  ```
133
139
  my-app/
134
140
  index.html
135
- scripts/
136
- vendor/
137
- zQuery.min.js ← only needed for manual setup; dev server auto-resolves
141
+ global.css
142
+ app/
138
143
  app.js
139
144
  routes.js
140
145
  store.js
@@ -149,14 +154,17 @@ my-app/
149
154
  contacts.js
150
155
  contacts.html
151
156
  contacts.css
152
- styles/
153
- styles.css
157
+ assets/
158
+ scripts/ ← third-party JS (e.g. zquery.min.js for manual setup)
159
+ styles/ ← additional stylesheets, fonts, etc.
154
160
  ```
155
161
 
156
162
  - One component per file inside `components/`.
157
163
  - Names **must contain a hyphen** (Web Component convention): `home-page`, `app-counter`, etc.
158
164
  - Components with external templates or styles can use a subfolder (e.g. `contacts/contacts.js` + `contacts.html` + `contacts.css`).
159
165
  - `app.js` is the single entry point — import components, create the store, and boot the router.
166
+ - `global.css` lives next to `index.html` for easy access; the bundler hashes it into `global.<hash>.min.css` for production.
167
+ - `assets/` holds static files that get copied to `dist/` as-is.
160
168
 
161
169
  ---
162
170
 
@@ -172,7 +180,7 @@ npx zquery bundle
172
180
  npx zquery bundle my-app/
173
181
 
174
182
  # Or pass a direct entry file (skips auto-detection)
175
- npx zquery bundle my-app/scripts/main.js
183
+ npx zquery bundle my-app/app/main.js
176
184
  ```
177
185
 
178
186
  Output goes to `dist/` next to your `index.html`:
@@ -183,7 +191,8 @@ dist/
183
191
  index.html
184
192
  z-app.<hash>.js
185
193
  z-app.<hash>.min.js
186
- styles/
194
+ global.<hash>.min.css
195
+ assets/
187
196
  local/ ← open from disk (file://) — no server needed
188
197
  index.html
189
198
  z-app.<hash>.js
@@ -196,7 +205,8 @@ dist/
196
205
  | --- | --- | --- |
197
206
  | `--out <path>` | `-o` | Custom output directory |
198
207
  | `--index <file>` | `-i` | Index HTML file (default: auto-detected) |
199
- | `--minimal` | `-m` | Only output HTML + bundled JS (skip static assets) |
208
+ | `--minimal` | `-m` | Only output HTML, bundled JS, and global CSS (skip static assets) |
209
+ | `--global-css <path>` | | Override global CSS input file (default: first `<link>` in HTML) |
200
210
 
201
211
  ### What the Bundler Does
202
212
 
@@ -204,7 +214,7 @@ dist/
204
214
  1. **HTML files** — `index.html` is checked first, then other `.html` files (root + one level deep).
205
215
  2. **Module scripts within HTML** — within each HTML file, a `<script type="module">` whose `src` resolves to `app.js` wins; otherwise the first module script tag is used.
206
216
  3. **JS file scan** — if no HTML match, JS files (up to 2 levels deep) are scanned in two passes: first for `$.router(` (the canonical app entry point), then for `$.mount(`, `$.store(`, or `mountAll(`.
207
- 4. **Convention fallbacks** — `scripts/app.js`, `src/app.js`, `js/app.js`, `app.js`, `main.js`.
217
+ 4. **Convention fallbacks** — `app/app.js`, `scripts/app.js`, `src/app.js`, `js/app.js`, `app.js`, `main.js`.
208
218
  2. Resolves all `import` statements and topologically sorts dependencies
209
219
  3. Strips `import`/`export` syntax, wraps in an IIFE
210
220
  4. Embeds zQuery library and inlines `templateUrl` / `styleUrl` / `pages` files
@@ -246,8 +256,8 @@ location / {
246
256
  | `$.create` | Element factory |
247
257
  | `$.ready` `$.on` `$.off` | DOM ready, global event delegation & direct listeners |
248
258
  | `$.fn` | Collection prototype (extend it) |
249
- | `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` | Component system |
250
- | `$.morph` | DOM morphing engine — patch existing DOM to match new HTML without destroying unchanged nodes |
259
+ | `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` `$.prefetch` | Component system |
260
+ | `$.morph` `$.morphElement` | DOM morphing engine — LIS-based keyed reconciliation, `isEqualNode()` bail-outs, `z-skip` opt-out. Patches existing DOM to match new HTML without destroying unchanged nodes. Auto-key detection (`id`, `data-id`, `data-key`) — no `z-key` required. `$().html()` and `$().replaceWith()` auto-morph existing content; `$().morph()` for explicit morph |
251
261
  | `$.safeEval` | CSP-safe expression evaluator (replaces `eval` / `new Function`) |
252
262
  | `$.style` | Dynamically load global stylesheet file(s) at runtime |
253
263
  | `$.router` `$.getRouter` | SPA router |
@@ -260,16 +270,16 @@ location / {
260
270
  | `$.param` `$.parseQuery` | URL utils |
261
271
  | `$.storage` `$.session` | Storage wrappers |
262
272
  | `$.bus` | Event bus |
263
- | `$.version` | Library version |
273
+ | `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~91 KB\"`) |
264
274
  | `$.meta` | Build metadata (populated by CLI bundler) |
265
275
  | `$.noConflict` | Release `$` global |
266
276
 
267
277
  | CLI Command | Description |
268
278
  | --- | --- |
269
279
  | `zquery create [dir]` | Scaffold a new project (index.html, components, store, styles) |
270
- | `zquery dev [root]` | Dev server with live-reload &amp; error overlay (port 3100). `--index` for custom HTML. |
280
+ | `zquery dev [root]` | Dev server with live-reload, CSS hot-swap, error overlay, floating toolbar &amp; inspector panel (port 3100). Visit `/_devtools` for the standalone panel. `--index` for custom HTML, `--bundle` for bundled mode, `--no-intercept` to skip CDN intercept. |
271
281
  | `zquery bundle [dir\|file]` | Bundle app into a single IIFE file. Accepts dir or direct entry file. |
272
- | `zquery build` | Build the zQuery library (`dist/zQuery.min.js`) |
282
+ | `zquery build` | Build the zQuery library (`dist/zquery.min.js`) |
273
283
  | `zquery --help` | Show CLI usage |
274
284
 
275
285
  For full method signatures, options, and examples, see **[API.md](API.md)**.
@@ -9,6 +9,7 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
+ const zlib = require('zlib');
12
13
  const { minify, sizeKB } = require('../utils');
13
14
 
14
15
  function buildLibrary() {
@@ -47,13 +48,121 @@ function buildLibrary() {
47
48
  const bundle = `${banner}\n(function(global) {\n 'use strict';\n\n${parts.join('\n\n')}\n\n// --- index.js (assembly) ${'-'.repeat(42)}\n${indexCode.trim().replace("'__VERSION__'", `'${VERSION}'`)}\n\n})(typeof window !== 'undefined' ? window : globalThis);\n`;
48
49
 
49
50
  fs.writeFileSync(OUT_FILE, bundle, 'utf-8');
50
- fs.writeFileSync(MIN_FILE, minify(bundle, banner), 'utf-8');
51
+ const minified = minify(bundle, banner);
52
+ fs.writeFileSync(MIN_FILE, minified, 'utf-8');
53
+
54
+ // Inject actual minified library size into both outputs
55
+ const libSizeKB = Math.round(Buffer.from(minified).length / 1024);
56
+ const libSizeStr = `~${libSizeKB} KB`;
57
+ const outContent = fs.readFileSync(OUT_FILE, 'utf-8').replace("'__LIB_SIZE__'", `'${libSizeStr}'`);
58
+ const minContent = minified.replace("'__LIB_SIZE__'", `'${libSizeStr}'`);
59
+ fs.writeFileSync(OUT_FILE, outContent, 'utf-8');
60
+ fs.writeFileSync(MIN_FILE, minContent, 'utf-8');
51
61
 
52
62
  const elapsed = Date.now() - start;
53
63
  console.log(` ✓ dist/zquery.js (${sizeKB(fs.readFileSync(OUT_FILE))} KB)`);
54
64
  console.log(` ✓ dist/zquery.min.js (${sizeKB(fs.readFileSync(MIN_FILE))} KB)`);
55
65
  console.log(` Done in ${elapsed}ms\n`);
56
66
 
67
+ // --- Create dist/zquery.dist.zip -----------------------------------------
68
+ const root = process.cwd();
69
+ const zipFiles = [
70
+ { src: OUT_FILE, name: 'zquery.js' },
71
+ { src: MIN_FILE, name: 'zquery.min.js' },
72
+ { src: path.join(root, 'LICENSE'), name: 'LICENSE' },
73
+ { src: path.join(root, 'API.md'), name: 'API.md' },
74
+ { src: path.join(root, 'README.md'), name: 'README.md' },
75
+ ];
76
+
77
+ // Minimal ZIP builder (deflate via zlib, no external deps)
78
+ function buildZip(entries) {
79
+ const localHeaders = [];
80
+ const centralHeaders = [];
81
+ let offset = 0;
82
+
83
+ for (const { name, data } of entries) {
84
+ const nameBytes = Buffer.from(name, 'utf-8');
85
+ const compressed = zlib.deflateRawSync(data, { level: 9 });
86
+ const crc = crc32(data);
87
+ const compLen = compressed.length;
88
+ const uncompLen = data.length;
89
+
90
+ // Local file header
91
+ const local = Buffer.alloc(30 + nameBytes.length);
92
+ local.writeUInt32LE(0x04034b50, 0); // signature
93
+ local.writeUInt16LE(20, 4); // version needed
94
+ local.writeUInt16LE(0, 6); // flags
95
+ local.writeUInt16LE(8, 8); // compression: deflate
96
+ local.writeUInt16LE(0, 10); // mod time
97
+ local.writeUInt16LE(0, 12); // mod date
98
+ local.writeUInt32LE(crc, 14);
99
+ local.writeUInt32LE(compLen, 18);
100
+ local.writeUInt32LE(uncompLen, 22);
101
+ local.writeUInt16LE(nameBytes.length, 26);
102
+ local.writeUInt16LE(0, 28); // extra field length
103
+ nameBytes.copy(local, 30);
104
+
105
+ localHeaders.push(Buffer.concat([local, compressed]));
106
+
107
+ // Central directory header
108
+ const central = Buffer.alloc(46 + nameBytes.length);
109
+ central.writeUInt32LE(0x02014b50, 0);
110
+ central.writeUInt16LE(20, 4); // version made by
111
+ central.writeUInt16LE(20, 6); // version needed
112
+ central.writeUInt16LE(0, 8); // flags
113
+ central.writeUInt16LE(8, 10); // compression: deflate
114
+ central.writeUInt16LE(0, 12); // mod time
115
+ central.writeUInt16LE(0, 14); // mod date
116
+ central.writeUInt32LE(crc, 16);
117
+ central.writeUInt32LE(compLen, 20);
118
+ central.writeUInt32LE(uncompLen, 24);
119
+ central.writeUInt16LE(nameBytes.length, 28);
120
+ central.writeUInt16LE(0, 30); // extra field length
121
+ central.writeUInt16LE(0, 32); // comment length
122
+ central.writeUInt16LE(0, 34); // disk number
123
+ central.writeUInt16LE(0, 36); // internal attrs
124
+ central.writeUInt32LE(0, 38); // external attrs
125
+ central.writeUInt32LE(offset, 42); // local header offset
126
+ nameBytes.copy(central, 46);
127
+ centralHeaders.push(central);
128
+
129
+ offset += local.length + compressed.length;
130
+ }
131
+
132
+ const centralDir = Buffer.concat(centralHeaders);
133
+ const eocd = Buffer.alloc(22);
134
+ eocd.writeUInt32LE(0x06054b50, 0);
135
+ eocd.writeUInt16LE(0, 4); // disk number
136
+ eocd.writeUInt16LE(0, 6); // central dir disk
137
+ eocd.writeUInt16LE(entries.length, 8); // entries on disk
138
+ eocd.writeUInt16LE(entries.length, 10); // total entries
139
+ eocd.writeUInt32LE(centralDir.length, 12);
140
+ eocd.writeUInt32LE(offset, 16); // central dir offset
141
+ eocd.writeUInt16LE(0, 20); // comment length
142
+
143
+ return Buffer.concat([...localHeaders, centralDir, eocd]);
144
+ }
145
+
146
+ function crc32(buf) {
147
+ let crc = 0xFFFFFFFF;
148
+ for (let i = 0; i < buf.length; i++) {
149
+ crc ^= buf[i];
150
+ for (let j = 0; j < 8; j++) {
151
+ crc = (crc >>> 1) ^ ((crc & 1) ? 0xEDB88320 : 0);
152
+ }
153
+ }
154
+ return (crc ^ 0xFFFFFFFF) >>> 0;
155
+ }
156
+
157
+ const entries = zipFiles
158
+ .filter(f => fs.existsSync(f.src))
159
+ .map(f => ({ name: f.name, data: fs.readFileSync(f.src) }));
160
+
161
+ const zipBuf = buildZip(entries);
162
+ const zipPath = path.join(DIST, 'zquery.dist.zip');
163
+ fs.writeFileSync(zipPath, zipBuf);
164
+ console.log(` ✓ dist/zquery.dist.zip (${sizeKB(zipBuf)} KB) — ${entries.length} files`);
165
+
57
166
  return { DIST, OUT_FILE, MIN_FILE };
58
167
  }
59
168
 
@@ -521,7 +521,7 @@ function detectEntry(projectRoot) {
521
521
  }
522
522
 
523
523
  // 3. Convention fallbacks
524
- 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'];
525
525
  for (const f of fallbacks) {
526
526
  const fp = path.join(projectRoot, f);
527
527
  if (fs.existsSync(fp)) return fp;
@@ -539,7 +539,7 @@ function detectEntry(projectRoot) {
539
539
  * server/index.html — <base href="/"> for SPA deep routes
540
540
  * local/index.html — relative paths for file:// access
541
541
  */
542
- function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir) {
542
+ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir, globalCssOrigHref, globalCssHash) {
543
543
  const htmlPath = path.resolve(projectRoot, htmlRelPath);
544
544
  if (!fs.existsSync(htmlPath)) {
545
545
  console.warn(` ⚠ HTML file not found: ${htmlRelPath}`);
@@ -647,6 +647,15 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
647
647
  );
648
648
  }
649
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
+
650
659
  const serverHtml = html;
651
660
  const localHtml = html.replace(/<base\s+href\s*=\s*["']\/["'][^>]*>\s*\n?\s*/i, '');
652
661
 
@@ -664,19 +673,26 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
664
673
 
665
674
  /**
666
675
  * Copy the entire app directory into both dist/server and dist/local,
667
- * skipping only build outputs and tooling dirs. This ensures all static
668
- * assets (icons/, images/, fonts/, styles/, scripts/, manifests, etc.)
669
- * 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.
670
680
  */
671
681
  function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
672
- 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
+ }
673
688
 
674
689
  let copiedCount = 0;
675
690
 
676
691
  function copyEntry(srcPath, relPath) {
677
692
  const stat = fs.statSync(srcPath);
678
693
  if (stat.isDirectory()) {
679
- if (SKIP_DIRS.has(path.basename(srcPath))) return;
694
+ const dirName = path.basename(srcPath);
695
+ if (SKIP_DIRS.has(dirName) || isAppDir(dirName)) return;
680
696
  for (const child of fs.readdirSync(srcPath)) {
681
697
  copyEntry(path.join(srcPath, child), path.join(relPath, child));
682
698
  }
@@ -695,7 +711,7 @@ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
695
711
 
696
712
  for (const entry of fs.readdirSync(appRoot, { withFileTypes: true })) {
697
713
  if (entry.isDirectory()) {
698
- if (SKIP_DIRS.has(entry.name)) continue;
714
+ if (SKIP_DIRS.has(entry.name) || isAppDir(entry.name)) continue;
699
715
  copyEntry(path.join(appRoot, entry.name), entry.name);
700
716
  } else {
701
717
  copyEntry(path.join(appRoot, entry.name), entry.name);
@@ -714,6 +730,7 @@ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
714
730
  function bundleApp() {
715
731
  const projectRoot = process.cwd();
716
732
  const minimal = flag('minimal', 'm');
733
+ const globalCssOverride = option('global-css', null, null);
717
734
 
718
735
  // Entry point — positional arg (directory or file) or auto-detection
719
736
  let entry = null;
@@ -833,14 +850,18 @@ function bundleApp() {
833
850
  console.log(` Output: ${path.relative(projectRoot, baseDistDir)}/server/ & local/`);
834
851
  console.log(` Library: embedded`);
835
852
  console.log(` HTML: ${htmlFile || 'not found (no HTML detected)'}`);
836
- if (minimal) console.log(` Mode: minimal (HTML + bundle only)`);
853
+ if (minimal) console.log(` Mode: minimal (HTML + JS + global CSS only)`);
837
854
  console.log('');
838
855
 
839
856
  // ------ doBuild (inlined) ------
840
857
  const start = Date.now();
841
858
 
842
- if (!fs.existsSync(serverDir)) fs.mkdirSync(serverDir, { recursive: true });
843
- 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 });
844
865
 
845
866
  const files = walkImportGraph(entry);
846
867
  console.log(` Resolved ${files.length} module(s):`);
@@ -876,8 +897,24 @@ function bundleApp() {
876
897
  }
877
898
 
878
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
+
879
914
  const libCandidates = [
880
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'),
881
918
  htmlDir && path.join(htmlDir, 'scripts/vendor/zquery.min.js'),
882
919
  htmlDir && path.join(htmlDir, 'vendor/zquery.min.js'),
883
920
  path.join(projectRoot, 'scripts/vendor/zquery.min.js'),
@@ -895,7 +932,7 @@ function bundleApp() {
895
932
  console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
896
933
  } else {
897
934
  console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
898
- 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/`);
899
936
  }
900
937
  }
901
938
 
@@ -921,8 +958,7 @@ function bundleApp() {
921
958
 
922
959
  // Content-hashed filenames
923
960
  const contentHash = crypto.createHash('sha256').update(bundle).digest('hex').slice(0, 8);
924
- const bundleBase = `z-${entryName}.${contentHash}.js`;
925
- const minBase = `z-${entryName}.${contentHash}.min.js`;
961
+ const minBase = `z-${entryName}.${contentHash}.min.js`;
926
962
 
927
963
  // Clean previous builds
928
964
  const escName = entryName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -935,21 +971,70 @@ function bundleApp() {
935
971
  }
936
972
  }
937
973
 
938
- // Write bundles
939
- const bundleFile = path.join(serverDir, bundleBase);
940
- const minFile = path.join(serverDir, minBase);
941
- fs.writeFileSync(bundleFile, bundle, 'utf-8');
974
+ // Write minified bundle
975
+ const minFile = path.join(serverDir, minBase);
942
976
  fs.writeFileSync(minFile, minify(bundle, banner), 'utf-8');
943
- fs.copyFileSync(bundleFile, path.join(localDir, bundleBase));
944
977
  fs.copyFileSync(minFile, path.join(localDir, minBase));
945
978
 
946
- console.log(`\n ✓ ${bundleBase} (${sizeKB(fs.readFileSync(bundleFile))} KB)`);
947
- 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
+ }
948
1031
 
949
1032
  // Rewrite HTML to reference the minified bundle
950
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));
951
1036
  if (htmlFile) {
952
- rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir);
1037
+ rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir, globalCssOrigHref, globalCssHash);
953
1038
  }
954
1039
 
955
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>`;
@@ -0,0 +1,49 @@
1
+ // ===================================================================
2
+ // Components
3
+ // ===================================================================
4
+ function renderComponents() {
5
+ var list = document.getElementById('comp-list');
6
+ if (!targetWin || !targetWin.$) {
7
+ list.innerHTML = '<div class="empty-state">Waiting for zQuery to load...</div>';
8
+ return;
9
+ }
10
+ var $ = targetWin.$;
11
+ var html = '';
12
+
13
+ // Iterate mounted components if components registry exists
14
+ if ($ && typeof $.components === 'function') {
15
+ var names = Object.keys($.components());
16
+ if (!names.length) {
17
+ list.innerHTML = '<div class="empty-state">No components registered</div>';
18
+ return;
19
+ }
20
+ for (var n = 0; n < names.length; n++) {
21
+ var name = names[n];
22
+ // Find mounted instances (exclude scoped <style> tags that carry data-zq-component)
23
+ var hosts = targetDoc.querySelectorAll(name + ':not(style)');
24
+ html += '<div class="comp-card">';
25
+ html += '<div class="comp-name">&lt;' + name + '&gt;</div>';
26
+ html += '<div class="comp-host">' + hosts.length + ' instance' + (hosts.length !== 1 ? 's' : '') + ' mounted</div>';
27
+ hosts.forEach(function(host) {
28
+ try {
29
+ var inst = $.getInstance(host);
30
+ if (inst && inst.state) {
31
+ html += '<div class="comp-state">';
32
+ var keys = Object.keys(inst.state);
33
+ for (var k = 0; k < keys.length; k++) {
34
+ var key = keys[k];
35
+ var val = inst.state[key];
36
+ var display = typeof val === 'object' ? JSON.stringify(val) : String(val);
37
+ if (display.length > 80) display = display.slice(0, 80) + '...';
38
+ html += '<div class="detail-row"><span class="comp-state-key">' + esc(key) + '</span><span class="comp-state-val">' + esc(display) + '</span></div>';
39
+ }
40
+ html += '</div>';
41
+ }
42
+ } catch(e) {}
43
+ });
44
+ html += '</div>';
45
+ }
46
+ }
47
+ if (!html) html = '<div class="empty-state">No components found</div>';
48
+ list.innerHTML = html;
49
+ }