@vmz/core 0.3.12 → 0.3.14

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 (35) hide show
  1. package/dist/browser/client-nav.types.d.ts +2 -5
  2. package/dist/browser/direct-api.types.d.ts +6 -0
  3. package/dist/browser/dom/attributes.js +142 -2
  4. package/dist/browser/dom/component.d.ts +10 -0
  5. package/dist/browser/dom/component.js +16 -4
  6. package/dist/browser/dom/direct-api.d.ts +43 -3
  7. package/dist/browser/dom/direct-api.js +421 -84
  8. package/dist/browser/dom-core.d.ts +1 -0
  9. package/dist/browser/dom-core.js +1 -0
  10. package/dist/browser/dom-core.types.d.ts +10 -2
  11. package/dist/browser/navigation/client-navigation.js +18 -30
  12. package/dist/browser/reactivity/write-barrier.js +38 -11
  13. package/dist/browser/resume/adopt.js +142 -10
  14. package/dist/browser/resume/hydrate.js +36 -5
  15. package/dist/browser/resume/resume.js +2 -5
  16. package/dist/host/deployment/deployment-registry.js +47 -1
  17. package/dist/host/render/render-host.js +2 -0
  18. package/dist/host/serve/bootstrap.js +8 -4
  19. package/dist/host/serve/hmr-dev.d.ts +16 -17
  20. package/dist/host/serve/hmr-dev.js +114 -35
  21. package/dist/host/serve/http.js +25 -8
  22. package/dist/host/serve/pack-entry.d.ts +31 -0
  23. package/dist/host/serve/pack-entry.js +98 -0
  24. package/dist/host/serve/page-render.js +12 -4
  25. package/dist/shared/dom-attr-normalize.d.ts +14 -0
  26. package/dist/shared/dom-attr-normalize.js +158 -19
  27. package/dist/shared/host.types.d.ts +9 -0
  28. package/dist/shared/html-digest.d.ts +15 -0
  29. package/dist/shared/html-digest.js +25 -0
  30. package/dist/ssr/onmount.d.ts +37 -0
  31. package/dist/ssr/onmount.js +94 -0
  32. package/dist/ssr/render.js +7 -8
  33. package/dist/ssr/serialize.d.ts +1 -1
  34. package/dist/ssr/serialize.js +309 -13
  35. package/package.json +7 -7
