@vmz/core 0.0.2 → 0.0.4

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
@@ -31,7 +31,8 @@ state write
31
31
  -> direct patch / switch / reconcile
32
32
  ```
33
33
 
34
- There is no default detour through “execute every component that might matter, build virtual nodes, then compare them.” If analysis must widen, it widens to a safe region and should preserve the reason.
34
+ There is no default detour through “execute every component that might matter, build virtual nodes, then compare them.”
35
+ If analysis must widen, it widens to a safe region and should preserve the reason.
35
36
 
36
37
  ## More than DOM updates
37
38
 
@@ -41,7 +42,8 @@ There is no default detour through “execute every component that might matter,
41
42
  - **Resumption:** the browser attaches to server-produced work at the smallest reachable boundary.
42
43
  - **Zero-JS delivery:** pages with no interactive requirement do not need an eager framework shell.
43
44
 
44
- The runtime should not grow a second compiler made of reflection, string dependencies, or generic proxies. Its quality comes from faithfully executing the generated plan while keeping the production surface focused.
45
+ The runtime should not grow a second compiler made of reflection, string dependencies, or generic proxies. Its quality
46
+ comes from faithfully executing the generated plan while keeping the production surface focused.
45
47
 
46
48
  ## License
47
49
 
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Browser client navigation — same-app `<Link>` SPA takeover.
3
+ * Zero-JS still works via real `<a href>`; with JS, intercept same-origin
4
+ * `a[data-vmz-route]` clicks, pushState, fetch SSR HTML, swap #app, hydrate.
5
+ */
6
+ /**
7
+ * @param {{
8
+ * fetchImpl?: typeof fetch,
9
+ * document?: Document,
10
+ * history?: History,
11
+ * location?: Location,
12
+ * hydrate?: (Ctor: any, root: Element, props: object) => Promise<unknown>,
13
+ * hydrateRoute?: (Page: any, root: Element, props: object, layouts?: any[]) => Promise<unknown>,
14
+ * destroy?: (inst: object) => void,
15
+ * importPage?: (chunkId: string) => Promise<any>,
16
+ * }} [opts]
17
+ */
18
+ export declare function installClientNavigation(opts?: {}): {
19
+ ok: boolean;
20
+ reason: string;
21
+ transitionTo?: undefined;
22
+ dispose?: undefined;
23
+ } | {
24
+ ok: boolean;
25
+ transitionTo: (url: any, { replace, fromPop }?: {
26
+ replace?: boolean;
27
+ fromPop?: boolean;
28
+ }) => Promise<{
29
+ ok: boolean;
30
+ reason: string;
31
+ href?: undefined;
32
+ chunkId?: undefined;
33
+ } | {
34
+ ok: boolean;
35
+ href: string;
36
+ chunkId: any;
37
+ reason?: undefined;
38
+ }>;
39
+ dispose(): void;
40
+ reason?: undefined;
41
+ };
42
+ /**
43
+ * @param {string} html
44
+ * @param {Document} doc
45
+ */
46
+ export declare function extractAppHtml(html: any, doc: any): any;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Browser client navigation — same-app `<Link>` SPA takeover.
3
+ * Zero-JS still works via real `<a href>`; with JS, intercept same-origin
4
+ * `a[data-vmz-route]` clicks, pushState, fetch SSR HTML, swap #app, hydrate.
5
+ */
6
+ // @ts-nocheck
7
+ /**
8
+ * @param {{
9
+ * fetchImpl?: typeof fetch,
10
+ * document?: Document,
11
+ * history?: History,
12
+ * location?: Location,
13
+ * hydrate?: (Ctor: any, root: Element, props: object) => Promise<unknown>,
14
+ * hydrateRoute?: (Page: any, root: Element, props: object, layouts?: any[]) => Promise<unknown>,
15
+ * destroy?: (inst: object) => void,
16
+ * importPage?: (chunkId: string) => Promise<any>,
17
+ * }} [opts]
18
+ */
19
+ export function installClientNavigation(opts = {}) {
20
+ const doc = opts.document || (typeof document !== 'undefined' ? document : null);
21
+ const win = typeof window !== 'undefined' ? window : null;
22
+ const hist = opts.history || win?.history;
23
+ const loc = opts.location || win?.location;
24
+ const fetchImpl = opts.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
25
+ if (!doc || !hist || !loc || !fetchImpl) {
26
+ return { ok: false, reason: 'missing document/history/fetch' };
27
+ }
28
+ if (win && !win.__vmzBootId) {
29
+ win.__vmzBootId = `boot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
30
+ }
31
+ if (win)
32
+ win.__vmzClientNavInstalled = true;
33
+ /** @type {AbortController | null} */
34
+ let inflight = null;
35
+ let navigating = false;
36
+ async function transitionTo(url, { replace = false, fromPop = false } = {}) {
37
+ const target = new URL(url, loc.href);
38
+ if (target.origin !== loc.origin) {
39
+ loc.assign(target.href);
40
+ return { ok: false, reason: 'cross-origin' };
41
+ }
42
+ if (inflight)
43
+ inflight.abort();
44
+ inflight = new AbortController();
45
+ const signal = inflight.signal;
46
+ navigating = true;
47
+ try {
48
+ const res = await fetchImpl(target.pathname + target.search, {
49
+ method: 'GET',
50
+ headers: { accept: 'text/html', 'x-vmz-client-nav': '1' },
51
+ signal,
52
+ });
53
+ if (!res.ok) {
54
+ // Fall back to full navigation on hard errors.
55
+ if (!fromPop)
56
+ loc.assign(target.href);
57
+ return { ok: false, reason: `http ${res.status}` };
58
+ }
59
+ const html = await res.text();
60
+ const nextApp = extractAppHtml(html, doc);
61
+ if (!nextApp) {
62
+ if (!fromPop)
63
+ loc.assign(target.href);
64
+ return { ok: false, reason: 'missing #app in response' };
65
+ }
66
+ const root = doc.getElementById('app');
67
+ if (!root) {
68
+ if (!fromPop)
69
+ loc.assign(target.href);
70
+ return { ok: false, reason: 'missing #app' };
71
+ }
72
+ // Dispose previous Direct instance if present.
73
+ const prev = root.__vmzInst;
74
+ if (prev && typeof opts.destroy === 'function') {
75
+ opts.destroy(prev);
76
+ }
77
+ else if (prev && win?.vmzDestroy) {
78
+ win.vmzDestroy(prev);
79
+ }
80
+ root.__vmzInst = null;
81
+ root.outerHTML = nextApp.outerHTML;
82
+ const fresh = doc.getElementById('app');
83
+ if (!fresh)
84
+ return { ok: false, reason: 'swap lost #app' };
85
+ if (!fromPop) {
86
+ if (replace)
87
+ hist.replaceState({ vmzClientNav: true }, '', target.href);
88
+ else
89
+ hist.pushState({ vmzClientNav: true }, '', target.href);
90
+ }
91
+ const chunkId = fresh.getAttribute('data-vmz-page') || '';
92
+ let props = {};
93
+ try {
94
+ const raw = fresh.getAttribute('data-vmz-props');
95
+ if (raw)
96
+ props = JSON.parse(raw);
97
+ }
98
+ catch {
99
+ /* ignore */
100
+ }
101
+ if (chunkId) {
102
+ const importChunk = opts.importPage || (async (id) => (await import(/* @vite-ignore */ `/${id}.client.js`)).default);
103
+ const Page = await importChunk(chunkId);
104
+ const layoutChain = (fresh.getAttribute('data-vmz-layout') || '')
105
+ .split(',')
106
+ .map((s) => s.trim())
107
+ .filter(Boolean);
108
+ /** @type {any[]} */
109
+ const layoutCtors = [];
110
+ for (const id of layoutChain) {
111
+ layoutCtors.push(await importChunk(id));
112
+ }
113
+ const dom = await import(/* @vite-ignore */ '/vmz-dom.js');
114
+ const hydrateRoute = opts.hydrateRoute || dom.hydrateRoute;
115
+ if (typeof hydrateRoute === 'function') {
116
+ await hydrateRoute(Page, fresh, props, layoutCtors);
117
+ }
118
+ else {
119
+ const hydrate = opts.hydrate || dom.hydrate;
120
+ await hydrate(Page, fresh, props);
121
+ }
122
+ }
123
+ if (win) {
124
+ win.__vmzClientNavCount = (win.__vmzClientNavCount || 0) + 1;
125
+ win.__vmzLastClientNav = {
126
+ href: target.pathname + target.search,
127
+ routeId: nextApp.getAttribute?.('data-vmz-route') || null,
128
+ chunkId,
129
+ bootId: win.__vmzBootId,
130
+ };
131
+ }
132
+ return { ok: true, href: target.pathname + target.search, chunkId };
133
+ }
134
+ finally {
135
+ navigating = false;
136
+ }
137
+ }
138
+ function onClick(ev) {
139
+ if (navigating)
140
+ return;
141
+ if (ev.defaultPrevented)
142
+ return;
143
+ if (ev.button !== 0)
144
+ return;
145
+ if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey)
146
+ return;
147
+ const a = ev.target?.closest?.('a[data-vmz-route][href]');
148
+ if (!a)
149
+ return;
150
+ const href = a.getAttribute('href');
151
+ if (!href || href.startsWith('#') || /^(mailto|tel|javascript):/i.test(href))
152
+ return;
153
+ const u = new URL(href, loc.href);
154
+ if (u.origin !== loc.origin)
155
+ return;
156
+ // Download / new tab
157
+ if (a.hasAttribute('download') || a.getAttribute('target') === '_blank')
158
+ return;
159
+ ev.preventDefault();
160
+ void transitionTo(u.pathname + u.search + u.hash, {
161
+ replace: a.getAttribute('data-vmz-replace') === 'true',
162
+ });
163
+ }
164
+ function onPopState() {
165
+ void transitionTo(loc.pathname + loc.search + loc.hash, { fromPop: true });
166
+ }
167
+ doc.addEventListener('click', onClick);
168
+ win?.addEventListener?.('popstate', onPopState);
169
+ return {
170
+ ok: true,
171
+ transitionTo,
172
+ dispose() {
173
+ doc.removeEventListener('click', onClick);
174
+ win?.removeEventListener?.('popstate', onPopState);
175
+ },
176
+ };
177
+ }
178
+ /**
179
+ * @param {string} html
180
+ * @param {Document} doc
181
+ */
182
+ export function extractAppHtml(html, doc) {
183
+ const parser = new (doc.defaultView?.DOMParser || globalThis.DOMParser)();
184
+ const parsed = parser.parseFromString(html, 'text/html');
185
+ return parsed.getElementById('app');
186
+ }
package/dist/dom.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * VMZ DOM / SSR runtime — precise patches, no VDOM diff.
3
3
  *
