@vmz/core 0.1.28 → 0.1.30
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/client-nav.js +77 -5
- package/dist/dom-core.d.ts +17 -0
- package/dist/dom-core.js +91 -6
- package/dist/dom-ssr.js +20 -0
- package/dist/localize-body-links.d.ts +4 -2
- package/dist/localize-body-links.js +20 -3
- package/dist/serve-host.d.ts +2 -2
- package/dist/serve-host.mjs +42 -29
- package/package.json +7 -7
package/dist/client-nav.js
CHANGED
|
@@ -370,21 +370,40 @@ export function installClientNavigation(opts = {}) {
|
|
|
370
370
|
if (a.hasAttribute('download') || a.getAttribute('target') === '_blank')
|
|
371
371
|
return;
|
|
372
372
|
ev.preventDefault();
|
|
373
|
-
//
|
|
374
|
-
const realized = localizeClickHref(u.pathname + u.search + u.hash);
|
|
373
|
+
// Prefer frozen RouteId×LocaleId href table (0.1.30); fall back only when table missing.
|
|
374
|
+
const realized = localizeClickHref(u.pathname + u.search + u.hash, a.getAttribute('data-vmz-route'));
|
|
375
375
|
void transitionTo(realized, {
|
|
376
376
|
replace: a.getAttribute('data-vmz-replace') === 'true',
|
|
377
377
|
});
|
|
378
378
|
}
|
|
379
379
|
/**
|
|
380
380
|
* @param {string} href
|
|
381
|
+
* @param {string | null} [routeId]
|
|
381
382
|
*/
|
|
382
|
-
function localizeClickHref(href) {
|
|
383
|
+
function localizeClickHref(href, routeId) {
|
|
383
384
|
if (!doc?.documentElement)
|
|
384
385
|
return href;
|
|
385
386
|
const locale = doc.documentElement.getAttribute('data-locale');
|
|
387
|
+
if (!locale)
|
|
388
|
+
return href;
|
|
389
|
+
const frozen = lookupFrozenLocaleHref(routeId, locale);
|
|
390
|
+
if (frozen) {
|
|
391
|
+
let search = '';
|
|
392
|
+
let hash = '';
|
|
393
|
+
const hashIdx = href.indexOf('#');
|
|
394
|
+
let pathPart = href;
|
|
395
|
+
if (hashIdx >= 0) {
|
|
396
|
+
hash = pathPart.slice(hashIdx);
|
|
397
|
+
pathPart = pathPart.slice(0, hashIdx);
|
|
398
|
+
}
|
|
399
|
+
const qIdx = pathPart.indexOf('?');
|
|
400
|
+
if (qIdx >= 0) {
|
|
401
|
+
search = pathPart.slice(qIdx);
|
|
402
|
+
}
|
|
403
|
+
return `${frozen}${search}${hash}`;
|
|
404
|
+
}
|
|
386
405
|
const raw = doc.documentElement.getAttribute('data-vmz-locale-routing');
|
|
387
|
-
if (!
|
|
406
|
+
if (!raw)
|
|
388
407
|
return href;
|
|
389
408
|
let routing;
|
|
390
409
|
try {
|
|
@@ -617,7 +636,7 @@ export function installClientNavigation(opts = {}) {
|
|
|
617
636
|
}
|
|
618
637
|
}
|
|
619
638
|
/**
|
|
620
|
-
* Re-realize current URL under target LocaleId
|
|
639
|
+
* Re-realize current URL under target LocaleId via frozen href table when present.
|
|
621
640
|
* @param {string} href
|
|
622
641
|
* @param {string} localeId
|
|
623
642
|
* @param {{ strategy?: string, defaultPrefix?: string, defaultLocale?: string, locales?: string[] }} routing
|
|
@@ -638,6 +657,13 @@ export function installClientNavigation(opts = {}) {
|
|
|
638
657
|
}
|
|
639
658
|
if (!pathname)
|
|
640
659
|
pathname = '/';
|
|
660
|
+
const fromLocale = doc.documentElement?.getAttribute('data-locale') || null;
|
|
661
|
+
const routeId = doc.documentElement?.getAttribute('data-vmz-route') ||
|
|
662
|
+
doc.querySelector?.('[data-vmz-app][data-vmz-route]')?.getAttribute?.('data-vmz-route') ||
|
|
663
|
+
resolveRouteIdFromHrefTable(pathname, fromLocale);
|
|
664
|
+
const frozen = lookupFrozenLocaleHref(routeId, localeId);
|
|
665
|
+
if (frozen)
|
|
666
|
+
return `${frozen}${search}${hash}`;
|
|
641
667
|
const supported = Array.isArray(routing.locales) ? routing.locales : [];
|
|
642
668
|
const parts = pathname.split('/').filter(Boolean);
|
|
643
669
|
let rest = pathname;
|
|
@@ -659,6 +685,52 @@ export function installClientNavigation(opts = {}) {
|
|
|
659
685
|
const pathOut = rest === '/' ? `/${localeId}` : `/${localeId}${rest}`;
|
|
660
686
|
return `${pathOut}${search}${hash}`;
|
|
661
687
|
}
|
|
688
|
+
/**
|
|
689
|
+
* @returns {Record<string, Record<string, string>> | null}
|
|
690
|
+
*/
|
|
691
|
+
function readLocaleHrefTable() {
|
|
692
|
+
const raw = doc.documentElement?.getAttribute('data-vmz-locale-hrefs');
|
|
693
|
+
if (!raw)
|
|
694
|
+
return null;
|
|
695
|
+
try {
|
|
696
|
+
const table = JSON.parse(raw);
|
|
697
|
+
return table && typeof table === 'object' ? table : null;
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* @param {string | null | undefined} routeId
|
|
705
|
+
* @param {string} localeId
|
|
706
|
+
*/
|
|
707
|
+
function lookupFrozenLocaleHref(routeId, localeId) {
|
|
708
|
+
if (!routeId || !localeId)
|
|
709
|
+
return null;
|
|
710
|
+
const table = readLocaleHrefTable();
|
|
711
|
+
const href = table?.[routeId]?.[localeId];
|
|
712
|
+
return typeof href === 'string' && href && !/\[[^\]]+\]/.test(href) && !/\/:[^/]+/.test(href) ? href : null;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Reverse-lookup RouteId from frozen table when html lacks data-vmz-route.
|
|
716
|
+
* @param {string} pathname
|
|
717
|
+
* @param {string | null} localeId
|
|
718
|
+
*/
|
|
719
|
+
function resolveRouteIdFromHrefTable(pathname, localeId) {
|
|
720
|
+
const table = readLocaleHrefTable();
|
|
721
|
+
if (!table || !localeId)
|
|
722
|
+
return null;
|
|
723
|
+
const norm = pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname || '/';
|
|
724
|
+
for (const [routeId, byLocale] of Object.entries(table)) {
|
|
725
|
+
const href = byLocale?.[localeId];
|
|
726
|
+
if (typeof href !== 'string')
|
|
727
|
+
continue;
|
|
728
|
+
const h = href.length > 1 && href.endsWith('/') ? href.slice(0, -1) : href;
|
|
729
|
+
if (h === norm)
|
|
730
|
+
return routeId;
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
662
734
|
if (win) {
|
|
663
735
|
win.__vmzTransitionLocale = transitionLocale;
|
|
664
736
|
win.__vmzClientNavSetFetch = (fn) => {
|
package/dist/dom-core.d.ts
CHANGED
|
@@ -142,6 +142,23 @@ export declare const directApi: {
|
|
|
142
142
|
* @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
|
|
143
143
|
*/
|
|
144
144
|
bindAttr(inst: any, bindingId: any, deps: any, get: any, el: any, name: any, cf: any): void;
|
|
145
|
+
/**
|
|
146
|
+
* Specialized single-field attr bind (0.1.29): compile-time field name, no generic get closure in artifact.
|
|
147
|
+
* @param {object} inst
|
|
148
|
+
* @param {number|string|null} bindingId
|
|
149
|
+
* @param {string} fieldName
|
|
150
|
+
* @param {Element} el
|
|
151
|
+
* @param {string} name
|
|
152
|
+
*/
|
|
153
|
+
specFieldAttr(inst: any, bindingId: any, fieldName: any, el: any, name: any): void;
|
|
154
|
+
/**
|
|
155
|
+
* Specialized single-field text bind (0.1.29): compile-time field name in generated artifact.
|
|
156
|
+
* @param {object} inst
|
|
157
|
+
* @param {number|string|null} bindingId
|
|
158
|
+
* @param {string} fieldName
|
|
159
|
+
* @param {Text} textNode
|
|
160
|
+
*/
|
|
161
|
+
specFieldText(inst: any, bindingId: any, fieldName: any, textNode: any): void;
|
|
145
162
|
setHtml(el: any, value: any): void;
|
|
146
163
|
/**
|
|
147
164
|
* Trusted HTML binding (`html={expr}`). Author/plugin responsibility.
|
package/dist/dom-core.js
CHANGED
|
@@ -404,9 +404,13 @@ export const directApi = {
|
|
|
404
404
|
* @param {{ skipFlush?: boolean } | null | undefined} [opts]
|
|
405
405
|
*/
|
|
406
406
|
onMethod(el, type, methodName, opts) {
|
|
407
|
-
|
|
407
|
+
// Prefer each-block owner: item create often runs during flush when `_inst` is unset.
|
|
408
|
+
const eachOwner = directApi._eachCtx && directApi._eachCtx.inst;
|
|
409
|
+
const inst = eachOwner || directApi._inst;
|
|
410
|
+
if (!inst)
|
|
411
|
+
return;
|
|
408
412
|
let skipFlush = !!(opts && opts.skipFlush);
|
|
409
|
-
|
|
413
|
+
const invoke = (ev) => {
|
|
410
414
|
if (type === 'submit' && ev && typeof ev.preventDefault === 'function') {
|
|
411
415
|
ev.preventDefault();
|
|
412
416
|
}
|
|
@@ -429,7 +433,15 @@ export const directApi = {
|
|
|
429
433
|
skipFlush = true;
|
|
430
434
|
}
|
|
431
435
|
}
|
|
432
|
-
}
|
|
436
|
+
};
|
|
437
|
+
if (directApi._eachCtx) {
|
|
438
|
+
/** @type {Record<string, Function>} */
|
|
439
|
+
const bag = el.__vmzEvt || (el.__vmzEvt = Object.create(null));
|
|
440
|
+
bag[type] = (ev) => invoke(ev);
|
|
441
|
+
directApi._eachCtx.needDelegate(type);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
el.addEventListener(type, (ev) => invoke(ev));
|
|
433
445
|
},
|
|
434
446
|
/**
|
|
435
447
|
* @param {object} inst
|
|
@@ -467,6 +479,44 @@ export const directApi = {
|
|
|
467
479
|
}
|
|
468
480
|
}, cf);
|
|
469
481
|
},
|
|
482
|
+
/**
|
|
483
|
+
* Specialized single-field attr bind (0.1.29): compile-time field name, no generic get closure in artifact.
|
|
484
|
+
* @param {object} inst
|
|
485
|
+
* @param {number|string|null} bindingId
|
|
486
|
+
* @param {string} fieldName
|
|
487
|
+
* @param {Element} el
|
|
488
|
+
* @param {string} name
|
|
489
|
+
*/
|
|
490
|
+
specFieldAttr(inst, bindingId, fieldName, el, name) {
|
|
491
|
+
wireDirectBind(inst, bindingId, [fieldName], function specFieldAttrGet() {
|
|
492
|
+
return this[fieldName];
|
|
493
|
+
}, (raw) => {
|
|
494
|
+
if (name === 'class' || name === 'className') {
|
|
495
|
+
const s = String(raw ?? '');
|
|
496
|
+
if (s)
|
|
497
|
+
el.setAttribute('class', s);
|
|
498
|
+
else if (el.hasAttribute('class'))
|
|
499
|
+
el.removeAttribute('class');
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
applyDomAttr(el, name, raw);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
},
|
|
506
|
+
/**
|
|
507
|
+
* Specialized single-field text bind (0.1.29): compile-time field name in generated artifact.
|
|
508
|
+
* @param {object} inst
|
|
509
|
+
* @param {number|string|null} bindingId
|
|
510
|
+
* @param {string} fieldName
|
|
511
|
+
* @param {Text} textNode
|
|
512
|
+
*/
|
|
513
|
+
specFieldText(inst, bindingId, fieldName, textNode) {
|
|
514
|
+
wireDirectBind(inst, bindingId, [fieldName], function specFieldTextGet() {
|
|
515
|
+
return this[fieldName];
|
|
516
|
+
}, (raw) => {
|
|
517
|
+
textNode.textContent = String(raw ?? '');
|
|
518
|
+
});
|
|
519
|
+
},
|
|
470
520
|
setHtml(el, value) {
|
|
471
521
|
el.innerHTML = value == null ? '' : String(value);
|
|
472
522
|
},
|
|
@@ -1153,7 +1203,9 @@ export const directApi = {
|
|
|
1153
1203
|
delegateTypes.add(type);
|
|
1154
1204
|
ensureDelegateAttached();
|
|
1155
1205
|
};
|
|
1156
|
-
|
|
1206
|
+
// `inst` must ride eachCtx: month/list rebuilds call createItem during flush
|
|
1207
|
+
// when `directApi._inst` is null (unlike ifBlock branch create which sets it).
|
|
1208
|
+
const eachCtx = { inst, noteItemBind, needDelegate };
|
|
1157
1209
|
const clearDomEvt = (root) => {
|
|
1158
1210
|
if (!root || root.nodeType !== 1)
|
|
1159
1211
|
return;
|
|
@@ -1657,8 +1709,20 @@ export const directApi = {
|
|
|
1657
1709
|
attrEls[i] = attrSlots[i].get(root);
|
|
1658
1710
|
for (let i = 0; i < nOn; i++) {
|
|
1659
1711
|
const el = onSlots[i].get(root);
|
|
1660
|
-
|
|
1661
|
-
|
|
1712
|
+
const slot = onSlots[i];
|
|
1713
|
+
if (slot.directMethod) {
|
|
1714
|
+
/** @type {Record<string, Function>} */
|
|
1715
|
+
const bag = el.__vmzEvt || (el.__vmzEvt = Object.create(null));
|
|
1716
|
+
const method = slot.method;
|
|
1717
|
+
bag[slot.type] = (ev) => {
|
|
1718
|
+
const fn = inst[method];
|
|
1719
|
+
if (typeof fn !== 'function')
|
|
1720
|
+
return;
|
|
1721
|
+
runDomEventHandler(inst, method, () => fn.call(inst, ev));
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
else if (!el.__vmzAct) {
|
|
1725
|
+
el.__vmzAct = slot.method;
|
|
1662
1726
|
}
|
|
1663
1727
|
}
|
|
1664
1728
|
for (let i = 0; i < nText; i++) {
|
|
@@ -1834,6 +1898,17 @@ export const directApi = {
|
|
|
1834
1898
|
api.on(el, type, handler);
|
|
1835
1899
|
recordFailed = true;
|
|
1836
1900
|
},
|
|
1901
|
+
onMethod(el, type, methodName, opts) {
|
|
1902
|
+
if (!recordFailed) {
|
|
1903
|
+
pending.ons.push({
|
|
1904
|
+
node: el,
|
|
1905
|
+
type,
|
|
1906
|
+
method: methodName,
|
|
1907
|
+
directMethod: true,
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
api.onMethod(el, type, methodName, opts);
|
|
1911
|
+
},
|
|
1837
1912
|
});
|
|
1838
1913
|
const dom = userCreateItem.call(inst, recordingApi, box);
|
|
1839
1914
|
if (!recordFailed && dom && dom.nodeType === 1 && root === dom && (pending.texts.length > 0 || pending.attrs.length > 0)) {
|
|
@@ -1915,6 +1990,7 @@ export const directApi = {
|
|
|
1915
1990
|
path,
|
|
1916
1991
|
type: p.type,
|
|
1917
1992
|
method: p.method,
|
|
1993
|
+
directMethod: !!p.directMethod,
|
|
1918
1994
|
get: makeChildGetter(path),
|
|
1919
1995
|
});
|
|
1920
1996
|
}
|
|
@@ -2070,8 +2146,10 @@ export const directApi = {
|
|
|
2070
2146
|
const patches0 = [];
|
|
2071
2147
|
const prevPatches = directApi._itemPatches;
|
|
2072
2148
|
const prevCtx = directApi._eachCtx;
|
|
2149
|
+
const prevInst = directApi._inst;
|
|
2073
2150
|
directApi._itemPatches = patches0;
|
|
2074
2151
|
directApi._eachCtx = eachCtx;
|
|
2152
|
+
directApi._inst = inst;
|
|
2075
2153
|
let dom0 = null;
|
|
2076
2154
|
try {
|
|
2077
2155
|
dom0 = createItem(directApi, box0, patches0);
|
|
@@ -2079,6 +2157,7 @@ export const directApi = {
|
|
|
2079
2157
|
finally {
|
|
2080
2158
|
directApi._itemPatches = prevPatches;
|
|
2081
2159
|
directApi._eachCtx = prevCtx;
|
|
2160
|
+
directApi._inst = prevInst;
|
|
2082
2161
|
}
|
|
2083
2162
|
if (applied !== gen || inst.__vmzDestroyed)
|
|
2084
2163
|
return;
|
|
@@ -2203,8 +2282,10 @@ export const directApi = {
|
|
|
2203
2282
|
const patches = [];
|
|
2204
2283
|
const prevPatches = directApi._itemPatches;
|
|
2205
2284
|
const prevCtx = directApi._eachCtx;
|
|
2285
|
+
const prevInst = directApi._inst;
|
|
2206
2286
|
directApi._itemPatches = patches;
|
|
2207
2287
|
directApi._eachCtx = eachCtx;
|
|
2288
|
+
directApi._inst = inst;
|
|
2208
2289
|
let dom = null;
|
|
2209
2290
|
try {
|
|
2210
2291
|
dom = createItem(directApi, box, patches);
|
|
@@ -2212,6 +2293,7 @@ export const directApi = {
|
|
|
2212
2293
|
finally {
|
|
2213
2294
|
directApi._itemPatches = prevPatches;
|
|
2214
2295
|
directApi._eachCtx = prevCtx;
|
|
2296
|
+
directApi._inst = prevInst;
|
|
2215
2297
|
}
|
|
2216
2298
|
if (applied !== gen || inst.__vmzDestroyed)
|
|
2217
2299
|
return;
|
|
@@ -2326,8 +2408,10 @@ export const directApi = {
|
|
|
2326
2408
|
const patches = [];
|
|
2327
2409
|
const prevPatches = directApi._itemPatches;
|
|
2328
2410
|
const prevCtx = directApi._eachCtx;
|
|
2411
|
+
const prevInst = directApi._inst;
|
|
2329
2412
|
directApi._itemPatches = patches;
|
|
2330
2413
|
directApi._eachCtx = eachCtx;
|
|
2414
|
+
directApi._inst = inst;
|
|
2331
2415
|
let dom = null;
|
|
2332
2416
|
try {
|
|
2333
2417
|
dom = createItem(directApi, box, patches);
|
|
@@ -2335,6 +2419,7 @@ export const directApi = {
|
|
|
2335
2419
|
finally {
|
|
2336
2420
|
directApi._itemPatches = prevPatches;
|
|
2337
2421
|
directApi._eachCtx = prevCtx;
|
|
2422
|
+
directApi._inst = prevInst;
|
|
2338
2423
|
}
|
|
2339
2424
|
if (applied !== gen || inst.__vmzDestroyed)
|
|
2340
2425
|
return;
|
package/dist/dom-ssr.js
CHANGED
|
@@ -591,6 +591,16 @@ const serializeApi = {
|
|
|
591
591
|
}
|
|
592
592
|
textNode.value = String(raw ?? '');
|
|
593
593
|
},
|
|
594
|
+
specFieldText(inst, _bindingId, fieldName, textNode) {
|
|
595
|
+
let raw;
|
|
596
|
+
try {
|
|
597
|
+
raw = inst[fieldName];
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
raw = '';
|
|
601
|
+
}
|
|
602
|
+
textNode.value = String(raw ?? '');
|
|
603
|
+
},
|
|
594
604
|
bindAttr(inst, bindingId, deps, get, el, name) {
|
|
595
605
|
let raw;
|
|
596
606
|
try {
|
|
@@ -601,6 +611,16 @@ const serializeApi = {
|
|
|
601
611
|
}
|
|
602
612
|
applySerializeAttr(el, name, raw);
|
|
603
613
|
},
|
|
614
|
+
specFieldAttr(inst, _bindingId, fieldName, el, name) {
|
|
615
|
+
let raw;
|
|
616
|
+
try {
|
|
617
|
+
raw = inst[fieldName];
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
raw = null;
|
|
621
|
+
}
|
|
622
|
+
applySerializeAttr(el, name, raw);
|
|
623
|
+
},
|
|
604
624
|
bindComponentProp() {
|
|
605
625
|
/* SSR: props already resolved into the child instance at create */
|
|
606
626
|
},
|
|
@@ -49,7 +49,9 @@ export declare function linkRouteAliasesFromUnits(units: Array<{
|
|
|
49
49
|
export declare function buildLocaleLinkPlan(artifact: LocaleHrefArtifact | null | undefined): LocaleLinkPlan;
|
|
50
50
|
/** Apply plan rows to `<a data-vmz-route="RouteId">` markers for one LocaleId. */
|
|
51
51
|
export declare function applyLocaleLinkPlan(html: string, localeId: string, plan: LocaleLinkPlan, escapeAttr?: (s: string) => string): string;
|
|
52
|
-
/** Rewrite `<a data-vmz-route href>` using locale
|
|
53
|
-
export declare function localizeBodyLinks(html: string, localeId: string, artifact: LocaleHrefArtifact, escapeAttr?: (s: string) => string): string;
|
|
52
|
+
/** Rewrite `<a data-vmz-route href>` using a frozen locale link plan (preferred) or build once from realization. */
|
|
53
|
+
export declare function localizeBodyLinks(html: string, localeId: string, artifact: LocaleHrefArtifact, escapeAttr?: (s: string) => string, frozenPlan?: LocaleLinkPlan | null): string;
|
|
54
|
+
/** Compact RouteId → LocaleId → href table for browser (no path-prefix algebra). */
|
|
55
|
+
export declare function localeHrefTableFromPlan(plan: LocaleLinkPlan | null | undefined): Record<string, Record<string, string>>;
|
|
54
56
|
/** Legacy same-app href helper for callers without `data-vmz-route` markers. */
|
|
55
57
|
export declare function localizeSameAppHref(href: string, localeId: string, artifact: LocaleHrefArtifact): string;
|
|
@@ -71,11 +71,28 @@ export function applyLocaleLinkPlan(html, localeId, plan, escapeAttr = (s) => St
|
|
|
71
71
|
return `<a${newAttrs}>`;
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
/** Rewrite `<a data-vmz-route href>` using locale
|
|
75
|
-
export function localizeBodyLinks(html, localeId, artifact, escapeAttr) {
|
|
74
|
+
/** Rewrite `<a data-vmz-route href>` using a frozen locale link plan (preferred) or build once from realization. */
|
|
75
|
+
export function localizeBodyLinks(html, localeId, artifact, escapeAttr, frozenPlan) {
|
|
76
76
|
if (!html || !localeId || !artifact)
|
|
77
77
|
return html;
|
|
78
|
-
|
|
78
|
+
const plan = frozenPlan && frozenPlan.schema === LOCALE_LINK_PLAN_SCHEMA && Array.isArray(frozenPlan.rows)
|
|
79
|
+
? frozenPlan
|
|
80
|
+
: buildLocaleLinkPlan(artifact);
|
|
81
|
+
return applyLocaleLinkPlan(html, localeId, plan, escapeAttr);
|
|
82
|
+
}
|
|
83
|
+
/** Compact RouteId → LocaleId → href table for browser (no path-prefix algebra). */
|
|
84
|
+
export function localeHrefTableFromPlan(plan) {
|
|
85
|
+
/** @type {Record<string, Record<string, string>>} */
|
|
86
|
+
const out = Object.create(null);
|
|
87
|
+
for (const row of plan?.rows || []) {
|
|
88
|
+
if (!row?.routeId || !row?.localeId || !row?.href)
|
|
89
|
+
continue;
|
|
90
|
+
if (/\[[^\]]+\]/.test(row.href) || /\/:[^/]+/.test(row.href))
|
|
91
|
+
continue;
|
|
92
|
+
const byLocale = out[row.routeId] || (out[row.routeId] = Object.create(null));
|
|
93
|
+
byLocale[row.localeId] = row.href;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
79
96
|
}
|
|
80
97
|
function normalizePath(pathname) {
|
|
81
98
|
let p = String(pathname || '/');
|
package/dist/serve-host.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Invoked by `vmz serve` / `vmz dev` (or: node dist/vmz-serve-host.mjs).
|
|
5
5
|
*
|
|
6
|
-
* Pathname matches `
|
|
7
|
-
*
|
|
6
|
+
* Pathname matches compiled `_vmz/route-catalog.json` (frozen segs / pathPattern).
|
|
7
|
+
* Hosts must not re-parse deployment pathPattern into a live catalog.
|
|
8
8
|
* Not an SPA shell.
|
|
9
9
|
*
|
|
10
10
|
* `VMZ_DEV=1`: POST `/__vmz/reload` soft-reloads modules (cache-bust import);
|
package/dist/serve-host.mjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Invoked by `vmz serve` / `vmz dev` (or: node dist/vmz-serve-host.mjs).
|
|
6
6
|
*
|
|
7
|
-
* Pathname matches `
|
|
8
|
-
*
|
|
7
|
+
* Pathname matches compiled `_vmz/route-catalog.json` (frozen segs / pathPattern).
|
|
8
|
+
* Hosts must not re-parse deployment pathPattern into a live catalog.
|
|
9
9
|
* Not an SPA shell.
|
|
10
10
|
*
|
|
11
11
|
* `VMZ_DEV=1`: POST `/__vmz/reload` soft-reloads modules (cache-bust import);
|
|
@@ -23,11 +23,14 @@ import { createRequire, registerHooks } from 'node:module';
|
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
25
|
import { listClientComponents } from './list-client-components.js';
|
|
26
|
-
import { linkRouteAliasesFromUnits, localizeBodyLinks } from './localize-body-links.js';
|
|
26
|
+
import { LOCALE_LINK_PLAN_SCHEMA, linkRouteAliasesFromUnits, localeHrefTableFromPlan, localizeBodyLinks } from './localize-body-links.js';
|
|
27
27
|
import { loadNativeAddon } from './native-addon.js';
|
|
28
28
|
import { createRenderHost } from './render-host.js';
|
|
29
29
|
import { resolveRouteLayoutChain } from './route-layout-chain.js';
|
|
30
30
|
import { handleNodeRequest, setRoutes, setServerModuleResolver } from './vmz-runtime.js';
|
|
31
|
+
const ROUTE_CATALOG_SCHEMA = 'vmz.route.catalog.v0';
|
|
32
|
+
const ROUTE_CATALOG_REL = '_vmz/route-catalog.json';
|
|
33
|
+
const LOCALE_LINK_PLAN_REL = '_vmz/locale-link-plan.json';
|
|
31
34
|
const require = createRequire(import.meta.url);
|
|
32
35
|
const distDir = process.env.VMZ_DIST ? path.resolve(process.env.VMZ_DIST) : path.dirname(fileURLToPath(import.meta.url));
|
|
33
36
|
/** App project root for bare import / JSON resolve (dev SSR ≡ build node_modules). */
|
|
@@ -154,6 +157,8 @@ let styleBundleHash = null;
|
|
|
154
157
|
let styleTheme = null;
|
|
155
158
|
/** Locale route realization artifact from `_vmz/locale-route-realization.json` (optional). */
|
|
156
159
|
let localeArtifact = null;
|
|
160
|
+
/** Frozen locale link plan from `_vmz/locale-link-plan.json` (0.1.30 authority). */
|
|
161
|
+
let localeLinkPlan = null;
|
|
157
162
|
/** @type {Set<import('node:http').ServerResponse>} */
|
|
158
163
|
const sseClients = new Set();
|
|
159
164
|
/** In-flight HTTP requests (graceful shutdown drain). */
|
|
@@ -575,6 +580,10 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
575
580
|
defaultLocale: localeArtifact.defaultLocale,
|
|
576
581
|
locales: (localeArtifact.locales || []).map((l) => l.id),
|
|
577
582
|
}));
|
|
583
|
+
const hrefTable = localeHrefTableFromPlan(localeLinkPlan);
|
|
584
|
+
if (hrefTable && Object.keys(hrefTable).length) {
|
|
585
|
+
htmlExtraAttrs.push('data-vmz-locale-hrefs', JSON.stringify(hrefTable));
|
|
586
|
+
}
|
|
578
587
|
}
|
|
579
588
|
const pageDocMeta = resolvePageDocumentMeta(Page);
|
|
580
589
|
const prevLocaleHint = globalThis.__vmzLocaleIdHint;
|
|
@@ -605,9 +614,9 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
605
614
|
if (signal?.aborted)
|
|
606
615
|
return;
|
|
607
616
|
}
|
|
608
|
-
// Locale discipline:
|
|
617
|
+
// Locale discipline: apply frozen link plan rows (0.1.30) — no path algebra.
|
|
609
618
|
if (localeArtifact && localeId) {
|
|
610
|
-
bodyHtml = localizeBodyLinks(bodyHtml, localeId, localeArtifact);
|
|
619
|
+
bodyHtml = localizeBodyLinks(bodyHtml, localeId, localeArtifact, undefined, localeLinkPlan);
|
|
611
620
|
}
|
|
612
621
|
}
|
|
613
622
|
finally {
|
|
@@ -816,6 +825,13 @@ async function softReload(opts = {}) {
|
|
|
816
825
|
catch {
|
|
817
826
|
localeArtifact = null;
|
|
818
827
|
}
|
|
828
|
+
try {
|
|
829
|
+
const rawPlan = JSON.parse(await readFile(path.join(distDir, ...LOCALE_LINK_PLAN_REL.split('/')), 'utf8'));
|
|
830
|
+
localeLinkPlan = rawPlan?.schema === LOCALE_LINK_PLAN_SCHEMA && Array.isArray(rawPlan.rows) ? rawPlan : null;
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
localeLinkPlan = null;
|
|
834
|
+
}
|
|
819
835
|
const componentEntries = await listClientComponents(distDir, { strict: !isDev });
|
|
820
836
|
ssrRenderHost = await createRenderHost(distDir, {
|
|
821
837
|
strictDeployment: !isDev,
|
|
@@ -1108,54 +1124,51 @@ async function loadPageCtor(chunkId) {
|
|
|
1108
1124
|
return mod.default;
|
|
1109
1125
|
}
|
|
1110
1126
|
/**
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1127
|
+
* Load compiled page catalog from `_vmz/route-catalog.json` (0.1.30 authority).
|
|
1128
|
+
* Hosts must not re-parse deployment pathPattern into a live catalog.
|
|
1113
1129
|
* @param {string} dir
|
|
1114
1130
|
*/
|
|
1115
1131
|
async function listPageClientFiles(dir) {
|
|
1116
|
-
const
|
|
1117
|
-
if (!
|
|
1132
|
+
const fromCatalog = await listPagesFromRouteCatalog(dir);
|
|
1133
|
+
if (!fromCatalog.length) {
|
|
1118
1134
|
if (isDev) {
|
|
1119
|
-
console.warn(`vmz dev:
|
|
1135
|
+
console.warn(`vmz dev: missing compiled ${ROUTE_CATALOG_SCHEMA} at ${path.join(dir, ...ROUTE_CATALOG_REL.split('/'))}`);
|
|
1120
1136
|
return [];
|
|
1121
1137
|
}
|
|
1122
|
-
throw new Error(`vmz serve:
|
|
1138
|
+
throw new Error(`vmz serve: missing compiled ${ROUTE_CATALOG_SCHEMA} at ${path.join(dir, ...ROUTE_CATALOG_REL.split('/'))}`);
|
|
1123
1139
|
}
|
|
1124
|
-
return
|
|
1140
|
+
return fromCatalog;
|
|
1125
1141
|
}
|
|
1126
1142
|
/**
|
|
1127
1143
|
* @param {string} dir
|
|
1128
1144
|
*/
|
|
1129
|
-
async function
|
|
1130
|
-
/** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parsePathPattern> }>} */
|
|
1145
|
+
async function listPagesFromRouteCatalog(dir) {
|
|
1146
|
+
/** @type {Array<{ chunkId: string, pageRel: string, routeId?: string, pathPattern?: string, segs: ReturnType<typeof parsePathPattern> }>} */
|
|
1131
1147
|
const out = [];
|
|
1132
1148
|
try {
|
|
1133
|
-
const raw = await readFile(path.join(dir,
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
const chunkId = String(
|
|
1149
|
+
const raw = await readFile(path.join(dir, ...ROUTE_CATALOG_REL.split('/')), 'utf8');
|
|
1150
|
+
const catalog = JSON.parse(raw);
|
|
1151
|
+
if (catalog?.schema !== ROUTE_CATALOG_SCHEMA || !Array.isArray(catalog.pages))
|
|
1152
|
+
return [];
|
|
1153
|
+
for (const page of catalog.pages) {
|
|
1154
|
+
const chunkId = String(page?.chunkId || '').replace(/\\/g, '/');
|
|
1139
1155
|
if (!chunkId.startsWith('pages/'))
|
|
1140
1156
|
continue;
|
|
1141
1157
|
const stem = chunkId.split('/').pop() || '';
|
|
1142
1158
|
if (isRouteBoundaryStem(stem))
|
|
1143
1159
|
continue;
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
throw new Error(`vmz serve: page unit ${chunkId} missing pathPattern in vmz-deployment.json (plan-only host)`);
|
|
1147
|
-
}
|
|
1148
|
-
const pageRel = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
|
|
1160
|
+
const segs = Array.isArray(page?.segs) && page.segs.length ? page.segs : parsePathPattern(String(page?.pathPattern || ''));
|
|
1161
|
+
const pageRel = String(page?.pageRel || `${chunkId}.client.js`).replace(/\\/g, '/');
|
|
1149
1162
|
out.push({
|
|
1150
1163
|
chunkId,
|
|
1151
1164
|
pageRel,
|
|
1152
|
-
|
|
1165
|
+
routeId: typeof page?.routeId === 'string' ? page.routeId : chunkId,
|
|
1166
|
+
pathPattern: typeof page?.pathPattern === 'string' ? page.pathPattern : undefined,
|
|
1167
|
+
segs,
|
|
1153
1168
|
});
|
|
1154
1169
|
}
|
|
1155
1170
|
}
|
|
1156
|
-
catch
|
|
1157
|
-
if (err && typeof err.message === 'string' && err.message.includes('plan-only host'))
|
|
1158
|
-
throw err;
|
|
1171
|
+
catch {
|
|
1159
1172
|
return [];
|
|
1160
1173
|
}
|
|
1161
1174
|
return out;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
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.30",
|
|
70
|
+
"@vmz/vmz-win32-arm64": "0.1.30",
|
|
71
|
+
"@vmz/vmz-darwin-x64": "0.1.30",
|
|
72
|
+
"@vmz/vmz-darwin-arm64": "0.1.30",
|
|
73
|
+
"@vmz/vmz-linux-x64": "0.1.30",
|
|
74
|
+
"@vmz/vmz-linux-arm64": "0.1.30"
|
|
75
75
|
},
|
|
76
76
|
"publishConfig": {
|
|
77
77
|
"access": "public"
|