@vmz/core 0.3.14 → 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/direct-api.types.d.ts +5 -0
- package/dist/browser/direct-host-box.d.ts +11 -0
- package/dist/browser/direct-host-box.js +13 -0
- package/dist/browser/dom/direct-api.d.ts +8 -0
- package/dist/browser/dom/direct-api.js +72 -8
- package/dist/browser/dom-core.types.d.ts +9 -0
- package/dist/browser/resume/adopt.js +144 -6
- package/dist/shared/row-key.d.ts +13 -0
- package/dist/shared/row-key.js +19 -0
- package/dist/ssr/serialize.js +63 -19
- package/package.json +7 -7
|
@@ -127,6 +127,10 @@ export type DirectApi = {
|
|
|
127
127
|
el: (tag: string) => Element;
|
|
128
128
|
text: (value?: unknown) => Text;
|
|
129
129
|
frag: () => DocumentFragment;
|
|
130
|
+
/** Multi-root `<template v-for>` row fragment (scoped on resume by key). */
|
|
131
|
+
fragRow: (key?: unknown) => DocumentFragment;
|
|
132
|
+
/** Closes the row scope opened by `fragRow`. */
|
|
133
|
+
rowEnd: () => void;
|
|
130
134
|
comment: (value?: string) => Comment;
|
|
131
135
|
attr: (el: Element, name: string, value: unknown) => void;
|
|
132
136
|
mergeClass: (...parts: unknown[]) => string;
|
|
@@ -140,6 +144,7 @@ export type DirectApi = {
|
|
|
140
144
|
onMethod: (el: Element, type: string, method: string) => void;
|
|
141
145
|
onComponentEvent: (el: Element, type: string, method: string) => void;
|
|
142
146
|
adoptEnter: (node: Element) => boolean;
|
|
147
|
+
adoptEnterSlot: (hostEl: Element, name: string) => boolean;
|
|
143
148
|
adoptLeave: () => void;
|
|
144
149
|
projectDefaultSlot: (hostEl: HTMLElement, node: Node) => void;
|
|
145
150
|
projectNamedSlot: (hostEl: HTMLElement, name: string, node: Node) => void;
|
|
@@ -9,3 +9,14 @@ export declare function resolveDirectHostBox(name: string, Ctor: ComponentCtor |
|
|
|
9
9
|
export declare function applyDirectHostBox(host: HTMLElement, name: string, Ctor: ComponentCtor | null | undefined): void;
|
|
10
10
|
/** SSR attrs fragment for host box (only when contents). */
|
|
11
11
|
export declare function directHostBoxStyleAttr(name: string, Ctor: ComponentCtor | null | undefined): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Host element tag (`div` vs `span`).
|
|
14
|
+
*
|
|
15
|
+
* `display: contents` chips are phrasing content, so a `<div>` host is invalid
|
|
16
|
+
* HTML inside `<p>`. The browser parser then closes the `<p>` early and moves
|
|
17
|
+
* the host (and its trailing text) out of it, which desynchronizes the SSR tree
|
|
18
|
+
* from the client tree and makes resume rebuild those nodes. Emitting a `<span>`
|
|
19
|
+
* host keeps the SSR markup parse-stable: the parsed tree matches the tree the
|
|
20
|
+
* client `__vmzCreate` builds.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveDirectHostTag(name: string, Ctor: ComponentCtor | null | undefined): 'div' | 'span';
|
|
@@ -23,3 +23,16 @@ export function applyDirectHostBox(host, name, Ctor) {
|
|
|
23
23
|
export function directHostBoxStyleAttr(name, Ctor) {
|
|
24
24
|
return resolveDirectHostBox(name, Ctor) === 'contents' ? 'display:contents' : null;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Host element tag (`div` vs `span`).
|
|
28
|
+
*
|
|
29
|
+
* `display: contents` chips are phrasing content, so a `<div>` host is invalid
|
|
30
|
+
* HTML inside `<p>`. The browser parser then closes the `<p>` early and moves
|
|
31
|
+
* the host (and its trailing text) out of it, which desynchronizes the SSR tree
|
|
32
|
+
* from the client tree and makes resume rebuild those nodes. Emitting a `<span>`
|
|
33
|
+
* host keeps the SSR markup parse-stable: the parsed tree matches the tree the
|
|
34
|
+
* client `__vmzCreate` builds.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveDirectHostTag(name, Ctor) {
|
|
37
|
+
return resolveDirectHostBox(name, Ctor) === 'contents' ? 'span' : 'div';
|
|
38
|
+
}
|
|
@@ -25,8 +25,16 @@ export declare const directApi: {
|
|
|
25
25
|
nsScopeDepth(): number;
|
|
26
26
|
rewindNsScope(depth: any): void;
|
|
27
27
|
adoptEnter(node: any): boolean;
|
|
28
|
+
adoptEnterSlot(hostEl: any, name: any): boolean;
|
|
28
29
|
adoptLeave(): void;
|
|
29
30
|
frag(): DocumentFragment;
|
|
31
|
+
/**
|
|
32
|
+
* Multi-root `<template v-for>` row. Resume narrows adoption to the SSR
|
|
33
|
+
* marker pair for this key; a key with no pair gets an empty scope so a new
|
|
34
|
+
* row can never steal nodes that belong to another key.
|
|
35
|
+
*/
|
|
36
|
+
fragRow(key?: any): DocumentFragment;
|
|
37
|
+
rowEnd(): void;
|
|
30
38
|
comment(value: any): Comment;
|
|
31
39
|
insertBefore(parent: any, node: any, ref: any): void;
|
|
32
40
|
removeNode(node: any): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mergeClassParts, mergeStyleParts } from '../../shared/dom-attr-normalize.js';
|
|
2
2
|
import { HTML_HASH_ATTR, htmlDigest } from '../../shared/html-digest.js';
|
|
3
|
-
import { applyDirectHostBox } from '../direct-host-box.js';
|
|
3
|
+
import { applyDirectHostBox, resolveDirectHostTag } from '../direct-host-box.js';
|
|
4
4
|
import { createUnknownComponentElement } from '../unknown-component.js';
|
|
5
5
|
import { applyDomAttr, eventPropHandlerName } from './attributes.js';
|
|
6
6
|
import { createInstance, destroy, disposeDomTree, findOwnedDefaultSlotTarget, findOwnedNamedSlot, insertProjectedSlotNode, parkSlotNode, runDirectCreate, slotAnchorFor, stripFns, } from './component.js';
|
|
@@ -192,15 +192,41 @@ export const directApi = {
|
|
|
192
192
|
* new payload and refreshes the marker for later identical runs.
|
|
193
193
|
*/
|
|
194
194
|
html(el, value) {
|
|
195
|
-
|
|
195
|
+
let next;
|
|
196
|
+
if (value == null) {
|
|
197
|
+
// `null` is "this create has no payload yet", not "the payload is
|
|
198
|
+
// empty": a resumed host may still own the SSR payload, so hand the
|
|
199
|
+
// parked subtree back instead of clearing the server's markup. After
|
|
200
|
+
// the resume pass ends `_resumeAdopt` is gone and a null patch
|
|
201
|
+
// clears the host like Vue's `v-html` does.
|
|
202
|
+
const adopt = directApi._resumeAdopt;
|
|
203
|
+
if (adopt && typeof adopt.keepHtml === 'function' && adopt.keepHtml(el))
|
|
204
|
+
return;
|
|
205
|
+
// Already holding the SSR payload from an earlier null create: the
|
|
206
|
+
// create and its tracked patch both pass null, and only a real payload
|
|
207
|
+
// (or a host without one) may release the server's markup.
|
|
208
|
+
if (el.__vmzHtmlKeep)
|
|
209
|
+
return;
|
|
210
|
+
next = '';
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
el.__vmzHtmlKeep = false;
|
|
214
|
+
next = String(value);
|
|
215
|
+
}
|
|
196
216
|
const digest = htmlDigest(next);
|
|
197
217
|
const adopt = directApi._resumeAdopt;
|
|
198
218
|
if (adopt && typeof adopt.reclaimHtml === 'function' && adopt.reclaimHtml(el, digest))
|
|
199
219
|
return;
|
|
200
|
-
|
|
220
|
+
// The browser serializer expands self-closing tags (`<path/>` becomes
|
|
221
|
+
// `<path></path>`), so `innerHTML` never equals the generated string for
|
|
222
|
+
// SVG payloads. Compare the payload digest instead: an equal stamp means
|
|
223
|
+
// this host already holds exactly this payload, so a repeated patch must
|
|
224
|
+
// keep the live nodes (identity, focus, and `ResizeObserver` targets).
|
|
225
|
+
if (typeof el.getAttribute === 'function' && el.getAttribute(HTML_HASH_ATTR) === digest)
|
|
226
|
+
return;
|
|
201
227
|
if (el.innerHTML !== next)
|
|
202
228
|
el.innerHTML = next;
|
|
203
|
-
if (
|
|
229
|
+
if (typeof el.setAttribute === 'function')
|
|
204
230
|
el.setAttribute(HTML_HASH_ATTR, digest);
|
|
205
231
|
},
|
|
206
232
|
nsScopeDepth() {
|
|
@@ -222,6 +248,17 @@ export const directApi = {
|
|
|
222
248
|
}
|
|
223
249
|
return false;
|
|
224
250
|
},
|
|
251
|
+
adoptEnterSlot(hostEl, name) {
|
|
252
|
+
nsStack.push(isSvgNode(hostEl));
|
|
253
|
+
const adopt = directApi._resumeAdopt;
|
|
254
|
+
if (adopt && typeof adopt.enterSlot === 'function') {
|
|
255
|
+
const ok = adopt.enterSlot(hostEl, name);
|
|
256
|
+
if (ok)
|
|
257
|
+
adopt._enterBalance = (adopt._enterBalance || 0) + 1;
|
|
258
|
+
return ok;
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
},
|
|
225
262
|
adoptLeave() {
|
|
226
263
|
nsStack.pop();
|
|
227
264
|
const adopt = directApi._resumeAdopt;
|
|
@@ -239,6 +276,22 @@ export const directApi = {
|
|
|
239
276
|
noteDomCreate();
|
|
240
277
|
return document.createDocumentFragment();
|
|
241
278
|
},
|
|
279
|
+
/**
|
|
280
|
+
* Multi-root `<template v-for>` row. Resume narrows adoption to the SSR
|
|
281
|
+
* marker pair for this key; a key with no pair gets an empty scope so a new
|
|
282
|
+
* row can never steal nodes that belong to another key.
|
|
283
|
+
*/
|
|
284
|
+
fragRow(key) {
|
|
285
|
+
const adopt = directApi._resumeAdopt;
|
|
286
|
+
if (adopt && typeof adopt.rowStart === 'function')
|
|
287
|
+
adopt.rowStart(key);
|
|
288
|
+
return directApi.frag();
|
|
289
|
+
},
|
|
290
|
+
rowEnd() {
|
|
291
|
+
const adopt = directApi._resumeAdopt;
|
|
292
|
+
if (adopt && typeof adopt.rowEnd === 'function')
|
|
293
|
+
adopt.rowEnd();
|
|
294
|
+
},
|
|
242
295
|
comment(value) {
|
|
243
296
|
noteDomCreate();
|
|
244
297
|
return document.createComment(value == null ? '' : String(value));
|
|
@@ -307,6 +360,15 @@ export const directApi = {
|
|
|
307
360
|
disposeDomTree(root);
|
|
308
361
|
},
|
|
309
362
|
attr(el, name, value) {
|
|
363
|
+
// `v-pre`: the authored pair goes out untouched — no `className` → `class`
|
|
364
|
+
// rewrite, no class/style merge, no property special-casing.
|
|
365
|
+
if (el.__vmzVerbatim) {
|
|
366
|
+
if (value == null || value === false)
|
|
367
|
+
el.removeAttribute(name);
|
|
368
|
+
else
|
|
369
|
+
el.setAttribute(name, value === true ? '' : String(value));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
310
372
|
applyDomAttr(el, name, value);
|
|
311
373
|
},
|
|
312
374
|
mergeClass(...parts) {
|
|
@@ -481,9 +543,11 @@ export const directApi = {
|
|
|
481
543
|
raw = get.call(inst);
|
|
482
544
|
}
|
|
483
545
|
catch {
|
|
484
|
-
raw =
|
|
546
|
+
raw = null;
|
|
485
547
|
}
|
|
486
|
-
|
|
548
|
+
// Same payload contract as the emitted `v-html` call: digest
|
|
549
|
+
// matching and resume adoption must not be bypassed here.
|
|
550
|
+
directApi.html(el, raw);
|
|
487
551
|
}, bindingId);
|
|
488
552
|
},
|
|
489
553
|
/**
|
|
@@ -513,7 +577,7 @@ export const directApi = {
|
|
|
513
577
|
}
|
|
514
578
|
if (!host) {
|
|
515
579
|
noteDomCreate();
|
|
516
|
-
host = document.createElement(
|
|
580
|
+
host = document.createElement(resolveDirectHostTag(name, nameOrCtor));
|
|
517
581
|
host.setAttribute('data-vmz', name);
|
|
518
582
|
}
|
|
519
583
|
if (key != null)
|
|
@@ -543,7 +607,7 @@ export const directApi = {
|
|
|
543
607
|
}
|
|
544
608
|
if (!host) {
|
|
545
609
|
noteDomCreate();
|
|
546
|
-
host = document.createElement(
|
|
610
|
+
host = document.createElement(resolveDirectHostTag(name, initial));
|
|
547
611
|
host.setAttribute('data-vmz', name);
|
|
548
612
|
}
|
|
549
613
|
if (key != null)
|
|
@@ -7,6 +7,8 @@ export type ComponentCtor = ComponentCtorLike & {
|
|
|
7
7
|
[key: string]: unknown;
|
|
8
8
|
};
|
|
9
9
|
export type VmzDomElement = Element & {
|
|
10
|
+
/** `v-pre` subtree: attributes are written exactly as authored. */
|
|
11
|
+
__vmzVerbatim?: boolean;
|
|
10
12
|
__vmzEvt?: Record<string, EventListener>;
|
|
11
13
|
__vmzObjEvt?: Record<string, {
|
|
12
14
|
types: Record<string, {
|
|
@@ -80,12 +82,19 @@ export type ResumeAdoptCtx = {
|
|
|
80
82
|
text: (value?: unknown) => Text;
|
|
81
83
|
componentHost?: (name: string, key?: unknown) => HTMLElement | null;
|
|
82
84
|
enter?: (node: Element) => boolean;
|
|
85
|
+
/** Open a named `<slot name="…">` projection scope inside a component host. */
|
|
86
|
+
enterSlot?: (hostEl: Element, name: string) => boolean;
|
|
83
87
|
leave?: () => void;
|
|
84
88
|
scopeDepth?: () => number;
|
|
85
89
|
rewindScope?: (depth: number) => void;
|
|
86
90
|
beginBranchScope?: () => () => void;
|
|
91
|
+
/** Open a multi-root `<template v-for>` row scope (claim the SSR marker pair). */
|
|
92
|
+
rowStart?: (key: unknown) => boolean;
|
|
93
|
+
rowEnd?: () => void;
|
|
87
94
|
/** Keep the parked SSR subtree when its SSR payload digest equals `digest`. */
|
|
88
95
|
reclaimHtml?: (el: Element, digest: string) => boolean;
|
|
96
|
+
/** Keep the parked SSR subtree when this create has no payload yet. */
|
|
97
|
+
keepHtml?: (el: Element) => boolean;
|
|
89
98
|
_enterBalance?: number;
|
|
90
99
|
inst?: DirectInstance;
|
|
91
100
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { directApi, findOwnedDefaultSlot, noteDomCreate, runDirectCreate, } from '../dom-core.js';
|
|
1
|
+
import { directApi, findOwnedDefaultSlot, findOwnedNamedSlot, noteDomCreate, runDirectCreate, } from '../dom-core.js';
|
|
2
2
|
import { HTML_HASH_ATTR } from '../../shared/html-digest.js';
|
|
3
|
+
import { ROW_END_MARKER_PREFIX, ROW_MARKER_PREFIX, rowEndMarker, rowMarker } from '../../shared/row-key.js';
|
|
3
4
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
4
5
|
function createResumeAdopt(pool, rootEl) {
|
|
5
6
|
const used = new WeakSet();
|
|
@@ -33,6 +34,13 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
33
34
|
n.__vmzResumePool = childPool;
|
|
34
35
|
return n;
|
|
35
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Component hosts are claimed only by `componentHost`, never by plain element
|
|
39
|
+
* adoption. Hosts may be `<span>` (phrasing inline chips) or `<div>` (block
|
|
40
|
+
* surfaces), so tag matching alone would let a page `<span>` steal a chip host
|
|
41
|
+
* and force a rebuild.
|
|
42
|
+
*/
|
|
43
|
+
const isComponentHost = (el) => typeof el.getAttribute === 'function' && el.getAttribute('data-vmz') != null;
|
|
36
44
|
function* elementCandidates() {
|
|
37
45
|
const scope = currentScope();
|
|
38
46
|
if (scope === pool && rootEl && !used.has(rootEl))
|
|
@@ -80,7 +88,7 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
80
88
|
return fresh;
|
|
81
89
|
}
|
|
82
90
|
for (const el of elementCandidates()) {
|
|
83
|
-
if (String(el.tagName).toLowerCase() === want)
|
|
91
|
+
if (String(el.tagName).toLowerCase() === want && !isComponentHost(el))
|
|
84
92
|
return markElement(el);
|
|
85
93
|
}
|
|
86
94
|
noteDomCreate();
|
|
@@ -153,8 +161,6 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
153
161
|
const want = String(name || '');
|
|
154
162
|
const wanted = key == null ? null : String(key);
|
|
155
163
|
for (const el of elementCandidates()) {
|
|
156
|
-
if (String(el.tagName).toLowerCase() !== 'div')
|
|
157
|
-
continue;
|
|
158
164
|
if (el.getAttribute('data-vmz') !== want)
|
|
159
165
|
continue;
|
|
160
166
|
if (wanted != null && el.getAttribute('data-vmz-key') !== wanted)
|
|
@@ -187,9 +193,42 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
187
193
|
return true;
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
|
-
|
|
196
|
+
// Emitted `adoptEnter` is always paired with `adoptLeave`, but a node that
|
|
197
|
+
// has no SSR counterpart (fresh element or fresh component host) has no
|
|
198
|
+
// parked pool. Returning without pushing made that paired leave pop the
|
|
199
|
+
// caller's scope instead, so each rebuilt node drifted the stack one level
|
|
200
|
+
// up and later projection siblings (slot roots 2..n) lost their pool.
|
|
201
|
+
// Push an isolated empty scope: the rebuilt subtree has no SSR counterpart
|
|
202
|
+
// to reclaim, and its children must not steal the enclosing siblings.
|
|
203
|
+
scopeStack.push(node.__vmzResumePool || document.createDocumentFragment());
|
|
204
|
+
return true;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Push the parked pool of an owned named `<slot name="…">` outlet.
|
|
208
|
+
*
|
|
209
|
+
* SSR inlines named projections exactly like the default outlet, so the nodes
|
|
210
|
+
* a page projects into `#header` sit under the named outlet and stay
|
|
211
|
+
* unreachable from the default pool that `enter(host)` pushes. Without this
|
|
212
|
+
* scope the named projection is rebuilt on every load while the default
|
|
213
|
+
* projection is reclaimed. A host without that outlet (fresh component or a
|
|
214
|
+
* name the component never declares) gets an isolated empty scope so the
|
|
215
|
+
* emitted enter/leave pair always stays balanced.
|
|
216
|
+
*/
|
|
217
|
+
const enterSlot = (hostEl, name) => {
|
|
218
|
+
if (!hostEl)
|
|
191
219
|
return false;
|
|
192
|
-
|
|
220
|
+
const wanted = name == null ? '' : String(name);
|
|
221
|
+
if (!wanted || wanted === 'default')
|
|
222
|
+
return enter(hostEl);
|
|
223
|
+
const owner = hostEl.__vmzInst;
|
|
224
|
+
const root = (owner && owner.__vmzDomRoot) || hostEl;
|
|
225
|
+
const slot = root && root.nodeType === 1 ? findOwnedNamedSlot(root, wanted) : null;
|
|
226
|
+
const slotPool = slot && slot.__vmzResumePool;
|
|
227
|
+
if (slotPool && hasUnusedNode(slotPool)) {
|
|
228
|
+
scopeStack.push(slotPool);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
scopeStack.push(document.createDocumentFragment());
|
|
193
232
|
return true;
|
|
194
233
|
};
|
|
195
234
|
const leave = () => {
|
|
@@ -251,18 +290,117 @@ function createResumeAdopt(pool, rootEl) {
|
|
|
251
290
|
el.appendChild(childPool.firstChild);
|
|
252
291
|
return true;
|
|
253
292
|
};
|
|
293
|
+
/**
|
|
294
|
+
* `v-html` create with no payload yet (e.g. an async highlighter that has
|
|
295
|
+
* not produced one). The host may still own the SSR payload: hand the parked
|
|
296
|
+
* subtree back and leave the SSR stamp untouched so a later authoritative
|
|
297
|
+
* patch can still match it. The host keeps that payload until a real payload
|
|
298
|
+
* lands; once one does, a null patch clears it like `v-html` always has.
|
|
299
|
+
*/
|
|
300
|
+
const keepHtml = (el) => {
|
|
301
|
+
if (!used.has(el))
|
|
302
|
+
return false;
|
|
303
|
+
// The emitted `v-html` create and its tracked patch both run with a null
|
|
304
|
+
// payload (the patch applies once right after create), so the hold must
|
|
305
|
+
// outlive the resume pass: one restore is enough until a payload lands.
|
|
306
|
+
if (el.__vmzHtmlKeep)
|
|
307
|
+
return true;
|
|
308
|
+
const childPool = el.__vmzResumePool;
|
|
309
|
+
if (!childPool || !childPool.firstChild)
|
|
310
|
+
return false;
|
|
311
|
+
while (childPool.firstChild)
|
|
312
|
+
el.appendChild(childPool.firstChild);
|
|
313
|
+
el.__vmzResumePool = null;
|
|
314
|
+
el.__vmzHtmlKeep = true;
|
|
315
|
+
return true;
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Scope adoption to one multi-root `<template v-for>` row.
|
|
319
|
+
*
|
|
320
|
+
* The row has no element root to carry `data-vmz-key`, so SSR wrote a
|
|
321
|
+
* `<!--vmz-row:KEY-->…<!--vmz-row-end:KEY-->` pair around its nodes. Claim
|
|
322
|
+
* exactly that pair, move its inner nodes into a private scope, and drop the
|
|
323
|
+
* markers so the resumed DOM matches a client-created row. A missing or
|
|
324
|
+
* unbalanced pair means "this row is not in SSR": push an empty scope so the
|
|
325
|
+
* row is built fresh instead of stealing a sibling row's nodes.
|
|
326
|
+
*/
|
|
327
|
+
const rowStart = (key) => {
|
|
328
|
+
const scope = currentScope();
|
|
329
|
+
const rowPool = document.createDocumentFragment();
|
|
330
|
+
let claimed = false;
|
|
331
|
+
if (key != null) {
|
|
332
|
+
const wanted = rowMarker(key);
|
|
333
|
+
const wantedEnd = rowEndMarker(key);
|
|
334
|
+
const nodes = [...scope.childNodes];
|
|
335
|
+
let start = -1;
|
|
336
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
337
|
+
const n = nodes[i];
|
|
338
|
+
if (n.nodeType !== 8)
|
|
339
|
+
continue;
|
|
340
|
+
if (String(n.nodeValue || '') === wanted) {
|
|
341
|
+
start = i;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
let end = -1;
|
|
346
|
+
if (start >= 0) {
|
|
347
|
+
// Nested rows directly inside this row contribute their own pair at
|
|
348
|
+
// the same sibling level, so pairing is depth-counted.
|
|
349
|
+
let depth = 0;
|
|
350
|
+
for (let i = start + 1; i < nodes.length; i++) {
|
|
351
|
+
const n = nodes[i];
|
|
352
|
+
if (n.nodeType !== 8)
|
|
353
|
+
continue;
|
|
354
|
+
const data = String(n.nodeValue || '');
|
|
355
|
+
if (data.startsWith(ROW_MARKER_PREFIX)) {
|
|
356
|
+
depth += 1;
|
|
357
|
+
}
|
|
358
|
+
else if (data.startsWith(ROW_END_MARKER_PREFIX)) {
|
|
359
|
+
if (depth === 0) {
|
|
360
|
+
if (data === wantedEnd)
|
|
361
|
+
end = i;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
depth -= 1;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (end > start) {
|
|
369
|
+
const startMarker = nodes[start];
|
|
370
|
+
const endMarker = nodes[end];
|
|
371
|
+
for (let i = start + 1; i < end; i++)
|
|
372
|
+
rowPool.appendChild(nodes[i]);
|
|
373
|
+
if (startMarker.parentNode === scope)
|
|
374
|
+
scope.removeChild(startMarker);
|
|
375
|
+
if (endMarker.parentNode === scope)
|
|
376
|
+
scope.removeChild(endMarker);
|
|
377
|
+
claimed = true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
scopeStack.push(rowPool);
|
|
381
|
+
return claimed;
|
|
382
|
+
};
|
|
383
|
+
const rowEnd = () => {
|
|
384
|
+
if (scopeStack.length <= 1)
|
|
385
|
+
return;
|
|
386
|
+
scopeStack.pop();
|
|
387
|
+
};
|
|
254
388
|
return {
|
|
255
389
|
el: adoptElement,
|
|
256
390
|
elNS: adoptElementNS,
|
|
257
391
|
claimKeyed,
|
|
392
|
+
rowStart,
|
|
393
|
+
rowEnd,
|
|
258
394
|
text: adoptText,
|
|
259
395
|
componentHost,
|
|
260
396
|
enter,
|
|
397
|
+
enterSlot,
|
|
261
398
|
leave,
|
|
262
399
|
scopeDepth,
|
|
263
400
|
rewindScope,
|
|
264
401
|
beginBranchScope,
|
|
265
402
|
reclaimHtml,
|
|
403
|
+
keepHtml,
|
|
266
404
|
};
|
|
267
405
|
}
|
|
268
406
|
export function runDirectResume(Component, inst, container) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** SSR row identity for multi-root `<template v-for>` rows. */
|
|
2
|
+
export declare const ROW_MARKER_PREFIX = "vmz-row:";
|
|
3
|
+
export declare const ROW_END_MARKER_PREFIX = "vmz-row-end:";
|
|
4
|
+
/**
|
|
5
|
+
* Comment-safe row key token.
|
|
6
|
+
*
|
|
7
|
+
* Row identity travels inside HTML comments, where `--` is illegal. Encode the
|
|
8
|
+
* key once (both SSR writer and resume reader use this same function) and strip
|
|
9
|
+
* the remaining hyphens so no key can ever close the comment early.
|
|
10
|
+
*/
|
|
11
|
+
export declare function rowKeyToken(key: unknown): string;
|
|
12
|
+
export declare function rowMarker(key: unknown): string;
|
|
13
|
+
export declare function rowEndMarker(key: unknown): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** SSR row identity for multi-root `<template v-for>` rows. */
|
|
2
|
+
export const ROW_MARKER_PREFIX = 'vmz-row:';
|
|
3
|
+
export const ROW_END_MARKER_PREFIX = 'vmz-row-end:';
|
|
4
|
+
/**
|
|
5
|
+
* Comment-safe row key token.
|
|
6
|
+
*
|
|
7
|
+
* Row identity travels inside HTML comments, where `--` is illegal. Encode the
|
|
8
|
+
* key once (both SSR writer and resume reader use this same function) and strip
|
|
9
|
+
* the remaining hyphens so no key can ever close the comment early.
|
|
10
|
+
*/
|
|
11
|
+
export function rowKeyToken(key) {
|
|
12
|
+
return encodeURIComponent(String(key)).replace(/-/g, '%2D');
|
|
13
|
+
}
|
|
14
|
+
export function rowMarker(key) {
|
|
15
|
+
return `${ROW_MARKER_PREFIX}${rowKeyToken(key)}`;
|
|
16
|
+
}
|
|
17
|
+
export function rowEndMarker(key) {
|
|
18
|
+
return `${ROW_END_MARKER_PREFIX}${rowKeyToken(key)}`;
|
|
19
|
+
}
|
package/dist/ssr/serialize.js
CHANGED
|
@@ -1,8 +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
4
|
import { splitStyleDeclarations } from '../shared/dom-attr-normalize.js';
|
|
5
5
|
import { HTML_HASH_ATTR, htmlDigest } from '../shared/html-digest.js';
|
|
6
|
+
import { rowEndMarker, rowMarker } from '../shared/row-key.js';
|
|
6
7
|
import { _ssrDocumentLastError, ensureSsrDocument } from './document.js';
|
|
7
8
|
import { awaitSsrOnMount, ssrComponentName, ssrOnMountContext } from './onmount.js';
|
|
8
9
|
function runDirectSerializeTree(Component, inst) {
|
|
@@ -141,26 +142,33 @@ function serializeStyleAttr(node) {
|
|
|
141
142
|
function serializeOpenTag(node) {
|
|
142
143
|
const tag = node.tag || 'div';
|
|
143
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;
|
|
144
149
|
let attrs = '';
|
|
145
150
|
for (const [k, v] of Object.entries(node.attrs || {})) {
|
|
146
151
|
if (v == null || v === false)
|
|
147
152
|
continue;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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)}"`;
|
|
162
170
|
}
|
|
163
|
-
const style = serializeStyleAttr(node);
|
|
171
|
+
const style = verbatim ? '' : serializeStyleAttr(node);
|
|
164
172
|
if (style)
|
|
165
173
|
attrs += ` style="${escapeHtml(style)}"`;
|
|
166
174
|
// `v-html` hosts carry the digest of the exact string SSR wrote, so the
|
|
@@ -182,7 +190,14 @@ export function flattenSerializeNode(node) {
|
|
|
182
190
|
if (node.__kind === 'frag') {
|
|
183
191
|
if (node.__rawHtml != null)
|
|
184
192
|
return String(node.__rawHtml);
|
|
185
|
-
|
|
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;
|
|
186
201
|
}
|
|
187
202
|
if (node.__kind === 'el') {
|
|
188
203
|
const tag = node.tag || 'div';
|
|
@@ -232,6 +247,13 @@ export function* streamSerializeChunks(node) {
|
|
|
232
247
|
yield String(node.__rawHtml);
|
|
233
248
|
return;
|
|
234
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
|
+
}
|
|
235
257
|
for (const c of node.children || [])
|
|
236
258
|
yield* streamSerializeChunks(c);
|
|
237
259
|
return;
|
|
@@ -578,6 +600,16 @@ const serializeApi = {
|
|
|
578
600
|
},
|
|
579
601
|
};
|
|
580
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
|
+
},
|
|
581
613
|
insertBefore(parent, node, ref) {
|
|
582
614
|
virtualInsertBefore(parent, node, ref);
|
|
583
615
|
},
|
|
@@ -627,6 +659,15 @@ const serializeApi = {
|
|
|
627
659
|
attr(el, name, value) {
|
|
628
660
|
if (!el || el.__kind !== 'el')
|
|
629
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
|
+
}
|
|
630
671
|
applySerializeAttr(el, name, value);
|
|
631
672
|
},
|
|
632
673
|
mergeClass(...parts) {
|
|
@@ -644,6 +685,9 @@ const serializeApi = {
|
|
|
644
685
|
adoptEnter() {
|
|
645
686
|
return false;
|
|
646
687
|
},
|
|
688
|
+
adoptEnterSlot() {
|
|
689
|
+
return false;
|
|
690
|
+
},
|
|
647
691
|
adoptLeave() {
|
|
648
692
|
/* resume scope only */
|
|
649
693
|
},
|
|
@@ -893,7 +937,7 @@ const serializeApi = {
|
|
|
893
937
|
}
|
|
894
938
|
return {
|
|
895
939
|
__kind: 'el',
|
|
896
|
-
tag:
|
|
940
|
+
tag: resolveDirectHostTag(name, Ctor),
|
|
897
941
|
attrs,
|
|
898
942
|
children: body ? [body] : [],
|
|
899
943
|
appendChild(c) {
|
|
@@ -923,7 +967,7 @@ const serializeApi = {
|
|
|
923
967
|
attrs['data-vmz-key'] = String(key);
|
|
924
968
|
return {
|
|
925
969
|
__kind: 'el',
|
|
926
|
-
tag:
|
|
970
|
+
tag: resolveDirectHostTag(name, Ctor),
|
|
927
971
|
attrs,
|
|
928
972
|
children: node ? [node] : [],
|
|
929
973
|
appendChild(c) {
|
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
|
}
|