zero-query 1.0.2 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 --save-dev # or: npm install zero-query -g
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:
@@ -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(({ component }) => {
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 just inject the SSR body into <z-outlet>.
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
- // SSR render — pass server-fetched data as props to the component
157
- const body = await app.renderToString(component, props);
158
- const shell = await getShell();
159
-
160
- // Inject SSR content into <z-outlet …>
161
- let html = shell.replace(/(<z-outlet[^>]*>)(<\/z-outlet>)?/, `$1${body}</z-outlet>`);
162
-
163
- // Inject page-specific <title> and meta tags for SEO / social sharing
164
- html = html.replace(/<title>[^<]*<\/title>/, `<title>${meta.title}</title>`);
165
- html = html.replace(
166
- /<meta name="description" content="[^"]*">/,
167
- `<meta name="description" content="${meta.description}">`
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 -----------------------------------------------------------
Binary file
package/dist/zquery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v1.0.2
2
+ * zQuery (zeroQuery) v1.0.6
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 fbKeys = [];
4486
- const fbPattern = route.fallback
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 = createRouter;
6143
- $.getRouter = 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.2';
6257
+ $.version = '1.0.6';
6208
6258
  $.libSize = '~107 KB';
6209
- $.unitTests = {"passed":1906,"failed":0,"total":1906,"suites":521,"duration":3744,"ok":true};
6259
+ $.unitTests = {"passed":1931,"failed":0,"total":1931,"suites":523,"duration":3807,"ok":true};
6210
6260
  $.meta = {}; // populated at build time by CLI bundler
6211
6261
 
6212
6262
  $.noConflict = () => {