@vmz/core 0.3.13 → 0.3.15
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 +11 -0
- package/dist/browser/direct-host-box.d.ts +11 -0
- package/dist/browser/direct-host-box.js +13 -0
- package/dist/browser/dom/attributes.js +142 -2
- package/dist/browser/dom/direct-api.d.ts +51 -3
- package/dist/browser/dom/direct-api.js +489 -88
- package/dist/browser/dom-core.types.d.ts +19 -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 +284 -14
- 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/shared/row-key.d.ts +13 -0
- package/dist/shared/row-key.js +19 -0
- package/dist/ssr/serialize.js +357 -17
- package/package.json +7 -7
package/dist/ssr/serialize.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { directHostBoxStyleAttr } from '../browser/direct-host-box.js';
|
|
1
|
+
import { directHostBoxStyleAttr, resolveDirectHostTag } 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
|
+
import { splitStyleDeclarations } from '../shared/dom-attr-normalize.js';
|
|
5
|
+
import { HTML_HASH_ATTR, htmlDigest } from '../shared/html-digest.js';
|
|
6
|
+
import { rowEndMarker, rowMarker } from '../shared/row-key.js';
|
|
4
7
|
import { _ssrDocumentLastError, ensureSsrDocument } from './document.js';
|
|
5
8
|
import { awaitSsrOnMount, ssrComponentName, ssrOnMountContext } from './onmount.js';
|
|
6
9
|
function runDirectSerializeTree(Component, inst) {
|
|
@@ -56,16 +59,122 @@ const VOID_TAGS = new Set([
|
|
|
56
59
|
function isVoidTag(tag) {
|
|
57
60
|
return VOID_TAGS.has(String(tag).toLowerCase());
|
|
58
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* `textarea` carries its initial value as text content and `select` carries it
|
|
64
|
+
* as a selected option, so neither can travel as a `value` attribute. `input`
|
|
65
|
+
* keeps the attribute, which browsers honor for the default value.
|
|
66
|
+
*/
|
|
67
|
+
function controlValue(node, tag) {
|
|
68
|
+
if (tag !== 'textarea' && tag !== 'select')
|
|
69
|
+
return null;
|
|
70
|
+
const v = node.attrs ? node.attrs.value : null;
|
|
71
|
+
if (v == null || v === false)
|
|
72
|
+
return null;
|
|
73
|
+
// A `multiple` select model is a list; keep every entry addressable.
|
|
74
|
+
if (tag === 'select' && Array.isArray(v))
|
|
75
|
+
return v.map((item) => String(item));
|
|
76
|
+
return String(v);
|
|
77
|
+
}
|
|
78
|
+
function nodeText(node) {
|
|
79
|
+
if (!node || typeof node !== 'object')
|
|
80
|
+
return '';
|
|
81
|
+
if (node.__kind === 'text')
|
|
82
|
+
return String(node.value ?? '');
|
|
83
|
+
if (node.__kind === 'el' || node.__kind === 'frag') {
|
|
84
|
+
return (node.children || []).map(nodeText).join('');
|
|
85
|
+
}
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
/** Mark the `<option>` whose value (or text) equals one of the model values. */
|
|
89
|
+
function markSelectSelection(node, wanted) {
|
|
90
|
+
const wants = Array.isArray(wanted) ? wanted.map((item) => String(item)) : [String(wanted)];
|
|
91
|
+
const visit = (n) => {
|
|
92
|
+
if (!n || typeof n !== 'object')
|
|
93
|
+
return;
|
|
94
|
+
if (n.__kind === 'el') {
|
|
95
|
+
const tag = String(n.tag || '').toLowerCase();
|
|
96
|
+
if (tag === 'option') {
|
|
97
|
+
const raw = n.attrs && n.attrs.value != null ? String(n.attrs.value) : nodeText(n).trim();
|
|
98
|
+
if (wants.includes(raw)) {
|
|
99
|
+
if (!n.attrs)
|
|
100
|
+
n.attrs = {};
|
|
101
|
+
n.attrs.selected = '';
|
|
102
|
+
}
|
|
103
|
+
else if (n.attrs && Object.prototype.hasOwnProperty.call(n.attrs, 'selected')) {
|
|
104
|
+
delete n.attrs.selected;
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const c of n.children || [])
|
|
110
|
+
visit(c);
|
|
111
|
+
};
|
|
112
|
+
for (const c of node.children || [])
|
|
113
|
+
visit(c);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Inline style as the browser would serialize it: the `style` attribute merged
|
|
117
|
+
* with the live `style.*` writes that Direct patches and `v-show` perform
|
|
118
|
+
* (`el.style.display = …`). SSR virtual elements only kept the attribute, so a
|
|
119
|
+
* server-first response showed elements that hydration hid.
|
|
120
|
+
*/
|
|
121
|
+
function serializeStyleAttr(node) {
|
|
122
|
+
const declared = node.attrs && node.attrs.style != null ? String(node.attrs.style) : '';
|
|
123
|
+
const live = node.style;
|
|
124
|
+
if (!live || typeof live !== 'object')
|
|
125
|
+
return declared;
|
|
126
|
+
const display = live.display;
|
|
127
|
+
if (display === undefined)
|
|
128
|
+
return declared;
|
|
129
|
+
// `el.style.display = ''` removes the declaration instead of writing it.
|
|
130
|
+
if (display === '' || display == null) {
|
|
131
|
+
return splitStyleDeclarations(declared)
|
|
132
|
+
.filter((chunk) => {
|
|
133
|
+
const idx = chunk.indexOf(':');
|
|
134
|
+
return idx < 0 || chunk.slice(0, idx).trim() !== 'display';
|
|
135
|
+
})
|
|
136
|
+
.map((chunk) => chunk.trim())
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.join('; ');
|
|
139
|
+
}
|
|
140
|
+
return mergeStyleParts(declared, { display });
|
|
141
|
+
}
|
|
59
142
|
function serializeOpenTag(node) {
|
|
60
143
|
const tag = node.tag || 'div';
|
|
144
|
+
const lowerTag = String(tag).toLowerCase();
|
|
145
|
+
// `v-pre`: the authored attribute names are the output. Skipping the four
|
|
146
|
+
// special branches here is what keeps `className` / `style` / `value` /
|
|
147
|
+
// `data-vmz-html-hash` literal inside a verbatim subtree.
|
|
148
|
+
const verbatim = node.__vmzVerbatim === true;
|
|
61
149
|
let attrs = '';
|
|
62
150
|
for (const [k, v] of Object.entries(node.attrs || {})) {
|
|
63
151
|
if (v == null || v === false)
|
|
64
152
|
continue;
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
153
|
+
if (!verbatim) {
|
|
154
|
+
// `textarea` value renders as text content and `select` value as a
|
|
155
|
+
// selected option, so the raw attribute must not be emitted.
|
|
156
|
+
if (k === 'value' && (lowerTag === 'textarea' || lowerTag === 'select'))
|
|
157
|
+
continue;
|
|
158
|
+
if (k === 'style')
|
|
159
|
+
continue;
|
|
160
|
+
// Always replaced by the digest of the payload written below, so an
|
|
161
|
+
// author-supplied marker can never mask a different payload.
|
|
162
|
+
if (k === HTML_HASH_ATTR)
|
|
163
|
+
continue;
|
|
164
|
+
if (k === 'className') {
|
|
165
|
+
attrs += ` class="${escapeHtml(v)}"`;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
attrs += ` ${k}="${escapeHtml(v)}"`;
|
|
170
|
+
}
|
|
171
|
+
const style = verbatim ? '' : serializeStyleAttr(node);
|
|
172
|
+
if (style)
|
|
173
|
+
attrs += ` style="${escapeHtml(style)}"`;
|
|
174
|
+
// `v-html` hosts carry the digest of the exact string SSR wrote, so the
|
|
175
|
+
// client can keep the SSR subtree without DOM-serializing its payload.
|
|
176
|
+
if (node.__rawHtml != null && !node.__rawOuter) {
|
|
177
|
+
attrs += ` ${HTML_HASH_ATTR}="${escapeHtml(htmlDigest(String(node.__rawHtml)))}"`;
|
|
69
178
|
}
|
|
70
179
|
return { tag, open: `<${tag}${attrs}>` };
|
|
71
180
|
}
|
|
@@ -81,7 +190,14 @@ export function flattenSerializeNode(node) {
|
|
|
81
190
|
if (node.__kind === 'frag') {
|
|
82
191
|
if (node.__rawHtml != null)
|
|
83
192
|
return String(node.__rawHtml);
|
|
84
|
-
|
|
193
|
+
const inner = (node.children || []).map(flattenSerializeNode).join('');
|
|
194
|
+
// Multi-root `<template v-for>` rows have no element root to carry
|
|
195
|
+
// `data-vmz-key`, so their identity travels as a marker pair around the
|
|
196
|
+
// row's nodes. Resume reads the same pair and scopes adoption to it.
|
|
197
|
+
if (node.__vmzRowKey != null) {
|
|
198
|
+
return `<!--${rowMarker(node.__vmzRowKey)}-->${inner}<!--${rowEndMarker(node.__vmzRowKey)}-->`;
|
|
199
|
+
}
|
|
200
|
+
return inner;
|
|
85
201
|
}
|
|
86
202
|
if (node.__kind === 'el') {
|
|
87
203
|
const tag = node.tag || 'div';
|
|
@@ -94,11 +210,22 @@ export function flattenSerializeNode(node) {
|
|
|
94
210
|
if (node.__rawOuter && node.__rawHtml != null)
|
|
95
211
|
return String(node.__rawHtml);
|
|
96
212
|
const { open } = serializeOpenTag(node);
|
|
213
|
+
const lowerTag = String(tag).toLowerCase();
|
|
214
|
+
const control = controlValue(node, lowerTag);
|
|
97
215
|
if (node.__rawHtml != null) {
|
|
98
|
-
|
|
216
|
+
const lead = needsSacrificialLF(node, lowerTag, null) ? '\n' : '';
|
|
217
|
+
const raw = String(node.__rawHtml);
|
|
218
|
+
return isVoidTag(tag) ? `${open}${raw}` : `${open}${lead}${raw}</${tag}>`;
|
|
219
|
+
}
|
|
220
|
+
if (lowerTag === 'textarea' && control != null) {
|
|
221
|
+
const lead = needsSacrificialLF(node, lowerTag, control) ? '\n' : '';
|
|
222
|
+
return `${open}${lead}${escapeHtml(control)}</${tag}>`;
|
|
99
223
|
}
|
|
224
|
+
if (lowerTag === 'select' && control != null)
|
|
225
|
+
markSelectSelection(node, control);
|
|
100
226
|
const inner = (node.children || []).map(flattenSerializeNode).join('');
|
|
101
|
-
|
|
227
|
+
const lead = needsSacrificialLF(node, lowerTag, control) ? '\n' : '';
|
|
228
|
+
return isVoidTag(tag) ? `${open}${inner}` : `${open}${lead}${inner}</${tag}>`;
|
|
102
229
|
}
|
|
103
230
|
return '';
|
|
104
231
|
}
|
|
@@ -120,6 +247,13 @@ export function* streamSerializeChunks(node) {
|
|
|
120
247
|
yield String(node.__rawHtml);
|
|
121
248
|
return;
|
|
122
249
|
}
|
|
250
|
+
if (node.__vmzRowKey != null) {
|
|
251
|
+
yield `<!--${rowMarker(node.__vmzRowKey)}-->`;
|
|
252
|
+
for (const c of node.children || [])
|
|
253
|
+
yield* streamSerializeChunks(c);
|
|
254
|
+
yield `<!--${rowEndMarker(node.__vmzRowKey)}-->`;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
123
257
|
for (const c of node.children || [])
|
|
124
258
|
yield* streamSerializeChunks(c);
|
|
125
259
|
return;
|
|
@@ -141,10 +275,20 @@ export function* streamSerializeChunks(node) {
|
|
|
141
275
|
}
|
|
142
276
|
const { open } = serializeOpenTag(node);
|
|
143
277
|
yield open;
|
|
278
|
+
const lowerTag = String(tag).toLowerCase();
|
|
279
|
+
const control = controlValue(node, lowerTag);
|
|
280
|
+
const lead = needsSacrificialLF(node, lowerTag, control) ? '\n' : '';
|
|
144
281
|
if (node.__rawHtml != null) {
|
|
145
|
-
yield String(node.__rawHtml);
|
|
282
|
+
yield lead + String(node.__rawHtml);
|
|
283
|
+
}
|
|
284
|
+
else if (lowerTag === 'textarea' && control != null) {
|
|
285
|
+
yield lead + escapeHtml(control);
|
|
146
286
|
}
|
|
147
287
|
else {
|
|
288
|
+
if (lowerTag === 'select' && control != null)
|
|
289
|
+
markSelectSelection(node, control);
|
|
290
|
+
if (lead)
|
|
291
|
+
yield lead;
|
|
148
292
|
for (const c of node.children || [])
|
|
149
293
|
yield* streamSerializeChunks(c);
|
|
150
294
|
}
|
|
@@ -274,6 +418,16 @@ function virtualRemove(node) {
|
|
|
274
418
|
p.children.splice(idx, 1);
|
|
275
419
|
node.parentNode = null;
|
|
276
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Replace an element's content with escaped text (`v-text` / `el.textContent`).
|
|
423
|
+
* Direct emit writes `textContent` for both static and reactive text, so SSR
|
|
424
|
+
* must model it or the first response ships an empty element.
|
|
425
|
+
*/
|
|
426
|
+
function replaceSerializeText(node, value) {
|
|
427
|
+
const text = value == null ? '' : String(value);
|
|
428
|
+
node.__rawHtml = null;
|
|
429
|
+
node.children = text === '' ? [] : [{ __kind: 'text', value: text, parentNode: node }];
|
|
430
|
+
}
|
|
277
431
|
function makeVirtualEl(tag) {
|
|
278
432
|
const node = {
|
|
279
433
|
__kind: 'el',
|
|
@@ -282,6 +436,7 @@ function makeVirtualEl(tag) {
|
|
|
282
436
|
children: [],
|
|
283
437
|
style: {},
|
|
284
438
|
__rawHtml: null,
|
|
439
|
+
__vmzKey: null,
|
|
285
440
|
parentNode: null,
|
|
286
441
|
setAttribute(name, value) {
|
|
287
442
|
applySerializeAttr(this, name, value);
|
|
@@ -305,6 +460,17 @@ function makeVirtualEl(tag) {
|
|
|
305
460
|
this.children = [];
|
|
306
461
|
},
|
|
307
462
|
});
|
|
463
|
+
// Direct emit writes `el.textContent` for v-text (static `api.setText` and
|
|
464
|
+
// reactive `el.textContent = …`). Mirror it so the serializer sees the text.
|
|
465
|
+
Object.defineProperty(node, 'textContent', {
|
|
466
|
+
configurable: true,
|
|
467
|
+
get() {
|
|
468
|
+
return nodeText(this);
|
|
469
|
+
},
|
|
470
|
+
set(value) {
|
|
471
|
+
replaceSerializeText(this, value);
|
|
472
|
+
},
|
|
473
|
+
});
|
|
308
474
|
return node;
|
|
309
475
|
}
|
|
310
476
|
/**
|
|
@@ -383,11 +549,19 @@ const serializeApi = {
|
|
|
383
549
|
serializeApi._ssrCollected.push(child);
|
|
384
550
|
return child;
|
|
385
551
|
},
|
|
386
|
-
el(tag) {
|
|
552
|
+
el(tag, key) {
|
|
387
553
|
const node = makeVirtualEl(tag);
|
|
388
554
|
if (String(tag || '').toLowerCase() === 'svg') {
|
|
389
555
|
node.namespaceURI = 'http://www.w3.org/2000/svg';
|
|
390
556
|
}
|
|
557
|
+
// Row roots need their key in the HTML so hydration reclaims by identity,
|
|
558
|
+
// not the order in which tags happen to appear.
|
|
559
|
+
if (key != null) {
|
|
560
|
+
node.__vmzKey = key;
|
|
561
|
+
if (!node.attrs)
|
|
562
|
+
node.attrs = Object.create(null);
|
|
563
|
+
node.attrs['data-vmz-key'] = String(key);
|
|
564
|
+
}
|
|
391
565
|
return node;
|
|
392
566
|
},
|
|
393
567
|
elNS(ns, tag) {
|
|
@@ -426,6 +600,16 @@ const serializeApi = {
|
|
|
426
600
|
},
|
|
427
601
|
};
|
|
428
602
|
},
|
|
603
|
+
/** Multi-root `<template v-for>` row fragment; serializes its marker pair. */
|
|
604
|
+
fragRow(key) {
|
|
605
|
+
const frag = serializeApi.frag();
|
|
606
|
+
if (key != null)
|
|
607
|
+
frag.__vmzRowKey = String(key);
|
|
608
|
+
return frag;
|
|
609
|
+
},
|
|
610
|
+
rowEnd() {
|
|
611
|
+
/* SSR rows are one-shot: the marker pair closes at serialization. */
|
|
612
|
+
},
|
|
429
613
|
insertBefore(parent, node, ref) {
|
|
430
614
|
virtualInsertBefore(parent, node, ref);
|
|
431
615
|
},
|
|
@@ -475,6 +659,15 @@ const serializeApi = {
|
|
|
475
659
|
attr(el, name, value) {
|
|
476
660
|
if (!el || el.__kind !== 'el')
|
|
477
661
|
return;
|
|
662
|
+
// `v-pre`: keep the authored name and value — no `className` → `class`
|
|
663
|
+
// rewrite, no class/style merge, no boolean-attribute normalization.
|
|
664
|
+
if (el.__vmzVerbatim) {
|
|
665
|
+
if (value == null || value === false)
|
|
666
|
+
delete el.attrs[name];
|
|
667
|
+
else
|
|
668
|
+
el.attrs[name] = value === true ? '' : String(value);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
478
671
|
applySerializeAttr(el, name, value);
|
|
479
672
|
},
|
|
480
673
|
mergeClass(...parts) {
|
|
@@ -492,6 +685,9 @@ const serializeApi = {
|
|
|
492
685
|
adoptEnter() {
|
|
493
686
|
return false;
|
|
494
687
|
},
|
|
688
|
+
adoptEnterSlot() {
|
|
689
|
+
return false;
|
|
690
|
+
},
|
|
495
691
|
adoptLeave() {
|
|
496
692
|
/* resume scope only */
|
|
497
693
|
},
|
|
@@ -529,6 +725,26 @@ const serializeApi = {
|
|
|
529
725
|
bindComponentProp() {
|
|
530
726
|
/* SSR: props already resolved into the child instance at create */
|
|
531
727
|
},
|
|
728
|
+
bindComponentProps() {
|
|
729
|
+
/* SSR: `...api.spreadProps()` already delivered the object props at create */
|
|
730
|
+
},
|
|
731
|
+
bindComponentEvents() {
|
|
732
|
+
/* component events attach only on the client */
|
|
733
|
+
},
|
|
734
|
+
bindObjectListeners() {
|
|
735
|
+
/* object listeners attach only on the client */
|
|
736
|
+
},
|
|
737
|
+
spreadProps(source) {
|
|
738
|
+
const out = Object.create(null);
|
|
739
|
+
if (source && typeof source === 'object') {
|
|
740
|
+
for (const key of Object.keys(source)) {
|
|
741
|
+
if (!key || key.startsWith('#'))
|
|
742
|
+
continue;
|
|
743
|
+
out[key] = source[key];
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return out;
|
|
747
|
+
},
|
|
532
748
|
projectDefaultSlot(hostEl, node) {
|
|
533
749
|
if (!hostEl || node == null)
|
|
534
750
|
return;
|
|
@@ -552,8 +768,13 @@ const serializeApi = {
|
|
|
552
768
|
target.__rawHtml = null;
|
|
553
769
|
if (!Array.isArray(target.children))
|
|
554
770
|
target.children = [];
|
|
555
|
-
//
|
|
556
|
-
//
|
|
771
|
+
// The client replaces the live `<slot>` with the first projected node,
|
|
772
|
+
// which discards its fallback children. Drop the fallback on the first
|
|
773
|
+
// projection too, then accumulate later projections as siblings.
|
|
774
|
+
if (!filledSerializeSlots.has(target)) {
|
|
775
|
+
filledSerializeSlots.add(target);
|
|
776
|
+
target.children.length = 0;
|
|
777
|
+
}
|
|
557
778
|
target.children.push(node);
|
|
558
779
|
return;
|
|
559
780
|
}
|
|
@@ -612,6 +833,23 @@ const serializeApi = {
|
|
|
612
833
|
el.__rawHtml = value == null ? '' : String(value);
|
|
613
834
|
el.children = [];
|
|
614
835
|
},
|
|
836
|
+
// Emitted `v-html` write. SSR always records the payload, and the open tag
|
|
837
|
+
// gains `data-vmz-html-hash` from that exact string at serialize time.
|
|
838
|
+
html(el, value) {
|
|
839
|
+
serializeApi.setHtml(el, value);
|
|
840
|
+
},
|
|
841
|
+
setText(el, value) {
|
|
842
|
+
if (!el || el.__kind !== 'el')
|
|
843
|
+
return;
|
|
844
|
+
replaceSerializeText(el, value);
|
|
845
|
+
},
|
|
846
|
+
setShow(el, condition) {
|
|
847
|
+
if (!el || el.__kind !== 'el')
|
|
848
|
+
return;
|
|
849
|
+
if (!el.style || typeof el.style !== 'object')
|
|
850
|
+
el.style = {};
|
|
851
|
+
el.style.display = condition ? '' : 'none';
|
|
852
|
+
},
|
|
615
853
|
bindHtml(inst, bindingId, deps, get, el) {
|
|
616
854
|
serializeApi.trackPatch(inst, deps || [], function bindHtmlPatch() {
|
|
617
855
|
let raw;
|
|
@@ -624,14 +862,20 @@ const serializeApi = {
|
|
|
624
862
|
serializeApi.setHtml(el, raw);
|
|
625
863
|
}, bindingId);
|
|
626
864
|
},
|
|
627
|
-
component(hostInst, nameOrCtor, props, client) {
|
|
865
|
+
component(hostInst, nameOrCtor, props, client, key) {
|
|
628
866
|
const name = typeof nameOrCtor === 'function' ? nameOrCtor.__vmzTag || nameOrCtor.name || 'Component' : nameOrCtor;
|
|
629
867
|
const Ctor = typeof nameOrCtor === 'function' ? nameOrCtor : getRegisteredComponent(name);
|
|
630
868
|
if (!Ctor) {
|
|
631
869
|
return serializeUnknownComponentNode(name);
|
|
632
870
|
}
|
|
871
|
+
const rawProps = (props || {});
|
|
872
|
+
// `id` / `data-*` / `aria-*` belong on the host element, matching the
|
|
873
|
+
// browser `api.component` fallthrough.
|
|
874
|
+
const hostAttrs = rawProps.__hostAttrs;
|
|
633
875
|
const resolved = {};
|
|
634
|
-
for (const [k, v] of Object.entries(
|
|
876
|
+
for (const [k, v] of Object.entries(rawProps)) {
|
|
877
|
+
if (k === '__hostAttrs')
|
|
878
|
+
continue;
|
|
635
879
|
const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
|
|
636
880
|
if (onKey)
|
|
637
881
|
continue;
|
|
@@ -640,6 +884,16 @@ const serializeApi = {
|
|
|
640
884
|
else
|
|
641
885
|
resolved[k] = v;
|
|
642
886
|
}
|
|
887
|
+
const applyHostAttrs = (attrs) => {
|
|
888
|
+
if (!hostAttrs)
|
|
889
|
+
return attrs;
|
|
890
|
+
for (const [k, v] of Object.entries(hostAttrs)) {
|
|
891
|
+
if (v == null || v === false)
|
|
892
|
+
continue;
|
|
893
|
+
attrs[k] = v === true ? '' : String(v);
|
|
894
|
+
}
|
|
895
|
+
return attrs;
|
|
896
|
+
};
|
|
643
897
|
if (client) {
|
|
644
898
|
// resume: Island SSR includes body + ResumeEntry slice (same Direct schedule).
|
|
645
899
|
const child = serializeApi._ssrChildInstance(Ctor, resolved);
|
|
@@ -675,12 +929,15 @@ const serializeApi = {
|
|
|
675
929
|
const boxStyle = directHostBoxStyleAttr(name, Ctor);
|
|
676
930
|
if (boxStyle)
|
|
677
931
|
attrs.style = boxStyle;
|
|
932
|
+
applyHostAttrs(attrs);
|
|
933
|
+
if (key != null)
|
|
934
|
+
attrs['data-vmz-key'] = String(key);
|
|
678
935
|
if (isEventEntryStrategy(String(client))) {
|
|
679
936
|
attrs['data-vmz-entry'] = 'event';
|
|
680
937
|
}
|
|
681
938
|
return {
|
|
682
939
|
__kind: 'el',
|
|
683
|
-
tag:
|
|
940
|
+
tag: resolveDirectHostTag(name, Ctor),
|
|
684
941
|
attrs,
|
|
685
942
|
children: body ? [body] : [],
|
|
686
943
|
appendChild(c) {
|
|
@@ -705,9 +962,12 @@ const serializeApi = {
|
|
|
705
962
|
const boxStyle = directHostBoxStyleAttr(name, Ctor);
|
|
706
963
|
if (boxStyle)
|
|
707
964
|
attrs.style = boxStyle;
|
|
965
|
+
applyHostAttrs(attrs);
|
|
966
|
+
if (key != null)
|
|
967
|
+
attrs['data-vmz-key'] = String(key);
|
|
708
968
|
return {
|
|
709
969
|
__kind: 'el',
|
|
710
|
-
tag:
|
|
970
|
+
tag: resolveDirectHostTag(name, Ctor),
|
|
711
971
|
attrs,
|
|
712
972
|
children: node ? [node] : [],
|
|
713
973
|
appendChild(c) {
|
|
@@ -723,6 +983,28 @@ const serializeApi = {
|
|
|
723
983
|
}
|
|
724
984
|
throw new Error(`vmz:dom serialize component <${name}> requires __vmzCreate (rebuild child with Direct)`);
|
|
725
985
|
},
|
|
986
|
+
/**
|
|
987
|
+
* `<component :is>` on the server: `is` and props are evaluated once at
|
|
988
|
+
* create time, so the dynamic boundary serializes exactly like the resolved
|
|
989
|
+
* static component.
|
|
990
|
+
*/
|
|
991
|
+
dynamicComponent(hostInst, bindingId, deps, getCtor, getProps, key) {
|
|
992
|
+
let Ctor = null;
|
|
993
|
+
try {
|
|
994
|
+
Ctor = getCtor.call(hostInst);
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
Ctor = null;
|
|
998
|
+
}
|
|
999
|
+
let props = {};
|
|
1000
|
+
try {
|
|
1001
|
+
props = getProps.call(hostInst) || {};
|
|
1002
|
+
}
|
|
1003
|
+
catch {
|
|
1004
|
+
props = {};
|
|
1005
|
+
}
|
|
1006
|
+
return serializeApi.component(hostInst, Ctor, props, null, key);
|
|
1007
|
+
},
|
|
726
1008
|
};
|
|
727
1009
|
function applySerializeAttr(el, name, value) {
|
|
728
1010
|
if (!el || el.__kind !== 'el')
|
|
@@ -751,11 +1033,69 @@ function applySerializeAttr(el, name, value) {
|
|
|
751
1033
|
el.attrs[key] = value === true ? '' : String(value);
|
|
752
1034
|
return;
|
|
753
1035
|
}
|
|
1036
|
+
// A `multiple` select model is a list. Stringifying it here would emit one
|
|
1037
|
+
// `value="c,a"` attribute, so `markSelectSelection` could never match an
|
|
1038
|
+
// option and SSR shipped a select with nothing selected.
|
|
1039
|
+
if (key === 'value' && String(el.tag || '').toLowerCase() === 'select' && Array.isArray(value)) {
|
|
1040
|
+
el.attrs[key] = value.map((item) => String(item));
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
754
1043
|
if (value == null || value === false)
|
|
755
1044
|
delete el.attrs[key];
|
|
756
1045
|
else
|
|
757
1046
|
el.attrs[key] = value === true ? '' : String(value);
|
|
758
1047
|
}
|
|
759
1048
|
function escapeHtml(s) {
|
|
760
|
-
return s
|
|
1049
|
+
return String(s)
|
|
1050
|
+
.replace(/&/g, '&')
|
|
1051
|
+
.replace(/</g, '<')
|
|
1052
|
+
.replace(/>/g, '>')
|
|
1053
|
+
.replace(/"/g, '"')
|
|
1054
|
+
// The HTML input stream normalizes a literal CR (and CRLF) to LF before
|
|
1055
|
+
// tokenization, so a raw `\r` would not survive the parse. Emit a character
|
|
1056
|
+
// reference instead: hydration then keeps the exact server value.
|
|
1057
|
+
.replace(/\r/g, ' ');
|
|
1058
|
+
}
|
|
1059
|
+
/** `<pre>` / `<textarea>` drop one leading LF after their start tag (HTML spec). */
|
|
1060
|
+
function dropsLeadingLF(tag) {
|
|
1061
|
+
return tag === 'pre' || tag === 'textarea';
|
|
1062
|
+
}
|
|
1063
|
+
/** Does the first token inside `node` start with LF? Comments and tags do not. */
|
|
1064
|
+
function firstTokenStartsWithLF(node) {
|
|
1065
|
+
if (node == null || node === false)
|
|
1066
|
+
return false;
|
|
1067
|
+
if (typeof node === 'string')
|
|
1068
|
+
return node.startsWith('\n');
|
|
1069
|
+
if (node.__kind === 'text')
|
|
1070
|
+
return String(node.value ?? '').startsWith('\n');
|
|
1071
|
+
if (node.__kind === 'comment')
|
|
1072
|
+
return false;
|
|
1073
|
+
if (node.__kind === 'frag') {
|
|
1074
|
+
if (node.__rawHtml != null)
|
|
1075
|
+
return String(node.__rawHtml).startsWith('\n');
|
|
1076
|
+
for (const c of node.children || []) {
|
|
1077
|
+
if (c == null || c === false)
|
|
1078
|
+
continue;
|
|
1079
|
+
return firstTokenStartsWithLF(c);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* A value that itself starts with LF needs one sacrificial newline: the parser
|
|
1086
|
+
* discards the first LF token, leaving the real one in the DOM.
|
|
1087
|
+
*/
|
|
1088
|
+
function needsSacrificialLF(node, lowerTag, control) {
|
|
1089
|
+
if (!dropsLeadingLF(lowerTag))
|
|
1090
|
+
return false;
|
|
1091
|
+
if (lowerTag === 'textarea' && control != null)
|
|
1092
|
+
return String(control).startsWith('\n');
|
|
1093
|
+
if (node.__rawHtml != null)
|
|
1094
|
+
return String(node.__rawHtml).startsWith('\n');
|
|
1095
|
+
for (const c of node.children || []) {
|
|
1096
|
+
if (c == null || c === false)
|
|
1097
|
+
continue;
|
|
1098
|
+
return firstTokenStartsWithLF(c);
|
|
1099
|
+
}
|
|
1100
|
+
return false;
|
|
761
1101
|
}
|
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.15",
|
|
72
|
+
"@vmz/vmz-darwin-x64": "0.3.15",
|
|
73
|
+
"@vmz/vmz-linux-arm64": "0.3.15",
|
|
74
|
+
"@vmz/vmz-linux-x64": "0.3.15",
|
|
75
|
+
"@vmz/vmz-win32-arm64": "0.3.15",
|
|
76
|
+
"@vmz/vmz-win32-x64": "0.3.15"
|
|
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.15"
|
|
92
92
|
}
|