zero-query 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,15 +6,15 @@
6
6
 
7
7
  <p align="center">
8
8
 
9
- [![npm version](https://img.shields.io/npm/v/@tonywied17/zero-query.svg)](https://www.npmjs.com/package/@tonywied17/zero-query)
10
- [![npm downloads](https://img.shields.io/npm/dm/@tonywied17/zero-query.svg)](https://www.npmjs.com/package/@tonywied17/zero-query)
9
+ [![npm version](https://img.shields.io/npm/v/zero-query.svg)](https://www.npmjs.com/package/zero-query)
10
+ [![npm downloads](https://img.shields.io/npm/dm/zero-query.svg)](https://www.npmjs.com/package/zero-query)
11
11
  [![GitHub](https://img.shields.io/badge/GitHub-zero--query-blue.svg)](https://github.com/tonywied17/zero-query)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
13
13
  [![Dependencies](https://img.shields.io/badge/dependencies-0-success.svg)](package.json)
14
14
 
15
15
  </p>
16
16
 
17
- > **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 ~45 KB minified browser bundle. No build step required.**
17
+ > **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 ~45 KB minified browser bundle. Works out of the box with ES modules — no build step required. An optional CLI bundler is available for single-file distribution.**
18
18
 
19
19
  ## Features
20
20
 
@@ -30,18 +30,22 @@
30
30
 
31
31
  ---
32
32
 
33
- ## Quick Start (Browser Bundle — Recommended)
33
+ ## Quick Start (Browser Bundle + ES Modules — Recommended)
34
34
 
35
- The preferred way to use zQuery is with the **pre-built browser bundle** (`zQuery.min.js`). No npm install, no bundler, no transpiler.
35
+ The preferred way to use zQuery is with the **pre-built browser bundle** (`zQuery.min.js`) paired with standard **ES module** `<script type="module">` tags for your app code. No npm install, no bundler, no transpiler — just grab the library and start writing components.
36
36
 
37
- ### 1. Build the bundle (one time)
37
+ ### 1. Get the library
38
+
39
+ Download `dist/zQuery.min.js` from the [GitHub releases](https://github.com/tonywied17/zero-query), or clone and build:
38
40
 
39
41
  ```bash
42
+ git clone https://github.com/tonywied17/zero-query.git
43
+ cd zero-query
40
44
  node build.js
45
+ # → dist/zQuery.js (~78 KB, readable)
46
+ # → dist/zQuery.min.js (~45 KB, production)
41
47
  ```
42
48
 
43
- This creates `dist/zQuery.js` and `dist/zQuery.min.js`.
44
-
45
49
  ### 2. Copy into your project
46
50
 
47
51
  ```
@@ -105,7 +109,9 @@ const router = $.router({
105
109
  });
106
110
  ```
107
111
 
108
- That's it — a fully working SPA with zero build tools.
112
+ That's it — a fully working SPA with zero build tools. Your files are served as individual ES modules, which means instant browser caching, easy debugging, and native import/export.
113
+
114
+ > **Want a single-file build instead?** See the [CLI Bundler](#cli-bundler-optional) section below for an optional bundling step that compiles your entire app into one file.
109
115
 
110
116
  ---
111
117
 
@@ -138,6 +144,152 @@ my-app/
138
144
 
139
145
  ---
140
146
 
147
+ ## CLI Bundler (Optional)
148
+
149
+ > **Beta.** The CLI bundler is functional but still maturing. The ES module setup above is the recommended and most reliable approach. The bundler is provided as a convenience for use cases where a single-file build is preferred.
150
+
151
+ zQuery ships with a zero-dependency CLI that can compile your entire app — all ES modules, the library, external templates, and assets — into a **single bundled file**. This is useful for:
152
+
153
+ - Distributing an app as one `.html` + one `.js` file
154
+ - Deploying to environments without a web server (open `index.html` from disk via `file://`)
155
+ - Reducing HTTP requests on legacy hosting without HTTP/2
156
+
157
+ ### Installation
158
+
159
+ The CLI is included in the `zero-query` npm package. Install it to get the `zquery` command along with the library source and pre-built bundles:
160
+
161
+ ```bash
162
+ # Add to your project as a dev dependency (recommended)
163
+ npm install zero-query --save-dev
164
+ npx zquery bundle
165
+
166
+ # Or install globally
167
+ npm install -g zero-query
168
+ zquery bundle
169
+
170
+ # Or run once without installing (npx fetches it on demand)
171
+ npx zero-query bundle
172
+ ```
173
+
174
+ The package includes:
175
+ - `dist/zQuery.min.js` — pre-built browser bundle (ready to copy into your project)
176
+ - `cli.js` — the CLI tool (`zquery build` / `zquery bundle`)
177
+ - `src/` — library source modules
178
+
179
+ ### Commands
180
+
181
+ #### `zquery build` — Build the Library
182
+
183
+ Compiles the zQuery library source into `dist/zQuery.js` and `dist/zQuery.min.js`. This is the same as running `node build.js`.
184
+
185
+ ```bash
186
+ zquery build # one-time build
187
+ zquery build --watch # rebuild on source changes
188
+ ```
189
+
190
+ #### `zquery bundle` — Bundle an App
191
+
192
+ Walks the ES module import graph starting from an entry file, topologically sorts all dependencies, strips `import`/`export` syntax, and concatenates everything into a single IIFE with content-hashed filenames for cache-busting.
193
+
194
+ ```bash
195
+ # Auto-detect entry from index.html's <script type="module">
196
+ zquery bundle
197
+
198
+ # Specify entry explicitly
199
+ zquery bundle scripts/app.js
200
+
201
+ # Full example with all options
202
+ zquery bundle scripts/app.js \
203
+ --out dist/ \
204
+ --include-lib \
205
+ --html index.html \
206
+ --watch
207
+ ```
208
+
209
+ ### Bundle Options
210
+
211
+ | Flag | Short | Description |
212
+ | --- | --- | --- |
213
+ | `--out <path>` | `-o` | Output directory (or file path — directory is extracted). Default: `dist/` |
214
+ | `--include-lib` | `-L` | Embed `zquery.min.js` directly in the bundle (no separate script tag needed) |
215
+ | `--html <file>` | — | Rewrite the HTML file: replaces `<script type="module">` with the bundle, copies assets into `dist/`, adjusts `<base href>` |
216
+ | `--watch` | `-w` | Watch source files and rebuild automatically on changes |
217
+
218
+ ### What the Bundler Does
219
+
220
+ 1. **Import graph walking** — Starting from the entry file, recursively resolves all `import` statements (including side-effect `import './foo.js'`) and produces a topologically sorted dependency list.
221
+
222
+ 2. **Module syntax stripping** — Removes `import`/`export` keywords while keeping all declarations. The output is plain browser-compatible JavaScript.
223
+
224
+ 3. **IIFE wrapping** — The concatenated code is wrapped in `(function() { 'use strict'; ... })()` to avoid polluting the global scope.
225
+
226
+ 4. **Library embedding** (`--include-lib`) — Finds `zquery.min.js` in common locations (`scripts/vendor/`, `dist/`, etc.) and embeds it at the top of the bundle. The resulting file is fully self-contained.
227
+
228
+ 5. **External resource inlining** — Automatically detects `pages` configs, `templateUrl`, and `styleUrl` references in your components and inlines the referenced HTML/CSS files into the bundle as a `window.__zqInline` map. This enables `file://` support where `fetch()` would otherwise fail due to CORS.
229
+
230
+ 6. **HTML rewriting** (`--html`) — Rewrites the specified HTML file:
231
+ - Replaces `<script type="module" src="...">` with `<script defer src="bundle.js">`
232
+ - Removes the standalone zQuery `<script>` tag when `--include-lib` is used
233
+ - Preserves `<base href="/">` for SPA deep-route support, with an inline `file://` fallback that switches to `./`
234
+ - Copies all referenced assets (CSS, images, vendor JS) into the `dist/` folder
235
+ - Scans CSS files for `url()` references and copies those assets too
236
+
237
+ 7. **Minification** — Produces both a readable `z-<name>.<hash>.js` and a minified `z-<name>.<hash>.min.js` (comment stripping + whitespace collapsing). The content hash changes only when the bundle changes, enabling long-lived cache headers. Previous hashed builds are automatically cleaned on each rebuild.
238
+
239
+ ### Step-by-Step: Bundling Your Own App
240
+
241
+ ```bash
242
+ # 1. Install zero-query in your project
243
+ cd my-app
244
+ npm init -y # if you don't have a package.json yet
245
+ npm install zero-query --save-dev
246
+
247
+ # 2. Copy the pre-built library into your project
248
+ cp node_modules/zero-query/dist/zQuery.min.js scripts/vendor/
249
+
250
+ # 3. Bundle the app
251
+ npx zquery bundle scripts/app.js -o dist/ -L --html index.html
252
+
253
+ # Output (filenames are content-hashed for cache-busting):
254
+ # dist/index.html ← rewritten HTML (src updated automatically)
255
+ # dist/z-app.a1b2c3d4.js ← hashed bundle (library + app + inlined templates)
256
+ # dist/z-app.a1b2c3d4.min.js ← minified version
257
+ # dist/styles/ ← copied CSS
258
+ # dist/scripts/vendor/ ← copied vendor assets
259
+
260
+ # 4. Open directly in a browser — no server needed
261
+ start dist/index.html # Windows
262
+ open dist/index.html # macOS
263
+ ```
264
+
265
+ #### Bundling the Starter App (from source)
266
+
267
+ If you cloned the zero-query repository:
268
+
269
+ ```bash
270
+ node build.js
271
+ cp dist/zQuery.min.js examples/starter-app/scripts/vendor/
272
+ cd examples/starter-app
273
+ npx zquery bundle scripts/app.js -o dist/ -L --html index.html
274
+ start dist/index.html
275
+ ```
276
+
277
+ ### Hash Routing on `file://`
278
+
279
+ When the bundled app is opened via `file://`, the router automatically detects the protocol and switches to **hash-based routing** (`#/about` instead of `/about`). No configuration needed — `z-link` attributes and programmatic navigation work identically.
280
+
281
+ ### Preparing Your App for Bundling
282
+
283
+ The bundler is designed to work with the standard zQuery project structure out of the box. A few things to keep in mind:
284
+
285
+ - **Use relative imports** — `import './components/home.js'` (not bare specifiers like `import 'home'`)
286
+ - **One component per file** — The import graph walker resolves each file once
287
+ - **`import.meta.url`** — Automatically replaced with a `document.baseURI`-based equivalent
288
+ - **External templates** — `templateUrl`, `styleUrl`, and `pages` configs are automatically detected and inlined. No changes to your component code are needed.
289
+ - **Vendor scripts** — If using `--include-lib`, the bundler finds `zquery.min.js` in `scripts/vendor/`, `vendor/`, `lib/`, or `dist/`
290
+
291
+ ---
292
+
141
293
  ## Selectors & DOM: `$(selector)` & `$.all(selector)`
142
294
 
143
295
  zQuery provides two selector functions:
@@ -433,7 +585,7 @@ Components can load their HTML templates and CSS from external files instead of
433
585
  Relative `templateUrl`, `styleUrl`, and `pages.dir` paths are automatically resolved **relative to the component file** — no extra configuration needed:
434
586
 
435
587
  ```js
436
- // File: scripts/components/widget/index.js
588
+ // File: scripts/components/widget/widget.js
437
589
  $.component('my-widget', {
438
590
  templateUrl: 'template.html', // → scripts/components/widget/template.html
439
591
  styleUrl: 'styles.css', // → scripts/components/widget/styles.css
@@ -569,7 +721,7 @@ $.style('overrides.css');
569
721
  The `pages` option is a high-level shorthand for components that display content from multiple HTML files in a directory (e.g. documentation, wizards, tabbed content). It replaces the need to manually build a `templateUrl` object map and maintain a separate page list.
570
722
 
571
723
  ```js
572
- // File: scripts/components/docs/index.js
724
+ // File: scripts/components/docs/docs.js
573
725
  $.component('docs-page', {
574
726
  pages: {
575
727
  dir: 'pages', // → scripts/components/docs/pages/
@@ -1146,13 +1298,17 @@ mq('.card').addClass('active');
1146
1298
  ## Building from Source
1147
1299
 
1148
1300
  ```bash
1149
- # One-time build
1301
+ # One-time library build
1150
1302
  node build.js
1151
1303
  # → dist/zQuery.js (development)
1152
1304
  # → dist/zQuery.min.js (production)
1153
1305
 
1154
1306
  # Watch mode (rebuilds on file changes)
1155
1307
  node build.js --watch
1308
+
1309
+ # Or use the CLI
1310
+ npx zquery build
1311
+ npx zquery build --watch
1156
1312
  ```
1157
1313
 
1158
1314
  The build script is zero-dependency — just Node.js. It concatenates all ES modules into a single IIFE and strips import/export statements. The minified version strips comments and collapses whitespace. For production builds, pipe through Terser for optimal compression.
@@ -1163,7 +1319,7 @@ The build script is zero-dependency — just Node.js. It concatenates all ES mod
1163
1319
 
1164
1320
  ```bash
1165
1321
  # From the project root
1166
- node build.js # build the bundle
1322
+ node build.js # build the library
1167
1323
  cp dist/zQuery.min.js examples/starter-app/scripts/vendor/ # copy to app
1168
1324
 
1169
1325
  # Start the dev server (uses zero-http)
@@ -1176,6 +1332,19 @@ npx serve examples/starter-app
1176
1332
 
1177
1333
  The starter app includes: Home, Counter (reactive state + z-model), Todos (global store + subscriptions), API Docs (full reference), and About pages.
1178
1334
 
1335
+ #### Bundled Version (Single-File)
1336
+
1337
+ You can also build a fully self-contained bundled version of the starter app:
1338
+
1339
+ ```bash
1340
+ cd examples/starter-app
1341
+ npx zquery bundle scripts/app.js -o dist/ -L --html index.html
1342
+
1343
+ # Open dist/index.html directly — no server needed
1344
+ ```
1345
+
1346
+ See [CLI Bundler](#cli-bundler-optional) for details.
1347
+
1179
1348
  ### Local Dev Server
1180
1349
 
1181
1350
  The project ships with a lightweight dev server powered by [zero-http](https://github.com/tonywied17/zero-http). It handles history-mode SPA routing (all non-file requests serve `index.html`).
@@ -1262,6 +1431,12 @@ location /my-app/ {
1262
1431
  | `$.version` | Library version |
1263
1432
  | `$.noConflict` | Release `$` global |
1264
1433
 
1434
+ | CLI | Description |
1435
+ | --- | --- |
1436
+ | `zquery build` | Build the zQuery library (`dist/zQuery.min.js`) |
1437
+ | `zquery bundle [entry]` | Bundle app ES modules into a single IIFE file |
1438
+ | `zquery --help` | Show CLI usage and options |
1439
+
1265
1440
  For full method signatures and options, see [API.md](API.md).
1266
1441
 
1267
1442
  ---
package/cli.js ADDED
@@ -0,0 +1,761 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * zQuery CLI
5
+ *
6
+ * Zero-dependency command-line tool for building the zQuery library
7
+ * and bundling zQuery-based applications into a single file.
8
+ *
9
+ * Usage:
10
+ * zquery build Build the zQuery library (dist/)
11
+ * zquery build --watch Build & watch for library changes
12
+ *
13
+ * zquery bundle [entry] Bundle an app's ES modules into one file
14
+ * zquery bundle scripts/app.js Specify entry explicitly
15
+ * zquery bundle -o dist/ Custom output directory
16
+ * zquery bundle --include-lib Embed zquery.min.js in the bundle
17
+ * zquery bundle --watch Watch & rebuild on changes
18
+ * zquery bundle --html index.html Rewrite <script type="module"> to use bundle
19
+ *
20
+ * Output files use content-hashed names for cache-busting:
21
+ * z-<entry>.<hash>.js / z-<entry>.<hash>.min.js
22
+ *
23
+ * Examples:
24
+ * cd my-zquery-app && npx zero-query bundle
25
+ * npx zero-query bundle scripts/app.js -o dist/ --include-lib
26
+ */
27
+
28
+ const fs = require('fs');
29
+ const path = require('path');
30
+ const crypto = require('crypto');
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // CLI argument parsing
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const args = process.argv.slice(2);
37
+ const command = args[0];
38
+
39
+ function flag(name, short) {
40
+ const i = args.indexOf(`--${name}`);
41
+ const j = short ? args.indexOf(`-${short}`) : -1;
42
+ return i !== -1 || j !== -1;
43
+ }
44
+
45
+ function option(name, short, fallback) {
46
+ let i = args.indexOf(`--${name}`);
47
+ if (i === -1 && short) i = args.indexOf(`-${short}`);
48
+ if (i !== -1 && i + 1 < args.length) return args[i + 1];
49
+ return fallback;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Shared utilities
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Context-aware comment stripper — skips strings, templates, regex.
58
+ * Reused from build.js.
59
+ */
60
+ function stripComments(code) {
61
+ let out = '';
62
+ let i = 0;
63
+ while (i < code.length) {
64
+ const ch = code[i];
65
+ const next = code[i + 1];
66
+
67
+ // String literals
68
+ if (ch === '"' || ch === "'" || ch === '`') {
69
+ const quote = ch;
70
+ out += ch; i++;
71
+ while (i < code.length) {
72
+ if (code[i] === '\\') { out += code[i] + (code[i + 1] || ''); i += 2; continue; }
73
+ out += code[i];
74
+ if (code[i] === quote) { i++; break; }
75
+ i++;
76
+ }
77
+ continue;
78
+ }
79
+
80
+ // Block comment
81
+ if (ch === '/' && next === '*') {
82
+ i += 2;
83
+ while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) i++;
84
+ i += 2;
85
+ continue;
86
+ }
87
+
88
+ // Line comment
89
+ if (ch === '/' && next === '/') {
90
+ i += 2;
91
+ while (i < code.length && code[i] !== '\n') i++;
92
+ continue;
93
+ }
94
+
95
+ // Regex literal
96
+ if (ch === '/') {
97
+ const before = out.replace(/\s+$/, '');
98
+ const last = before[before.length - 1];
99
+ const isRegexCtx = !last || '=({[,;:!&|?~+-*/%^>'.includes(last)
100
+ || before.endsWith('return') || before.endsWith('typeof')
101
+ || before.endsWith('case') || before.endsWith('in')
102
+ || before.endsWith('delete') || before.endsWith('void')
103
+ || before.endsWith('throw') || before.endsWith('new');
104
+ if (isRegexCtx) {
105
+ out += ch; i++;
106
+ let inCharClass = false;
107
+ while (i < code.length) {
108
+ const rc = code[i];
109
+ if (rc === '\\') { out += rc + (code[i + 1] || ''); i += 2; continue; }
110
+ if (rc === '[') inCharClass = true;
111
+ if (rc === ']') inCharClass = false;
112
+ out += rc; i++;
113
+ if (rc === '/' && !inCharClass) {
114
+ while (i < code.length && /[gimsuy]/.test(code[i])) { out += code[i]; i++; }
115
+ break;
116
+ }
117
+ }
118
+ continue;
119
+ }
120
+ }
121
+
122
+ out += ch; i++;
123
+ }
124
+ return out;
125
+ }
126
+
127
+ /** Quick minification (same approach as build.js). */
128
+ function minify(code, banner) {
129
+ const body = stripComments(code.replace(banner, ''))
130
+ .replace(/^\s*\n/gm, '')
131
+ .replace(/\n\s+/g, '\n')
132
+ .replace(/\s{2,}/g, ' ');
133
+ return banner + '\n' + body;
134
+ }
135
+
136
+ function sizeKB(buf) {
137
+ return (buf.length / 1024).toFixed(1);
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // "build" command — library build (mirrors build.js)
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function buildLibrary() {
145
+ const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
146
+ const VERSION = pkg.version;
147
+
148
+ const modules = [
149
+ 'src/reactive.js', 'src/core.js', 'src/component.js',
150
+ 'src/router.js', 'src/store.js', 'src/http.js', 'src/utils.js',
151
+ ];
152
+
153
+ const DIST = path.join(process.cwd(), 'dist');
154
+ const OUT_FILE = path.join(DIST, 'zquery.js');
155
+ const MIN_FILE = path.join(DIST, 'zquery.min.js');
156
+
157
+ const start = Date.now();
158
+ if (!fs.existsSync(DIST)) fs.mkdirSync(DIST, { recursive: true });
159
+
160
+ const parts = modules.map(file => {
161
+ let code = fs.readFileSync(path.join(process.cwd(), file), 'utf-8');
162
+ code = code.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
163
+ code = code.replace(/^export\s+(default\s+)?/gm, '');
164
+ code = code.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
165
+ return `// --- ${file} ${'—'.repeat(60 - file.length)}\n${code.trim()}`;
166
+ });
167
+
168
+ let indexCode = fs.readFileSync(path.join(process.cwd(), 'index.js'), 'utf-8');
169
+ indexCode = indexCode.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
170
+ indexCode = indexCode.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
171
+ indexCode = indexCode.replace(/^export\s+(default\s+)?/gm, '');
172
+
173
+ const banner = `/**\n * zQuery (zeroQuery) v${VERSION}\n * Lightweight Frontend Library\n * https://github.com/tonywied17/zero-query\n * (c) ${new Date().getFullYear()} Anthony Wiedman — MIT License\n */`;
174
+
175
+ 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`;
176
+
177
+ fs.writeFileSync(OUT_FILE, bundle, 'utf-8');
178
+ fs.writeFileSync(MIN_FILE, minify(bundle, banner), 'utf-8');
179
+
180
+ const elapsed = Date.now() - start;
181
+ console.log(` ✓ dist/zquery.js (${sizeKB(fs.readFileSync(OUT_FILE))} KB)`);
182
+ console.log(` ✓ dist/zquery.min.js (${sizeKB(fs.readFileSync(MIN_FILE))} KB)`);
183
+ console.log(` Done in ${elapsed}ms\n`);
184
+
185
+ return { DIST, OUT_FILE, MIN_FILE };
186
+ }
187
+
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // "bundle" command — app bundler
191
+ // ---------------------------------------------------------------------------
192
+
193
+ /**
194
+ * Resolve an import specifier relative to the importing file.
195
+ */
196
+ function resolveImport(specifier, fromFile) {
197
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) return null; // bare specifier
198
+ let resolved = path.resolve(path.dirname(fromFile), specifier);
199
+ // If no extension and a .js file exists, add it
200
+ if (!path.extname(resolved) && fs.existsSync(resolved + '.js')) {
201
+ resolved += '.js';
202
+ }
203
+ return resolved;
204
+ }
205
+
206
+ /**
207
+ * Extract import specifiers from a source file.
208
+ * Handles: import 'x', import x from 'x', import { a } from 'x', import * as x from 'x'
209
+ * (including multi-line destructured imports)
210
+ */
211
+ function extractImports(code) {
212
+ const specifiers = [];
213
+ let m;
214
+ // Pattern 1: import ... from 'specifier' (works for single & multi-line)
215
+ const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g;
216
+ while ((m = fromRe.exec(code)) !== null) {
217
+ specifiers.push(m[1]);
218
+ }
219
+ // Pattern 2: side-effect imports — import './foo.js';
220
+ const sideRe = /^\s*import\s+['"]([^'"]+)['"]\s*;?\s*$/gm;
221
+ while ((m = sideRe.exec(code)) !== null) {
222
+ if (!specifiers.includes(m[1])) specifiers.push(m[1]);
223
+ }
224
+ return specifiers;
225
+ }
226
+
227
+ /**
228
+ * Walk the import graph starting from `entry`, return files in dependency
229
+ * order (leaves first — topological sort).
230
+ */
231
+ function walkImportGraph(entry) {
232
+ const visited = new Set();
233
+ const order = [];
234
+
235
+ function visit(file) {
236
+ const abs = path.resolve(file);
237
+ if (visited.has(abs)) return;
238
+ visited.add(abs);
239
+
240
+ if (!fs.existsSync(abs)) {
241
+ console.warn(` ⚠ Missing file: ${abs}`);
242
+ return;
243
+ }
244
+
245
+ const code = fs.readFileSync(abs, 'utf-8');
246
+ const imports = extractImports(code);
247
+
248
+ for (const spec of imports) {
249
+ const resolved = resolveImport(spec, abs);
250
+ if (resolved) visit(resolved);
251
+ }
252
+
253
+ order.push(abs);
254
+ }
255
+
256
+ visit(entry);
257
+ return order;
258
+ }
259
+
260
+ /**
261
+ * Strip ES module import/export syntax from code, keeping declarations.
262
+ */
263
+ function stripModuleSyntax(code) {
264
+ // Remove import lines (single-line and multi-line from ... )
265
+ code = code.replace(/^\s*import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
266
+ // Remove side-effect imports import './foo.js';
267
+ code = code.replace(/^\s*import\s+['"].*?['"];?\s*$/gm, '');
268
+ // Remove export default but keep the expression
269
+ code = code.replace(/^(\s*)export\s+default\s+/gm, '$1');
270
+ // Remove export keyword but keep declarations
271
+ code = code.replace(/^(\s*)export\s+(const|let|var|function|class|async\s+function)\s/gm, '$1$2 ');
272
+ // Remove standalone export { ... } blocks
273
+ code = code.replace(/^\s*export\s*\{[\s\S]*?\};?\s*$/gm, '');
274
+ return code;
275
+ }
276
+
277
+ /**
278
+ * Replace `import.meta.url` with a runtime equivalent.
279
+ * In a non-module <script>, import.meta doesn't exist, so we substitute
280
+ * document.currentScript.src (set at load time) relative to the original
281
+ * file's path inside the project.
282
+ */
283
+ function replaceImportMeta(code, filePath, projectRoot) {
284
+ if (!code.includes('import.meta')) return code;
285
+ // Compute the web-relative path of this file from the project root
286
+ const rel = path.relative(projectRoot, filePath).replace(/\\/g, '/');
287
+ // Replace import.meta.url with a constructed URL based on the page origin
288
+ code = code.replace(
289
+ /import\.meta\.url/g,
290
+ `(new URL('${rel}', document.baseURI).href)`
291
+ );
292
+ return code;
293
+ }
294
+
295
+ /**
296
+ * Scan bundled source files for external resource references
297
+ * (pages config, templateUrl, styleUrl) and read those files so they
298
+ * can be inlined into the bundle for file:// support.
299
+ *
300
+ * Returns a map of { relativePath: fileContent }.
301
+ */
302
+ function collectInlineResources(files, projectRoot) {
303
+ const inlineMap = {};
304
+
305
+ for (const file of files) {
306
+ const code = fs.readFileSync(file, 'utf-8');
307
+ const fileDir = path.dirname(file);
308
+
309
+ // Detect `pages:` config — look for dir and items
310
+ const pagesMatch = code.match(/pages\s*:\s*\{[^}]*dir\s*:\s*['"]([^'"]+)['"]/s);
311
+ if (pagesMatch) {
312
+ const pagesDir = pagesMatch[1];
313
+ const ext = (code.match(/pages\s*:\s*\{[^}]*ext\s*:\s*['"]([^'"]+)['"]/s) || [])[1] || '.html';
314
+
315
+ // Extract item IDs from the items array
316
+ const itemsMatch = code.match(/items\s*:\s*\[([\s\S]*?)\]/);
317
+ if (itemsMatch) {
318
+ const itemsBlock = itemsMatch[1];
319
+ const ids = [];
320
+ // Match string items: 'getting-started'
321
+ let m;
322
+ const strRe = /['"]([^'"]+)['"]/g;
323
+ const objIdRe = /id\s*:\s*['"]([^'"]+)['"]/g;
324
+ // Collect all quoted strings that look like page IDs
325
+ while ((m = strRe.exec(itemsBlock)) !== null) {
326
+ // Skip labels (preceded by "label:")
327
+ const before = itemsBlock.substring(Math.max(0, m.index - 20), m.index);
328
+ if (/label\s*:\s*$/.test(before)) continue;
329
+ ids.push(m[1]);
330
+ }
331
+
332
+ // Read each page file
333
+ const absPagesDir = path.join(fileDir, pagesDir);
334
+ for (const id of ids) {
335
+ const pagePath = path.join(absPagesDir, id + ext);
336
+ if (fs.existsSync(pagePath)) {
337
+ const relKey = path.relative(projectRoot, pagePath).replace(/\\/g, '/');
338
+ inlineMap[relKey] = fs.readFileSync(pagePath, 'utf-8');
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ // Detect `styleUrl:` — single string
345
+ const styleMatch = code.match(/styleUrl\s*:\s*['"]([^'"]+)['"]/);
346
+ if (styleMatch) {
347
+ const stylePath = path.join(fileDir, styleMatch[1]);
348
+ if (fs.existsSync(stylePath)) {
349
+ const relKey = path.relative(projectRoot, stylePath).replace(/\\/g, '/');
350
+ inlineMap[relKey] = fs.readFileSync(stylePath, 'utf-8');
351
+ }
352
+ }
353
+
354
+ // Detect `templateUrl:` — single string
355
+ const tmplMatch = code.match(/templateUrl\s*:\s*['"]([^'"]+)['"]/);
356
+ if (tmplMatch) {
357
+ const tmplPath = path.join(fileDir, tmplMatch[1]);
358
+ if (fs.existsSync(tmplPath)) {
359
+ const relKey = path.relative(projectRoot, tmplPath).replace(/\\/g, '/');
360
+ inlineMap[relKey] = fs.readFileSync(tmplPath, 'utf-8');
361
+ }
362
+ }
363
+ }
364
+
365
+ return inlineMap;
366
+ }
367
+
368
+ /**
369
+ * Try to auto-detect the app entry point.
370
+ * Looks for <script type="module" src="..."> in an index.html,
371
+ * or falls back to common conventions.
372
+ */
373
+ function detectEntry(projectRoot) {
374
+ const htmlCandidates = ['index.html', 'public/index.html'];
375
+ for (const htmlFile of htmlCandidates) {
376
+ const htmlPath = path.join(projectRoot, htmlFile);
377
+ if (fs.existsSync(htmlPath)) {
378
+ const html = fs.readFileSync(htmlPath, 'utf-8');
379
+ const m = html.match(/<script[^>]+type\s*=\s*["']module["'][^>]+src\s*=\s*["']([^"']+)["']/);
380
+ if (m) return path.join(projectRoot, m[1]);
381
+ }
382
+ }
383
+ // Convention fallback
384
+ const fallbacks = ['scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
385
+ for (const f of fallbacks) {
386
+ const fp = path.join(projectRoot, f);
387
+ if (fs.existsSync(fp)) return fp;
388
+ }
389
+ return null;
390
+ }
391
+
392
+
393
+ function bundleApp() {
394
+ const projectRoot = process.cwd();
395
+
396
+ // Entry point
397
+ let entry = null;
398
+ // First positional arg after "bundle" that doesn't start with -
399
+ for (let i = 1; i < args.length; i++) {
400
+ if (!args[i].startsWith('-') && args[i - 1] !== '-o' && args[i - 1] !== '--out' && args[i - 1] !== '--html') {
401
+ entry = path.resolve(projectRoot, args[i]);
402
+ break;
403
+ }
404
+ }
405
+ if (!entry) entry = detectEntry(projectRoot);
406
+
407
+ if (!entry || !fs.existsSync(entry)) {
408
+ console.error(`\n ✗ Could not find entry file.`);
409
+ console.error(` Provide one explicitly: zquery bundle scripts/app.js\n`);
410
+ process.exit(1);
411
+ }
412
+
413
+ const includeLib = flag('include-lib', 'L');
414
+ const outPath = option('out', 'o', null);
415
+ const htmlFile = option('html', null, null);
416
+ const watchMode = flag('watch', 'w');
417
+
418
+ // Derive output directory (filename is auto-generated with a content hash)
419
+ const entryRel = path.relative(projectRoot, entry);
420
+ const entryName = path.basename(entry, '.js');
421
+ let distDir;
422
+ if (outPath) {
423
+ const resolved = path.resolve(projectRoot, outPath);
424
+ // -o accepts a directory path or a file path (directory is extracted)
425
+ if (outPath.endsWith('/') || outPath.endsWith('\\') || !path.extname(outPath) ||
426
+ (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory())) {
427
+ distDir = resolved;
428
+ } else {
429
+ distDir = path.dirname(resolved);
430
+ }
431
+ } else {
432
+ distDir = path.join(projectRoot, 'dist');
433
+ }
434
+
435
+ console.log(`\n zQuery App Bundler`);
436
+ console.log(` Entry: ${entryRel}`);
437
+ console.log(` Output: ${path.relative(projectRoot, distDir)}/z-${entryName}.[hash].js`);
438
+ if (includeLib) console.log(` Library: embedded`);
439
+ console.log('');
440
+
441
+ function doBuild() {
442
+ const start = Date.now();
443
+
444
+ if (!fs.existsSync(distDir)) fs.mkdirSync(distDir, { recursive: true });
445
+
446
+ // Walk the import graph
447
+ const files = walkImportGraph(entry);
448
+ console.log(` Resolved ${files.length} module(s):`);
449
+ files.forEach(f => console.log(` • ${path.relative(projectRoot, f)}`));
450
+
451
+ // Build concatenated source
452
+ const sections = files.map(file => {
453
+ let code = fs.readFileSync(file, 'utf-8');
454
+ code = stripModuleSyntax(code);
455
+ code = replaceImportMeta(code, file, projectRoot);
456
+ const rel = path.relative(projectRoot, file);
457
+ return `// --- ${rel} ${'—'.repeat(Math.max(1, 60 - rel.length))}\n${code.trim()}`;
458
+ });
459
+
460
+ // Optionally prepend zquery.min.js
461
+ let libSection = '';
462
+ if (includeLib) {
463
+ // Look for the library in common locations
464
+ const libCandidates = [
465
+ path.join(projectRoot, 'scripts/vendor/zquery.min.js'),
466
+ path.join(projectRoot, 'vendor/zquery.min.js'),
467
+ path.join(projectRoot, 'lib/zquery.min.js'),
468
+ path.join(projectRoot, 'dist/zquery.min.js'),
469
+ path.join(projectRoot, 'zquery.min.js'),
470
+ // If run from the zero-query repo itself
471
+ path.join(__dirname, 'dist/zquery.min.js'),
472
+ ];
473
+ const libPath = libCandidates.find(p => fs.existsSync(p));
474
+ if (libPath) {
475
+ libSection = `// --- zquery.min.js (library) ${'—'.repeat(34)}\n${fs.readFileSync(libPath, 'utf-8').trim()}\n\n`;
476
+ console.log(`\n Embedded library from ${path.relative(projectRoot, libPath)}`);
477
+ } else {
478
+ console.warn(`\n ⚠ Could not find zquery.min.js — skipping --include-lib`);
479
+ console.warn(` Build the library first: zquery build`);
480
+ }
481
+ }
482
+
483
+ const banner = `/**\n * App bundle — built by zQuery CLI\n * Entry: ${entryRel}\n * ${new Date().toISOString()}\n */`;
484
+
485
+ // Scan for external resources (pages, templateUrl, styleUrl) and inline them
486
+ const inlineMap = collectInlineResources(files, projectRoot);
487
+ let inlineSection = '';
488
+ if (Object.keys(inlineMap).length > 0) {
489
+ const entries = Object.entries(inlineMap).map(([key, content]) => {
490
+ // Escape for embedding in a JS string literal
491
+ const escaped = content
492
+ .replace(/\\/g, '\\\\')
493
+ .replace(/'/g, "\\'")
494
+ .replace(/\n/g, '\\n')
495
+ .replace(/\r/g, '');
496
+ return ` '${key}': '${escaped}'`;
497
+ });
498
+ inlineSection = `// --- Inlined resources (file:// support) ${'—'.repeat(20)}\nwindow.__zqInline = {\n${entries.join(',\n')}\n};\n\n`;
499
+ console.log(`\n Inlined ${Object.keys(inlineMap).length} external resource(s)`);
500
+ }
501
+
502
+ const bundle = `${banner}\n(function() {\n 'use strict';\n\n${libSection}${inlineSection}${sections.join('\n\n')}\n\n})();\n`;
503
+
504
+ // Content-hashed output filenames (z-<name>.<hash>.js)
505
+ const contentHash = crypto.createHash('sha256').update(bundle).digest('hex').slice(0, 8);
506
+ const bundleFile = path.join(distDir, `z-${entryName}.${contentHash}.js`);
507
+ const minFile = path.join(distDir, `z-${entryName}.${contentHash}.min.js`);
508
+
509
+ // Remove previous hashed builds
510
+ const escName = entryName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
511
+ const cleanRe = new RegExp(`^z-${escName}\\.[a-f0-9]{8}\\.(?:min\\.)?js$`);
512
+ if (fs.existsSync(distDir)) {
513
+ for (const f of fs.readdirSync(distDir)) {
514
+ if (cleanRe.test(f)) fs.unlinkSync(path.join(distDir, f));
515
+ }
516
+ }
517
+
518
+ fs.writeFileSync(bundleFile, bundle, 'utf-8');
519
+ fs.writeFileSync(minFile, minify(bundle, banner), 'utf-8');
520
+
521
+ console.log(`\n ✓ ${path.relative(projectRoot, bundleFile)} (${sizeKB(fs.readFileSync(bundleFile))} KB)`);
522
+ console.log(` ✓ ${path.relative(projectRoot, minFile)} (${sizeKB(fs.readFileSync(minFile))} KB)`);
523
+
524
+ // Optionally rewrite index.html
525
+ if (htmlFile) {
526
+ const bundledFileSet = new Set(files);
527
+ rewriteHtml(projectRoot, htmlFile, bundleFile, includeLib, bundledFileSet);
528
+ }
529
+
530
+ const elapsed = Date.now() - start;
531
+ console.log(` Done in ${elapsed}ms\n`);
532
+ }
533
+
534
+ doBuild();
535
+
536
+ // Watch mode
537
+ if (watchMode) {
538
+ const watchDirs = new Set();
539
+ const files = walkImportGraph(entry);
540
+ files.forEach(f => watchDirs.add(path.dirname(f)));
541
+
542
+ console.log(' Watching for changes...\n');
543
+ let debounceTimer;
544
+ for (const dir of watchDirs) {
545
+ fs.watch(dir, { recursive: true }, (_, filename) => {
546
+ if (!filename || !filename.endsWith('.js')) return;
547
+ clearTimeout(debounceTimer);
548
+ debounceTimer = setTimeout(() => {
549
+ console.log(` Changed: ${filename} — rebuilding...`);
550
+ try { doBuild(); } catch (e) { console.error(` ✗ ${e.message}`); }
551
+ }, 200);
552
+ });
553
+ }
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Recursively copy a directory.
559
+ */
560
+ function copyDirSync(src, dest) {
561
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
562
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
563
+ const srcPath = path.join(src, entry.name);
564
+ const destPath = path.join(dest, entry.name);
565
+ if (entry.isDirectory()) {
566
+ copyDirSync(srcPath, destPath);
567
+ } else {
568
+ fs.copyFileSync(srcPath, destPath);
569
+ }
570
+ }
571
+ }
572
+
573
+ /**
574
+ * Rewrite an HTML file to replace the module <script> with the bundle.
575
+ * Copies all referenced assets (CSS, JS, images) into dist/ so the
576
+ * output folder is fully self-contained and deployable.
577
+ */
578
+ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles) {
579
+ const htmlPath = path.resolve(projectRoot, htmlRelPath);
580
+ if (!fs.existsSync(htmlPath)) {
581
+ console.warn(` ⚠ HTML file not found: ${htmlRelPath}`);
582
+ return;
583
+ }
584
+
585
+ const htmlDir = path.dirname(htmlPath);
586
+ const distDir = path.dirname(bundleFile);
587
+ let html = fs.readFileSync(htmlPath, 'utf-8');
588
+
589
+ // Collect all asset references from the HTML (src=, href= on link/script/img)
590
+ const assetRe = /(?:<(?:link|script|img)[^>]*?\s(?:src|href)\s*=\s*["'])([^"']+)["']/gi;
591
+ const assets = new Set();
592
+ let m;
593
+ while ((m = assetRe.exec(html)) !== null) {
594
+ const ref = m[1];
595
+ // Skip absolute URLs, data URIs, protocol-relative, and anchors
596
+ if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:') || ref.startsWith('#')) continue;
597
+ // Skip the module entry (already bundled)
598
+ const refAbs = path.resolve(htmlDir, ref);
599
+ if (bundledFiles && bundledFiles.has(refAbs)) continue;
600
+ // Skip zquery lib if we're embedding it
601
+ if (includeLib && /zquery(?:\.min)?\.js$/i.test(ref)) continue;
602
+ assets.add(ref);
603
+ }
604
+
605
+ // Copy each referenced asset into dist/, preserving directory structure
606
+ let copiedCount = 0;
607
+ const copiedDirs = new Set();
608
+ for (const asset of assets) {
609
+ const srcFile = path.resolve(htmlDir, asset);
610
+ if (!fs.existsSync(srcFile)) continue;
611
+
612
+ const destFile = path.join(distDir, asset);
613
+ const destDir = path.dirname(destFile);
614
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
615
+
616
+ fs.copyFileSync(srcFile, destFile);
617
+ copiedCount++;
618
+
619
+ // If this is inside a directory that may contain sibling assets
620
+ // (fonts referenced by CSS, etc.), track it
621
+ copiedDirs.add(path.dirname(srcFile));
622
+ }
623
+
624
+ // Also copy any CSS-referenced assets: scan copied CSS files for url()
625
+ // references and copy those too
626
+ for (const asset of assets) {
627
+ const srcFile = path.resolve(htmlDir, asset);
628
+ if (!fs.existsSync(srcFile) || !asset.endsWith('.css')) continue;
629
+
630
+ const cssContent = fs.readFileSync(srcFile, 'utf-8');
631
+ const urlRe = /url\(\s*["']?([^"')]+?)["']?\s*\)/g;
632
+ let cm;
633
+ while ((cm = urlRe.exec(cssContent)) !== null) {
634
+ const ref = cm[1];
635
+ if (ref.startsWith('data:') || ref.startsWith('http') || ref.startsWith('//')) continue;
636
+ const cssSrcDir = path.dirname(srcFile);
637
+ const assetSrc = path.resolve(cssSrcDir, ref);
638
+ const cssDestDir = path.dirname(path.join(distDir, asset));
639
+ const assetDest = path.resolve(cssDestDir, ref);
640
+ if (fs.existsSync(assetSrc) && !fs.existsSync(assetDest)) {
641
+ const dir = path.dirname(assetDest);
642
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
643
+ fs.copyFileSync(assetSrc, assetDest);
644
+ copiedCount++;
645
+ }
646
+ }
647
+ }
648
+
649
+ // Make the bundle path relative to the dist/ HTML
650
+ const bundleRel = path.relative(distDir, bundleFile).replace(/\\/g, '/');
651
+
652
+ // Replace <script type="module" src="..."> with the bundle (regular script)
653
+ // Use "defer" so the script runs after DOM is parsed — module scripts are
654
+ // deferred by default, but regular scripts in <head> are not.
655
+ html = html.replace(
656
+ /<script\s+type\s*=\s*["']module["']\s+src\s*=\s*["'][^"']+["']\s*>\s*<\/script>/gi,
657
+ `<script defer src="${bundleRel}"></script>`
658
+ );
659
+
660
+ // If library is embedded, remove the standalone zquery script tag
661
+ if (includeLib) {
662
+ html = html.replace(
663
+ /\s*<script\s+src\s*=\s*["'][^"']*zquery(?:\.min)?\.js["']\s*>\s*<\/script>/gi,
664
+ ''
665
+ );
666
+ }
667
+
668
+ // SPA deep-route fix: keep <base href="/"> so refreshing on a route like
669
+ // /docs/project-structure still resolves assets from the root. However,
670
+ // when opened via file:// the root is the drive letter (file:///C:/), so
671
+ // inject a tiny inline script that switches the base to "./" for file://.
672
+ html = html.replace(
673
+ /(<base\s+href\s*=\s*["']\/["'][^>]*>)/i,
674
+ '$1<script>if(location.protocol==="file:")document.querySelector("base").href="./"</script>'
675
+ );
676
+
677
+ // Write HTML
678
+ const outHtml = path.join(distDir, path.basename(htmlRelPath));
679
+ fs.writeFileSync(outHtml, html, 'utf-8');
680
+ console.log(` ✓ ${path.relative(projectRoot, outHtml)} (HTML rewritten)`);
681
+ console.log(` ✓ Copied ${copiedCount} asset(s) into ${path.relative(projectRoot, distDir)}/`);
682
+ }
683
+
684
+
685
+ // ---------------------------------------------------------------------------
686
+ // Help
687
+ // ---------------------------------------------------------------------------
688
+
689
+ function showHelp() {
690
+ console.log(`
691
+ zQuery CLI — build & bundle tool
692
+
693
+ COMMANDS
694
+
695
+ build Build the zQuery library → dist/
696
+ --watch, -w Watch src/ and rebuild on changes
697
+
698
+ bundle [entry] Bundle app ES modules into a single file
699
+ --out, -o <path> Output directory (default: dist/)
700
+ --include-lib, -L Embed zquery.min.js in the bundle
701
+ --html <file> Rewrite HTML file to reference the bundle
702
+ --watch, -w Watch source files and rebuild on changes
703
+
704
+ OUTPUT
705
+
706
+ Bundle filenames are content-hashed for cache-busting:
707
+ z-<entry>.<hash>.js readable bundle
708
+ z-<entry>.<hash>.min.js minified bundle
709
+ Previous hashed builds are automatically cleaned on each rebuild.
710
+
711
+ EXAMPLES
712
+
713
+ # Build the library
714
+ zquery build
715
+
716
+ # Bundle an app (auto-detects entry from index.html)
717
+ cd my-app && zquery bundle
718
+
719
+ # Bundle with all options
720
+ zquery bundle scripts/app.js -o dist/ -L --html index.html
721
+
722
+ # Watch mode
723
+ zquery bundle --watch
724
+
725
+ The bundler walks the ES module import graph starting from the entry
726
+ file, topologically sorts dependencies, strips import/export syntax,
727
+ and concatenates everything into a single IIFE with content-hashed
728
+ filenames for cache-busting. No dependencies needed — just Node.js.
729
+ `);
730
+ }
731
+
732
+
733
+ // ---------------------------------------------------------------------------
734
+ // Main
735
+ // ---------------------------------------------------------------------------
736
+
737
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
738
+ showHelp();
739
+ } else if (command === 'build') {
740
+ console.log('\n zQuery Library Build\n');
741
+ buildLibrary();
742
+ if (flag('watch', 'w')) {
743
+ console.log(' Watching src/ for changes...\n');
744
+ const srcDir = path.join(process.cwd(), 'src');
745
+ let debounceTimer;
746
+ const rebuild = () => {
747
+ clearTimeout(debounceTimer);
748
+ debounceTimer = setTimeout(() => {
749
+ console.log(' Rebuilding...');
750
+ try { buildLibrary(); } catch (e) { console.error(` ✗ ${e.message}`); }
751
+ }, 200);
752
+ };
753
+ fs.watch(srcDir, { recursive: true }, rebuild);
754
+ fs.watch(path.join(process.cwd(), 'index.js'), rebuild);
755
+ }
756
+ } else if (command === 'bundle') {
757
+ bundleApp();
758
+ } else {
759
+ console.error(`\n Unknown command: ${command}\n Run "zquery --help" for usage.\n`);
760
+ process.exit(1);
761
+ }
package/index.js CHANGED
@@ -132,7 +132,7 @@ $.session = session;
132
132
  $.bus = bus;
133
133
 
134
134
  // --- Meta ------------------------------------------------------------------
135
- $.version = '0.1.0';
135
+ $.version = '__VERSION__';
136
136
 
137
137
  $.noConflict = () => {
138
138
  if (typeof window !== 'undefined' && window.$ === $) {
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "zero-query",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight modern frontend library — jQuery-like selectors, reactive components, SPA router, and state management with zero dependencies.",
5
5
  "main": "index.js",
6
+ "bin": {
7
+ "zquery": "./cli.js"
8
+ },
6
9
  "files": [
7
10
  "src",
8
11
  "index.js",
12
+ "cli.js",
9
13
  "LICENSE",
10
14
  "README.md"
11
15
  ],
@@ -13,7 +17,8 @@
13
17
  "build": "node build.js",
14
18
  "dev": "node build.js --watch",
15
19
  "serve": "node examples/starter-app/local-server.js",
16
- "watch": "node watch.js"
20
+ "bundle": "node cli.js bundle",
21
+ "bundle:app": "node cli.js bundle examples/starter-app/scripts/app.js -o examples/starter-app/dist -L --html examples/starter-app/index.html"
17
22
  },
18
23
  "keywords": [
19
24
  "dom",
@@ -38,7 +43,7 @@
38
43
  "bugs": {
39
44
  "url": "https://github.com/tonywied17/zero-query/issues"
40
45
  },
41
- "homepage": "https://github.com/tonywied17/zero-query#readme",
46
+ "homepage": "https://zquery.molex.cloud/docs",
42
47
  "publishConfig": {
43
48
  "access": "public"
44
49
  },
package/src/component.js CHANGED
@@ -39,6 +39,18 @@ let _uid = 0;
39
39
  function _fetchResource(url) {
40
40
  if (_resourceCache.has(url)) return _resourceCache.get(url);
41
41
 
42
+ // Check inline resource map (populated by CLI bundler for file:// support).
43
+ // Keys are relative paths; match against the URL suffix.
44
+ if (typeof window !== 'undefined' && window.__zqInline) {
45
+ for (const [path, content] of Object.entries(window.__zqInline)) {
46
+ if (url === path || url.endsWith('/' + path) || url.endsWith('\\' + path)) {
47
+ const resolved = Promise.resolve(content);
48
+ _resourceCache.set(url, resolved);
49
+ return resolved;
50
+ }
51
+ }
52
+ }
53
+
42
54
  // Resolve relative URLs against <base href> or origin root.
43
55
  // This prevents SPA route paths (e.g. /docs/advanced) from
44
56
  // breaking relative resource URLs like 'scripts/components/foo.css'.
package/src/router.js CHANGED
@@ -22,7 +22,9 @@ import { mount, destroy } from './component.js';
22
22
  class Router {
23
23
  constructor(config = {}) {
24
24
  this._el = null;
25
- this._mode = config.mode || 'history'; // 'history' | 'hash'
25
+ // Auto-detect: file:// protocol can't use pushState, fall back to hash
26
+ const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
27
+ this._mode = config.mode || (isFile ? 'hash' : 'history');
26
28
 
27
29
  // Base path for sub-path deployments
28
30
  // Priority: explicit config.base → window.__ZQ_BASE → <base href> tag
@@ -119,22 +121,31 @@ class Router {
119
121
  // --- Navigation ----------------------------------------------------------
120
122
 
121
123
  navigate(path, options = {}) {
122
- let normalized = this._normalizePath(path);
124
+ // Separate hash fragment (e.g. /docs/getting-started#cli-bundler)
125
+ const [cleanPath, fragment] = (path || '').split('#');
126
+ let normalized = this._normalizePath(cleanPath);
127
+ const hash = fragment ? '#' + fragment : '';
123
128
  if (this._mode === 'hash') {
129
+ // Hash mode uses the URL hash for routing, so a #fragment can't live
130
+ // in the URL. Store it as a scroll target for the destination component.
131
+ if (fragment) window.__zqScrollTarget = fragment;
124
132
  window.location.hash = '#' + normalized;
125
133
  } else {
126
- window.history.pushState(options.state || {}, '', this._base + normalized);
134
+ window.history.pushState(options.state || {}, '', this._base + normalized + hash);
127
135
  this._resolve();
128
136
  }
129
137
  return this;
130
138
  }
131
139
 
132
140
  replace(path, options = {}) {
133
- let normalized = this._normalizePath(path);
141
+ const [cleanPath, fragment] = (path || '').split('#');
142
+ let normalized = this._normalizePath(cleanPath);
143
+ const hash = fragment ? '#' + fragment : '';
134
144
  if (this._mode === 'hash') {
145
+ if (fragment) window.__zqScrollTarget = fragment;
135
146
  window.location.replace('#' + normalized);
136
147
  } else {
137
- window.history.replaceState(options.state || {}, '', this._base + normalized);
148
+ window.history.replaceState(options.state || {}, '', this._base + normalized + hash);
138
149
  this._resolve();
139
150
  }
140
151
  return this;