mnfst 0.5.40 → 0.5.42

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.
@@ -173,6 +173,87 @@ 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
+ // When the URL already has a locale prefix (e.g. /zh/pricing), keep it for same-origin links
232
+ // that omit the prefix (/articles → /zh/articles). No-op on default-locale URLs (/pricing).
233
+ function applyStickyLocaleToPathname(absolutePathname) {
234
+ const codes = getLocalizationCodesFromManifest();
235
+ if (!codes.length) return absolutePathname;
236
+
237
+ const currentSegs = logicalSegmentsFromPathname(window.location.pathname);
238
+ const sticky = currentSegs.length && codes.includes(currentSegs[0]) ? currentSegs[0] : null;
239
+ if (!sticky) return absolutePathname;
240
+
241
+ const targetSegs = logicalSegmentsFromPathname(absolutePathname);
242
+ if (targetSegs.length && codes.includes(targetSegs[0])) {
243
+ return absolutePathname;
244
+ }
245
+ if (shouldSkipStickyLocaleForLogicalSegments(targetSegs)) {
246
+ return absolutePathname;
247
+ }
248
+
249
+ const base = getBasePath();
250
+ const newLogical = targetSegs.length ? `/${sticky}/${targetSegs.join('/')}` : `/${sticky}`;
251
+ const normalizedLogical = newLogical.replace(/\/{2,}/g, '/') || '/';
252
+ if (!base) return normalizedLogical;
253
+ const combined = `${base}${normalizedLogical}`.replace(/([^:])\/{2,}/g, '$1/');
254
+ return combined.startsWith('/') ? combined : `/${combined}`;
255
+ }
256
+
176
257
  function pathnameToLogical(pathname) {
177
258
  const base = getBasePath();
178
259
  if (!base) {
@@ -252,27 +333,79 @@ function resolveHref(href) {
252
333
  const url = new URL(href, baseUrl);
253
334
  if (url.origin !== window.location.origin) return href;
254
335
  let path = url.pathname.replace(/\/$/, '') || '/';
255
- if (!base) return path.startsWith('/') ? path : '/' + path;
256
- if (path === base || path.startsWith(base + '/')) return path;
257
- // path may be above base (e.g. /gadget when base is /src/dist) or unrelated; take only the route part and put under base so we never stack.
258
- if (path.startsWith('/')) {
336
+ let resolved;
337
+ if (!base) {
338
+ resolved = path.startsWith('/') ? path : '/' + path;
339
+ } else if (path === base || path.startsWith(base + '/')) {
340
+ resolved = path;
341
+ } else if (path.startsWith('/')) {
259
342
  const pathSegs = path.split('/').filter(Boolean);
260
343
  const baseSegs = base.split('/').filter(Boolean);
261
344
  let i = 0;
262
345
  while (i < baseSegs.length && i < pathSegs.length && baseSegs[i] === pathSegs[i]) i++;
263
346
  const routeSegs = pathSegs.slice(i);
264
- if (routeSegs.length) return base + '/' + routeSegs.join('/');
347
+ if (routeSegs.length) {
348
+ resolved = base + '/' + routeSegs.join('/');
349
+ } else {
350
+ const out = base + (path.startsWith('/') ? path : '/' + path);
351
+ resolved = out.startsWith('/') ? out : '/' + out;
352
+ }
353
+ } else {
354
+ const out = base + (path.startsWith('/') ? path : '/' + path);
355
+ resolved = out.startsWith('/') ? out : '/' + out;
265
356
  }
266
- const out = base + (path.startsWith('/') ? path : '/' + path);
267
- return out.startsWith('/') ? out : '/' + out;
357
+ return applyStickyLocaleToPathname(resolved);
268
358
  } catch {
269
359
  const base = getBasePath();
270
360
  const safe = (href || '').trim();
271
- if (!safe) return base || '/';
272
- return base ? (base + (safe.startsWith('/') ? safe : '/' + safe)) : (safe.startsWith('/') ? safe : '/' + safe);
361
+ if (!safe) return applyStickyLocaleToPathname(base || '/');
362
+ const raw = base ? (base + (safe.startsWith('/') ? safe : '/' + safe)) : (safe.startsWith('/') ? safe : '/' + safe);
363
+ return applyStickyLocaleToPathname(raw);
273
364
  }
274
365
  }
275
366
 
367
+ // Prerendered MPA: same-origin navigations use full page loads; rewrite targets so locale prefix sticks.
368
+ function installMpaStickyLocaleLinks() {
369
+ document.addEventListener('click', (event) => {
370
+ if (event.defaultPrevented) return;
371
+ if (event.button !== 0) return;
372
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
373
+
374
+ const link = event.target.closest('a');
375
+ if (!link || link.closest('[data-manifest-skip-locale-sticky]')) return;
376
+ if (link.hasAttribute('download')) return;
377
+
378
+ const hrefAttr = link.getAttribute('href');
379
+ if (!hrefAttr || hrefAttr.startsWith('mailto:') || hrefAttr.startsWith('tel:') || hrefAttr.startsWith('javascript:')) return;
380
+ if (hrefAttr.startsWith('#')) return;
381
+
382
+ let url;
383
+ try {
384
+ url = new URL(hrefAttr, window.location.href);
385
+ } catch {
386
+ return;
387
+ }
388
+ if (url.origin !== window.location.origin) return;
389
+
390
+ const path = url.pathname.replace(/\/$/, '') || '/';
391
+ const adjusted = applyStickyLocaleToPathname(path);
392
+ if (adjusted === path) return;
393
+
394
+ event.preventDefault();
395
+ url.pathname = adjusted;
396
+
397
+ const dest = url.toString();
398
+ if (link.target === '_blank') {
399
+ const features = link.relList?.contains('noopener') || link.relList?.contains('noreferrer')
400
+ ? 'noopener,noreferrer'
401
+ : undefined;
402
+ window.open(dest, '_blank', features);
403
+ } else {
404
+ window.location.assign(dest);
405
+ }
406
+ }, true);
407
+ }
408
+
276
409
  // Intercept link clicks to prevent page reloads
277
410
  function interceptLinkClicks() {
278
411
  // Use capture phase to intercept before other handlers
@@ -359,23 +492,27 @@ function initializeNavigation() {
359
492
  // Set initial route (logical path for matching)
360
493
  currentRoute = pathnameToLogical(window.location.pathname);
361
494
 
362
- // In prerendered/static output, use default browser navigation (no interception)
495
+ // In prerendered/static output, use default browser navigation (no SPA interception)
363
496
  if (!isPrerenderedStaticBuild()) {
364
- // Intercept link clicks
365
497
  interceptLinkClicks();
366
498
 
367
- // Listen for popstate events (browser back/forward)
368
499
  window.addEventListener('popstate', () => {
369
500
  if (!isInternalNavigation) {
370
501
  handleRouteChange();
371
502
  }
372
503
  });
504
+ } else {
505
+ installMpaStickyLocaleLinks();
373
506
  }
374
507
 
375
508
  // Handle initial route
376
509
  handleRouteChange();
377
510
  }
378
511
 
512
+ // Match the browser URL as soon as this module loads. Later chunks in the same bundle (e.g. router magic)
513
+ // may initialize before DOMContentLoaded; getCurrentRoute() must not stay at '/' or $route breaks article pages.
514
+ currentRoute = pathnameToLogical(window.location.pathname);
515
+
379
516
  // Run immediately if DOM is ready, otherwise wait
380
517
  if (document.readyState === 'loading') {
381
518
  document.addEventListener('DOMContentLoaded', initializeNavigation);
@@ -1292,6 +1429,8 @@ function initializeRouterMagic() {
1292
1429
  console.error('[Manifest Router Magic] Alpine is not available');
1293
1430
  return;
1294
1431
  }
1432
+ if (window.__manifestRouterMagicInitialized) return;
1433
+ window.__manifestRouterMagicInitialized = true;
1295
1434
 
1296
1435
  // Create a reactive object for route data (use logical path when app is in a subpath)
1297
1436
  const route = Alpine.reactive({
@@ -1336,6 +1475,9 @@ function initializeRouterMagic() {
1336
1475
  window.addEventListener('manifest:route-change', updateRoute);
1337
1476
  window.addEventListener('popstate', updateRoute);
1338
1477
 
1478
+ // Align with navigation + locale stripping; initial reactive value can be wrong if magic ran before DOMContentLoaded.
1479
+ updateRoute();
1480
+
1339
1481
  // Register $route magic property - return the route string directly
1340
1482
  Alpine.magic('route', () => route.current);
1341
1483
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.40",
3
+ "version": "0.5.42",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",