zero-query 1.0.2 → 1.0.9
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/bundle.js +133 -8
- package/cli/commands/dev/server.js +2 -1
- package/cli/scaffold/ssr/app/app.js +2 -2
- package/cli/scaffold/ssr/app/components/blog/post.js +8 -0
- package/cli/scaffold/ssr/server/index.js +15 -61
- package/cli/utils.js +10 -1
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +68 -18
- package/dist/zquery.min.js +601 -2
- package/index.d.ts +5 -2
- package/index.js +5 -4
- package/package.json +2 -2
- package/src/router.js +61 -12
- package/src/ssr.js +100 -0
- package/tests/cli.test.js +263 -0
- package/tests/router.test.js +65 -1
- package/tests/ssr.test.js +175 -0
- package/types/router.d.ts +25 -0
- package/types/ssr.d.ts +33 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
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 ~108 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
|
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
| **Selectors & DOM** | jQuery-like chainable selectors, traversal, DOM manipulation, events, animation |
|
|
30
30
|
| **HTTP** | Fetch wrapper with auto-JSON, interceptors (with unsubscribe & clear), HEAD requests, parallel requests (`http.all`), config inspection (`getConfig`), timeout/abort, base URL |
|
|
31
31
|
| **Utils** | debounce, throttle, pipe, once, sleep, memoize (LRU), escapeHtml, stripHtml, uuid, capitalize, truncate, range, chunk, groupBy, unique, pick, omit, getPath/setPath, isEmpty, clamp, retry, timeout, deepClone (enhanced fallback), deepMerge (prototype-pollution safe), storage/session wrappers, event bus |
|
|
32
|
-
| **Security** | XSS-safe template expressions (`{{}}` auto-escaping), sandboxed expression evaluator (blocks `window`, `Function`, `eval`, `RegExp`, `Error`, prototype chains), prototype pollution prevention in `deepMerge`/`setPath`, `z-link` protocol validation, SSR error sanitization |
|
|
32
|
+
| **Security** | XSS-safe template expressions (`{{}}` auto-escaping), sandboxed expression evaluator (blocks `window`, `Function`, `eval`, `RegExp`, `Error`, prototype chains), prototype pollution prevention in `deepMerge`/`setPath`, `z-link` protocol validation, SSR error sanitization, `renderShell()` metadata injection hardening (script-tag breakout prevention, ReDoS-safe OG keys, safe `.replace()` patterns) |
|
|
33
33
|
| **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 |
|
|
34
|
-
| **SSR** | Server-side rendering to HTML strings in Node.js - `createSSRApp()`, `renderToString()`, `renderPage()` with SEO/Open Graph support, `renderBatch()` for parallel rendering, fragment mode, hydration markers, graceful error handling, `escapeHtml()` utility |
|
|
34
|
+
| **SSR** | Server-side rendering to HTML strings in Node.js - `createSSRApp()`, `renderToString()`, `renderPage()` with SEO/Open Graph support, `renderShell()` for injecting SSR into custom HTML shells, `renderBatch()` for parallel rendering, fragment mode, hydration markers, graceful error handling, `escapeHtml()` utility |
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
@@ -43,7 +43,7 @@ The fastest way to develop with zQuery is via the built-in **CLI dev server** wi
|
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
# Install (per-project or globally)
|
|
46
|
-
npm install zero-query
|
|
46
|
+
npm install zero-query # or: npm install zero-query -g
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Scaffold a new project and start the server:
|
|
@@ -77,7 +77,7 @@ If you prefer **zero tooling**, download `dist/zquery.min.js` from the [dist/ fo
|
|
|
77
77
|
git clone https://github.com/tonywied17/zero-query.git
|
|
78
78
|
cd zero-query
|
|
79
79
|
npx zquery build
|
|
80
|
-
# → dist/zquery.min.js (~
|
|
80
|
+
# → dist/zquery.min.js (~108 KB)
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
### Include in HTML
|
|
@@ -363,7 +363,7 @@ $.router({ base: '/my-app', routes });
|
|
|
363
363
|
| `$.storage` `$.session` | Storage wrappers |
|
|
364
364
|
| `$.EventBus` `$.bus` | Event bus |
|
|
365
365
|
| `$.onError` `$.ZQueryError` `$.ErrorCode` `$.guardCallback` `$.guardAsync` `$.formatError` `$.validate` | Error handling |
|
|
366
|
-
| `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~
|
|
366
|
+
| `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~108 KB\"`) |
|
|
367
367
|
| `$.unitTests` | Build-time test results `{ passed, failed, total, suites, duration, ok }` |
|
|
368
368
|
| `$.meta` | Build metadata (populated by CLI bundler) |
|
|
369
369
|
| `$.noConflict` | Release `$` global |
|
package/cli/commands/bundle.js
CHANGED
|
@@ -87,14 +87,137 @@ function walkImportGraph(entry) {
|
|
|
87
87
|
return order;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Strip ES module import/export syntax, keeping declarations.
|
|
92
|
+
* Exported const/let are converted to var so they hoist past the per-module
|
|
93
|
+
* block scope and remain accessible to downstream modules.
|
|
94
|
+
* Exported function/class are converted to var assignments for the same reason.
|
|
95
|
+
* Non-exported declarations stay as-is and remain block-scoped (private).
|
|
96
|
+
*
|
|
97
|
+
* Template literals are temporarily hidden via a character-level scanner
|
|
98
|
+
* (handles nested backtick expressions) so that code examples inside
|
|
99
|
+
* backtick strings aren't accidentally rewritten.
|
|
100
|
+
*/
|
|
91
101
|
function stripModuleSyntax(code) {
|
|
102
|
+
// -- Hide template literals (supports nesting) -------------------------
|
|
103
|
+
const templates = [];
|
|
104
|
+
|
|
105
|
+
function scanTemplateLiteral(str, start) {
|
|
106
|
+
let i = start + 1; // skip opening backtick
|
|
107
|
+
while (i < str.length) {
|
|
108
|
+
const ch = str[i];
|
|
109
|
+
if (ch === '\\') { i += 2; continue; }
|
|
110
|
+
if (ch === '`') { return i + 1; } // end of template
|
|
111
|
+
if (ch === '$' && str[i + 1] === '{') {
|
|
112
|
+
i += 2; // skip ${
|
|
113
|
+
let depth = 1;
|
|
114
|
+
while (i < str.length && depth > 0) {
|
|
115
|
+
const c = str[i];
|
|
116
|
+
if (c === '{') { depth++; i++; }
|
|
117
|
+
else if (c === '}') { depth--; i++; }
|
|
118
|
+
else if (c === '`') { i = scanTemplateLiteral(str, i); }
|
|
119
|
+
else if (c === "'" || c === '"') { i = skipString(str, i); }
|
|
120
|
+
else if (c === '/' && str[i + 1] === '/') { while (i < str.length && str[i] !== '\n') i++; }
|
|
121
|
+
else if (c === '/' && str[i + 1] === '*') { i += 2; while (i < str.length - 1 && !(str[i] === '*' && str[i + 1] === '/')) i++; i += 2; }
|
|
122
|
+
else { i++; }
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
return i;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function skipString(str, start) {
|
|
132
|
+
const q = str[start];
|
|
133
|
+
let i = start + 1;
|
|
134
|
+
while (i < str.length) {
|
|
135
|
+
if (str[i] === '\\') { i += 2; continue; }
|
|
136
|
+
if (str[i] === q) { return i + 1; }
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
return i;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let hidden = '';
|
|
143
|
+
let pos = 0;
|
|
144
|
+
while (pos < code.length) {
|
|
145
|
+
const ch = code[pos];
|
|
146
|
+
if (ch === "'" || ch === '"') {
|
|
147
|
+
const end = skipString(code, pos);
|
|
148
|
+
hidden += code.substring(pos, end);
|
|
149
|
+
pos = end;
|
|
150
|
+
} else if (ch === '/' && code[pos + 1] === '/') {
|
|
151
|
+
let end = pos;
|
|
152
|
+
while (end < code.length && code[end] !== '\n') end++;
|
|
153
|
+
hidden += code.substring(pos, end);
|
|
154
|
+
pos = end;
|
|
155
|
+
} else if (ch === '/' && code[pos + 1] === '*') {
|
|
156
|
+
let end = pos + 2;
|
|
157
|
+
while (end < code.length - 1 && !(code[end] === '*' && code[end + 1] === '/')) end++;
|
|
158
|
+
end += 2;
|
|
159
|
+
hidden += code.substring(pos, end);
|
|
160
|
+
pos = end;
|
|
161
|
+
} else if (ch === '`') {
|
|
162
|
+
const end = scanTemplateLiteral(code, pos);
|
|
163
|
+
templates.push(code.substring(pos, end));
|
|
164
|
+
hidden += `__TPL_${templates.length - 1}__`;
|
|
165
|
+
pos = end;
|
|
166
|
+
} else {
|
|
167
|
+
hidden += ch;
|
|
168
|
+
pos++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// -- Apply import / export transforms -----------------------------------
|
|
173
|
+
code = hidden;
|
|
92
174
|
code = code.replace(/^\s*import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
93
175
|
code = code.replace(/^\s*import\s+['"].*?['"];?\s*$/gm, '');
|
|
94
176
|
code = code.replace(/^(\s*)export\s+default\s+/gm, '$1');
|
|
95
|
-
|
|
96
|
-
code = code.replace(
|
|
97
|
-
|
|
177
|
+
// Convert exported const/let/var to var (hoists past block scope)
|
|
178
|
+
code = code.replace(/^(\s*)export\s+(const|let|var)\s/gm, '$1var ');
|
|
179
|
+
// Convert exported function/async function to var assignment (hoists past block scope)
|
|
180
|
+
code = code.replace(/^(\s*)export\s+async\s+function\s+(\w+)/gm, '$1var $2 = async function $2');
|
|
181
|
+
code = code.replace(/^(\s*)export\s+function\s+(\w+)/gm, '$1var $2 = function $2');
|
|
182
|
+
// Convert exported class to var assignment
|
|
183
|
+
code = code.replace(/^(\s*)export\s+class\s+(\w+)/gm, '$1var $2 = class $2');
|
|
184
|
+
// Collect names from bare export blocks: export { a, b, c };
|
|
185
|
+
// These names are declared elsewhere (function/const/let) and need to be
|
|
186
|
+
// hoisted past the per-module block scope. We collect them here and
|
|
187
|
+
// the caller converts their declarations to var-based forms.
|
|
188
|
+
const bareExportNames = [];
|
|
189
|
+
code = code.replace(/^\s*export\s*\{([^}]+)\};?\s*$/gm, (_, names) => {
|
|
190
|
+
for (const n of names.split(',')) {
|
|
191
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
192
|
+
if (parts[0]) bareExportNames.push({ local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() });
|
|
193
|
+
}
|
|
194
|
+
return '';
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// For every bare-exported name, convert its block-scoped declaration to a
|
|
198
|
+
// var-based form so it hoists past the per-module { } wrapper:
|
|
199
|
+
// function foo(…) { … } → var foo = function foo(…) { … }
|
|
200
|
+
// async function foo(…) → var foo = async function foo(…)
|
|
201
|
+
// const/let foo = … → var foo = …
|
|
202
|
+
for (const { local } of bareExportNames) {
|
|
203
|
+
const fnRe = new RegExp(`^(\\s*)function\\s+${local}\\s*\\(`, 'gm');
|
|
204
|
+
code = code.replace(fnRe, `$1var ${local} = function ${local}(`);
|
|
205
|
+
const asyncFnRe = new RegExp(`^(\\s*)async\\s+function\\s+${local}\\s*\\(`, 'gm');
|
|
206
|
+
code = code.replace(asyncFnRe, `$1var ${local} = async function ${local}(`);
|
|
207
|
+
const constLetRe = new RegExp(`^(\\s*)(const|let)\\s+${local}\\b`, 'gm');
|
|
208
|
+
code = code.replace(constLetRe, `$1var ${local}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create aliases for "export { local as exported }" where the names differ
|
|
212
|
+
for (const { local, exported } of bareExportNames) {
|
|
213
|
+
if (exported !== local) {
|
|
214
|
+
code += `\nvar ${exported} = ${local};`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// -- Restore template literals ------------------------------------------
|
|
219
|
+
code = code.replace(/__TPL_(\d+)__/g, (_, i) => templates[i]);
|
|
220
|
+
return { code, bareExportNames };
|
|
98
221
|
}
|
|
99
222
|
|
|
100
223
|
/** Replace import.meta.url with a runtime equivalent. */
|
|
@@ -381,8 +504,8 @@ function collectInlineResources(files, projectRoot) {
|
|
|
381
504
|
|
|
382
505
|
// styleUrl:
|
|
383
506
|
const styleUrlRe = /styleUrl\s*:\s*['"]([^'"]+)['"]/g;
|
|
384
|
-
|
|
385
|
-
|
|
507
|
+
let styleMatch;
|
|
508
|
+
while ((styleMatch = styleUrlRe.exec(code)) !== null) {
|
|
386
509
|
const stylePath = path.join(fileDir, styleMatch[1]);
|
|
387
510
|
if (fs.existsSync(stylePath)) {
|
|
388
511
|
const relKey = path.relative(projectRoot, stylePath).replace(/\\/g, '/');
|
|
@@ -877,12 +1000,13 @@ function bundleApp() {
|
|
|
877
1000
|
|
|
878
1001
|
const sections = files.map(file => {
|
|
879
1002
|
let code = fs.readFileSync(file, 'utf-8');
|
|
880
|
-
|
|
1003
|
+
const stripped = stripModuleSyntax(code);
|
|
1004
|
+
code = stripped.code;
|
|
881
1005
|
code = replaceImportMeta(code, file, projectRoot);
|
|
882
1006
|
code = rewriteResourceUrls(code, file, projectRoot);
|
|
883
1007
|
code = minifyTemplateLiterals(code);
|
|
884
1008
|
const rel = path.relative(projectRoot, file);
|
|
885
|
-
return `// --- ${rel} ${'-'.repeat(Math.max(1, 60 - rel.length))}\n${code.trim()}`;
|
|
1009
|
+
return `// --- ${rel} ${'-'.repeat(Math.max(1, 60 - rel.length))}\n{\n${code.trim()}\n}`;
|
|
886
1010
|
});
|
|
887
1011
|
|
|
888
1012
|
// Embed zquery.min.js
|
|
@@ -1056,3 +1180,4 @@ function bundleApp() {
|
|
|
1056
1180
|
}
|
|
1057
1181
|
|
|
1058
1182
|
module.exports = bundleApp;
|
|
1183
|
+
module.exports.stripModuleSyntax = stripModuleSyntax;
|
|
@@ -92,7 +92,8 @@ async function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
|
92
92
|
zeroHttp = require('zero-http');
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const { createApp, static: serveStatic } = zeroHttp;
|
|
95
|
+
const { createApp, static: serveStatic, debug } = zeroHttp;
|
|
96
|
+
debug.level('silent');
|
|
96
97
|
|
|
97
98
|
const app = createApp();
|
|
98
99
|
const pool = new SSEPool();
|
|
@@ -35,7 +35,7 @@ const router = $.router({
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
// Update document.title on client-side navigations
|
|
38
|
-
router.onChange(({
|
|
39
|
-
const title = routeTitles[component];
|
|
38
|
+
router.onChange(({ route }) => {
|
|
39
|
+
const title = routeTitles[route.component];
|
|
40
40
|
if (title) document.title = title;
|
|
41
41
|
});
|
|
@@ -34,6 +34,7 @@ export const blogPost = {
|
|
|
34
34
|
if (ssrData && ssrData.component === 'blog-post' && ssrData.params.slug === slug) {
|
|
35
35
|
this.state.post = ssrData.props.post;
|
|
36
36
|
this.state.loaded = true;
|
|
37
|
+
if (ssrData.meta && ssrData.meta.title) document.title = ssrData.meta.title;
|
|
37
38
|
window.__SSR_DATA__ = null;
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
@@ -44,6 +45,13 @@ export const blogPost = {
|
|
|
44
45
|
this.state.post = await res.json();
|
|
45
46
|
}
|
|
46
47
|
this.state.loaded = true;
|
|
48
|
+
|
|
49
|
+
// Update page title for client-side navigations
|
|
50
|
+
if (this.state.post) {
|
|
51
|
+
document.title = `${this.state.post.title} — {{NAME}}`;
|
|
52
|
+
} else {
|
|
53
|
+
document.title = 'Post Not Found — {{NAME}}';
|
|
54
|
+
}
|
|
47
55
|
},
|
|
48
56
|
|
|
49
57
|
render() {
|
|
@@ -12,7 +12,7 @@ import { createServer } from 'node:http';
|
|
|
12
12
|
import { readFile } from 'node:fs/promises';
|
|
13
13
|
import { join, extname, resolve } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { createSSRApp } from 'zero-query/ssr';
|
|
15
|
+
import { createSSRApp, matchRoute } from 'zero-query/ssr';
|
|
16
16
|
|
|
17
17
|
// Shared component definitions - same ones the client registers
|
|
18
18
|
import { homePage } from '../app/components/home.js';
|
|
@@ -38,29 +38,6 @@ app.component('blog-list', blogList);
|
|
|
38
38
|
app.component('blog-post', blogPost);
|
|
39
39
|
app.component('not-found', notFound);
|
|
40
40
|
|
|
41
|
-
// --- Route matching ---------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Match a pathname to a route definition. Supports :param segments.
|
|
45
|
-
* Returns { component, params } or the not-found fallback.
|
|
46
|
-
*/
|
|
47
|
-
function matchRoute(pathname) {
|
|
48
|
-
for (const route of routes) {
|
|
49
|
-
const paramNames = [];
|
|
50
|
-
const pattern = route.path.replace(/:(\w+)/g, (_, name) => {
|
|
51
|
-
paramNames.push(name);
|
|
52
|
-
return '([^/]+)';
|
|
53
|
-
});
|
|
54
|
-
const match = new RegExp(`^${pattern}$`).exec(pathname);
|
|
55
|
-
if (match) {
|
|
56
|
-
const params = {};
|
|
57
|
-
paramNames.forEach((name, i) => { params[name] = match[i + 1]; });
|
|
58
|
-
return { component: route.component, params };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return { component: 'not-found', params: {} };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
41
|
// --- Server-side data fetching ----------------------------------------------
|
|
65
42
|
|
|
66
43
|
/**
|
|
@@ -141,7 +118,7 @@ function getMetaForRoute(component, params, props) {
|
|
|
141
118
|
|
|
142
119
|
// Read the index.html shell once at startup — it already has z-link nav,
|
|
143
120
|
// client scripts (zquery.min.js + app/app.js), and the <z-outlet> tag.
|
|
144
|
-
// On each request we
|
|
121
|
+
// On each request we render the matched component into the shell.
|
|
145
122
|
let shellCache = null;
|
|
146
123
|
async function getShell() {
|
|
147
124
|
if (!shellCache) shellCache = await readFile(join(ROOT, 'index.html'), 'utf-8');
|
|
@@ -149,45 +126,22 @@ async function getShell() {
|
|
|
149
126
|
}
|
|
150
127
|
|
|
151
128
|
async function render(pathname) {
|
|
152
|
-
const { component, params } = matchRoute(pathname);
|
|
129
|
+
const { component, params } = matchRoute(routes, pathname);
|
|
153
130
|
const props = getPropsForRoute(component, params);
|
|
154
131
|
const meta = getMetaForRoute(component, params, props);
|
|
155
132
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
);
|
|
169
|
-
html = html.replace(
|
|
170
|
-
/<meta property="og:title" content="[^"]*">/,
|
|
171
|
-
`<meta property="og:title" content="${meta.title}">`
|
|
172
|
-
);
|
|
173
|
-
html = html.replace(
|
|
174
|
-
/<meta property="og:description" content="[^"]*">/,
|
|
175
|
-
`<meta property="og:description" content="${meta.description}">`
|
|
176
|
-
);
|
|
177
|
-
html = html.replace(
|
|
178
|
-
/<meta property="og:type" content="[^"]*">/,
|
|
179
|
-
`<meta property="og:type" content="${meta.ogType}">`
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
// Embed server data so the client can hydrate without re-fetching.
|
|
183
|
-
// Also include meta so client can update document.title on navigation.
|
|
184
|
-
const ssrData = JSON.stringify({ component, params, props, meta });
|
|
185
|
-
html = html.replace(
|
|
186
|
-
'</head>',
|
|
187
|
-
`<script>window.__SSR_DATA__=${ssrData};</script>\n</head>`
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
return html;
|
|
133
|
+
return app.renderShell(await getShell(), {
|
|
134
|
+
component,
|
|
135
|
+
props,
|
|
136
|
+
title: meta.title,
|
|
137
|
+
description: meta.description,
|
|
138
|
+
og: {
|
|
139
|
+
title: meta.title,
|
|
140
|
+
description: meta.description,
|
|
141
|
+
type: meta.ogType,
|
|
142
|
+
},
|
|
143
|
+
ssrData: { component, params, props, meta },
|
|
144
|
+
});
|
|
191
145
|
}
|
|
192
146
|
|
|
193
147
|
// --- Static files -----------------------------------------------------------
|
package/cli/utils.js
CHANGED
|
@@ -206,9 +206,18 @@ function _minifyBody(code) {
|
|
|
206
206
|
|
|
207
207
|
// ── Whitespace: collapse ────────────────────────────────────
|
|
208
208
|
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
209
|
-
|
|
209
|
+
let hasNewline = ch === '\n' || ch === '\r';
|
|
210
|
+
while (i < code.length && (code[i] === ' ' || code[i] === '\t' || code[i] === '\n' || code[i] === '\r')) {
|
|
211
|
+
if (code[i] === '\n' || code[i] === '\r') hasNewline = true;
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
210
214
|
const before = out[out.length - 1];
|
|
211
215
|
const after = code[i];
|
|
216
|
+
// After '}', a newline may be needed for ASI (e.g. var x=function(){}⏎var y).
|
|
217
|
+
// A space alone doesn't trigger ASI, so preserve ';\n' when '}' precedes
|
|
218
|
+
// an identifier-start character and the original whitespace had a newline.
|
|
219
|
+
const afterIsId = after && ((after >= 'a' && after <= 'z') || (after >= 'A' && after <= 'Z') || after === '_' || after === '$');
|
|
220
|
+
if (before === '}' && afterIsId && hasNewline) { out += '\n'; continue; }
|
|
212
221
|
if (_needsSpace(before, after)) out += ' ';
|
|
213
222
|
continue;
|
|
214
223
|
}
|
package/dist/zquery.dist.zip
CHANGED
|
Binary file
|
package/dist/zquery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v1.0.
|
|
2
|
+
* zQuery (zeroQuery) v1.0.9
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
5
|
* (c) 2026 Anthony Wiedman - MIT License
|
|
@@ -4470,24 +4470,15 @@ class Router {
|
|
|
4470
4470
|
|
|
4471
4471
|
add(route) {
|
|
4472
4472
|
// Compile path pattern into regex
|
|
4473
|
-
const keys =
|
|
4474
|
-
const pattern = route.path
|
|
4475
|
-
.replace(/:(\w+)/g, (_, key) => { keys.push(key); return '([^/]+)'; })
|
|
4476
|
-
.replace(/\*/g, '(.*)');
|
|
4477
|
-
const regex = new RegExp(`^${pattern}$`);
|
|
4478
|
-
|
|
4473
|
+
const { regex, keys } = compilePath(route.path);
|
|
4479
4474
|
this._routes.push({ ...route, _regex: regex, _keys: keys });
|
|
4480
4475
|
|
|
4481
4476
|
// Per-route fallback: register an alias path for the same component.
|
|
4482
4477
|
// e.g. { path: '/docs/:section', fallback: '/docs', component: 'docs-page' }
|
|
4483
4478
|
// When matched via fallback, missing params are undefined.
|
|
4484
4479
|
if (route.fallback) {
|
|
4485
|
-
const
|
|
4486
|
-
|
|
4487
|
-
.replace(/:(\w+)/g, (_, key) => { fbKeys.push(key); return '([^/]+)'; })
|
|
4488
|
-
.replace(/\*/g, '(.*)');
|
|
4489
|
-
const fbRegex = new RegExp(`^${fbPattern}$`);
|
|
4490
|
-
this._routes.push({ ...route, path: route.fallback, _regex: fbRegex, _keys: fbKeys });
|
|
4480
|
+
const fb = compilePath(route.fallback);
|
|
4481
|
+
this._routes.push({ ...route, path: route.fallback, _regex: fb.regex, _keys: fb.keys });
|
|
4491
4482
|
}
|
|
4492
4483
|
|
|
4493
4484
|
return this;
|
|
@@ -4984,6 +4975,64 @@ class Router {
|
|
|
4984
4975
|
}
|
|
4985
4976
|
|
|
4986
4977
|
|
|
4978
|
+
// ---------------------------------------------------------------------------
|
|
4979
|
+
// Path compilation (shared by Router.add and matchRoute)
|
|
4980
|
+
// ---------------------------------------------------------------------------
|
|
4981
|
+
|
|
4982
|
+
/**
|
|
4983
|
+
* Compile a route path pattern into a RegExp and param key list.
|
|
4984
|
+
* Supports `:param` segments and `*` wildcard.
|
|
4985
|
+
* @param {string} path - e.g. '/user/:id' or '/files/*'
|
|
4986
|
+
* @returns {{ regex: RegExp, keys: string[] }}
|
|
4987
|
+
*/
|
|
4988
|
+
function compilePath(path) {
|
|
4989
|
+
const keys = [];
|
|
4990
|
+
const pattern = path
|
|
4991
|
+
.replace(/:(\w+)/g, (_, key) => { keys.push(key); return '([^/]+)'; })
|
|
4992
|
+
.replace(/\*/g, '(.*)');
|
|
4993
|
+
return { regex: new RegExp(`^${pattern}$`), keys };
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4996
|
+
// ---------------------------------------------------------------------------
|
|
4997
|
+
// Standalone route matcher (DOM-free — usable on server and client)
|
|
4998
|
+
// ---------------------------------------------------------------------------
|
|
4999
|
+
|
|
5000
|
+
/**
|
|
5001
|
+
* Match a pathname against an array of route definitions.
|
|
5002
|
+
* Returns `{ component, params }`. If no route matches, falls back to the
|
|
5003
|
+
* `fallback` component name (default `'not-found'`).
|
|
5004
|
+
*
|
|
5005
|
+
* This is the same matching logic the client-side router uses internally,
|
|
5006
|
+
* extracted so SSR servers can resolve URLs without the DOM.
|
|
5007
|
+
*
|
|
5008
|
+
* @param {Array<{ path: string, component: string, fallback?: string }>} routes
|
|
5009
|
+
* @param {string} pathname - URL path to match, e.g. '/blog/my-post'
|
|
5010
|
+
* @param {string} [fallback='not-found'] - Component name when nothing matches
|
|
5011
|
+
* @returns {{ component: string, params: Record<string, string> }}
|
|
5012
|
+
*/
|
|
5013
|
+
function matchRoute(routes, pathname, fallback = 'not-found') {
|
|
5014
|
+
for (const route of routes) {
|
|
5015
|
+
const { regex, keys } = compilePath(route.path);
|
|
5016
|
+
const m = pathname.match(regex);
|
|
5017
|
+
if (m) {
|
|
5018
|
+
const params = {};
|
|
5019
|
+
keys.forEach((key, i) => { params[key] = m[i + 1]; });
|
|
5020
|
+
return { component: route.component, params };
|
|
5021
|
+
}
|
|
5022
|
+
// Per-route fallback alias (same as Router.add)
|
|
5023
|
+
if (route.fallback) {
|
|
5024
|
+
const fb = compilePath(route.fallback);
|
|
5025
|
+
const fbm = pathname.match(fb.regex);
|
|
5026
|
+
if (fbm) {
|
|
5027
|
+
const params = {};
|
|
5028
|
+
fb.keys.forEach((key, i) => { params[key] = fbm[i + 1]; });
|
|
5029
|
+
return { component: route.component, params };
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
return { component: fallback, params: {} };
|
|
5034
|
+
}
|
|
5035
|
+
|
|
4987
5036
|
// ---------------------------------------------------------------------------
|
|
4988
5037
|
// Factory
|
|
4989
5038
|
// ---------------------------------------------------------------------------
|
|
@@ -6139,8 +6188,9 @@ $.morphElement = morphElement;
|
|
|
6139
6188
|
$.safeEval = safeEval;
|
|
6140
6189
|
|
|
6141
6190
|
// --- Router ----------------------------------------------------------------
|
|
6142
|
-
$.router
|
|
6143
|
-
$.getRouter
|
|
6191
|
+
$.router = createRouter;
|
|
6192
|
+
$.getRouter = getRouter;
|
|
6193
|
+
$.matchRoute = matchRoute;
|
|
6144
6194
|
|
|
6145
6195
|
// --- Store -----------------------------------------------------------------
|
|
6146
6196
|
$.store = createStore;
|
|
@@ -6204,9 +6254,9 @@ $.validate = validate;
|
|
|
6204
6254
|
$.formatError = formatError;
|
|
6205
6255
|
|
|
6206
6256
|
// --- Meta ------------------------------------------------------------------
|
|
6207
|
-
$.version = '1.0.
|
|
6208
|
-
$.libSize = '~
|
|
6209
|
-
$.unitTests = {"passed":
|
|
6257
|
+
$.version = '1.0.9';
|
|
6258
|
+
$.libSize = '~108 KB';
|
|
6259
|
+
$.unitTests = {"passed":1965,"failed":0,"total":1965,"suites":525,"duration":3752,"ok":true};
|
|
6210
6260
|
$.meta = {}; // populated at build time by CLI bundler
|
|
6211
6261
|
|
|
6212
6262
|
$.noConflict = () => {
|