4
- * Design: 规划设计/vmz/04 · Gate 3 (no production `render()`)
5
4
  *
6
5
  * Direct components expose `__vmzCreate` / `__vmzSerialize` / `__vmzPlan`.
7
6
  * Mount, SSR, hydrate, and resume all run that same schedule.
@@ -14,7 +13,7 @@ export declare function __vmzTraceEnable(on?: boolean): void;
14
13
  export declare function __vmzPrecisionReset(): void;
15
14
  export declare function __vmzTraceReset(): void;
16
15
  /**
17
- * X5 StableId event snapshot (`vmz.dx.trace.v0` shape without schema stamp —
16
+ * StableId event snapshot (`vmz.dx.trace.v0` shape without schema stamp —
18
17
  * host may wrap via ingestRuntimeTrace).
19
18
  * @returns {{ schema: string, events: typeof traceBuf.events, status: string }}
20
19
  */
@@ -45,27 +44,46 @@ export declare function registerComponents(map: any): void;
45
44
  * @param {new (props?: object) => any} Component
46
45
  * @param {object} [props]
47
46
  */
48
- export declare function renderToString(Component: any, props?: {}): Promise<any>;
47
+ export declare function renderToString(Component: any, props?: {}, opts?: {}): Promise<any>;
49
48
  /**
50
49
  * Stream SSR via the same Direct serialize schedule as `renderToString`.
51
50
  * Yields HTML chunks (open tag → children → close). Joining chunks equals `renderToString`.
52
51
  * Supports AbortSignal for cancel; consumers should respect backpressure (await between chunks).
53
52
  * @param {new (props?: object) => any} Component
54
53
  * @param {object} [props]
55
- * @param {{ signal?: AbortSignal }} [opts]
54
+ * @param {{ signal?: AbortSignal, slotHtml?: string }} [opts]
56
55
  * @returns {AsyncGenerator<string, void, void>}
57
56
  */
