@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
package/dist/ssr/serialize.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
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
|
+
import { splitStyleDeclarations } from '../shared/dom-attr-normalize.js';
|
|
5
|
+
import { HTML_HASH_ATTR, htmlDigest } from '../shared/html-digest.js';
|
|
4
6
|
import { _ssrDocumentLastError, ensureSsrDocument } from './document.js';
|
|
5
7
|
import { awaitSsrOnMount, ssrComponentName, ssrOnMountContext } from './onmount.js';
|
|
6
8
|
function runDirectSerializeTree(Component, inst) {
|
|
@@ -56,17 +58,116 @@ const VOID_TAGS = new Set([
|
|
|
56
58
|
function isVoidTag(tag) {
|
|
57
59
|
return VOID_TAGS.has(String(tag).toLowerCase());
|
|
58
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* `textarea` carries its initial value as text content and `select` carries it
|
|
63
|
+
* as a selected option, so neither can travel as a `value` attribute. `input`
|
|
64
|
+
* keeps the attribute, which browsers honor for the default value.
|
|
65
|
+
*/
|
|
66
|
+
function controlValue(node, tag) {
|
|
67
|
+
if (tag !== 'textarea' && tag !== 'select')
|
|
68
|
+
return null;
|
|
69
|
+
const v = node.attrs ? node.attrs.value : null;
|
|
70
|
+
if (v == null || v === false)
|
|
71
|
+
return null;
|
|
72
|
+
// A `multiple` select model is a list; keep every entry addressable.
|
|
73
|
+
if (tag === 'select' && Array.isArray(v))
|
|
74
|
+
return v.map((item) => String(item));
|
|
75
|
+
return String(v);
|
|
76
|
+
}
|
|
77
|
+
function nodeText(node) {
|
|
78
|
+
if (!node || typeof node !== 'object')
|
|
79
|
+
return '';
|
|
80
|
+
if (node.__kind === 'text')
|
|
81
|
+
return String(node.value ?? '');
|
|
82
|
+
if (node.__kind === 'el' || node.__kind === 'frag') {
|
|
83
|
+
return (node.children || []).map(nodeText).join('');
|
|
84
|
+
}
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
/** Mark the `<option>` whose value (or text) equals one of the model values. */
|
|
88
|
+
function markSelectSelection(node, wanted) {
|
|
89
|
+
const wants = Array.isArray(wanted) ? wanted.map((item) => String(item)) : [String(wanted)];
|
|
90
|
+
const visit = (n) => {
|
|
91
|
+
if (!n || typeof n !== 'object')
|
|
92
|
+
return;
|
|
93
|
+
if (n.__kind === 'el') {
|
|
94
|
+
const tag = String(n.tag || '').toLowerCase();
|
|
95
|
+
if (tag === 'option') {
|
|
96
|
+
const raw = n.attrs && n.attrs.value != null ? String(n.attrs.value) : nodeText(n).trim();
|
|
97
|
+
if (wants.includes(raw)) {
|
|
98
|
+
if (!n.attrs)
|
|
99
|
+
n.attrs = {};
|
|
100
|
+
n.attrs.selected = '';
|
|
101
|
+
}
|
|
102
|
+
else if (n.attrs && Object.prototype.hasOwnProperty.call(n.attrs, 'selected')) {
|
|
103
|
+
delete n.attrs.selected;
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const c of n.children || [])
|
|
109
|
+
visit(c);
|
|
110
|
+
};
|
|
111
|
+
for (const c of node.children || [])
|
|
112
|
+
visit(c);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Inline style as the browser would serialize it: the `style` attribute merged
|
|
116
|
+
* with the live `style.*` writes that Direct patches and `v-show` perform
|
|
117
|
+
* (`el.style.display = …`). SSR virtual elements only kept the attribute, so a
|
|
118
|
+
* server-first response showed elements that hydration hid.
|
|
119
|
+
*/
|
|
120
|
+
function serializeStyleAttr(node) {
|
|
121
|
+
const declared = node.attrs && node.attrs.style != null ? String(node.attrs.style) : '';
|
|
122
|
+
const live = node.style;
|
|
123
|
+
if (!live || typeof live !== 'object')
|
|
124
|
+
return declared;
|
|
125
|
+
const display = live.display;
|
|
126
|
+
if (display === undefined)
|
|
127
|
+
return declared;
|
|
128
|
+
// `el.style.display = ''` removes the declaration instead of writing it.
|
|
129
|
+
if (display === '' || display == null) {
|
|
130
|
+
return splitStyleDeclarations(declared)
|
|
131
|
+
.filter((chunk) => {
|
|
132
|
+
const idx = chunk.indexOf(':');
|
|
133
|
+
return idx < 0 || chunk.slice(0, idx).trim() !== 'display';
|
|
134
|
+
})
|
|
135
|
+
.map((chunk) => chunk.trim())
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.join('; ');
|
|
138
|
+
}
|
|
139
|
+
return mergeStyleParts(declared, { display });
|
|
140
|
+
}
|
|
59
141
|
function serializeOpenTag(node) {
|
|
60
142
|
const tag = node.tag || 'div';
|
|
143
|
+
const lowerTag = String(tag).toLowerCase();
|
|
61
144
|
let attrs = '';
|
|
62
145
|
for (const [k, v] of Object.entries(node.attrs || {})) {
|
|
63
146
|
if (v == null || v === false)
|
|
64
147
|
continue;
|
|
148
|
+
// `textarea` value renders as text content and `select` value as a
|
|
149
|
+
// selected option, so the raw attribute must not be emitted.
|
|
150
|
+
if (k === 'value' && (lowerTag === 'textarea' || lowerTag === 'select'))
|
|
151
|
+
continue;
|
|
152
|
+
if (k === 'style')
|
|
153
|
+
continue;
|
|
154
|
+
// Always replaced by the digest of the payload written below, so an
|
|
155
|
+
// author-supplied marker can never mask a different payload.
|
|
156
|
+
if (k === HTML_HASH_ATTR)
|
|
157
|
+
continue;
|
|
65
158
|
if (k === 'className')
|
|
66
159
|
attrs += ` class="${escapeHtml(v)}"`;
|
|
67
160
|
else
|
|
68
161
|
attrs += ` ${k}="${escapeHtml(v)}"`;
|
|
69
162
|
}
|
|
163
|
+
const style = serializeStyleAttr(node);
|
|
164
|
+
if (style)
|
|
165
|
+
attrs += ` style="${escapeHtml(style)}"`;
|
|
166
|
+
// `v-html` hosts carry the digest of the exact string SSR wrote, so the
|
|
167
|
+
// client can keep the SSR subtree without DOM-serializing its payload.
|
|
168
|
+
if (node.__rawHtml != null && !node.__rawOuter) {
|
|
169
|
+
attrs += ` ${HTML_HASH_ATTR}="${escapeHtml(htmlDigest(String(node.__rawHtml)))}"`;
|
|
170
|
+
}
|
|
70
171
|
return { tag, open: `<${tag}${attrs}>` };
|
|
71
172
|
}
|
|
72
173
|
export function flattenSerializeNode(node) {
|
|
@@ -94,11 +195,22 @@ export function flattenSerializeNode(node) {
|
|
|
94
195
|
if (node.__rawOuter && node.__rawHtml != null)
|
|
95
196
|
return String(node.__rawHtml);
|
|
96
197
|
const { open } = serializeOpenTag(node);
|
|
198
|
+
const lowerTag = String(tag).toLowerCase();
|
|
199
|
+
const control = controlValue(node, lowerTag);
|
|
97
200
|
if (node.__rawHtml != null) {
|
|
98
|
-
|
|
201
|
+
const lead = needsSacrificialLF(node, lowerTag, null) ? '\n' : '';
|
|
202
|
+
const raw = String(node.__rawHtml);
|
|
203
|
+
return isVoidTag(tag) ? `${open}${raw}` : `${open}${lead}${raw}</${tag}>`;
|
|
99
204
|
}
|
|
205
|
+
if (lowerTag === 'textarea' && control != null) {
|
|
206
|
+
const lead = needsSacrificialLF(node, lowerTag, control) ? '\n' : '';
|
|
207
|
+
return `${open}${lead}${escapeHtml(control)}</${tag}>`;
|
|
208
|
+
}
|
|
209
|
+
if (lowerTag === 'select' && control != null)
|
|
210
|
+
markSelectSelection(node, control);
|
|
100
211
|
const inner = (node.children || []).map(flattenSerializeNode).join('');
|
|
101
|
-
|
|
212
|
+
const lead = needsSacrificialLF(node, lowerTag, control) ? '\n' : '';
|
|
213
|
+
return isVoidTag(tag) ? `${open}${inner}` : `${open}${lead}${inner}</${tag}>`;
|
|
102
214
|
}
|
|
103
215
|
return '';
|
|
104
216
|
}
|
|
@@ -141,10 +253,20 @@ export function* streamSerializeChunks(node) {
|
|
|
141
253
|
}
|
|
142
254
|
const { open } = serializeOpenTag(node);
|
|
143
255
|
yield open;
|
|
256
|
+
const lowerTag = String(tag).toLowerCase();
|
|
257
|
+
const control = controlValue(node, lowerTag);
|
|
258
|
+
const lead = needsSacrificialLF(node, lowerTag, control) ? '\n' : '';
|
|
144
259
|
if (node.__rawHtml != null) {
|
|
145
|
-
yield String(node.__rawHtml);
|
|
260
|
+
yield lead + String(node.__rawHtml);
|
|
261
|
+
}
|
|
262
|
+
else if (lowerTag === 'textarea' && control != null) {
|
|
263
|
+
yield lead + escapeHtml(control);
|
|
146
264
|
}
|
|
147
265
|
else {
|
|
266
|
+
if (lowerTag === 'select' && control != null)
|
|
267
|
+
markSelectSelection(node, control);
|
|
268
|
+
if (lead)
|
|
269
|
+
yield lead;
|
|
148
270
|
for (const c of node.children || [])
|
|
149
271
|
yield* streamSerializeChunks(c);
|
|
150
272
|
}
|
|
@@ -274,6 +396,16 @@ function virtualRemove(node) {
|
|
|
274
396
|
p.children.splice(idx, 1);
|
|
275
397
|
node.parentNode = null;
|
|
276
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Replace an element's content with escaped text (`v-text` / `el.textContent`).
|
|
401
|
+
* Direct emit writes `textContent` for both static and reactive text, so SSR
|
|
402
|
+
* must model it or the first response ships an empty element.
|
|
403
|
+
*/
|
|
404
|
+
function replaceSerializeText(node, value) {
|
|
405
|
+
const text = value == null ? '' : String(value);
|
|
406
|
+
node.__rawHtml = null;
|
|
407
|
+
node.children = text === '' ? [] : [{ __kind: 'text', value: text, parentNode: node }];
|
|
408
|
+
}
|
|
277
409
|
function makeVirtualEl(tag) {
|
|
278
410
|
const node = {
|
|
279
411
|
__kind: 'el',
|
|
@@ -282,6 +414,7 @@ function makeVirtualEl(tag) {
|
|
|
282
414
|
children: [],
|
|
283
415
|
style: {},
|
|
284
416
|
__rawHtml: null,
|
|
417
|
+
__vmzKey: null,
|
|
285
418
|
parentNode: null,
|
|
286
419
|
setAttribute(name, value) {
|
|
287
420
|
applySerializeAttr(this, name, value);
|
|
@@ -305,6 +438,17 @@ function makeVirtualEl(tag) {
|
|
|
305
438
|
this.children = [];
|
|
306
439
|
},
|
|
307
440
|
});
|
|
441
|
+
// Direct emit writes `el.textContent` for v-text (static `api.setText` and
|
|
442
|
+
// reactive `el.textContent = …`). Mirror it so the serializer sees the text.
|
|
443
|
+
Object.defineProperty(node, 'textContent', {
|
|
444
|
+
configurable: true,
|
|
445
|
+
get() {
|
|
446
|
+
return nodeText(this);
|
|
447
|
+
},
|
|
448
|
+
set(value) {
|
|
449
|
+
replaceSerializeText(this, value);
|
|
450
|
+
},
|
|
451
|
+
});
|
|
308
452
|
return node;
|
|
309
453
|
}
|
|
310
454
|
/**
|
|
@@ -383,11 +527,19 @@ const serializeApi = {
|
|
|
383
527
|
serializeApi._ssrCollected.push(child);
|
|
384
528
|
return child;
|
|
385
529
|
},
|
|
386
|
-
el(tag) {
|
|
530
|
+
el(tag, key) {
|
|
387
531
|
const node = makeVirtualEl(tag);
|
|
388
532
|
if (String(tag || '').toLowerCase() === 'svg') {
|
|
389
533
|
node.namespaceURI = 'http://www.w3.org/2000/svg';
|
|
390
534
|
}
|
|
535
|
+
// Row roots need their key in the HTML so hydration reclaims by identity,
|
|
536
|
+
// not the order in which tags happen to appear.
|
|
537
|
+
if (key != null) {
|
|
538
|
+
node.__vmzKey = key;
|
|
539
|
+
if (!node.attrs)
|
|
540
|
+
node.attrs = Object.create(null);
|
|
541
|
+
node.attrs['data-vmz-key'] = String(key);
|
|
542
|
+
}
|
|
391
543
|
return node;
|
|
392
544
|
},
|
|
393
545
|
elNS(ns, tag) {
|
|
@@ -529,6 +681,26 @@ const serializeApi = {
|
|
|
529
681
|
bindComponentProp() {
|
|
530
682
|
/* SSR: props already resolved into the child instance at create */
|
|
531
683
|
},
|
|
684
|
+
bindComponentProps() {
|
|
685
|
+
/* SSR: `...api.spreadProps()` already delivered the object props at create */
|
|
686
|
+
},
|
|
687
|
+
bindComponentEvents() {
|
|
688
|
+
/* component events attach only on the client */
|
|
689
|
+
},
|
|
690
|
+
bindObjectListeners() {
|
|
691
|
+
/* object listeners attach only on the client */
|
|
692
|
+
},
|
|
693
|
+
spreadProps(source) {
|
|
694
|
+
const out = Object.create(null);
|
|
695
|
+
if (source && typeof source === 'object') {
|
|
696
|
+
for (const key of Object.keys(source)) {
|
|
697
|
+
if (!key || key.startsWith('#'))
|
|
698
|
+
continue;
|
|
699
|
+
out[key] = source[key];
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return out;
|
|
703
|
+
},
|
|
532
704
|
projectDefaultSlot(hostEl, node) {
|
|
533
705
|
if (!hostEl || node == null)
|
|
534
706
|
return;
|
|
@@ -552,8 +724,13 @@ const serializeApi = {
|
|
|
552
724
|
target.__rawHtml = null;
|
|
553
725
|
if (!Array.isArray(target.children))
|
|
554
726
|
target.children = [];
|
|
555
|
-
//
|
|
556
|
-
//
|
|
727
|
+
// The client replaces the live `<slot>` with the first projected node,
|
|
728
|
+
// which discards its fallback children. Drop the fallback on the first
|
|
729
|
+
// projection too, then accumulate later projections as siblings.
|
|
730
|
+
if (!filledSerializeSlots.has(target)) {
|
|
731
|
+
filledSerializeSlots.add(target);
|
|
732
|
+
target.children.length = 0;
|
|
733
|
+
}
|
|
557
734
|
target.children.push(node);
|
|
558
735
|
return;
|
|
559
736
|
}
|
|
@@ -612,6 +789,23 @@ const serializeApi = {
|
|
|
612
789
|
el.__rawHtml = value == null ? '' : String(value);
|
|
613
790
|
el.children = [];
|
|
614
791
|
},
|
|
792
|
+
// Emitted `v-html` write. SSR always records the payload, and the open tag
|
|
793
|
+
// gains `data-vmz-html-hash` from that exact string at serialize time.
|
|
794
|
+
html(el, value) {
|
|
795
|
+
serializeApi.setHtml(el, value);
|
|
796
|
+
},
|
|
797
|
+
setText(el, value) {
|
|
798
|
+
if (!el || el.__kind !== 'el')
|
|
799
|
+
return;
|
|
800
|
+
replaceSerializeText(el, value);
|
|
801
|
+
},
|
|
802
|
+
setShow(el, condition) {
|
|
803
|
+
if (!el || el.__kind !== 'el')
|
|
804
|
+
return;
|
|
805
|
+
if (!el.style || typeof el.style !== 'object')
|
|
806
|
+
el.style = {};
|
|
807
|
+
el.style.display = condition ? '' : 'none';
|
|
808
|
+
},
|
|
615
809
|
bindHtml(inst, bindingId, deps, get, el) {
|
|
616
810
|
serializeApi.trackPatch(inst, deps || [], function bindHtmlPatch() {
|
|
617
811
|
let raw;
|
|
@@ -624,14 +818,20 @@ const serializeApi = {
|
|
|
624
818
|
serializeApi.setHtml(el, raw);
|
|
625
819
|
}, bindingId);
|
|
626
820
|
},
|
|
627
|
-
component(hostInst, nameOrCtor, props, client) {
|
|
821
|
+
component(hostInst, nameOrCtor, props, client, key) {
|
|
628
822
|
const name = typeof nameOrCtor === 'function' ? nameOrCtor.__vmzTag || nameOrCtor.name || 'Component' : nameOrCtor;
|
|
629
823
|
const Ctor = typeof nameOrCtor === 'function' ? nameOrCtor : getRegisteredComponent(name);
|
|
630
824
|
if (!Ctor) {
|
|
631
825
|
return serializeUnknownComponentNode(name);
|
|
632
826
|
}
|
|
827
|
+
const rawProps = (props || {});
|
|
828
|
+
// `id` / `data-*` / `aria-*` belong on the host element, matching the
|
|
829
|
+
// browser `api.component` fallthrough.
|
|
830
|
+
const hostAttrs = rawProps.__hostAttrs;
|
|
633
831
|
const resolved = {};
|
|
634
|
-
for (const [k, v] of Object.entries(
|
|
832
|
+
for (const [k, v] of Object.entries(rawProps)) {
|
|
833
|
+
if (k === '__hostAttrs')
|
|
834
|
+
continue;
|
|
635
835
|
const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
|
|
636
836
|
if (onKey)
|
|
637
837
|
continue;
|
|
@@ -640,6 +840,16 @@ const serializeApi = {
|
|
|
640
840
|
else
|
|
641
841
|
resolved[k] = v;
|
|
642
842
|
}
|
|
843
|
+
const applyHostAttrs = (attrs) => {
|
|
844
|
+
if (!hostAttrs)
|
|
845
|
+
return attrs;
|
|
846
|
+
for (const [k, v] of Object.entries(hostAttrs)) {
|
|
847
|
+
if (v == null || v === false)
|
|
848
|
+
continue;
|
|
849
|
+
attrs[k] = v === true ? '' : String(v);
|
|
850
|
+
}
|
|
851
|
+
return attrs;
|
|
852
|
+
};
|
|
643
853
|
if (client) {
|
|
644
854
|
// resume: Island SSR includes body + ResumeEntry slice (same Direct schedule).
|
|
645
855
|
const child = serializeApi._ssrChildInstance(Ctor, resolved);
|
|
@@ -675,6 +885,9 @@ const serializeApi = {
|
|
|
675
885
|
const boxStyle = directHostBoxStyleAttr(name, Ctor);
|
|
676
886
|
if (boxStyle)
|
|
677
887
|
attrs.style = boxStyle;
|
|
888
|
+
applyHostAttrs(attrs);
|
|
889
|
+
if (key != null)
|
|
890
|
+
attrs['data-vmz-key'] = String(key);
|
|
678
891
|
if (isEventEntryStrategy(String(client))) {
|
|
679
892
|
attrs['data-vmz-entry'] = 'event';
|
|
680
893
|
}
|
|
@@ -705,6 +918,9 @@ const serializeApi = {
|
|
|
705
918
|
const boxStyle = directHostBoxStyleAttr(name, Ctor);
|
|
706
919
|
if (boxStyle)
|
|
707
920
|
attrs.style = boxStyle;
|
|
921
|
+
applyHostAttrs(attrs);
|
|
922
|
+
if (key != null)
|
|
923
|
+
attrs['data-vmz-key'] = String(key);
|
|
708
924
|
return {
|
|
709
925
|
__kind: 'el',
|
|
710
926
|
tag: 'div',
|
|
@@ -723,6 +939,28 @@ const serializeApi = {
|
|
|
723
939
|
}
|
|
724
940
|
throw new Error(`vmz:dom serialize component <${name}> requires __vmzCreate (rebuild child with Direct)`);
|
|
725
941
|
},
|
|
942
|
+
/**
|
|
943
|
+
* `<component :is>` on the server: `is` and props are evaluated once at
|
|
944
|
+
* create time, so the dynamic boundary serializes exactly like the resolved
|
|
945
|
+
* static component.
|
|
946
|
+
*/
|
|
947
|
+
dynamicComponent(hostInst, bindingId, deps, getCtor, getProps, key) {
|
|
948
|
+
let Ctor = null;
|
|
949
|
+
try {
|
|
950
|
+
Ctor = getCtor.call(hostInst);
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
Ctor = null;
|
|
954
|
+
}
|
|
955
|
+
let props = {};
|
|
956
|
+
try {
|
|
957
|
+
props = getProps.call(hostInst) || {};
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
props = {};
|
|
961
|
+
}
|
|
962
|
+
return serializeApi.component(hostInst, Ctor, props, null, key);
|
|
963
|
+
},
|
|
726
964
|
};
|
|
727
965
|
function applySerializeAttr(el, name, value) {
|
|
728
966
|
if (!el || el.__kind !== 'el')
|
|
@@ -751,11 +989,69 @@ function applySerializeAttr(el, name, value) {
|
|
|
751
989
|
el.attrs[key] = value === true ? '' : String(value);
|
|
752
990
|
return;
|
|
753
991
|
}
|
|
992
|
+
// A `multiple` select model is a list. Stringifying it here would emit one
|
|
993
|
+
// `value="c,a"` attribute, so `markSelectSelection` could never match an
|
|
994
|
+
// option and SSR shipped a select with nothing selected.
|
|
995
|
+
if (key === 'value' && String(el.tag || '').toLowerCase() === 'select' && Array.isArray(value)) {
|
|
996
|
+
el.attrs[key] = value.map((item) => String(item));
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
754
999
|
if (value == null || value === false)
|
|
755
1000
|
delete el.attrs[key];
|
|
756
1001
|
else
|
|
757
1002
|
el.attrs[key] = value === true ? '' : String(value);
|
|
758
1003
|
}
|
|
759
1004
|
function escapeHtml(s) {
|
|
760
|
-
return s
|
|
1005
|
+
return String(s)
|
|
1006
|
+
.replace(/&/g, '&')
|
|
1007
|
+
.replace(/</g, '<')
|
|
1008
|
+
.replace(/>/g, '>')
|
|
1009
|
+
.replace(/"/g, '"')
|
|
1010
|
+
// The HTML input stream normalizes a literal CR (and CRLF) to LF before
|
|
1011
|
+
// tokenization, so a raw `\r` would not survive the parse. Emit a character
|
|
1012
|
+
// reference instead: hydration then keeps the exact server value.
|
|
1013
|
+
.replace(/\r/g, ' ');
|
|
1014
|
+
}
|
|
1015
|
+
/** `<pre>` / `<textarea>` drop one leading LF after their start tag (HTML spec). */
|
|
1016
|
+
function dropsLeadingLF(tag) {
|
|
1017
|
+
return tag === 'pre' || tag === 'textarea';
|
|
1018
|
+
}
|
|
1019
|
+
/** Does the first token inside `node` start with LF? Comments and tags do not. */
|
|
1020
|
+
function firstTokenStartsWithLF(node) {
|
|
1021
|
+
if (node == null || node === false)
|
|
1022
|
+
return false;
|
|
1023
|
+
if (typeof node === 'string')
|
|
1024
|
+
return node.startsWith('\n');
|
|
1025
|
+
if (node.__kind === 'text')
|
|
1026
|
+
return String(node.value ?? '').startsWith('\n');
|
|
1027
|
+
if (node.__kind === 'comment')
|
|
1028
|
+
return false;
|
|
1029
|
+
if (node.__kind === 'frag') {
|
|
1030
|
+
if (node.__rawHtml != null)
|
|
1031
|
+
return String(node.__rawHtml).startsWith('\n');
|
|
1032
|
+
for (const c of node.children || []) {
|
|
1033
|
+
if (c == null || c === false)
|
|
1034
|
+
continue;
|
|
1035
|
+
return firstTokenStartsWithLF(c);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* A value that itself starts with LF needs one sacrificial newline: the parser
|
|
1042
|
+
* discards the first LF token, leaving the real one in the DOM.
|
|
1043
|
+
*/
|
|
1044
|
+
function needsSacrificialLF(node, lowerTag, control) {
|
|
1045
|
+
if (!dropsLeadingLF(lowerTag))
|
|
1046
|
+
return false;
|
|
1047
|
+
if (lowerTag === 'textarea' && control != null)
|
|
1048
|
+
return String(control).startsWith('\n');
|
|
1049
|
+
if (node.__rawHtml != null)
|
|
1050
|
+
return String(node.__rawHtml).startsWith('\n');
|
|
1051
|
+
for (const c of node.children || []) {
|
|
1052
|
+
if (c == null || c === false)
|
|
1053
|
+
continue;
|
|
1054
|
+
return firstTokenStartsWithLF(c);
|
|
1055
|
+
}
|
|
1056
|
+
return false;
|
|
761
1057
|
}
|
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.14",
|
|
72
|
+
"@vmz/vmz-darwin-x64": "0.3.14",
|
|
73
|
+
"@vmz/vmz-linux-arm64": "0.3.14",
|
|
74
|
+
"@vmz/vmz-linux-x64": "0.3.14",
|
|
75
|
+
"@vmz/vmz-win32-arm64": "0.3.14",
|
|
76
|
+
"@vmz/vmz-win32-x64": "0.3.14"
|
|
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.14"
|
|
92
92
|
}
|