zero-query 0.9.9 → 1.0.0

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.
Files changed (97) hide show
  1. package/README.md +33 -32
  2. package/cli/args.js +1 -1
  3. package/cli/commands/build.js +2 -2
  4. package/cli/commands/bundle.js +15 -15
  5. package/cli/commands/create.js +2 -2
  6. package/cli/commands/dev/devtools/index.js +1 -1
  7. package/cli/commands/dev/devtools/js/core.js +14 -14
  8. package/cli/commands/dev/devtools/js/elements.js +4 -4
  9. package/cli/commands/dev/devtools/js/stats.js +1 -1
  10. package/cli/commands/dev/devtools/styles.css +2 -2
  11. package/cli/commands/dev/index.js +2 -2
  12. package/cli/commands/dev/logger.js +1 -1
  13. package/cli/commands/dev/overlay.js +21 -14
  14. package/cli/commands/dev/server.js +5 -5
  15. package/cli/commands/dev/validator.js +7 -7
  16. package/cli/commands/dev/watcher.js +6 -6
  17. package/cli/help.js +3 -3
  18. package/cli/index.js +2 -2
  19. package/cli/scaffold/default/app/app.js +17 -18
  20. package/cli/scaffold/default/app/components/about.js +9 -9
  21. package/cli/scaffold/default/app/components/api-demo.js +6 -6
  22. package/cli/scaffold/default/app/components/contact-card.js +4 -4
  23. package/cli/scaffold/default/app/components/contacts/contacts.css +2 -2
  24. package/cli/scaffold/default/app/components/contacts/contacts.html +3 -3
  25. package/cli/scaffold/default/app/components/contacts/contacts.js +11 -11
  26. package/cli/scaffold/default/app/components/counter.js +8 -8
  27. package/cli/scaffold/default/app/components/home.js +13 -13
  28. package/cli/scaffold/default/app/components/not-found.js +1 -1
  29. package/cli/scaffold/default/app/components/playground/playground.css +1 -1
  30. package/cli/scaffold/default/app/components/playground/playground.html +11 -11
  31. package/cli/scaffold/default/app/components/playground/playground.js +11 -11
  32. package/cli/scaffold/default/app/components/todos.js +8 -8
  33. package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
  34. package/cli/scaffold/default/app/components/toolkit/toolkit.html +4 -4
  35. package/cli/scaffold/default/app/components/toolkit/toolkit.js +7 -7
  36. package/cli/scaffold/default/app/routes.js +1 -1
  37. package/cli/scaffold/default/app/store.js +1 -1
  38. package/cli/scaffold/default/global.css +2 -2
  39. package/cli/scaffold/default/index.html +2 -2
  40. package/cli/scaffold/minimal/app/app.js +6 -7
  41. package/cli/scaffold/minimal/app/components/about.js +5 -5
  42. package/cli/scaffold/minimal/app/components/counter.js +6 -6
  43. package/cli/scaffold/minimal/app/components/home.js +8 -8
  44. package/cli/scaffold/minimal/app/components/not-found.js +1 -1
  45. package/cli/scaffold/minimal/app/routes.js +1 -1
  46. package/cli/scaffold/minimal/app/store.js +1 -1
  47. package/cli/scaffold/minimal/global.css +2 -2
  48. package/cli/scaffold/minimal/index.html +1 -1
  49. package/cli/scaffold/ssr/app/app.js +1 -2
  50. package/cli/scaffold/ssr/app/components/about.js +5 -5
  51. package/cli/scaffold/ssr/app/components/home.js +2 -2
  52. package/cli/scaffold/ssr/app/components/not-found.js +1 -1
  53. package/cli/scaffold/ssr/app/routes.js +1 -1
  54. package/cli/scaffold/ssr/global.css +2 -2
  55. package/cli/scaffold/ssr/index.html +2 -2
  56. package/cli/scaffold/ssr/server/index.js +4 -4
  57. package/cli/utils.js +6 -6
  58. package/dist/zquery.dist.zip +0 -0
  59. package/dist/zquery.js +508 -227
  60. package/dist/zquery.min.js +2 -2
  61. package/index.d.ts +16 -13
  62. package/index.js +7 -5
  63. package/package.json +2 -2
  64. package/src/component.js +64 -63
  65. package/src/core.js +15 -15
  66. package/src/diff.js +38 -38
  67. package/src/errors.js +17 -17
  68. package/src/expression.js +15 -17
  69. package/src/http.js +4 -4
  70. package/src/reactive.js +75 -9
  71. package/src/router.js +104 -24
  72. package/src/ssr.js +28 -28
  73. package/src/store.js +103 -21
  74. package/src/utils.js +64 -12
  75. package/tests/audit.test.js +143 -15
  76. package/tests/cli.test.js +20 -20
  77. package/tests/component.test.js +121 -121
  78. package/tests/core.test.js +56 -56
  79. package/tests/diff.test.js +42 -42
  80. package/tests/errors.test.js +5 -5
  81. package/tests/expression.test.js +58 -53
  82. package/tests/http.test.js +20 -20
  83. package/tests/reactive.test.js +185 -24
  84. package/tests/router.test.js +501 -74
  85. package/tests/ssr.test.js +15 -13
  86. package/tests/store.test.js +264 -23
  87. package/tests/utils.test.js +163 -26
  88. package/types/collection.d.ts +2 -2
  89. package/types/component.d.ts +5 -5
  90. package/types/errors.d.ts +3 -3
  91. package/types/http.d.ts +3 -3
  92. package/types/misc.d.ts +9 -9
  93. package/types/reactive.d.ts +25 -3
  94. package/types/router.d.ts +10 -6
  95. package/types/ssr.d.ts +2 -2
  96. package/types/store.d.ts +40 -5
  97. package/types/utils.d.ts +1 -1
