@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.10 → 3.9.0-ultramodern.12

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.
Files changed (66) hide show
  1. package/dist/cjs/localisedUrls/config.js +49 -0
  2. package/dist/cjs/localisedUrls/index.js +95 -0
  3. package/dist/cjs/localisedUrls/normalise.js +87 -0
  4. package/dist/cjs/localisedUrls/pathname.js +68 -0
  5. package/dist/cjs/localisedUrls/patterns.js +214 -0
  6. package/dist/cjs/localisedUrls/redirect.js +100 -0
  7. package/dist/cjs/localisedUrls/resolve.js +78 -0
  8. package/dist/cjs/localisedUrls/routeIdentity.js +92 -0
  9. package/dist/cjs/localisedUrls/routes.js +217 -0
  10. package/dist/cjs/localisedUrls/types.js +18 -0
  11. package/dist/cjs/routerState.js +48 -77
  12. package/dist/cjs/routerStatePlugin.js +36 -9
  13. package/dist/esm/localisedUrls/config.mjs +11 -0
  14. package/dist/esm/localisedUrls/index.mjs +8 -0
  15. package/dist/esm/localisedUrls/normalise.mjs +37 -0
  16. package/dist/esm/localisedUrls/pathname.mjs +24 -0
  17. package/dist/esm/localisedUrls/patterns.mjs +152 -0
  18. package/dist/esm/localisedUrls/redirect.mjs +53 -0
  19. package/dist/esm/localisedUrls/resolve.mjs +37 -0
  20. package/dist/esm/localisedUrls/routeIdentity.mjs +54 -0
  21. package/dist/esm/localisedUrls/routes.mjs +176 -0
  22. package/dist/esm/localisedUrls/types.mjs +0 -0
  23. package/dist/esm/routerState.mjs +49 -75
  24. package/dist/esm/routerStatePlugin.mjs +37 -10
  25. package/dist/esm-node/localisedUrls/config.mjs +12 -0
  26. package/dist/esm-node/localisedUrls/index.mjs +9 -0
  27. package/dist/esm-node/localisedUrls/normalise.mjs +38 -0
  28. package/dist/esm-node/localisedUrls/pathname.mjs +25 -0
  29. package/dist/esm-node/localisedUrls/patterns.mjs +153 -0
  30. package/dist/esm-node/localisedUrls/redirect.mjs +54 -0
  31. package/dist/esm-node/localisedUrls/resolve.mjs +38 -0
  32. package/dist/esm-node/localisedUrls/routeIdentity.mjs +55 -0
  33. package/dist/esm-node/localisedUrls/routes.mjs +177 -0
  34. package/dist/esm-node/localisedUrls/types.mjs +1 -0
  35. package/dist/esm-node/routerState.mjs +49 -75
  36. package/dist/esm-node/routerStatePlugin.mjs +37 -10
  37. package/dist/types/localisedUrls/config.d.ts +9 -0
  38. package/dist/types/localisedUrls/index.d.ts +10 -0
  39. package/dist/types/localisedUrls/normalise.d.ts +10 -0
  40. package/dist/types/localisedUrls/pathname.d.ts +4 -0
  41. package/dist/types/localisedUrls/patterns.d.ts +11 -0
  42. package/dist/types/localisedUrls/redirect.d.ts +19 -0
  43. package/dist/types/localisedUrls/resolve.d.ts +7 -0
  44. package/dist/types/localisedUrls/routeIdentity.d.ts +22 -0
  45. package/dist/types/localisedUrls/routes.d.ts +3 -0
  46. package/dist/types/localisedUrls/types.d.ts +14 -0
  47. package/dist/types/routeHydration.d.ts +2 -2
  48. package/dist/types/routerState.d.ts +5 -6
  49. package/dist/types/routerStateTypes.d.ts +47 -17
  50. package/package.json +16 -4
  51. package/src/localisedUrls/config.ts +18 -0
  52. package/src/localisedUrls/index.ts +25 -0
  53. package/src/localisedUrls/normalise.ts +62 -0
  54. package/src/localisedUrls/pathname.ts +60 -0
  55. package/src/localisedUrls/patterns.ts +283 -0
  56. package/src/localisedUrls/redirect.ts +107 -0
  57. package/src/localisedUrls/resolve.ts +106 -0
  58. package/src/localisedUrls/routeIdentity.ts +83 -0
  59. package/src/localisedUrls/routes.ts +451 -0
  60. package/src/localisedUrls/types.ts +18 -0
  61. package/src/routeHydration.ts +2 -2
  62. package/src/routerState.ts +52 -122
  63. package/src/routerStatePlugin.ts +49 -10
  64. package/src/routerStateTypes.ts +47 -17
  65. package/tests/localisedUrls.test.ts +560 -0
  66. package/tests/routerState.test.ts +7 -8
