mnfst 0.5.41 → 0.5.43
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/manifest.router.js +228 -13
- package/package.json +1 -1
package/dist/manifest.router.js
CHANGED
|
@@ -173,6 +173,169 @@ function getBasePath() {
|
|
|
173
173
|
return (typeof window.getManifestBasePath === 'function' ? window.getManifestBasePath() : '') || '';
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
// Locale codes from manifest data (same idea as ManifestRouting.matchesCondition).
|
|
177
|
+
function getLocalizationCodesFromManifest() {
|
|
178
|
+
const localizationCodes = [];
|
|
179
|
+
try {
|
|
180
|
+
const manifest = window.ManifestComponentsRegistry?.manifest || window.manifest;
|
|
181
|
+
if (manifest?.data && typeof manifest.data === 'object') {
|
|
182
|
+
Object.values(manifest.data).forEach((dataSource) => {
|
|
183
|
+
if (typeof dataSource === 'object' && dataSource !== null) {
|
|
184
|
+
Object.keys(dataSource).forEach((key) => {
|
|
185
|
+
if (key.match(/^[a-z]{2}(-[A-Z]{2})?$/)) {
|
|
186
|
+
localizationCodes.push(key);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
/* ignore */
|
|
194
|
+
}
|
|
195
|
+
return [...new Set(localizationCodes)];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function logicalSegmentsFromPathname(pathname) {
|
|
199
|
+
const logical = pathnameToLogical(pathname);
|
|
200
|
+
const s = logical.replace(/^\/+|\/+$/g, '');
|
|
201
|
+
return s ? s.split('/') : [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const STICKY_LOCALE_SKIP_FIRST_SEGMENTS = new Set([
|
|
205
|
+
'api',
|
|
206
|
+
'assets',
|
|
207
|
+
'static',
|
|
208
|
+
'public',
|
|
209
|
+
'dist',
|
|
210
|
+
'icons',
|
|
211
|
+
'fonts',
|
|
212
|
+
'media',
|
|
213
|
+
'.well-known',
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
const STICKY_LOCALE_STATIC_FILE_EXT = new Set([
|
|
217
|
+
'js', 'mjs', 'cjs', 'css', 'map', 'png', 'jpg', 'jpeg', 'webp', 'gif', 'svg', 'ico',
|
|
218
|
+
'woff', 'woff2', 'ttf', 'eot', 'json', 'xml', 'txt', 'pdf', 'zip', 'wasm', 'avif',
|
|
219
|
+
'mp4', 'webm', 'mp3',
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
function shouldSkipStickyLocaleForLogicalSegments(segments) {
|
|
223
|
+
if (!segments.length) return false;
|
|
224
|
+
if (STICKY_LOCALE_SKIP_FIRST_SEGMENTS.has(segments[0])) return true;
|
|
225
|
+
const last = segments[segments.length - 1];
|
|
226
|
+
if (!last || !last.includes('.')) return false;
|
|
227
|
+
const ext = last.slice(last.lastIndexOf('.') + 1).toLowerCase();
|
|
228
|
+
return STICKY_LOCALE_STATIC_FILE_EXT.has(ext);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// manifest.prerender.localeRouteExclude → JSON array in meta; logical path prefixes (after locale).
|
|
232
|
+
function parseLocaleRouteExcludePatterns() {
|
|
233
|
+
const meta = document.querySelector('meta[name="manifest:locale-route-exclude"]');
|
|
234
|
+
const raw = meta?.getAttribute('content') || '';
|
|
235
|
+
if (raw.trim().startsWith('[')) {
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(raw.replace(/"/g, '"'));
|
|
238
|
+
if (Array.isArray(parsed)) {
|
|
239
|
+
return parsed.map((s) => String(s).trim()).filter(Boolean);
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
/* fall through */
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const legacy = document.querySelector('meta[name="manifest:locale-sticky-exclude"]');
|
|
246
|
+
const legacyRaw = legacy?.getAttribute('content') || '';
|
|
247
|
+
if (!legacyRaw.trim()) return [];
|
|
248
|
+
return legacyRaw.split(',').map((s) => s.trim().replace(/^\/+/, '').split('/')[0]).filter(Boolean);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function logicalPathMatchesLocaleRouteExclude(segments, patterns) {
|
|
252
|
+
if (!patterns.length || !segments.length) return false;
|
|
253
|
+
const lower = segments.map((s) => s.toLowerCase());
|
|
254
|
+
for (const pattern of patterns) {
|
|
255
|
+
const p = String(pattern)
|
|
256
|
+
.trim()
|
|
257
|
+
.replace(/^\/+/, '')
|
|
258
|
+
.split('/')
|
|
259
|
+
.filter(Boolean)
|
|
260
|
+
.map((x) => x.toLowerCase());
|
|
261
|
+
if (p.length === 0) continue;
|
|
262
|
+
if (lower.length < p.length) continue;
|
|
263
|
+
let match = true;
|
|
264
|
+
for (let i = 0; i < p.length; i++) {
|
|
265
|
+
if (lower[i] !== p[i]) {
|
|
266
|
+
match = false;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (match) return true;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// /fr/legal/terms → /legal/terms when patterns include "legal" or "legal/terms" (prefix match).
|
|
276
|
+
function normalizeRedundantLocalePrefixInUrl() {
|
|
277
|
+
if (!isPrerenderedStaticBuild()) return false;
|
|
278
|
+
const codes = getLocalizationCodesFromManifest();
|
|
279
|
+
const patterns = parseLocaleRouteExcludePatterns();
|
|
280
|
+
if (!codes.length || !patterns.length) return false;
|
|
281
|
+
|
|
282
|
+
const segs = logicalSegmentsFromPathname(window.location.pathname);
|
|
283
|
+
if (segs.length < 2) return false;
|
|
284
|
+
if (!codes.includes(segs[0])) return false;
|
|
285
|
+
const rest = segs.slice(1);
|
|
286
|
+
if (!logicalPathMatchesLocaleRouteExclude(rest, patterns)) return false;
|
|
287
|
+
|
|
288
|
+
const newLogical = '/' + rest.join('/');
|
|
289
|
+
const base = getBasePath();
|
|
290
|
+
let newPathname = base ? base.replace(/\/+$/, '') + newLogical : newLogical;
|
|
291
|
+
newPathname = newPathname.replace(/\/{2,}/g, '/');
|
|
292
|
+
if (!newPathname.startsWith('/')) newPathname = '/' + newPathname;
|
|
293
|
+
if (newPathname === window.location.pathname) return false;
|
|
294
|
+
|
|
295
|
+
const prevLogical = pathnameToLogical(window.location.pathname);
|
|
296
|
+
const u = new URL(window.location.href);
|
|
297
|
+
u.pathname = newPathname;
|
|
298
|
+
history.replaceState(null, '', u.toString());
|
|
299
|
+
currentRoute = pathnameToLogical(newPathname);
|
|
300
|
+
const np = currentRoute === '/' ? '/' : String(currentRoute).replace(/^\/|\/$/g, '');
|
|
301
|
+
window.dispatchEvent(new CustomEvent('manifest:route-change', {
|
|
302
|
+
detail: { from: prevLogical, to: currentRoute, normalizedPath: np }
|
|
303
|
+
}));
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// When the URL already has a locale prefix (e.g. /zh/pricing), keep it for same-origin links
|
|
308
|
+
// that omit the prefix (/articles → /zh/articles). No-op on default-locale URLs (/pricing).
|
|
309
|
+
function applyStickyLocaleToPathname(absolutePathname) {
|
|
310
|
+
const codes = getLocalizationCodesFromManifest();
|
|
311
|
+
if (!codes.length) return absolutePathname;
|
|
312
|
+
|
|
313
|
+
const currentSegs = logicalSegmentsFromPathname(window.location.pathname);
|
|
314
|
+
const sticky = currentSegs.length && codes.includes(currentSegs[0]) ? currentSegs[0] : null;
|
|
315
|
+
if (!sticky) return absolutePathname;
|
|
316
|
+
|
|
317
|
+
const targetSegs = logicalSegmentsFromPathname(absolutePathname);
|
|
318
|
+
if (targetSegs.length && codes.includes(targetSegs[0])) {
|
|
319
|
+
return absolutePathname;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const routeEx = parseLocaleRouteExcludePatterns();
|
|
323
|
+
if (routeEx.length && logicalPathMatchesLocaleRouteExclude(targetSegs, routeEx)) {
|
|
324
|
+
return absolutePathname;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (shouldSkipStickyLocaleForLogicalSegments(targetSegs)) {
|
|
328
|
+
return absolutePathname;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const base = getBasePath();
|
|
332
|
+
const newLogical = targetSegs.length ? `/${sticky}/${targetSegs.join('/')}` : `/${sticky}`;
|
|
333
|
+
const normalizedLogical = newLogical.replace(/\/{2,}/g, '/') || '/';
|
|
334
|
+
if (!base) return normalizedLogical;
|
|
335
|
+
const combined = `${base}${normalizedLogical}`.replace(/([^:])\/{2,}/g, '$1/');
|
|
336
|
+
return combined.startsWith('/') ? combined : `/${combined}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
176
339
|
function pathnameToLogical(pathname) {
|
|
177
340
|
const base = getBasePath();
|
|
178
341
|
if (!base) {
|
|
@@ -252,27 +415,79 @@ function resolveHref(href) {
|
|
|
252
415
|
const url = new URL(href, baseUrl);
|
|
253
416
|
if (url.origin !== window.location.origin) return href;
|
|
254
417
|
let path = url.pathname.replace(/\/$/, '') || '/';
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
if (path.startsWith('/')) {
|
|
418
|
+
let resolved;
|
|
419
|
+
if (!base) {
|
|
420
|
+
resolved = path.startsWith('/') ? path : '/' + path;
|
|
421
|
+
} else if (path === base || path.startsWith(base + '/')) {
|
|
422
|
+
resolved = path;
|
|
423
|
+
} else if (path.startsWith('/')) {
|
|
259
424
|
const pathSegs = path.split('/').filter(Boolean);
|
|
260
425
|
const baseSegs = base.split('/').filter(Boolean);
|
|
261
426
|
let i = 0;
|
|
262
427
|
while (i < baseSegs.length && i < pathSegs.length && baseSegs[i] === pathSegs[i]) i++;
|
|
263
428
|
const routeSegs = pathSegs.slice(i);
|
|
264
|
-
if (routeSegs.length)
|
|
429
|
+
if (routeSegs.length) {
|
|
430
|
+
resolved = base + '/' + routeSegs.join('/');
|
|
431
|
+
} else {
|
|
432
|
+
const out = base + (path.startsWith('/') ? path : '/' + path);
|
|
433
|
+
resolved = out.startsWith('/') ? out : '/' + out;
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
const out = base + (path.startsWith('/') ? path : '/' + path);
|
|
437
|
+
resolved = out.startsWith('/') ? out : '/' + out;
|
|
265
438
|
}
|
|
266
|
-
|
|
267
|
-
return out.startsWith('/') ? out : '/' + out;
|
|
439
|
+
return applyStickyLocaleToPathname(resolved);
|
|
268
440
|
} catch {
|
|
269
441
|
const base = getBasePath();
|
|
270
442
|
const safe = (href || '').trim();
|
|
271
|
-
if (!safe) return base || '/';
|
|
272
|
-
|
|
443
|
+
if (!safe) return applyStickyLocaleToPathname(base || '/');
|
|
444
|
+
const raw = base ? (base + (safe.startsWith('/') ? safe : '/' + safe)) : (safe.startsWith('/') ? safe : '/' + safe);
|
|
445
|
+
return applyStickyLocaleToPathname(raw);
|
|
273
446
|
}
|
|
274
447
|
}
|
|
275
448
|
|
|
449
|
+
// Prerendered MPA: same-origin navigations use full page loads; rewrite targets so locale prefix sticks.
|
|
450
|
+
function installMpaStickyLocaleLinks() {
|
|
451
|
+
document.addEventListener('click', (event) => {
|
|
452
|
+
if (event.defaultPrevented) return;
|
|
453
|
+
if (event.button !== 0) return;
|
|
454
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
455
|
+
|
|
456
|
+
const link = event.target.closest('a');
|
|
457
|
+
if (!link || link.closest('[data-manifest-skip-locale-sticky]')) return;
|
|
458
|
+
if (link.hasAttribute('download')) return;
|
|
459
|
+
|
|
460
|
+
const hrefAttr = link.getAttribute('href');
|
|
461
|
+
if (!hrefAttr || hrefAttr.startsWith('mailto:') || hrefAttr.startsWith('tel:') || hrefAttr.startsWith('javascript:')) return;
|
|
462
|
+
if (hrefAttr.startsWith('#')) return;
|
|
463
|
+
|
|
464
|
+
let url;
|
|
465
|
+
try {
|
|
466
|
+
url = new URL(hrefAttr, window.location.href);
|
|
467
|
+
} catch {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (url.origin !== window.location.origin) return;
|
|
471
|
+
|
|
472
|
+
const path = url.pathname.replace(/\/$/, '') || '/';
|
|
473
|
+
const adjusted = applyStickyLocaleToPathname(path);
|
|
474
|
+
if (adjusted === path) return;
|
|
475
|
+
|
|
476
|
+
event.preventDefault();
|
|
477
|
+
url.pathname = adjusted;
|
|
478
|
+
|
|
479
|
+
const dest = url.toString();
|
|
480
|
+
if (link.target === '_blank') {
|
|
481
|
+
const features = link.relList?.contains('noopener') || link.relList?.contains('noreferrer')
|
|
482
|
+
? 'noopener,noreferrer'
|
|
483
|
+
: undefined;
|
|
484
|
+
window.open(dest, '_blank', features);
|
|
485
|
+
} else {
|
|
486
|
+
window.location.assign(dest);
|
|
487
|
+
}
|
|
488
|
+
}, true);
|
|
489
|
+
}
|
|
490
|
+
|
|
276
491
|
// Intercept link clicks to prevent page reloads
|
|
277
492
|
function interceptLinkClicks() {
|
|
278
493
|
// Use capture phase to intercept before other handlers
|
|
@@ -356,20 +571,20 @@ function interceptLinkClicks() {
|
|
|
356
571
|
|
|
357
572
|
// Initialize navigation
|
|
358
573
|
function initializeNavigation() {
|
|
359
|
-
// Set initial route (logical path for matching)
|
|
360
574
|
currentRoute = pathnameToLogical(window.location.pathname);
|
|
575
|
+
normalizeRedundantLocalePrefixInUrl();
|
|
361
576
|
|
|
362
|
-
// In prerendered/static output, use default browser navigation (no interception)
|
|
577
|
+
// In prerendered/static output, use default browser navigation (no SPA interception)
|
|
363
578
|
if (!isPrerenderedStaticBuild()) {
|
|
364
|
-
// Intercept link clicks
|
|
365
579
|
interceptLinkClicks();
|
|
366
580
|
|
|
367
|
-
// Listen for popstate events (browser back/forward)
|
|
368
581
|
window.addEventListener('popstate', () => {
|
|
369
582
|
if (!isInternalNavigation) {
|
|
370
583
|
handleRouteChange();
|
|
371
584
|
}
|
|
372
585
|
});
|
|
586
|
+
} else {
|
|
587
|
+
installMpaStickyLocaleLinks();
|
|
373
588
|
}
|
|
374
589
|
|
|
375
590
|
// Handle initial route
|