@vmz/core 0.3.13 → 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.
- package/dist/browser/client-nav.types.d.ts +2 -5
- package/dist/browser/direct-api.types.d.ts +6 -0
- package/dist/browser/dom/attributes.js +142 -2
- package/dist/browser/dom/direct-api.d.ts +43 -3
- package/dist/browser/dom/direct-api.js +421 -84
- package/dist/browser/dom-core.types.d.ts +10 -2
- package/dist/browser/navigation/client-navigation.js +18 -30
- package/dist/browser/reactivity/write-barrier.js +5 -1
- package/dist/browser/resume/adopt.js +142 -10
- package/dist/browser/resume/hydrate.js +34 -0
- package/dist/host/deployment/deployment-registry.js +5 -2
- package/dist/host/render/render-host.js +2 -0
- package/dist/host/serve/bootstrap.js +8 -4
- package/dist/host/serve/hmr-dev.d.ts +16 -17
- package/dist/host/serve/hmr-dev.js +96 -35
- package/dist/host/serve/http.js +25 -8
- package/dist/host/serve/pack-entry.d.ts +21 -0
- package/dist/host/serve/pack-entry.js +78 -7
- package/dist/host/serve/page-render.js +10 -8
- package/dist/shared/dom-attr-normalize.d.ts +6 -0
- package/dist/shared/dom-attr-normalize.js +1 -1
- package/dist/shared/host.types.d.ts +9 -0
- package/dist/shared/html-digest.d.ts +15 -0
- package/dist/shared/html-digest.js +25 -0
- package/dist/ssr/serialize.js +305 -9
- package/package.json +7 -7
|
@@ -39,9 +39,6 @@ export function installClientNavigation(opts = {}) {
|
|
|
39
39
|
else if (win?.vmzDestroy)
|
|
40
40
|
win.vmzDestroy(inst);
|
|
41
41
|
}
|
|
42
|
-
async function loadDomFallback() {
|
|
43
|
-
return import(/* @vite-ignore */ '/dom.browser.js');
|
|
44
|
-
}
|
|
45
42
|
async function transitionTo(url, { replace = false, fromPop = false, softFail = false } = {}) {
|
|
46
43
|
const target = new URL(url, loc.href);
|
|
47
44
|
if (target.origin !== loc.origin) {
|
|
@@ -98,7 +95,12 @@ export function installClientNavigation(opts = {}) {
|
|
|
98
95
|
else
|
|
99
96
|
hist.pushState({ vmzClientNav: true }, '', target.href);
|
|
100
97
|
}
|
|
101
|
-
const importChunk =
|
|
98
|
+
const importChunk = (id) => {
|
|
99
|
+
if (typeof opts.importPage !== 'function') {
|
|
100
|
+
throw new Error('vmz:client-nav missing importPage — the generated entry must pass the pack manifest table');
|
|
101
|
+
}
|
|
102
|
+
return opts.importPage(id);
|
|
103
|
+
};
|
|
102
104
|
let liveRoot = root;
|
|
103
105
|
let retainedLayout = false;
|
|
104
106
|
applyLocaleRealization(doc, nextApp);
|
|
@@ -106,28 +108,21 @@ export function installClientNavigation(opts = {}) {
|
|
|
106
108
|
applyAppAttrs(root, nextApp);
|
|
107
109
|
if (chunkId) {
|
|
108
110
|
const Page = await importChunk(chunkId);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const dom = await loadDomFallback();
|
|
112
|
-
hydrateRoutePage = dom.hydrateRoutePage;
|
|
113
|
-
}
|
|
114
|
-
if (typeof hydrateRoutePage === 'function') {
|
|
115
|
-
await hydrateRoutePage(Page, root, props);
|
|
111
|
+
if (typeof opts.hydrateRoutePage === 'function') {
|
|
112
|
+
await opts.hydrateRoutePage(Page, root, props);
|
|
116
113
|
}
|
|
117
|
-
else {
|
|
118
|
-
let hydrate = opts.hydrate;
|
|
119
|
-
if (typeof hydrate !== 'function') {
|
|
120
|
-
const dom = await loadDomFallback();
|
|
121
|
-
hydrate = dom.hydrate;
|
|
122
|
-
}
|
|
114
|
+
else if (typeof opts.hydrate === 'function') {
|
|
123
115
|
const pageHost = root.__vmzPageHost || root;
|
|
124
116
|
if (pageHost.__vmzInst)
|
|
125
117
|
destroyInst(pageHost.__vmzInst);
|
|
126
|
-
await hydrate(Page, pageHost, props);
|
|
118
|
+
await opts.hydrate(Page, pageHost, props);
|
|
127
119
|
root.__vmzPageHost = pageHost;
|
|
128
120
|
if (!root.__vmzLayoutInsts?.length)
|
|
129
121
|
root.__vmzInst = pageHost.__vmzInst;
|
|
130
122
|
}
|
|
123
|
+
else {
|
|
124
|
+
throw new Error('vmz:client-nav missing hydrateRoutePage and hydrate — the generated entry must pass the packed face');
|
|
125
|
+
}
|
|
131
126
|
}
|
|
132
127
|
retainedLayout = true;
|
|
133
128
|
}
|
|
@@ -148,21 +143,14 @@ export function installClientNavigation(opts = {}) {
|
|
|
148
143
|
for (const id of nextLayout) {
|
|
149
144
|
layoutCtors.push(await importChunk(id));
|
|
150
145
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const dom = await loadDomFallback();
|
|
154
|
-
hydrateRoute = dom.hydrateRoute;
|
|
146
|
+
if (typeof opts.hydrateRoute === 'function') {
|
|
147
|
+
await opts.hydrateRoute(Page, fresh, props, layoutCtors);
|
|
155
148
|
}
|
|
156
|
-
if (typeof
|
|
157
|
-
await
|
|
149
|
+
else if (typeof opts.hydrate === 'function') {
|
|
150
|
+
await opts.hydrate(Page, fresh, props);
|
|
158
151
|
}
|
|
159
152
|
else {
|
|
160
|
-
|
|
161
|
-
if (typeof hydrate !== 'function') {
|
|
162
|
-
const dom = await loadDomFallback();
|
|
163
|
-
hydrate = dom.hydrate;
|
|
164
|
-
}
|
|
165
|
-
await hydrate(Page, fresh, props);
|
|
153
|
+
throw new Error('vmz:client-nav missing hydrateRoute and hydrate — the generated entry must pass the packed face');
|
|
166
154
|
}
|
|
167
155
|
}
|
|
168
156
|
}
|
|
@@ -187,7 +187,11 @@ export function __vmzWritePath(inst, root, segs, value) {
|
|
|
187
187
|
const rootObj = inst[root];
|
|
188
188
|
const rootArr = rootObj;
|
|
189
189
|
const isRootIndex = normSegs.length === 1 && Array.isArray(rootArr) && leaf !== 'length' && String(Number(leaf)) === leaf;
|
|
190
|
-
|
|
190
|
+
// Root `rows.length = n` truncates or grows the list itself: the bare `rows`
|
|
191
|
+
// structure depot only matches a replace notice, so a path notice on
|
|
192
|
+
// `rows.length` leaves the rendered rows stale.
|
|
193
|
+
const isRootLength = normSegs.length === 1 && Array.isArray(rootArr) && leaf === 'length';
|
|
194
|
+
if (isRootIndex || isRootLength) {
|
|
191
195
|
if (!notifyWbShared(rootObj, null)) {
|
|
192
196
|
scheduleRefresh(inst, { type: 'replace', root });
|
|
193
197
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { directApi, noteDomCreate, runDirectCreate, } from '../dom-core.js';
|
|
1
|
+
import { directApi, findOwnedDefaultSlot, noteDomCreate, runDirectCreate, } from '../dom-core.js';
|
|
2
|
+
import { HTML_HASH_ATTR } from '../../shared/html-digest.js';
|
|
2
3
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
3
4
|
function createResumeAdopt(pool, rootEl) {
|
|
4
5
|
const used = new WeakSet();
|
|
5
6
|
const scopeStack = [pool];
|
|
6
7
|
const currentScope = () => scopeStack[scopeStack.length - 1] || pool;
|
|
8
|
+
/** `rootEl`'s children were parked into `pool` before create — see bottom. */
|
|
9
|
+
let rootElTookPool = false;
|
|
7
10
|
const markElement = (n) => {
|
|
8
11
|
used.add(n);
|
|
9
12
|
if (typeof n.getAttribute === 'function') {
|
|
@@ -11,11 +14,22 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
11
14
|
if (k != null && n.__vmzKey == null)
|
|
12
15
|
n.__vmzKey = k;
|
|
13
16
|
}
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// `runDirectResume` parks `rootEl`'s children into the top-level pool
|
|
18
|
+
// before create. Adopting `rootEl` must hand that same pool to it as the
|
|
19
|
+
// child scope: making a fresh (empty) child pool here would leave every
|
|
20
|
+
// SSR descendant unreachable and silently recreate the whole subtree.
|
|
21
|
+
let childPool;
|
|
22
|
+
if (n === rootEl && !rootElTookPool) {
|
|
23
|
+
rootElTookPool = true;
|
|
24
|
+
childPool = pool;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Private child pool ??never dump into the parent/global pool (that let
|
|
28
|
+
// if branches reclaim Drawer/sibling hosts parked as uncles).
|
|
29
|
+
childPool = document.createDocumentFragment();
|
|
30
|
+
while (n.firstChild)
|
|
31
|
+
childPool.appendChild(n.firstChild);
|
|
32
|
+
}
|
|
19
33
|
n.__vmzResumePool = childPool;
|
|
20
34
|
return n;
|
|
21
35
|
};
|
|
@@ -44,14 +58,40 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
44
58
|
yield n;
|
|
45
59
|
}
|
|
46
60
|
}
|
|
47
|
-
const adoptElement = (tag, svg = false) => {
|
|
61
|
+
const adoptElement = (tag, svg = false, key = null) => {
|
|
48
62
|
const want = String(tag || 'div').toLowerCase();
|
|
63
|
+
// Keyed non-kernel rows (`component`, `v-once`, `v-memo`) carry their key on
|
|
64
|
+
// the root, so reclaim by identity. Falling back to tag order reuses a node
|
|
65
|
+
// that belongs to another key as soon as list order diverges from SSR.
|
|
66
|
+
if (key != null) {
|
|
67
|
+
const wanted = String(key);
|
|
68
|
+
for (const el of elementCandidates()) {
|
|
69
|
+
if (String(el.tagName).toLowerCase() !== want)
|
|
70
|
+
continue;
|
|
71
|
+
if (typeof el.getAttribute !== 'function')
|
|
72
|
+
continue;
|
|
73
|
+
if (el.getAttribute('data-vmz-key') !== wanted)
|
|
74
|
+
continue;
|
|
75
|
+
return markElement(el);
|
|
76
|
+
}
|
|
77
|
+
noteDomCreate();
|
|
78
|
+
const fresh = svg ? document.createElementNS(SVG_NS, tag || 'svg') : document.createElement(tag || 'div');
|
|
79
|
+
fresh.__vmzKey = wanted;
|
|
80
|
+
return fresh;
|
|
81
|
+
}
|
|
49
82
|
for (const el of elementCandidates()) {
|
|
50
83
|
if (String(el.tagName).toLowerCase() === want)
|
|
51
84
|
return markElement(el);
|
|
52
85
|
}
|
|
53
86
|
noteDomCreate();
|
|
54
|
-
|
|
87
|
+
const fresh = svg ? document.createElementNS(SVG_NS, tag || 'svg') : document.createElement(tag || 'div');
|
|
88
|
+
// Direct SSR flattens `<slot/>` away and writes the projected children in
|
|
89
|
+
// place, so a rebuilt outlet has no SSR counterpart: its projection stays
|
|
90
|
+
// in the enclosing scope. Share that scope as the outlet pool so a later
|
|
91
|
+
// `adoptEnter(host)` can still reclaim the projected subtree.
|
|
92
|
+
if (want === 'slot')
|
|
93
|
+
fresh.__vmzResumePool = currentScope();
|
|
94
|
+
return fresh;
|
|
55
95
|
};
|
|
56
96
|
const adoptElementNS = (ns, tag) => {
|
|
57
97
|
const want = String(tag || 'g');
|
|
@@ -62,6 +102,43 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
62
102
|
noteDomCreate();
|
|
63
103
|
return document.createElementNS(ns || SVG_NS, want);
|
|
64
104
|
};
|
|
105
|
+
/**
|
|
106
|
+
* Reclaim the SSR row root for one keyed-each key.
|
|
107
|
+
*
|
|
108
|
+
* Tag-order adoption cannot distinguish rows when the client list is already
|
|
109
|
+
* ahead of the SSR order (persisted state, re-filtered store): the first
|
|
110
|
+
* `<li>` would be reused for whichever key the client renders first, so the
|
|
111
|
+
* retained DOM node (focus, caret, typed value) belongs to another key.
|
|
112
|
+
* Keyed rows carry `data-vmz-key` on the server, so match it exactly and leave
|
|
113
|
+
* unmatched SSR rows for the pool teardown. `null` means "not present in SSR"
|
|
114
|
+
* and the caller creates a fresh row.
|
|
115
|
+
*/
|
|
116
|
+
const claimKeyed = (tag, key) => {
|
|
117
|
+
if (key == null)
|
|
118
|
+
return null;
|
|
119
|
+
const want = String(tag || '').toLowerCase();
|
|
120
|
+
const wanted = String(key);
|
|
121
|
+
if (!want)
|
|
122
|
+
return null;
|
|
123
|
+
for (const el of elementCandidates()) {
|
|
124
|
+
if (String(el.tagName).toLowerCase() !== want)
|
|
125
|
+
continue;
|
|
126
|
+
if (typeof el.getAttribute !== 'function')
|
|
127
|
+
continue;
|
|
128
|
+
if (el.getAttribute('data-vmz-key') !== wanted)
|
|
129
|
+
continue;
|
|
130
|
+
// Row kernels own the whole row subtree (no nested each/component), and
|
|
131
|
+
// `hydrate` reads `root.firstChild` for its text slots. Do not stash the
|
|
132
|
+
// row's children into a resume pool here: the row must keep its SSR
|
|
133
|
+
// structure as-is.
|
|
134
|
+
used.add(el);
|
|
135
|
+
const k = el.getAttribute('data-vmz-key');
|
|
136
|
+
if (k != null && el.__vmzKey == null)
|
|
137
|
+
el.__vmzKey = k;
|
|
138
|
+
return el;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
};
|
|
65
142
|
const adoptText = (value) => {
|
|
66
143
|
for (const n of textCandidates()) {
|
|
67
144
|
used.add(n);
|
|
@@ -72,19 +149,45 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
72
149
|
noteDomCreate();
|
|
73
150
|
return document.createTextNode(value == null ? '' : String(value));
|
|
74
151
|
};
|
|
75
|
-
const componentHost = (name) => {
|
|
152
|
+
const componentHost = (name, key = null) => {
|
|
76
153
|
const want = String(name || '');
|
|
154
|
+
const wanted = key == null ? null : String(key);
|
|
77
155
|
for (const el of elementCandidates()) {
|
|
78
156
|
if (String(el.tagName).toLowerCase() !== 'div')
|
|
79
157
|
continue;
|
|
80
158
|
if (el.getAttribute('data-vmz') !== want)
|
|
81
159
|
continue;
|
|
160
|
+
if (wanted != null && el.getAttribute('data-vmz-key') !== wanted)
|
|
161
|
+
continue;
|
|
82
162
|
return markElement(el);
|
|
83
163
|
}
|
|
84
164
|
return null;
|
|
85
165
|
};
|
|
166
|
+
/** Any parked SSR node in `frag` that no earlier create has claimed. */
|
|
167
|
+
const hasUnusedNode = (frag) => {
|
|
168
|
+
for (const n of frag.childNodes) {
|
|
169
|
+
if (!used.has(n))
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
};
|
|
86
174
|
const enter = (node) => {
|
|
87
|
-
if (!node
|
|
175
|
+
if (!node)
|
|
176
|
+
return false;
|
|
177
|
+
// `adoptEnter(host)` in the parent template wraps the *projection*, not the
|
|
178
|
+
// component's own internals (its host pool was consumed by the component
|
|
179
|
+
// create). The parked projection lives under the default `<slot/>` outlet,
|
|
180
|
+
// so entering the host must push that outlet's pool.
|
|
181
|
+
const owner = node.__vmzInst;
|
|
182
|
+
if (owner) {
|
|
183
|
+
const slot = findOwnedDefaultSlot(owner.__vmzDomRoot);
|
|
184
|
+
const slotPool = slot && slot.__vmzResumePool;
|
|
185
|
+
if (slotPool && hasUnusedNode(slotPool)) {
|
|
186
|
+
scopeStack.push(slotPool);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!node.__vmzResumePool)
|
|
88
191
|
return false;
|
|
89
192
|
scopeStack.push(node.__vmzResumePool);
|
|
90
193
|
return true;
|
|
@@ -121,9 +224,37 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
121
224
|
rewindScope(depth - 1);
|
|
122
225
|
};
|
|
123
226
|
};
|
|
227
|
+
/**
|
|
228
|
+
* `v-html` on a reclaimed element. `markElement` parked its SSR children, so
|
|
229
|
+
* the live element reads empty. SSR stamped the digest of the exact payload
|
|
230
|
+
* string it wrote on the host (`HTML_HASH_ATTR`), so compare that marker with
|
|
231
|
+
* the digest of the string this create would write: equal means the parked
|
|
232
|
+
* subtree already *is* that payload, and moving it back keeps every SSR node.
|
|
233
|
+
* A different digest drops the parked payload so the normal write replaces it.
|
|
234
|
+
*/
|
|
235
|
+
const reclaimHtml = (el, digest) => {
|
|
236
|
+
if (!used.has(el))
|
|
237
|
+
return false;
|
|
238
|
+
const childPool = el.__vmzResumePool;
|
|
239
|
+
const stamp = String(el.getAttribute(HTML_HASH_ATTR) || '');
|
|
240
|
+
if (stamp !== digest) {
|
|
241
|
+
// A different payload replaces this host, so parked SSR children can
|
|
242
|
+
// never be adopted afterwards. Drop them now instead of leaving a
|
|
243
|
+
// stale pool for a later identical patch to resurrect.
|
|
244
|
+
if (childPool)
|
|
245
|
+
while (childPool.firstChild)
|
|
246
|
+
childPool.removeChild(childPool.firstChild);
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (childPool)
|
|
250
|
+
while (childPool.firstChild)
|
|
251
|
+
el.appendChild(childPool.firstChild);
|
|
252
|
+
return true;
|
|
253
|
+
};
|
|
124
254
|
return {
|
|
125
255
|
el: adoptElement,
|
|
126
256
|
elNS: adoptElementNS,
|
|
257
|
+
claimKeyed,
|
|
127
258
|
text: adoptText,
|
|
128
259
|
componentHost,
|
|
129
260
|
enter,
|
|
@@ -131,6 +262,7 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
131
262
|
scopeDepth,
|
|
132
263
|
rewindScope,
|
|
133
264
|
beginBranchScope,
|
|
265
|
+
reclaimHtml,
|
|
134
266
|
};
|
|
135
267
|
}
|
|
136
268
|
export function runDirectResume(Component, inst, container) {
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { adoptTopLevelDomRoot, applyPreservedState, createInstance, destroy, findOwnedDefaultSlot, hasMeaningfulChild, mount, runDirectCreate, settlePendingChildMounts, snapshotInstanceState, } from '../dom-core.js';
|
|
2
2
|
import { runDirectResume } from './adopt.js';
|
|
3
3
|
import { resumeIslands } from './resume.js';
|
|
4
|
+
/**
|
|
5
|
+
* A layout whose whole template is a default `<slot/>` contributes no DOM of its
|
|
6
|
+
* own: SSR renders the page straight into the route container. Mounting such a
|
|
7
|
+
* layout would `replaceChildren()` the SSR tree, so the page hydrate that follows
|
|
8
|
+
* always sees an empty host and recreates every node (verified on the homepage and
|
|
9
|
+
* charts route: 0 of 336 / 0 of 18743 SSR nodes were reused).
|
|
10
|
+
*
|
|
11
|
+
* Only trusted for layouts with no state, props, or lifecycle, so dropping the
|
|
12
|
+
* instance cannot change behavior — a pass-through layout has nothing to run.
|
|
13
|
+
*/
|
|
14
|
+
function isTransparentSlotLayout(Layout) {
|
|
15
|
+
const plan = Layout && Layout.__vmzPlan;
|
|
16
|
+
if (!plan || !Array.isArray(plan.root_ids) || !Array.isArray(plan.nodes) || plan.root_ids.length !== 1) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const root = plan.nodes.find((n) => n && n.id === plan.root_ids[0]);
|
|
20
|
+
if (!root || root.kind !== 'slot')
|
|
21
|
+
return false;
|
|
22
|
+
if (Array.isArray(Layout.__vmzState) && Layout.__vmzState.length)
|
|
23
|
+
return false;
|
|
24
|
+
if (Array.isArray(Layout.__vmzProps) && Layout.__vmzProps.length)
|
|
25
|
+
return false;
|
|
26
|
+
const proto = Layout.prototype;
|
|
27
|
+
return !(proto && (typeof proto.onMount === 'function' || typeof proto.onDestroy === 'function'));
|
|
28
|
+
}
|
|
4
29
|
export async function hydrateRoute(Page, container, props = {}, layoutCtors = [], opts = {}) {
|
|
5
30
|
if (typeof document === 'undefined') {
|
|
6
31
|
throw new Error('vmz:dom hydrateRoute() requires a document (browser)');
|
|
@@ -14,6 +39,15 @@ export async function hydrateRoute(Page, container, props = {}, layoutCtors = []
|
|
|
14
39
|
const layoutInsts = [];
|
|
15
40
|
let host = container;
|
|
16
41
|
const ctors = Array.isArray(layoutCtors) ? layoutCtors.filter(Boolean) : [];
|
|
42
|
+
// Pass-through layout chain: hydrate the page into the SSR container directly so
|
|
43
|
+
// the rendered subtree becomes the adopt pool instead of being wiped by mount().
|
|
44
|
+
if (ctors.length && ctors.every(isTransparentSlotLayout) && hasMeaningfulChild(container)) {
|
|
45
|
+
const pageInst = await hydrate(Page, container, props, opts);
|
|
46
|
+
container.__vmzPageHost = container;
|
|
47
|
+
container.__vmzLayoutInsts = [];
|
|
48
|
+
container.__vmzInst = pageInst;
|
|
49
|
+
return pageInst;
|
|
50
|
+
}
|
|
17
51
|
for (const Layout of ctors) {
|
|
18
52
|
const inst = await mount(Layout, host, {});
|
|
19
53
|
layoutInsts.push(inst);
|
|
@@ -97,14 +97,17 @@ export async function loadComponentEntries(distDir, opts = {}) {
|
|
|
97
97
|
export async function importAndRegisterComponentEntries(distDir, entries, registerComponents, opts = {}) {
|
|
98
98
|
const map = {};
|
|
99
99
|
const loaded = opts.loaded ?? null;
|
|
100
|
-
const packNode = readPackNodeManifest(distDir);
|
|
100
|
+
const packNode = opts.unpacked ? null : readPackNodeManifest(distDir);
|
|
101
101
|
for (const entry of entries) {
|
|
102
102
|
if (loaded && loaded.has(entry.chunkId))
|
|
103
103
|
continue;
|
|
104
104
|
// Node/SSR consumes the packed unit bytes (`_vmz/pack-node/`) whenever the
|
|
105
105
|
// build packed the node graph — bare package imports in dependency
|
|
106
106
|
// components only exist in vendored form there. Dev sessions do not pack:
|
|
107
|
-
// the unpacked `${chunkId}.client.js` remains the module identity.
|
|
107
|
+
// the unpacked `${chunkId}.client.js` remains the module identity. Client
|
|
108
|
+
// hosts pass `unpacked` and stay on that tree: units they mount come from
|
|
109
|
+
// the unpacked client graph, including client-only fixtures that no page
|
|
110
|
+
// reaches and the node graph therefore excludes.
|
|
108
111
|
const abs = packNode ? packNodeEntryAbs(distDir, entry, packNode) : path.join(distDir, entry.entry);
|
|
109
112
|
let href = pathToFileURL(abs).href;
|
|
110
113
|
if (opts.cacheBust != null && opts.cacheBust !== '') {
|
|
@@ -18,6 +18,7 @@ export async function createRenderHost(distDir, opts = {}) {
|
|
|
18
18
|
loaded: loadedChunkIds,
|
|
19
19
|
preload: opts.preload ?? 'none',
|
|
20
20
|
closureRoots: opts.closureRoots,
|
|
21
|
+
unpacked: opts.unpackedEntries === true,
|
|
21
22
|
};
|
|
22
23
|
if (bootstrapOpts.preload !== 'none') {
|
|
23
24
|
await bootstrapComponentRegistry(distDir, dom.registerComponents, bootstrapOpts);
|
|
@@ -36,6 +37,7 @@ export async function createRenderHost(distDir, opts = {}) {
|
|
|
36
37
|
return importAndRegisterComponentEntries(distDir, entries, dom.registerComponents, {
|
|
37
38
|
cacheBust: opts.cacheBust,
|
|
38
39
|
loaded: loadedChunkIds,
|
|
40
|
+
unpacked: opts.unpackedEntries === true,
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
import { setServerModuleResolver } from '../../faces/vmz-runtime.js';
|
|
4
|
-
import { installDevImportTokenHooks, normalizeDevError,
|
|
4
|
+
import { commitReload, installDevImportTokenHooks, normalizeDevError, prepareReload } from './hmr-dev.js';
|
|
5
5
|
import { bustUrl } from './page-catalog.js';
|
|
6
6
|
import { installAppModuleResolveHooks } from './paths.js';
|
|
7
7
|
import { serveState } from './state.js';
|
|
@@ -13,13 +13,17 @@ export async function bootstrapServeHost() {
|
|
|
13
13
|
const rel = moduleId.replace(/^#server\//, '') + '.js';
|
|
14
14
|
return bustUrl(pathToFileURL(path.join(serveState.distDir, '#server', rel)).href);
|
|
15
15
|
});
|
|
16
|
+
// dev:入口模块写好后必须由 CLI 侧 pack 再 commit,host 自己不能置 ready。
|
|
17
|
+
// 生产 serve:pack 已在构建期完成,直接 prepare + commit。
|
|
18
|
+
if (serveState.isDev)
|
|
19
|
+
return;
|
|
16
20
|
try {
|
|
17
|
-
await
|
|
18
|
-
|
|
21
|
+
await prepareReload({ quiet: true });
|
|
22
|
+
commitReload();
|
|
19
23
|
}
|
|
20
24
|
catch (err) {
|
|
21
25
|
serveState.lastDevError = normalizeDevError(err);
|
|
22
26
|
serveState.ready = true;
|
|
23
|
-
console.error('vmz serve: initial load failed (
|
|
27
|
+
console.error('vmz serve: initial load failed (host stays up)', serveState.lastDevError.message);
|
|
24
28
|
}
|
|
25
29
|
}
|
|
@@ -13,22 +13,21 @@ export declare function emitDevErrorHtml(err: {
|
|
|
13
13
|
message?: string;
|
|
14
14
|
stack?: string;
|
|
15
15
|
}): AsyncGenerator<any, void, unknown>;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* 第一阶段:加载构建产物、写客户端入口模块,但**不**发布 SSE、不置 ready。
|
|
18
|
+
*
|
|
19
|
+
* dev 的浏览器交付必须经 `vmz-pack`,而 pack 由 CLI 侧执行(runtime 不反向
|
|
20
|
+
* 依赖 `@vmz/vmz`)。因此先把入口写到 outDir,让调用方 pack,再 `commitReload`。
|
|
21
|
+
*/
|
|
22
|
+
export declare function prepareReload(opts?: {
|
|
17
23
|
quiet?: boolean;
|
|
18
24
|
payload?: Record<string, unknown>;
|
|
19
|
-
}): Promise<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
reloadAllPages: boolean;
|
|
29
|
-
buildId: string;
|
|
30
|
-
sourceRevision: string;
|
|
31
|
-
bundleRevision: string;
|
|
32
|
-
serveRevision: string;
|
|
33
|
-
token: number;
|
|
34
|
-
}>;
|
|
25
|
+
}): Promise<Record<string, unknown>>;
|
|
26
|
+
/**
|
|
27
|
+
* 第二阶段:发布 SSE 并置 ready。island 热更新的地址表从 pack manifest 精确
|
|
28
|
+
* 查得(逻辑 `chunkId` → chunk 文件、含 `dom.browser` 的共享 chunk),不做后缀
|
|
29
|
+
* 猜测;查不到即整页 reload。
|
|
30
|
+
*/
|
|
31
|
+
export declare function commitReload(): Record<string, unknown>;
|
|
32
|
+
/** pack 失败:回滚到上一轮已发布状态,并以 SSE error 显式告知浏览器。 */
|
|
33
|
+
export declare function abortReload(err: unknown): void;
|