zero-query 0.6.3 → 0.7.5
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 +6 -6
- package/cli/commands/build.js +3 -3
- package/cli/commands/bundle.js +286 -8
- package/cli/commands/dev/index.js +2 -2
- package/cli/commands/dev/overlay.js +51 -2
- package/cli/commands/dev/server.js +34 -5
- package/cli/commands/dev/watcher.js +33 -0
- package/cli/scaffold/index.html +1 -0
- package/cli/scaffold/scripts/app.js +15 -22
- package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
- package/cli/scaffold/scripts/components/contacts/contacts.html +3 -3
- package/cli/scaffold/styles/styles.css +1 -0
- package/cli/utils.js +111 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +379 -27
- package/dist/zquery.min.js +3 -16
- package/index.d.ts +127 -1290
- package/package.json +5 -5
- package/src/component.js +11 -1
- package/src/core.js +305 -10
- package/src/router.js +49 -2
- package/tests/component.test.js +304 -0
- package/tests/core.test.js +726 -0
- package/tests/diff.test.js +194 -0
- package/tests/errors.test.js +162 -0
- package/tests/expression.test.js +334 -0
- package/tests/http.test.js +181 -0
- package/tests/reactive.test.js +191 -0
- package/tests/router.test.js +332 -0
- package/tests/store.test.js +253 -0
- package/tests/utils.test.js +353 -0
- package/types/collection.d.ts +368 -0
- package/types/component.d.ts +210 -0
- package/types/errors.d.ts +103 -0
- package/types/http.d.ts +81 -0
- package/types/misc.d.ts +166 -0
- package/types/reactive.d.ts +76 -0
- package/types/router.d.ts +132 -0
- package/types/ssr.d.ts +49 -0
- package/types/store.d.ts +107 -0
- package/types/utils.d.ts +142 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src=".github/images/logo.svg" alt="zQuery logo" width="300" height="300">
|
|
2
|
+
<img src=".github/images/logo-animated.svg" alt="zQuery logo" width="300" height="300">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">zQuery</h1>
|
|
@@ -15,18 +15,18 @@
|
|
|
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 ~80 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
|
-
| **
|
|
24
|
+
| **Router** | History & hash mode, route params (`:id`), guards, lazy loading, `z-link` navigation, `z-to-top` scroll modifier (`instant`/`smooth`) |
|
|
25
25
|
| **Components** | Reactive state, template literals, `@event` delegation (8 modifiers), `z-model` two-way binding, computed properties, watch callbacks, slot-based content projection, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`), DOM morphing engine (no innerHTML rebuild), CSP-safe expression evaluation, scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles |
|
|
26
|
-
| **Router** | History & hash mode, route params (`:id`), guards, lazy loading, `z-link` navigation |
|
|
27
26
|
| **Store** | Reactive global state, named actions, computed getters, middleware, subscriptions |
|
|
28
27
|
| **HTTP** | Fetch wrapper with auto-JSON, interceptors, timeout/abort, base URL |
|
|
29
28
|
| **Reactive** | Deep proxy reactivity, Signals, computed values, effects |
|
|
29
|
+
| **Selectors & DOM** | jQuery-like chainable selectors, traversal, DOM manipulation, events, animation |
|
|
30
30
|
| **Utils** | debounce, throttle, pipe, once, sleep, escapeHtml, uuid, deepClone, deepMerge, storage/session wrappers, event bus |
|
|
31
31
|
|
|
32
32
|
---
|
|
@@ -69,7 +69,7 @@ If you prefer **zero tooling**, download `dist/zQuery.min.js` from the [GitHub r
|
|
|
69
69
|
git clone https://github.com/tonywied17/zero-query.git
|
|
70
70
|
cd zero-query
|
|
71
71
|
npx zquery build
|
|
72
|
-
# → dist/zQuery.min.js (~
|
|
72
|
+
# → dist/zQuery.min.js (~80 KB)
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
### Include in HTML
|
|
@@ -278,7 +278,7 @@ For full method signatures, options, and examples, see **[API.md](API.md)**.
|
|
|
278
278
|
|
|
279
279
|
## Editor Support
|
|
280
280
|
|
|
281
|
-
The official **[zQuery for VS Code](https://marketplace.visualstudio.com/items?itemName=zQuery.zquery-vs-code)** extension provides autocomplete, hover docs, HTML directive support, and
|
|
281
|
+
The official **[zQuery for VS Code](https://marketplace.visualstudio.com/items?itemName=zQuery.zquery-vs-code)** extension provides autocomplete, hover docs, HTML directive support, and 185+ code snippets for every API method and directive. Install it from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=zQuery.zquery-vs-code) or search **"zQuery for VS Code"** in Extensions.
|
|
282
282
|
|
|
283
283
|
---
|
|
284
284
|
|
package/cli/commands/build.js
CHANGED
|
@@ -34,7 +34,7 @@ function buildLibrary() {
|
|
|
34
34
|
code = code.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
35
35
|
code = code.replace(/^export\s+(default\s+)?/gm, '');
|
|
36
36
|
code = code.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
|
|
37
|
-
return `// --- ${file} ${'
|
|
37
|
+
return `// --- ${file} ${'-'.repeat(60 - file.length)}\n${code.trim()}`;
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
let indexCode = fs.readFileSync(path.join(process.cwd(), 'index.js'), 'utf-8');
|
|
@@ -42,9 +42,9 @@ function buildLibrary() {
|
|
|
42
42
|
indexCode = indexCode.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
|
|
43
43
|
indexCode = indexCode.replace(/^export\s+(default\s+)?/gm, '');
|
|
44
44
|
|
|
45
|
-
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
|
|
45
|
+
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 */`;
|
|
46
46
|
|
|
47
|
-
const bundle = `${banner}\n(function(global) {\n 'use strict';\n\n${parts.join('\n\n')}\n\n// --- index.js (assembly) ${'
|
|
47
|
+
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
48
|
|
|
49
49
|
fs.writeFileSync(OUT_FILE, bundle, 'utf-8');
|
|
50
50
|
fs.writeFileSync(MIN_FILE, minify(bundle, banner), 'utf-8');
|
package/cli/commands/bundle.js
CHANGED
|
@@ -24,6 +24,8 @@ const buildLibrary = require('./build');
|
|
|
24
24
|
/** Resolve an import specifier relative to the importing file. */
|
|
25
25
|
function resolveImport(specifier, fromFile) {
|
|
26
26
|
if (!specifier.startsWith('.') && !specifier.startsWith('/')) return null;
|
|
27
|
+
// Skip non-JS assets (CSS, images, etc.) referenced in code examples
|
|
28
|
+
if (/\.(?:css|json|svg|png|jpg|gif|woff2?|ttf|eot)$/i.test(specifier)) return null;
|
|
27
29
|
let resolved = path.resolve(path.dirname(fromFile), specifier);
|
|
28
30
|
if (!path.extname(resolved) && fs.existsSync(resolved + '.js')) {
|
|
29
31
|
resolved += '.js';
|
|
@@ -112,6 +114,243 @@ function rewriteResourceUrls(code, filePath, projectRoot) {
|
|
|
112
114
|
);
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Minify HTML for inlining — strips indentation and collapses whitespace
|
|
119
|
+
* between tags. Preserves content inside <pre>, <code>, and <textarea>
|
|
120
|
+
* blocks verbatim so syntax-highlighted code samples survive.
|
|
121
|
+
*/
|
|
122
|
+
function minifyHTML(html) {
|
|
123
|
+
const preserved = [];
|
|
124
|
+
// Protect <pre>…</pre> and <textarea>…</textarea> (multiline code blocks)
|
|
125
|
+
html = html.replace(/<(pre|textarea)(\b[^>]*)>[\s\S]*?<\/\1>/gi, m => {
|
|
126
|
+
preserved.push(m);
|
|
127
|
+
return `\x00P${preserved.length - 1}\x00`;
|
|
128
|
+
});
|
|
129
|
+
// Strip HTML comments
|
|
130
|
+
html = html.replace(/<!--[\s\S]*?-->/g, '');
|
|
131
|
+
// Collapse runs of whitespace (newlines + indentation) to a single space
|
|
132
|
+
html = html.replace(/\s{2,}/g, ' ');
|
|
133
|
+
// Remove space between tags: "> <" → "><"
|
|
134
|
+
// but preserve a space when an inline element is involved (a, span, strong, em, b, i, code, small, sub, sup, abbr, label)
|
|
135
|
+
html = html.replace(/>\s+</g, (m, offset) => {
|
|
136
|
+
const before = html.slice(Math.max(0, offset - 80), offset + 1);
|
|
137
|
+
const after = html.slice(offset + m.length - 1, offset + m.length + 40);
|
|
138
|
+
const inlineTags = /\b(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\b/i;
|
|
139
|
+
const closingInline = /<\/\s*(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\s*>$/i.test(before);
|
|
140
|
+
const openingInline = /^<(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)[\s>]/i.test(after);
|
|
141
|
+
return (closingInline || openingInline) ? '> <' : '><';
|
|
142
|
+
});
|
|
143
|
+
// Remove spaces inside opening tags: <tag attr = "val" > → <tag attr="val">
|
|
144
|
+
html = html.replace(/ *\/ *>/g, '/>');
|
|
145
|
+
html = html.replace(/ *= */g, '=');
|
|
146
|
+
// Trim
|
|
147
|
+
html = html.trim();
|
|
148
|
+
// Restore preserved blocks
|
|
149
|
+
html = html.replace(/\x00P(\d+)\x00/g, (_, i) => preserved[+i]);
|
|
150
|
+
return html;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Minify CSS for inlining — strips comments, collapses whitespace,
|
|
155
|
+
* removes unnecessary spaces around punctuation.
|
|
156
|
+
*/
|
|
157
|
+
function minifyCSS(css) {
|
|
158
|
+
// Strip block comments
|
|
159
|
+
css = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
160
|
+
// Collapse whitespace
|
|
161
|
+
css = css.replace(/\s{2,}/g, ' ');
|
|
162
|
+
// Remove spaces around { } : ; ,
|
|
163
|
+
css = css.replace(/\s*([{};:,])\s*/g, '$1');
|
|
164
|
+
// Remove trailing semicolons before }
|
|
165
|
+
css = css.replace(/;}/g, '}');
|
|
166
|
+
return css.trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Walk JS source and minify the HTML/CSS inside template literals.
|
|
171
|
+
* Handles ${…} interpolations (with nesting) and preserves <pre> blocks.
|
|
172
|
+
* Only trims whitespace sequences that appear between/around HTML tags.
|
|
173
|
+
*/
|
|
174
|
+
function minifyTemplateLiterals(code) {
|
|
175
|
+
let out = '';
|
|
176
|
+
let i = 0;
|
|
177
|
+
while (i < code.length) {
|
|
178
|
+
const ch = code[i];
|
|
179
|
+
|
|
180
|
+
// Regular string: copy verbatim
|
|
181
|
+
if (ch === '"' || ch === "'") {
|
|
182
|
+
const q = ch; out += ch; i++;
|
|
183
|
+
while (i < code.length) {
|
|
184
|
+
if (code[i] === '\\') { out += code[i] + (code[i + 1] || ''); i += 2; continue; }
|
|
185
|
+
out += code[i];
|
|
186
|
+
if (code[i] === q) { i++; break; }
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Line comment: copy verbatim
|
|
193
|
+
if (ch === '/' && code[i + 1] === '/') {
|
|
194
|
+
while (i < code.length && code[i] !== '\n') out += code[i++];
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// Block comment: copy verbatim
|
|
198
|
+
if (ch === '/' && code[i + 1] === '*') {
|
|
199
|
+
out += '/*'; i += 2;
|
|
200
|
+
while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) out += code[i++];
|
|
201
|
+
out += '*/'; i += 2;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Template literal: extract, minify HTML, and emit
|
|
206
|
+
if (ch === '`') {
|
|
207
|
+
out += _minifyTemplate(code, i);
|
|
208
|
+
// Advance past the template
|
|
209
|
+
i = _skipTemplateLiteral(code, i);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
out += ch; i++;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Extract a full template literal (handling nested ${…}) and return it minified. */
|
|
219
|
+
function _minifyTemplate(code, start) {
|
|
220
|
+
const end = _skipTemplateLiteral(code, start);
|
|
221
|
+
const raw = code.substring(start, end);
|
|
222
|
+
// Only minify templates that contain HTML tags or CSS rules
|
|
223
|
+
if (/<\w/.test(raw)) return _collapseTemplateWS(raw);
|
|
224
|
+
if (/[{};]\s/.test(raw) && /[.#\w-]+\s*\{/.test(raw)) return _collapseTemplateCSS(raw);
|
|
225
|
+
return raw;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Return index past the closing backtick of a template starting at `start`. */
|
|
229
|
+
function _skipTemplateLiteral(code, start) {
|
|
230
|
+
let i = start + 1; // skip opening backtick
|
|
231
|
+
let depth = 0;
|
|
232
|
+
while (i < code.length) {
|
|
233
|
+
if (code[i] === '\\') { i += 2; continue; }
|
|
234
|
+
if (code[i] === '$' && code[i + 1] === '{') { depth++; i += 2; continue; }
|
|
235
|
+
if (depth > 0) {
|
|
236
|
+
if (code[i] === '{') { depth++; i++; continue; }
|
|
237
|
+
if (code[i] === '}') { depth--; i++; continue; }
|
|
238
|
+
if (code[i] === '`') { i = _skipTemplateLiteral(code, i); continue; }
|
|
239
|
+
if (code[i] === '"' || code[i] === "'") {
|
|
240
|
+
const q = code[i]; i++;
|
|
241
|
+
while (i < code.length) {
|
|
242
|
+
if (code[i] === '\\') { i += 2; continue; }
|
|
243
|
+
if (code[i] === q) { i++; break; }
|
|
244
|
+
i++;
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
i++; continue;
|
|
249
|
+
}
|
|
250
|
+
if (code[i] === '`') { i++; return i; } // closing backtick
|
|
251
|
+
i++;
|
|
252
|
+
}
|
|
253
|
+
return i;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Collapse whitespace in the text portions of an HTML template literal,
|
|
258
|
+
* preserving ${…} expressions, <pre> blocks, and inline text spacing.
|
|
259
|
+
*/
|
|
260
|
+
function _collapseTemplateWS(tpl) {
|
|
261
|
+
// Build array of segments: text portions vs ${…} expressions
|
|
262
|
+
const segments = [];
|
|
263
|
+
let i = 1; // skip opening backtick
|
|
264
|
+
let text = '';
|
|
265
|
+
while (i < tpl.length - 1) { // stop before closing backtick
|
|
266
|
+
if (tpl[i] === '\\') { text += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
267
|
+
if (tpl[i] === '$' && tpl[i + 1] === '{') {
|
|
268
|
+
if (text) { segments.push({ type: 'text', val: text }); text = ''; }
|
|
269
|
+
// Collect the full expression
|
|
270
|
+
let depth = 1; let expr = '${'; i += 2;
|
|
271
|
+
while (i < tpl.length - 1 && depth > 0) {
|
|
272
|
+
if (tpl[i] === '\\') { expr += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
273
|
+
if (tpl[i] === '{') depth++;
|
|
274
|
+
if (tpl[i] === '}') depth--;
|
|
275
|
+
if (depth > 0) { expr += tpl[i]; i++; } else { expr += '}'; i++; }
|
|
276
|
+
}
|
|
277
|
+
segments.push({ type: 'expr', val: expr });
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
text += tpl[i]; i++;
|
|
281
|
+
}
|
|
282
|
+
if (text) segments.push({ type: 'text', val: text });
|
|
283
|
+
|
|
284
|
+
// Minify text segments (collapse whitespace between/around tags)
|
|
285
|
+
for (let s = 0; s < segments.length; s++) {
|
|
286
|
+
if (segments[s].type !== 'text') continue;
|
|
287
|
+
let t = segments[s].val;
|
|
288
|
+
// Protect <pre>…</pre> regions
|
|
289
|
+
const preserved = [];
|
|
290
|
+
t = t.replace(/<pre(\b[^>]*)>[\s\S]*?<\/pre>/gi, m => {
|
|
291
|
+
preserved.push(m);
|
|
292
|
+
return `\x00P${preserved.length - 1}\x00`;
|
|
293
|
+
});
|
|
294
|
+
// Collapse whitespace runs that touch a < or > but preserve a space
|
|
295
|
+
// when an inline element boundary is involved
|
|
296
|
+
t = t.replace(/>\s{2,}/g, (m, offset) => {
|
|
297
|
+
const before = t.slice(Math.max(0, offset - 80), offset + 1);
|
|
298
|
+
if (/<\/\s*(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\s*>$/i.test(before)) return '> ';
|
|
299
|
+
return '>';
|
|
300
|
+
});
|
|
301
|
+
t = t.replace(/\s{2,}</g, (m, offset) => {
|
|
302
|
+
const after = t.slice(offset + m.length - 1, offset + m.length + 40);
|
|
303
|
+
if (/^<(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)[\s>]/i.test(after)) return ' <';
|
|
304
|
+
return '<';
|
|
305
|
+
});
|
|
306
|
+
// Collapse other multi-whitespace runs to a single space
|
|
307
|
+
t = t.replace(/\s{2,}/g, ' ');
|
|
308
|
+
// Restore <pre> blocks
|
|
309
|
+
t = t.replace(/\x00P(\d+)\x00/g, (_, idx) => preserved[+idx]);
|
|
310
|
+
segments[s].val = t;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return '`' + segments.map(s => s.val).join('') + '`';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Collapse CSS whitespace inside a template literal (styles: `…`).
|
|
318
|
+
* Uses the same segment-splitting approach as _collapseTemplateWS so
|
|
319
|
+
* ${…} expressions are preserved untouched.
|
|
320
|
+
*/
|
|
321
|
+
function _collapseTemplateCSS(tpl) {
|
|
322
|
+
const segments = [];
|
|
323
|
+
let i = 1; let text = '';
|
|
324
|
+
while (i < tpl.length - 1) {
|
|
325
|
+
if (tpl[i] === '\\') { text += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
326
|
+
if (tpl[i] === '$' && tpl[i + 1] === '{') {
|
|
327
|
+
if (text) { segments.push({ type: 'text', val: text }); text = ''; }
|
|
328
|
+
let depth = 1; let expr = '${'; i += 2;
|
|
329
|
+
while (i < tpl.length - 1 && depth > 0) {
|
|
330
|
+
if (tpl[i] === '\\') { expr += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
|
|
331
|
+
if (tpl[i] === '{') depth++;
|
|
332
|
+
if (tpl[i] === '}') depth--;
|
|
333
|
+
if (depth > 0) { expr += tpl[i]; i++; } else { expr += '}'; i++; }
|
|
334
|
+
}
|
|
335
|
+
segments.push({ type: 'expr', val: expr });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
text += tpl[i]; i++;
|
|
339
|
+
}
|
|
340
|
+
if (text) segments.push({ type: 'text', val: text });
|
|
341
|
+
|
|
342
|
+
for (let s = 0; s < segments.length; s++) {
|
|
343
|
+
if (segments[s].type !== 'text') continue;
|
|
344
|
+
let t = segments[s].val;
|
|
345
|
+
t = t.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
346
|
+
t = t.replace(/\s{2,}/g, ' ');
|
|
347
|
+
t = t.replace(/\s*([{};:,])\s*/g, '$1');
|
|
348
|
+
t = t.replace(/;}/g, '}');
|
|
349
|
+
segments[s].val = t;
|
|
350
|
+
}
|
|
351
|
+
return '`' + segments.map(s => s.val).join('') + '`';
|
|
352
|
+
}
|
|
353
|
+
|
|
115
354
|
/**
|
|
116
355
|
* Scan bundled source files for external resource references
|
|
117
356
|
* (pages config, templateUrl, styleUrl) and return a map of
|
|
@@ -152,7 +391,8 @@ function collectInlineResources(files, projectRoot) {
|
|
|
152
391
|
}
|
|
153
392
|
|
|
154
393
|
// styleUrl:
|
|
155
|
-
const
|
|
394
|
+
const styleUrlRe = /styleUrl\s*:\s*['"]([^'"]+)['"]/g;
|
|
395
|
+
const styleMatch = styleUrlRe.exec(code);
|
|
156
396
|
if (styleMatch) {
|
|
157
397
|
const stylePath = path.join(fileDir, styleMatch[1]);
|
|
158
398
|
if (fs.existsSync(stylePath)) {
|
|
@@ -172,6 +412,12 @@ function collectInlineResources(files, projectRoot) {
|
|
|
172
412
|
}
|
|
173
413
|
}
|
|
174
414
|
|
|
415
|
+
// Minify inlined resources by type
|
|
416
|
+
for (const key of Object.keys(inlineMap)) {
|
|
417
|
+
if (key.endsWith('.html')) inlineMap[key] = minifyHTML(inlineMap[key]);
|
|
418
|
+
else if (key.endsWith('.css')) inlineMap[key] = minifyCSS(inlineMap[key]);
|
|
419
|
+
}
|
|
420
|
+
|
|
175
421
|
return inlineMap;
|
|
176
422
|
}
|
|
177
423
|
|
|
@@ -316,6 +562,37 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
|
|
|
316
562
|
assets.add(ref);
|
|
317
563
|
}
|
|
318
564
|
|
|
565
|
+
// Also scan the bundled JS for src/href references to local assets
|
|
566
|
+
const bundleContent = fs.existsSync(bundleFile) ? fs.readFileSync(bundleFile, 'utf-8') : '';
|
|
567
|
+
const jsAssetRe = /\b(?:src|href)\s*=\s*["\\]*["']([^"']+\.(?:svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|eot|mp4|webm))["']/gi;
|
|
568
|
+
while ((m = jsAssetRe.exec(bundleContent)) !== null) {
|
|
569
|
+
const ref = m[1];
|
|
570
|
+
if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:')) continue;
|
|
571
|
+
if (!assets.has(ref)) {
|
|
572
|
+
const refAbs = path.resolve(htmlDir, ref);
|
|
573
|
+
if (fs.existsSync(refAbs)) assets.add(ref);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// For any referenced asset directories, copy all sibling files too
|
|
578
|
+
const assetDirs = new Set();
|
|
579
|
+
for (const asset of assets) {
|
|
580
|
+
const dir = path.dirname(asset);
|
|
581
|
+
if (dir && dir !== '.') assetDirs.add(dir);
|
|
582
|
+
}
|
|
583
|
+
for (const dir of assetDirs) {
|
|
584
|
+
const absDirPath = path.resolve(htmlDir, dir);
|
|
585
|
+
if (fs.existsSync(absDirPath) && fs.statSync(absDirPath).isDirectory()) {
|
|
586
|
+
for (const child of fs.readdirSync(absDirPath)) {
|
|
587
|
+
const childRel = path.join(dir, child).replace(/\\/g, '/');
|
|
588
|
+
const childAbs = path.join(absDirPath, child);
|
|
589
|
+
if (fs.statSync(childAbs).isFile() && !assets.has(childRel)) {
|
|
590
|
+
assets.add(childRel);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
319
596
|
// Copy assets into both dist dirs
|
|
320
597
|
let copiedCount = 0;
|
|
321
598
|
for (const asset of assets) {
|
|
@@ -574,8 +851,9 @@ function bundleApp() {
|
|
|
574
851
|
code = stripModuleSyntax(code);
|
|
575
852
|
code = replaceImportMeta(code, file, projectRoot);
|
|
576
853
|
code = rewriteResourceUrls(code, file, projectRoot);
|
|
854
|
+
code = minifyTemplateLiterals(code);
|
|
577
855
|
const rel = path.relative(projectRoot, file);
|
|
578
|
-
return `// --- ${rel} ${'
|
|
856
|
+
return `// --- ${rel} ${'-'.repeat(Math.max(1, 60 - rel.length))}\n${code.trim()}`;
|
|
579
857
|
});
|
|
580
858
|
|
|
581
859
|
// Embed zquery.min.js
|
|
@@ -612,8 +890,8 @@ function bundleApp() {
|
|
|
612
890
|
|
|
613
891
|
if (libPath) {
|
|
614
892
|
const libBytes = fs.statSync(libPath).size;
|
|
615
|
-
libSection = `// --- zquery.min.js (library) ${'
|
|
616
|
-
+ `// --- Build-time metadata
|
|
893
|
+
libSection = `// --- zquery.min.js (library) ${'-'.repeat(34)}\n${fs.readFileSync(libPath, 'utf-8').trim()}\n\n`
|
|
894
|
+
+ `// --- Build-time metadata ${'-'.repeat(50)}\nif(typeof $!=="undefined"){$.meta=Object.assign($.meta||{},{libSize:${libBytes}});}\n\n`;
|
|
617
895
|
console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
|
|
618
896
|
} else {
|
|
619
897
|
console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
|
|
@@ -621,7 +899,7 @@ function bundleApp() {
|
|
|
621
899
|
}
|
|
622
900
|
}
|
|
623
901
|
|
|
624
|
-
const banner = `/**\n * App bundle
|
|
902
|
+
const banner = `/**\n * App bundle - built by zQuery CLI\n * Entry: ${entryRel}\n * ${new Date().toISOString()}\n */`;
|
|
625
903
|
|
|
626
904
|
// Inline resources
|
|
627
905
|
const inlineMap = collectInlineResources(files, projectRoot);
|
|
@@ -635,7 +913,7 @@ function bundleApp() {
|
|
|
635
913
|
.replace(/\r/g, '');
|
|
636
914
|
return ` '${key}': '${escaped}'`;
|
|
637
915
|
});
|
|
638
|
-
inlineSection = `// --- Inlined resources (file:// support) ${'
|
|
916
|
+
inlineSection = `// --- Inlined resources (file:// support) ${'-'.repeat(20)}\nwindow.__zqInline = {\n${entries.join(',\n')}\n};\n\n`;
|
|
639
917
|
console.log(`\n Inlined ${Object.keys(inlineMap).length} external resource(s)`);
|
|
640
918
|
}
|
|
641
919
|
|
|
@@ -668,10 +946,10 @@ function bundleApp() {
|
|
|
668
946
|
console.log(`\n ✓ ${bundleBase} (${sizeKB(fs.readFileSync(bundleFile))} KB)`);
|
|
669
947
|
console.log(` ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
|
|
670
948
|
|
|
671
|
-
// Rewrite HTML
|
|
949
|
+
// Rewrite HTML to reference the minified bundle
|
|
672
950
|
const bundledFileSet = new Set(files);
|
|
673
951
|
if (htmlFile) {
|
|
674
|
-
rewriteHtml(projectRoot, htmlFile,
|
|
952
|
+
rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir);
|
|
675
953
|
}
|
|
676
954
|
|
|
677
955
|
// Copy static asset directories (icons/, images/, fonts/, etc.)
|
|
@@ -46,14 +46,14 @@ function resolveRoot(htmlEntry) {
|
|
|
46
46
|
// devServer — main entry point (called from cli/index.js)
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
48
|
|
|
49
|
-
function devServer() {
|
|
49
|
+
async function devServer() {
|
|
50
50
|
const htmlEntry = option('index', 'i', 'index.html');
|
|
51
51
|
const port = parseInt(option('port', 'p', '3100'), 10);
|
|
52
52
|
const noIntercept = flag('no-intercept');
|
|
53
53
|
const root = resolveRoot(htmlEntry);
|
|
54
54
|
|
|
55
55
|
// Start HTTP server + SSE pool
|
|
56
|
-
const { app, pool, listen } = createServer({ root, htmlEntry, port, noIntercept });
|
|
56
|
+
const { app, pool, listen } = await createServer({ root, htmlEntry, port, noIntercept });
|
|
57
57
|
|
|
58
58
|
// Start file watcher
|
|
59
59
|
const watcher = startWatcher({ root, pool });
|
|
@@ -281,14 +281,63 @@ const OVERLAY_SCRIPT = `<script>
|
|
|
281
281
|
location.reload();
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
-
es.addEventListener('css', function() {
|
|
284
|
+
es.addEventListener('css', function(e) {
|
|
285
|
+
var changedPath = (e.data || '').replace(/^\\/+/, '');
|
|
286
|
+
var matched = false;
|
|
287
|
+
|
|
288
|
+
// 1) Try cache-busting matching <link rel="stylesheet"> tags
|
|
285
289
|
var sheets = document.querySelectorAll('link[rel="stylesheet"]');
|
|
286
290
|
sheets.forEach(function(l) {
|
|
287
291
|
var href = l.getAttribute('href');
|
|
288
292
|
if (!href) return;
|
|
293
|
+
var clean = href.replace(/[?&]_zqr=\\d+/, '').replace(/^\\/+/, '');
|
|
294
|
+
if (changedPath && clean.indexOf(changedPath) === -1) return;
|
|
295
|
+
matched = true;
|
|
289
296
|
var sep = href.indexOf('?') >= 0 ? '&' : '?';
|
|
290
|
-
l.setAttribute('href', href.replace(/[?&]_zqr
|
|
297
|
+
l.setAttribute('href', href.replace(/[?&]_zqr=\\d+/, '') + sep + '_zqr=' + Date.now());
|
|
291
298
|
});
|
|
299
|
+
|
|
300
|
+
// 2) Try hot-swapping scoped <style data-zq-style-urls> elements
|
|
301
|
+
// These come from component styleUrl — the CSS was fetched, scoped,
|
|
302
|
+
// and injected as an inline <style>. We re-fetch and re-scope it.
|
|
303
|
+
if (!matched) {
|
|
304
|
+
var scopedEls = document.querySelectorAll('style[data-zq-style-urls]');
|
|
305
|
+
scopedEls.forEach(function(el) {
|
|
306
|
+
var urls = el.getAttribute('data-zq-style-urls') || '';
|
|
307
|
+
var hit = urls.split(' ').some(function(u) {
|
|
308
|
+
return u && u.replace(/^\\/+/, '').indexOf(changedPath) !== -1;
|
|
309
|
+
});
|
|
310
|
+
if (!hit) return;
|
|
311
|
+
matched = true;
|
|
312
|
+
|
|
313
|
+
var scopeAttr = el.getAttribute('data-zq-scope') || '';
|
|
314
|
+
var inlineStyles = el.getAttribute('data-zq-inline') || '';
|
|
315
|
+
|
|
316
|
+
// Re-fetch all style URLs (cache-busted)
|
|
317
|
+
var urlList = urls.split(' ').filter(Boolean);
|
|
318
|
+
Promise.all(urlList.map(function(u) {
|
|
319
|
+
return fetch(u + (u.indexOf('?') >= 0 ? '&' : '?') + '_zqr=' + Date.now())
|
|
320
|
+
.then(function(r) { return r.text(); });
|
|
321
|
+
})).then(function(results) {
|
|
322
|
+
var raw = (inlineStyles ? inlineStyles + '\\n' : '') + results.join('\\n');
|
|
323
|
+
// Re-scope CSS with the same scope attribute
|
|
324
|
+
if (scopeAttr) {
|
|
325
|
+
var inAt = 0;
|
|
326
|
+
raw = raw.replace(/([^{}]+)\\{|\\}/g, function(m, sel) {
|
|
327
|
+
if (m === '}') { if (inAt > 0) inAt--; return m; }
|
|
328
|
+
var t = sel.trim();
|
|
329
|
+
if (t.charAt(0) === '@') { inAt++; return m; }
|
|
330
|
+
if (inAt > 0 && /^[\\d%\\s,fromto]+$/.test(t.replace(/\\s/g, ''))) return m;
|
|
331
|
+
return sel.split(',').map(function(s) { return '[' + scopeAttr + '] ' + s.trim(); }).join(', ') + ' {';
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
el.textContent = raw;
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 3) Nothing matched — fall back to full reload
|
|
340
|
+
if (!matched) { location.reload(); }
|
|
292
341
|
});
|
|
293
342
|
|
|
294
343
|
es.addEventListener('error:syntax', function(e) {
|
|
@@ -44,22 +44,51 @@ class SSEPool {
|
|
|
44
44
|
// Server factory
|
|
45
45
|
// ---------------------------------------------------------------------------
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Prompt the user to auto-install zero-http when it isn't found.
|
|
49
|
+
* Resolves `true` if the user accepts, `false` otherwise.
|
|
50
|
+
*/
|
|
51
|
+
function promptInstall() {
|
|
52
|
+
const rl = require('readline').createInterface({
|
|
53
|
+
input: process.stdin,
|
|
54
|
+
output: process.stdout,
|
|
55
|
+
});
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
rl.question(
|
|
58
|
+
'\n The local dev server requires zero-http, which is not installed.\n' +
|
|
59
|
+
' This package is only used during development and is not needed\n' +
|
|
60
|
+
' for building, bundling, or production.\n' +
|
|
61
|
+
' Install it now? (y/n): ',
|
|
62
|
+
(answer) => {
|
|
63
|
+
rl.close();
|
|
64
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
/**
|
|
48
71
|
* @param {object} opts
|
|
49
72
|
* @param {string} opts.root — absolute path to project root
|
|
50
73
|
* @param {string} opts.htmlEntry — e.g. 'index.html'
|
|
51
74
|
* @param {number} opts.port
|
|
52
75
|
* @param {boolean} opts.noIntercept — skip zquery.min.js auto-resolve
|
|
53
|
-
* @returns {{ app, pool: SSEPool, listen: Function }}
|
|
76
|
+
* @returns {Promise<{ app, pool: SSEPool, listen: Function }>}
|
|
54
77
|
*/
|
|
55
|
-
function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
78
|
+
async function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
56
79
|
let zeroHttp;
|
|
57
80
|
try {
|
|
58
81
|
zeroHttp = require('zero-http');
|
|
59
82
|
} catch {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
const ok = await promptInstall();
|
|
84
|
+
if (!ok) {
|
|
85
|
+
console.error('\n ✖ Cannot start dev server without zero-http.\n');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const { execSync } = require('child_process');
|
|
89
|
+
console.log('\n Installing zero-http...\n');
|
|
90
|
+
execSync('npm install zero-http --save-dev', { stdio: 'inherit' });
|
|
91
|
+
zeroHttp = require('zero-http');
|
|
63
92
|
}
|
|
64
93
|
|
|
65
94
|
const { createApp, static: serveStatic } = zeroHttp;
|
|
@@ -28,6 +28,14 @@ function isIgnored(filepath) {
|
|
|
28
28
|
return filepath.split(path.sep).some(p => IGNORE_DIRS.has(p));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Return the file's mtime as a millisecond timestamp, or 0 if unreadable.
|
|
33
|
+
* Used to ignore spurious fs.watch events (Windows fires on reads too).
|
|
34
|
+
*/
|
|
35
|
+
function mtime(filepath) {
|
|
36
|
+
try { return fs.statSync(filepath).mtimeMs; } catch { return 0; }
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
/** Recursively collect every directory under `dir` (excluding ignored). */
|
|
32
40
|
function collectWatchDirs(dir) {
|
|
33
41
|
const dirs = [dir];
|
|
@@ -59,6 +67,24 @@ function startWatcher({ root, pool }) {
|
|
|
59
67
|
let debounceTimer;
|
|
60
68
|
let currentError = null; // track which file has an active error
|
|
61
69
|
|
|
70
|
+
// Track file mtimes so we only react to genuine writes.
|
|
71
|
+
// On Windows, fs.watch fires on reads/access too, which causes
|
|
72
|
+
// spurious reloads the first time the server serves a file.
|
|
73
|
+
// We seed the cache with current mtimes so the first real save
|
|
74
|
+
// (which changes the mtime) is always detected.
|
|
75
|
+
const mtimeCache = new Map();
|
|
76
|
+
for (const d of watchDirs) {
|
|
77
|
+
try {
|
|
78
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
79
|
+
if (entry.isFile()) {
|
|
80
|
+
const fp = path.join(d, entry.name);
|
|
81
|
+
const mt = mtime(fp);
|
|
82
|
+
if (mt) mtimeCache.set(fp, mt);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch { /* skip */ }
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
for (const dir of watchDirs) {
|
|
63
89
|
try {
|
|
64
90
|
const watcher = fs.watch(dir, (_, filename) => {
|
|
@@ -68,6 +94,13 @@ function startWatcher({ root, pool }) {
|
|
|
68
94
|
|
|
69
95
|
clearTimeout(debounceTimer);
|
|
70
96
|
debounceTimer = setTimeout(() => {
|
|
97
|
+
// Skip if the file hasn't actually been modified
|
|
98
|
+
const mt = mtime(fullPath);
|
|
99
|
+
if (mt === 0) return; // deleted or unreadable
|
|
100
|
+
const prev = mtimeCache.get(fullPath);
|
|
101
|
+
mtimeCache.set(fullPath, mt);
|
|
102
|
+
if (prev !== undefined && mt === prev) return; // unchanged
|
|
103
|
+
|
|
71
104
|
const rel = path.relative(root, fullPath).replace(/\\/g, '/');
|
|
72
105
|
const ext = path.extname(filename).toLowerCase();
|
|
73
106
|
|
package/cli/scaffold/index.html
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
<title>{{NAME}}</title>
|
|
7
7
|
<base href="/">
|
|
8
8
|
<link rel="stylesheet" href="styles/styles.css">
|
|
9
|
+
<link rel="icon" type="image/png" href="favicon.ico">
|
|
9
10
|
<script src="scripts/vendor/zquery.min.js"></script>
|
|
10
11
|
<script type="module" src="scripts/app.js"></script>
|
|
11
12
|
</head>
|