@vmz/core 0.1.24 → 0.1.25
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-core.d.ts +25 -2
- package/dist/dom-core.js +103 -13
- package/dist/dom-ssr.d.ts +3 -1
- package/dist/dom-ssr.js +207 -78
- package/dist/localize-body-links.d.ts +24 -5
- package/dist/localize-body-links.js +55 -58
- package/package.json +7 -7
package/dist/dom-core.d.ts
CHANGED
|
@@ -71,6 +71,14 @@ export declare function settlePendingChildMounts(inst: any): Promise<void>;
|
|
|
71
71
|
*/
|
|
72
72
|
export declare function runDirectCreate(Component: any, inst: any): any;
|
|
73
73
|
/** Host API for compiler-emitted `__vmzCreate` (direct path, Program IR B). */
|
|
74
|
+
/**
|
|
75
|
+
* First default `<slot>` owned by this component tree — skip nested `[data-vmz]`
|
|
76
|
+
* hosts (Button/Link/Empty label slots). `querySelector('slot')` steals those and
|
|
77
|
+
* projects parent siblings into the wrong host (commercial Drawer into Create btn).
|
|
78
|
+
* @param {Element | null | undefined} root
|
|
79
|
+
* @returns {Element | null}
|
|
80
|
+
*/
|
|
81
|
+
export declare function findOwnedDefaultSlot(root: any): any;
|
|
74
82
|
export declare const directApi: {
|
|
75
83
|
/** @type {object | null} */
|
|
76
84
|
_inst: any;
|
|
@@ -86,8 +94,22 @@ export declare const directApi: {
|
|
|
86
94
|
* }}
|
|
87
95
|
*/
|
|
88
96
|
_eachCtx: any;
|
|
97
|
+
/** @type {null | {
|
|
98
|
+
* el: (tag: string) => Element,
|
|
99
|
+
* text: (value?: any) => Text,
|
|
100
|
+
* componentHost?: (name: string) => HTMLElement | null,
|
|
101
|
+
* enter?: (node: Element) => boolean,
|
|
102
|
+
* leave?: () => void,
|
|
103
|
+
* scopeDepth?: () => number,
|
|
104
|
+
* rewindScope?: (depth: number) => void,
|
|
105
|
+
* beginBranchScope?: () => () => void,
|
|
106
|
+
* }} */
|
|
107
|
+
_resumeAdopt: any;
|
|
89
108
|
el(tag: any): any;
|
|
90
|
-
text(value: any):
|
|
109
|
+
text(value: any): any;
|
|
110
|
+
/** Push adopted node's private resume pool for depth-first child create. */
|
|
111
|
+
adoptEnter(node: any): any;
|
|
112
|
+
adoptLeave(): void;
|
|
91
113
|
frag(): DocumentFragment;
|
|
92
114
|
attr(el: any, name: any, value: any): void;
|
|
93
115
|
on(el: any, type: any, handler: any): void;
|
|
@@ -138,7 +160,7 @@ export declare const directApi: {
|
|
|
138
160
|
* @param {Record<string, any>} props
|
|
139
161
|
* @param {string | null} client
|
|
140
162
|
*/
|
|
141
|
-
component(hostInst: any, name: any, props: any, client: any):
|
|
163
|
+
component(hostInst: any, name: any, props: any, client: any): any;
|
|
142
164
|
/**
|
|
143
165
|
* Keep nested Direct child props live with parent field writes.
|
|
144
166
|
* @param {object} hostInst
|
|
@@ -150,6 +172,7 @@ export declare const directApi: {
|
|
|
150
172
|
bindComponentProp(hostInst: any, hostEl: any, propName: any, deps: any, get: any): void;
|
|
151
173
|
/**
|
|
152
174
|
* Project parent children into nested Direct component default `<slot>`.
|
|
175
|
+
* Uses {@link findOwnedDefaultSlot} so nested Button/Link slots are not stolen.
|
|
153
176
|
* @param {HTMLElement} hostEl
|
|
154
177
|
* @param {Node} node
|
|
155
178
|
*/
|
package/dist/dom-core.js
CHANGED
|
@@ -262,6 +262,34 @@ export function runDirectCreate(Component, inst) {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
/** Host API for compiler-emitted `__vmzCreate` (direct path, Program IR B). */
|
|
265
|
+
/**
|
|
266
|
+
* First default `<slot>` owned by this component tree — skip nested `[data-vmz]`
|
|
267
|
+
* hosts (Button/Link/Empty label slots). `querySelector('slot')` steals those and
|
|
268
|
+
* projects parent siblings into the wrong host (commercial Drawer into Create btn).
|
|
269
|
+
* @param {Element | null | undefined} root
|
|
270
|
+
* @returns {Element | null}
|
|
271
|
+
*/
|
|
272
|
+
export function findOwnedDefaultSlot(root) {
|
|
273
|
+
if (!root || root.nodeType !== 1)
|
|
274
|
+
return null;
|
|
275
|
+
const tag = String(root.tagName || '').toLowerCase();
|
|
276
|
+
if (tag === 'slot' && !root.getAttribute('name'))
|
|
277
|
+
return root;
|
|
278
|
+
const kids = root.children;
|
|
279
|
+
if (!kids || !kids.length)
|
|
280
|
+
return null;
|
|
281
|
+
for (let i = 0; i < kids.length; i++) {
|
|
282
|
+
const c = kids[i];
|
|
283
|
+
if (c.nodeType !== 1)
|
|
284
|
+
continue;
|
|
285
|
+
if (c.hasAttribute('data-vmz'))
|
|
286
|
+
continue;
|
|
287
|
+
const hit = findOwnedDefaultSlot(c);
|
|
288
|
+
if (hit)
|
|
289
|
+
return hit;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
265
293
|
export const directApi = {
|
|
266
294
|
/** @type {object | null} */
|
|
267
295
|
_inst: null,
|
|
@@ -277,14 +305,52 @@ export const directApi = {
|
|
|
277
305
|
* }}
|
|
278
306
|
*/
|
|
279
307
|
_eachCtx: null,
|
|
308
|
+
/** @type {null | {
|
|
309
|
+
* el: (tag: string) => Element,
|
|
310
|
+
* text: (value?: any) => Text,
|
|
311
|
+
* componentHost?: (name: string) => HTMLElement | null,
|
|
312
|
+
* enter?: (node: Element) => boolean,
|
|
313
|
+
* leave?: () => void,
|
|
314
|
+
* scopeDepth?: () => number,
|
|
315
|
+
* rewindScope?: (depth: number) => void,
|
|
316
|
+
* beginBranchScope?: () => () => void,
|
|
317
|
+
* }} */
|
|
318
|
+
_resumeAdopt: null,
|
|
280
319
|
el(tag) {
|
|
320
|
+
if (directApi._resumeAdopt)
|
|
321
|
+
return directApi._resumeAdopt.el(tag);
|
|
281
322
|
noteDomCreate();
|
|
282
323
|
return document.createElement(tag || 'div');
|
|
283
324
|
},
|
|
284
325
|
text(value) {
|
|
326
|
+
if (directApi._resumeAdopt)
|
|
327
|
+
return directApi._resumeAdopt.text(value);
|
|
285
328
|
noteDomCreate();
|
|
286
329
|
return document.createTextNode(value == null ? '' : String(value));
|
|
287
330
|
},
|
|
331
|
+
/** Push adopted node's private resume pool for depth-first child create. */
|
|
332
|
+
adoptEnter(node) {
|
|
333
|
+
const adopt = directApi._resumeAdopt;
|
|
334
|
+
if (adopt && typeof adopt.enter === 'function') {
|
|
335
|
+
const ok = adopt.enter(node);
|
|
336
|
+
if (ok)
|
|
337
|
+
adopt._enterBalance = (adopt._enterBalance || 0) + 1;
|
|
338
|
+
return ok;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
},
|
|
342
|
+
adoptLeave() {
|
|
343
|
+
const adopt = directApi._resumeAdopt;
|
|
344
|
+
// Emit always pairs leave after enter; if enter failed (fresh node, no
|
|
345
|
+
// pool), do not pop — unbalanced leave previously stole parent scopes
|
|
346
|
+
// and projected siblings into nested Button slots.
|
|
347
|
+
if (!adopt || typeof adopt.leave !== 'function')
|
|
348
|
+
return;
|
|
349
|
+
if ((adopt._enterBalance || 0) <= 0)
|
|
350
|
+
return;
|
|
351
|
+
adopt._enterBalance -= 1;
|
|
352
|
+
adopt.leave();
|
|
353
|
+
},
|
|
288
354
|
frag() {
|
|
289
355
|
noteDomCreate();
|
|
290
356
|
return document.createDocumentFragment();
|
|
@@ -426,9 +492,16 @@ export const directApi = {
|
|
|
426
492
|
* @param {string | null} client
|
|
427
493
|
*/
|
|
428
494
|
component(hostInst, name, props, client) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
495
|
+
/** @type {HTMLElement | null} */
|
|
496
|
+
let host = null;
|
|
497
|
+
if (directApi._resumeAdopt && typeof directApi._resumeAdopt.componentHost === 'function') {
|
|
498
|
+
host = directApi._resumeAdopt.componentHost(name);
|
|
499
|
+
}
|
|
500
|
+
if (!host) {
|
|
501
|
+
noteDomCreate();
|
|
502
|
+
host = document.createElement('div');
|
|
503
|
+
host.setAttribute('data-vmz', name);
|
|
504
|
+
}
|
|
432
505
|
/** @type {Record<string, any>} */
|
|
433
506
|
const resolved = {};
|
|
434
507
|
for (const [k, v] of Object.entries(props || {})) {
|
|
@@ -471,9 +544,23 @@ export const directApi = {
|
|
|
471
544
|
if (!(Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function')) {
|
|
472
545
|
throw new Error(`vmz:dom direct component <${name}> requires __vmzCreate (rebuild child with Direct)`);
|
|
473
546
|
}
|
|
474
|
-
|
|
547
|
+
// Nested resume: keep parent `_resumeAdopt` so child reclaim parked SSR nodes.
|
|
548
|
+
// Enter the host's private child pool so nested create cannot see uncle siblings.
|
|
549
|
+
const adopt = directApi._resumeAdopt;
|
|
550
|
+
const entered = adopt && typeof adopt.enter === 'function' ? adopt.enter(host) : false;
|
|
551
|
+
let node;
|
|
552
|
+
try {
|
|
553
|
+
node = runDirectCreate(Ctor, child);
|
|
554
|
+
}
|
|
555
|
+
finally {
|
|
556
|
+
if (entered && adopt && typeof adopt.leave === 'function')
|
|
557
|
+
adopt.leave();
|
|
558
|
+
}
|
|
475
559
|
if (node) {
|
|
476
|
-
|
|
560
|
+
// ifBlock/eachBlock roots are DocumentFragments: appendChild moves
|
|
561
|
+
// children into `host` and empties the fragment. Keep a live Element
|
|
562
|
+
// root so projectDefaultSlot can find `<slot>` (Button v-if/v-else).
|
|
563
|
+
child.__vmzDomRoot = node.nodeType === 11 ? host : node;
|
|
477
564
|
host.appendChild(node);
|
|
478
565
|
}
|
|
479
566
|
host.__vmzInst = child;
|
|
@@ -524,6 +611,7 @@ export const directApi = {
|
|
|
524
611
|
},
|
|
525
612
|
/**
|
|
526
613
|
* Project parent children into nested Direct component default `<slot>`.
|
|
614
|
+
* Uses {@link findOwnedDefaultSlot} so nested Button/Link slots are not stolen.
|
|
527
615
|
* @param {HTMLElement} hostEl
|
|
528
616
|
* @param {Node} node
|
|
529
617
|
*/
|
|
@@ -531,22 +619,20 @@ export const directApi = {
|
|
|
531
619
|
if (!hostEl || node == null)
|
|
532
620
|
return;
|
|
533
621
|
const child = hostEl.__vmzInst;
|
|
534
|
-
|
|
622
|
+
let root = (child && child.__vmzDomRoot) || hostEl;
|
|
623
|
+
// Emptied DocumentFragment after append must not receive slot kids.
|
|
624
|
+
if (!root || root.nodeType !== 1)
|
|
625
|
+
root = hostEl;
|
|
535
626
|
/** @type {Element | null} */
|
|
536
627
|
let slot = null;
|
|
537
628
|
if (root && root.nodeType === 1) {
|
|
538
|
-
|
|
539
|
-
slot = root;
|
|
540
|
-
}
|
|
541
|
-
else if (typeof root.querySelector === 'function') {
|
|
542
|
-
slot = root.querySelector('slot:not([name])');
|
|
543
|
-
}
|
|
629
|
+
slot = findOwnedDefaultSlot(root);
|
|
544
630
|
}
|
|
545
631
|
if (slot && slot.parentNode) {
|
|
546
632
|
slot.replaceWith(node);
|
|
547
633
|
return;
|
|
548
634
|
}
|
|
549
|
-
if (root && typeof root.appendChild === 'function')
|
|
635
|
+
if (root && root.nodeType === 1 && typeof root.appendChild === 'function')
|
|
550
636
|
root.appendChild(node);
|
|
551
637
|
else
|
|
552
638
|
hostEl.appendChild(node);
|
|
@@ -632,10 +718,14 @@ export const directApi = {
|
|
|
632
718
|
directApi._branchBinds = binds;
|
|
633
719
|
directApi._inst = inst;
|
|
634
720
|
let created = null;
|
|
721
|
+
const adopt = directApi._resumeAdopt;
|
|
722
|
+
const endBranch = adopt && typeof adopt.beginBranchScope === 'function' ? adopt.beginBranchScope() : null;
|
|
635
723
|
try {
|
|
636
724
|
created = branches[next].create.call(inst, directApi);
|
|
637
725
|
}
|
|
638
726
|
finally {
|
|
727
|
+
if (typeof endBranch === 'function')
|
|
728
|
+
endBranch();
|
|
639
729
|
directApi._branchBinds = prevSink;
|
|
640
730
|
directApi._inst = prevInst;
|
|
641
731
|
}
|
package/dist/dom-ssr.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
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
|
+
import { findOwnedDefaultSlot } from './dom-core.js';
|
|
5
6
|
export declare function renderToString(Component: any, props?: {}, opts?: {}): Promise<any>;
|
|
6
7
|
/**
|
|
7
8
|
* Stream SSR via the same Direct serialize schedule as `renderToString`.
|
|
@@ -16,10 +17,11 @@ export declare function renderToStream(Component: any, props?: {}, opts?: {}): A
|
|
|
16
17
|
/**
|
|
17
18
|
* Live-DOM counterpart: first default `<slot>` owned by this tree, not by a nested
|
|
18
19
|
* `[data-vmz]` component (Button/Link labels, etc.).
|
|
20
|
+
* Re-exported from dom-core (single implementation).
|
|
19
21
|
* @param {Element | null | undefined} root
|
|
20
22
|
* @returns {Element | null}
|
|
21
23
|
*/
|
|
22
|
-
export
|
|
24
|
+
export { findOwnedDefaultSlot };
|
|
23
25
|
/**
|
|
24
26
|
* Hydrate/mount a file-route page inside an optional layout chain (outer → inner).
|
|
25
27
|
* Mirrors SSR `slotHtml` wrapping: each layout's owned default slot becomes the
|
package/dist/dom-ssr.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Imports client DOM primitives from ./dom-core.js for tree-shakeable browser entry.
|
|
5
5
|
*/
|
|
6
6
|
import { applyDirectHostBox, directHostBoxStyleAttr } from './direct-host-box.js';
|
|
7
|
-
import {
|
|
7
|
+
import { applyPreservedState, BOOLEAN_HTML_ATTRS, createInstance, destroy, directApi, eventPropHandlerName, findOwnedDefaultSlot, getRegisteredComponent, hasMeaningfulChild, isEventEntryStrategy, mount, noteDomCreate, resolveComponent, runDirectCreate, scheduleClientOn, settlePendingChildMounts, snapshotInstanceState, stripFns, } from './dom-core.js';
|
|
8
8
|
import { markUnknownComponentHost, serializeUnknownComponentNode } from './unknown-component.js';
|
|
9
9
|
/** @type {Error | null} last linkedom resolve failure (for clear SSR errors) */
|
|
10
10
|
let _ssrDocumentLastError = null;
|
|
@@ -185,31 +185,11 @@ function injectDefaultSlotHtml(node, html) {
|
|
|
185
185
|
/**
|
|
186
186
|
* Live-DOM counterpart: first default `<slot>` owned by this tree, not by a nested
|
|
187
187
|
* `[data-vmz]` component (Button/Link labels, etc.).
|
|
188
|
+
* Re-exported from dom-core (single implementation).
|
|
188
189
|
* @param {Element | null | undefined} root
|
|
189
190
|
* @returns {Element | null}
|
|
190
191
|
*/
|
|
191
|
-
export
|
|
192
|
-
if (!root || root.nodeType !== 1)
|
|
193
|
-
return null;
|
|
194
|
-
const tag = String(root.tagName || '').toLowerCase();
|
|
195
|
-
if (tag === 'slot' && !root.getAttribute('name'))
|
|
196
|
-
return root;
|
|
197
|
-
const kids = root.children;
|
|
198
|
-
if (!kids || !kids.length)
|
|
199
|
-
return null;
|
|
200
|
-
for (let i = 0; i < kids.length; i++) {
|
|
201
|
-
const c = kids[i];
|
|
202
|
-
if (c.nodeType !== 1)
|
|
203
|
-
continue;
|
|
204
|
-
// Nested component host — its slots are not the layout page outlet.
|
|
205
|
-
if (c.hasAttribute('data-vmz'))
|
|
206
|
-
continue;
|
|
207
|
-
const hit = findOwnedDefaultSlot(c);
|
|
208
|
-
if (hit)
|
|
209
|
-
return hit;
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
192
|
+
export { findOwnedDefaultSlot };
|
|
213
193
|
/**
|
|
214
194
|
* Hydrate/mount a file-route page inside an optional layout chain (outer → inner).
|
|
215
195
|
* Mirrors SSR `slotHtml` wrapping: each layout's owned default slot becomes the
|
|
@@ -592,6 +572,12 @@ const serializeApi = {
|
|
|
592
572
|
onMethod() {
|
|
593
573
|
/* named method events are also attached only during client resume */
|
|
594
574
|
},
|
|
575
|
+
adoptEnter() {
|
|
576
|
+
return false;
|
|
577
|
+
},
|
|
578
|
+
adoptLeave() {
|
|
579
|
+
/* resume scope only */
|
|
580
|
+
},
|
|
595
581
|
bindText(inst, bindingId, deps, get, textNode) {
|
|
596
582
|
let raw = '';
|
|
597
583
|
try {
|
|
@@ -620,13 +606,15 @@ const serializeApi = {
|
|
|
620
606
|
return;
|
|
621
607
|
// serializeApi.component returns a serialize el tree (or island shell).
|
|
622
608
|
const root = hostEl.__kind === 'el' ? hostEl : null;
|
|
623
|
-
const
|
|
609
|
+
const findOwnedSlot = (n) => {
|
|
624
610
|
if (!n || n.__kind !== 'el')
|
|
625
611
|
return null;
|
|
626
612
|
if (n.tag === 'slot' && !(n.attrs && n.attrs.name))
|
|
627
613
|
return n;
|
|
628
614
|
for (const c of n.children || []) {
|
|
629
|
-
|
|
615
|
+
if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
|
|
616
|
+
continue;
|
|
617
|
+
const hit = findOwnedSlot(c);
|
|
630
618
|
if (hit)
|
|
631
619
|
return hit;
|
|
632
620
|
}
|
|
@@ -636,12 +624,14 @@ const serializeApi = {
|
|
|
636
624
|
let slot = null;
|
|
637
625
|
if (root) {
|
|
638
626
|
for (const c of root.children || []) {
|
|
639
|
-
|
|
627
|
+
if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
|
|
628
|
+
continue;
|
|
629
|
+
slot = findOwnedSlot(c);
|
|
640
630
|
if (slot)
|
|
641
631
|
break;
|
|
642
632
|
}
|
|
643
633
|
if (!slot)
|
|
644
|
-
slot =
|
|
634
|
+
slot = findOwnedSlot(root);
|
|
645
635
|
}
|
|
646
636
|
if (slot) {
|
|
647
637
|
slot.__rawHtml = null;
|
|
@@ -974,6 +964,167 @@ export function attachEventEntries(root = globalThis.document) {
|
|
|
974
964
|
});
|
|
975
965
|
}
|
|
976
966
|
}
|
|
967
|
+
/**
|
|
968
|
+
* Adopt parked SSR/Island nodes while `__vmzCreate` rebuilds the live tree.
|
|
969
|
+
* Children of `rootEl` are moved into `pool` first so the create schedule never
|
|
970
|
+
* leaves orphan SSR siblings beside if/each markers (duplicate h2 / stale text).
|
|
971
|
+
*
|
|
972
|
+
* Scope rules (sibling-host invariant):
|
|
973
|
+
* - Candidates are **direct children** of the current scope pool only (no deep
|
|
974
|
+
* walk into other parked siblings).
|
|
975
|
+
* - Adopting a node parks its children into a **private** `__vmzResumePool`.
|
|
976
|
+
* - `enter(node)` / `leave()` push that private pool for depth-first child create.
|
|
977
|
+
* - `beginBranchScope()` moves one top-level SSR node into an isolated pool so
|
|
978
|
+
* ifBlock branch create cannot steal parent siblings (Drawer / Button hosts).
|
|
979
|
+
*
|
|
980
|
+
* @param {DocumentFragment} pool
|
|
981
|
+
* @param {Element} rootEl
|
|
982
|
+
*/
|
|
983
|
+
function createResumeAdopt(pool, rootEl) {
|
|
984
|
+
const used = new WeakSet();
|
|
985
|
+
/** @type {DocumentFragment[]} */
|
|
986
|
+
const scopeStack = [pool];
|
|
987
|
+
/**
|
|
988
|
+
* @param {Node} root
|
|
989
|
+
* @param {(n: Text) => void} visit
|
|
990
|
+
*/
|
|
991
|
+
const walkTexts = (root, visit) => {
|
|
992
|
+
if (!root)
|
|
993
|
+
return;
|
|
994
|
+
if (root.nodeType === 3) {
|
|
995
|
+
visit(/** @type {Text} */ (root));
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (root.nodeType !== 1)
|
|
999
|
+
return;
|
|
1000
|
+
for (let c = root.firstChild; c; c = c.nextSibling)
|
|
1001
|
+
walkTexts(c, visit);
|
|
1002
|
+
};
|
|
1003
|
+
const currentScope = () => scopeStack[scopeStack.length - 1] || pool;
|
|
1004
|
+
const markElement = (n) => {
|
|
1005
|
+
used.add(n);
|
|
1006
|
+
if (typeof n.getAttribute === 'function') {
|
|
1007
|
+
const k = n.getAttribute('data-vmz-key');
|
|
1008
|
+
if (k != null && n.__vmzKey == null)
|
|
1009
|
+
n.__vmzKey = k;
|
|
1010
|
+
}
|
|
1011
|
+
// Private child pool — never dump into the parent/global pool (that let
|
|
1012
|
+
// if branches reclaim Drawer/sibling hosts parked as uncles).
|
|
1013
|
+
const childPool = document.createDocumentFragment();
|
|
1014
|
+
while (n.firstChild)
|
|
1015
|
+
childPool.appendChild(n.firstChild);
|
|
1016
|
+
n.__vmzResumePool = childPool;
|
|
1017
|
+
return n;
|
|
1018
|
+
};
|
|
1019
|
+
/** @returns {Generator<Element>} */
|
|
1020
|
+
function* elementCandidates() {
|
|
1021
|
+
const scope = currentScope();
|
|
1022
|
+
if (scope === pool && rootEl && !used.has(rootEl))
|
|
1023
|
+
yield rootEl;
|
|
1024
|
+
// Snapshot — adopt may move nodes out of scope while iterating.
|
|
1025
|
+
const kids = [...scope.childNodes];
|
|
1026
|
+
for (const n of kids) {
|
|
1027
|
+
if (n.nodeType === 1 && !used.has(/** @type {Element} */ (n))) {
|
|
1028
|
+
yield /** @type {Element} */ (n);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
/** @returns {Generator<Text>} */
|
|
1033
|
+
function* textCandidates() {
|
|
1034
|
+
const scope = currentScope();
|
|
1035
|
+
const kids = [...scope.childNodes];
|
|
1036
|
+
for (const n of kids) {
|
|
1037
|
+
/** @type {Text[]} */
|
|
1038
|
+
const found = [];
|
|
1039
|
+
walkTexts(n, (t) => {
|
|
1040
|
+
if (!used.has(t))
|
|
1041
|
+
found.push(t);
|
|
1042
|
+
});
|
|
1043
|
+
for (const t of found)
|
|
1044
|
+
yield t;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const adoptElement = (tag) => {
|
|
1048
|
+
const want = String(tag || 'div').toLowerCase();
|
|
1049
|
+
for (const el of elementCandidates()) {
|
|
1050
|
+
if (String(el.tagName).toLowerCase() === want)
|
|
1051
|
+
return markElement(el);
|
|
1052
|
+
}
|
|
1053
|
+
noteDomCreate();
|
|
1054
|
+
return document.createElement(tag || 'div');
|
|
1055
|
+
};
|
|
1056
|
+
const adoptText = (value) => {
|
|
1057
|
+
for (const n of textCandidates()) {
|
|
1058
|
+
used.add(n);
|
|
1059
|
+
if (value != null && value !== '')
|
|
1060
|
+
n.textContent = String(value);
|
|
1061
|
+
return n;
|
|
1062
|
+
}
|
|
1063
|
+
noteDomCreate();
|
|
1064
|
+
return document.createTextNode(value == null ? '' : String(value));
|
|
1065
|
+
};
|
|
1066
|
+
/** Reclaim SSR `<div data-vmz="Name">` hosts for nested Direct components. */
|
|
1067
|
+
const componentHost = (name) => {
|
|
1068
|
+
const want = String(name || '');
|
|
1069
|
+
for (const el of elementCandidates()) {
|
|
1070
|
+
if (String(el.tagName).toLowerCase() !== 'div')
|
|
1071
|
+
continue;
|
|
1072
|
+
if (el.getAttribute('data-vmz') !== want)
|
|
1073
|
+
continue;
|
|
1074
|
+
return markElement(el);
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
};
|
|
1078
|
+
const enter = (node) => {
|
|
1079
|
+
if (!node || !node.__vmzResumePool)
|
|
1080
|
+
return false;
|
|
1081
|
+
scopeStack.push(node.__vmzResumePool);
|
|
1082
|
+
return true;
|
|
1083
|
+
};
|
|
1084
|
+
const leave = () => {
|
|
1085
|
+
if (scopeStack.length <= 1)
|
|
1086
|
+
return;
|
|
1087
|
+
scopeStack.pop();
|
|
1088
|
+
};
|
|
1089
|
+
const scopeDepth = () => scopeStack.length;
|
|
1090
|
+
const rewindScope = (depth) => {
|
|
1091
|
+
const d = Math.max(1, Number(depth) || 1);
|
|
1092
|
+
while (scopeStack.length > d)
|
|
1093
|
+
scopeStack.pop();
|
|
1094
|
+
};
|
|
1095
|
+
/**
|
|
1096
|
+
* Isolate the next top-level SSR sibling as the only candidates for an
|
|
1097
|
+
* if/else branch create (commercial Card: Empty branch must not adopt Drawer).
|
|
1098
|
+
* @returns {() => void} end callback
|
|
1099
|
+
*/
|
|
1100
|
+
const beginBranchScope = () => {
|
|
1101
|
+
const parent = currentScope();
|
|
1102
|
+
const branchPool = document.createDocumentFragment();
|
|
1103
|
+
for (const n of [...parent.childNodes]) {
|
|
1104
|
+
if (n.nodeType === 1 && !used.has(/** @type {Element} */ (n))) {
|
|
1105
|
+
branchPool.appendChild(n);
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
scopeStack.push(branchPool);
|
|
1110
|
+
const depth = scopeStack.length;
|
|
1111
|
+
return () => {
|
|
1112
|
+
while (branchPool.firstChild)
|
|
1113
|
+
branchPool.removeChild(branchPool.firstChild);
|
|
1114
|
+
rewindScope(depth - 1);
|
|
1115
|
+
};
|
|
1116
|
+
};
|
|
1117
|
+
return {
|
|
1118
|
+
el: adoptElement,
|
|
1119
|
+
text: adoptText,
|
|
1120
|
+
componentHost,
|
|
1121
|
+
enter,
|
|
1122
|
+
leave,
|
|
1123
|
+
scopeDepth,
|
|
1124
|
+
rewindScope,
|
|
1125
|
+
beginBranchScope,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
977
1128
|
/**
|
|
978
1129
|
* Adopt existing Island DOM while running the same `__vmzCreate` schedule (resume).
|
|
979
1130
|
* @param {new (props?: object) => any} Component
|
|
@@ -985,50 +1136,32 @@ function runDirectResume(Component, inst, container) {
|
|
|
985
1136
|
if (!rootEl || rootEl.nodeType !== 1) {
|
|
986
1137
|
return runDirectCreate(Component, inst);
|
|
987
1138
|
}
|
|
988
|
-
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
},
|
|
1015
|
-
attr(el, name, value) {
|
|
1016
|
-
applyDomAttr(el, name, value);
|
|
1017
|
-
},
|
|
1018
|
-
on(el, type, handler) {
|
|
1019
|
-
el.addEventListener(type, handler);
|
|
1020
|
-
},
|
|
1021
|
-
bindText: directApi.bindText,
|
|
1022
|
-
bindAttr: directApi.bindAttr,
|
|
1023
|
-
bindComponentProp: directApi.bindComponentProp,
|
|
1024
|
-
projectDefaultSlot: directApi.projectDefaultSlot,
|
|
1025
|
-
setHtml: directApi.setHtml,
|
|
1026
|
-
bindHtml: directApi.bindHtml,
|
|
1027
|
-
ifBlock: directApi.ifBlock,
|
|
1028
|
-
eachBlock: directApi.eachBlock,
|
|
1029
|
-
component: directApi.component,
|
|
1030
|
-
};
|
|
1031
|
-
return Component.__vmzCreate.call(inst, api);
|
|
1139
|
+
// Park SSR descendants so create can append if/each structure without orphan siblings.
|
|
1140
|
+
const pool = document.createDocumentFragment();
|
|
1141
|
+
while (rootEl.firstChild)
|
|
1142
|
+
pool.appendChild(rootEl.firstChild);
|
|
1143
|
+
const prevInst = directApi._inst;
|
|
1144
|
+
const prevBranch = directApi._branchBinds;
|
|
1145
|
+
const prevItems = directApi._itemPatches;
|
|
1146
|
+
const prevEach = directApi._eachCtx;
|
|
1147
|
+
const prevAdopt = directApi._resumeAdopt;
|
|
1148
|
+
directApi._inst = inst;
|
|
1149
|
+
directApi._branchBinds = null;
|
|
1150
|
+
directApi._itemPatches = null;
|
|
1151
|
+
directApi._eachCtx = null;
|
|
1152
|
+
directApi._resumeAdopt = createResumeAdopt(pool, rootEl);
|
|
1153
|
+
try {
|
|
1154
|
+
return Component.__vmzCreate.call(inst, directApi);
|
|
1155
|
+
}
|
|
1156
|
+
finally {
|
|
1157
|
+
directApi._resumeAdopt = prevAdopt;
|
|
1158
|
+
directApi._inst = prevInst;
|
|
1159
|
+
directApi._branchBinds = prevBranch;
|
|
1160
|
+
directApi._itemPatches = prevItems;
|
|
1161
|
+
directApi._eachCtx = prevEach;
|
|
1162
|
+
while (pool.firstChild)
|
|
1163
|
+
pool.removeChild(pool.firstChild);
|
|
1164
|
+
}
|
|
1032
1165
|
}
|
|
1033
1166
|
/**
|
|
1034
1167
|
* @param {new (props?: object) => any} Component
|
|
@@ -1071,14 +1204,10 @@ export async function hydrate(Component, container, props = {}, opts = {}) {
|
|
|
1071
1204
|
}
|
|
1072
1205
|
}
|
|
1073
1206
|
else {
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const node = runDirectCreate(Component, inst);
|
|
1078
|
-
if (node) {
|
|
1207
|
+
// Deep adopt: park SSR children, rebuild schedule, reclaim nodes (no orphan siblings).
|
|
1208
|
+
const node = runDirectResume(Component, inst, container);
|
|
1209
|
+
if (node)
|
|
1079
1210
|
inst.__vmzDomRoot = node;
|
|
1080
|
-
container.appendChild(node);
|
|
1081
|
-
}
|
|
1082
1211
|
}
|
|
1083
1212
|
await settlePendingChildMounts(inst);
|
|
1084
1213
|
container.__vmzInst = inst;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Plan-native locale link rewrite for serve-host and static-emit.
|
|
3
|
+
* Href authority is RouteId × LocaleId realization rows — not path parsing on existing href.
|
|
4
4
|
*/
|
|
5
|
+
export declare const LOCALE_LINK_PLAN_SCHEMA = "vmz.static.locale_link_plan.v0";
|
|
5
6
|
export type LocaleHrefArtifact = {
|
|
6
7
|
locales?: Array<{
|
|
7
8
|
id: string;
|
|
@@ -12,8 +13,26 @@ export type LocaleHrefArtifact = {
|
|
|
12
13
|
defaultPrefix?: string;
|
|
13
14
|
defaultLocale?: string;
|
|
14
15
|
};
|
|
16
|
+
realizations?: Array<{
|
|
17
|
+
routeId: string;
|
|
18
|
+
localeId: string;
|
|
19
|
+
path: string;
|
|
20
|
+
}>;
|
|
15
21
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
export type LocaleLinkPlanRow = {
|
|
23
|
+
routeId: string;
|
|
24
|
+
localeId: string;
|
|
25
|
+
href: string;
|
|
26
|
+
};
|
|
27
|
+
export type LocaleLinkPlan = {
|
|
28
|
+
schema: typeof LOCALE_LINK_PLAN_SCHEMA;
|
|
29
|
+
rows: LocaleLinkPlanRow[];
|
|
30
|
+
};
|
|
31
|
+
/** Build RouteId × LocaleId → href rows from locale route realization artifact. */
|
|
32
|
+
export declare function buildLocaleLinkPlan(artifact: LocaleHrefArtifact | null | undefined): LocaleLinkPlan;
|
|
33
|
+
/** Apply plan rows to `<a data-vmz-route="RouteId">` markers for one LocaleId. */
|
|
34
|
+
export declare function applyLocaleLinkPlan(html: string, localeId: string, plan: LocaleLinkPlan, escapeAttr?: (s: string) => string): string;
|
|
35
|
+
/** Rewrite `<a data-vmz-route href>` using locale realization plan (Plan-native). */
|
|
19
36
|
export declare function localizeBodyLinks(html: string, localeId: string, artifact: LocaleHrefArtifact, escapeAttr?: (s: string) => string): string;
|
|
37
|
+
/** Legacy same-app href helper for callers without `data-vmz-route` markers. */
|
|
38
|
+
export declare function localizeSameAppHref(href: string, localeId: string, artifact: LocaleHrefArtifact): string;
|
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Plan-native locale link rewrite for serve-host and static-emit.
|
|
3
|
+
* Href authority is RouteId × LocaleId realization rows — not path parsing on existing href.
|
|
4
4
|
*/
|
|
5
|
+
export const LOCALE_LINK_PLAN_SCHEMA = 'vmz.static.locale_link_plan.v0';
|
|
6
|
+
/** Build RouteId × LocaleId → href rows from locale route realization artifact. */
|
|
7
|
+
export function buildLocaleLinkPlan(artifact) {
|
|
8
|
+
const rows = [];
|
|
9
|
+
for (const r of artifact?.realizations || []) {
|
|
10
|
+
if (!r?.routeId || !r?.localeId || !r?.path)
|
|
11
|
+
continue;
|
|
12
|
+
rows.push({ routeId: String(r.routeId), localeId: String(r.localeId), href: String(r.path) });
|
|
13
|
+
}
|
|
14
|
+
rows.sort((a, b) => (a.routeId === b.routeId ? a.localeId.localeCompare(b.localeId) : a.routeId.localeCompare(b.routeId)));
|
|
15
|
+
return { schema: LOCALE_LINK_PLAN_SCHEMA, rows };
|
|
16
|
+
}
|
|
17
|
+
/** Apply plan rows to `<a data-vmz-route="RouteId">` markers for one LocaleId. */
|
|
18
|
+
export function applyLocaleLinkPlan(html, localeId, plan, escapeAttr = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"')) {
|
|
19
|
+
if (!html || !localeId || !plan?.rows?.length)
|
|
20
|
+
return html;
|
|
21
|
+
const byRoute = new Map();
|
|
22
|
+
for (const row of plan.rows) {
|
|
23
|
+
if (row.localeId === localeId)
|
|
24
|
+
byRoute.set(row.routeId, row.href);
|
|
25
|
+
}
|
|
26
|
+
if (!byRoute.size)
|
|
27
|
+
return html;
|
|
28
|
+
return String(html).replace(/<a\b([^>]*)>/gi, (full, attrs) => {
|
|
29
|
+
const rm = attrs.match(/\bdata-vmz-route\s*=\s*"([^"]*)"/i);
|
|
30
|
+
if (!rm)
|
|
31
|
+
return full;
|
|
32
|
+
const href = byRoute.get(rm[1]);
|
|
33
|
+
if (!href)
|
|
34
|
+
return full;
|
|
35
|
+
const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
|
|
36
|
+
if (hm && hm[1] === href)
|
|
37
|
+
return full;
|
|
38
|
+
const newAttrs = hm ? attrs.replace(/\bhref\s*=\s*"[^"]*"/i, `href="${escapeAttr(href)}"`) : ` href="${escapeAttr(href)}"${attrs}`;
|
|
39
|
+
return `<a${newAttrs}>`;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Rewrite `<a data-vmz-route href>` using locale realization plan (Plan-native). */
|
|
43
|
+
export function localizeBodyLinks(html, localeId, artifact, escapeAttr) {
|
|
44
|
+
if (!html || !localeId || !artifact)
|
|
45
|
+
return html;
|
|
46
|
+
return applyLocaleLinkPlan(html, localeId, buildLocaleLinkPlan(artifact), escapeAttr);
|
|
47
|
+
}
|
|
5
48
|
function normalizePath(pathname) {
|
|
6
49
|
let p = String(pathname || '/');
|
|
7
50
|
if (!p.startsWith('/'))
|
|
@@ -10,32 +53,7 @@ function normalizePath(pathname) {
|
|
|
10
53
|
p = p.slice(0, -1);
|
|
11
54
|
return p || '/';
|
|
12
55
|
}
|
|
13
|
-
|
|
14
|
-
const parts = normalizePath(pathname).split('/').filter(Boolean);
|
|
15
|
-
if (!parts.length)
|
|
16
|
-
return { localeId: null, restPath: '/' };
|
|
17
|
-
if (supportedLocales.includes(parts[0])) {
|
|
18
|
-
const rest = parts.slice(1);
|
|
19
|
-
return { localeId: parts[0], restPath: rest.length ? `/${rest.join('/')}` : '/' };
|
|
20
|
-
}
|
|
21
|
-
return { localeId: null, restPath: normalizePath(pathname) };
|
|
22
|
-
}
|
|
23
|
-
function realizeRoutePath(localeId, pathPattern, routing) {
|
|
24
|
-
const strategy = routing.strategy || 'prefix';
|
|
25
|
-
const defaultPrefix = routing.defaultPrefix || 'include';
|
|
26
|
-
const defaultLocale = routing.defaultLocale;
|
|
27
|
-
const base = normalizePath(pathPattern);
|
|
28
|
-
if (strategy === 'none' || strategy === 'domain') {
|
|
29
|
-
return { path: base };
|
|
30
|
-
}
|
|
31
|
-
const omitDefault = defaultPrefix === 'omit' && localeId === defaultLocale;
|
|
32
|
-
if (omitDefault) {
|
|
33
|
-
return { path: base };
|
|
34
|
-
}
|
|
35
|
-
const pathOut = base === '/' ? `/${localeId}` : `/${localeId}${base}`;
|
|
36
|
-
return { path: pathOut };
|
|
37
|
-
}
|
|
38
|
-
/** Rewrite a same-app href to the given LocaleId via route realization. */
|
|
56
|
+
/** Legacy same-app href helper for callers without `data-vmz-route` markers. */
|
|
39
57
|
export function localizeSameAppHref(href, localeId, artifact) {
|
|
40
58
|
if (!href || !localeId || !artifact)
|
|
41
59
|
return href;
|
|
@@ -43,6 +61,7 @@ export function localizeSameAppHref(href, localeId, artifact) {
|
|
|
43
61
|
return href;
|
|
44
62
|
if (/^[a-z][a-z0-9+.-]*:/i.test(href) && !href.startsWith('/'))
|
|
45
63
|
return href;
|
|
64
|
+
const plan = buildLocaleLinkPlan(artifact);
|
|
46
65
|
let pathname = String(href);
|
|
47
66
|
let search = '';
|
|
48
67
|
let hash = '';
|
|
@@ -56,34 +75,12 @@ export function localizeSameAppHref(href, localeId, artifact) {
|
|
|
56
75
|
search = pathname.slice(qIdx);
|
|
57
76
|
pathname = pathname.slice(0, qIdx);
|
|
58
77
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
68
|
-
const parsed = parseLocaleFromPath(pathname, supported);
|
|
69
|
-
const rest = parsed.restPath || '/';
|
|
70
|
-
const realized = realizeRoutePath(localeId, rest, routing);
|
|
71
|
-
return `${realized.path}${search}${hash}`;
|
|
72
|
-
}
|
|
73
|
-
/** Rewrite `<a data-vmz-route href>` in HTML body to retain `localeId`. */
|
|
74
|
-
export function localizeBodyLinks(html, localeId, artifact, escapeAttr = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"')) {
|
|
75
|
-
if (!html || !localeId || !artifact)
|
|
76
|
-
return html;
|
|
77
|
-
return String(html).replace(/<a\b([^>]*)>/gi, (full, attrs) => {
|
|
78
|
-
if (!/\bdata-vmz-route\s*=/.test(attrs))
|
|
79
|
-
return full;
|
|
80
|
-
const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
|
|
81
|
-
if (!hm)
|
|
82
|
-
return full;
|
|
83
|
-
const next = localizeSameAppHref(hm[1], localeId, artifact);
|
|
84
|
-
if (next === hm[1])
|
|
85
|
-
return full;
|
|
86
|
-
const newAttrs = attrs.replace(/\bhref\s*=\s*"[^"]*"/i, `href="${escapeAttr(next)}"`);
|
|
87
|
-
return `<a${newAttrs}>`;
|
|
88
|
-
});
|
|
78
|
+
pathname = normalizePath(pathname || '/');
|
|
79
|
+
for (const row of plan.rows) {
|
|
80
|
+
if (row.localeId !== localeId)
|
|
81
|
+
continue;
|
|
82
|
+
if (normalizePath(row.href) === pathname)
|
|
83
|
+
return `${row.href}${search}${hash}`;
|
|
84
|
+
}
|
|
85
|
+
return href;
|
|
89
86
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
|
|
6
6
|
"exports": {
|
|
@@ -66,12 +66,12 @@
|
|
|
66
66
|
"linkedom": "^0.18.13"
|
|
67
67
|
},
|
|
68
68
|
"optionalDependencies": {
|
|
69
|
-
"@vmz/vmz-win32-x64": "0.1.
|
|
70
|
-
"@vmz/vmz-win32-arm64": "0.1.
|
|
71
|
-
"@vmz/vmz-darwin-x64": "0.1.
|
|
72
|
-
"@vmz/vmz-darwin-arm64": "0.1.
|
|
73
|
-
"@vmz/vmz-linux-x64": "0.1.
|
|
74
|
-
"@vmz/vmz-linux-arm64": "0.1.
|
|
69
|
+
"@vmz/vmz-win32-x64": "0.1.25",
|
|
70
|
+
"@vmz/vmz-win32-arm64": "0.1.25",
|
|
71
|
+
"@vmz/vmz-darwin-x64": "0.1.25",
|
|
72
|
+
"@vmz/vmz-darwin-arm64": "0.1.25",
|
|
73
|
+
"@vmz/vmz-linux-x64": "0.1.25",
|
|
74
|
+
"@vmz/vmz-linux-arm64": "0.1.25"
|
|
75
75
|
},
|
|
76
76
|
"publishConfig": {
|
|
77
77
|
"access": "public"
|