zero-query 0.7.5 → 0.8.7

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 (65) hide show
  1. package/README.md +39 -30
  2. package/cli/commands/build.js +110 -1
  3. package/cli/commands/bundle.js +127 -50
  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 +740 -226
  34. package/dist/zquery.min.js +2 -2
  35. package/index.d.ts +11 -11
  36. package/index.js +15 -10
  37. package/package.json +3 -2
  38. package/src/component.js +154 -139
  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 +196 -7
  44. package/src/ssr.js +1 -1
  45. package/tests/component.test.js +582 -0
  46. package/tests/core.test.js +251 -0
  47. package/tests/diff.test.js +333 -2
  48. package/tests/expression.test.js +148 -0
  49. package/tests/http.test.js +108 -0
  50. package/tests/reactive.test.js +148 -0
  51. package/tests/router.test.js +317 -0
  52. package/tests/store.test.js +126 -0
  53. package/tests/utils.test.js +161 -2
  54. package/types/collection.d.ts +17 -2
  55. package/types/component.d.ts +10 -34
  56. package/types/misc.d.ts +13 -0
  57. package/types/router.d.ts +30 -1
  58. package/cli/commands/dev.old.js +0 -520
  59. package/cli/scaffold/scripts/components/home.js +0 -137
  60. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -0
  61. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +0 -0
  62. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
  63. /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
  64. /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
  65. /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`:
@@ -181,12 +189,12 @@ Output goes to `dist/` next to your `index.html`:
181
189
  dist/
182
190
  server/ ← deploy to your web server (<base href="/"> for SPA routes)
183
191
  index.html
184
- z-app.<hash>.js
185
192
  z-app.<hash>.min.js
186
- styles/
193
+ global.<hash>.min.css
194
+ assets/
187
195
  local/ ← open from disk (file://) — no server needed
188
196
  index.html
189
- z-app.<hash>.js
197
+ z-app.<hash>.min.js
190
198
  ...
191
199
  ```
192
200
 
@@ -196,7 +204,8 @@ dist/
196
204
  | --- | --- | --- |
197
205
  | `--out <path>` | `-o` | Custom output directory |
198
206
  | `--index <file>` | `-i` | Index HTML file (default: auto-detected) |
199
- | `--minimal` | `-m` | Only output HTML + bundled JS (skip static assets) |
207
+ | `--minimal` | `-m` | Only output HTML, bundled JS, and global CSS (skip static assets) |
208
+ | `--global-css <path>` | | Override global CSS input file (default: first `<link>` in HTML) |
200
209
 
201
210
  ### What the Bundler Does
202
211
 
@@ -204,10 +213,10 @@ dist/
204
213
  1. **HTML files** — `index.html` is checked first, then other `.html` files (root + one level deep).
205
214
  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
215
  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`.
216
+ 4. **Convention fallbacks** — `app/app.js`, `scripts/app.js`, `src/app.js`, `js/app.js`, `app.js`, `main.js`.
208
217
  2. Resolves all `import` statements and topologically sorts dependencies
209
218
  3. Strips `import`/`export` syntax, wraps in an IIFE
210
- 4. Embeds zQuery library and inlines `templateUrl` / `styleUrl` / `pages` files
219
+ 4. Embeds zQuery library and inlines `templateUrl` / `styleUrl` files
211
220
  5. Rewrites HTML, copies assets, produces hashed filenames
212
221
 
213
222
  ---
@@ -246,8 +255,8 @@ location / {
246
255
  | `$.create` | Element factory |
247
256
  | `$.ready` `$.on` `$.off` | DOM ready, global event delegation & direct listeners |
248
257
  | `$.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 |
258
+ | `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` `$.prefetch` | Component system |
259
+ | `$.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
260
  | `$.safeEval` | CSP-safe expression evaluator (replaces `eval` / `new Function`) |
252
261
  | `$.style` | Dynamically load global stylesheet file(s) at runtime |
253
262
  | `$.router` `$.getRouter` | SPA router |
@@ -260,16 +269,16 @@ location / {
260
269
  | `$.param` `$.parseQuery` | URL utils |
261
270
  | `$.storage` `$.session` | Storage wrappers |
262
271
  | `$.bus` | Event bus |
263
- | `$.version` | Library version |
272
+ | `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~91 KB\"`) |
264
273
  | `$.meta` | Build metadata (populated by CLI bundler) |
