@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.11 → 3.9.0-ultramodern.13
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/cjs/localisedUrls/config.js +49 -0
- package/dist/cjs/localisedUrls/index.js +95 -0
- package/dist/cjs/localisedUrls/normalise.js +87 -0
- package/dist/cjs/localisedUrls/pathname.js +68 -0
- package/dist/cjs/localisedUrls/patterns.js +214 -0
- package/dist/cjs/localisedUrls/redirect.js +100 -0
- package/dist/cjs/localisedUrls/resolve.js +78 -0
- package/dist/cjs/localisedUrls/routeIdentity.js +92 -0
- package/dist/cjs/localisedUrls/routes.js +217 -0
- package/dist/cjs/localisedUrls/types.js +18 -0
- package/dist/cjs/routerState.js +48 -77
- package/dist/cjs/routerStatePlugin.js +36 -9
- package/dist/esm/localisedUrls/config.mjs +11 -0
- package/dist/esm/localisedUrls/index.mjs +8 -0
- package/dist/esm/localisedUrls/normalise.mjs +37 -0
- package/dist/esm/localisedUrls/pathname.mjs +24 -0
- package/dist/esm/localisedUrls/patterns.mjs +152 -0
- package/dist/esm/localisedUrls/redirect.mjs +53 -0
- package/dist/esm/localisedUrls/resolve.mjs +37 -0
- package/dist/esm/localisedUrls/routeIdentity.mjs +54 -0
- package/dist/esm/localisedUrls/routes.mjs +176 -0
- package/dist/esm/localisedUrls/types.mjs +0 -0
- package/dist/esm/routerState.mjs +49 -75
- package/dist/esm/routerStatePlugin.mjs +37 -10
- package/dist/esm-node/localisedUrls/config.mjs +12 -0
- package/dist/esm-node/localisedUrls/index.mjs +9 -0
- package/dist/esm-node/localisedUrls/normalise.mjs +38 -0
- package/dist/esm-node/localisedUrls/pathname.mjs +25 -0
- package/dist/esm-node/localisedUrls/patterns.mjs +153 -0
- package/dist/esm-node/localisedUrls/redirect.mjs +54 -0
- package/dist/esm-node/localisedUrls/resolve.mjs +38 -0
- package/dist/esm-node/localisedUrls/routeIdentity.mjs +55 -0
- package/dist/esm-node/localisedUrls/routes.mjs +177 -0
- package/dist/esm-node/localisedUrls/types.mjs +1 -0
- package/dist/esm-node/routerState.mjs +49 -75
- package/dist/esm-node/routerStatePlugin.mjs +37 -10
- package/dist/types/localisedUrls/config.d.ts +9 -0
- package/dist/types/localisedUrls/index.d.ts +10 -0
- package/dist/types/localisedUrls/normalise.d.ts +10 -0
- package/dist/types/localisedUrls/pathname.d.ts +4 -0
- package/dist/types/localisedUrls/patterns.d.ts +11 -0
- package/dist/types/localisedUrls/redirect.d.ts +19 -0
- package/dist/types/localisedUrls/resolve.d.ts +7 -0
- package/dist/types/localisedUrls/routeIdentity.d.ts +22 -0
- package/dist/types/localisedUrls/routes.d.ts +3 -0
- package/dist/types/localisedUrls/types.d.ts +14 -0
- package/dist/types/request-policy/types.d.ts +1 -1
- package/dist/types/routeHydration.d.ts +2 -2
- package/dist/types/routerState.d.ts +5 -6
- package/dist/types/routerStateTypes.d.ts +47 -17
- package/package.json +24 -12
- package/src/localisedUrls/config.ts +18 -0
- package/src/localisedUrls/index.ts +25 -0
- package/src/localisedUrls/normalise.ts +62 -0
- package/src/localisedUrls/pathname.ts +60 -0
- package/src/localisedUrls/patterns.ts +283 -0
- package/src/localisedUrls/redirect.ts +107 -0
- package/src/localisedUrls/resolve.ts +106 -0
- package/src/localisedUrls/routeIdentity.ts +83 -0
- package/src/localisedUrls/routes.ts +451 -0
- package/src/localisedUrls/types.ts +18 -0
- package/src/request-policy/types.ts +1 -1
- package/src/routeHydration.ts +2 -2
- package/src/routerState.ts +52 -122
- package/src/routerStatePlugin.ts +49 -10
- package/src/routerStateTypes.ts +47 -17
- package/tests/localisedUrls.test.ts +560 -0
- 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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
...
|
|
65
|
-
|
|
33
|
+
...matches ? {
|
|
34
|
+
matches: Object.freeze(matches)
|
|
35
|
+
} : {},
|
|
36
|
+
...snapshot.errors ? {
|
|
37
|
+
errors: Object.freeze({
|
|
38
|
+
...snapshot.errors
|
|
39
|
+
})
|
|
66
40
|
} : {},
|
|
67
|
-
...
|
|
68
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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:
|
|
37
|
-
|
|
38
|
-
|
|
66
|
+
instance: router,
|
|
67
|
+
...api?.Link ? {
|
|
68
|
+
navigation: createReactRouterNavigation(router, api.Link)
|
|
69
|
+
} : {}
|
|
39
70
|
});
|
|
40
|
-
}
|
|
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
|
+
}
|
|
@@ -106,7 +106,7 @@ export type OperationContractOptions = {
|
|
|
106
106
|
requireOperationVersion?: boolean;
|
|
107
107
|
onViolation?: (violation: OperationContractViolation) => void;
|
|
108
108
|
};
|
|
109
|
-
export type OperationContextSource = 'client' | 'server' | 'generated-client' | 'effect-
|
|
109
|
+
export type OperationContextSource = 'client' | 'server' | 'generated-client' | 'effect-server' | 'data-platform' | 'unknown';
|
|
110
110
|
export type OperationContext = {
|
|
111
111
|
requestId?: string;
|
|
112
112
|
operationId?: string;
|
|
@@ -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
|
-
|
|
6
|
-
export declare function createRouterServerSnapshot(
|
|
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>;
|