@vmz/core 0.1.4 → 0.1.6
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/dom-ssr.d.ts +0 -4
- package/dist/dom-ssr.js +134 -15
- package/package.json +1 -1
package/dist/dom-ssr.d.ts
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
* VMZ DOM SSR / hydrate / resume — precise patches, no VDOM diff.
|
|
3
3
|
* Imports client DOM primitives from ./dom-core.js for tree-shakeable browser entry.
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
6
|
-
* @param {new (props?: object) => any} Component
|
|
7
|
-
* @param {object} [props]
|
|
8
|
-
*/
|
|
9
5
|
export declare function renderToString(Component: any, props?: {}, opts?: {}): Promise<any>;
|
|
10
6
|
/**
|
|
11
7
|
* Stream SSR via the same Direct serialize schedule as `renderToString`.
|
package/dist/dom-ssr.js
CHANGED
|
@@ -4,40 +4,90 @@
|
|
|
4
4
|
* Imports client DOM primitives from ./dom-core.js for tree-shakeable browser entry.
|
|
5
5
|
*/
|
|
6
6
|
import { applyDomAttr, applyPreservedState, BOOLEAN_HTML_ATTRS, createInstance, destroy, directApi, getRegisteredComponent, hasMeaningfulChild, isEventEntryStrategy, isEventPropName, mount, noteDomCreate, resolveComponent, runDirectCreate, scheduleClientOn, settlePendingChildMounts, snapshotInstanceState, stripFns, } from './dom-core.js';
|
|
7
|
+
/** @type {Error | null} last linkedom resolve failure (for clear SSR errors) */
|
|
8
|
+
let _ssrDocumentLastError = null;
|
|
7
9
|
/**
|
|
8
|
-
* Node SSR has no browser `document`. rowKernel
|
|
9
|
-
*
|
|
10
|
-
* Install a linkedom document once when missing (optional at runtime if linkedom is present).
|
|
10
|
+
* Node SSR has no browser `document`. rowKernel prefers document-free HTML fill;
|
|
11
|
+
* DOM hydrate is a fallback (class ternaries / missing textSlots) and needs linkedom.
|
|
11
12
|
*
|
|
12
13
|
* Must not statically import `node:module`: `dom.js` / `vmz-dom.js` re-export this file,
|
|
13
14
|
* and browser hosts load that barrel.
|
|
15
|
+
*
|
|
16
|
+
* When this file is copied into an app `dist/`, `createRequire(import.meta.url)` cannot
|
|
17
|
+
* see `@vmz/core`'s dependency tree — resolve linkedom from cwd / `@vmz/core` as well.
|
|
14
18
|
*/
|
|
15
19
|
function ensureSsrDocument() {
|
|
16
20
|
if (typeof globalThis.document !== 'undefined' && typeof globalThis.document.createElement === 'function') {
|
|
17
21
|
return true;
|
|
18
22
|
}
|
|
19
23
|
const proc = globalThis.process;
|
|
20
|
-
if (!proc?.versions?.node)
|
|
24
|
+
if (!proc?.versions?.node) {
|
|
25
|
+
_ssrDocumentLastError = new Error('vmz:dom SSR document: not running on Node');
|
|
21
26
|
return false;
|
|
27
|
+
}
|
|
22
28
|
try {
|
|
23
29
|
// Node 20.16+ / 22.3+: sync builtin load without a static `node:` import.
|
|
24
30
|
const mod = typeof proc.getBuiltinModule === 'function' ? proc.getBuiltinModule('module') : null;
|
|
25
|
-
if (!mod?.createRequire)
|
|
31
|
+
if (!mod?.createRequire) {
|
|
32
|
+
_ssrDocumentLastError = new Error('vmz:dom SSR document: createRequire unavailable');
|
|
26
33
|
return false;
|
|
27
|
-
|
|
34
|
+
}
|
|
35
|
+
const pathMod = typeof proc.getBuiltinModule === 'function' ? proc.getBuiltinModule('path') : null;
|
|
36
|
+
const createRequire = mod.createRequire;
|
|
37
|
+
/** @type {string[]} */
|
|
38
|
+
const bases = [];
|
|
39
|
+
bases.push(import.meta.url);
|
|
40
|
+
if (pathMod && typeof proc.cwd === 'function') {
|
|
41
|
+
bases.push(pathMod.join(proc.cwd(), 'package.json'));
|
|
42
|
+
}
|
|
43
|
+
/** @type {string[]} */
|
|
44
|
+
const errors = [];
|
|
45
|
+
let parseHTML = null;
|
|
46
|
+
for (const base of bases) {
|
|
47
|
+
let req;
|
|
48
|
+
try {
|
|
49
|
+
req = createRequire(base);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
errors.push(`${base}: createRequire failed: ${e && e.message ? e.message : e}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
parseHTML = req('linkedom').parseHTML;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
errors.push(`${base} → linkedom: ${e && e.message ? e.message : e}`);
|
|
61
|
+
}
|
|
62
|
+
// Walk into @vmz/core's dependency tree (linkedom is declared there).
|
|
63
|
+
for (const coreId of ['@vmz/core', '@vmz/core/dom', '@vmz/core/server']) {
|
|
64
|
+
try {
|
|
65
|
+
const coreEntry = req.resolve(coreId);
|
|
66
|
+
parseHTML = createRequire(coreEntry)('linkedom').parseHTML;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
errors.push(`${base} → ${coreId}/linkedom: ${e && e.message ? e.message : e}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (parseHTML)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (typeof parseHTML !== 'function') {
|
|
77
|
+
const detail = errors.length ? `\n${errors.join('\n')}` : '';
|
|
78
|
+
throw new Error(`linkedom unresolved for SSR document${detail}`);
|
|
79
|
+
}
|
|
28
80
|
const { window, document } = parseHTML('<!DOCTYPE html><html><body></body></html>');
|
|
29
81
|
globalThis.window = window;
|
|
30
82
|
globalThis.document = document;
|
|
83
|
+
_ssrDocumentLastError = null;
|
|
31
84
|
return typeof document.createElement === 'function';
|
|
32
85
|
}
|
|
33
|
-
catch {
|
|
86
|
+
catch (err) {
|
|
87
|
+
_ssrDocumentLastError = err instanceof Error ? err : new Error(String(err));
|
|
34
88
|
return false;
|
|
35
89
|
}
|
|
36
90
|
}
|
|
37
|
-
/**
|
|
38
|
-
* @param {new (props?: object) => any} Component
|
|
39
|
-
* @param {object} [props]
|
|
40
|
-
*/
|
|
41
91
|
export async function renderToString(Component, props = {}, opts = {}) {
|
|
42
92
|
ensureSsrDocument();
|
|
43
93
|
const signal = opts && opts.signal;
|
|
@@ -374,17 +424,86 @@ function* streamSerializeChunks(node) {
|
|
|
374
424
|
yield `</${tag}>`;
|
|
375
425
|
}
|
|
376
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Document-free rowKernel SSR: fill generator text placeholders (` `) from textSlots + item.
|
|
429
|
+
* Prefer this over linkedom — createItem was omitted because html + slots are enough.
|
|
430
|
+
* @param {{ html: string, textSlots?: Record<string, number>, hostFields?: string[] }} rk
|
|
431
|
+
* @param {any} item
|
|
432
|
+
* @param {any} key
|
|
433
|
+
* @returns {string | null} filled outer HTML, or null if shape is not fillable
|
|
434
|
+
*/
|
|
435
|
+
function fillRowKernelHtml(rk, item, key) {
|
|
436
|
+
const slots = rk.textSlots;
|
|
437
|
+
if (!slots || typeof slots !== 'object')
|
|
438
|
+
return null;
|
|
439
|
+
/** @type {string[]} */
|
|
440
|
+
const fields = Object.entries(slots)
|
|
441
|
+
.filter(([, i]) => typeof i === 'number' && Number.isFinite(i))
|
|
442
|
+
.sort((a, b) => /** @type {number} */ (a[1]) - /** @type {number} */ (b[1]))
|
|
443
|
+
.map(([f]) => f);
|
|
444
|
+
if (!fields.length)
|
|
445
|
+
return null;
|
|
446
|
+
let slotI = 0;
|
|
447
|
+
// Generator emits one space per text interp as a dedicated text node (`> <`).
|
|
448
|
+
const filled = String(rk.html).replace(/>([^<]*)</g, (m, text) => {
|
|
449
|
+
if (text === ' ' && slotI < fields.length) {
|
|
450
|
+
const f = fields[slotI++];
|
|
451
|
+
const v = item == null ? '' : item[f];
|
|
452
|
+
return `>${escapeHtml(v == null ? '' : String(v))}<`;
|
|
453
|
+
}
|
|
454
|
+
return m;
|
|
455
|
+
});
|
|
456
|
+
if (slotI !== fields.length)
|
|
457
|
+
return null;
|
|
458
|
+
if (key == null)
|
|
459
|
+
return filled;
|
|
460
|
+
// Inject data-vmz-key on the root opening tag (same attr hydrate would set).
|
|
461
|
+
return filled.replace(/^<([A-Za-z][\w:-]*)/, `<$1 data-vmz-key="${escapeHtml(String(key))}"`);
|
|
462
|
+
}
|
|
377
463
|
/**
|
|
378
464
|
* SSR row when `createItem` was omitted (rowKernel client emit).
|
|
379
|
-
*
|
|
465
|
+
* Prefer document-free fill from `html` + `textSlots`; fall back to linkedom + hydrate
|
|
466
|
+
* when host class ternaries need live DOM, or when placeholders cannot be string-filled.
|
|
380
467
|
* @param {object} inst
|
|
381
|
-
* @param {{ html: string, hydrate?: Function }} rk
|
|
468
|
+
* @param {{ html: string, hydrate?: Function, textSlots?: Record<string, number>, hostFields?: string[] }} rk
|
|
382
469
|
* @param {{ item: any, index: number }} box
|
|
383
470
|
* @param {any} key
|
|
384
471
|
*/
|
|
385
472
|
function serializeRowFromKernel(inst, rk, box, key) {
|
|
473
|
+
const item = box.item;
|
|
474
|
+
const hostFields = Array.isArray(rk.hostFields) ? rk.hostFields : [];
|
|
475
|
+
// Text-only kernels: no document. Host class ternaries still need hydrate/DOM.
|
|
476
|
+
if (hostFields.length === 0) {
|
|
477
|
+
const raw = fillRowKernelHtml(rk, item, key);
|
|
478
|
+
if (raw != null) {
|
|
479
|
+
return {
|
|
480
|
+
__kind: 'el',
|
|
481
|
+
tag: 'div',
|
|
482
|
+
attrs: Object.create(null),
|
|
483
|
+
children: [],
|
|
484
|
+
__rawOuter: true,
|
|
485
|
+
__rawHtml: raw,
|
|
486
|
+
appendChild() { },
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
386
490
|
if (!ensureSsrDocument()) {
|
|
387
|
-
|
|
491
|
+
// Degrade: still ship text fill if possible (class may be wrong until client hydrate).
|
|
492
|
+
const raw = fillRowKernelHtml(rk, item, key);
|
|
493
|
+
if (raw != null) {
|
|
494
|
+
return {
|
|
495
|
+
__kind: 'el',
|
|
496
|
+
tag: 'div',
|
|
497
|
+
attrs: Object.create(null),
|
|
498
|
+
children: [],
|
|
499
|
+
__rawOuter: true,
|
|
500
|
+
__rawHtml: raw,
|
|
501
|
+
appendChild() { },
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const cause = _ssrDocumentLastError;
|
|
505
|
+
const detail = cause && cause.message ? cause.message : 'unavailable';
|
|
506
|
+
throw new Error(`vmz:dom SSR rowKernel requires a document (createItem omitted): ${detail}`, cause ? { cause } : undefined);
|
|
388
507
|
}
|
|
389
508
|
const tpl = document.createElement('template');
|
|
390
509
|
tpl.innerHTML = rk.html;
|
|
@@ -393,7 +512,7 @@ function serializeRowFromKernel(inst, rk, box, key) {
|
|
|
393
512
|
throw new Error('vmz:dom SSR rowKernel html produced no element');
|
|
394
513
|
}
|
|
395
514
|
if (typeof rk.hydrate === 'function') {
|
|
396
|
-
rk.hydrate.call(inst, root,
|
|
515
|
+
rk.hydrate.call(inst, root, item);
|
|
397
516
|
}
|
|
398
517
|
if (key != null)
|
|
399
518
|
root.setAttribute('data-vmz-key', String(key));
|