265
274
  | `$.noConflict` | Release `$` global |
266
275
 
267
276
  | CLI Command | Description |
268
277
  | --- | --- |
269
278
  | `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. |
279
+ | `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
280
  | `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`) |
281
+ | `zquery build` | Build the zQuery library (`dist/zquery.min.js`) |
273
282
  | `zquery --help` | Show CLI usage |
274
283
 
275
284
  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
 
@@ -353,7 +353,7 @@ function _collapseTemplateCSS(tpl) {
353
353
 
354
354
  /**
355
355
  * Scan bundled source files for external resource references
356
- * (pages config, templateUrl, styleUrl) and return a map of
356
+ * (templateUrl, styleUrl) and return a map of
357
357
  * { relativePath: fileContent } for inlining.
358
358
  */
359
359
  function collectInlineResources(files, projectRoot) {
@@ -363,33 +363,6 @@ function collectInlineResources(files, projectRoot) {
363
363
  const code = fs.readFileSync(file, 'utf-8');
364
364
  const fileDir = path.dirname(file);
365
365
 
366
- // pages: config
367
- const pagesMatch = code.match(/pages\s*:\s*\{[^}]*dir\s*:\s*['"]([^'"]+)['"]/s);
368
- if (pagesMatch) {
369
- const pagesDir = pagesMatch[1];
370
- const ext = (code.match(/pages\s*:\s*\{[^}]*ext\s*:\s*['"]([^'"]+)['"]/s) || [])[1] || '.html';
371
- const itemsMatch = code.match(/items\s*:\s*\[([\s\S]*?)\]/);
372
- if (itemsMatch) {
373
- const itemsBlock = itemsMatch[1];
374
- const ids = [];
375
- let m;
376
- const strRe = /['"]([^'"]+)['"]/g;
377
- while ((m = strRe.exec(itemsBlock)) !== null) {
378
- const before = itemsBlock.substring(Math.max(0, m.index - 20), m.index);
379
- if (/label\s*:\s*$/.test(before)) continue;
380
- ids.push(m[1]);
381
- }
382
- const absPagesDir = path.join(fileDir, pagesDir);
383
- for (const id of ids) {
384
- const pagePath = path.join(absPagesDir, id + ext);
385
- if (fs.existsSync(pagePath)) {
386
- const relKey = path.relative(projectRoot, pagePath).replace(/\\/g, '/');
387
- inlineMap[relKey] = fs.readFileSync(pagePath, 'utf-8');
388
- }
389
- }
390
- }
391
- }
392
-
393
366
  // styleUrl:
394
367
  const styleUrlRe = /styleUrl\s*:\s*['"]([^'"]+)['"]/g;
395
368
  const styleMatch = styleUrlRe.exec(code);
@@ -409,6 +382,25 @@ function collectInlineResources(files, projectRoot) {
409
382
  const relKey = path.relative(projectRoot, tmplPath).replace(/\\/g, '/');
410
383
  inlineMap[relKey] = fs.readFileSync(tmplPath, 'utf-8');
411
384
  }
385
+ } else if (/templateUrl\s*:/.test(code)) {
386
+ // Dynamic templateUrl (e.g. Object.fromEntries, computed map) —
387
+ // inline all .html files in the component's directory tree so
388
+ // the runtime __zqInline lookup can resolve them by suffix.
389
+ (function scanHtml(dir) {
390
+ try {
391
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
392
+ const full = path.join(dir, entry.name);
393
+ if (entry.isFile() && entry.name.endsWith('.html')) {
394
+ const relKey = path.relative(projectRoot, full).replace(/\\/g, '/');
395
+ if (!inlineMap[relKey]) {
396
+ inlineMap[relKey] = fs.readFileSync(full, 'utf-8');
397
+ }
398
+ } else if (entry.isDirectory()) {
399
+ scanHtml(full);
400
+ }
401
+ }
402
+ } catch { /* permission error — skip */ }
403
+ })(fileDir);
412
404
  }
413
405
  }