58
57
  export declare function renderToStream(Component: any, props?: {}, opts?: {}): AsyncGenerator<any, void, unknown>;
58
+ /**
59
+ * Live-DOM counterpart: first default `<slot>` owned by this tree, not by a nested
60
+ * `[data-vmz]` component (Button/Link labels, etc.).
61
+ * @param {Element | null | undefined} root
62
+ * @returns {Element | null}
63
+ */
64
+ export declare function findOwnedDefaultSlot(root: any): any;
65
+ /**
66
+ * Hydrate/mount a file-route page inside an optional layout chain (outer → inner).
67
+ * Mirrors SSR `slotHtml` wrapping: each layout's owned default slot becomes the
68
+ * outlet for the next layout or the page. Retains layout instances on `container`
69
+ * so SPA transitions can dispose only the page host.
70
+ * @param {new (props?: object) => any} Page
71
+ * @param {Element} container
72
+ * @param {object} [props]
73
+ * @param {Array<new (props?: object) => any>} [layoutCtors] outer → inner
74
+ * @param {{ preserveState?: boolean | Record<string, unknown>, skipOnMount?: boolean }} [opts]
75
+ */
76
+ export declare function hydrateRoute(Page: any, container: any, props?: {}, layoutCtors?: any[], opts?: {}): Promise<any>;
59
77
  /**
60
78
  * Mount once; later updates are dep patches only (never re-run structure).
61
- * Requires compiler `__vmzCreate` (Gate 3 — no blueprint fallback).
79
+ * Requires compiler `__vmzCreate` (production Direct emit — no blueprint fallback).
62
80
  * @param {new (props?: object) => any} Component
63
81
  * @param {Element} container
64
82
  * @param {object} [props]
65
83
  */
