zero-query 0.7.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -27
- package/cli/commands/build.js +110 -1
- package/cli/commands/bundle.js +107 -22
- 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 +746 -134
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +11 -9
- package/index.js +15 -10
- package/package.json +3 -2
- package/src/component.js +161 -48
- 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 +195 -6
- 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 +7 -0
- 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`:
|
|
@@ -183,7 +191,8 @@ dist/
|
|
|
183
191
|
index.html
|
|
184
192
|
z-app.<hash>.js
|
|
185
193
|
z-app.<hash>.min.js
|
|
186
|
-
|
|
194
|
+
global.<hash>.min.css
|
|
195
|
+
assets/
|
|
187
196
|
local/ ← open from disk (file://) — no server needed
|
|
188
197
|
index.html
|
|
189
198
|
z-app.<hash>.js
|
|
@@ -196,7 +205,8 @@ dist/
|
|
|
196
205
|
| --- | --- | --- |
|
|
197
206
|
| `--out <path>` | `-o` | Custom output directory |
|
|
198
207
|
| `--index <file>` | `-i` | Index HTML file (default: auto-detected) |
|
|
199
|
-
| `--minimal` | `-m` | Only output HTML
|
|
208
|
+
| `--minimal` | `-m` | Only output HTML, bundled JS, and global CSS (skip static assets) |
|
|
209
|
+
| `--global-css <path>` | | Override global CSS input file (default: first `<link>` in HTML) |
|
|
200
210
|
|
|
201
211
|
### What the Bundler Does
|
|
202
212
|
|
|
@@ -204,7 +214,7 @@ dist/
|
|
|
204
214
|
1. **HTML files** — `index.html` is checked first, then other `.html` files (root + one level deep).
|
|
205
215
|
2. **Module scripts within HTML** — within each HTML file, a `<script type="module">` whose `src` resolves to `app.js` wins; otherwise the first module script tag is used.
|
|
206
216
|
3. **JS file scan** — if no HTML match, JS files (up to 2 levels deep) are scanned in two passes: first for `$.router(` (the canonical app entry point), then for `$.mount(`, `$.store(`, or `mountAll(`.
|
|
207
|
-
4. **Convention fallbacks** — `scripts/app.js`, `src/app.js`, `js/app.js`, `app.js`, `main.js`.
|
|
217
|
+
4. **Convention fallbacks** — `app/app.js`, `scripts/app.js`, `src/app.js`, `js/app.js`, `app.js`, `main.js`.
|
|
208
218
|
2. Resolves all `import` statements and topologically sorts dependencies
|
|
209
219
|
3. Strips `import`/`export` syntax, wraps in an IIFE
|
|
210
220
|
4. Embeds zQuery library and inlines `templateUrl` / `styleUrl` / `pages` files
|
|
@@ -246,8 +256,8 @@ location / {
|
|
|
246
256
|
| `$.create` | Element factory |
|
|
247
257
|
| `$.ready` `$.on` `$.off` | DOM ready, global event delegation & direct listeners |
|
|
248
258
|
| `$.fn` | Collection prototype (extend it) |
|
|
249
|
-
| `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` | Component system |
|
|
250
|
-
| `$.morph` | DOM morphing engine —
|
|
259
|
+
| `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` `$.prefetch` | Component system |
|
|
260
|
+
| `$.morph` `$.morphElement` | DOM morphing engine — LIS-based keyed reconciliation, `isEqualNode()` bail-outs, `z-skip` opt-out. Patches existing DOM to match new HTML without destroying unchanged nodes. Auto-key detection (`id`, `data-id`, `data-key`) — no `z-key` required. `$().html()` and `$().replaceWith()` auto-morph existing content; `$().morph()` for explicit morph |
|
|
251
261
|
| `$.safeEval` | CSP-safe expression evaluator (replaces `eval` / `new Function`) |
|
|
252
262
|
| `$.style` | Dynamically load global stylesheet file(s) at runtime |
|
|
253
263
|
| `$.router` `$.getRouter` | SPA router |
|
|
@@ -260,16 +270,16 @@ location / {
|
|
|
260
270
|
| `$.param` `$.parseQuery` | URL utils |
|
|
261
271
|
| `$.storage` `$.session` | Storage wrappers |
|
|
262
272
|
| `$.bus` | Event bus |
|
|
263
|
-
| `$.version` | Library version |
|
|
273
|
+
| `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~91 KB\"`) |
|
|
264
274
|
| `$.meta` | Build metadata (populated by CLI bundler) |
|
|
265
275
|
| `$.noConflict` | Release `$` global |
|
|
266
276
|
|
|
267
277
|
| CLI Command | Description |
|
|
268
278
|
| --- | --- |
|
|
269
279
|
| `zquery create [dir]` | Scaffold a new project (index.html, components, store, styles) |
|
|
270
|
-
| `zquery dev [root]` | Dev server with live-reload &
|
|
280
|
+
| `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
281
|
| `zquery bundle [dir\|file]` | Bundle app into a single IIFE file. Accepts dir or direct entry file. |
|
|
272
|
-
| `zquery build` | Build the zQuery library (`dist/
|
|
282
|
+
| `zquery build` | Build the zQuery library (`dist/zquery.min.js`) |
|
|
273
283
|
| `zquery --help` | Show CLI usage |
|
|
274
284
|
|
|
275
285
|
For full method signatures, options, and examples, see **[API.md](API.md)**.
|
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
|
@@ -521,7 +521,7 @@ function detectEntry(projectRoot) {
|
|
|
521
521
|
}
|
|
522
522
|
|
|
523
523
|
// 3. Convention fallbacks
|
|
524
|
-
const fallbacks = ['scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
|
|
524
|
+
const fallbacks = ['app/app.js', 'scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
|
|
525
525
|
for (const f of fallbacks) {
|
|
526
526
|
const fp = path.join(projectRoot, f);
|
|
527
527
|
if (fs.existsSync(fp)) return fp;
|
|
@@ -539,7 +539,7 @@ function detectEntry(projectRoot) {
|
|
|
539
539
|
* server/index.html — <base href="/"> for SPA deep routes
|
|
540
540
|
* local/index.html — relative paths for file:// access
|
|
541
541
|
*/
|
|
542
|
-
function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir) {
|
|
542
|
+
function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir, globalCssOrigHref, globalCssHash) {
|
|
543
543
|
const htmlPath = path.resolve(projectRoot, htmlRelPath);
|
|
544
544
|
if (!fs.existsSync(htmlPath)) {
|
|
545
545
|
console.warn(` ⚠ HTML file not found: ${htmlRelPath}`);
|
|
@@ -647,6 +647,15 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
|
|
|
647
647
|
);
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
+
// Rewrite global CSS link to hashed version
|
|
651
|
+
if (globalCssOrigHref && globalCssHash) {
|
|
652
|
+
const escapedHref = globalCssOrigHref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
653
|
+
const cssLinkRe = new RegExp(
|
|
654
|
+
`(<link[^>]+href\\s*=\\s*["'])${escapedHref}(["'][^>]*>)`, 'i'
|
|
655
|
+
);
|
|
656
|
+
html = html.replace(cssLinkRe, `$1${globalCssHash}$2`);
|
|
657
|
+
}
|
|
658
|
+
|
|
650
659
|
const serverHtml = html;
|
|
651
660
|
const localHtml = html.replace(/<base\s+href\s*=\s*["']\/["'][^>]*>\s*\n?\s*/i, '');
|
|
652
661
|
|
|
@@ -664,19 +673,26 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
|
|
|
664
673
|
|
|
665
674
|
/**
|
|
666
675
|
* Copy the entire app directory into both dist/server and dist/local,
|
|
667
|
-
* skipping only build outputs
|
|
668
|
-
* assets (icons/, images/, fonts/,
|
|
669
|
-
* are available in the built output without maintaining
|
|
676
|
+
* skipping only build outputs, tooling dirs, and the app/ source dir.
|
|
677
|
+
* This ensures all static assets (assets/, icons/, images/, fonts/,
|
|
678
|
+
* manifests, etc.) are available in the built output without maintaining
|
|
679
|
+
* a fragile whitelist.
|
|
670
680
|
*/
|
|
671
681
|
function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
|
|
672
|
-
const SKIP_DIRS = new Set(['dist', 'node_modules', '.git', '.vscode'
|
|
682
|
+
const SKIP_DIRS = new Set(['dist', 'node_modules', '.git', '.vscode']);
|
|
683
|
+
|
|
684
|
+
// Case-insensitive match for App/ directory (contains bundled source)
|
|
685
|
+
function isAppDir(name) {
|
|
686
|
+
return name.toLowerCase() === 'app';
|
|
687
|
+
}
|
|
673
688
|
|
|
674
689
|
let copiedCount = 0;
|
|
675
690
|
|
|
676
691
|
function copyEntry(srcPath, relPath) {
|
|
677
692
|
const stat = fs.statSync(srcPath);
|
|
678
693
|
if (stat.isDirectory()) {
|
|
679
|
-
|
|
694
|
+
const dirName = path.basename(srcPath);
|
|
695
|
+
if (SKIP_DIRS.has(dirName) || isAppDir(dirName)) return;
|
|
680
696
|
for (const child of fs.readdirSync(srcPath)) {
|
|
681
697
|
copyEntry(path.join(srcPath, child), path.join(relPath, child));
|
|
682
698
|
}
|
|
@@ -695,7 +711,7 @@ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
|
|
|
695
711
|
|
|
696
712
|
for (const entry of fs.readdirSync(appRoot, { withFileTypes: true })) {
|
|
697
713
|
if (entry.isDirectory()) {
|
|
698
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
714
|
+
if (SKIP_DIRS.has(entry.name) || isAppDir(entry.name)) continue;
|
|
699
715
|
copyEntry(path.join(appRoot, entry.name), entry.name);
|
|
700
716
|
} else {
|
|
701
717
|
copyEntry(path.join(appRoot, entry.name), entry.name);
|
|
@@ -714,6 +730,7 @@ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
|
|
|
714
730
|
function bundleApp() {
|
|
715
731
|
const projectRoot = process.cwd();
|
|
716
732
|
const minimal = flag('minimal', 'm');
|
|
733
|
+
const globalCssOverride = option('global-css', null, null);
|
|
717
734
|
|
|
718
735
|
// Entry point — positional arg (directory or file) or auto-detection
|
|
719
736
|
let entry = null;
|
|
@@ -833,14 +850,18 @@ function bundleApp() {
|
|
|
833
850
|
console.log(` Output: ${path.relative(projectRoot, baseDistDir)}/server/ & local/`);
|
|
834
851
|
console.log(` Library: embedded`);
|
|
835
852
|
console.log(` HTML: ${htmlFile || 'not found (no HTML detected)'}`);
|
|
836
|
-
if (minimal) console.log(` Mode: minimal (HTML +
|
|
853
|
+
if (minimal) console.log(` Mode: minimal (HTML + JS + global CSS only)`);
|
|
837
854
|
console.log('');
|
|
838
855
|
|
|
839
856
|
// ------ doBuild (inlined) ------
|
|
840
857
|
const start = Date.now();
|
|
841
858
|
|
|
842
|
-
|
|
843
|
-
|
|
859
|
+
// Clean previous dist outputs for a fresh build
|
|
860
|
+
for (const dir of [serverDir, localDir]) {
|
|
861
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true });
|
|
862
|
+
}
|
|
863
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
864
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
844
865
|
|
|
845
866
|
const files = walkImportGraph(entry);
|
|
846
867
|
console.log(` Resolved ${files.length} module(s):`);
|
|
@@ -876,8 +897,24 @@ function bundleApp() {
|
|
|
876
897
|
}
|
|
877
898
|
|
|
878
899
|
const htmlDir = htmlAbs ? path.dirname(htmlAbs) : null;
|
|
900
|
+
|
|
901
|
+
// Case-insensitive search for Assets/ directory
|
|
902
|
+
function findAssetsDir(root) {
|
|
903
|
+
try {
|
|
904
|
+
for (const e of fs.readdirSync(root, { withFileTypes: true })) {
|
|
905
|
+
if (e.isDirectory() && e.name.toLowerCase() === 'assets') return path.join(root, e.name);
|
|
906
|
+
}
|
|
907
|
+
} catch { /* skip */ }
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const assetsDir = findAssetsDir(htmlDir || projectRoot);
|
|
912
|
+
const altAssetsDir = htmlDir ? findAssetsDir(projectRoot) : null;
|
|
913
|
+
|
|
879
914
|
const libCandidates = [
|
|
880
915
|
path.join(pkgRoot, 'dist/zquery.min.js'),
|
|
916
|
+
assetsDir && path.join(assetsDir, 'scripts/zquery.min.js'),
|
|
917
|
+
altAssetsDir && path.join(altAssetsDir, 'scripts/zquery.min.js'),
|
|
881
918
|
htmlDir && path.join(htmlDir, 'scripts/vendor/zquery.min.js'),
|
|
882
919
|
htmlDir && path.join(htmlDir, 'vendor/zquery.min.js'),
|
|
883
920
|
path.join(projectRoot, 'scripts/vendor/zquery.min.js'),
|
|
@@ -895,7 +932,7 @@ function bundleApp() {
|
|
|
895
932
|
console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
|
|
896
933
|
} else {
|
|
897
934
|
console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
|
|
898
|
-
console.warn(` Place zquery.min.js in scripts
|
|
935
|
+
console.warn(` Place zquery.min.js in assets/scripts/, vendor/, lib/, or dist/`);
|
|
899
936
|
}
|
|
900
937
|
}
|
|
901
938
|
|
|
@@ -921,8 +958,7 @@ function bundleApp() {
|
|
|
921
958
|
|
|
922
959
|
// Content-hashed filenames
|
|
923
960
|
const contentHash = crypto.createHash('sha256').update(bundle).digest('hex').slice(0, 8);
|
|
924
|
-
const
|
|
925
|
-
const minBase = `z-${entryName}.${contentHash}.min.js`;
|
|
961
|
+
const minBase = `z-${entryName}.${contentHash}.min.js`;
|
|
926
962
|
|
|
927
963
|
// Clean previous builds
|
|
928
964
|
const escName = entryName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -935,21 +971,70 @@ function bundleApp() {
|
|
|
935
971
|
}
|
|
936
972
|
}
|
|
937
973
|
|
|
938
|
-
// Write
|
|
939
|
-
const
|
|
940
|
-
const minFile = path.join(serverDir, minBase);
|
|
941
|
-
fs.writeFileSync(bundleFile, bundle, 'utf-8');
|
|
974
|
+
// Write minified bundle
|
|
975
|
+
const minFile = path.join(serverDir, minBase);
|
|
942
976
|
fs.writeFileSync(minFile, minify(bundle, banner), 'utf-8');
|
|
943
|
-
fs.copyFileSync(bundleFile, path.join(localDir, bundleBase));
|
|
944
977
|
fs.copyFileSync(minFile, path.join(localDir, minBase));
|
|
945
978
|
|
|
946
|
-
console.log(`\n ✓ ${
|
|
947
|
-
|
|
979
|
+
console.log(`\n ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
|
|
980
|
+
|
|
981
|
+
// ------------------------------------------------------------------
|
|
982
|
+
// Global CSS bundling — extract from index.html <link> or --global-css
|
|
983
|
+
// ------------------------------------------------------------------
|
|
984
|
+
let globalCssHash = null;
|
|
985
|
+
let globalCssOrigHref = null;
|
|
986
|
+
let globalCssPath = null;
|
|
987
|
+
if (htmlAbs) {
|
|
988
|
+
const htmlContent = fs.readFileSync(htmlAbs, 'utf-8');
|
|
989
|
+
const htmlDir = path.dirname(htmlAbs);
|
|
990
|
+
|
|
991
|
+
// Determine global CSS path: --global-css flag overrides, else first <link rel="stylesheet"> in HTML
|
|
992
|
+
globalCssPath = null;
|
|
993
|
+
if (globalCssOverride) {
|
|
994
|
+
globalCssPath = path.resolve(projectRoot, globalCssOverride);
|
|
995
|
+
// Reconstruct relative href for HTML rewriting
|
|
996
|
+
globalCssOrigHref = path.relative(htmlDir, globalCssPath).replace(/\\/g, '/');
|
|
997
|
+
} else {
|
|
998
|
+
const linkRe = /<link[^>]+rel\s*=\s*["']stylesheet["'][^>]+href\s*=\s*["']([^"']+)["']/gi;
|
|
999
|
+
const altRe = /<link[^>]+href\s*=\s*["']([^"']+)["'][^>]+rel\s*=\s*["']stylesheet["']/gi;
|
|
1000
|
+
let linkMatch = linkRe.exec(htmlContent) || altRe.exec(htmlContent);
|
|
1001
|
+
if (linkMatch) {
|
|
1002
|
+
globalCssOrigHref = linkMatch[1];
|
|
1003
|
+
// Strip query string / fragment so the path resolves to the actual file
|
|
1004
|
+
const cleanHref = linkMatch[1].split('?')[0].split('#')[0];
|
|
1005
|
+
globalCssPath = path.resolve(htmlDir, cleanHref);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (globalCssPath && fs.existsSync(globalCssPath)) {
|
|
1010
|
+
let cssContent = fs.readFileSync(globalCssPath, 'utf-8');
|
|
1011
|
+
const cssMin = minifyCSS(cssContent);
|
|
1012
|
+
const cssHash = crypto.createHash('sha256').update(cssMin).digest('hex').slice(0, 8);
|
|
1013
|
+
const cssOutName = `global.${cssHash}.min.css`;
|
|
1014
|
+
|
|
1015
|
+
// Clean previous global CSS builds
|
|
1016
|
+
const cssCleanRe = /^global\.[a-f0-9]{8}\.min\.css$/;
|
|
1017
|
+
for (const dir of [serverDir, localDir]) {
|
|
1018
|
+
if (fs.existsSync(dir)) {
|
|
1019
|
+
for (const f of fs.readdirSync(dir)) {
|
|
1020
|
+
if (cssCleanRe.test(f)) fs.unlinkSync(path.join(dir, f));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
fs.writeFileSync(path.join(serverDir, cssOutName), cssMin, 'utf-8');
|
|
1026
|
+
fs.writeFileSync(path.join(localDir, cssOutName), cssMin, 'utf-8');
|
|
1027
|
+
globalCssHash = cssOutName;
|
|
1028
|
+
console.log(` ✓ ${cssOutName} (${sizeKB(Buffer.from(cssMin))} KB)`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
948
1031
|
|
|
949
1032
|
// Rewrite HTML to reference the minified bundle
|
|
950
1033
|
const bundledFileSet = new Set(files);
|
|
1034
|
+
// Skip the original unminified global CSS from static asset copying
|
|
1035
|
+
if (globalCssPath) bundledFileSet.add(path.resolve(globalCssPath));
|
|
951
1036
|
if (htmlFile) {
|
|
952
|
-
rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir);
|
|
1037
|
+
rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir, globalCssOrigHref, globalCssHash);
|
|
953
1038
|
}
|
|
954
1039
|
|
|
955
1040
|
// Copy static asset directories (icons/, images/, fonts/, etc.)
|
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) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/devtools/index.js — DevTools HTML assembler
|
|
3
|
+
*
|
|
4
|
+
* Reads CSS, HTML, and JS partials from this folder and concatenates them
|
|
5
|
+
* into a single self-contained HTML page served at /_devtools.
|
|
6
|
+
*
|
|
7
|
+
* Communication:
|
|
8
|
+
* - window.opener: direct DOM access (same-origin popup)
|
|
9
|
+
* - BroadcastChannel('__zq_devtools'): cross-tab fallback
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { readFileSync } = require('fs');
|
|
15
|
+
const { join } = require('path');
|
|
16
|
+
|
|
17
|
+
const dir = __dirname;
|
|
18
|
+
const read = (f) => readFileSync(join(dir, f), 'utf8');
|
|
19
|
+
|
|
20
|
+
const css = read('styles.css');
|
|
21
|
+
const html = read('panel.html');
|
|
22
|
+
|
|
23
|
+
const jsFiles = [
|
|
24
|
+
'js/core.js',
|
|
25
|
+
'js/tabs.js',
|
|
26
|
+
'js/source.js',
|
|
27
|
+
'js/elements.js',
|
|
28
|
+
'js/network.js',
|
|
29
|
+
'js/components.js',
|
|
30
|
+
'js/performance.js',
|
|
31
|
+
'js/router.js',
|
|
32
|
+
'js/stats.js'
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const js = jsFiles.map(read).join('\n\n');
|
|
36
|
+
|
|
37
|
+
module.exports = `<!DOCTYPE html>
|
|
38
|
+
<html lang="en">
|
|
39
|
+
<head>
|
|
40
|
+
<meta charset="UTF-8">
|
|
41
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
42
|
+
<title>zQuery DevTools</title>
|
|
43
|
+
<style>
|
|
44
|
+
${css}
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
${html}
|
|
49
|
+
<script>
|
|
50
|
+
(function() {
|
|
51
|
+
'use strict';
|
|
52
|
+
${js}
|
|
53
|
+
})();
|
|
54
|
+
</script>
|
|
55
|
+
</body>
|
|
56
|
+
</html>`;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// ===================================================================
|
|
2
|
+
// Components
|
|
3
|
+
// ===================================================================
|
|
4
|
+
function renderComponents() {
|
|
5
|
+
var list = document.getElementById('comp-list');
|
|
6
|
+
if (!targetWin || !targetWin.$) {
|
|
7
|
+
list.innerHTML = '<div class="empty-state">Waiting for zQuery to load...</div>';
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
var $ = targetWin.$;
|
|
11
|
+
var html = '';
|
|
12
|
+
|
|
13
|
+
// Iterate mounted components if components registry exists
|
|
14
|
+
if ($ && typeof $.components === 'function') {
|
|
15
|
+
var names = Object.keys($.components());
|
|
16
|
+
if (!names.length) {
|
|
17
|
+
list.innerHTML = '<div class="empty-state">No components registered</div>';
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (var n = 0; n < names.length; n++) {
|
|
21
|
+
var name = names[n];
|
|
22
|
+
// Find mounted instances (exclude scoped <style> tags that carry data-zq-component)
|
|
23
|
+
var hosts = targetDoc.querySelectorAll(name + ':not(style)');
|
|
24
|
+
html += '<div class="comp-card">';
|
|
25
|
+
html += '<div class="comp-name"><' + name + '></div>';
|
|
26
|
+
html += '<div class="comp-host">' + hosts.length + ' instance' + (hosts.length !== 1 ? 's' : '') + ' mounted</div>';
|
|
27
|
+
hosts.forEach(function(host) {
|
|
28
|
+
try {
|
|
29
|
+
var inst = $.getInstance(host);
|
|
30
|
+
if (inst && inst.state) {
|
|
31
|
+
html += '<div class="comp-state">';
|
|
32
|
+
var keys = Object.keys(inst.state);
|
|
33
|
+
for (var k = 0; k < keys.length; k++) {
|
|
34
|
+
var key = keys[k];
|
|
35
|
+
var val = inst.state[key];
|
|
36
|
+
var display = typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
37
|
+
if (display.length > 80) display = display.slice(0, 80) + '...';
|
|
38
|
+
html += '<div class="detail-row"><span class="comp-state-key">' + esc(key) + '</span><span class="comp-state-val">' + esc(display) + '</span></div>';
|
|
39
|
+
}
|
|
40
|
+
html += '</div>';
|
|
41
|
+
}
|
|
42
|
+
} catch(e) {}
|
|
43
|
+
});
|
|
44
|
+
html += '</div>';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!html) html = '<div class="empty-state">No components found</div>';
|
|
48
|
+
list.innerHTML = html;
|
|
49
|
+
}
|