@vmz/core 0.3.12 → 0.3.13
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/dom/component.d.ts +10 -0
- package/dist/browser/dom/component.js +16 -4
- package/dist/browser/dom/direct-api.js +1 -1
- package/dist/browser/dom-core.d.ts +1 -0
- package/dist/browser/dom-core.js +1 -0
- package/dist/browser/reactivity/write-barrier.js +33 -10
- package/dist/browser/resume/hydrate.js +2 -5
- package/dist/browser/resume/resume.js +2 -5
- package/dist/host/deployment/deployment-registry.js +44 -1
- package/dist/host/serve/hmr-dev.js +22 -4
- package/dist/host/serve/pack-entry.d.ts +10 -0
- package/dist/host/serve/pack-entry.js +27 -0
- package/dist/host/serve/page-render.js +7 -1
- package/dist/shared/dom-attr-normalize.d.ts +8 -0
- package/dist/shared/dom-attr-normalize.js +158 -19
- package/dist/ssr/onmount.d.ts +37 -0
- package/dist/ssr/onmount.js +94 -0
- package/dist/ssr/render.js +7 -8
- package/dist/ssr/serialize.d.ts +1 -1
- package/dist/ssr/serialize.js +4 -4
- package/package.json +7 -7
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export declare function mount(Component: any, container: any, props?: any): Promise<any>;
|
|
2
|
+
/**
|
|
3
|
+
* Root ownership for top-level creates (`mount` / `hydrate` / `resume`).
|
|
4
|
+
*
|
|
5
|
+
* Multi-root templates compile to a DocumentFragment; `appendChild` moves the
|
|
6
|
+
* children into the container and leaves the fragment empty and detached. The
|
|
7
|
+
* instance root must track the live container in that case — the same contract
|
|
8
|
+
* nested component hosts already follow (`nodeType === 11 ? host : node`) — or
|
|
9
|
+
* slot outlet searches and dispose walks run against a detached tree.
|
|
10
|
+
*/
|
|
11
|
+
export declare function adoptTopLevelDomRoot(inst: any, node: any, container: any): void;
|
|
2
12
|
export declare function settlePendingChildMounts(inst: any): Promise<void>;
|
|
3
13
|
export declare function runDirectCreate(Component: any, inst: any): any;
|
|
4
14
|
export declare function findOwnedDefaultSlot(root: any): any;
|
|
@@ -15,10 +15,7 @@ export async function mount(Component, container, props = {}) {
|
|
|
15
15
|
inst.__vmzDepToBindings = Object.create(null);
|
|
16
16
|
container.replaceChildren();
|
|
17
17
|
const node = await createFromComponent(Component, inst);
|
|
18
|
-
|
|
19
|
-
inst.__vmzDomRoot = node;
|
|
20
|
-
container.appendChild(node);
|
|
21
|
-
}
|
|
18
|
+
adoptTopLevelDomRoot(inst, node, container);
|
|
22
19
|
if (typeof inst.onMount === 'function') {
|
|
23
20
|
await inst.onMount();
|
|
24
21
|
}
|
|
@@ -26,6 +23,21 @@ export async function mount(Component, container, props = {}) {
|
|
|
26
23
|
container.__vmzInst = inst;
|
|
27
24
|
return inst;
|
|
28
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Root ownership for top-level creates (`mount` / `hydrate` / `resume`).
|
|
28
|
+
*
|
|
29
|
+
* Multi-root templates compile to a DocumentFragment; `appendChild` moves the
|
|
30
|
+
* children into the container and leaves the fragment empty and detached. The
|
|
31
|
+
* instance root must track the live container in that case — the same contract
|
|
32
|
+
* nested component hosts already follow (`nodeType === 11 ? host : node`) — or
|
|
33
|
+
* slot outlet searches and dispose walks run against a detached tree.
|
|
34
|
+
*/
|
|
35
|
+
export function adoptTopLevelDomRoot(inst, node, container) {
|
|
36
|
+
if (!node)
|
|
37
|
+
return;
|
|
38
|
+
inst.__vmzDomRoot = node.nodeType === 11 ? container : node;
|
|
39
|
+
container.appendChild(node);
|
|
40
|
+
}
|
|
29
41
|
export async function settlePendingChildMounts(inst) {
|
|
30
42
|
if (!inst || !Array.isArray(inst.__vmzPendingChildMounts) || !inst.__vmzPendingChildMounts.length)
|
|
31
43
|
return;
|
|
@@ -398,7 +398,7 @@ export const directApi = {
|
|
|
398
398
|
host.replaceWith(err);
|
|
399
399
|
return;
|
|
400
400
|
}
|
|
401
|
-
const { resume } = await import('
|
|
401
|
+
const { resume } = await import('../resume/resume.js');
|
|
402
402
|
await resume(Ctor, host, { props: resolved, state: {} });
|
|
403
403
|
});
|
|
404
404
|
return host;
|
|
@@ -12,6 +12,7 @@ export { registerComponents } from './dom/registry.js';
|
|
|
12
12
|
export { getRegisteredComponent } from './dom/registry.js';
|
|
13
13
|
export { resolveComponent } from './dom/registry.js';
|
|
14
14
|
export { mount } from './dom/component.js';
|
|
15
|
+
export { adoptTopLevelDomRoot } from './dom/component.js';
|
|
15
16
|
export { settlePendingChildMounts } from './dom/component.js';
|
|
16
17
|
export { runDirectCreate } from './dom/component.js';
|
|
17
18
|
export { findOwnedDefaultSlot } from './dom/component.js';
|
package/dist/browser/dom-core.js
CHANGED
|
@@ -12,6 +12,7 @@ export { registerComponents } from './dom/registry.js';
|
|
|
12
12
|
export { getRegisteredComponent } from './dom/registry.js';
|
|
13
13
|
export { resolveComponent } from './dom/registry.js';
|
|
14
14
|
export { mount } from './dom/component.js';
|
|
15
|
+
export { adoptTopLevelDomRoot } from './dom/component.js';
|
|
15
16
|
export { settlePendingChildMounts } from './dom/component.js';
|
|
16
17
|
export { runDirectCreate } from './dom/component.js';
|
|
17
18
|
export { findOwnedDefaultSlot } from './dom/component.js';
|
|
@@ -71,6 +71,7 @@ export function __vmzReadPath(inst, root, segs) {
|
|
|
71
71
|
return obj;
|
|
72
72
|
}
|
|
73
73
|
export function __vmzWritePathLogical(inst, root, segs, kind, rhs) {
|
|
74
|
+
assertWritePathSegs('__vmzWritePathLogical', root, segs);
|
|
74
75
|
const cur = __vmzReadPath(inst, root, segs);
|
|
75
76
|
if (kind === '||') {
|
|
76
77
|
if (cur)
|
|
@@ -139,16 +140,26 @@ function notifyArrayItemLeaf(inst, root, idx, leaf) {
|
|
|
139
140
|
}
|
|
140
141
|
scheduleRefresh(inst, { type: 'path', root, segs: [String(idx), leaf] });
|
|
141
142
|
}
|
|
143
|
+
function assertWritePathSegs(fn, root, segs) {
|
|
144
|
+
if (!root)
|
|
145
|
+
return;
|
|
146
|
+
if (!Array.isArray(segs) || segs.length === 0) {
|
|
147
|
+
throw new Error(`vmz:dom ${fn} requires a non-empty path under root ${JSON.stringify(root)}; root field writes must stay plain assignments so the field setter reports them`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
142
150
|
export function __vmzWritePath(inst, root, segs, value) {
|
|
143
151
|
if (!inst || inst.__vmzDestroyed)
|
|
144
152
|
return value;
|
|
145
|
-
|
|
153
|
+
assertWritePathSegs('__vmzWritePath', root, segs);
|
|
154
|
+
if (!root)
|
|
146
155
|
return value;
|
|
156
|
+
const normSegs = segs.map((s) => String(s));
|
|
147
157
|
// Hot path: array item field write (`rows[i].label`) ??no map/slice, no shared-owner walk.
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
// Only a real array root takes the item fast path; a two-segment object path
|
|
159
|
+
// (`user.address.city`) must stay on the generic walk or the write is dropped.
|
|
160
|
+
if (normSegs.length === 2 && Array.isArray(inst[root])) {
|
|
161
|
+
return __vmzWritePathItem(inst, root, normSegs[0], normSegs[1], value);
|
|
150
162
|
}
|
|
151
|
-
const normSegs = segs.map((s) => String(s));
|
|
152
163
|
let obj = inst[root];
|
|
153
164
|
if (obj == null || typeof obj !== 'object')
|
|
154
165
|
return value;
|
|
@@ -192,8 +203,12 @@ export function __vmzWritePathItem(inst, root, idx, leaf, value) {
|
|
|
192
203
|
if (!root || leaf == null)
|
|
193
204
|
return value;
|
|
194
205
|
const arr = inst[root];
|
|
195
|
-
if (!Array.isArray(arr))
|
|
196
|
-
|
|
206
|
+
if (!Array.isArray(arr)) {
|
|
207
|
+
// Computed member on a plain object root: same write, generic path.
|
|
208
|
+
if (arr == null || typeof arr !== 'object')
|
|
209
|
+
return value;
|
|
210
|
+
return __vmzWritePath(inst, root, [idx, leaf], value);
|
|
211
|
+
}
|
|
197
212
|
const item = arr[idx];
|
|
198
213
|
if (item == null || typeof item !== 'object')
|
|
199
214
|
return value;
|
|
@@ -226,9 +241,10 @@ export function __vmzListTranspose(inst, root, ia, ib) {
|
|
|
226
241
|
export function __vmzWritePathCompound(inst, root, segs, op, rhs) {
|
|
227
242
|
if (!inst || inst.__vmzDestroyed)
|
|
228
243
|
return undefined;
|
|
229
|
-
|
|
244
|
+
assertWritePathSegs('__vmzWritePathCompound', root, segs);
|
|
245
|
+
if (!root)
|
|
230
246
|
return undefined;
|
|
231
|
-
if (segs.length === 2) {
|
|
247
|
+
if (segs.length === 2 && Array.isArray(inst[root])) {
|
|
232
248
|
return __vmzWritePathCompoundItem(inst, root, segs[0], segs[1], op, rhs);
|
|
233
249
|
}
|
|
234
250
|
const cur = __vmzReadPath(inst, root, segs);
|
|
@@ -241,8 +257,12 @@ export function __vmzWritePathCompoundItem(inst, root, idx, leaf, op, rhs) {
|
|
|
241
257
|
if (!root || leaf == null)
|
|
242
258
|
return undefined;
|
|
243
259
|
const arr = inst[root];
|
|
244
|
-
if (!Array.isArray(arr))
|
|
245
|
-
|
|
260
|
+
if (!Array.isArray(arr)) {
|
|
261
|
+
// Computed member on a plain object root: same write, generic path.
|
|
262
|
+
if (arr == null || typeof arr !== 'object')
|
|
263
|
+
return undefined;
|
|
264
|
+
return __vmzWritePathCompound(inst, root, [idx, leaf], op, rhs);
|
|
265
|
+
}
|
|
246
266
|
const item = arr[idx];
|
|
247
267
|
if (item == null || typeof item !== 'object')
|
|
248
268
|
return undefined;
|
|
@@ -333,6 +353,9 @@ export function __vmzArrayMutate(inst, root, baseSegs, method, args) {
|
|
|
333
353
|
}
|
|
334
354
|
if (!Array.isArray(arr) || typeof arr[method] !== 'function')
|
|
335
355
|
return undefined;
|
|
356
|
+
if (args !== undefined && !Array.isArray(args)) {
|
|
357
|
+
throw new Error(`vmz:dom __vmzArrayMutate expects the argument list as one array (root ${JSON.stringify(root)}, method ${JSON.stringify(method)}); stale artifacts must be rebuilt`);
|
|
358
|
+
}
|
|
336
359
|
const list = Array.isArray(args) ? args : [];
|
|
337
360
|
const ret = arr[method](...list);
|
|
338
361
|
const rootObj = inst[root];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyPreservedState, createInstance, destroy, findOwnedDefaultSlot, hasMeaningfulChild, mount, runDirectCreate, settlePendingChildMounts, snapshotInstanceState, } from '../dom-core.js';
|
|
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
4
|
export async function hydrateRoute(Page, container, props = {}, layoutCtors = [], opts = {}) {
|
|
@@ -82,10 +82,7 @@ export async function hydrate(Component, container, props = {}, opts = {}) {
|
|
|
82
82
|
// onMount may still patch state / redirect afterwards (same end state as SSR order).
|
|
83
83
|
if (!hasMeaningfulChild(container)) {
|
|
84
84
|
const node = runDirectCreate(Component, inst);
|
|
85
|
-
|
|
86
|
-
inst.__vmzDomRoot = node;
|
|
87
|
-
container.appendChild(node);
|
|
88
|
-
}
|
|
85
|
+
adoptTopLevelDomRoot(inst, node, container);
|
|
89
86
|
}
|
|
90
87
|
else {
|
|
91
88
|
// Deep adopt: park SSR children, rebuild schedule, reclaim nodes (no orphan siblings).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { applyDirectHostBox } from '../direct-host-box.js';
|
|
2
|
-
import { applyPreservedState, createInstance, destroy, hasMeaningfulChild, isEventEntryStrategy, resolveComponent, runDirectCreate, scheduleClientOn, } from '../dom-core.js';
|
|
2
|
+
import { adoptTopLevelDomRoot, applyPreservedState, createInstance, destroy, hasMeaningfulChild, isEventEntryStrategy, resolveComponent, runDirectCreate, scheduleClientOn, } from '../dom-core.js';
|
|
3
3
|
import { markUnknownComponentHost } from '../unknown-component.js';
|
|
4
4
|
import { runDirectResume } from './adopt.js';
|
|
5
5
|
export async function resume(Component, container, slice = null) {
|
|
@@ -42,10 +42,7 @@ export async function resume(Component, container, slice = null) {
|
|
|
42
42
|
if (Component.__vmzDirect && typeof Component.__vmzCreate === 'function') {
|
|
43
43
|
if (!hasMeaningfulChild(container)) {
|
|
44
44
|
const node = runDirectCreate(Component, inst);
|
|
45
|
-
|
|
46
|
-
inst.__vmzDomRoot = node;
|
|
47
|
-
container.appendChild(node);
|
|
48
|
-
}
|
|
45
|
+
adoptTopLevelDomRoot(inst, node, container);
|
|
49
46
|
}
|
|
50
47
|
else {
|
|
51
48
|
// Island leaf adopt: preserve Element identity (resume nodeIdentity).
|
|
@@ -97,10 +97,15 @@ 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
101
|
for (const entry of entries) {
|
|
101
102
|
if (loaded && loaded.has(entry.chunkId))
|
|
102
103
|
continue;
|
|
103
|
-
|
|
104
|
+
// Node/SSR consumes the packed unit bytes (`_vmz/pack-node/`) whenever the
|
|
105
|
+
// build packed the node graph — bare package imports in dependency
|
|
106
|
+
// components only exist in vendored form there. Dev sessions do not pack:
|
|
107
|
+
// the unpacked `${chunkId}.client.js` remains the module identity.
|
|
108
|
+
const abs = packNode ? packNodeEntryAbs(distDir, entry, packNode) : path.join(distDir, entry.entry);
|
|
104
109
|
let href = pathToFileURL(abs).href;
|
|
105
110
|
if (opts.cacheBust != null && opts.cacheBust !== '') {
|
|
106
111
|
href = `${href}?t=${encodeURIComponent(String(opts.cacheBust))}`;
|
|
@@ -114,6 +119,44 @@ export async function importAndRegisterComponentEntries(distDir, entries, regist
|
|
|
114
119
|
registerComponents(map);
|
|
115
120
|
return map;
|
|
116
121
|
}
|
|
122
|
+
/** `vmz.pack.node-manifest.v0` 的唯一消费点:缺失即未打包(dev),调用方回退未打包模块。 */
|
|
123
|
+
function readPackNodeManifest(distDir) {
|
|
124
|
+
const manifestPath = path.join(distDir, '_vmz', 'pack-node-manifest.json');
|
|
125
|
+
if (!fs.existsSync(manifestPath))
|
|
126
|
+
return null;
|
|
127
|
+
// Freshness gate: `vmz build` writes the deployment document first and packs
|
|
128
|
+
// the node graph strictly after; a dev soft rebuild rewrites the deployment
|
|
129
|
+
// document without re-packing. A deployment newer than the manifest means the
|
|
130
|
+
// unpacked tree moved on — packed bytes are stale and must not be served.
|
|
131
|
+
try {
|
|
132
|
+
const deploymentMtime = fs.statSync(path.join(distDir, 'vmz-deployment.json')).mtimeMs;
|
|
133
|
+
const manifestMtime = fs.statSync(manifestPath).mtimeMs;
|
|
134
|
+
if (deploymentMtime > manifestMtime)
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
142
|
+
return Array.isArray(parsed.entryFiles) ? { entryFiles: parsed.entryFiles } : null;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Packed node chunk for one component entry; missing logical/file is an explicit error, never a fallback. */
|
|
149
|
+
function packNodeEntryAbs(distDir, entry, manifest) {
|
|
150
|
+
const hit = manifest.entryFiles.find((e) => e && e.logical === entry.chunkId);
|
|
151
|
+
if (!hit || typeof hit.file !== 'string' || !hit.file) {
|
|
152
|
+
throw new Error(`vmz: pack-node manifest has no entry file for "${entry.chunkId}" under ${distDir}`);
|
|
153
|
+
}
|
|
154
|
+
const abs = path.join(distDir, '_vmz', 'pack-node', ...hit.file.split('/'));
|
|
155
|
+
if (!fs.existsSync(abs)) {
|
|
156
|
+
throw new Error(`vmz: pack-node chunk missing on disk for "${entry.chunkId}": ${path.relative(distDir, abs)}`);
|
|
157
|
+
}
|
|
158
|
+
return abs;
|
|
159
|
+
}
|
|
117
160
|
export async function bootstrapComponentRegistry(distDir, registerComponents, opts = {}) {
|
|
118
161
|
const preload = opts.preload ?? (opts.closureRoots?.length ? 'closure' : 'all');
|
|
119
162
|
if (preload === 'none')
|
|
@@ -7,6 +7,7 @@ import { listClientComponents } from '../list-client-components.js';
|
|
|
7
7
|
import { LOCALE_LINK_PLAN_SCHEMA, linkRouteAliasesFromUnits } from '../localize-body-links.js';
|
|
8
8
|
import { loadNativeAddon } from '../native-addon.js';
|
|
9
9
|
import { createRenderHost } from '../render/render-host.js';
|
|
10
|
+
import { resolveRouteLayoutChain } from '../render/route-layout-chain.js';
|
|
10
11
|
import { setRoutes } from '../../faces/vmz-runtime.js';
|
|
11
12
|
import { LOCALE_LINK_PLAN_REL } from './constants.js';
|
|
12
13
|
import { loadDeploymentStyle } from './locale-theme.js';
|
|
@@ -140,13 +141,13 @@ function emitEntryClient(eager, lazy, token) {
|
|
|
140
141
|
}
|
|
141
142
|
return native.generateServeEntryClient(eager, lazy, q);
|
|
142
143
|
}
|
|
143
|
-
function emitEntryEvent(token) {
|
|
144
|
+
function emitEntryEvent(eager, lazy, token) {
|
|
144
145
|
const q = `?t=${token}`;
|
|
145
146
|
const native = loadNativeAddon();
|
|
146
147
|
if (typeof native.generateServeEntryEvent !== 'function') {
|
|
147
148
|
throw new Error('vmz native addon missing generateServeEntryEvent — rebuild with `pnpm napi:build`');
|
|
148
149
|
}
|
|
149
|
-
return native.generateServeEntryEvent(q);
|
|
150
|
+
return native.generateServeEntryEvent(eager, lazy, q);
|
|
150
151
|
}
|
|
151
152
|
export async function softReload(opts = {}) {
|
|
152
153
|
const prevToken = serveState.reloadToken;
|
|
@@ -237,10 +238,27 @@ export async function softReload(opts = {}) {
|
|
|
237
238
|
.map((e) => e.component)
|
|
238
239
|
.filter(Boolean);
|
|
239
240
|
const lazySet = new Set(lazyEventNames);
|
|
240
|
-
|
|
241
|
+
const eagerComponents = componentEntries.filter((e) => !lazySet.has(e.name));
|
|
242
|
+
const lazyComponents = componentEntries.filter((e) => lazySet.has(e.name));
|
|
243
|
+
// Bootstrap tables are literal maps keyed by chunkId / island name:
|
|
244
|
+
// page/layout units must be enumerated (name = chunkId, entry =
|
|
245
|
+
// `<chunkId>.client.js`), unknown names throw at runtime by design.
|
|
246
|
+
const pageEntries = serveState.pageCatalog.map((p) => ({ name: p.chunkId, entry: `${p.chunkId}.client.js` }));
|
|
247
|
+
const layoutNames = new Set();
|
|
248
|
+
for (const p of serveState.pageCatalog) {
|
|
249
|
+
try {
|
|
250
|
+
for (const id of resolveRouteLayoutChain(serveState.distDir, p.chunkId))
|
|
251
|
+
layoutNames.add(String(id));
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
/* page unit without layoutChain in a partial dev build */
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const layoutEntries = [...layoutNames].sort().map((id) => ({ name: id, entry: `${id}.client.js` }));
|
|
258
|
+
await writeFile(path.join(serveState.distDir, 'entry-client.js'), emitEntryClient([...eagerComponents, ...pageEntries, ...layoutEntries], lazyComponents, skipEntryRewrite ? serveState.reloadToken : nextToken), 'utf8');
|
|
241
259
|
const strategies = resumeEntries.map((e) => e.strategy);
|
|
242
260
|
const eventOnlyShell = isEventOnlyShell(strategies);
|
|
243
|
-
await writeFile(path.join(serveState.distDir, 'entry-event.js'), emitEntryEvent(skipEntryRewrite ? serveState.reloadToken : nextToken), 'utf8');
|
|
261
|
+
await writeFile(path.join(serveState.distDir, 'entry-event.js'), emitEntryEvent([], lazyComponents, skipEntryRewrite ? serveState.reloadToken : nextToken), 'utf8');
|
|
244
262
|
serveState.lastDevError = null;
|
|
245
263
|
const mode = islandHmr ? 'island' : eventOnlyShell ? 'event-shell' : 'full';
|
|
246
264
|
notifySse(JSON.stringify({
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the browser URL of a packed bootstrap entry from
|
|
3
|
+
* `_vmz/pack-manifest.json`.
|
|
4
|
+
*
|
|
5
|
+
* The manifest is the only logical → physical map for browser delivery. A
|
|
6
|
+
* missing manifest or a missing logical name is an explicit error: serving the
|
|
7
|
+
* unpacked root module would hand the browser bare package specifiers that only
|
|
8
|
+
* `vmz-pack` rewrites.
|
|
9
|
+
*/
|
|
10
|
+
export declare function packEntryHref(distDir: string, logical: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const PACK_MANIFEST_REL = ['_vmz', 'pack-manifest.json'];
|
|
4
|
+
const PACK_OUTPUT_DIR_REL = ['_vmz', 'pack'];
|
|
5
|
+
const DEFAULT_PUBLIC_BASE = '/';
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the browser URL of a packed bootstrap entry from
|
|
8
|
+
* `_vmz/pack-manifest.json`.
|
|
9
|
+
*
|
|
10
|
+
* The manifest is the only logical → physical map for browser delivery. A
|
|
11
|
+
* missing manifest or a missing logical name is an explicit error: serving the
|
|
12
|
+
* unpacked root module would hand the browser bare package specifiers that only
|
|
13
|
+
* `vmz-pack` rewrites.
|
|
14
|
+
*/
|
|
15
|
+
export function packEntryHref(distDir, logical) {
|
|
16
|
+
const manifestPath = path.join(distDir, ...PACK_MANIFEST_REL);
|
|
17
|
+
if (!fs.existsSync(manifestPath)) {
|
|
18
|
+
throw new Error(`packEntryHref: missing ${manifestPath} — run vmz build (pack) first`);
|
|
19
|
+
}
|
|
20
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
21
|
+
const hit = (manifest.entryFiles || []).find((entry) => entry && entry.logical === logical);
|
|
22
|
+
if (!hit || typeof hit.file !== 'string' || !hit.file) {
|
|
23
|
+
throw new Error(`packEntryHref: pack manifest has no entry file for "${logical}"`);
|
|
24
|
+
}
|
|
25
|
+
const base = String(manifest.publicBase || DEFAULT_PUBLIC_BASE).replace(/\/$/, '');
|
|
26
|
+
return `${base}/${PACK_OUTPUT_DIR_REL.join('/')}/${hit.file}`;
|
|
27
|
+
}
|
|
@@ -6,6 +6,7 @@ import { extractRouteParams, findRootCatchAll, isRootCatchAll, matchFileRoute }
|
|
|
6
6
|
import { emitDevErrorHtml, normalizeDevError } from './hmr-dev.js';
|
|
7
7
|
import { cssEntryWithBust, htmlThemeAttrPair, localeBootstrapScript, pageMetaAlternates, resolveLocalePath, resolveThemeId, siteFaviconHeadHtml, themeBootstrapScript, } from './locale-theme.js';
|
|
8
8
|
import { isEventOnlyShell, loadPageCtor, loadPageResumeEntries, runRouteGate } from './page-catalog.js';
|
|
9
|
+
import { packEntryHref } from './pack-entry.js';
|
|
9
10
|
import { serveState } from './state.js';
|
|
10
11
|
export async function renderPage(pathname, opts = {}) {
|
|
11
12
|
const rendered = await renderPageStream(pathname, opts);
|
|
@@ -393,7 +394,12 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
393
394
|
if (typeof native.generatePageShell !== 'function') {
|
|
394
395
|
throw new Error('vmz native addon missing generatePageShell — rebuild with `pnpm napi:build`');
|
|
395
396
|
}
|
|
396
|
-
|
|
397
|
+
// 生产 serve 必须注入打包条目:原始 `entry-client.js` 及其拉取的客户端模块保留
|
|
398
|
+
// 裸包说明符,浏览器无法解析。dev 仍走带 token 的原始 bootstrap。
|
|
399
|
+
const entryLogical = eventOnlyShell ? 'entry-event.js' : 'entry-client.js';
|
|
400
|
+
const entrySrc = serveState.isDev
|
|
401
|
+
? `/${entryLogical}?t=${serveState.reloadToken}`
|
|
402
|
+
: packEntryHref(serveState.distDir, entryLogical);
|
|
397
403
|
const cssHref = cssEntryWithBust(serveState.cssEntry);
|
|
398
404
|
yield native.generatePageShell({
|
|
399
405
|
bodyHtml,
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared class/style normalization for Direct SSR + client (0.1.33).
|
|
3
3
|
* Vue-compatible: string | string[] | object map | falsy parts.
|
|
4
|
+
*
|
|
5
|
+
* Style keys are normalized to CSS property spelling so SSR text and client
|
|
6
|
+
* patches agree: camelCase becomes kebab-case, custom properties (`--x`) pass
|
|
7
|
+
* through untouched, and names that cannot be CSS properties (whitespace, `;`,
|
|
8
|
+
* `:`, leading digit) are dropped — the same fate invalid declarations have in
|
|
9
|
+
* the browser. Numeric values gain `px` except for unitless properties, `0`,
|
|
10
|
+
* and custom properties. Array values emit repeated declarations (vendor
|
|
11
|
+
* fallback chains) and nested arrays flatten.
|
|
4
12
|
*/
|
|
5
13
|
/** Normalize one or more class fragments into a deduped class string. */
|
|
6
14
|
export declare function mergeClassParts(...parts: unknown[]): string;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared class/style normalization for Direct SSR + client (0.1.33).
|
|
3
3
|
* Vue-compatible: string | string[] | object map | falsy parts.
|
|
4
|
+
*
|
|
5
|
+
* Style keys are normalized to CSS property spelling so SSR text and client
|
|
6
|
+
* patches agree: camelCase becomes kebab-case, custom properties (`--x`) pass
|
|
7
|
+
* through untouched, and names that cannot be CSS properties (whitespace, `;`,
|
|
8
|
+
* `:`, leading digit) are dropped — the same fate invalid declarations have in
|
|
9
|
+
* the browser. Numeric values gain `px` except for unitless properties, `0`,
|
|
10
|
+
* and custom properties. Array values emit repeated declarations (vendor
|
|
11
|
+
* fallback chains) and nested arrays flatten.
|
|
4
12
|
*/
|
|
5
13
|
function pushClassTokens(out, value) {
|
|
6
14
|
if (value == null || value === false)
|
|
@@ -35,37 +43,168 @@ export function mergeClassParts(...parts) {
|
|
|
35
43
|
pushClassTokens(tokens, part);
|
|
36
44
|
return [...new Set(tokens)].join(' ');
|
|
37
45
|
}
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
/** CSS properties whose numeric values stay unitless (kebab-case names). */
|
|
47
|
+
const UNITLESS_STYLE_PROPS = new Set([
|
|
48
|
+
'animation-iteration-count',
|
|
49
|
+
'aspect-ratio',
|
|
50
|
+
'border-image-outset',
|
|
51
|
+
'border-image-slice',
|
|
52
|
+
'border-image-width',
|
|
53
|
+
'box-flex',
|
|
54
|
+
'box-flex-group',
|
|
55
|
+
'box-ordinal-group',
|
|
56
|
+
'column-count',
|
|
57
|
+
'columns',
|
|
58
|
+
'flex',
|
|
59
|
+
'flex-grow',
|
|
60
|
+
'flex-positive',
|
|
61
|
+
'flex-shrink',
|
|
62
|
+
'flex-negative',
|
|
63
|
+
'flex-order',
|
|
64
|
+
'grid-row',
|
|
65
|
+
'grid-row-end',
|
|
66
|
+
'grid-row-span',
|
|
67
|
+
'grid-row-start',
|
|
68
|
+
'grid-column',
|
|
69
|
+
'grid-column-end',
|
|
70
|
+
'grid-column-span',
|
|
71
|
+
'grid-column-start',
|
|
72
|
+
'font-weight',
|
|
73
|
+
'line-clamp',
|
|
74
|
+
'line-height',
|
|
75
|
+
'opacity',
|
|
76
|
+
'order',
|
|
77
|
+
'orphans',
|
|
78
|
+
'scale',
|
|
79
|
+
'tab-size',
|
|
80
|
+
'widows',
|
|
81
|
+
'z-index',
|
|
82
|
+
'zoom',
|
|
83
|
+
'fill-opacity',
|
|
84
|
+
'flood-opacity',
|
|
85
|
+
'stop-opacity',
|
|
86
|
+
'stroke-dasharray',
|
|
87
|
+
'stroke-dashoffset',
|
|
88
|
+
'stroke-miterlimit',
|
|
89
|
+
'stroke-opacity',
|
|
90
|
+
'stroke-width',
|
|
91
|
+
]);
|
|
92
|
+
/** camelCase → kebab-case, `--custom` preserved; `null` when not a CSS property name. */
|
|
93
|
+
function normalizeStyleKey(raw) {
|
|
94
|
+
const key = raw.trim();
|
|
95
|
+
if (!key)
|
|
96
|
+
return null;
|
|
97
|
+
if (key.startsWith('--')) {
|
|
98
|
+
return /^--[\w-]+$/.test(key) ? key : null;
|
|
99
|
+
}
|
|
100
|
+
const kebab = key.replace(/\B([A-Z])/g, '-$1').toLowerCase();
|
|
101
|
+
return /^[a-z][a-z0-9-]*$/.test(kebab) ? kebab : null;
|
|
102
|
+
}
|
|
103
|
+
/** One style value as declaration text; `null` drops the declaration. */
|
|
104
|
+
function normalizeStyleValue(key, value) {
|
|
105
|
+
if (value == null || value === false)
|
|
106
|
+
return null;
|
|
107
|
+
if (typeof value === 'number' && value !== 0 && !key.startsWith('--') && !isUnitlessStyleProp(key)) {
|
|
108
|
+
return `${value}px`;
|
|
109
|
+
}
|
|
110
|
+
return String(value);
|
|
111
|
+
}
|
|
112
|
+
/** Unitless lookup ignores vendor prefixes (`webkit-line-clamp` ≙ `line-clamp`). */
|
|
113
|
+
function isUnitlessStyleProp(key) {
|
|
114
|
+
const unprefixed = key.replace(/^(?:-?(?:webkit|moz|ms|o))-/, '');
|
|
115
|
+
return UNITLESS_STYLE_PROPS.has(unprefixed);
|
|
116
|
+
}
|
|
117
|
+
function pushStyleValues(out, key, value) {
|
|
118
|
+
if (Array.isArray(value)) {
|
|
119
|
+
for (const item of value)
|
|
120
|
+
pushStyleValues(out, key, item);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const text = normalizeStyleValue(key, value);
|
|
124
|
+
if (text != null)
|
|
125
|
+
out.push(text);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Split a style fragment on top-level `;`. Semicolons inside `url(...)` /
|
|
129
|
+
* nested parens and inside quotes (e.g. `content: 'a;b'`, data URIs with
|
|
130
|
+
* `;base64,`) do not split — a naive split would truncate those values.
|
|
131
|
+
*/
|
|
132
|
+
function splitStyleDeclarations(text) {
|
|
133
|
+
const chunks = [];
|
|
134
|
+
let depth = 0;
|
|
135
|
+
let quote = null;
|
|
136
|
+
let start = 0;
|
|
137
|
+
let i = 0;
|
|
138
|
+
while (i < text.length) {
|
|
139
|
+
const ch = text[i];
|
|
140
|
+
if (quote != null) {
|
|
141
|
+
if (ch === '\\') {
|
|
142
|
+
i += 2;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (ch === quote)
|
|
146
|
+
quote = null;
|
|
147
|
+
}
|
|
148
|
+
else if (ch === '"' || ch === "'") {
|
|
149
|
+
quote = ch;
|
|
150
|
+
}
|
|
151
|
+
else if (ch === '(') {
|
|
152
|
+
depth += 1;
|
|
153
|
+
}
|
|
154
|
+
else if (ch === ')') {
|
|
155
|
+
if (depth > 0)
|
|
156
|
+
depth -= 1;
|
|
157
|
+
}
|
|
158
|
+
else if (ch === ';' && depth === 0) {
|
|
159
|
+
chunks.push(text.slice(start, i));
|
|
160
|
+
start = i + 1;
|
|
161
|
+
}
|
|
162
|
+
i += 1;
|
|
163
|
+
}
|
|
164
|
+
chunks.push(text.slice(start));
|
|
165
|
+
return chunks;
|
|
166
|
+
}
|
|
167
|
+
function collectStyleDeclarations(merged, value) {
|
|
40
168
|
if (value == null || value === false)
|
|
41
|
-
return
|
|
169
|
+
return;
|
|
42
170
|
if (typeof value === 'string') {
|
|
43
|
-
for (const chunk of value
|
|
171
|
+
for (const chunk of splitStyleDeclarations(value)) {
|
|
44
172
|
const idx = chunk.indexOf(':');
|
|
45
173
|
if (idx < 0)
|
|
46
174
|
continue;
|
|
47
|
-
const key = chunk.slice(0, idx)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
175
|
+
const key = normalizeStyleKey(chunk.slice(0, idx));
|
|
176
|
+
if (!key)
|
|
177
|
+
continue;
|
|
178
|
+
merged.set(key, [chunk.slice(idx + 1).trim()]);
|
|
51
179
|
}
|
|
52
|
-
return
|
|
180
|
+
return;
|
|
53
181
|
}
|
|
54
|
-
if (
|
|
55
|
-
for (const
|
|
56
|
-
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
for (const item of value)
|
|
184
|
+
collectStyleDeclarations(merged, item);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (typeof value === 'object') {
|
|
188
|
+
for (const [rawKey, raw] of Object.entries(value)) {
|
|
189
|
+
const key = normalizeStyleKey(rawKey);
|
|
190
|
+
if (!key)
|
|
57
191
|
continue;
|
|
58
|
-
|
|
192
|
+
const values = [];
|
|
193
|
+
pushStyleValues(values, key, raw);
|
|
194
|
+
if (values.length)
|
|
195
|
+
merged.set(key, values);
|
|
59
196
|
}
|
|
60
197
|
}
|
|
61
|
-
return out;
|
|
62
198
|
}
|
|
63
199
|
/** Merge static + dynamic style fragments; later parts override same keys. */
|
|
64
200
|
export function mergeStyleParts(...parts) {
|
|
65
|
-
const merged =
|
|
201
|
+
const merged = new Map();
|
|
66
202
|
for (const part of parts)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
203
|
+
collectStyleDeclarations(merged, part);
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const [key, values] of merged) {
|
|
206
|
+
for (const value of values)
|
|
207
|
+
out.push(`${key}: ${value}`);
|
|
208
|
+
}
|
|
209
|
+
return out.join('; ');
|
|
71
210
|
}
|
|
@@ -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
|
+
}
|
package/dist/ssr/render.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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())
|
package/dist/ssr/serialize.d.ts
CHANGED
|
@@ -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;
|
package/dist/ssr/serialize.js
CHANGED
|
@@ -2,6 +2,7 @@ import { directHostBoxStyleAttr } from '../browser/direct-host-box.js';
|
|
|
2
2
|
import { BOOLEAN_HTML_ATTRS, createInstance, eventPropHandlerName, getRegisteredComponent, isEventEntryStrategy, mergeClassParts, mergeStyleParts, snapshotInstanceState, stripFns, } from '../browser/dom-core.js';
|
|
3
3
|
import { serializeUnknownComponentNode } from '../browser/unknown-component.js';
|
|
4
4
|
import { _ssrDocumentLastError, ensureSsrDocument } from './document.js';
|
|
5
|
+
import { awaitSsrOnMount, ssrComponentName, ssrOnMountContext } from './onmount.js';
|
|
5
6
|
function runDirectSerializeTree(Component, inst) {
|
|
6
7
|
serializeApi._inst = inst;
|
|
7
8
|
try {
|
|
@@ -11,7 +12,8 @@ function runDirectSerializeTree(Component, inst) {
|
|
|
11
12
|
serializeApi._inst = null;
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
|
-
export async function runDirectSerializeTreeWithMounts(Component, inst) {
|
|
15
|
+
export async function runDirectSerializeTreeWithMounts(Component, inst, onMountCtx) {
|
|
16
|
+
const ctx = onMountCtx || ssrOnMountContext(null);
|
|
15
17
|
let preMounted = [];
|
|
16
18
|
let tree = null;
|
|
17
19
|
for (let round = 0; round < 32; round++) {
|
|
@@ -28,9 +30,7 @@ export async function runDirectSerializeTreeWithMounts(Component, inst) {
|
|
|
28
30
|
if (!collected.length)
|
|
29
31
|
return tree;
|
|
30
32
|
for (const child of collected) {
|
|
31
|
-
|
|
32
|
-
await child.onMount();
|
|
33
|
-
}
|
|
33
|
+
await awaitSsrOnMount(child, ssrComponentName(null, child), ctx);
|
|
34
34
|
}
|
|
35
35
|
preMounted = preMounted.concat(collected);
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -68,12 +68,12 @@
|
|
|
68
68
|
"main": "./dist/faces/server.js",
|
|
69
69
|
"name": "@vmz/core",
|
|
70
70
|
"optionalDependencies": {
|
|
71
|
-
"@vmz/vmz-darwin-arm64": "0.3.
|
|
72
|
-
"@vmz/vmz-darwin-x64": "0.3.
|
|
73
|
-
"@vmz/vmz-linux-arm64": "0.3.
|
|
74
|
-
"@vmz/vmz-linux-x64": "0.3.
|
|
75
|
-
"@vmz/vmz-win32-arm64": "0.3.
|
|
76
|
-
"@vmz/vmz-win32-x64": "0.3.
|
|
71
|
+
"@vmz/vmz-darwin-arm64": "0.3.13",
|
|
72
|
+
"@vmz/vmz-darwin-x64": "0.3.13",
|
|
73
|
+
"@vmz/vmz-linux-arm64": "0.3.13",
|
|
74
|
+
"@vmz/vmz-linux-x64": "0.3.13",
|
|
75
|
+
"@vmz/vmz-win32-arm64": "0.3.13",
|
|
76
|
+
"@vmz/vmz-win32-x64": "0.3.13"
|
|
77
77
|
},
|
|
78
78
|
"publishConfig": {
|
|
79
79
|
"access": "public"
|
|
@@ -88,5 +88,5 @@
|
|
|
88
88
|
},
|
|
89
89
|
"type": "module",
|
|
90
90
|
"types": "./dist/faces/index.d.ts",
|
|
91
|
-
"version": "0.3.
|
|
91
|
+
"version": "0.3.13"
|
|
92
92
|
}
|