zero-query 0.7.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -30
- package/cli/commands/build.js +110 -1
- package/cli/commands/bundle.js +127 -50
- package/cli/commands/create.js +1 -1
- package/cli/commands/dev/devtools/index.js +56 -0
- package/cli/commands/dev/devtools/js/components.js +49 -0
- package/cli/commands/dev/devtools/js/core.js +409 -0
- package/cli/commands/dev/devtools/js/elements.js +413 -0
- package/cli/commands/dev/devtools/js/network.js +166 -0
- package/cli/commands/dev/devtools/js/performance.js +73 -0
- package/cli/commands/dev/devtools/js/router.js +105 -0
- package/cli/commands/dev/devtools/js/source.js +132 -0
- package/cli/commands/dev/devtools/js/stats.js +35 -0
- package/cli/commands/dev/devtools/js/tabs.js +79 -0
- package/cli/commands/dev/devtools/panel.html +95 -0
- package/cli/commands/dev/devtools/styles.css +244 -0
- package/cli/commands/dev/index.js +28 -3
- package/cli/commands/dev/logger.js +6 -1
- package/cli/commands/dev/overlay.js +377 -0
- package/cli/commands/dev/server.js +8 -0
- package/cli/commands/dev/watcher.js +26 -1
- package/cli/help.js +8 -5
- package/cli/scaffold/{scripts → app}/app.js +1 -1
- package/cli/scaffold/{scripts → app}/components/about.js +4 -4
- package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
- package/cli/scaffold/app/components/home.js +137 -0
- package/cli/scaffold/{scripts → app}/routes.js +1 -1
- package/cli/scaffold/{scripts → app}/store.js +6 -6
- package/cli/scaffold/assets/.gitkeep +0 -0
- package/cli/scaffold/{styles/styles.css → global.css} +3 -2
- package/cli/scaffold/index.html +11 -11
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +740 -226
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +11 -11
- package/index.js +15 -10
- package/package.json +3 -2
- package/src/component.js +154 -139
- package/src/core.js +57 -11
- package/src/diff.js +256 -58
- package/src/expression.js +33 -3
- package/src/reactive.js +37 -5
- package/src/router.js +196 -7
- package/src/ssr.js +1 -1
- package/tests/component.test.js +582 -0
- package/tests/core.test.js +251 -0
- package/tests/diff.test.js +333 -2
- package/tests/expression.test.js +148 -0
- package/tests/http.test.js +108 -0
- package/tests/reactive.test.js +148 -0
- package/tests/router.test.js +317 -0
- package/tests/store.test.js +126 -0
- package/tests/utils.test.js +161 -2
- package/types/collection.d.ts +17 -2
- package/types/component.d.ts +10 -34
- package/types/misc.d.ts +13 -0
- package/types/router.d.ts +30 -1
- package/cli/commands/dev.old.js +0 -520
- package/cli/scaffold/scripts/components/home.js +0 -137
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -0
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +0 -0
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
package/README.md
CHANGED
|
@@ -15,19 +15,21 @@
|
|
|
15
15
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
-
> **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~
|
|
18
|
+
> **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~91 KB minified browser bundle. Works out of the box with ES modules. An optional CLI bundler is available for single-file production builds.**
|
|
19
19
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
22
22
|
| Module | Highlights |
|
|
23
23
|
| --- | --- |
|
|
24
|
-
| **
|
|
25
|
-
| **
|
|
26
|
-
| **
|
|
27
|
-
| **
|
|
28
|
-
| **
|
|
24
|
+
| **Components** | Reactive state, template literals, `@event` delegation (8 modifiers), `z-model` two-way binding, computed properties, watch callbacks, slot-based content projection, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`, `z-skip`), DOM morphing engine with LIS-based keyed reconciliation (no innerHTML rebuild), CSP-safe expression evaluation with AST caching, scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles |
|
|
25
|
+
| **Router** | History & hash mode, route params (`:id`), wildcards, guards (`beforeEach`/`afterEach`), lazy loading, `z-link` navigation, `z-to-top` scroll modifier (`instant`/`smooth`), sub-route history substates (`pushSubstate`/`onSubstate`) |
|
|
26
|
+
| **Directives** | `z-if`, `z-for`, `z-model`, `z-show`, `z-bind`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`, `z-skip` — 17 built-in template directives |
|
|
27
|
+
| **Reactive** | Deep proxy reactivity, Signals (`.value`, `.peek()`), computed values, effects (auto-tracked with dispose) |
|
|
28
|
+
| **Store** | Reactive global state, named actions, computed getters, middleware, subscriptions, time-travel (undo/redo/history) |
|
|
29
29
|
| **Selectors & DOM** | jQuery-like chainable selectors, traversal, DOM manipulation, events, animation |
|
|
30
|
+
| **HTTP** | Fetch wrapper with auto-JSON, interceptors, timeout/abort, base URL |
|
|
30
31
|
| **Utils** | debounce, throttle, pipe, once, sleep, escapeHtml, uuid, deepClone, deepMerge, storage/session wrappers, event bus |
|
|
32
|
+
| **Dev Tools** | CLI dev server with live-reload, CSS hot-swap, full-screen error overlay, floating toolbar, dark-themed inspector panel (Router view, DOM tree, network log, component viewer, performance dashboard), fetch interceptor, render instrumentation, CLI bundler for single-file production builds |
|
|
31
33
|
|
|
32
34
|
---
|
|
33
35
|
|
|
@@ -61,15 +63,19 @@ The dev server includes a **full-screen error overlay** that surfaces errors dir
|
|
|
61
63
|
- **Runtime errors** — uncaught exceptions and unhandled promise rejections are captured and displayed in the same overlay with a cleaned-up stack trace.
|
|
62
64
|
- The overlay **auto-clears** when you fix the error and save. Press `Esc` or click `×` to dismiss manually.
|
|
63
65
|
|
|
66
|
+
#### Floating Toolbar & Inspector
|
|
67
|
+
|
|
68
|
+
A compact toolbar appears in the bottom-right corner showing live request/render counters and a DOM button. Click any counter to open a **dark-themed DevTools inspector** as a popup — or visit `http://localhost:<port>/_devtools` for a standalone split-view panel with four tabs: **Elements** (live DOM tree with component badges, source viewer, expand/collapse), **Network** (fetch log with JSON viewer), **Components** (live state cards), and **Performance** (render timeline with timing metrics).
|
|
69
|
+
|
|
64
70
|
### Alternative: Manual Setup (No npm)
|
|
65
71
|
|
|
66
|
-
If you prefer **zero tooling**, download `dist/
|
|
72
|
+
If you prefer **zero tooling**, download `dist/zquery.min.js` from the [dist/ folder](https://github.com/tonywied17/zero-query/tree/main/dist) and drop it into your project root or `assets/scripts/`. Then open `index.html` directly in a browser — no Node.js required.
|
|
67
73
|
|
|
68
74
|
```bash
|
|
69
75
|
git clone https://github.com/tonywied17/zero-query.git
|
|
70
76
|
cd zero-query
|
|
71
77
|
npx zquery build
|
|
72
|
-
# → dist/
|
|
78
|
+
# → dist/zquery.min.js (~91 KB)
|
|
73
79
|
```
|
|
74
80
|
|
|
75
81
|
### Include in HTML
|
|
@@ -80,9 +86,9 @@ npx zquery build
|
|
|
80
86
|
<head>
|
|
81
87
|
<meta charset="UTF-8">
|
|
82
88
|
<title>My App</title>
|
|
83
|
-
<link rel="stylesheet" href="
|
|
84
|
-
<script src="
|
|
85
|
-
<script type="module" src="
|
|
89
|
+
<link rel="stylesheet" href="global.css">
|
|
90
|
+
<script src="zquery.min.js"></script>
|
|
91
|
+
<script type="module" src="app/app.js"></script>
|
|
86
92
|
</head>
|
|
87
93
|
<body>
|
|
88
94
|
<nav>
|
|
@@ -97,7 +103,7 @@ npx zquery build
|
|
|
97
103
|
### Boot Your App
|
|
98
104
|
|
|
99
105
|
```js
|
|
100
|
-
//
|
|
106
|
+
// app/app.js
|
|
101
107
|
import './components/home.js';
|
|
102
108
|
import './components/about.js';
|
|
103
109
|
import './components/contacts/contacts.js';
|
|
@@ -109,7 +115,7 @@ $.router({ el: '#app', routes, fallback: 'not-found' });
|
|
|
109
115
|
### Define a Component
|
|
110
116
|
|
|
111
117
|
```js
|
|
112
|
-
//
|
|
118
|
+
// app/components/home.js
|
|
113
119
|
$.component('home-page', {
|
|
114
120
|
state: () => ({ count: 0 }),
|
|
115
121
|
increment() { this.state.count++; },
|
|
@@ -132,9 +138,8 @@ That's it — a fully working SPA with the dev server's live-reload.
|
|
|
132
138
|
```
|
|
133
139
|
my-app/
|
|
134
140
|
index.html
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
zQuery.min.js ← only needed for manual setup; dev server auto-resolves
|
|
141
|
+
global.css
|
|
142
|
+
app/
|
|
138
143
|
app.js
|
|
139
144
|
routes.js
|
|
140
145
|
store.js
|
|
@@ -149,14 +154,17 @@ my-app/
|
|
|
149
154
|
contacts.js
|
|
150
155
|
contacts.html
|
|
151
156
|
contacts.css
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
assets/
|
|
158
|
+
scripts/ ← third-party JS (e.g. zquery.min.js for manual setup)
|
|
159
|
+
styles/ ← additional stylesheets, fonts, etc.
|
|
154
160
|
```
|
|
155
161
|
|
|
156
162
|
- One component per file inside `components/`.
|
|
157
163
|
- Names **must contain a hyphen** (Web Component convention): `home-page`, `app-counter`, etc.
|
|
158
164
|
- Components with external templates or styles can use a subfolder (e.g. `contacts/contacts.js` + `contacts.html` + `contacts.css`).
|
|
159
165
|
- `app.js` is the single entry point — import components, create the store, and boot the router.
|
|
166
|
+
- `global.css` lives next to `index.html` for easy access; the bundler hashes it into `global.<hash>.min.css` for production.
|
|
167
|
+
- `assets/` holds static files that get copied to `dist/` as-is.
|
|
160
168
|
|
|
161
169
|
---
|
|
162
170
|
|
|
@@ -172,7 +180,7 @@ npx zquery bundle
|
|
|
172
180
|
npx zquery bundle my-app/
|
|
173
181
|
|
|
174
182
|
# Or pass a direct entry file (skips auto-detection)
|
|
175
|
-
npx zquery bundle my-app/
|
|
183
|
+
npx zquery bundle my-app/app/main.js
|
|
176
184
|
```
|
|
177
185
|
|
|
178
186
|
Output goes to `dist/` next to your `index.html`:
|
|
@@ -181,12 +189,12 @@ Output goes to `dist/` next to your `index.html`:
|
|
|
181
189
|
dist/
|
|
182
190
|
server/ ← deploy to your web server (<base href="/"> for SPA routes)
|
|
183
191
|
index.html
|
|
184
|
-
z-app.<hash>.js
|
|
185
192
|
z-app.<hash>.min.js
|
|
186
|
-
|
|
193
|
+
global.<hash>.min.css
|
|
194
|
+
assets/
|
|
187
195
|
local/ ← open from disk (file://) — no server needed
|
|
188
196
|
index.html
|
|
189
|
-
z-app.<hash>.js
|
|
197
|
+
z-app.<hash>.min.js
|
|
190
198
|
...
|
|
191
199
|
```
|
|
192
200
|
|
|
@@ -196,7 +204,8 @@ dist/
|
|
|
196
204
|
| --- | --- | --- |
|
|
197
205
|
| `--out <path>` | `-o` | Custom output directory |
|
|
198
206
|
| `--index <file>` | `-i` | Index HTML file (default: auto-detected) |
|
|
199
|
-
| `--minimal` | `-m` | Only output HTML
|
|
207
|
+
| `--minimal` | `-m` | Only output HTML, bundled JS, and global CSS (skip static assets) |
|
|
208
|
+
| `--global-css <path>` | | Override global CSS input file (default: first `<link>` in HTML) |
|
|
200
209
|
|
|
201
210
|
### What the Bundler Does
|
|
202
211
|
|
|
@@ -204,10 +213,10 @@ dist/
|
|
|
204
213
|
1. **HTML files** — `index.html` is checked first, then other `.html` files (root + one level deep).
|
|
205
214
|
2. **Module scripts within HTML** — within each HTML file, a `<script type="module">` whose `src` resolves to `app.js` wins; otherwise the first module script tag is used.
|
|
206
215
|
3. **JS file scan** — if no HTML match, JS files (up to 2 levels deep) are scanned in two passes: first for `$.router(` (the canonical app entry point), then for `$.mount(`, `$.store(`, or `mountAll(`.
|
|
207
|
-
4. **Convention fallbacks** — `scripts/app.js`, `src/app.js`, `js/app.js`, `app.js`, `main.js`.
|
|
216
|
+
4. **Convention fallbacks** — `app/app.js`, `scripts/app.js`, `src/app.js`, `js/app.js`, `app.js`, `main.js`.
|
|
208
217
|
2. Resolves all `import` statements and topologically sorts dependencies
|
|
209
218
|
3. Strips `import`/`export` syntax, wraps in an IIFE
|
|
210
|
-
4. Embeds zQuery library and inlines `templateUrl` / `styleUrl`
|
|
219
|
+
4. Embeds zQuery library and inlines `templateUrl` / `styleUrl` files
|
|
211
220
|
5. Rewrites HTML, copies assets, produces hashed filenames
|
|
212
221
|
|
|
213
222
|
---
|
|
@@ -246,8 +255,8 @@ location / {
|
|
|
246
255
|
| `$.create` | Element factory |
|
|
247
256
|
| `$.ready` `$.on` `$.off` | DOM ready, global event delegation & direct listeners |
|
|
248
257
|
| `$.fn` | Collection prototype (extend it) |
|
|
249
|
-
| `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` | Component system |
|
|
250
|
-
| `$.morph` | DOM morphing engine —
|
|
258
|
+
| `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` `$.prefetch` | Component system |
|
|
259
|
+
| `$.morph` `$.morphElement` | DOM morphing engine — LIS-based keyed reconciliation, `isEqualNode()` bail-outs, `z-skip` opt-out. Patches existing DOM to match new HTML without destroying unchanged nodes. Auto-key detection (`id`, `data-id`, `data-key`) — no `z-key` required. `$().html()` and `$().replaceWith()` auto-morph existing content; `$().morph()` for explicit morph |
|
|
251
260
|
| `$.safeEval` | CSP-safe expression evaluator (replaces `eval` / `new Function`) |
|
|
252
261
|
| `$.style` | Dynamically load global stylesheet file(s) at runtime |
|
|
253
262
|
| `$.router` `$.getRouter` | SPA router |
|
|
@@ -260,16 +269,16 @@ location / {
|
|
|
260
269
|
| `$.param` `$.parseQuery` | URL utils |
|
|
261
270
|
| `$.storage` `$.session` | Storage wrappers |
|
|
262
271
|
| `$.bus` | Event bus |
|
|
263
|
-
| `$.version` | Library version |
|
|
272
|
+
| `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~91 KB\"`) |
|
|
264
273
|
| `$.meta` | Build metadata (populated by CLI bundler) |
|
|
265
274
|
| `$.noConflict` | Release `$` global |
|
|
266
275
|
|
|
267
276
|
| CLI Command | Description |
|
|
268
277
|
| --- | --- |
|
|
269
278
|
| `zquery create [dir]` | Scaffold a new project (index.html, components, store, styles) |
|
|
270
|
-
| `zquery dev [root]` | Dev server with live-reload &
|
|
279
|
+
| `zquery dev [root]` | Dev server with live-reload, CSS hot-swap, error overlay, floating toolbar & inspector panel (port 3100). Visit `/_devtools` for the standalone panel. `--index` for custom HTML, `--bundle` for bundled mode, `--no-intercept` to skip CDN intercept. |
|
|
271
280
|
| `zquery bundle [dir\|file]` | Bundle app into a single IIFE file. Accepts dir or direct entry file. |
|
|
272
|
-
| `zquery build` | Build the zQuery library (`dist/
|
|
281
|
+
| `zquery build` | Build the zQuery library (`dist/zquery.min.js`) |
|
|
273
282
|
| `zquery --help` | Show CLI usage |
|
|
274
283
|
|
|
275
284
|
For full method signatures, options, and examples, see **[API.md](API.md)**.
|
package/cli/commands/build.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const zlib = require('zlib');
|
|
12
13
|
const { minify, sizeKB } = require('../utils');
|
|
13
14
|
|
|
14
15
|
function buildLibrary() {
|
|
@@ -47,13 +48,121 @@ function buildLibrary() {
|
|
|
47
48
|
const bundle = `${banner}\n(function(global) {\n 'use strict';\n\n${parts.join('\n\n')}\n\n// --- index.js (assembly) ${'-'.repeat(42)}\n${indexCode.trim().replace("'__VERSION__'", `'${VERSION}'`)}\n\n})(typeof window !== 'undefined' ? window : globalThis);\n`;
|
|
48
49
|
|
|
49
50
|
fs.writeFileSync(OUT_FILE, bundle, 'utf-8');
|
|
50
|
-
|
|
51
|
+
const minified = minify(bundle, banner);
|
|
52
|
+
fs.writeFileSync(MIN_FILE, minified, 'utf-8');
|
|
53
|
+
|
|
54
|
+
// Inject actual minified library size into both outputs
|
|
55
|
+
const libSizeKB = Math.round(Buffer.from(minified).length / 1024);
|
|
56
|
+
const libSizeStr = `~${libSizeKB} KB`;
|
|
57
|
+
const outContent = fs.readFileSync(OUT_FILE, 'utf-8').replace("'__LIB_SIZE__'", `'${libSizeStr}'`);
|
|
58
|
+
const minContent = minified.replace("'__LIB_SIZE__'", `'${libSizeStr}'`);
|
|
59
|
+
fs.writeFileSync(OUT_FILE, outContent, 'utf-8');
|
|
60
|
+
fs.writeFileSync(MIN_FILE, minContent, 'utf-8');
|
|
51
61
|
|
|
52
62
|
const elapsed = Date.now() - start;
|
|
53
63
|
console.log(` ✓ dist/zquery.js (${sizeKB(fs.readFileSync(OUT_FILE))} KB)`);
|
|
54
64
|
console.log(` ✓ dist/zquery.min.js (${sizeKB(fs.readFileSync(MIN_FILE))} KB)`);
|
|
55
65
|
console.log(` Done in ${elapsed}ms\n`);
|
|
56
66
|
|
|
67
|
+
// --- Create dist/zquery.dist.zip -----------------------------------------
|
|
68
|
+
const root = process.cwd();
|
|
69
|
+
const zipFiles = [
|
|
70
|
+
{ src: OUT_FILE, name: 'zquery.js' },
|
|
71
|
+
{ src: MIN_FILE, name: 'zquery.min.js' },
|
|
72
|
+
{ src: path.join(root, 'LICENSE'), name: 'LICENSE' },
|
|
73
|
+
{ src: path.join(root, 'API.md'), name: 'API.md' },
|
|
74
|
+
{ src: path.join(root, 'README.md'), name: 'README.md' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Minimal ZIP builder (deflate via zlib, no external deps)
|
|
78
|
+
function buildZip(entries) {
|
|
79
|
+
const localHeaders = [];
|
|
80
|
+
const centralHeaders = [];
|
|
81
|
+
let offset = 0;
|
|
82
|
+
|
|
83
|
+
for (const { name, data } of entries) {
|
|
84
|
+
const nameBytes = Buffer.from(name, 'utf-8');
|
|
85
|
+
const compressed = zlib.deflateRawSync(data, { level: 9 });
|
|
86
|
+
const crc = crc32(data);
|
|
87
|
+
const compLen = compressed.length;
|
|
88
|
+
const uncompLen = data.length;
|
|
89
|
+
|
|
90
|
+
// Local file header
|
|
91
|
+
const local = Buffer.alloc(30 + nameBytes.length);
|
|
92
|
+
local.writeUInt32LE(0x04034b50, 0); // signature
|
|
93
|
+
local.writeUInt16LE(20, 4); // version needed
|
|
94
|
+
local.writeUInt16LE(0, 6); // flags
|
|
95
|
+
local.writeUInt16LE(8, 8); // compression: deflate
|
|
96
|
+
local.writeUInt16LE(0, 10); // mod time
|
|
97
|
+
local.writeUInt16LE(0, 12); // mod date
|
|
98
|
+
local.writeUInt32LE(crc, 14);
|
|
99
|
+
local.writeUInt32LE(compLen, 18);
|
|
100
|
+
local.writeUInt32LE(uncompLen, 22);
|
|
101
|
+
local.writeUInt16LE(nameBytes.length, 26);
|
|
102
|
+
local.writeUInt16LE(0, 28); // extra field length
|
|
103
|
+
nameBytes.copy(local, 30);
|
|
104
|
+
|
|
105
|
+
localHeaders.push(Buffer.concat([local, compressed]));
|
|
106
|
+
|
|
107
|
+
// Central directory header
|
|
108
|
+
const central = Buffer.alloc(46 + nameBytes.length);
|
|
109
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
110
|
+
central.writeUInt16LE(20, 4); // version made by
|
|
111
|
+
central.writeUInt16LE(20, 6); // version needed
|
|
112
|
+
central.writeUInt16LE(0, 8); // flags
|
|
113
|
+
central.writeUInt16LE(8, 10); // compression: deflate
|
|
114
|
+
central.writeUInt16LE(0, 12); // mod time
|
|
115
|
+
central.writeUInt16LE(0, 14); // mod date
|
|
116
|
+
central.writeUInt32LE(crc, 16);
|
|
117
|
+
central.writeUInt32LE(compLen, 20);
|
|
118
|
+
central.writeUInt32LE(uncompLen, 24);
|
|
119
|
+
central.writeUInt16LE(nameBytes.length, 28);
|
|
120
|
+
central.writeUInt16LE(0, 30); // extra field length
|
|
121
|
+
central.writeUInt16LE(0, 32); // comment length
|
|
122
|
+
central.writeUInt16LE(0, 34); // disk number
|
|
123
|
+
central.writeUInt16LE(0, 36); // internal attrs
|
|
124
|
+
central.writeUInt32LE(0, 38); // external attrs
|
|
125
|
+
central.writeUInt32LE(offset, 42); // local header offset
|
|
126
|
+
nameBytes.copy(central, 46);
|
|
127
|
+
centralHeaders.push(central);
|
|
128
|
+
|
|
129
|
+
offset += local.length + compressed.length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const centralDir = Buffer.concat(centralHeaders);
|
|
133
|
+
const eocd = Buffer.alloc(22);
|
|
134
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
135
|
+
eocd.writeUInt16LE(0, 4); // disk number
|
|
136
|
+
eocd.writeUInt16LE(0, 6); // central dir disk
|
|
137
|
+
eocd.writeUInt16LE(entries.length, 8); // entries on disk
|
|
138
|
+
eocd.writeUInt16LE(entries.length, 10); // total entries
|
|
139
|
+
eocd.writeUInt32LE(centralDir.length, 12);
|
|
140
|
+
eocd.writeUInt32LE(offset, 16); // central dir offset
|
|
141
|
+
eocd.writeUInt16LE(0, 20); // comment length
|
|
142
|
+
|
|
143
|
+
return Buffer.concat([...localHeaders, centralDir, eocd]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function crc32(buf) {
|
|
147
|
+
let crc = 0xFFFFFFFF;
|
|
148
|
+
for (let i = 0; i < buf.length; i++) {
|
|
149
|
+
crc ^= buf[i];
|
|
150
|
+
for (let j = 0; j < 8; j++) {
|
|
151
|
+
crc = (crc >>> 1) ^ ((crc & 1) ? 0xEDB88320 : 0);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const entries = zipFiles
|
|
158
|
+
.filter(f => fs.existsSync(f.src))
|
|
159
|
+
.map(f => ({ name: f.name, data: fs.readFileSync(f.src) }));
|
|
160
|
+
|
|
161
|
+
const zipBuf = buildZip(entries);
|
|
162
|
+
const zipPath = path.join(DIST, 'zquery.dist.zip');
|
|
163
|
+
fs.writeFileSync(zipPath, zipBuf);
|
|
164
|
+
console.log(` ✓ dist/zquery.dist.zip (${sizeKB(zipBuf)} KB) — ${entries.length} files`);
|
|
165
|
+
|
|
57
166
|
return { DIST, OUT_FILE, MIN_FILE };
|
|
58
167
|
}
|
|
59
168
|
|
package/cli/commands/bundle.js
CHANGED
|
@@ -353,7 +353,7 @@ function _collapseTemplateCSS(tpl) {
|
|
|
353
353
|
|
|
354
354
|
/**
|
|
355
355
|
* Scan bundled source files for external resource references
|
|
356
|
-
* (
|
|
356
|
+
* (templateUrl, styleUrl) and return a map of
|
|
357
357
|
* { relativePath: fileContent } for inlining.
|
|
358
358
|
*/
|
|
359
359
|
function collectInlineResources(files, projectRoot) {
|
|
@@ -363,33 +363,6 @@ function collectInlineResources(files, projectRoot) {
|
|
|
363
363
|
const code = fs.readFileSync(file, 'utf-8');
|
|
364
364
|
const fileDir = path.dirname(file);
|
|
365
365
|
|
|
366
|
-
// pages: config
|
|
367
|
-
const pagesMatch = code.match(/pages\s*:\s*\{[^}]*dir\s*:\s*['"]([^'"]+)['"]/s);
|
|
368
|
-
if (pagesMatch) {
|
|
369
|
-
const pagesDir = pagesMatch[1];
|
|
370
|
-
const ext = (code.match(/pages\s*:\s*\{[^}]*ext\s*:\s*['"]([^'"]+)['"]/s) || [])[1] || '.html';
|
|
371
|
-
const itemsMatch = code.match(/items\s*:\s*\[([\s\S]*?)\]/);
|
|
372
|
-
if (itemsMatch) {
|
|
373
|
-
const itemsBlock = itemsMatch[1];
|
|
374
|
-
const ids = [];
|
|
375
|
-
let m;
|
|
376
|
-
const strRe = /['"]([^'"]+)['"]/g;
|
|
377
|
-
while ((m = strRe.exec(itemsBlock)) !== null) {
|
|
378
|
-
const before = itemsBlock.substring(Math.max(0, m.index - 20), m.index);
|
|
379
|
-
if (/label\s*:\s*$/.test(before)) continue;
|
|
380
|
-
ids.push(m[1]);
|
|
381
|
-
}
|
|
382
|
-
const absPagesDir = path.join(fileDir, pagesDir);
|
|
383
|
-
for (const id of ids) {
|
|
384
|
-
const pagePath = path.join(absPagesDir, id + ext);
|
|
385
|
-
if (fs.existsSync(pagePath)) {
|
|
386
|
-
const relKey = path.relative(projectRoot, pagePath).replace(/\\/g, '/');
|
|
387
|
-
inlineMap[relKey] = fs.readFileSync(pagePath, 'utf-8');
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
366
|
// styleUrl:
|
|
394
367
|
const styleUrlRe = /styleUrl\s*:\s*['"]([^'"]+)['"]/g;
|
|
395
368
|
const styleMatch = styleUrlRe.exec(code);
|
|
@@ -409,6 +382,25 @@ function collectInlineResources(files, projectRoot) {
|
|
|
409
382
|
const relKey = path.relative(projectRoot, tmplPath).replace(/\\/g, '/');
|
|
410
383
|
inlineMap[relKey] = fs.readFileSync(tmplPath, 'utf-8');
|
|
411
384
|
}
|
|
385
|
+
} else if (/templateUrl\s*:/.test(code)) {
|
|
386
|
+
// Dynamic templateUrl (e.g. Object.fromEntries, computed map) —
|
|
387
|
+
// inline all .html files in the component's directory tree so
|
|
388
|
+
// the runtime __zqInline lookup can resolve them by suffix.
|
|
389
|
+
(function scanHtml(dir) {
|
|
390
|
+
try {
|
|
391
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
392
|
+
const full = path.join(dir, entry.name);
|
|
393
|
+
if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
394
|
+
const relKey = path.relative(projectRoot, full).replace(/\\/g, '/');
|
|
395
|
+
if (!inlineMap[relKey]) {
|
|
396
|
+
inlineMap[relKey] = fs.readFileSync(full, 'utf-8');
|
|
397
|
+
}
|
|
398
|
+
} else if (entry.isDirectory()) {
|
|
399
|
+
scanHtml(full);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch { /* permission error — skip */ }
|
|
403
|
+
})(fileDir);
|
|
412
404
|
}
|
|
413
405
|
}
|
|
414
406
|
|
|
@@ -521,7 +513,7 @@ function detectEntry(projectRoot) {
|
|
|
521
513
|
}
|
|
522
514
|
|
|
523
515
|
// 3. Convention fallbacks
|
|
524
|
-
const fallbacks = ['scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
|
|
516
|
+
const fallbacks = ['app/app.js', 'scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
|
|
525
517
|
for (const f of fallbacks) {
|
|
526
518
|
const fp = path.join(projectRoot, f);
|
|
527
519
|
if (fs.existsSync(fp)) return fp;
|
|
@@ -539,7 +531,7 @@ function detectEntry(projectRoot) {
|
|
|
539
531
|
* server/index.html — <base href="/"> for SPA deep routes
|
|
540
532
|
* local/index.html — relative paths for file:// access
|
|
541
533
|
*/
|
|
542
|
-
function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir) {
|
|
534
|
+
function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir, globalCssOrigHref, globalCssHash) {
|
|
543
535
|
const htmlPath = path.resolve(projectRoot, htmlRelPath);
|
|
544
536
|
if (!fs.existsSync(htmlPath)) {
|
|
545
537
|
console.warn(` ⚠ HTML file not found: ${htmlRelPath}`);
|
|
@@ -647,6 +639,15 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
|
|
|
647
639
|
);
|
|
648
640
|
}
|
|
649
641
|
|
|
642
|
+
// Rewrite global CSS link to hashed version
|
|
643
|
+
if (globalCssOrigHref && globalCssHash) {
|
|
644
|
+
const escapedHref = globalCssOrigHref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
645
|
+
const cssLinkRe = new RegExp(
|
|
646
|
+
`(<link[^>]+href\\s*=\\s*["'])${escapedHref}(["'][^>]*>)`, 'i'
|
|
647
|
+
);
|
|
648
|
+
html = html.replace(cssLinkRe, `$1${globalCssHash}$2`);
|
|
649
|
+
}
|
|
650
|
+
|
|
650
651
|
const serverHtml = html;
|
|
651
652
|
const localHtml = html.replace(/<base\s+href\s*=\s*["']\/["'][^>]*>\s*\n?\s*/i, '');
|
|
652
653
|
|
|
@@ -664,19 +665,26 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
|
|
|
664
665
|
|
|
665
666
|
/**
|
|
666
667
|
* Copy the entire app directory into both dist/server and dist/local,
|
|
667
|
-
* skipping only build outputs
|
|
668
|
-
* assets (icons/, images/, fonts/,
|
|
669
|
-
* are available in the built output without maintaining
|
|
668
|
+
* skipping only build outputs, tooling dirs, and the app/ source dir.
|
|
669
|
+
* This ensures all static assets (assets/, icons/, images/, fonts/,
|
|
670
|
+
* manifests, etc.) are available in the built output without maintaining
|
|
671
|
+
* a fragile whitelist.
|
|
670
672
|
*/
|
|
671
673
|
function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
|
|
672
|
-
const SKIP_DIRS = new Set(['dist', 'node_modules', '.git', '.vscode'
|
|
674
|
+
const SKIP_DIRS = new Set(['dist', 'node_modules', '.git', '.vscode']);
|
|
675
|
+
|
|
676
|
+
// Case-insensitive match for App/ directory (contains bundled source)
|
|
677
|
+
function isAppDir(name) {
|
|
678
|
+
return name.toLowerCase() === 'app';
|
|
679
|
+
}
|
|
673
680
|
|
|
674
681
|
let copiedCount = 0;
|
|
675
682
|
|
|
676
683
|
function copyEntry(srcPath, relPath) {
|
|
677
684
|
const stat = fs.statSync(srcPath);
|
|
678
685
|
if (stat.isDirectory()) {
|
|
679
|
-
|
|
686
|
+
const dirName = path.basename(srcPath);
|
|
687
|
+
if (SKIP_DIRS.has(dirName) || isAppDir(dirName)) return;
|
|
680
688
|
for (const child of fs.readdirSync(srcPath)) {
|
|
681
689
|
copyEntry(path.join(srcPath, child), path.join(relPath, child));
|
|
682
690
|
}
|
|
@@ -695,7 +703,7 @@ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
|
|
|
695
703
|
|
|
696
704
|
for (const entry of fs.readdirSync(appRoot, { withFileTypes: true })) {
|
|
697
705
|
if (entry.isDirectory()) {
|
|
698
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
706
|
+
if (SKIP_DIRS.has(entry.name) || isAppDir(entry.name)) continue;
|
|
699
707
|
copyEntry(path.join(appRoot, entry.name), entry.name);
|
|
700
708
|
} else {
|
|
701
709
|
copyEntry(path.join(appRoot, entry.name), entry.name);
|
|
@@ -714,6 +722,7 @@ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
|
|
|
714
722
|
function bundleApp() {
|
|
715
723
|
const projectRoot = process.cwd();
|
|
716
724
|
const minimal = flag('minimal', 'm');
|
|
725
|
+
const globalCssOverride = option('global-css', null, null);
|
|
717
726
|
|
|
718
727
|
// Entry point — positional arg (directory or file) or auto-detection
|
|
719
728
|
let entry = null;
|
|
@@ -833,14 +842,18 @@ function bundleApp() {
|
|
|
833
842
|
console.log(` Output: ${path.relative(projectRoot, baseDistDir)}/server/ & local/`);
|
|
834
843
|
console.log(` Library: embedded`);
|
|
835
844
|
console.log(` HTML: ${htmlFile || 'not found (no HTML detected)'}`);
|
|
836
|
-
if (minimal) console.log(` Mode: minimal (HTML +
|
|
845
|
+
if (minimal) console.log(` Mode: minimal (HTML + JS + global CSS only)`);
|
|
837
846
|
console.log('');
|
|
838
847
|
|
|
839
848
|
// ------ doBuild (inlined) ------
|
|
840
849
|
const start = Date.now();
|
|
841
850
|
|
|
842
|
-
|
|
843
|
-
|
|
851
|
+
// Clean previous dist outputs for a fresh build
|
|
852
|
+
for (const dir of [serverDir, localDir]) {
|
|
853
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true });
|
|
854
|
+
}
|
|
855
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
856
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
844
857
|
|
|
845
858
|
const files = walkImportGraph(entry);
|
|
846
859
|
console.log(` Resolved ${files.length} module(s):`);
|
|
@@ -876,8 +889,24 @@ function bundleApp() {
|
|
|
876
889
|
}
|
|
877
890
|
|
|
878
891
|
const htmlDir = htmlAbs ? path.dirname(htmlAbs) : null;
|
|
892
|
+
|
|
893
|
+
// Case-insensitive search for Assets/ directory
|
|
894
|
+
function findAssetsDir(root) {
|
|
895
|
+
try {
|
|
896
|
+
for (const e of fs.readdirSync(root, { withFileTypes: true })) {
|
|
897
|
+
if (e.isDirectory() && e.name.toLowerCase() === 'assets') return path.join(root, e.name);
|
|
898
|
+
}
|
|
899
|
+
} catch { /* skip */ }
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const assetsDir = findAssetsDir(htmlDir || projectRoot);
|
|
904
|
+
const altAssetsDir = htmlDir ? findAssetsDir(projectRoot) : null;
|
|
905
|
+
|
|
879
906
|
const libCandidates = [
|
|
880
907
|
path.join(pkgRoot, 'dist/zquery.min.js'),
|
|
908
|
+
assetsDir && path.join(assetsDir, 'scripts/zquery.min.js'),
|
|
909
|
+
altAssetsDir && path.join(altAssetsDir, 'scripts/zquery.min.js'),
|
|
881
910
|
htmlDir && path.join(htmlDir, 'scripts/vendor/zquery.min.js'),
|
|
882
911
|
htmlDir && path.join(htmlDir, 'vendor/zquery.min.js'),
|
|
883
912
|
path.join(projectRoot, 'scripts/vendor/zquery.min.js'),
|
|
@@ -895,7 +924,7 @@ function bundleApp() {
|
|
|
895
924
|
console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
|
|
896
925
|
} else {
|
|
897
926
|
console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
|
|
898
|
-
console.warn(` Place zquery.min.js in scripts
|
|
927
|
+
console.warn(` Place zquery.min.js in assets/scripts/, vendor/, lib/, or dist/`);
|
|
899
928
|
}
|
|
900
929
|
}
|
|
901
930
|
|
|
@@ -921,8 +950,7 @@ function bundleApp() {
|
|
|
921
950
|
|
|
922
951
|
// Content-hashed filenames
|
|
923
952
|
const contentHash = crypto.createHash('sha256').update(bundle).digest('hex').slice(0, 8);
|
|
924
|
-
const
|
|
925
|
-
const minBase = `z-${entryName}.${contentHash}.min.js`;
|
|
953
|
+
const minBase = `z-${entryName}.${contentHash}.min.js`;
|
|
926
954
|
|
|
927
955
|
// Clean previous builds
|
|
928
956
|
const escName = entryName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -935,21 +963,70 @@ function bundleApp() {
|
|
|
935
963
|
}
|
|
936
964
|
}
|
|
937
965
|
|
|
938
|
-
// Write
|
|
939
|
-
const
|
|
940
|
-
const minFile = path.join(serverDir, minBase);
|
|
941
|
-
fs.writeFileSync(bundleFile, bundle, 'utf-8');
|
|
966
|
+
// Write minified bundle
|
|
967
|
+
const minFile = path.join(serverDir, minBase);
|
|
942
968
|
fs.writeFileSync(minFile, minify(bundle, banner), 'utf-8');
|
|
943
|
-
fs.copyFileSync(bundleFile, path.join(localDir, bundleBase));
|
|
944
969
|
fs.copyFileSync(minFile, path.join(localDir, minBase));
|
|
945
970
|
|
|
946
|
-
console.log(`\n ✓ ${
|
|
947
|
-
|
|
971
|
+
console.log(`\n ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
|
|
972
|
+
|
|
973
|
+
// ------------------------------------------------------------------
|
|
974
|
+
// Global CSS bundling — extract from index.html <link> or --global-css
|
|
975
|
+
// ------------------------------------------------------------------
|
|
976
|
+
let globalCssHash = null;
|
|
977
|
+
let globalCssOrigHref = null;
|
|
978
|
+
let globalCssPath = null;
|
|
979
|
+
if (htmlAbs) {
|
|
980
|
+
const htmlContent = fs.readFileSync(htmlAbs, 'utf-8');
|
|
981
|
+
const htmlDir = path.dirname(htmlAbs);
|
|
982
|
+
|
|
983
|
+
// Determine global CSS path: --global-css flag overrides, else first <link rel="stylesheet"> in HTML
|
|
984
|
+
globalCssPath = null;
|
|
985
|
+
if (globalCssOverride) {
|
|
986
|
+
globalCssPath = path.resolve(projectRoot, globalCssOverride);
|
|
987
|
+
// Reconstruct relative href for HTML rewriting
|
|
988
|
+
globalCssOrigHref = path.relative(htmlDir, globalCssPath).replace(/\\/g, '/');
|
|
989
|
+
} else {
|
|
990
|
+
const linkRe = /<link[^>]+rel\s*=\s*["']stylesheet["'][^>]+href\s*=\s*["']([^"']+)["']/gi;
|
|
991
|
+
const altRe = /<link[^>]+href\s*=\s*["']([^"']+)["'][^>]+rel\s*=\s*["']stylesheet["']/gi;
|
|
992
|
+
let linkMatch = linkRe.exec(htmlContent) || altRe.exec(htmlContent);
|
|
993
|
+
if (linkMatch) {
|
|
994
|
+
globalCssOrigHref = linkMatch[1];
|
|
995
|
+
// Strip query string / fragment so the path resolves to the actual file
|
|
996
|
+
const cleanHref = linkMatch[1].split('?')[0].split('#')[0];
|
|
997
|
+
globalCssPath = path.resolve(htmlDir, cleanHref);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (globalCssPath && fs.existsSync(globalCssPath)) {
|
|
1002
|
+
let cssContent = fs.readFileSync(globalCssPath, 'utf-8');
|
|
1003
|
+
const cssMin = minifyCSS(cssContent);
|
|
1004
|
+
const cssHash = crypto.createHash('sha256').update(cssMin).digest('hex').slice(0, 8);
|
|
1005
|
+
const cssOutName = `global.${cssHash}.min.css`;
|
|
1006
|
+
|
|
1007
|
+
// Clean previous global CSS builds
|
|
1008
|
+
const cssCleanRe = /^global\.[a-f0-9]{8}\.min\.css$/;
|
|
1009
|
+
for (const dir of [serverDir, localDir]) {
|
|
1010
|
+
if (fs.existsSync(dir)) {
|
|
1011
|
+
for (const f of fs.readdirSync(dir)) {
|
|
1012
|
+
if (cssCleanRe.test(f)) fs.unlinkSync(path.join(dir, f));
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
fs.writeFileSync(path.join(serverDir, cssOutName), cssMin, 'utf-8');
|
|
1018
|
+
fs.writeFileSync(path.join(localDir, cssOutName), cssMin, 'utf-8');
|
|
1019
|
+
globalCssHash = cssOutName;
|
|
1020
|
+
console.log(` ✓ ${cssOutName} (${sizeKB(Buffer.from(cssMin))} KB)`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
948
1023
|
|
|
949
1024
|
// Rewrite HTML to reference the minified bundle
|
|
950
1025
|
const bundledFileSet = new Set(files);
|
|
1026
|
+
// Skip the original unminified global CSS from static asset copying
|
|
1027
|
+
if (globalCssPath) bundledFileSet.add(path.resolve(globalCssPath));
|
|
951
1028
|
if (htmlFile) {
|
|
952
|
-
rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir);
|
|
1029
|
+
rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir, globalCssOrigHref, globalCssHash);
|
|
953
1030
|
}
|
|
954
1031
|
|
|
955
1032
|
// Copy static asset directories (icons/, images/, fonts/, etc.)
|
package/cli/commands/create.js
CHANGED
|
@@ -26,7 +26,7 @@ function createProject(args) {
|
|
|
26
26
|
const name = path.basename(target);
|
|
27
27
|
|
|
28
28
|
// Guard: refuse to overwrite existing files
|
|
29
|
-
const conflicts = ['index.html', '
|
|
29
|
+
const conflicts = ['index.html', 'global.css', 'app', 'assets'].filter(f =>
|
|
30
30
|
fs.existsSync(path.join(target, f))
|
|
31
31
|
);
|
|
32
32
|
if (conflicts.length) {
|