414
406
 
@@ -521,7 +513,7 @@ function detectEntry(projectRoot) {
521
513
  }
522
514
 
523
515
  // 3. Convention fallbacks
524
- const fallbacks = ['scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
516
+ const fallbacks = ['app/app.js', 'scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
525
517
  for (const f of fallbacks) {
526
518
  const fp = path.join(projectRoot, f);
527
519
  if (fs.existsSync(fp)) return fp;
@@ -539,7 +531,7 @@ function detectEntry(projectRoot) {
539
531
  * server/index.html — <base href="/"> for SPA deep routes
540
532
  * local/index.html — relative paths for file:// access
541
533
  */
542
- function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir) {
534
+ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir, globalCssOrigHref, globalCssHash) {
543
535
  const htmlPath = path.resolve(projectRoot, htmlRelPath);
544
536
  if (!fs.existsSync(htmlPath)) {
545
537
  console.warn(` ⚠ HTML file not found: ${htmlRelPath}`);
@@ -647,6 +639,15 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
647
639
  );
648
640
  }
649
641
 
642
+ // Rewrite global CSS link to hashed version
643
+ if (globalCssOrigHref && globalCssHash) {
644
+ const escapedHref = globalCssOrigHref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
645
+ const cssLinkRe = new RegExp(
646
+ `(<link[^>]+href\\s*=\\s*["'])${escapedHref}(["'][^>]*>)`, 'i'
647
+ );
648
+ html = html.replace(cssLinkRe, `$1${globalCssHash}$2`);
649
+ }
650
+
650
651
  const serverHtml = html;
651
652
  const localHtml = html.replace(/<base\s+href\s*=\s*["']\/["'][^>]*>\s*\n?\s*/i, '');
652
653
 
@@ -664,19 +665,26 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
664
665
 
665
666
  /**
666
667
  * 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.
668
+ * skipping only build outputs, tooling dirs, and the app/ source dir.
669
+ * This ensures all static assets (assets/, icons/, images/, fonts/,
670
+ * manifests, etc.) are available in the built output without maintaining
671
+ * a fragile whitelist.
670
672
  */
671
673
  function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
672
- const SKIP_DIRS = new Set(['dist', 'node_modules', '.git', '.vscode', 'scripts']);
674
+ const SKIP_DIRS = new Set(['dist', 'node_modules', '.git', '.vscode']);
675
+
676
+ // Case-insensitive match for App/ directory (contains bundled source)
677
+ function isAppDir(name) {
678
+ return name.toLowerCase() === 'app';
679
+ }
673
680
 
674
681
  let copiedCount = 0;
675
682
 
676
683
  function copyEntry(srcPath, relPath) {
677
684
  const stat = fs.statSync(srcPath);
678
685
  if (stat.isDirectory()) {
679
- if (SKIP_DIRS.has(path.basename(srcPath))) return;
686
+ const dirName = path.basename(srcPath);
687
+ if (SKIP_DIRS.has(dirName) || isAppDir(dirName)) return;
680
688
  for (const child of fs.readdirSync(srcPath)) {
681
689
  copyEntry(path.join(srcPath, child), path.join(relPath, child));
682
690
  }
@@ -695,7 +703,7 @@ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
695
703
 
696
704
  for (const entry of fs.readdirSync(appRoot, { withFileTypes: true })) {
697
705
  if (entry.isDirectory()) {
698
- if (SKIP_DIRS.has(entry.name)) continue;
706
+ if (SKIP_DIRS.has(entry.name) || isAppDir(entry.name)) continue;
699
707
  copyEntry(path.join(appRoot, entry.name), entry.name);
700
708
  } else {
701
709
  copyEntry(path.join(appRoot, entry.name), entry.name);
@@ -714,6 +722,7 @@ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
714
722
  function bundleApp() {
715
723
  const projectRoot = process.cwd();
716
724
  const minimal = flag('minimal', 'm');
725
+ const globalCssOverride = option('global-css', null, null);
717
726
 
718
727
  // Entry point — positional arg (directory or file) or auto-detection
719
728
  let entry = null;
@@ -833,14 +842,18 @@ function bundleApp() {
833
842
  console.log(` Output: ${path.relative(projectRoot, baseDistDir)}/server/ & local/`);
834
843
  console.log(` Library: embedded`);
835
844
  console.log(` HTML: ${htmlFile || 'not found (no HTML detected)'}`);
836
- if (minimal) console.log(` Mode: minimal (HTML + bundle only)`);
845
+ if (minimal) console.log(` Mode: minimal (HTML + JS + global CSS only)`);
837
846
  console.log('');
838
847
 
839
848
  // ------ doBuild (inlined) ------
840
849
  const start = Date.now();
841
850
 
842
- if (!fs.existsSync(serverDir)) fs.mkdirSync(serverDir, { recursive: true });
843
- if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
851
+ // Clean previous dist outputs for a fresh build
852
+ for (const dir of [serverDir, localDir]) {
853
+ if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true });
854
+ }
855
+ fs.mkdirSync(serverDir, { recursive: true });
856
+ fs.mkdirSync(localDir, { recursive: true });
844
857
 
845
858
  const files = walkImportGraph(entry);
846
859
  console.log(` Resolved ${files.length} module(s):`);
@@ -876,8 +889,24 @@ function bundleApp() {
876
889
  }
877
890
 
878
891
  const htmlDir = htmlAbs ? path.dirname(htmlAbs) : null;
892
+
893
+ // Case-insensitive search for Assets/ directory
894
+ function findAssetsDir(root) {
895
+ try {
896
+ for (const e of fs.readdirSync(root, { withFileTypes: true })) {
897
+ if (e.isDirectory() && e.name.toLowerCase() === 'assets') return path.join(root, e.name);
898
+ }
899
+ } catch { /* skip */ }
900
+ return null;
901
+ }
902
+
903
+ const assetsDir = findAssetsDir(htmlDir || projectRoot);
904
+ const altAssetsDir = htmlDir ? findAssetsDir(projectRoot) : null;
905
+
879
906
  const libCandidates = [
880
907
  path.join(pkgRoot, 'dist/zquery.min.js'),
908
+ assetsDir && path.join(assetsDir, 'scripts/zquery.min.js'),
909
+ altAssetsDir && path.join(altAssetsDir, 'scripts/zquery.min.js'),
881
910
  htmlDir && path.join(htmlDir, 'scripts/vendor/zquery.min.js'),
882
911
  htmlDir && path.join(htmlDir, 'vendor/zquery.min.js'),
883
912
  path.join(projectRoot, 'scripts/vendor/zquery.min.js'),
@@ -895,7 +924,7 @@ function bundleApp() {
895
924
  console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
896
925
  } else {
897
926
  console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
898
- console.warn(` Place zquery.min.js in scripts/vendor/, vendor/, lib/, or dist/`);
927
+ console.warn(` Place zquery.min.js in assets/scripts/, vendor/, lib/, or dist/`);
899
928
  }
900
929
  }
901
930
 
@@ -921,8 +950,7 @@ function bundleApp() {
921
950
 
922
951
  // Content-hashed filenames
923
952
  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`;
953
+ const minBase = `z-${entryName}.${contentHash}.min.js`;
926
954
 
927
955
  // Clean previous builds
928
956
  const escName = entryName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -935,21 +963,70 @@ function bundleApp() {
935
963
  }
936
964
  }
937
965
 
938
- // Write bundles
939
- const bundleFile = path.join(serverDir, bundleBase);
940
- const minFile = path.join(serverDir, minBase);
941
- fs.writeFileSync(bundleFile, bundle, 'utf-8');
966
+ // Write minified bundle
967
+ const minFile = path.join(serverDir, minBase);
942
968
  fs.writeFileSync(minFile, minify(bundle, banner), 'utf-8');
943
- fs.copyFileSync(bundleFile, path.join(localDir, bundleBase));
944
969
  fs.copyFileSync(minFile, path.join(localDir, minBase));
945
970
 
946
- console.log(`\n ✓ ${bundleBase} (${sizeKB(fs.readFileSync(bundleFile))} KB)`);
947
- console.log(` ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
971
+ console.log(`\n ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
972
+
973
+ // ------------------------------------------------------------------
974
+ // Global CSS bundling — extract from index.html <link> or --global-css
975
+ // ------------------------------------------------------------------
976
+ let globalCssHash = null;
977
+ let globalCssOrigHref = null;
978
+ let globalCssPath = null;
979
+ if (htmlAbs) {
980
+ const htmlContent = fs.readFileSync(htmlAbs, 'utf-8');
981
+ const htmlDir = path.dirname(htmlAbs);
982
+
983
+ // Determine global CSS path: --global-css flag overrides, else first <link rel="stylesheet"> in HTML
984
+ globalCssPath = null;
985
+ if (globalCssOverride) {
986
+ globalCssPath = path.resolve(projectRoot, globalCssOverride);
987
+ // Reconstruct relative href for HTML rewriting
988
+ globalCssOrigHref = path.relative(htmlDir, globalCssPath).replace(/\\/g, '/');
989
+ } else {
990
+ const linkRe = /<link[^>]+rel\s*=\s*["']stylesheet["'][^>]+href\s*=\s*["']([^"']+)["']/gi;
991
+ const altRe = /<link[^>]+href\s*=\s*["']([^"']+)["'][^>]+rel\s*=\s*["']stylesheet["']/gi;
992
+ let linkMatch = linkRe.exec(htmlContent) || altRe.exec(htmlContent);
993
+ if (linkMatch) {
994
+ globalCssOrigHref = linkMatch[1];
995
+ // Strip query string / fragment so the path resolves to the actual file
996
+ const cleanHref = linkMatch[1].split('?')[0].split('#')[0];
997
+ globalCssPath = path.resolve(htmlDir, cleanHref);
998
+ }
999
+ }
1000
+
1001
+ if (globalCssPath && fs.existsSync(globalCssPath)) {
1002
+ let cssContent = fs.readFileSync(globalCssPath, 'utf-8');
1003
+ const cssMin = minifyCSS(cssContent);
1004
+ const cssHash = crypto.createHash('sha256').update(cssMin).digest('hex').slice(0, 8);
1005
+ const cssOutName = `global.${cssHash}.min.css`;
1006
+
1007
+ // Clean previous global CSS builds
1008
+ const cssCleanRe = /^global\.[a-f0-9]{8}\.min\.css$/;
1009
+ for (const dir of [serverDir, localDir]) {
1010
+ if (fs.existsSync(dir)) {
1011
+ for (const f of fs.readdirSync(dir)) {
1012
+ if (cssCleanRe.test(f)) fs.unlinkSync(path.join(dir, f));
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ fs.writeFileSync(path.join(serverDir, cssOutName), cssMin, 'utf-8');
1018
+ fs.writeFileSync(path.join(localDir, cssOutName), cssMin, 'utf-8');
1019
+ globalCssHash = cssOutName;
1020
+ console.log(` ✓ ${cssOutName} (${sizeKB(Buffer.from(cssMin))} KB)`);
1021
+ }
1022
+ }
948
1023
 
949
1024
  // Rewrite HTML to reference the minified bundle
950
1025
  const bundledFileSet = new Set(files);
1026
+ // Skip the original unminified global CSS from static asset copying
1027
+ if (globalCssPath) bundledFileSet.add(path.resolve(globalCssPath));
951
1028
  if (htmlFile) {
952
- rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir);
1029
+ rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir, globalCssOrigHref, globalCssHash);
953
1030
  }
954
1031
 
955
1032
  // 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) {