@vmz/core 0.1.32 → 0.2.1

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 (66) hide show
  1. package/dist/{client-nav.d.ts → browser/client-nav.d.ts} +4 -13
  2. package/dist/{client-nav.js → browser/client-nav.js} +16 -74
  3. package/dist/browser/direct-api.types.d.ts +141 -0
  4. package/dist/browser/direct-api.types.js +4 -0
  5. package/dist/browser/direct-host-box.d.ts +11 -0
  6. package/dist/{direct-host-box.js → browser/direct-host-box.js} +2 -18
  7. package/dist/{dom-core.d.ts → browser/dom-core.d.ts} +67 -220
  8. package/dist/browser/dom-core.js +2484 -0
  9. package/dist/browser/dom-core.types.d.ts +127 -0
  10. package/dist/browser/dom-core.types.js +4 -0
  11. package/dist/{position-context.js → browser/position-context.js} +1 -1
  12. package/dist/browser/unknown-component.d.ts +14 -0
  13. package/dist/{unknown-component.js → browser/unknown-component.js} +5 -30
  14. package/dist/{dom.browser.d.ts → faces/dom.browser.d.ts} +2 -2
  15. package/dist/{dom.browser.js → faces/dom.browser.js} +2 -3
  16. package/dist/faces/dom.client.d.ts +4 -0
  17. package/dist/faces/dom.client.js +4 -0
  18. package/dist/faces/dom.d.ts +6 -0
  19. package/dist/faces/dom.js +6 -0
  20. package/dist/faces/http.d.ts +6 -0
  21. package/dist/{http.js → faces/http.js} +0 -1
  22. package/dist/faces/server.d.ts +30 -0
  23. package/dist/{server.js → faces/server.js} +27 -149
  24. package/dist/{vmz-dom.d.ts → faces/vmz-dom.d.ts} +1 -1
  25. package/dist/{vmz-dom.js → faces/vmz-dom.js} +1 -2
  26. package/dist/{vmz-runtime.d.ts → faces/vmz-runtime.d.ts} +1 -1
  27. package/dist/{vmz-runtime.js → faces/vmz-runtime.js} +1 -2
  28. package/dist/host/deployment-registry.d.ts +16 -0
  29. package/dist/{deployment-registry.js → host/deployment-registry.js} +1 -68
  30. package/dist/host/list-client-components.d.ts +14 -0
  31. package/dist/{list-client-components.js → host/list-client-components.js} +0 -37
  32. package/dist/{localize-body-links.js → host/localize-body-links.js} +0 -1
  33. package/dist/host/native-addon.d.ts +6 -0
  34. package/dist/{native-addon.js → host/native-addon.js} +0 -20
  35. package/dist/host/render-host.d.ts +20 -0
  36. package/dist/{render-host.js → host/render-host.js} +1 -17
  37. package/dist/{serve-host.d.ts → host/serve-host.d.ts} +3 -3
  38. package/dist/{serve-host.mjs → host/serve-host.mjs} +45 -168
  39. package/dist/shared/dom-attr-normalize.d.ts +8 -0
  40. package/dist/shared/dom-attr-normalize.js +71 -0
  41. package/dist/shared/host.types.d.ts +113 -0
  42. package/dist/shared/host.types.js +4 -0
  43. package/dist/shared/server.types.d.ts +67 -0
  44. package/dist/shared/server.types.js +4 -0
  45. package/dist/{dom-ssr.d.ts → ssr/dom-ssr.d.ts} +8 -37
  46. package/dist/{dom-ssr.js → ssr/dom-ssr.js} +189 -217
  47. package/package.json +34 -34
  48. package/dist/deployment-registry.d.ts +0 -70
  49. package/dist/direct-host-box.d.ts +0 -26
  50. package/dist/dom-core.js +0 -4815
  51. package/dist/dom.client.d.ts +0 -4
  52. package/dist/dom.client.js +0 -5
  53. package/dist/dom.d.ts +0 -6
  54. package/dist/dom.js +0 -7
  55. package/dist/http.d.ts +0 -6
  56. package/dist/list-client-components.d.ts +0 -62
  57. package/dist/native-addon.d.ts +0 -12
  58. package/dist/render-host.d.ts +0 -32
  59. package/dist/server.d.ts +0 -46
  60. package/dist/unknown-component.d.ts +0 -40
  61. /package/dist/{position-context.d.ts → browser/position-context.d.ts} +0 -0
  62. /package/dist/{index.d.ts → faces/index.d.ts} +0 -0
  63. /package/dist/{index.js → faces/index.js} +0 -0
  64. /package/dist/{localize-body-links.d.ts → host/localize-body-links.d.ts} +0 -0
  65. /package/dist/{route-layout-chain.d.ts → host/route-layout-chain.d.ts} +0 -0
  66. /package/dist/{route-layout-chain.js → host/route-layout-chain.js} +0 -0
@@ -1,12 +1,10 @@
1
- // @ts-nocheck
2
1
  /**
3
- * VMZ DOM SSR / hydrate / resume — precise patches, no VDOM diff.
2
+ * VMZ DOM SSR / hydrate / resume ??precise patches, no VDOM diff.
4
3
  * Imports client DOM primitives from ./dom-core.js for tree-shakeable browser entry.
5
4
  */
