@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2

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 (124) hide show
  1. package/dist/cjs/{reactContext.js → localisedUrls/config.js} +14 -10
  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 +104 -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/rendererHead.js +18 -5
  12. package/dist/cjs/request-policy/policyCore.js +1 -1
  13. package/dist/cjs/request-policy/transport.js +2 -2
  14. package/dist/cjs/routerState.js +85 -77
  15. package/dist/cjs/routerStatePlugin.js +63 -9
  16. package/dist/cjs/server-config/serverSource.js +18 -0
  17. package/dist/esm/localisedUrls/config.mjs +11 -0
  18. package/dist/esm/localisedUrls/index.mjs +8 -0
  19. package/dist/esm/localisedUrls/normalise.mjs +37 -0
  20. package/dist/esm/localisedUrls/pathname.mjs +24 -0
  21. package/dist/esm/localisedUrls/patterns.mjs +152 -0
  22. package/dist/esm/localisedUrls/redirect.mjs +57 -0
  23. package/dist/esm/localisedUrls/resolve.mjs +37 -0
  24. package/dist/esm/localisedUrls/routeIdentity.mjs +54 -0
  25. package/dist/esm/localisedUrls/routes.mjs +176 -0
  26. package/dist/esm/localisedUrls/types.mjs +0 -0
  27. package/dist/esm/rendererHead.mjs +14 -5
  28. package/dist/esm/request-policy/policyCore.mjs +1 -1
  29. package/dist/esm/request-policy/transport.mjs +2 -2
  30. package/dist/esm/routerState.mjs +81 -73
  31. package/dist/esm/routerStatePlugin.mjs +62 -11
  32. package/dist/esm/server-config/serverSource.mjs +0 -0
  33. package/dist/esm-node/localisedUrls/config.mjs +12 -0
  34. package/dist/esm-node/localisedUrls/index.mjs +9 -0
  35. package/dist/esm-node/localisedUrls/normalise.mjs +38 -0
  36. package/dist/esm-node/localisedUrls/pathname.mjs +25 -0
  37. package/dist/esm-node/localisedUrls/patterns.mjs +153 -0
  38. package/dist/esm-node/localisedUrls/redirect.mjs +58 -0
  39. package/dist/esm-node/localisedUrls/resolve.mjs +38 -0
  40. package/dist/esm-node/localisedUrls/routeIdentity.mjs +55 -0
  41. package/dist/esm-node/localisedUrls/routes.mjs +177 -0
  42. package/dist/esm-node/localisedUrls/types.mjs +1 -0
  43. package/dist/esm-node/rendererHead.mjs +14 -5
  44. package/dist/esm-node/request-policy/policyCore.mjs +1 -1
  45. package/dist/esm-node/request-policy/transport.mjs +2 -2
  46. package/dist/esm-node/routerState.mjs +81 -73
  47. package/dist/esm-node/routerStatePlugin.mjs +62 -11
  48. package/dist/esm-node/server-config/serverSource.mjs +1 -0
  49. package/dist/types/helmetCompat.d.ts +1 -1
  50. package/dist/types/localisedUrls/config.d.ts +9 -0
  51. package/dist/types/localisedUrls/index.d.ts +10 -0
  52. package/dist/types/localisedUrls/normalise.d.ts +10 -0
  53. package/dist/types/localisedUrls/pathname.d.ts +4 -0
  54. package/dist/types/localisedUrls/patterns.d.ts +11 -0
  55. package/dist/types/localisedUrls/redirect.d.ts +19 -0
  56. package/dist/types/localisedUrls/resolve.d.ts +7 -0
  57. package/dist/types/localisedUrls/routeIdentity.d.ts +22 -0
  58. package/dist/types/localisedUrls/routes.d.ts +3 -0
  59. package/dist/types/localisedUrls/types.d.ts +14 -0
  60. package/dist/types/rendererHead.d.ts +5 -3
  61. package/dist/types/request-policy/types.d.ts +1 -1
  62. package/dist/types/routeHydration.d.ts +2 -2
  63. package/dist/types/routerProvider.d.ts +1 -3
  64. package/dist/types/routerState.d.ts +11 -6
  65. package/dist/types/routerStatePlugin.d.ts +12 -0
  66. package/dist/types/routerStateTypes.d.ts +47 -17
  67. package/dist/types/server-config/index.d.ts +1 -0
  68. package/dist/types/server-config/serverSource.d.ts +16 -0
  69. package/package.json +42 -44
  70. package/src/helmetCompat.ts +1 -1
  71. package/src/localisedUrls/config.ts +18 -0
  72. package/src/localisedUrls/index.ts +25 -0
  73. package/src/localisedUrls/normalise.ts +62 -0
  74. package/src/localisedUrls/pathname.ts +60 -0
  75. package/src/localisedUrls/patterns.ts +283 -0
  76. package/src/localisedUrls/redirect.ts +110 -0
  77. package/src/localisedUrls/resolve.ts +106 -0
  78. package/src/localisedUrls/routeIdentity.ts +83 -0
  79. package/src/localisedUrls/routes.ts +451 -0
  80. package/src/localisedUrls/types.ts +18 -0
  81. package/src/rendererHead.ts +25 -8
  82. package/src/request-policy/policyCore.ts +1 -1
  83. package/src/request-policy/transport.ts +2 -2
  84. package/src/request-policy/types.ts +1 -1
  85. package/src/routeHydration.ts +2 -2
  86. package/src/routerProvider.ts +1 -1
  87. package/src/routerState.ts +127 -115
  88. package/src/routerStatePlugin.ts +106 -10
  89. package/src/routerStateTypes.ts +47 -17
  90. package/src/server-config/index.ts +1 -0
  91. package/src/server-config/serverSource.ts +23 -0
  92. package/dist/cjs/buildIdentity.js +0 -73
  93. package/dist/esm/buildIdentity.mjs +0 -35
  94. package/dist/esm/reactContext.mjs +0 -7
  95. package/dist/esm-node/buildIdentity.mjs +0 -36
  96. package/dist/esm-node/reactContext.mjs +0 -8
  97. package/dist/types/buildIdentity.d.ts +0 -37
  98. package/dist/types/reactContext.d.ts +0 -6
  99. package/rslib.config.mts +0 -19
  100. package/rstest.config.mts +0 -38
  101. package/src/buildIdentity.ts +0 -61
  102. package/src/reactContext.ts +0 -16
  103. package/tests/buildIdentity.test.ts +0 -51
  104. package/tests/contextExtensions.test.ts +0 -27
  105. package/tests/headRuntime.test.tsx +0 -229
  106. package/tests/rendererHead.test.ts +0 -187
  107. package/tests/request-policy/browser-policy.test.ts +0 -108
  108. package/tests/request-policy/client-isolation.test.ts +0 -109
  109. package/tests/request-policy/node-policy.test.ts +0 -447
  110. package/tests/request-policy/policyCore.test.ts +0 -129
  111. package/tests/request-policy/requestContext.test.ts +0 -66
  112. package/tests/request-policy/requestFactory.outbound-contract.test.ts +0 -290
  113. package/tests/request-policy/traceparent.test.ts +0 -52
  114. package/tests/request-policy/transport-retry.test.ts +0 -367
  115. package/tests/routeHydration.test.ts +0 -180
  116. package/tests/routerPrefetchPolicy.test.ts +0 -102
  117. package/tests/routerProvider.test.ts +0 -29
  118. package/tests/routerState.test.ts +0 -78
  119. package/tests/routerStatePlugin.test.ts +0 -63
  120. package/tests/runtimeContextProjection.test.ts +0 -83
  121. package/tests/safeFailure.test.ts +0 -136
  122. package/tests/shellStream.test.ts +0 -122
  123. package/tests/workspaceEvents.test.ts +0 -221
  124. 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
