@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 +4 -2
- package/dist/client-nav.d.ts +46 -0
- package/dist/client-nav.js +186 -0
- package/dist/dom.d.ts +28 -12
- package/dist/dom.js +1540 -226
- package/dist/serve-host.mjs +546 -90
- package/dist/server.d.ts +1 -2
- package/dist/server.js +103 -18
- package/package.json +6 -2
- package/dist/serve-host.js +0 -736
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.”
|
|
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
|
|
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
|
-
*
|
|
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` (
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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;
|