@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 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 omits `createItem` and materializes
9
- * rows via `html` + `hydrate`, which needs `document.createElement('template')`.
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
- const { parseHTML } = mod.createRequire(import.meta.url)('linkedom');
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
- * Hydrate a detached DOM node from `rowKernel.html`, then ship outerHTML.
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
- throw new Error('vmz:dom SSR rowKernel requires a document (createItem omitted)');
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, box.item);
515
+ rk.hydrate.call(inst, root, item);
397
516
  }
398
517
  if (key != null)
399
518
  root.setAttribute('data-vmz-key', String(key));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/core",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
6
6
  "exports": {