package/src/reactive.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery Reactive Proxy-based deep reactivity system
2
+ * zQuery Reactive - Proxy-based deep reactivity system
3
3
  *
4
4
  * Creates observable objects that trigger callbacks on mutation.
5
5
  * Used internally by components and store for auto-updates.
@@ -67,7 +67,7 @@ export function reactive(target, onChange, _path = '') {
67
67
 
68
68
 
69
69
  // ---------------------------------------------------------------------------
70
- // Signal lightweight reactive primitive (inspired by Solid/Preact signals)
70
+ // Signal - lightweight reactive primitive (inspired by Solid/Preact signals)
71
71
  // ---------------------------------------------------------------------------
72
72
  export class Signal {
73
73
  constructor(value) {
@@ -96,7 +96,11 @@ export class Signal {
96
96
  peek() { return this._value; }
97
97
 
98
98
  _notify() {
99
- // Snapshot subscribers before iterating — a subscriber might modify
99
+ if (Signal._batching) {
100
+ Signal._batchQueue.add(this);
101
+ return;
102
+ }
103
+ // Snapshot subscribers before iterating - a subscriber might modify
100
104
  // the set (e.g., an effect re-running, adding itself back)
101
105
  const subs = [...this._subscribers];
102
106
  for (let i = 0; i < subs.length; i++) {
@@ -117,10 +121,13 @@ export class Signal {
117
121
 
118
122
  // Active effect tracking
119
123
  Signal._activeEffect = null;
124
+ // Batch state
125
+ Signal._batching = false;
126
+ Signal._batchQueue = new Set();
120
127
 
121
128
  /**
122
129
  * Create a signal
123
- * @param {*} initial initial value
130
+ * @param {*} initial - initial value
124
131
  * @returns {Signal}
125
132
  */
126
133
  export function signal(initial) {
@@ -129,7 +136,7 @@ export function signal(initial) {
129
136
 
130
137
  /**
131
138
  * Create a computed signal (derived from other signals)
132
- * @param {Function} fn computation function
139
+ * @param {Function} fn - computation function
133
140
  * @returns {Signal}
134
141
  */
135
142
  export function computed(fn) {
@@ -147,10 +154,10 @@ export function computed(fn) {
147
154
  /**
148
155
  * Create a side-effect that auto-tracks signal dependencies.
149
156
  * Returns a dispose function that removes the effect from all
150
- * signals it subscribed to prevents memory leaks.
157
+ * signals it subscribed to - prevents memory leaks.
151
158
  *
152
- * @param {Function} fn effect function
153
- * @returns {Function} dispose function
159
+ * @param {Function} fn - effect function
160
+ * @returns {Function} - dispose function
154
161
  */
155
162
  export function effect(fn) {
156
163
  const execute = () => {
@@ -183,6 +190,65 @@ export function effect(fn) {
183
190
  }
184
191
  execute._deps.clear();
185
192
  }
186
- // Don't clobber _activeEffect another effect may be running
193
+ // Don't clobber _activeEffect - another effect may be running
187
194
  };
188
195
  }
196
+
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // batch() - defer signal notifications until the batch completes
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /**
203
+ * Batch multiple signal writes - subscribers and effects fire once at the end.
204
+ * @param {Function} fn - function that performs signal writes
205
+ */
206
+ export function batch(fn) {
207
+ if (Signal._batching) {
208
+ // Already inside a batch, just run
209
+ fn();
210
+ return;
211
+ }
212
+ Signal._batching = true;
213
+ Signal._batchQueue.clear();
214
+ try {
215
+ fn();
216
+ } finally {
217
+ Signal._batching = false;
218
+ // Collect all unique subscribers across all queued signals
219
+ // so each subscriber/effect runs exactly once
220
+ const subs = new Set();
221
+ for (const sig of Signal._batchQueue) {
222
+ for (const sub of sig._subscribers) {
223
+ subs.add(sub);
224
+ }
225
+ }
226
+ Signal._batchQueue.clear();
227
+ for (const sub of subs) {
228
+ try { sub(); }
229
+ catch (err) {
230
+ reportError(ErrorCode.SIGNAL_CALLBACK, 'Signal subscriber threw', {}, err);
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // untracked() - read signals without creating dependencies
239
+ // ---------------------------------------------------------------------------
240
+
241
+ /**
242
+ * Execute a function without tracking signal reads as dependencies.
243
+ * @param {Function} fn - function to run
244
+ * @returns {*} the return value of fn
245
+ */
246
+ export function untracked(fn) {
247
+ const prev = Signal._activeEffect;
248
+ Signal._activeEffect = null;
249
+ try {
250
+ return fn();
251
+ } finally {
252
+ Signal._activeEffect = prev;
253
+ }
254
+ }
package/src/router.js CHANGED
@@ -1,14 +1,13 @@
1
1
  /**
2
- * zQuery Router Client-side SPA router
2
+ * zQuery Router - Client-side SPA router
3
3
  *
4
4
  * Supports hash mode (#/path) and history mode (/path).
5
5
  * Route params, query strings, navigation guards, and lazy loading.
6
6
  * Sub-route history substates for in-page UI changes (modals, tabs, etc.).
7
7
  *
8
8
  * Usage:
9
+ * // HTML: <z-outlet></z-outlet>
9
10
  * $.router({
10
- * el: '#app',
11
- * mode: 'hash',
12
11
  * routes: [
13
12
  * { path: '/', component: 'home-page' },
14
13
  * { path: '/user/:id', component: 'user-profile' },
@@ -44,7 +43,7 @@ function _shallowEqual(a, b) {
44
43
  class Router {
45
44
  constructor(config = {}) {
46
45
  this._el = null;
47
- // file:// protocol can't use pushState always force hash mode
46
+ // file:// protocol can't use pushState - always force hash mode
48
47
  const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
49
48
  this._mode = isFile ? 'hash' : (config.mode || 'history');
50
49
 
@@ -79,8 +78,30 @@ class Router {
79
78
  this._inSubstate = false; // true while substate entries are in the history stack
80
79
 
81
80
  // Set outlet element
81
+ // Priority: explicit config.el → <z-outlet> tag in the DOM
82
82
  if (config.el) {
83
83
  this._el = typeof config.el === 'string' ? document.querySelector(config.el) : config.el;
84
+ } else if (typeof document !== 'undefined') {
85
+ const outlet = document.querySelector('z-outlet');
86
+ if (outlet) {
87
+ this._el = outlet;
88
+ // Read inline attribute overrides from <z-outlet> (config takes priority)
89
+ if (!config.fallback && outlet.getAttribute('fallback')) {
90
+ this._fallback = outlet.getAttribute('fallback');
91
+ }
92
+ if (!config.mode && outlet.getAttribute('mode')) {
93
+ const attrMode = outlet.getAttribute('mode');
94
+ if (attrMode === 'hash' || attrMode === 'history') {
95
+ this._mode = isFile ? 'hash' : attrMode;
96
+ }
97
+ }
98
+ if (config.base == null && outlet.getAttribute('base')) {
99
+ let ob = outlet.getAttribute('base');
100
+ ob = String(ob).replace(/\/+$/, '');
101
+ if (ob && !ob.startsWith('/')) ob = '/' + ob;
102
+ this._base = ob;
103
+ }
104
+ }
84
105
  }
85
106
 
86
107
  // Register routes
@@ -88,21 +109,43 @@ class Router {
88
109
  config.routes.forEach(r => this.add(r));
89
110
  }
90
111
 
91
- // Listen for navigation store handler references for cleanup in destroy()
112
+ // Listen for navigation - store handler references for cleanup in destroy()
92
113
  if (this._mode === 'hash') {
93
114
  this._onNavEvent = () => this._resolve();
94
115
  window.addEventListener('hashchange', this._onNavEvent);
116
+ // Hash mode also needs popstate for substates (pushSubstate uses pushState)
117
+ this._onPopState = (e) => {
118
+ const st = e.state;
119
+ if (st && st[_ZQ_STATE_KEY] === 'substate') {
120
+ const handled = this._fireSubstate(st.key, st.data, 'pop');
121
+ if (handled) return;
122
+ this._resolve().then(() => {
123
+ this._fireSubstate(st.key, st.data, 'pop');
124
+ });
125
+ return;
126
+ } else if (this._inSubstate) {
127
+ this._inSubstate = false;
128
+ this._fireSubstate(null, null, 'reset');
129
+ }
130
+ };
131
+ window.addEventListener('popstate', this._onPopState);
95
132
  } else {
96
133
  this._onNavEvent = (e) => {
97
- // Check for substate pop first if a listener handles it, don't route
134
+ // Check for substate pop first - if a listener handles it, don't route
98
135
  const st = e.state;
99
136
  if (st && st[_ZQ_STATE_KEY] === 'substate') {
100
137
  const handled = this._fireSubstate(st.key, st.data, 'pop');
101
138
  if (handled) return;
102
- // Unhandled substate — fall through to route resolve
103
- // _inSubstate stays true so the next non-substate pop triggers reset
139
+ // Unhandled substate — the owning component was likely destroyed
140
+ // (e.g. user navigated away then pressed back). Resolve the route
141
+ // first (which may mount a fresh component that registers a listener),
142
+ // then retry the substate so the new listener can restore the UI.
143
+ this._resolve().then(() => {
144
+ this._fireSubstate(st.key, st.data, 'pop');
145
+ });
146
+ return;
104
147
  } else if (this._inSubstate) {
105
- // Popped past all substates notify listeners to reset to defaults
148
+ // Popped past all substates - notify listeners to reset to defaults
106
149
  this._inSubstate = false;
107
150
  this._fireSubstate(null, null, 'reset');
108
151
  }
@@ -120,13 +163,17 @@ class Router {
120
163
  if (link.getAttribute('target') === '_blank') return;
121
164
  e.preventDefault();
122
165
  let href = link.getAttribute('z-link');
166
+ // Reject absolute URLs and dangerous protocols — z-link is for internal routes only
167
+ if (href && /^[a-z][a-z0-9+.-]*:/i.test(href)) return;
123
168
  // Support z-link-params for dynamic :param interpolation
124
169
  const paramsAttr = link.getAttribute('z-link-params');
125
170
  if (paramsAttr) {
126
171
  try {
127
172
  const params = JSON.parse(paramsAttr);
128
173
  href = this._interpolateParams(href, params);
129
- } catch { /* ignore malformed JSON */ }
174
+ } catch (err) {
175
+ reportError(ErrorCode.ROUTER_RESOLVE, 'Malformed JSON in z-link-params', { href, paramsAttr }, err);
176
+ }
130
177
  }
131
178
  this.navigate(href);
132
179
  // z-to-top modifier: scroll to top after navigation
@@ -180,8 +227,8 @@ class Router {
180
227
 
181
228
  /**
182
229
  * Interpolate :param placeholders in a path with the given values.
183
- * @param {string} path e.g. '/user/:id/posts/:pid'
184
- * @param {Object} params e.g. { id: 42, pid: 7 }
230
+ * @param {string} path - e.g. '/user/:id/posts/:pid'
231
+ * @param {Object} params - e.g. { id: 42, pid: 7 }
185
232
  * @returns {string}
186
233
  */
187
234
  _interpolateParams(path, params) {
@@ -225,7 +272,7 @@ class Router {
225
272
  const currentURL = (window.location.pathname || '/') + (window.location.hash || '');
226
273
 
227
274
  if (targetURL === currentURL && !options.force) {
228
- // Same full URL (path + hash) don't push duplicate entry.
275
+ // Same full URL (path + hash) - don't push duplicate entry.
229
276
  // If only the hash changed to a fragment target, scroll to it.
230
277
  if (fragment) {
231
278
  const el = document.getElementById(fragment);
@@ -234,7 +281,7 @@ class Router {
234
281
  return this;
235
282
  }
236
283
 
237
- // Same route path but different hash fragment use replaceState
284
+ // Same route path but different hash fragment - use replaceState
238
285
  // so back goes to the previous *route*, not the previous scroll position.
239
286
  const targetPathOnly = this._base + normalized;
240
287
  const currentPathOnly = window.location.pathname || '/';
@@ -249,7 +296,7 @@ class Router {
249
296
  const el = document.getElementById(fragment);
250
297
  if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
251
298
  }
252
- // Don't re-resolve same route, just a hash change
299
+ // Don't re-resolve - same route, just a hash change
253
300
  return this;
254
301
  }
255
302
 
@@ -285,8 +332,8 @@ class Router {
285
332
 
286
333
  /**
287
334
  * Normalize an app-relative path and guard against double base-prefixing.
288
- * @param {string} path e.g. '/docs', 'docs', or '/app/docs' when base is '/app'
289
- * @returns {string} always starts with '/'
335
+ * @param {string} path - e.g. '/docs', 'docs', or '/app/docs' when base is '/app'
336
+ * @returns {string} - always starts with '/'
290
337
  */
291
338
  _normalizePath(path) {
292
339
  let p = path && path.startsWith('/') ? path : (path ? `/${path}` : '/');
@@ -336,12 +383,12 @@ class Router {
336
383
 
337
384
  /**
338
385
  * Push a lightweight history entry for in-component UI state.
339
- * The URL path does NOT change only a history entry is added so the
386
+ * The URL path does NOT change - only a history entry is added so the
340
387
  * back button can undo the UI change (close modal, revert tab, etc.)
341
388
  * before navigating away.
342
389
  *
343
- * @param {string} key identifier (e.g. 'modal', 'tab', 'panel')
344
- * @param {*} data arbitrary state (serializable)
390
+ * @param {string} key - identifier (e.g. 'modal', 'tab', 'panel')
391
+ * @param {*} data - arbitrary state (serializable)
345
392
  * @returns {Router}
346
393
  *
347
394
  * @example
@@ -352,7 +399,7 @@ class Router {
352
399
  pushSubstate(key, data) {
353
400
  this._inSubstate = true;
354
401
  if (this._mode === 'hash') {
355
- // Hash mode: stash the substate in a global hashchange will check.
402
+ // Hash mode: stash the substate in a global - hashchange will check.
356
403
  // We still push a history entry via a sentinel hash suffix.
357
404
  const current = window.location.hash || '#/';
358
405
  window.history.pushState(
@@ -468,12 +515,12 @@ class Router {
468
515
  async __resolve() {
469
516
  // Check if we're landing on a substate entry (e.g. page refresh on a
470
517
  // substate bookmark, or hash-mode popstate). Fire listeners and bail
471
- // if handled the URL hasn't changed so there's no route to resolve.
518
+ // if handled - the URL hasn't changed so there's no route to resolve.
472
519
  const histState = window.history.state;
473
520
  if (histState && histState[_ZQ_STATE_KEY] === 'substate') {
474
521
  const handled = this._fireSubstate(histState.key, histState.data, 'resolve');
475
522
  if (handled) return;
476
- // No listener handled it fall through to normal routing
523
+ // No listener handled it - fall through to normal routing
477
524
  }
478
525
 
479
526
  const fullPath = this.path;
@@ -510,7 +557,7 @@ class Router {
510
557
  const sameParams = _shallowEqual(params, from.params);
511
558
  const sameQuery = _shallowEqual(query, from.query);
512
559
  if (sameParams && sameQuery) {
513
- // Identical navigation nothing to do
560
+ // Identical navigation - nothing to do
514
561
  return;
515
562
  }
516
563
  }
@@ -598,6 +645,9 @@ class Router {
598
645
  }
599
646
  }
600
647
 
648
+ // Update z-active-route elements
649
+ this._updateActiveRoutes(path);
650
+
601
651
  // Run after guards
602
652
  for (const guard of this._guards.after) {
603
653
  await guard(to, from);
@@ -607,6 +657,32 @@ class Router {
607
657
  this._listeners.forEach(fn => fn(to, from));
608
658
  }
609
659
 
660
+ // --- Active route class management ----------------------------------------
661
+
662
+ /**
663
+ * Update all elements with z-active-route to toggle their active class
664
+ * based on the current path.
665
+ *
666
+ * Usage:
667
+ * <a z-link="/docs" z-active-route="/docs">Docs</a>
668
+ * <a z-link="/about" z-active-route="/about" z-active-class="selected">About</a>
669
+ * <a z-link="/" z-active-route="/" z-active-exact>Home</a>
670
+ */
671
+ _updateActiveRoutes(currentPath) {
672
+ if (typeof document === 'undefined') return;
673
+ const els = document.querySelectorAll('[z-active-route]');
674
+ for (let i = 0; i < els.length; i++) {
675
+ const el = els[i];
676
+ const route = el.getAttribute('z-active-route');
677
+ const cls = el.getAttribute('z-active-class') || 'active';
678
+ const exact = el.hasAttribute('z-active-exact');
679
+ const isActive = exact
680
+ ? currentPath === route
681
+ : (route === '/' ? currentPath === '/' : currentPath.startsWith(route));
682
+ el.classList.toggle(cls, isActive);
683
+ }
684
+ }
685
+
610
686
  // --- Destroy -------------------------------------------------------------
611
687
 
612
688
  destroy() {
@@ -615,6 +691,10 @@ class Router {
615
691
  window.removeEventListener(this._mode === 'hash' ? 'hashchange' : 'popstate', this._onNavEvent);
616
692
  this._onNavEvent = null;
617
693
  }
694
+ if (this._onPopState) {
695
+ window.removeEventListener('popstate', this._onPopState);
696
+ this._onPopState = null;
697
+ }
618
698
  if (this._onLinkClick) {
619
699
  document.removeEventListener('click', this._onLinkClick);
620
700
  this._onLinkClick = null;
package/src/ssr.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * zQuery SSR Server-side rendering to HTML string
2
+ * zQuery SSR - Server-side rendering to HTML string
3
3
  *
4
4
  * Renders registered components to static HTML strings for SEO,
5
5
  * initial page load performance, and static site generation.
6
6
  *
7
- * Works in Node.js no DOM required for basic rendering.
7
+ * Works in Node.js - no DOM required for basic rendering.
8
8
  * Supports hydration markers for client-side takeover.
9
9
  *
10
10
  * Usage (Node.js):
@@ -67,7 +67,7 @@ class SSRComponent {
67
67
  }
68
68
  }
69
69
 
70
- // Init lifecycle guarded so a broken init doesn't crash the whole render
70
+ // Init lifecycle - guarded so a broken init doesn't crash the whole render
71
71
  if (definition.init) {
72
72
  try {
73
73
  definition.init.call(this);
@@ -85,7 +85,7 @@ class SSRComponent {
85
85
  return html;
86
86
  } catch (err) {
87
87
  reportError(ErrorCode.SSR_RENDER, 'Component render() threw during SSR', {}, err);
88
- return `<!-- SSR render error: ${_escapeHtml(err.message)} -->`;
88
+ return `<!-- SSR render error -->`;
89
89
  }
90
90
  }
91
91
  return '';
@@ -123,7 +123,7 @@ function _escapeHtml(str) {
123
123
  }
124
124
 
125
125
  // ---------------------------------------------------------------------------
126
- // SSR App component registry + renderer
126
+ // SSR App - component registry + renderer
127
127
  // ---------------------------------------------------------------------------
128
128
  class SSRApp {
129
129
  constructor() {
@@ -158,12 +158,12 @@ class SSRApp {
158
158
  /**
159
159
  * Render a component to an HTML string.
160
160
  *
161
- * @param {string} componentName registered component name
162
- * @param {object} [props] props to pass
163
- * @param {object} [options] rendering options
164
- * @param {boolean} [options.hydrate=true] add hydration marker
165
- * @param {string} [options.mode='html'] 'html' (default) or 'fragment' (no wrapper tag)
166
- * @returns {Promise<string>} rendered HTML
161
+ * @param {string} componentName - registered component name
162
+ * @param {object} [props] - props to pass
163
+ * @param {object} [options] - rendering options
164
+ * @param {boolean} [options.hydrate=true] - add hydration marker
165
+ * @param {string} [options.mode='html'] - 'html' (default) or 'fragment' (no wrapper tag)
166
+ * @returns {Promise<string>} - rendered HTML
167
167
  */
168
168
  async renderToString(componentName, props = {}, options = {}) {
169
169
  const def = this._registry.get(componentName);
@@ -185,7 +185,7 @@ class SSRApp {
185
185
  html = html.replace(/\s*@[\w.]+="[^"]*"/g, ''); // Remove event bindings
186
186
  html = html.replace(/\s*z-on:[\w.]+="[^"]*"/g, '');
187
187
 
188
- // Fragment mode return inner HTML without wrapper tag
188
+ // Fragment mode - return inner HTML without wrapper tag
189
189
  if (options.mode === 'fragment') return html;
190
190
 
191
191
  const hydrate = options.hydrate !== false;
@@ -198,7 +198,7 @@ class SSRApp {
198
198
  * Render multiple components as a batch.
199
199
  *
200
200
  * @param {Array<{ name: string, props?: object, options?: object }>} entries
201
- * @returns {Promise<string[]>} array of rendered HTML strings
201
+ * @returns {Promise<string[]>} - array of rendered HTML strings
202
202
  */
203
203
  async renderBatch(entries) {
204
204
  return Promise.all(
@@ -210,18 +210,18 @@ class SSRApp {
210
210
  * Render a full HTML page with a component mounted in a shell.
211
211
  *
212
212
  * @param {object} options
213
- * @param {string} options.component component name to render
214
- * @param {object} [options.props] props
215
- * @param {string} [options.title] page title
216
- * @param {string} [options.description] meta description for SEO
217
- * @param {string[]} [options.styles] CSS file paths
218
- * @param {string[]} [options.scripts] JS file paths
219
- * @param {string} [options.lang] html lang attribute
220
- * @param {string} [options.meta] additional head content
221
- * @param {string} [options.bodyAttrs] extra body attributes
222
- * @param {object} [options.head] structured head options
223
- * @param {string} [options.head.canonical] canonical URL
224
- * @param {object} [options.head.og] Open Graph tags
213
+ * @param {string} options.component - component name to render
214
+ * @param {object} [options.props] - props
215
+ * @param {string} [options.title] - page title
216
+ * @param {string} [options.description] - meta description for SEO
217
+ * @param {string[]} [options.styles] - CSS file paths
218
+ * @param {string[]} [options.scripts] - JS file paths
219
+ * @param {string} [options.lang] - html lang attribute
220
+ * @param {string} [options.meta] - additional head content
221
+ * @param {string} [options.bodyAttrs] - extra body attributes
222
+ * @param {object} [options.head] - structured head options
223
+ * @param {string} [options.head.canonical] - canonical URL
224
+ * @param {object} [options.head.og] - Open Graph tags
225
225
  * @returns {Promise<string>}
226
226
  */
227
227
  async renderPage(options = {}) {
@@ -296,8 +296,8 @@ export function createSSRApp() {
296
296
 
297
297
  /**
298
298
  * Quick one-shot render of a component definition to string.
299
- * @param {object} definition component definition
300
- * @param {object} [props] props
299
+ * @param {object} definition - component definition
300
+ * @param {object} [props] - props
301
301
  * @returns {string}
302
302
  */
303
303
  export function renderToString(definition, props = {}) {
@@ -309,7 +309,7 @@ export function renderToString(definition, props = {}) {
309
309
  }
310
310
 
311
311
  /**
312
- * Escape HTML entities exposed for use in SSR templates.
312
+ * Escape HTML entities - exposed for use in SSR templates.
313
313
  * @param {string} str
314
314
  * @returns {string}
315
315
  */