@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.10
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/{reactContext.js → localisedUrls/config.js} +14 -10
- 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 +104 -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/rendererHead.js +18 -5
- package/dist/cjs/request-policy/policyCore.js +1 -1
- package/dist/cjs/request-policy/transport.js +2 -2
- package/dist/cjs/routerState.js +85 -77
- package/dist/cjs/routerStatePlugin.js +63 -9
- package/dist/cjs/server-config/serverSource.js +18 -0
- 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 +57 -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/rendererHead.mjs +14 -5
- package/dist/esm/request-policy/policyCore.mjs +1 -1
- package/dist/esm/request-policy/transport.mjs +2 -2
- package/dist/esm/routerState.mjs +81 -73
- package/dist/esm/routerStatePlugin.mjs +62 -11
- package/dist/esm/server-config/serverSource.mjs +0 -0
- 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 +58 -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/rendererHead.mjs +14 -5
- package/dist/esm-node/request-policy/policyCore.mjs +1 -1
- package/dist/esm-node/request-policy/transport.mjs +2 -2
- package/dist/esm-node/routerState.mjs +81 -73
- package/dist/esm-node/routerStatePlugin.mjs +62 -11
- package/dist/esm-node/server-config/serverSource.mjs +1 -0
- package/dist/types/helmetCompat.d.ts +1 -1
- 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/rendererHead.d.ts +5 -3
- package/dist/types/request-policy/browser.d.ts +5 -1
- package/dist/types/request-policy/node.d.ts +5 -1
- package/dist/types/request-policy/types.d.ts +1 -1
- package/dist/types/routeHydration.d.ts +2 -2
- package/dist/types/routerProvider.d.ts +1 -3
- package/dist/types/routerState.d.ts +11 -6
- package/dist/types/routerStatePlugin.d.ts +12 -0
- package/dist/types/routerStateTypes.d.ts +47 -17
- package/dist/types/server-config/index.d.ts +1 -0
- package/dist/types/server-config/serverSource.d.ts +16 -0
- package/package.json +43 -45
- package/src/helmetCompat.ts +1 -1
- 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 +110 -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/rendererHead.ts +25 -8
- package/src/request-policy/policyCore.ts +1 -1
- package/src/request-policy/transport.ts +2 -2
- package/src/request-policy/types.ts +1 -1
- package/src/routeHydration.ts +2 -2
- package/src/routerProvider.ts +1 -1
- package/src/routerState.ts +127 -115
- package/src/routerStatePlugin.ts +106 -10
- package/src/routerStateTypes.ts +47 -17
- package/src/server-config/index.ts +1 -0
- package/src/server-config/serverSource.ts +23 -0
- package/dist/cjs/buildIdentity.js +0 -73
- package/dist/esm/buildIdentity.mjs +0 -35
- package/dist/esm/reactContext.mjs +0 -7
- package/dist/esm-node/buildIdentity.mjs +0 -36
- package/dist/esm-node/reactContext.mjs +0 -8
- package/dist/types/buildIdentity.d.ts +0 -37
- package/dist/types/reactContext.d.ts +0 -6
- package/rslib.config.mts +0 -19
- package/rstest.config.mts +0 -38
- package/src/buildIdentity.ts +0 -61
- package/src/reactContext.ts +0 -16
- package/tests/buildIdentity.test.ts +0 -51
- package/tests/contextExtensions.test.ts +0 -27
- package/tests/headRuntime.test.tsx +0 -229
- package/tests/rendererHead.test.ts +0 -187
- package/tests/request-policy/browser-policy.test.ts +0 -108
- package/tests/request-policy/client-isolation.test.ts +0 -109
- package/tests/request-policy/node-policy.test.ts +0 -447
- package/tests/request-policy/policyCore.test.ts +0 -129
- package/tests/request-policy/requestContext.test.ts +0 -66
- package/tests/request-policy/requestFactory.outbound-contract.test.ts +0 -290
- package/tests/request-policy/traceparent.test.ts +0 -52
- package/tests/request-policy/transport-retry.test.ts +0 -367
- package/tests/routeHydration.test.ts +0 -180
- package/tests/routerPrefetchPolicy.test.ts +0 -102
- package/tests/routerProvider.test.ts +0 -29
- package/tests/routerState.test.ts +0 -78
- package/tests/routerStatePlugin.test.ts +0 -63
- package/tests/runtimeContextProjection.test.ts +0 -83
- package/tests/safeFailure.test.ts +0 -136
- package/tests/shellStream.test.ts +0 -122
- package/tests/workspaceEvents.test.ts +0 -221
- package/tsconfig.json +0 -10
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getLeadingLocaleParam,
|
|
3
|
+
normalisePathPattern,
|
|
4
|
+
normaliseRoutePath,
|
|
5
|
+
stripLeadingLocaleParam,
|
|
6
|
+
} from './normalise';
|
|
7
|
+
import {
|
|
8
|
+
doPathPatternsOverlap,
|
|
9
|
+
getOrderedPathParamSignature,
|
|
10
|
+
getPathParamSignature,
|
|
11
|
+
getPathPatternValidationIssue,
|
|
12
|
+
getPhysicalPathPattern,
|
|
13
|
+
haveEqualPatternSpecificity,
|
|
14
|
+
} from './patterns';
|
|
15
|
+
import type {
|
|
16
|
+
LocalisedRoute,
|
|
17
|
+
LocalisedUrlPathMap,
|
|
18
|
+
LocalisedUrlsMap,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
const isLocalisableRoutePath = (path?: string): path is string => {
|
|
22
|
+
const pathWithoutLocale = stripLeadingLocaleParam(path);
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
!pathWithoutLocale ||
|
|
26
|
+
pathWithoutLocale === '/' ||
|
|
27
|
+
pathWithoutLocale === '*'
|
|
28
|
+
) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return true;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const joinPath = (parentPath: string, routePath?: string): string => {
|
|
36
|
+
if (!isLocalisableRoutePath(routePath)) {
|
|
37
|
+
return parentPath;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const segment = normaliseRoutePath(stripLeadingLocaleParam(routePath) || '');
|
|
41
|
+
return normalisePathPattern(`${parentPath}/${segment}`);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isFrameworkInternalRoutePath = (canonicalPath: string) =>
|
|
45
|
+
canonicalPath === '/_mf' || canonicalPath.startsWith('/_mf/');
|
|
46
|
+
|
|
47
|
+
const legalRouteIdPart = (value: string): string =>
|
|
48
|
+
value.replace(/[^a-zA-Z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'index';
|
|
49
|
+
|
|
50
|
+
const formatParamSignature = (signature: string[]): string =>
|
|
51
|
+
signature.length > 0 ? signature.join(', ') : '(none)';
|
|
52
|
+
|
|
53
|
+
const validateLocalisedUrlPatterns = (
|
|
54
|
+
languages: string[],
|
|
55
|
+
localisedUrls: LocalisedUrlsMap,
|
|
56
|
+
) => {
|
|
57
|
+
const physicalPatterns = new Map<
|
|
58
|
+
string,
|
|
59
|
+
{ canonicalPath: string; source: string }
|
|
60
|
+
>();
|
|
61
|
+
const claimedPatterns: Array<{
|
|
62
|
+
canonicalPath: string;
|
|
63
|
+
pattern: string;
|
|
64
|
+
source: string;
|
|
65
|
+
}> = [];
|
|
66
|
+
|
|
67
|
+
const claimPhysicalPattern = (
|
|
68
|
+
sourcePattern: string,
|
|
69
|
+
canonicalPath: string,
|
|
70
|
+
source: string,
|
|
71
|
+
) => {
|
|
72
|
+
const physicalPattern = getPhysicalPathPattern(sourcePattern);
|
|
73
|
+
const existing = physicalPatterns.get(physicalPattern);
|
|
74
|
+
if (existing && existing.canonicalPath !== canonicalPath) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`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.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const overlapping = claimedPatterns.find(
|
|
81
|
+
claimed =>
|
|
82
|
+
claimed.canonicalPath !== canonicalPath &&
|
|
83
|
+
haveEqualPatternSpecificity(claimed.pattern, sourcePattern) &&
|
|
84
|
+
doPathPatternsOverlap(claimed.pattern, sourcePattern),
|
|
85
|
+
);
|
|
86
|
+
if (overlapping) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`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.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
physicalPatterns.set(physicalPattern, { canonicalPath, source });
|
|
93
|
+
claimedPatterns.push({ canonicalPath, pattern: sourcePattern, source });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (const [canonicalPath, entry] of Object.entries(localisedUrls).sort(
|
|
97
|
+
([left], [right]) => (left < right ? -1 : left > right ? 1 : 0),
|
|
98
|
+
)) {
|
|
99
|
+
const canonicalIssue = getPathPatternValidationIssue(canonicalPath);
|
|
100
|
+
if (canonicalIssue) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`localisedUrls["${canonicalPath}"] canonical pattern has ${canonicalIssue}.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const expectedSignature = getPathParamSignature(canonicalPath);
|
|
106
|
+
const expectedOrderedSignature =
|
|
107
|
+
getOrderedPathParamSignature(canonicalPath);
|
|
108
|
+
claimPhysicalPattern(canonicalPath, canonicalPath, 'canonical pattern');
|
|
109
|
+
|
|
110
|
+
for (const language of languages) {
|
|
111
|
+
const localisedPattern = entry[language];
|
|
112
|
+
if (!localisedPattern) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const localisedIssue = getPathPatternValidationIssue(localisedPattern);
|
|
117
|
+
if (localisedIssue) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`localisedUrls["${canonicalPath}"].${language} has ${localisedIssue}.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const actualSignature = getPathParamSignature(localisedPattern);
|
|
124
|
+
if (
|
|
125
|
+
actualSignature.length !== expectedSignature.length ||
|
|
126
|
+
actualSignature.some(
|
|
127
|
+
(parameter, index) => parameter !== expectedSignature[index],
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`localisedUrls["${canonicalPath}"].${language} must use the same parameter signature as "${canonicalPath}": expected ${formatParamSignature(
|
|
132
|
+
expectedSignature,
|
|
133
|
+
)}; received ${formatParamSignature(actualSignature)}.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const actualOrderedSignature =
|
|
138
|
+
getOrderedPathParamSignature(localisedPattern);
|
|
139
|
+
if (
|
|
140
|
+
expectedOrderedSignature.some(
|
|
141
|
+
parameter => parameter === '*' || parameter.endsWith('?'),
|
|
142
|
+
) &&
|
|
143
|
+
actualOrderedSignature.some(
|
|
144
|
+
(parameter, index) => parameter !== expectedOrderedSignature[index],
|
|
145
|
+
)
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`localisedUrls["${canonicalPath}"].${language} must keep optional and splat parameters in canonical order: expected ${formatParamSignature(
|
|
149
|
+
expectedOrderedSignature,
|
|
150
|
+
)}; received ${formatParamSignature(actualOrderedSignature)}.`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
claimPhysicalPattern(localisedPattern, canonicalPath, `.${language}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const ensureLocalisedUrlsForPath = (
|
|
160
|
+
canonicalPath: string,
|
|
161
|
+
languages: string[],
|
|
162
|
+
localisedUrls: LocalisedUrlsMap,
|
|
163
|
+
): LocalisedUrlPathMap => {
|
|
164
|
+
const entry = localisedUrls[canonicalPath];
|
|
165
|
+
if (!entry) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`localisedUrls is enabled, but route "${canonicalPath}" does not define localised URLs for languages: ${languages.join(
|
|
168
|
+
', ',
|
|
169
|
+
)}. Add localisedUrls["${canonicalPath}"] or set localeDetection.localisedUrls to false.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const missingLanguages = languages.filter(language => !entry[language]);
|
|
174
|
+
if (missingLanguages.length > 0) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`localisedUrls["${canonicalPath}"] is missing languages: ${missingLanguages.join(
|
|
177
|
+
', ',
|
|
178
|
+
)}. Every configured language must have a localised URL.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return entry;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const validateLocalisedUrls = (
|
|
186
|
+
routes: LocalisedRoute[],
|
|
187
|
+
languages: string[],
|
|
188
|
+
localisedUrls: LocalisedUrlsMap,
|
|
189
|
+
) => {
|
|
190
|
+
validateLocalisedUrlPatterns(languages, localisedUrls);
|
|
191
|
+
|
|
192
|
+
const visit = (route: LocalisedRoute, parentPath: string) => {
|
|
193
|
+
const canonicalPath = joinPath(parentPath, route.path);
|
|
194
|
+
if (
|
|
195
|
+
isLocalisableRoutePath(route.path) &&
|
|
196
|
+
!isFrameworkInternalRoutePath(canonicalPath)
|
|
197
|
+
) {
|
|
198
|
+
ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if ('children' in route && route.children) {
|
|
202
|
+
route.children.forEach(child => visit(child, canonicalPath));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
routes.forEach(route => visit(route, ''));
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const getLocalisedRouteIdSuffixes = (
|
|
210
|
+
paths: string[],
|
|
211
|
+
): Array<string | undefined> => {
|
|
212
|
+
const usedSuffixes = new Set<string>();
|
|
213
|
+
|
|
214
|
+
return paths.map((path, index) => {
|
|
215
|
+
if (index === 0) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const baseSuffix = legalRouteIdPart(path);
|
|
220
|
+
let suffix = baseSuffix;
|
|
221
|
+
let disambiguator = 2;
|
|
222
|
+
while (usedSuffixes.has(suffix)) {
|
|
223
|
+
suffix = `${baseSuffix}_${disambiguator++}`;
|
|
224
|
+
}
|
|
225
|
+
usedSuffixes.add(suffix);
|
|
226
|
+
return suffix;
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const getLocalisedRoutePaths = (
|
|
231
|
+
canonicalPath: string,
|
|
232
|
+
parentLocalisedPaths: Record<string, string>,
|
|
233
|
+
languages: string[],
|
|
234
|
+
entry: LocalisedUrlPathMap,
|
|
235
|
+
): string[] => {
|
|
236
|
+
const paths = languages.map(language => {
|
|
237
|
+
const fullPath = normalisePathPattern(entry[language]);
|
|
238
|
+
const parentPath = normalisePathPattern(
|
|
239
|
+
parentLocalisedPaths[language] || '/',
|
|
240
|
+
);
|
|
241
|
+
if (parentPath === '/') {
|
|
242
|
+
return normaliseRoutePath(fullPath) || undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const parentPrefix = `${parentPath}/`;
|
|
246
|
+
if (!fullPath.startsWith(parentPrefix)) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`localisedUrls["${canonicalPath}"].${language} must be nested under "${parentPath}" because its parent route is localised there.`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return normaliseRoutePath(fullPath.slice(parentPath.length));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const uniquePaths = Array.from(new Set(paths.filter(Boolean) as string[]));
|
|
256
|
+
return uniquePaths;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const transformLocalisedRoute = (
|
|
260
|
+
route: LocalisedRoute,
|
|
261
|
+
parentCanonicalPath: string,
|
|
262
|
+
parentLocalisedPaths: Record<string, string>,
|
|
263
|
+
languages: string[],
|
|
264
|
+
localisedUrls: LocalisedUrlsMap,
|
|
265
|
+
projection: 'physical' | 'canonical',
|
|
266
|
+
): LocalisedRoute[] => {
|
|
267
|
+
const isSplat = stripLeadingLocaleParam(route.path) === '*';
|
|
268
|
+
const canonicalPath = isSplat
|
|
269
|
+
? normalisePathPattern(`${parentCanonicalPath}/*`)
|
|
270
|
+
: joinPath(parentCanonicalPath, route.path);
|
|
271
|
+
const localisedUrlEntry =
|
|
272
|
+
isLocalisableRoutePath(route.path) &&
|
|
273
|
+
!isFrameworkInternalRoutePath(canonicalPath)
|
|
274
|
+
? ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls)
|
|
275
|
+
: undefined;
|
|
276
|
+
const routeLocalisedPaths = localisedUrlEntry
|
|
277
|
+
? languages.reduce<Record<string, string>>((acc, language) => {
|
|
278
|
+
acc[language] = normalisePathPattern(localisedUrlEntry[language]);
|
|
279
|
+
return acc;
|
|
280
|
+
}, {})
|
|
281
|
+
: isSplat
|
|
282
|
+
? Object.fromEntries(
|
|
283
|
+
Object.entries(parentLocalisedPaths).map(([language, path]) => [
|
|
284
|
+
language,
|
|
285
|
+
normalisePathPattern(`${path}/*`),
|
|
286
|
+
]),
|
|
287
|
+
)
|
|
288
|
+
: parentLocalisedPaths;
|
|
289
|
+
|
|
290
|
+
const children =
|
|
291
|
+
'children' in route && route.children
|
|
292
|
+
? route.children.flatMap(child =>
|
|
293
|
+
transformLocalisedRoute(
|
|
294
|
+
child,
|
|
295
|
+
canonicalPath,
|
|
296
|
+
routeLocalisedPaths,
|
|
297
|
+
languages,
|
|
298
|
+
localisedUrls,
|
|
299
|
+
projection,
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
: undefined;
|
|
303
|
+
|
|
304
|
+
const baseRoute = {
|
|
305
|
+
...route,
|
|
306
|
+
...(!isFrameworkInternalRoutePath(canonicalPath)
|
|
307
|
+
? {
|
|
308
|
+
modernLocalisedRoute: {
|
|
309
|
+
id: route.id,
|
|
310
|
+
path: route.path,
|
|
311
|
+
canonicalPath,
|
|
312
|
+
paths:
|
|
313
|
+
localisedUrlEntry ?? (isSplat ? routeLocalisedPaths : undefined),
|
|
314
|
+
},
|
|
315
|
+
}
|
|
316
|
+
: {}),
|
|
317
|
+
...(children ? { children } : {}),
|
|
318
|
+
} as LocalisedRoute;
|
|
319
|
+
|
|
320
|
+
if (!localisedUrlEntry) {
|
|
321
|
+
return [baseRoute];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const localisedPaths = getLocalisedRoutePaths(
|
|
325
|
+
canonicalPath,
|
|
326
|
+
parentLocalisedPaths,
|
|
327
|
+
languages,
|
|
328
|
+
localisedUrlEntry,
|
|
329
|
+
);
|
|
330
|
+
// Validate nesting for both adapters, but only React Router needs physical
|
|
331
|
+
// variants. TanStack consumes the source identity and native URL rewrites.
|
|
332
|
+
if (projection === 'canonical') return [baseRoute];
|
|
333
|
+
|
|
334
|
+
const leadingLocaleParam = getLeadingLocaleParam(route.path);
|
|
335
|
+
const routeIdPaths = leadingLocaleParam
|
|
336
|
+
? localisedPaths.map(path =>
|
|
337
|
+
normaliseRoutePath(`${leadingLocaleParam}/${path}`),
|
|
338
|
+
)
|
|
339
|
+
: localisedPaths;
|
|
340
|
+
const routeIdSuffixes = getLocalisedRouteIdSuffixes(routeIdPaths);
|
|
341
|
+
|
|
342
|
+
return localisedPaths.map((localisedPath, index) =>
|
|
343
|
+
cloneRouteWithLocalisedPath(
|
|
344
|
+
baseRoute,
|
|
345
|
+
localisedPath,
|
|
346
|
+
routeIdSuffixes[index],
|
|
347
|
+
canonicalPath,
|
|
348
|
+
),
|
|
349
|
+
);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const suffixRouteIds = <T extends LocalisedRoute>(
|
|
353
|
+
route: T,
|
|
354
|
+
suffix: string,
|
|
355
|
+
): T => {
|
|
356
|
+
const children =
|
|
357
|
+
'children' in route && route.children
|
|
358
|
+
? route.children.map(child => suffixRouteIds(child, suffix))
|
|
359
|
+
: undefined;
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
...route,
|
|
363
|
+
...(route.id ? { id: `${route.id}__localised_${suffix}` } : {}),
|
|
364
|
+
...(children ? { children } : {}),
|
|
365
|
+
};
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const collectRouteIdCounts = (
|
|
369
|
+
routes: LocalisedRoute[],
|
|
370
|
+
counts = new Map<string, number>(),
|
|
371
|
+
): Map<string, number> => {
|
|
372
|
+
for (const route of routes) {
|
|
373
|
+
if (route.id) {
|
|
374
|
+
counts.set(route.id, (counts.get(route.id) ?? 0) + 1);
|
|
375
|
+
}
|
|
376
|
+
if ('children' in route && route.children) {
|
|
377
|
+
collectRouteIdCounts(route.children, counts);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return counts;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const validateNoNewRouteIdCollisions = (
|
|
384
|
+
sourceRoutes: LocalisedRoute[],
|
|
385
|
+
localisedRoutes: LocalisedRoute[],
|
|
386
|
+
) => {
|
|
387
|
+
const sourceCounts = collectRouteIdCounts(sourceRoutes);
|
|
388
|
+
const localisedCounts = collectRouteIdCounts(localisedRoutes);
|
|
389
|
+
|
|
390
|
+
for (const [routeId, count] of localisedCounts) {
|
|
391
|
+
if (count > 1 && count > (sourceCounts.get(routeId) ?? 0)) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`Localised route expansion generated duplicate route ID "${routeId}". Change the source route ID or localised path so every generated route remains identifiable.`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const cloneRouteWithLocalisedPath = (
|
|
400
|
+
route: LocalisedRoute,
|
|
401
|
+
path: string,
|
|
402
|
+
routeIdSuffix: string | undefined,
|
|
403
|
+
canonicalPath: string,
|
|
404
|
+
): LocalisedRoute => {
|
|
405
|
+
const leadingLocaleParam = getLeadingLocaleParam(route.path);
|
|
406
|
+
const localisedPath = leadingLocaleParam
|
|
407
|
+
? normaliseRoutePath(`${leadingLocaleParam}/${path}`)
|
|
408
|
+
: path;
|
|
409
|
+
const routeWithPath = {
|
|
410
|
+
...route,
|
|
411
|
+
path: localisedPath,
|
|
412
|
+
} as LocalisedRoute;
|
|
413
|
+
// Language-agnostic source pattern; lets downstream codegen collapse the
|
|
414
|
+
// localized physical variants back to one canonical route.
|
|
415
|
+
(routeWithPath as { modernCanonicalPath?: string }).modernCanonicalPath =
|
|
416
|
+
canonicalPath;
|
|
417
|
+
|
|
418
|
+
return routeIdSuffix
|
|
419
|
+
? suffixRouteIds(routeWithPath, routeIdSuffix)
|
|
420
|
+
: routeWithPath;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
export const applyLocalisedUrlsToRoutes = (
|
|
424
|
+
routes: LocalisedRoute[],
|
|
425
|
+
languages: string[],
|
|
426
|
+
localisedUrls: LocalisedUrlsMap,
|
|
427
|
+
projection: 'physical' | 'canonical' = 'physical',
|
|
428
|
+
): LocalisedRoute[] => {
|
|
429
|
+
const rootLocalisedPaths = languages.reduce<Record<string, string>>(
|
|
430
|
+
(acc, language) => {
|
|
431
|
+
acc[language] = '/';
|
|
432
|
+
return acc;
|
|
433
|
+
},
|
|
434
|
+
{},
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
validateLocalisedUrls(routes, languages, localisedUrls);
|
|
438
|
+
|
|
439
|
+
const localisedRoutes = routes.flatMap(route =>
|
|
440
|
+
transformLocalisedRoute(
|
|
441
|
+
route,
|
|
442
|
+
'',
|
|
443
|
+
rootLocalisedPaths,
|
|
444
|
+
languages,
|
|
445
|
+
localisedUrls,
|
|
446
|
+
projection,
|
|
447
|
+
),
|
|
448
|
+
);
|
|
449
|
+
validateNoNewRouteIdCollisions(routes, localisedRoutes);
|
|
450
|
+
return localisedRoutes;
|
|
451
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type LocalisedRoute = {
|
|
2
|
+
type: 'nested' | 'page';
|
|
3
|
+
path?: string;
|
|
4
|
+
id?: string;
|
|
5
|
+
children?: LocalisedRoute[];
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type LocalisedUrlPathMap = Record<string, string>;
|
|
10
|
+
|
|
11
|
+
export type LocalisedUrlsMap = Record<string, LocalisedUrlPathMap>;
|
|
12
|
+
|
|
13
|
+
export type LocalisedUrlsOption = boolean | LocalisedUrlsMap;
|
|
14
|
+
|
|
15
|
+
export interface ResolvedLocalisedUrlsConfig {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
map: LocalisedUrlsMap;
|
|
18
|
+
}
|
package/src/rendererHead.ts
CHANGED
|
@@ -9,6 +9,10 @@ export type RendererHeadMarkerProps = Record<typeof MARKER_ATTRIBUTE, string>;
|
|
|
9
9
|
export * from './helmetCompat';
|
|
10
10
|
|
|
11
11
|
type PublishRecords = (records: unknown[]) => void;
|
|
12
|
+
export type ReportLateHead = (message: string) => void;
|
|
13
|
+
|
|
14
|
+
export const LATE_HEAD_MESSAGE =
|
|
15
|
+
"<Helmet> rendered in a boundary that completed after the shell; head tags dropped. Move it above the Suspense boundary or set server.ssrByEntries[entry].mode to 'string'.";
|
|
12
16
|
|
|
13
17
|
type HeadTransaction = {
|
|
14
18
|
recordsByToken: Map<string, unknown>;
|
|
@@ -18,6 +22,8 @@ type HeadTransaction = {
|
|
|
18
22
|
nonce: string;
|
|
19
23
|
previousRecords: unknown[];
|
|
20
24
|
sealed: boolean;
|
|
25
|
+
reportLateHead: ReportLateHead;
|
|
26
|
+
lateHeadReported: boolean;
|
|
21
27
|
};
|
|
22
28
|
|
|
23
29
|
type HeadState = {
|
|
@@ -48,7 +54,16 @@ const createToken = (transaction: HeadTransaction): string => {
|
|
|
48
54
|
return token;
|
|
49
55
|
};
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
const dropLateHead = (transaction: HeadTransaction): void => {
|
|
58
|
+
if (transaction.lateHeadReported) return;
|
|
59
|
+
transaction.lateHeadReported = true;
|
|
60
|
+
transaction.reportLateHead(LATE_HEAD_MESSAGE);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const beginHeadRender = (
|
|
64
|
+
context: object,
|
|
65
|
+
reportLateHead: ReportLateHead,
|
|
66
|
+
): void => {
|
|
52
67
|
const state = getState(context);
|
|
53
68
|
state.transaction = {
|
|
54
69
|
recordsByToken: new Map(),
|
|
@@ -58,6 +73,8 @@ export const beginHeadRender = (context: object): void => {
|
|
|
58
73
|
nonce: globalThis.crypto.randomUUID(),
|
|
59
74
|
previousRecords: state.publishedRecords,
|
|
60
75
|
sealed: false,
|
|
76
|
+
reportLateHead,
|
|
77
|
+
lateHeadReported: false,
|
|
61
78
|
};
|
|
62
79
|
state.publish?.([]);
|
|
63
80
|
};
|
|
@@ -66,7 +83,7 @@ export const collectHeadRecord = <RecordType>(
|
|
|
66
83
|
context: object,
|
|
67
84
|
createRecord: () => RecordType,
|
|
68
85
|
publish: (records: RecordType[]) => void,
|
|
69
|
-
): RendererHeadMarkerProps |
|
|
86
|
+
): RendererHeadMarkerProps | undefined => {
|
|
70
87
|
const state = getState(context);
|
|
71
88
|
const transaction = state.transaction;
|
|
72
89
|
if (transaction === undefined) {
|
|
@@ -78,10 +95,8 @@ export const collectHeadRecord = <RecordType>(
|
|
|
78
95
|
if (!hadPublisher) {
|
|
79
96
|
state.publish([]);
|
|
80
97
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
98
|
+
// A Helmet rendered after the seal still gets a marker: it is dropped, and
|
|
99
|
+
// reported, only if its boundary actually commits to the output.
|
|
85
100
|
const token = createToken(transaction);
|
|
86
101
|
transaction.recordsByToken.set(token, createRecord());
|
|
87
102
|
return { [MARKER_ATTRIBUTE]: token };
|
|
@@ -92,7 +107,7 @@ export const collectHeadState = <RecordType, StateType>(
|
|
|
92
107
|
createRecord: () => RecordType,
|
|
93
108
|
derive: (records: RecordType[]) => StateType,
|
|
94
109
|
target: { helmet?: StateType },
|
|
95
|
-
): RendererHeadMarkerProps |
|
|
110
|
+
): RendererHeadMarkerProps | undefined =>
|
|
96
111
|
collectHeadRecord(context, createRecord, records => {
|
|
97
112
|
target.helmet = derive(records);
|
|
98
113
|
});
|
|
@@ -123,7 +138,9 @@ const consumeMarkers = (context: object, html: string): string => {
|
|
|
123
138
|
stripped += html.slice(cursor, end + MARKER_SUFFIX.length);
|
|
124
139
|
} else {
|
|
125
140
|
stripped += html.slice(cursor, start);
|
|
126
|
-
if (
|
|
141
|
+
if (transaction.sealed) {
|
|
142
|
+
if (!transaction.committedTokens.has(token)) dropLateHead(transaction);
|
|
143
|
+
} else if (!transaction.committedTokens.has(token)) {
|
|
127
144
|
transaction.committedTokens.add(token);
|
|
128
145
|
transaction.committedRecords.push(
|
|
129
146
|
transaction.recordsByToken.get(token),
|
|
@@ -131,7 +131,7 @@ const shouldRetry = (
|
|
|
131
131
|
if (typeof retryOptions?.shouldRetry === 'function') {
|
|
132
132
|
try {
|
|
133
133
|
return retryOptions.shouldRetry(context);
|
|
134
|
-
} catch
|
|
134
|
+
} catch {
|
|
135
135
|
return false;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -176,7 +176,7 @@ const emitDegradedEvent = (
|
|
|
176
176
|
|
|
177
177
|
try {
|
|
178
178
|
transport.onDegraded(event);
|
|
179
|
-
} catch
|
|
179
|
+
} catch {
|
|
180
180
|
// best-effort telemetry only
|
|
181
181
|
}
|
|
182
182
|
};
|
package/src/routeHydration.ts
CHANGED
|
@@ -71,7 +71,7 @@ const isAsyncEntryScriptChunk = (chunk: ScriptChunkLike, entryName: string) => {
|
|
|
71
71
|
|
|
72
72
|
export function getMatchedRouteChunks<T>(
|
|
73
73
|
routeManifest: RouteManifestLike | undefined,
|
|
74
|
-
matchedRouteIds: string[],
|
|
74
|
+
matchedRouteIds: readonly string[],
|
|
75
75
|
routeAssetToChunk: (asset: string) => T,
|
|
76
76
|
): T[] {
|
|
77
77
|
const routeAssets = routeManifest?.routeAssets;
|
|
@@ -167,7 +167,7 @@ export function replaceChunkJsPlaceholder(
|
|
|
167
167
|
|
|
168
168
|
export function createRouteHydrationScriptTags(
|
|
169
169
|
routeManifest: RouteManifestLike | undefined,
|
|
170
|
-
matchedRouteIds: string[],
|
|
170
|
+
matchedRouteIds: readonly string[],
|
|
171
171
|
entryName: string,
|
|
172
172
|
options: {
|
|
173
173
|
nonce?: string;
|
package/src/routerProvider.ts
CHANGED
|
@@ -144,7 +144,7 @@ export function createRouterPlugin<Hooks extends Record<string, unknown>>({
|
|
|
144
144
|
name: '@modern-js/plugin-router',
|
|
145
145
|
registryHooks,
|
|
146
146
|
setup(api: {
|
|
147
|
-
getRuntimeConfig: () =>
|
|
147
|
+
getRuntimeConfig: () => Record<string, unknown>;
|
|
148
148
|
[key: string]: any;
|
|
149
149
|
}) {
|
|
150
150
|
const mergedConfig = merge(
|