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 +188 -13
- package/cli.js +761 -0
- package/index.js +1 -1
- package/package.json +8 -3
- package/src/component.js +12 -0
- package/src/router.js +16 -5
package/README.md
CHANGED
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/zero-query)
|
|
10
|
+
[](https://www.npmjs.com/package/zero-query)
|
|
11
11
|
[](https://github.com/tonywied17/zero-query)
|
|
12
12
|
[](https://opensource.org/licenses/MIT)
|
|
13
13
|
[](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.
|
|
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.
|
|
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/
|
|
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/
|
|
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
|
|
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 = '
|
|
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.
|
|
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
|
-
"
|
|
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://
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|