6
- import { applyDirectHostBox, directHostBoxStyleAttr } from './direct-host-box.js';
7
- import { applyPreservedState, BOOLEAN_HTML_ATTRS, createInstance, destroy, directApi, eventPropHandlerName, findOwnedDefaultSlot, getRegisteredComponent, hasMeaningfulChild, isEventEntryStrategy, mount, noteDomCreate, resolveComponent, runDirectCreate, scheduleClientOn, settlePendingChildMounts, snapshotInstanceState, stripFns, } from './dom-core.js';
8
- import { markUnknownComponentHost, serializeUnknownComponentNode } from './unknown-component.js';
9
- /** @type {Error | null} last linkedom resolve failure (for clear SSR errors) */
5
+ import { applyDirectHostBox, directHostBoxStyleAttr } from '../browser/direct-host-box.js';
6
+ import { applyPreservedState, BOOLEAN_HTML_ATTRS, createInstance, destroy, directApi, eventPropHandlerName, findOwnedDefaultSlot, getRegisteredComponent, hasMeaningfulChild, isEventEntryStrategy, mergeClassParts, mergeStyleParts, mount, noteDomCreate, resolveComponent, runDirectCreate, scheduleClientOn, settlePendingChildMounts, snapshotInstanceState, stripFns, } from '../browser/dom-core.js';
7
+ import { markUnknownComponentHost, serializeUnknownComponentNode } from '../browser/unknown-component.js';
10
8
  let _ssrDocumentLastError = null;
11
9
  /**
12
10
  * Node SSR has no browser `document`. rowKernel prefers document-free HTML fill;
@@ -16,7 +14,7 @@ let _ssrDocumentLastError = null;
16
14
  * and browser hosts load that barrel.
17
15
  *
18
16
  * When this file is copied into an app `dist/`, `createRequire(import.meta.url)` cannot
19
- * see `@vmz/core`'s dependency tree — resolve linkedom from cwd / `@vmz/core` as well.
17
+ * see `@vmz/core`'s dependency tree ??resolve linkedom from cwd / `@vmz/core` as well.
20
18
  */