+ }
@@ -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
- export const beginHeadRender = (context: object): void => {
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 | null | undefined => {
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
- if (transaction.sealed) {
82
- return null;
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 | null | undefined =>
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 (!transaction.sealed && !transaction.committedTokens.has(token)) {
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),
@@ -92,7 +92,7 @@ export const toOrigin = (value?: string) => {
92
92
  }
93
93
  try {
94
94
  return new URL(value).origin;
95
- } catch (error) {
95
+ } catch {
96
96
  return undefined;
97
97
  }
98
98
  };
@@ -131,7 +131,7 @@ const shouldRetry = (
131
131
  if (typeof retryOptions?.shouldRetry === 'function') {
132
132
  try {
133
133
  return retryOptions.shouldRetry(context);
134
- } catch (error) {
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 (error) {
179
+ } catch {
180
180
  // best-effort telemetry only
181
181
  }
182
182
  };
@@ -174,7 +174,7 @@ export type OperationContextSource =
174
174
  | 'client'
175
175
  | 'server'
176
176
  | 'generated-client'
177
- | 'effect-adapter'
177
+ | 'effect-server'
178
178
  | 'data-platform'
179
179
  | 'unknown';
180
180
 
@@ -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;
@@ -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: () => { router?: Record<string, unknown> };
147
+ getRuntimeConfig: () => Record<string, unknown>;
148
148
  [key: string]: any;
149
149
  }) {
150
150
  const mergedConfig = merge(