@@ -0,0 +1,177 @@
1
+ import "node:module";
2
+ import { getLeadingLocaleParam, normalisePathPattern, normaliseRoutePath, stripLeadingLocaleParam } from "./normalise.mjs";
3
+ import { doPathPatternsOverlap, getOrderedPathParamSignature, getPathParamSignature, getPathPatternValidationIssue, getPhysicalPathPattern, haveEqualPatternSpecificity } from "./patterns.mjs";
4
+ const isLocalisableRoutePath = (path)=>{
5
+ const pathWithoutLocale = stripLeadingLocaleParam(path);
6
+ if (!pathWithoutLocale || '/' === pathWithoutLocale || '*' === pathWithoutLocale) return false;
7
+ return true;
8
+ };
9
+ const joinPath = (parentPath, routePath)=>{
10
+ if (!isLocalisableRoutePath(routePath)) return parentPath;
11
+ const segment = normaliseRoutePath(stripLeadingLocaleParam(routePath) || '');
12
+ return normalisePathPattern(`${parentPath}/${segment}`);
13
+ };
14
+ const isFrameworkInternalRoutePath = (canonicalPath)=>'/_mf' === canonicalPath || canonicalPath.startsWith('/_mf/');
15
+ const legalRouteIdPart = (value)=>value.replace(/[^a-zA-Z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'index';
16
+ const formatParamSignature = (signature)=>signature.length > 0 ? signature.join(', ') : '(none)';
17
+ const validateLocalisedUrlPatterns = (languages, localisedUrls)=>{
18
+ const physicalPatterns = new Map();
19
+ const claimedPatterns = [];
20
+ const claimPhysicalPattern = (sourcePattern, canonicalPath, source)=>{
21
+ const physicalPattern = getPhysicalPathPattern(sourcePattern);
22
+ const existing = physicalPatterns.get(physicalPattern);
23
+ if (existing && existing.canonicalPath !== canonicalPath) throw new Error(`localisedUrls["${existing.canonicalPath}"] ${existing.source} and localisedUrls["${canonicalPath}"] ${source} generate the same physical route pattern "${physicalPattern}". Localised route patterns must identify exactly one canonical route.`);
24
+ const overlapping = claimedPatterns.find((claimed)=>claimed.canonicalPath !== canonicalPath && haveEqualPatternSpecificity(claimed.pattern, sourcePattern) && doPathPatternsOverlap(claimed.pattern, sourcePattern));
25
+ if (overlapping) throw new Error(`localisedUrls["${overlapping.canonicalPath}"] ${overlapping.source} pattern "${overlapping.pattern}" and localisedUrls["${canonicalPath}"] ${source} pattern "${sourcePattern}" are equal-specificity overlapping route patterns. Localised route patterns must identify exactly one canonical route.`);
26
+ physicalPatterns.set(physicalPattern, {
27
+ canonicalPath,
28
+ source
29
+ });
30
+ claimedPatterns.push({
31
+ canonicalPath,
32
+ pattern: sourcePattern,
33
+ source
34
+ });
35
+ };
36
+ for (const [canonicalPath, entry] of Object.entries(localisedUrls).sort(([left], [right])=>left < right ? -1 : left > right ? 1 : 0)){
37
+ const canonicalIssue = getPathPatternValidationIssue(canonicalPath);
38
+ if (canonicalIssue) throw new Error(`localisedUrls["${canonicalPath}"] canonical pattern has ${canonicalIssue}.`);
39
+ const expectedSignature = getPathParamSignature(canonicalPath);
40
+ const expectedOrderedSignature = getOrderedPathParamSignature(canonicalPath);
41
+ claimPhysicalPattern(canonicalPath, canonicalPath, 'canonical pattern');
42
+ for (const language of languages){
43
+ const localisedPattern = entry[language];
44
+ if (!localisedPattern) continue;
45
+ const localisedIssue = getPathPatternValidationIssue(localisedPattern);
46
+ if (localisedIssue) throw new Error(`localisedUrls["${canonicalPath}"].${language} has ${localisedIssue}.`);
47
+ const actualSignature = getPathParamSignature(localisedPattern);
48
+ if (actualSignature.length !== expectedSignature.length || actualSignature.some((parameter, index)=>parameter !== expectedSignature[index])) throw new Error(`localisedUrls["${canonicalPath}"].${language} must use the same parameter signature as "${canonicalPath}": expected ${formatParamSignature(expectedSignature)}; received ${formatParamSignature(actualSignature)}.`);
49
+ const actualOrderedSignature = getOrderedPathParamSignature(localisedPattern);
50
+ if (expectedOrderedSignature.some((parameter)=>'*' === parameter || parameter.endsWith('?')) && actualOrderedSignature.some((parameter, index)=>parameter !== expectedOrderedSignature[index])) throw new Error(`localisedUrls["${canonicalPath}"].${language} must keep optional and splat parameters in canonical order: expected ${formatParamSignature(expectedOrderedSignature)}; received ${formatParamSignature(actualOrderedSignature)}.`);
51
+ claimPhysicalPattern(localisedPattern, canonicalPath, `.${language}`);
52
+ }
53
+ }
54
+ };
55
+ const ensureLocalisedUrlsForPath = (canonicalPath, languages, localisedUrls)=>{
56
+ const entry = localisedUrls[canonicalPath];
57
+ if (!entry) throw new Error(`localisedUrls is enabled, but route "${canonicalPath}" does not define localised URLs for languages: ${languages.join(', ')}. Add localisedUrls["${canonicalPath}"] or set localeDetection.localisedUrls to false.`);
58
+ const missingLanguages = languages.filter((language)=>!entry[language]);
59
+ if (missingLanguages.length > 0) throw new Error(`localisedUrls["${canonicalPath}"] is missing languages: ${missingLanguages.join(', ')}. Every configured language must have a localised URL.`);
60
+ return entry;
61
+ };
62
+ const validateLocalisedUrls = (routes, languages, localisedUrls)=>{
63
+ validateLocalisedUrlPatterns(languages, localisedUrls);
64
+ const visit = (route, parentPath)=>{
65
+ const canonicalPath = joinPath(parentPath, route.path);
66
+ if (isLocalisableRoutePath(route.path) && !isFrameworkInternalRoutePath(canonicalPath)) ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls);
67
+ if ('children' in route && route.children) route.children.forEach((child)=>visit(child, canonicalPath));
68
+ };
69
+ routes.forEach((route)=>visit(route, ''));
70
+ };
71
+ const getLocalisedRouteIdSuffixes = (paths)=>{
72
+ const usedSuffixes = new Set();
73
+ return paths.map((path, index)=>{
74
+ if (0 === index) return;
75
+ const baseSuffix = legalRouteIdPart(path);
76
+ let suffix = baseSuffix;
77
+ let disambiguator = 2;
78
+ while(usedSuffixes.has(suffix))suffix = `${baseSuffix}_${disambiguator++}`;
79
+ usedSuffixes.add(suffix);
80
+ return suffix;
81
+ });
82
+ };
83
+ const getLocalisedRoutePaths = (canonicalPath, parentLocalisedPaths, languages, entry)=>{
84
+ const paths = languages.map((language)=>{
85
+ const fullPath = normalisePathPattern(entry[language]);
86
+ const parentPath = normalisePathPattern(parentLocalisedPaths[language] || '/');
87
+ if ('/' === parentPath) return normaliseRoutePath(fullPath) || void 0;
88
+ const parentPrefix = `${parentPath}/`;
89
+ if (!fullPath.startsWith(parentPrefix)) throw new Error(`localisedUrls["${canonicalPath}"].${language} must be nested under "${parentPath}" because its parent route is localised there.`);
90
+ return normaliseRoutePath(fullPath.slice(parentPath.length));
91
+ });
92
+ const uniquePaths = Array.from(new Set(paths.filter(Boolean)));
93
+ return uniquePaths;
94
+ };
95
+ const transformLocalisedRoute = (route, parentCanonicalPath, parentLocalisedPaths, languages, localisedUrls, projection)=>{
96
+ const isSplat = '*' === stripLeadingLocaleParam(route.path);
97
+ const canonicalPath = isSplat ? normalisePathPattern(`${parentCanonicalPath}/*`) : joinPath(parentCanonicalPath, route.path);
98
+ const localisedUrlEntry = isLocalisableRoutePath(route.path) && !isFrameworkInternalRoutePath(canonicalPath) ? ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls) : void 0;
99
+ const routeLocalisedPaths = localisedUrlEntry ? languages.reduce((acc, language)=>{
100
+ acc[language] = normalisePathPattern(localisedUrlEntry[language]);
101
+ return acc;
102
+ }, {}) : isSplat ? Object.fromEntries(Object.entries(parentLocalisedPaths).map(([language, path])=>[
103
+ language,
104
+ normalisePathPattern(`${path}/*`)
105
+ ])) : parentLocalisedPaths;
106
+ const children = 'children' in route && route.children ? route.children.flatMap((child)=>transformLocalisedRoute(child, canonicalPath, routeLocalisedPaths, languages, localisedUrls, projection)) : void 0;
107
+ const baseRoute = {
108
+ ...route,
109
+ ...!isFrameworkInternalRoutePath(canonicalPath) ? {
110
+ modernLocalisedRoute: {
111
+ id: route.id,
112
+ path: route.path,
113
+ canonicalPath,
114
+ paths: localisedUrlEntry ?? (isSplat ? routeLocalisedPaths : void 0)
115
+ }
116
+ } : {},
117
+ ...children ? {
118
+ children
119
+ } : {}
120
+ };
121
+ if (!localisedUrlEntry) return [
122
+ baseRoute
123
+ ];
124
+ const localisedPaths = getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry);
125
+ if ('canonical' === projection) return [
126
+ baseRoute
127
+ ];
128
+ const leadingLocaleParam = getLeadingLocaleParam(route.path);
129
+ const routeIdPaths = leadingLocaleParam ? localisedPaths.map((path)=>normaliseRoutePath(`${leadingLocaleParam}/${path}`)) : localisedPaths;
130
+ const routeIdSuffixes = getLocalisedRouteIdSuffixes(routeIdPaths);
131
+ return localisedPaths.map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, routeIdSuffixes[index], canonicalPath));
132
+ };
133
+ const suffixRouteIds = (route, suffix)=>{
134
+ const children = 'children' in route && route.children ? route.children.map((child)=>suffixRouteIds(child, suffix)) : void 0;
135
+ return {
136
+ ...route,
137
+ ...route.id ? {
138
+ id: `${route.id}__localised_${suffix}`
139
+ } : {},
140
+ ...children ? {
141
+ children
142
+ } : {}
143
+ };
144
+ };
145
+ const collectRouteIdCounts = (routes, counts = new Map())=>{
146
+ for (const route of routes){
147
+ if (route.id) counts.set(route.id, (counts.get(route.id) ?? 0) + 1);
148
+ if ('children' in route && route.children) collectRouteIdCounts(route.children, counts);
149
+ }
150
+ return counts;
151
+ };
152
+ const validateNoNewRouteIdCollisions = (sourceRoutes, localisedRoutes)=>{
153
+ const sourceCounts = collectRouteIdCounts(sourceRoutes);
154
+ const localisedCounts = collectRouteIdCounts(localisedRoutes);
155
+ for (const [routeId, count] of localisedCounts)if (count > 1 && count > (sourceCounts.get(routeId) ?? 0)) throw new Error(`Localised route expansion generated duplicate route ID "${routeId}". Change the source route ID or localised path so every generated route remains identifiable.`);
156
+ };
157
+ const cloneRouteWithLocalisedPath = (route, path, routeIdSuffix, canonicalPath)=>{
158
+ const leadingLocaleParam = getLeadingLocaleParam(route.path);
159
+ const localisedPath = leadingLocaleParam ? normaliseRoutePath(`${leadingLocaleParam}/${path}`) : path;
160
+ const routeWithPath = {
161
+ ...route,
162
+ path: localisedPath
163
+ };
164
+ routeWithPath.modernCanonicalPath = canonicalPath;
165
+ return routeIdSuffix ? suffixRouteIds(routeWithPath, routeIdSuffix) : routeWithPath;
166
+ };
167
+ const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls, projection = 'physical')=>{
168
+ const rootLocalisedPaths = languages.reduce((acc, language)=>{
169
+ acc[language] = '/';
170
+ return acc;
171
+ }, {});
172
+ validateLocalisedUrls(routes, languages, localisedUrls);
173
+ const localisedRoutes = routes.flatMap((route)=>transformLocalisedRoute(route, '', rootLocalisedPaths, languages, localisedUrls, projection));
174
+ validateNoNewRouteIdCollisions(routes, localisedRoutes);
175
+ return localisedRoutes;
176
+ };
177
+ export { applyLocalisedUrlsToRoutes, validateLocalisedUrls };
@@ -0,0 +1 @@
1
+ import "node:module";
@@ -8,66 +8,52 @@ function getRouterRuntimeState(runtimeContext) {
8
8
  function getRouterServerSnapshot(runtimeContext) {
9
9
  return routerServerSnapshotExtension.get(runtimeContext);
10
10
  }
11
- function toHydrationScripts(state) {
12
- if (state.hydrationScripts?.length) return state.hydrationScripts;
13
- return state.hydrationScript ? [
14
- state.hydrationScript
15
- ] : void 0;
16
- }
17
- function getMatchedRouteIdsFromMatches(matches) {
18
- const routeIds = matches?.map((match)=>match.assetRouteId ?? match.routeId).filter((routeId)=>'string' == typeof routeId);
19
- return routeIds?.length ? routeIds : void 0;
20
- }
21
- function createRouterServerSnapshot(state) {
22
- const hydrationScripts = toHydrationScripts(state);
23
- const matchedRouteIds = state.matchedRouteIds ?? getMatchedRouteIdsFromMatches(state.matches);
24
- return {
25
- ...state,
26
- ...hydrationScripts?.length ? {
27
- hydrationScript: state.hydrationScript ?? hydrationScripts[0],
28
- hydrationScripts
11
+ function createRouterServerSnapshot(snapshot) {
12
+ const matches = snapshot.matches?.map((match)=>Object.freeze({
13
+ ...match,
14
+ ...match.params ? {
15
+ params: Object.freeze({
16
+ ...match.params
17
+ })
18
+ } : {}
19
+ }));
20
+ const matchedRouteIds = snapshot.matchedRouteIds ?? matches?.map((match)=>match.assetRouteId ?? match.routeId);
21
+ return Object.freeze({
22
+ ...snapshot,
23
+ ...snapshot.hydrationScripts ? {
24
+ hydrationScripts: Object.freeze([
25
+ ...snapshot.hydrationScripts
26
+ ])
29
27
  } : {},
30
28
  ...matchedRouteIds ? {
31
- matchedRouteIds
32
- } : {}
33
- };
34
- }
35
- function createRouterRuntimeState(state) {
36
- const hasSnapshotState = Boolean(state.serverSnapshot) || Boolean(state.hydrationScript) || Boolean(state.hydrationScripts?.length) || Boolean(state.matchedRouteIds?.length) || Boolean(state.matches?.length);
37
- const serverSnapshot = state.serverSnapshot ? createRouterServerSnapshot({
38
- ...state.serverSnapshot,
39
- framework: state.serverSnapshot.framework ?? state.framework,
40
- basename: state.serverSnapshot.basename ?? state.basename,
41
- hydrationScript: state.serverSnapshot.hydrationScript ?? state.hydrationScript,
42
- hydrationScripts: state.serverSnapshot.hydrationScripts ?? state.hydrationScripts,
43
- matchedRouteIds: state.serverSnapshot.matchedRouteIds ?? state.matchedRouteIds,
44
- matches: state.serverSnapshot.matches ?? state.matches
45
- }) : hasSnapshotState ? createRouterServerSnapshot({
46
- framework: state.framework,
47
- basename: state.basename,
48
- hydrationScript: state.hydrationScript,
49
- hydrationScripts: state.hydrationScripts,
50
- matchedRouteIds: state.matchedRouteIds,
51
- matches: state.matches
52
- }) : void 0;
53
- const hydrationScripts = toHydrationScripts({
54
- hydrationScript: state.hydrationScript ?? serverSnapshot?.hydrationScript,
55
- hydrationScripts: state.hydrationScripts ?? serverSnapshot?.hydrationScripts
56
- });
57
- const matchedRouteIds = state.matchedRouteIds ?? serverSnapshot?.matchedRouteIds ?? getMatchedRouteIdsFromMatches(state.matches);
58
- return {
59
- ...state,
60
- ...hydrationScripts?.length ? {
61
- hydrationScript: state.hydrationScript ?? hydrationScripts[0],
62
- hydrationScripts
29
+ matchedRouteIds: Object.freeze([
30
+ ...matchedRouteIds
31
+ ])
63
32
  } : {},
64
- ...matchedRouteIds ? {
65
- matchedRouteIds
33
+ ...matches ? {
34
+ matches: Object.freeze(matches)
35
+ } : {},
36
+ ...snapshot.errors ? {
37
+ errors: Object.freeze({
38
+ ...snapshot.errors
39
+ })
66
40
  } : {},
67
- ...serverSnapshot ? {
68
- serverSnapshot
41
+ ...snapshot.routerData ? {
42
+ routerData: Object.freeze({
43
+ ...snapshot.routerData,
44
+ ...snapshot.routerData.loaderData ? {
45
+ loaderData: Object.freeze({
46
+ ...snapshot.routerData.loaderData
47
+ })
48
+ } : {},
49
+ ...snapshot.routerData.errors ? {
50
+ errors: Object.freeze({
51
+ ...snapshot.routerData.errors
52
+ })
53
+ } : {}
54
+ })
69
55
  } : {}
70
- };
56
+ });
71
57
  }
72
58
  const routerRuntimeStateListeners = new WeakMap();
73
59
  function subscribeRouterRuntimeState(runtimeContext, listener) {
@@ -103,38 +89,26 @@ function notifyRouterRuntimeState(runtimeContext) {
103
89
  }
104
90
  function applyRouterRuntimeState(runtimeContext, state) {
105
91
  const previous = routerRuntimeStateExtension.get(runtimeContext);
106
- const normalized = createRouterRuntimeState(state);
107
- routerRuntimeStateExtension.set(runtimeContext, normalized);
108
- if (normalized.serverSnapshot) routerServerSnapshotExtension.set(runtimeContext, normalized.serverSnapshot);
109
- if (previous?.instance !== normalized.instance || previous?.framework !== normalized.framework) notifyRouterRuntimeState(runtimeContext);
92
+ routerRuntimeStateExtension.set(runtimeContext, state);
93
+ if (previous?.instance !== state.instance || previous?.framework !== state.framework || previous?.navigation !== state.navigation) notifyRouterRuntimeState(runtimeContext);
110
94
  return runtimeContext;
111
95
  }
112
96
  function applyRouterServerPrepareResult(runtimeContext, result) {
113
- const state = createRouterRuntimeState({
97
+ if (result.snapshot) routerServerSnapshotExtension.set(runtimeContext, createRouterServerSnapshot(result.snapshot));
98
+ return applyRouterRuntimeState(runtimeContext, {
114
99
  ...result.state,
115
- cleanup: result.cleanup ?? result.state.cleanup,
116
- serverSnapshot: result.snapshot ?? result.state.serverSnapshot
100
+ cleanup: result.cleanup ?? result.state.cleanup
117
101
  });
118
- applyRouterRuntimeState(runtimeContext, state);
119
- return runtimeContext;
120
102
  }
121
103
  function getRouterHydrationScripts(runtimeContext) {
122
- const serverSnapshot = getRouterServerSnapshot(runtimeContext);
123
- const runtimeState = getRouterRuntimeState(runtimeContext);
124
- return serverSnapshot?.hydrationScripts ?? toHydrationScripts({
125
- hydrationScript: serverSnapshot?.hydrationScript
126
- }) ?? runtimeState?.hydrationScripts ?? toHydrationScripts({
127
- hydrationScript: runtimeState?.hydrationScript
128
- }) ?? [];
104
+ return getRouterServerSnapshot(runtimeContext)?.hydrationScripts ?? [];
129
105
  }
130
106
  function getRouterMatchedRouteIds(runtimeContext) {
131
- const serverSnapshot = getRouterServerSnapshot(runtimeContext);
132
- const runtimeState = getRouterRuntimeState(runtimeContext);
133
- return serverSnapshot?.matchedRouteIds ?? getMatchedRouteIdsFromMatches(serverSnapshot?.matches) ?? runtimeState?.matchedRouteIds ?? getMatchedRouteIdsFromMatches(runtimeState?.matches);
107
+ return getRouterServerSnapshot(runtimeContext)?.matchedRouteIds;
134
108
  }
135
109
  async function cleanupRouterRuntimeState(runtimeContext) {
136
110
  try {
137
111
  await getRouterRuntimeState(runtimeContext)?.cleanup?.();
138
112
  } catch {}
139
113
  }
140
- export { applyRouterRuntimeState, applyRouterServerPrepareResult, cleanupRouterRuntimeState, createRouterRuntimeState, createRouterServerSnapshot, getRouterHydrationScripts, getRouterMatchedRouteIds, getRouterRuntimeState, getRouterServerSnapshot, subscribeRouterRuntimeState };
114
+ export { applyRouterRuntimeState, applyRouterServerPrepareResult, cleanupRouterRuntimeState, createRouterServerSnapshot, getRouterHydrationScripts, getRouterMatchedRouteIds, getRouterRuntimeState, getRouterServerSnapshot, subscribeRouterRuntimeState };
@@ -1,6 +1,25 @@
1
1
  import "node:module";
2
2
  import { createRouterPrefetchPolicy } from "./routerPrefetchPolicy.mjs";
3
- import { applyRouterRuntimeState, createRouterServerSnapshot } from "./routerState.mjs";
3
+ import { applyRouterRuntimeState, applyRouterServerPrepareResult } from "./routerState.mjs";
4
+ function createReactRouterNavigation(router, Link) {
5
+ let state;
6
+ let snapshot;
7
+ return {
8
+ Link,
9
+ getSnapshot () {
10
+ if (state !== router.state) {
11
+ state = router.state;
12
+ snapshot = {
13
+ location: state.location,
14
+ params: Object.assign({}, ...state.matches.map((match)=>match.params))
15
+ };
16
+ }
17
+ return snapshot;
18
+ },
19
+ subscribe: (listener)=>router.subscribe(listener),
20
+ navigate: (href, options)=>router.navigate(href, options)
21
+ };
22
+ }
4
23
  function createRouterStatePlugin({ registryHooks }) {
5
24
  return {
6
25
  name: '@modern-js/router-runtime-policy',
@@ -15,7 +34,7 @@ function createRouterStatePlugin({ registryHooks }) {
15
34
  if ('ssr-prepare' === event.phase) {
16
35
  const { routerContext } = event.runtimeContext;
17
36
  if (!routerContext) return;
18
- const serverSnapshot = createRouterServerSnapshot({
37
+ const snapshot = {
19
38
  framework: event.framework,
20
39
  basename: event.basename,
21
40
  statusCode: routerContext.statusCode,
@@ -29,19 +48,27 @@ function createRouterStatePlugin({ registryHooks }) {
29
48
  routeId: match.route.id
30
49
  }
31
50
  ] : [])
51
+ };
52
+ applyRouterServerPrepareResult(event.runtimeContext, {
53
+ snapshot,
54
+ state: {
55
+ framework: event.framework,
56
+ basename: event.basename,
57
+ instance: event.router
58
+ }
32
59
  });
60
+ } else if ('client-create' === event.phase) {
61
+ const router = event.router;
62
+ const { router: api } = event.runtimeContext;
33
63
  applyRouterRuntimeState(event.runtimeContext, {
34
64
  framework: event.framework,
35
65
  basename: event.basename,
36
- instance: event.router,
37
- matchedRouteIds: serverSnapshot.matchedRouteIds,
38
- serverSnapshot
66
+ instance: router,
67
+ ...api?.Link ? {
68
+ navigation: createReactRouterNavigation(router, api.Link)
69
+ } : {}
39
70
  });
40
- } else if ('client-create' === event.phase) applyRouterRuntimeState(event.runtimeContext, {
41
- framework: event.framework,
42
- basename: event.basename,
43
- instance: event.router
44
- });
71
+ }
45
72
  });
46
73
  }
47
74
  };
@@ -0,0 +1,9 @@
1
+ import type { LocalisedUrlsOption, ResolvedLocalisedUrlsConfig } from './types.js';
2
+ /**
3
+ * Localised URLs are strictly opt-in: only an explicit, non-empty map enables
4
+ * route expansion and validation. `true`, `false`, an empty map and absence
5
+ * all resolve to disabled, so upstream-style configs (`localePathRedirect` +
6
+ * `languages` without a map) keep plain locale-prefix behavior instead of
7
+ * failing the build for every route missing from a map they never wrote.
8
+ */
9
+ export declare const resolveLocalisedUrlsConfig: (option: LocalisedUrlsOption | undefined) => ResolvedLocalisedUrlsConfig;
@@ -0,0 +1,10 @@
1
+ export { resolveLocalisedUrlsConfig } from './config.js';
2
+ export { normalisePathname, normalisePathPattern } from './normalise.js';
3
+ export { canonicalTargetPathname, localiseTargetPathname, stripLanguagePrefix, } from './pathname.js';
4
+ export { buildPathFromPattern, matchPathPattern } from './patterns.js';
5
+ export type { LocaleRedirectSkipRule } from './redirect.js';
6
+ export { DEFAULT_LOCALE_REDIRECT_SKIP_RULES, isDefaultLocaleRedirectSkipPath, matchesPathPrefix, shouldSkipLocaleRedirect, } from './redirect.js';
7
+ export { resolveCanonicalLocalisedPath, resolveLocalisedPath } from './resolve.js';
8
+ export { createLocalisedRouteRewrite } from './routeIdentity.js';
9
+ export { applyLocalisedUrlsToRoutes, validateLocalisedUrls } from './routes.js';
10
+ export type { LocalisedRoute, LocalisedUrlPathMap, LocalisedUrlsMap, LocalisedUrlsOption, ResolvedLocalisedUrlsConfig, } from './types.js';
@@ -0,0 +1,10 @@
1
+ export declare const normalisePathPattern: (path: string) => string;
2
+ /**
3
+ * Normalise a concrete request pathname: slash cleanup only. Unlike
4
+ * {@link normalisePathPattern} it must not rewrite literal `[x]` segments to
5
+ * `:x` params — pathnames are values, not patterns.
6
+ */
7
+ export declare const normalisePathname: (pathname: string) => string;
8
+ export declare const normaliseRoutePath: (path: string) => string;
9
+ export declare const stripLeadingLocaleParam: (path?: string) => string | undefined;
10
+ export declare const getLeadingLocaleParam: (path?: string) => string | null;
@@ -0,0 +1,4 @@
1
+ import type { LocalisedUrlsOption } from './types.js';
2
+ export declare const stripLanguagePrefix: (pathname: string, languages: string[]) => string;
3
+ export declare const localiseTargetPathname: (pathname: string, language: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
4
+ export declare const canonicalTargetPathname: (pathname: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
@@ -0,0 +1,11 @@
1
+ export declare const getOrderedPathParamSignature: (pattern: string) => string[];
2
+ export declare const getPathParamSignature: (pattern: string) => string[];
3
+ export declare const getPathPatternValidationIssue: (pattern: string) => string | undefined;
4
+ export declare const getPhysicalPathPattern: (pattern: string) => string;
5
+ export declare const haveEqualPatternSpecificity: (left: string, right: string) => boolean;
6
+ export declare const doPathPatternsOverlap: (left: string, right: string) => boolean;
7
+ export declare const sortPatternsBySpecificity: <T extends {
8
+ pattern: string;
9
+ }>(patterns: T[]) => T[];
10
+ export declare const matchPathPattern: (pathname: string, pattern: string) => Record<string, string> | null;
11
+ export declare const buildPathFromPattern: (pattern: string, params: Record<string, string>) => string;
@@ -0,0 +1,19 @@
1
+ export type LocaleRedirectSkipRule = {
2
+ type: 'exact';
3
+ path: string;
4
+ } | {
5
+ type: 'prefix';
6
+ path: string;
7
+ } | {
8
+ type: 'pattern';
9
+ path: RegExp;
10
+ };
11
+ /**
12
+ * Federation artifacts are never pages. The remote entries match the same
13
+ * shapes the federation layer itself recognises and serves (`mfCache.ts`):
14
+ * a manifest may name a root entry such as `remoteEntry.catalog.js`.
15
+ */
16
+ export declare const DEFAULT_LOCALE_REDIRECT_SKIP_RULES: readonly LocaleRedirectSkipRule[];
17
+ export declare const matchesPathPrefix: (pathname: string, prefix: string) => boolean;
18
+ export declare const isDefaultLocaleRedirectSkipPath: (pathname: string, languages?: string[]) => boolean;
19
+ export declare const shouldSkipLocaleRedirect: (pathname: string, languages?: string[], ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)) => boolean;
@@ -0,0 +1,7 @@
1
+ import type { LocalisedUrlsMap } from './types.js';
2
+ export declare const resolveLocalisedPath: (pathname: string, targetLanguage: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
3
+ /**
4
+ * Reverse-map language-specific pathnames back to language-agnostic
5
+ * LocalisedUrlsMap keys.
6
+ */
7
+ export declare const resolveCanonicalLocalisedPath: (pathname: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
@@ -0,0 +1,22 @@
1
+ interface RouteIdentity {
2
+ id?: string;
3
+ path?: string;
4
+ canonicalPath: string;
5
+ paths?: Record<string, string>;
6
+ }
7
+ interface RouteNode {
8
+ id?: string;
9
+ path?: string;
10
+ children?: RouteNode[];
11
+ modernLocalisedRoute?: RouteIdentity;
12
+ }
13
+ /** Use the router's URL rewrite seam; browser URLs retain their locale spelling. */
14
+ export declare function createLocalisedRouteRewrite(routes: RouteNode[]): {
15
+ input: ({ url }: {
16
+ url: URL;
17
+ }) => URL;
18
+ output: ({ url }: {
19
+ url: URL;
20
+ }) => URL;
21
+ } | undefined;
22
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { LocalisedRoute, LocalisedUrlsMap } from './types.js';
2
+ export declare const validateLocalisedUrls: (routes: LocalisedRoute[], languages: string[], localisedUrls: LocalisedUrlsMap) => void;
3
+ export declare const applyLocalisedUrlsToRoutes: (routes: LocalisedRoute[], languages: string[], localisedUrls: LocalisedUrlsMap, projection?: 'physical' | 'canonical') => LocalisedRoute[];
@@ -0,0 +1,14 @@
1
+ export type LocalisedRoute = {
2
+ type: 'nested' | 'page';
3
+ path?: string;
4
+ id?: string;
5
+ children?: LocalisedRoute[];
6
+ [key: string]: any;
7
+ };
8
+ export type LocalisedUrlPathMap = Record<string, string>;
9
+ export type LocalisedUrlsMap = Record<string, LocalisedUrlPathMap>;
10
+ export type LocalisedUrlsOption = boolean | LocalisedUrlsMap;
11
+ export interface ResolvedLocalisedUrlsConfig {
12
+ enabled: boolean;
13
+ map: LocalisedUrlsMap;
14
+ }
@@ -8,7 +8,7 @@ export type ScriptChunkLike = {
8
8
  filename?: string;
9
9
  url?: string;
10
10
  };
11
- export declare function getMatchedRouteChunks<T>(routeManifest: RouteManifestLike | undefined, matchedRouteIds: string[], routeAssetToChunk: (asset: string) => T): T[];
11
+ export declare function getMatchedRouteChunks<T>(routeManifest: RouteManifestLike | undefined, matchedRouteIds: readonly string[], routeAssetToChunk: (asset: string) => T): T[];
12
12
  export declare const orderHydrationScriptChunks: <T extends ScriptChunkLike>({ asyncEntryChunks, collectedChunks, matchedRouteChunks, entryName, }: {
13
13
  asyncEntryChunks: T[];
14
14
  collectedChunks: T[];
@@ -17,7 +17,7 @@ export declare const orderHydrationScriptChunks: <T extends ScriptChunkLike>({ a
17
17
  }) => T[];
18
18
  export declare function injectBeforeHydrationEntryScript(template: string, scripts: string, entryName?: string): string;
19
19
  export declare function replaceChunkJsPlaceholder(template: string, scripts: string, entryName: string | undefined, placeholder: string): string;
20
- export declare function createRouteHydrationScriptTags(routeManifest: RouteManifestLike | undefined, matchedRouteIds: string[], entryName: string, options?: {
20
+ export declare function createRouteHydrationScriptTags(routeManifest: RouteManifestLike | undefined, matchedRouteIds: readonly string[], entryName: string, options?: {
21
21
  nonce?: string;
22
22
  template?: string;
23
23
  }): string;
@@ -1,10 +1,9 @@
1
1
  import type { InternalRouterRuntimeState, InternalRouterServerSnapshot, RouterServerPrepareResult } from './routerStateTypes.js';
2
- export type { BuiltInRouterFramework, InternalRouterRuntimeState, InternalRouterServerSnapshot, RouterFramework, RouterLifecyclePhase, RouterRouteMatchSnapshot, RouterServerPrepareResult, } from './routerStateTypes.js';
2
+ export type { BuiltInRouterFramework, InternalRouterRuntimeState, InternalRouterServerSnapshot, RouterFramework, RouterLifecyclePhase, RouterLinkTarget, RouterNavigationCapability, RouterNavigationSnapshot, RouterRouteMatchSnapshot, RouterServerPrepareResult, } from './routerStateTypes.js';
3
3
  export declare function getRouterRuntimeState(runtimeContext: object): InternalRouterRuntimeState | undefined;
4
4
  export declare function getRouterServerSnapshot(runtimeContext: object): InternalRouterServerSnapshot | undefined;
5
- type RouterSnapshotLike = Partial<InternalRouterServerSnapshot>;
6
- export declare function createRouterServerSnapshot(state: RouterSnapshotLike): InternalRouterServerSnapshot;
7
- export declare function createRouterRuntimeState(state: InternalRouterRuntimeState): InternalRouterRuntimeState;
5
+ /** Capture prepared metadata once; live router updates cannot rewrite it. */
6
+ export declare function createRouterServerSnapshot(snapshot: InternalRouterServerSnapshot): InternalRouterServerSnapshot;
8
7
  /**
9
8
  * Observe the router runtime-state slot for the given runtime context.
10
9
  * Returns an unsubscribe function; safe to call for a context that never
@@ -13,6 +12,6 @@ export declare function createRouterRuntimeState(state: InternalRouterRuntimeSta
13
12
  export declare function subscribeRouterRuntimeState(runtimeContext: object, listener: () => void): () => void;
14
13
  export declare function applyRouterRuntimeState<Context extends object>(runtimeContext: Context, state: InternalRouterRuntimeState): Context;
15
14
  export declare function applyRouterServerPrepareResult<Context extends object>(runtimeContext: Context, result: RouterServerPrepareResult): Context;
16
- export declare function getRouterHydrationScripts(runtimeContext: object): string[];
17
- export declare function getRouterMatchedRouteIds(runtimeContext: object): string[] | undefined;
15
+ export declare function getRouterHydrationScripts(runtimeContext: object): readonly string[];
16
+ export declare function getRouterMatchedRouteIds(runtimeContext: object): readonly string[] | undefined;
18
17
  export declare function cleanupRouterRuntimeState(runtimeContext: object): Promise<void>;