21
19
  function ensureSsrDocument() {
22
20
  if (typeof globalThis.document !== 'undefined' && typeof globalThis.document.createElement === 'function') {
@@ -36,13 +34,11 @@ function ensureSsrDocument() {
36
34
  }
37
35
  const pathMod = typeof proc.getBuiltinModule === 'function' ? proc.getBuiltinModule('path') : null;
38
36
  const createRequire = mod.createRequire;
39
- /** @type {string[]} */
40
37
  const bases = [];
41
38
  bases.push(import.meta.url);
42
39
  if (pathMod && typeof proc.cwd === 'function') {
43
40
  bases.push(pathMod.join(proc.cwd(), 'package.json'));
44
41
  }
45
- /** @type {string[]} */
46
42
  const errors = [];
47
43
  let parseHTML = null;
48
44
  for (const base of bases) {
@@ -59,7 +55,7 @@ function ensureSsrDocument() {
59
55
  break;
60
56
  }
61
57
  catch (e) {
62
- errors.push(`${base} → linkedom: ${e && e.message ? e.message : e}`);
58
+ errors.push(`${base} ??linkedom: ${e && e.message ? e.message : e}`);
63
59
  }
64
60
  // Walk into @vmz/core's dependency tree (linkedom is declared there).
65
61
  for (const coreId of ['@vmz/core', '@vmz/core/dom', '@vmz/core/server']) {
@@ -69,7 +65,7 @@ function ensureSsrDocument() {
69
65
  break;
70
66
  }
71
67
  catch (e) {
72
- errors.push(`${base} → ${coreId}/linkedom: ${e && e.message ? e.message : e}`);
68
+ errors.push(`${base} ??${coreId}/linkedom: ${e && e.message ? e.message : e}`);
73
69
  }
74
70
  }
75
71
  if (parseHTML)
@@ -101,7 +97,7 @@ export async function renderToString(Component, props = {}, opts = {}) {
101
97
  }
102
98
  if (signal && signal.aborted)
103
99
  return '';
104
- // production Direct emit: SSR only via Direct serialize schedule — never `render`.
100
+ // production Direct emit: SSR only via Direct serialize schedule ??never `render`.
105
101
  if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
106
102
  throw new Error(`vmz:dom renderToString() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
107
103
  }
@@ -112,12 +108,8 @@ export async function renderToString(Component, props = {}, opts = {}) {
112
108
  }
113
109
  /**
114
110
  * Stream SSR via the same Direct serialize schedule as `renderToString`.
115
- * Yields HTML chunks (open tag → children → close). Joining chunks equals `renderToString`.
111
+ * Yields HTML chunks (open tag ??children ??close). Joining chunks equals `renderToString`.
116
112
  * Supports AbortSignal for cancel; consumers should respect backpressure (await between chunks).
117
- * @param {new (props?: object) => any} Component
118
- * @param {object} [props]
119
- * @param {{ signal?: AbortSignal, slotHtml?: string }} [opts]
120
- * @returns {AsyncGenerator<string, void, void>}
121
113
  */
122
114
  export async function* renderToStream(Component, props = {}, opts = {}) {
123
115
  ensureSsrDocument();
@@ -155,11 +147,9 @@ export async function* renderToStream(Component, props = {}, opts = {}) {
155
147
  }
156
148
  /**
157
149
  * Fill the layout-owned default `<slot>` with pre-rendered HTML (layout SSR wrap).
158
- * Skips nested component hosts (`data-vmz`) — their slots are for child projection
159
- * (e.g. Button label), not the page outlet. Without this, DFS hits LocaleToggle→Button
150
+ * Skips nested component hosts (`data-vmz`) ??their slots are for child projection
151
+ * (e.g. Button label), not the page outlet. Without this, DFS hits LocaleToggle?Button
160
152
  * before Layout's `<main><slot>`, and the entire page HTML lands inside a button.
161
- * @param {any} node
162
- * @param {string} html
163
153
  */
164
154
  function injectDefaultSlotHtml(node, html) {
165
155
  if (!node || typeof node !== 'object')
@@ -169,7 +159,7 @@ function injectDefaultSlotHtml(node, html) {
169
159
  node.children = [];
170
160
  return true;
171
161
  }
172
- // Nested Direct component wrapper from serializeApi.component — do not search inside.
162
+ // Nested Direct component wrapper from serializeApi.component ??do not search inside.
173
163
  if (node.__kind === 'el' && node.attrs && node.attrs['data-vmz'] != null) {
174
164
  return false;
175
165
  }
@@ -186,20 +176,13 @@ function injectDefaultSlotHtml(node, html) {
186
176
  * Live-DOM counterpart: first default `<slot>` owned by this tree, not by a nested
187
177
  * `[data-vmz]` component (Button/Link labels, etc.).
188
178
  * Re-exported from dom-core (single implementation).
189
- * @param {Element | null | undefined} root
190
- * @returns {Element | null}
191
179
  */
192
180
  export { findOwnedDefaultSlot };
193
181
  /**
194
- * Hydrate/mount a file-route page inside an optional layout chain (outer → inner).
182
+ * Hydrate/mount a file-route page inside an optional layout chain (outer ??inner).
195
183
  * Mirrors SSR `slotHtml` wrapping: each layout's owned default slot becomes the
196
184
  * outlet for the next layout or the page. Retains layout instances on `container`
197
185
  * so SPA transitions can dispose only the page host.
198
- * @param {new (props?: object) => any} Page
199
- * @param {Element} container
200
- * @param {object} [props]
201
- * @param {Array<new (props?: object) => any>} [layoutCtors] outer → inner
202
- * @param {{ preserveState?: boolean | Record<string, unknown>, skipOnMount?: boolean }} [opts]
203
186
  */
204
187
  export async function hydrateRoute(Page, container, props = {}, layoutCtors = [], opts = {}) {
205
188
  if (typeof document === 'undefined') {
@@ -211,7 +194,6 @@ export async function hydrateRoute(Page, container, props = {}, layoutCtors = []
211
194
  }
212
195
  container.__vmzPageHost = null;
213
196
  container.__vmzLayoutInsts = null;
214
- /** @type {object[]} */
215
197
  const layoutInsts = [];
216
198
  let host = container;
217
199
  const ctors = Array.isArray(layoutCtors) ? layoutCtors.filter(Boolean) : [];
@@ -241,10 +223,6 @@ export async function hydrateRoute(Page, container, props = {}, layoutCtors = []
241
223
  /**
242
224
  * SPA same-layout transition: dispose only the page host, keep layout instances.
243
225
  * Callers must verify `data-vmz-layout` is unchanged before using this.
244
- * @param {new (props?: object) => any} Page
245
- * @param {Element} container `#app` that already has `__vmzPageHost` / `__vmzLayoutInsts`
246
- * @param {object} [props]
247
- * @param {{ preserveState?: boolean | Record<string, unknown>, skipOnMount?: boolean }} [opts]
248
226
  */
249
227
  export async function hydrateRoutePage(Page, container, props = {}, opts = {}) {
250
228
  if (typeof document === 'undefined') {
@@ -265,8 +243,6 @@ export async function hydrateRoutePage(Page, container, props = {}, opts = {}) {
265
243
  }
266
244
  /**
267
245
  * SSR: run the same __vmzCreate schedule against a serialize host (no render).
268
- * @param {new (props?: object) => any} Component
269
- * @param {object} inst
270
246
  */
271
247
  function runDirectSerializeTree(Component, inst) {
272
248
  serializeApi._inst = inst;
@@ -279,14 +255,10 @@ function runDirectSerializeTree(Component, inst) {
279
255
  }
280
256
  /**
281
257
  * SSR child onMount: sync `__vmzCreate` cannot await nested mounts.
282
- * Expand rounds — reuse prior child instances, await newly discovered onMounts, re-emit.
283
- * @param {new (props?: object) => any} Component
284
- * @param {object} inst
258
+ * Expand rounds ??reuse prior child instances, await newly discovered onMounts, re-emit.
285
259
  */
286
260
  async function runDirectSerializeTreeWithMounts(Component, inst) {
287
- /** @type {object[]} */
288
261
  let preMounted = [];
289
- /** @type {any} */
290
262
  let tree = null;
291
263
  for (let round = 0; round < 32; round++) {
292
264
  serializeApi._ssrPreMounted = preMounted;
@@ -330,6 +302,8 @@ function flattenSerializeNode(node) {
330
302
  return escapeHtml(node);
331
303
  if (node.__kind === 'text')
332
304
  return escapeHtml(node.value);
305
+ if (node.__kind === 'comment')
306
+ return '';
333
307
  if (node.__kind === 'frag') {
334
308
  if (node.__rawHtml != null)
335
309
  return String(node.__rawHtml);
@@ -356,8 +330,6 @@ function flattenSerializeNode(node) {
356
330
  }
357
331
  /**
358
332
  * Progressive HTML chunks from a serialize tree (same nodes as flattenSerializeNode).
359
- * @param {any} node
360
- * @returns {Generator<string, void, void>}
361
333
  */
362
334
  function* streamSerializeChunks(node) {
363
335
  if (node == null || node === false)
@@ -370,6 +342,8 @@ function* streamSerializeChunks(node) {
370
342
  yield escapeHtml(node.value);
371
343
  return;
372
344
  }
345
+ if (node.__kind === 'comment')
346
+ return;
373
347
  if (node.__kind === 'frag') {
374
348
  if (node.__rawHtml != null) {
375
349
  yield String(node.__rawHtml);
@@ -409,19 +383,14 @@ function* streamSerializeChunks(node) {
409
383
  /**
410
384
  * Document-free rowKernel SSR fill (transitional / pre-0.1.7 emit only).
411
385
  * Prefer `serializeItem` (IR schedule). Do not treat this as the long-term contract.
412
- * @param {{ html: string, textSlots?: Record<string, number>, hostFields?: string[] }} rk
413
- * @param {any} item
414
- * @param {any} key
415
- * @returns {string | null} filled outer HTML, or null if shape is not fillable
416
386
  */
417
387
  function fillRowKernelHtml(rk, item, key) {
418
388
  const slots = rk.textSlots;
419
389
  if (!slots || typeof slots !== 'object')
420
390
  return null;
421
- /** @type {string[]} */
422
391
  const fields = Object.entries(slots)
423
392
  .filter(([, i]) => typeof i === 'number' && Number.isFinite(i))
424
- .sort((a, b) => /** @type {number} */ (a[1]) - /** @type {number} */ (b[1]))
393
+ .sort((a, b) => Number(a[1]) - Number(b[1]))
425
394
  .map(([f]) => f);
426
395
  if (!fields.length)
427
396
  return null;
@@ -446,10 +415,6 @@ function fillRowKernelHtml(rk, item, key) {
446
415
  * SSR row when `createItem` was omitted (rowKernel client emit).
447
416
  * Prefer document-free fill from `html` + `textSlots`; fall back to linkedom + hydrate
448
417
  * when host class ternaries need live DOM, or when placeholders cannot be string-filled.
449
- * @param {object} inst
450
- * @param {{ html: string, hydrate?: Function, textSlots?: Record<string, number>, hostFields?: string[] }} rk
451
- * @param {{ item: any, index: number }} box
452
- * @param {any} key
453
418
  */
454
419
  function serializeRowFromKernel(inst, rk, box, key) {
455
420
  const item = box.item;
@@ -508,24 +473,71 @@ function serializeRowFromKernel(inst, rk, box, key) {
508
473
  appendChild() { },
509
474
  };
510
475
  }
511
- /** Serialize host mirroring directApi — returns virtual nodes, not DOM. */
476
+ function virtualLink(parent, child) {
477
+ if (child != null && typeof child === 'object')
478
+ child.parentNode = parent;
479
+ }
480
+ function virtualAppend(parent, child) {
481
+ if (parent == null || child == null)
482
+ return;
483
+ if (!Array.isArray(parent.children))
484
+ parent.children = [];
485
+ parent.children.push(child);
486
+ virtualLink(parent, child);
487
+ }
488
+ function virtualInsertBefore(parent, node, ref) {
489
+ if (parent == null || node == null)
490
+ return;
491
+ if (!Array.isArray(parent.children))
492
+ parent.children = [];
493
+ if (ref == null) {
494
+ virtualAppend(parent, node);
495
+ return;
496
+ }
497
+ const idx = parent.children.indexOf(ref);
498
+ if (idx < 0)
499
+ virtualAppend(parent, node);
500
+ else {
501
+ parent.children.splice(idx, 0, node);
502
+ virtualLink(parent, node);
503
+ }
504
+ }
505
+ function virtualRemove(node) {
506
+ const p = node && node.parentNode;
507
+ if (!p || !Array.isArray(p.children))
508
+ return;
509
+ const idx = p.children.indexOf(node);
510
+ if (idx >= 0)
511
+ p.children.splice(idx, 1);
512
+ node.parentNode = null;
513
+ }
514
+ function makeVirtualEl(tag) {
515
+ return {
516
+ __kind: 'el',
517
+ tag: tag || 'div',
518
+ attrs: {},
519
+ children: [],
520
+ style: {},
521
+ parentNode: null,
522
+ setAttribute(name, value) {
523
+ applySerializeAttr(this, name, value);
524
+ },
525
+ appendChild(c) {
526
+ virtualAppend(this, c);
527
+ },
528
+ insertBefore(node, ref) {
529
+ virtualInsertBefore(this, node, ref);
530
+ },
531
+ };
532
+ }
512
533
  const serializeApi = {
513
- /** @type {object | null} */
514
534
  _inst: null,
515
- /** @type {null} */
516
535
  _branchBinds: null,
517
- /** @type {null} */
518
536
  _itemPatches: null,
519
- /** @type {object[] | null} reused child instances from prior SSR mount rounds */
537
+ _eachCtx: null,
520
538
  _ssrPreMounted: null,
521
- /** @type {number} */
522
539
  _ssrPreIdx: 0,
523
- /** @type {object[] | null} newly created child instances this round */
524
540
  _ssrCollected: null,
525
- /**
526
- * @param {new (props?: object) => any} Ctor
527
- * @param {object} resolved
528
- */
529
541
  _ssrChildInstance(Ctor, resolved) {
530
542
  const pre = serializeApi._ssrPreMounted;
531
543
  if (pre && serializeApi._ssrPreIdx < pre.length) {
@@ -537,35 +549,96 @@ const serializeApi = {
537
549
  return child;
538
550
  },
539
551
  el(tag) {
540
- return {
541
- __kind: 'el',
542
- tag: tag || 'div',
543
- attrs: {},
544
- children: [],
545
- appendChild(c) {
546
- if (c != null)
547
- this.children.push(c);
548
- },
549
- };
552
+ return makeVirtualEl(tag);
550
553
  },
551
554
  text(value) {
552
- return { __kind: 'text', value: value == null ? '' : String(value) };
555
+ const node = { __kind: 'text', value: value == null ? '' : String(value), parentNode: null };
556
+ // Generated Direct patches use DOM `textContent`; virtual text uses `value`.
557
+ Object.defineProperty(node, 'textContent', {
558
+ configurable: true,
559
+ enumerable: false,
560
+ get() {
561
+ return node.value;
562
+ },
563
+ set(v) {
564
+ node.value = v == null ? '' : String(v);
565
+ },
566
+ });
567
+ return node;
568
+ },
569
+ comment(value) {
570
+ return { __kind: 'comment', value: value == null ? '' : String(value), parentNode: null };
553
571
  },
554
572
  frag() {
555
573
  return {
556
574
  __kind: 'frag',
557
575
  children: [],
576
+ parentNode: null,
558
577
  appendChild(c) {
559
- if (c != null)
560
- this.children.push(c);
578
+ virtualAppend(this, c);
579
+ },
580
+ insertBefore(node, ref) {
581
+ virtualInsertBefore(this, node, ref);
561
582
  },
562
583
  };
563
584
  },
585
+ insertBefore(parent, node, ref) {
586
+ virtualInsertBefore(parent, node, ref);
587
+ },
588
+ removeNode(node) {
589
+ virtualRemove(node);
590
+ },
591
+ trackPatch(inst, deps, patch, bindingId = null) {
592
+ if (typeof patch !== 'function')
593
+ return;
594
+ if (serializeApi._branchBinds) {
595
+ serializeApi._branchBinds.push({ deps: deps || [], fn: patch, bindingId });
596
+ try {
597
+ patch.call(inst);
598
+ }
599
+ catch (err) {
600
+ console.error('vmz:ssr trackPatch branch', err);
601
+ }
602
+ return;
603
+ }
604
+ if (serializeApi._itemPatches) {
605
+ patch.__vmzItemLocal = true;
606
+ serializeApi._itemPatches.push(patch);
607
+ if (serializeApi._eachCtx) {
608
+ serializeApi._eachCtx.noteItemBind(bindingId, deps || [], patch);
609
+ }
610
+ try {
611
+ patch.call(inst);
612
+ }
613
+ catch (err) {
614
+ console.error('vmz:ssr trackPatch item', err);
615
+ }
616
+ return;
617
+ }
618
+ try {
619
+ patch.call(inst);
620
+ }
621
+ catch (err) {
622
+ console.error('vmz:ssr trackPatch', err);
623
+ }
624
+ },
625
+ untrackPatch() {
626
+ /* SSR one-shot schedule ??no reactive unregister */
627
+ },
628
+ disposeTree() {
629
+ /* SSR one-shot schedule */
630
+ },
564
631
  attr(el, name, value) {
565
632
  if (!el || el.__kind !== 'el')
566
633
  return;
567
634
  applySerializeAttr(el, name, value);
568
635
  },
636
+ mergeClass(...parts) {
637
+ return mergeClassParts(...parts);
638
+ },
639
+ mergeStyle(...parts) {
640
+ return mergeStyleParts(...parts);
641
+ },
569
642
  on() {
570
643
  /* events are no-ops during SSR */
571
644
  },
@@ -581,16 +654,6 @@ const serializeApi = {
581
654
  onComponentEvent() {
582
655
  /* component events attach only on the client */
583
656
  },
584
- bindText(inst, bindingId, deps, get, textNode) {
585
- let raw = '';
586
- try {
587
- raw = get.call(inst);
588
- }
589
- catch {
590
- raw = '';
591
- }
592
- textNode.value = String(raw ?? '');
593
- },
594
657
  specFieldText(inst, _bindingId, fieldName, textNode) {
595
658
  let raw;
596
659
  try {
@@ -601,16 +664,6 @@ const serializeApi = {
601
664
  }
602
665
  textNode.value = String(raw ?? '');
603
666
  },
604
- bindAttr(inst, bindingId, deps, get, el, name) {
605
- let raw;
606
- try {
607
- raw = get.call(inst);
608
- }
609
- catch {
610
- raw = null;
611
- }
612
- applySerializeAttr(el, name, raw);
613
- },
614
667
  specFieldAttr(inst, _bindingId, fieldName, el, name) {
615
668
  let raw;
616
669
  try {
@@ -619,6 +672,14 @@ const serializeApi = {
619
672
  catch {
620
673
  raw = null;
621
674
  }
675
+ if (name === 'class' || name === 'className') {
676
+ applySerializeAttr(el, 'class', mergeClassParts(raw));
677
+ return;
678
+ }
679
+ if (name === 'style') {
680
+ applySerializeAttr(el, 'style', mergeStyleParts(raw));
681
+ return;
682
+ }
622
683
  applySerializeAttr(el, name, raw);
623
684
  },
624
685
  bindComponentProp() {
@@ -660,7 +721,7 @@ const serializeApi = {
660
721
  slot.__rawHtml = null;
661
722
  if (!Array.isArray(slot.children))
662
723
  slot.children = [];
663
- // Append — multiple projectDefaultSlot calls must accumulate (SSR).
724
+ // Append ??multiple projectDefaultSlot calls must accumulate (SSR).
664
725
  // Client path replaces the live <slot> then appends siblings; serialize must push.
665
726
  slot.children.push(node);
666
727
  return;
@@ -675,85 +736,16 @@ const serializeApi = {
675
736
  el.children = [];
676
737
  },
677
738
  bindHtml(inst, bindingId, deps, get, el) {
678
- let raw = '';
679
- try {
680
- raw = get.call(inst);
681
- }
682
- catch {
683
- raw = '';
684
- }
685
- el.__rawHtml = raw == null ? '' : String(raw);
686
- el.children = [];
687
- },
688
- ifBlock(inst, bindingId, deps, branches) {
689
- // No empty `span[data-vmz-if]` box (`ui-vif-dom`): false → empty frag.
690
- const frag = serializeApi.frag();
691
- let idx = -1;
692
- for (let i = 0; i < branches.length; i++) {
693
- const b = branches[i];
694
- if (!b.cond) {
695
- idx = i;
696
- break;
697
- }
739
+ serializeApi.trackPatch(inst, deps || [], function bindHtmlPatch() {
740
+ let raw;
698
741
  try {
699
- if (b.cond.call(inst)) {
700
- idx = i;
701
- break;
702
- }
742
+ raw = get.call(inst);
703
743
  }
704
744
  catch {
705
- /* continue */
706
- }
707
- }
708
- if (idx >= 0 && branches[idx].create) {
709
- const created = branches[idx].create.call(inst, serializeApi);
710
- if (created)
711
- frag.appendChild(created);
712
- }
713
- return frag;
714
- },
715
- eachBlock(inst, bindingId, deps, spec) {
716
- const frag = serializeApi.frag();
717
- let list = [];
718
- try {
719
- list = spec.list.call(inst) || [];
720
- }
721
- catch {
722
- list = [];
723
- }
724
- if (!Array.isArray(list))
725
- list = [...list];
726
- for (let i = 0; i < list.length; i++) {
727
- const box = { item: list[i], index: i };
728
- let k = i;
729
- if (typeof spec.key === 'function') {
730
- try {
731
- k = spec.key.call(inst, box);
732
- }
733
- catch {
734
- k = i;
735
- }
736
- }
737
- let dom = null;
738
- if (typeof spec.createItem === 'function') {
739
- dom = spec.createItem.call(inst, serializeApi, box);
740
- }
741
- else if (typeof spec.serializeItem === 'function') {
742
- // IR-homologous schedule (v0.1.7): same Direct body as fat createItem.
743
- dom = spec.serializeItem.call(inst, serializeApi, box);
745
+ raw = '';
744
746
  }
745
- else if (spec.rowKernel && typeof spec.rowKernel.html === 'string') {
746
- // Transitional only: pre-0.1.7 emit without serializeItem.
747
- dom = serializeRowFromKernel(inst, spec.rowKernel, box, k);
748
- }
749
- if (dom) {
750
- // SSR only: serialize key into HTML for hydrate/debug. Direct client does not write this attr.
751
- if (dom.__kind === 'el' && !dom.__rawOuter)
752
- serializeApi.attr(dom, 'data-vmz-key', String(k));
753
- frag.appendChild(dom);
754
- }
755
- }
756
- return frag;
747
+ serializeApi.setHtml(el, raw);
748
+ }, bindingId);
757
749
  },
758
750
  component(hostInst, nameOrCtor, props, client) {
759
751
  const name = typeof nameOrCtor === 'function' ? nameOrCtor.__vmzTag || nameOrCtor.name || 'Component' : nameOrCtor;
@@ -761,7 +753,6 @@ const serializeApi = {
761
753
  if (!Ctor) {
762
754
  return serializeUnknownComponentNode(name);
763
755
  }
764
- /** @type {Record<string, any>} */
765
756
  const resolved = {};
766
757
  for (const [k, v] of Object.entries(props || {})) {
767
758
  const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
@@ -797,7 +788,6 @@ const serializeApi = {
797
788
  planSchema: plan?.schema || null,
798
789
  planRootIds: plan?.root_ids || [],
799
790
  };
800
- /** @type {Record<string, string>} */
801
791
  const attrs = {
802
792
  'data-vmz': name,
803
793
  'data-vmz-island': name,
@@ -828,7 +818,6 @@ const serializeApi = {
828
818
  serializeApi._inst = child;
829
819
  try {
830
820
  const node = Ctor.__vmzCreate.call(child, serializeApi);
831
- /** @type {Record<string, string>} */
832
821
  const attrs = { 'data-vmz': name };
833
822
  const boxStyle = directHostBoxStyleAttr(name, Ctor);
834
823
  if (boxStyle)
@@ -853,14 +842,27 @@ const serializeApi = {
853
842
  };
854
843
  /**
855
844
  * Serialize-tree attr write (SSR).
856
- * @param {any} el
857
- * @param {string} name
858
- * @param {any} value
859
845
  */
860
846
  function applySerializeAttr(el, name, value) {
861
847
  if (!el || el.__kind !== 'el')
862
848
  return;
863
849
  const key = name === 'className' ? 'class' : name;
850
+ if (key === 'class') {
851
+ const s = mergeClassParts(value);
852
+ if (s)
853
+ el.attrs[key] = s;
854
+ else
855
+ delete el.attrs[key];
856
+ return;
857
+ }
858
+ if (key === 'style') {
859
+ const s = mergeStyleParts(value);
860
+ if (s)
861
+ el.attrs[key] = s;
862
+ else
863
+ delete el.attrs[key];
864
+ return;
865
+ }
864
866
  if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
865
867
  if (value === false || value == null || value === '')
866
868
  delete el.attrs[key];
@@ -876,9 +878,6 @@ function applySerializeAttr(el, name, value) {
876
878
  /**
877
879
  * resume: attach to existing Island DOM without re-running construct structure or onMount.
878
880
  * Consumes ResumeEntry product (`data-vmz-resume`) derived from the same Execution Plan.
879
- * @param {new (props?: object) => any} Component
880
- * @param {HTMLElement} container
881
- * @param {{ props?: object, state?: Record<string, unknown>, strategy?: string } | null} [slice]
882
881
  */
883
882
  export async function resume(Component, container, slice = null) {
884
883
  if (typeof document === 'undefined') {
@@ -916,7 +915,7 @@ export async function resume(Component, container, slice = null) {
916
915
  const inst = createInstance(Component, props);
917
916
  if (parsed.state)
918
917
  applyPreservedState(inst, parsed.state);
919
- // Intentionally never call onMount — SSR already completed that work.
918
+ // Intentionally never call onMount ??SSR already completed that work.
920
919
  if (Component.__vmzDirect && typeof Component.__vmzCreate === 'function') {
921
920
  if (!hasMeaningfulChild(container)) {
922
921
  const node = runDirectCreate(Component, inst);
@@ -942,7 +941,6 @@ export async function resume(Component, container, slice = null) {
942
941
  /**
943
942
  * Resume all `[data-vmz-island]` hosts (prefer ResumeEntry / EventEntry over mount).
944
943
  * Event strategy islands wait for the DOM event before attach (lazy EventEntry).
945
- * @param {ParentNode} [root]
946
944
  */
947
945
  export function resumeIslands(root = globalThis.document) {
948
946
  if (!root || typeof root.querySelectorAll !== 'function') {
@@ -965,7 +963,6 @@ export function resumeIslands(root = globalThis.document) {
965
963
  /**
966
964
  * EventEntry attach: only wire `client:event` / `client:event:*` islands.
967
965
  * Idle/load/visible ResumeEntries are left alone (static shell can defer framework work).
968
- * @param {ParentNode} [root]
969
966
  */
970
967
  export function attachEventEntries(root = globalThis.document) {
971
968
  if (!root || typeof root.querySelectorAll !== 'function') {
@@ -1001,22 +998,15 @@ export function attachEventEntries(root = globalThis.document) {
1001
998
  * - `beginBranchScope()` moves one top-level SSR node into an isolated pool so
1002
999
  * ifBlock branch create cannot steal parent siblings (Drawer / Button hosts).
1003
1000
  *
1004
- * @param {DocumentFragment} pool
1005
- * @param {Element} rootEl
1006
1001
  */
1007
1002
  function createResumeAdopt(pool, rootEl) {
1008
1003
  const used = new WeakSet();
1009
- /** @type {DocumentFragment[]} */
1010
1004
  const scopeStack = [pool];
1011
- /**
1012
- * @param {Node} root
1013
- * @param {(n: Text) => void} visit
1014
- */
1015
1005
  const walkTexts = (root, visit) => {
1016
1006
  if (!root)
1017
1007
  return;
1018
1008
  if (root.nodeType === 3) {
1019
- visit(/** @type {Text} */ (root));
1009
+ visit(root);
1020
1010
  return;
1021
1011
  }
1022
1012
  if (root.nodeType !== 1)
@@ -1032,7 +1022,7 @@ function createResumeAdopt(pool, rootEl) {
1032
1022
  if (k != null && n.__vmzKey == null)
1033
1023
  n.__vmzKey = k;
1034
1024
  }
1035
- // Private child pool — never dump into the parent/global pool (that let
1025
+ // Private child pool ??never dump into the parent/global pool (that let
1036
1026
  // if branches reclaim Drawer/sibling hosts parked as uncles).
1037
1027
  const childPool = document.createDocumentFragment();
1038
1028
  while (n.firstChild)
@@ -1040,25 +1030,22 @@ function createResumeAdopt(pool, rootEl) {
1040
1030
  n.__vmzResumePool = childPool;
1041
1031
  return n;
1042
1032
  };
1043
- /** @returns {Generator<Element>} */
1044
1033
  function* elementCandidates() {
1045
1034
  const scope = currentScope();
1046
1035
  if (scope === pool && rootEl && !used.has(rootEl))
1047
1036
  yield rootEl;
1048
- // Snapshot — adopt may move nodes out of scope while iterating.
1037
+ // Snapshot ??adopt may move nodes out of scope while iterating.
1049
1038
  const kids = [...scope.childNodes];
1050
1039
  for (const n of kids) {
1051
- if (n.nodeType === 1 && !used.has(/** @type {Element} */ (n))) {
1052
- yield /** @type {Element} */ (n);
1040
+ if (n.nodeType === 1 && !used.has(n)) {
1041
+ yield n;
1053
1042
  }
1054
1043
  }
1055
1044
  }
1056
- /** @returns {Generator<Text>} */
1057
1045
  function* textCandidates() {
1058
1046
  const scope = currentScope();
1059
1047
  const kids = [...scope.childNodes];
1060
1048
  for (const n of kids) {
1061
- /** @type {Text[]} */
1062
1049
  const found = [];
1063
1050
  walkTexts(n, (t) => {
1064
1051
  if (!used.has(t))
@@ -1087,7 +1074,6 @@ function createResumeAdopt(pool, rootEl) {
1087
1074
  noteDomCreate();
1088
1075
  return document.createTextNode(value == null ? '' : String(value));
1089
1076
  };
1090
- /** Reclaim SSR `<div data-vmz="Name">` hosts for nested Direct components. */
1091
1077
  const componentHost = (name) => {
1092
1078
  const want = String(name || '');
1093
1079
  for (const el of elementCandidates()) {
@@ -1119,13 +1105,12 @@ function createResumeAdopt(pool, rootEl) {
1119
1105
  /**
1120
1106
  * Isolate the next top-level SSR sibling as the only candidates for an
1121
1107
  * if/else branch create (commercial Card: Empty branch must not adopt Drawer).
1122
- * @returns {() => void} end callback
1123
1108
  */
1124
1109
  const beginBranchScope = () => {
1125
1110
  const parent = currentScope();
1126
1111
  const branchPool = document.createDocumentFragment();
1127
1112
  for (const n of [...parent.childNodes]) {
1128
- if (n.nodeType === 1 && !used.has(/** @type {Element} */ (n))) {
1113
+ if (n.nodeType === 1 && !used.has(n)) {
1129
1114
  branchPool.appendChild(n);
1130
1115
  break;
1131
1116
  }
@@ -1151,9 +1136,6 @@ function createResumeAdopt(pool, rootEl) {
1151
1136
  }
1152
1137
  /**
1153
1138
  * Adopt existing Island DOM while running the same `__vmzCreate` schedule (resume).
1154
- * @param {new (props?: object) => any} Component
1155
- * @param {object} inst
1156
- * @param {Element} container
1157
1139
  */
1158
1140
  function runDirectResume(Component, inst, container) {
1159
1141
  const rootEl = [...container.childNodes].find((n) => n.nodeType === 1 || (n.nodeType === 3 && String(n.textContent).trim() !== ''));
@@ -1187,17 +1169,10 @@ function runDirectResume(Component, inst, container) {
1187
1169
  pool.removeChild(pool.firstChild);
1188
1170
  }
1189
1171
  }
1190
- /**
1191
- * @param {new (props?: object) => any} Component
1192
- * @param {HTMLElement} container
1193
- * @param {object} [props]
1194
- * @param {{ preserveState?: boolean | Record<string, unknown>, skipOnMount?: boolean }} [opts]
1195
- */
1196
1172
  export async function hydrate(Component, container, props = {}, opts = {}) {
1197
1173
  if (typeof document === 'undefined') {
1198
1174
  throw new Error('vmz:dom hydrate() requires a document (browser)');
1199
1175
  }
1200
- /** @type {Record<string, unknown> | null} */
1201
1176
  let preserved = null;
1202
1177
  if (opts.preserveState && typeof opts.preserveState === 'object') {
1203
1178
  preserved = opts.preserveState;
@@ -1241,9 +1216,6 @@ export async function hydrate(Component, container, props = {}, opts = {}) {
1241
1216
  }
1242
1217
  return inst;
1243
1218
  }
1244
- /**
1245
- * @param {ParentNode} [root]
1246
- */
1247
1219
  export function hydrateIslands(root = globalThis.document) {
1248
1220
  // resume: hydrateIslands is an alias for resumeIslands (same Plan attach).
1249
1221
  return resumeIslands(root);