zero-query 0.5.2 → 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 +12 -10
- package/cli/commands/build.js +7 -5
- package/cli/commands/bundle.js +286 -8
- package/cli/commands/dev/index.js +82 -0
- package/cli/commands/dev/logger.js +70 -0
- package/cli/commands/dev/overlay.js +366 -0
- package/cli/commands/dev/server.js +158 -0
- package/cli/commands/dev/validator.js +94 -0
- package/cli/commands/dev/watcher.js +147 -0
- package/cli/scaffold/favicon.ico +0 -0
- package/cli/scaffold/index.html +1 -0
- package/cli/scaffold/scripts/app.js +15 -22
- package/cli/scaffold/scripts/components/about.js +14 -2
- package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
- package/cli/scaffold/scripts/components/contacts/contacts.html +8 -7
- package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
- package/cli/scaffold/scripts/components/counter.js +30 -10
- package/cli/scaffold/scripts/components/home.js +3 -3
- package/cli/scaffold/scripts/components/todos.js +6 -5
- 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 +2005 -216
- package/dist/zquery.min.js +3 -13
- package/index.d.ts +149 -1080
- package/index.js +18 -7
- package/package.json +9 -3
- package/src/component.js +186 -45
- package/src/core.js +327 -35
- package/src/diff.js +280 -0
- package/src/errors.js +155 -0
- package/src/expression.js +806 -0
- package/src/http.js +18 -10
- package/src/reactive.js +29 -4
- package/src/router.js +59 -6
- package/src/ssr.js +224 -0
- package/src/store.js +24 -8
- 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/cli/commands/{dev.js → dev.old.js} +0 -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
|
-
| **
|
|
25
|
-
| **Components** | Reactive state, template literals, `@event` delegation (8 modifiers), `z-model` two-way binding, 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`), 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 |
|
|
24
|
+
| **Router** | History & hash mode, route params (`:id`), guards, lazy loading, `z-link` navigation, `z-to-top` scroll modifier (`instant`/`smooth`) |
|
|
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 |
|
|
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
|
|
@@ -240,13 +240,15 @@ location / {
|
|
|
240
240
|
|
|
241
241
|
| Namespace | Methods |
|
|
242
242
|
| --- | --- |
|
|
243
|
-
| `$()` |
|
|
244
|
-
| `$.all()` |
|
|
245
|
-
| `$.id` `$.class` `$.classes` `$.tag` `$.children` | Quick DOM refs |
|
|
243
|
+
| `$()` | Chainable selector → `ZQueryCollection` (CSS selectors, elements, NodeLists, HTML strings) |
|
|
244
|
+
| `$.all()` | Alias for `$()` — identical behavior |
|
|
245
|
+
| `$.id` `$.class` `$.classes` `$.tag` `$.name` `$.children` | Quick DOM refs |
|
|
246
246
|
| `$.create` | Element factory |
|
|
247
247
|
| `$.ready` `$.on` `$.off` | DOM ready, global event delegation & direct listeners |
|
|
248
248
|
| `$.fn` | Collection prototype (extend it) |
|
|
249
249
|
| `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` | Component system |
|
|
250
|
+
| `$.morph` | DOM morphing engine — patch existing DOM to match new HTML without destroying unchanged nodes |
|
|
251
|
+
| `$.safeEval` | CSP-safe expression evaluator (replaces `eval` / `new Function`) |
|
|
250
252
|
| `$.style` | Dynamically load global stylesheet file(s) at runtime |
|
|
251
253
|
| `$.router` `$.getRouter` | SPA router |
|
|
252
254
|
| `$.store` `$.getStore` | State management |
|
|
@@ -276,7 +278,7 @@ For full method signatures, options, and examples, see **[API.md](API.md)**.
|
|
|
276
278
|
|
|
277
279
|
## Editor Support
|
|
278
280
|
|
|
279
|
-
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.
|
|
280
282
|
|
|
281
283
|
---
|
|
282
284
|
|
package/cli/commands/build.js
CHANGED
|
@@ -16,8 +16,10 @@ function buildLibrary() {
|
|
|
16
16
|
const VERSION = pkg.version;
|
|
17
17
|
|
|
18
18
|
const modules = [
|
|
19
|
-
'src/
|
|
20
|
-
'src/
|
|
19
|
+
'src/errors.js',
|
|
20
|
+
'src/reactive.js', 'src/core.js', 'src/expression.js', 'src/diff.js',
|
|
21
|
+
'src/component.js', 'src/router.js', 'src/store.js', 'src/http.js',
|
|
22
|
+
'src/utils.js',
|
|
21
23
|
];
|
|
22
24
|
|
|
23
25
|
const DIST = path.join(process.cwd(), 'dist');
|
|
@@ -32,7 +34,7 @@ function buildLibrary() {
|
|
|
32
34
|
code = code.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
33
35
|
code = code.replace(/^export\s+(default\s+)?/gm, '');
|
|
34
36
|
code = code.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
|
|
35
|
-
return `// --- ${file} ${'
|
|
37
|
+
return `// --- ${file} ${'-'.repeat(60 - file.length)}\n${code.trim()}`;
|
|
36
38
|
});
|
|
37
39
|
|
|
38
40
|
let indexCode = fs.readFileSync(path.join(process.cwd(), 'index.js'), 'utf-8');
|
|
@@ -40,9 +42,9 @@ function buildLibrary() {
|
|
|
40
42
|
indexCode = indexCode.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
|
|
41
43
|
indexCode = indexCode.replace(/^export\s+(default\s+)?/gm, '');
|
|
42
44
|
|
|
43
|
-
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 */`;
|
|
44
46
|
|
|
45
|
-
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`;
|
|
46
48
|
|
|
47
49
|
fs.writeFileSync(OUT_FILE, bundle, 'utf-8');
|
|
48
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.)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/index.js — Dev server orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Ties together the HTTP server, file watcher, logger, and overlay
|
|
5
|
+
* to provide a complete development environment with live-reload,
|
|
6
|
+
* syntax validation, and a full-screen error overlay that surfaces
|
|
7
|
+
* both build-time and runtime ZQueryErrors.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const { args, flag, option } = require('../../args');
|
|
16
|
+
const { createServer } = require('./server');
|
|
17
|
+
const { startWatcher } = require('./watcher');
|
|
18
|
+
const { printBanner } = require('./logger');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Resolve project root
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
function resolveRoot(htmlEntry) {
|
|
25
|
+
// Explicit positional argument → zquery dev <dir>
|
|
26
|
+
for (let i = 1; i < args.length; i++) {
|
|
27
|
+
const prev = args[i - 1];
|
|
28
|
+
if (!args[i].startsWith('-') && prev !== '-p' && prev !== '--port' && prev !== '--index' && prev !== '-i') {
|
|
29
|
+
return path.resolve(process.cwd(), args[i]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Auto-detect: first candidate that contains the HTML entry file
|
|
34
|
+
const candidates = [
|
|
35
|
+
process.cwd(),
|
|
36
|
+
path.join(process.cwd(), 'public'),
|
|
37
|
+
path.join(process.cwd(), 'src'),
|
|
38
|
+
];
|
|
39
|
+
for (const c of candidates) {
|
|
40
|
+
if (fs.existsSync(path.join(c, htmlEntry))) return c;
|
|
41
|
+
}
|
|
42
|
+
return process.cwd();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// devServer — main entry point (called from cli/index.js)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
async function devServer() {
|
|
50
|
+
const htmlEntry = option('index', 'i', 'index.html');
|
|
51
|
+
const port = parseInt(option('port', 'p', '3100'), 10);
|
|
52
|
+
const noIntercept = flag('no-intercept');
|
|
53
|
+
const root = resolveRoot(htmlEntry);
|
|
54
|
+
|
|
55
|
+
// Start HTTP server + SSE pool
|
|
56
|
+
const { app, pool, listen } = await createServer({ root, htmlEntry, port, noIntercept });
|
|
57
|
+
|
|
58
|
+
// Start file watcher
|
|
59
|
+
const watcher = startWatcher({ root, pool });
|
|
60
|
+
|
|
61
|
+
// Boot
|
|
62
|
+
listen(() => {
|
|
63
|
+
printBanner({
|
|
64
|
+
port,
|
|
65
|
+
root: path.relative(process.cwd(), root) || '.',
|
|
66
|
+
htmlEntry,
|
|
67
|
+
noIntercept,
|
|
68
|
+
watchDirCount: watcher.dirs.length,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Graceful shutdown
|
|
73
|
+
process.on('SIGINT', () => {
|
|
74
|
+
console.log('\n Shutting down...');
|
|
75
|
+
watcher.destroy();
|
|
76
|
+
pool.closeAll();
|
|
77
|
+
app.close(() => process.exit(0));
|
|
78
|
+
setTimeout(() => process.exit(0), 1000);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = devServer;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/logger.js — Terminal output helpers
|
|
3
|
+
*
|
|
4
|
+
* Provides styled console output for the dev server: startup banner,
|
|
5
|
+
* timestamped file-change messages, and error formatting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// ANSI colour helpers (works on all modern terminals)
|
|
11
|
+
const c = {
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
bold: '\x1b[1m',
|
|
14
|
+
dim: '\x1b[2m',
|
|
15
|
+
red: '\x1b[31m',
|
|
16
|
+
green: '\x1b[32m',
|
|
17
|
+
yellow: '\x1b[33m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
magenta: '\x1b[35m',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function timestamp() {
|
|
23
|
+
return new Date().toLocaleTimeString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Event-level log helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function logCSS(relPath) {
|
|
31
|
+
console.log(` ${timestamp()} ${c.magenta} css ${c.reset} ${relPath}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function logReload(relPath) {
|
|
35
|
+
console.log(` ${timestamp()} ${c.cyan} reload ${c.reset} ${relPath}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function logError(descriptor) {
|
|
39
|
+
const t = timestamp();
|
|
40
|
+
console.log(` ${t} ${c.red} error ${c.reset} ${descriptor.file}`);
|
|
41
|
+
console.log(` ${c.red}${descriptor.type}: ${descriptor.message}${c.reset}`);
|
|
42
|
+
if (descriptor.line) {
|
|
43
|
+
console.log(` ${c.dim}at line ${descriptor.line}${descriptor.column ? ':' + descriptor.column : ''}${c.reset}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Startup banner
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
function printBanner({ port, root, htmlEntry, noIntercept, watchDirCount }) {
|
|
52
|
+
const rule = c.dim + '-'.repeat(40) + c.reset;
|
|
53
|
+
console.log(`\n ${c.bold}zQuery Dev Server${c.reset}`);
|
|
54
|
+
console.log(` ${rule}`);
|
|
55
|
+
console.log(` Local: ${c.cyan}http://localhost:${port}/${c.reset}`);
|
|
56
|
+
console.log(` Root: ${root}`);
|
|
57
|
+
if (htmlEntry !== 'index.html') {
|
|
58
|
+
console.log(` HTML: ${c.cyan}${htmlEntry}${c.reset}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(` Live Reload: ${c.green}enabled${c.reset} (SSE)`);
|
|
61
|
+
console.log(` Overlay: ${c.green}enabled${c.reset} (syntax + runtime + ZQueryError)`);
|
|
62
|
+
if (noIntercept) {
|
|
63
|
+
console.log(` Intercept: ${c.yellow}disabled${c.reset} (--no-intercept)`);
|
|
64
|
+
}
|
|
65
|
+
console.log(` Watching: all files in ${watchDirCount} director${watchDirCount === 1 ? 'y' : 'ies'}`);
|
|
66
|
+
console.log(` ${rule}`);
|
|
67
|
+
console.log(` Press Ctrl+C to stop\n`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { logCSS, logReload, logError, printBanner };
|