@@ -66,6 +66,13 @@ export type DedupeComponentEntriesOpts = {
66
66
  export type ImportComponentEntriesOpts = {
67
67
  cacheBust?: string | number;
68
68
  loaded?: Set<string>;
69
+ /**
70
+ * Import the unpacked client tree (`${chunkId}.client.js`) even when
71
+ * `_vmz/pack-node-manifest.json` exists. Client hosts (headless logic
72
+ * mounts) execute unpacked client bytes, so their registry must use the
73
+ * same tree. Node/SSR hosts keep the default packed-node lookup.
74
+ */
75
+ unpacked?: boolean;
69
76
  };
70
77
  export type BootstrapComponentRegistryOpts = LoadComponentEntriesOpts & ImportComponentEntriesOpts & {
71
78
  preload?: 'all' | 'closure' | 'none';
@@ -109,5 +116,7 @@ export type RenderHostOpts = {
109
116
  preload?: 'all' | 'closure' | 'none';
110
117
  cacheBust?: string | number;
111
118
  rootChunkIds?: string[] | null;
119
+ /** Client hosts import registry entries from the unpacked client tree. */
120
+ unpackedEntries?: boolean;
112
121
  [key: string]: unknown;
113
122
  };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Content identity for `v-html` payloads shared by SSR and resume.
3
+ *
4
+ * The browser serializer expands self-closing tags (`<path/>` becomes
5
+ * `<path></path>`), so comparing a generated markup string with the serialized
6
+ * SSR DOM always misses. SSR therefore records the digest of the exact string
7
+ * it wrote, and resume compares that marker with the digest of the string it
8
+ * would write. The digest is taken over the generated string only: no DOM
9
+ * serialization, no normalization.
10
+ *
11
+ * Two independent 32-bit FNV-1a lanes plus the code-unit length give 64 bits of
12
+ * identity without pulling a crypto dependency into the browser bundle.
13
+ */
14
+ export declare const HTML_HASH_ATTR = "data-vmz-html-hash";
15
+ export declare function htmlDigest(value: string): string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Content identity for `v-html` payloads shared by SSR and resume.
3
+ *
4
+ * The browser serializer expands self-closing tags (`<path/>` becomes
5
+ * `<path></path>`), so comparing a generated markup string with the serialized
6
+ * SSR DOM always misses. SSR therefore records the digest of the exact string
7
+ * it wrote, and resume compares that marker with the digest of the string it
8
+ * would write. The digest is taken over the generated string only: no DOM
9
+ * serialization, no normalization.
10
+ *
11
+ * Two independent 32-bit FNV-1a lanes plus the code-unit length give 64 bits of
12
+ * identity without pulling a crypto dependency into the browser bundle.
13
+ */
14
+ export const HTML_HASH_ATTR = 'data-vmz-html-hash';
15
+ export function htmlDigest(value) {
16
+ const text = value == null ? '' : String(value);
17
+ let laneA = 0x811c9dc5;
18
+ let laneB = 0x9e3779b9;
19
+ for (let i = 0; i < text.length; i++) {
20
+ const code = text.charCodeAt(i);
21
+ laneA = Math.imul(laneA ^ code, 0x01000193) >>> 0;
22
+ laneB = Math.imul(laneB ^ code, 0x85ebca77) >>> 0;
23
+ }
24
+ return `${text.length.toString(36)}-${laneA.toString(36)}-${laneB.toString(36)}`;
25
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * SSR `onMount` execution budget.
3
+ *
4
+ * Contract: every per-instance `onMount` await during SSR is bounded (default
5
+ * 5000ms, hosts may override via `opts.onMountBudgetMs`). Budget exhaustion and
6
+ * `signal` abort are the same event: the render terminates and fails — no
7
+ * half-baked HTML, no silent empty-string downgrade, no retry. An `onMount`
8
+ * that throws propagates its original error unchanged.
9
+ */
10
+ export declare const DEFAULT_SSR_ONMOUNT_BUDGET_MS = 5000;
11
+ /** `onMount` exceeded the SSR budget. Carries component, budget and elapsed. */
12
+ export declare class SsrOnMountTimeoutError extends Error {
13
+ readonly component: string;
14
+ readonly budgetMs: number;
15
+ readonly elapsedMs: number;
16
+ constructor(component: string, budgetMs: number, elapsedMs: number);
17
+ }
18
+ /** The host signal aborted a bounded `onMount` await. */
19
+ export declare class SsrRenderAbortError extends Error {
20
+ readonly component: string;
21
+ constructor(component: string);
22
+ }
23
+ export type SsrOnMountContext = {
24
+ budgetMs: number;
25
+ signal: AbortSignal | null;
26
+ };
27
+ /** Read the host options into a budget context (`opts.onMountBudgetMs`, `opts.signal`). */
28
+ export declare function ssrOnMountContext(opts: any): SsrOnMountContext;
29
+ /** Best-effort component label for diagnostics. */
30
+ export declare function ssrComponentName(Component: any, inst: any): string;
31
+ /**
32
+ * Await one instance's `onMount` under the budget. The race is settled exactly
33
+ * once; a late-resolving author promise after timeout/abort is dropped. The
34
+ * budget timer itself bounds the wait, so a hung `onMount` can never stall the
35
+ * host past `budgetMs`.
36
+ */
37
+ export declare function awaitSsrOnMount(inst: any, component: string, ctx: SsrOnMountContext): Promise<void>;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * SSR `onMount` execution budget.
3
+ *
4
+ * Contract: every per-instance `onMount` await during SSR is bounded (default
5
+ * 5000ms, hosts may override via `opts.onMountBudgetMs`). Budget exhaustion and
6
+ * `signal` abort are the same event: the render terminates and fails — no
7
+ * half-baked HTML, no silent empty-string downgrade, no retry. An `onMount`
8
+ * that throws propagates its original error unchanged.
9
+ */
10
+ export const DEFAULT_SSR_ONMOUNT_BUDGET_MS = 5000;
11
+ /** `onMount` exceeded the SSR budget. Carries component, budget and elapsed. */
12
+ export class SsrOnMountTimeoutError extends Error {
13
+ component;
14
+ budgetMs;
15
+ elapsedMs;
16
+ constructor(component, budgetMs, elapsedMs) {
17
+ super(`vmz:ssr onMount budget exceeded: component=${component} budget=${budgetMs}ms elapsed=${elapsedMs}ms`);
18
+ this.name = 'SsrOnMountTimeoutError';
19
+ this.component = component;
20
+ this.budgetMs = budgetMs;
21
+ this.elapsedMs = elapsedMs;
22
+ }
23
+ }
24
+ /** The host signal aborted a bounded `onMount` await. */
25
+ export class SsrRenderAbortError extends Error {
26
+ component;
27
+ constructor(component) {
28
+ super(`vmz:ssr onMount aborted by host signal: component=${component}`);
29
+ this.name = 'SsrRenderAbortError';
30
+ this.component = component;
31
+ }
32
+ }
33
+ /** Read the host options into a budget context (`opts.onMountBudgetMs`, `opts.signal`). */
34
+ export function ssrOnMountContext(opts) {
35
+ const raw = opts && opts.onMountBudgetMs;
36
+ const budgetMs = Number.isFinite(raw) && Number(raw) > 0 ? Number(raw) : DEFAULT_SSR_ONMOUNT_BUDGET_MS;
37
+ const signal = opts && opts.signal ? opts.signal : null;
38
+ return { budgetMs, signal };
39
+ }
40
+ /** Best-effort component label for diagnostics. */
41
+ export function ssrComponentName(Component, inst) {
42
+ return ((Component && (Component.__vmzTag || Component.name)) ||
43
+ (inst && inst.constructor && inst.constructor.name) ||
44
+ 'AnonymousComponent');
45
+ }
46
+ /**
47
+ * Await one instance's `onMount` under the budget. The race is settled exactly
48
+ * once; a late-resolving author promise after timeout/abort is dropped. The
49
+ * budget timer itself bounds the wait, so a hung `onMount` can never stall the
50
+ * host past `budgetMs`.
51
+ */
52
+ export async function awaitSsrOnMount(inst, component, ctx) {
53
+ const onMount = inst && inst.onMount;
54
+ if (typeof onMount !== 'function')
55
+ return;
56
+ if (ctx.signal && ctx.signal.aborted)
57
+ throw new SsrRenderAbortError(component);
58
+ const startedAt = Date.now();
59
+ let timer = null;
60
+ let onAbort = null;
61
+ try {
62
+ await new Promise((resolve, reject) => {
63
+ let settled = false;
64
+ const settle = (fn) => {
65
+ if (settled)
66
+ return;
67
+ settled = true;
68
+ fn();
69
+ };
70
+ timer = setTimeout(() => {
71
+ settle(() => reject(new SsrOnMountTimeoutError(component, ctx.budgetMs, Date.now() - startedAt)));
72
+ }, ctx.budgetMs);
73
+ if (ctx.signal) {
74
+ onAbort = () => settle(() => reject(new SsrRenderAbortError(component)));
75
+ ctx.signal.addEventListener('abort', onAbort, { once: true });
76
+ }
77
+ let result;
78
+ try {
79
+ result = onMount.call(inst);
80
+ }
81
+ catch (err) {
82
+ settle(() => reject(err));
83
+ return;
84
+ }
85
+ Promise.resolve(result).then(() => settle(resolve), (err) => settle(() => reject(err)));
86
+ });
87
+ }
88
+ finally {
89
+ if (timer)
90
+ clearTimeout(timer);
91
+ if (ctx.signal && onAbort)
92
+ ctx.signal.removeEventListener('abort', onAbort);
93
+ }
94
+ }
@@ -1,5 +1,6 @@
1
1
  import { createInstance, destroy, } from '../browser/dom-core.js';
2
2
  import { ensureSsrDocument } from './document.js';
3
+ import { awaitSsrOnMount, ssrComponentName, ssrOnMountContext } from './onmount.js';
3
4
  import { flattenSerializeNode, runDirectSerializeTreeWithMounts, streamSerializeChunks } from './serialize.js';
4
5
  export async function renderToString(Component, props = {}, opts = {}) {
5
6
  ensureSsrDocument();
@@ -7,16 +8,15 @@ export async function renderToString(Component, props = {}, opts = {}) {
7
8
  if (signal && signal.aborted)
8
9
  return '';
9
10
  const inst = createInstance(Component, props);
10
- if (typeof inst.onMount === 'function') {
11
- await inst.onMount();
12
- }
11
+ const onMountCtx = ssrOnMountContext(opts);
12
+ await awaitSsrOnMount(inst, ssrComponentName(Component, inst), onMountCtx);
13
13
  if (signal && signal.aborted)
14
14
  return '';
15
15
  // production Direct emit: SSR only via Direct serialize schedule ??never `render`.
16
16
  if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
17
17
  throw new Error(`vmz:dom renderToString() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
18
18
  }
19
- const root = await runDirectSerializeTreeWithMounts(Component, inst);
19
+ const root = await runDirectSerializeTreeWithMounts(Component, inst, onMountCtx);
20
20
  if (opts && opts.slotHtml != null)
21
21
  injectDefaultSlotHtml(root, opts.slotHtml);
22
22
  return flattenSerializeNode(root);
@@ -29,15 +29,14 @@ export async function* renderToStream(Component, props = {}, opts = {}) {
29
29
  return;
30
30
  const inst = createInstance(Component, props);
31
31
  try {
32
- if (typeof inst.onMount === 'function') {
33
- await inst.onMount();
34
- }
32
+ const onMountCtx = ssrOnMountContext(opts);
33
+ await awaitSsrOnMount(inst, ssrComponentName(Component, inst), onMountCtx);
35
34
  if (aborted())
36
35
  return;
37
36
  if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
38
37
  throw new Error(`vmz:dom renderToStream() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
39
38
  }
40
- const root = await runDirectSerializeTreeWithMounts(Component, inst);
39
+ const root = await runDirectSerializeTreeWithMounts(Component, inst, onMountCtx);
41
40
  if (opts && opts.slotHtml != null)
42
41
  injectDefaultSlotHtml(root, opts.slotHtml);
43
42
  if (aborted())
@@ -1,3 +1,3 @@
1
- export declare function runDirectSerializeTreeWithMounts(Component: any, inst: any): Promise<any>;
1
+ export declare function runDirectSerializeTreeWithMounts(Component: any, inst: any, onMountCtx?: any): Promise<any>;
2
2
  export declare function flattenSerializeNode(node: any): any;
3
3
  export declare function streamSerializeChunks(node: any): any;