66
84
  export declare function mount(Component: any, container: any, props?: {}): Promise<any>;
67
85
  /**
68
- * Snapshot plain state/prop field values for Island HMR (N4.3).
86
+ * Snapshot plain state/prop field values for Island HMR (session).
69
87
  * @param {object} inst
70
88
  * @returns {Record<string, unknown> | null}
71
89
  */
@@ -76,7 +94,7 @@ export declare function snapshotInstanceState(inst: any): {};
76
94
  */
77
95
  export declare function applyPreservedState(inst: any, state: any): void;
78
96
  /**
79
- * L5: attach to existing Island DOM without re-running construct structure or onMount.
97
+ * resume: attach to existing Island DOM without re-running construct structure or onMount.
80
98
  * Consumes ResumeEntry product (`data-vmz-resume`) derived from the same Execution Plan.
81
99
  * @param {new (props?: object) => any} Component
82
100
  * @param {HTMLElement} container
@@ -105,12 +123,12 @@ export declare function hydrate(Component: any, container: any, props?: {}, opts
105
123
  /**
106
124
  * Tear down binders and stop patches. Safe to call more than once.
107
125
  * Field writes after destroy no longer update DOM (values may still change).
108
- * L4: also dispose owned DOM trees (child __vmzInst / region __vmzDispose).
126
+ *: also dispose owned DOM trees (child __vmzInst / region __vmzDispose).
109
127
  * @param {object} inst
110
128
  */
111
129
  export declare function destroy(inst: any): void;
112
130
  /**
113
- * L4: walk a DOM subtree and run lifetime dispose hooks + nested instance destroy.
131
+ *: walk a DOM subtree and run lifetime dispose hooks + nested instance destroy.
114
132
  * Does not mark the *calling* parent destroyed; safe from destroy(inst).
115
133
  * @param {Node | null | undefined} root
116
134
  */
@@ -135,7 +153,6 @@ export declare function __vmzCancelTasks(inst: any): void;
135
153
  export declare function __vmzTaskStatus(inst: any, key: any): any;
136
154
  /**
137
155
  * Mark a plain object as intentionally shared across ownership boundaries.
138
- * Suppresses cross-component shared diagnostics (规划设计/vmz/13 §7.3).
139
156
  * @param {any} value
140
157
  */
141
158
  export declare function __vmzAllowShared(value: any): any;
@@ -167,7 +184,6 @@ export declare function __vmzReadPath(inst: any, root: any, segs: any): any;
167
184
  */
168
185
  export declare function __vmzWritePathLogical(inst: any, root: any, segs: any, kind: any, rhs: any): any;
169
186
  /**
170
- * Compiler-inserted path write barrier (规划设计/vmz/13 §7.3).
171
187
  * Mutates a plain owned object/array and schedules the same path notice Proxy would.
172
188
  *
173
189
  * Root-array index assigns (`tags[0] = x`) notify as field replace (structural),
@@ -193,7 +209,7 @@ export declare function __vmzWritePath(inst: any, root: any, segs: any, value: a
193
209
  */
194
210
  export declare function __vmzArrayMutate(inst: any, root: any, baseSegs: any, method: any, args: any): any;
195
211
  /**
196
- * L4 WriteBarrier: true when value is an owned plain object with path barriers (no Proxy).
212
+ * WriteBarrier: true when value is an owned plain object with path barriers (no Proxy).
197
213
  * @param {any} value
198
214
  */
199
215
  export declare function __vmzIsWriteBarrierOwned(value: any): boolean;