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

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 (251) hide show
  1. package/dist/cjs/buildIdentity.js +73 -0
  2. package/dist/cjs/contextExtensions.js +74 -0
  3. package/dist/cjs/headRuntime.js +91 -0
  4. package/dist/cjs/helmetContext.js +54 -0
  5. package/dist/cjs/index.js +18 -4
  6. package/dist/cjs/localisedLoaderIdentity.js +51 -0
  7. package/dist/cjs/localisedUrls/config.js +49 -0
  8. package/dist/cjs/localisedUrls/index.js +95 -0
  9. package/dist/cjs/localisedUrls/normalise.js +87 -0
  10. package/dist/cjs/localisedUrls/pathname.js +68 -0
  11. package/dist/cjs/localisedUrls/patterns.js +214 -0
  12. package/dist/cjs/localisedUrls/redirect.js +100 -0
  13. package/dist/cjs/localisedUrls/resolve.js +78 -0
  14. package/dist/cjs/localisedUrls/routeIdentity.js +92 -0
  15. package/dist/cjs/localisedUrls/routes.js +217 -0
  16. package/dist/cjs/localisedUrls/types.js +18 -0
  17. package/dist/cjs/prefetchPolicy.js +52 -0
  18. package/dist/cjs/rendererHead.js +18 -5
  19. package/dist/cjs/request-policy/browser.js +139 -0
  20. package/dist/cjs/request-policy/client.js +234 -0
  21. package/dist/cjs/request-policy/node.js +149 -0
  22. package/dist/cjs/request-policy/policyCore.js +266 -0
  23. package/dist/cjs/request-policy/requestContext.js +137 -0
  24. package/dist/cjs/request-policy/traceparent.js +56 -0
  25. package/dist/cjs/request-policy/transport.js +234 -0
  26. package/dist/cjs/request-policy/types.js +57 -0
  27. package/dist/cjs/routeHydration.js +127 -0
  28. package/dist/cjs/routerPrefetchPolicy.js +153 -0
  29. package/dist/cjs/routerProvider.js +105 -0
  30. package/dist/cjs/routerState.js +175 -0
  31. package/dist/cjs/routerStatePlugin.js +113 -0
  32. package/dist/cjs/routerStateTypes.js +18 -0
  33. package/dist/cjs/rsc-disabled/rscDisabledClientBrowserRuntime.js +69 -0
  34. package/dist/cjs/rsc-disabled/rscDisabledClientEdgeRuntime.js +61 -0
  35. package/dist/cjs/rsc-disabled/rscDisabledClientNodeRuntime.js +65 -0
  36. package/dist/cjs/rsc-disabled/rscDisabledRuntimeError.js +42 -0
  37. package/dist/cjs/rsc-disabled/rscDisabledServerEdgeRuntime.js +93 -0
  38. package/dist/cjs/rsc-disabled/rscDisabledServerNodeRuntime.js +101 -0
  39. package/dist/cjs/runtimeContextProjection.js +112 -0
  40. package/dist/cjs/server-config/bffRuntime.js +18 -0
  41. package/dist/cjs/server-config/index.js +18 -0
  42. package/dist/cjs/server-config/serverTelemetry.js +18 -0
  43. package/dist/cjs/shellStream.js +34 -2
  44. package/dist/cjs/workspaceEvents.js +159 -0
  45. package/dist/esm/buildIdentity.mjs +35 -0
  46. package/dist/esm/contextExtensions.mjs +33 -0
  47. package/dist/esm/headRuntime.mjs +43 -0
  48. package/dist/esm/helmetContext.mjs +13 -0
  49. package/dist/esm/index.mjs +2 -0
  50. package/dist/esm/localisedLoaderIdentity.mjs +13 -0
  51. package/dist/esm/localisedUrls/config.mjs +11 -0
  52. package/dist/esm/localisedUrls/index.mjs +8 -0
  53. package/dist/esm/localisedUrls/normalise.mjs +37 -0
  54. package/dist/esm/localisedUrls/pathname.mjs +24 -0
  55. package/dist/esm/localisedUrls/patterns.mjs +152 -0
  56. package/dist/esm/localisedUrls/redirect.mjs +53 -0
  57. package/dist/esm/localisedUrls/resolve.mjs +37 -0
  58. package/dist/esm/localisedUrls/routeIdentity.mjs +54 -0
  59. package/dist/esm/localisedUrls/routes.mjs +176 -0
  60. package/dist/esm/localisedUrls/types.mjs +0 -0
  61. package/dist/esm/prefetchPolicy.mjs +11 -0
  62. package/dist/esm/rendererHead.mjs +14 -5
  63. package/dist/esm/request-policy/browser.mjs +11 -0
  64. package/dist/esm/request-policy/client.mjs +195 -0
  65. package/dist/esm/request-policy/policyCore.mjs +171 -0
  66. package/dist/esm/request-policy/requestContext.mjs +75 -0
  67. package/dist/esm/request-policy/traceparent.mjs +18 -0
  68. package/dist/esm/request-policy/transport.mjs +196 -0
  69. package/dist/esm/request-policy/types.mjs +10 -0
  70. package/dist/esm/routeHydration.mjs +77 -0
  71. package/dist/esm/routerPrefetchPolicy.mjs +115 -0
  72. package/dist/esm/routerProvider.mjs +58 -0
  73. package/dist/esm/routerState.mjs +113 -0
  74. package/dist/esm/routerStatePlugin.mjs +75 -0
  75. package/dist/esm/routerStateTypes.mjs +0 -0
  76. package/dist/esm/rsc-disabled/rscDisabledClientBrowserRuntime.mjs +10 -0
  77. package/dist/esm/rsc-disabled/rscDisabledClientEdgeRuntime.mjs +8 -0
  78. package/dist/esm/rsc-disabled/rscDisabledClientNodeRuntime.mjs +9 -0
  79. package/dist/esm/rsc-disabled/rscDisabledRuntimeError.mjs +4 -0
  80. package/dist/esm/rsc-disabled/rscDisabledServerEdgeRuntime.mjs +16 -0
  81. package/dist/esm/rsc-disabled/rscDisabledServerNodeRuntime.mjs +18 -0
  82. package/dist/esm/runtimeContextProjection.mjs +74 -0
  83. package/dist/esm/server-config/bffRuntime.mjs +0 -0
  84. package/dist/esm/server-config/index.mjs +0 -0
  85. package/dist/esm/server-config/serverTelemetry.mjs +0 -0
  86. package/dist/esm/shellStream.mjs +34 -2
  87. package/dist/esm/workspaceEvents.mjs +66 -0
  88. package/dist/esm-node/buildIdentity.mjs +36 -0
  89. package/dist/esm-node/contextExtensions.mjs +34 -0
  90. package/dist/esm-node/headRuntime.mjs +44 -0
  91. package/dist/esm-node/helmetContext.mjs +14 -0
  92. package/dist/esm-node/index.mjs +2 -0
  93. package/dist/esm-node/localisedLoaderIdentity.mjs +14 -0
  94. package/dist/esm-node/localisedUrls/config.mjs +12 -0
  95. package/dist/esm-node/localisedUrls/index.mjs +9 -0
  96. package/dist/esm-node/localisedUrls/normalise.mjs +38 -0
  97. package/dist/esm-node/localisedUrls/pathname.mjs +25 -0
  98. package/dist/esm-node/localisedUrls/patterns.mjs +153 -0
  99. package/dist/esm-node/localisedUrls/redirect.mjs +54 -0
  100. package/dist/esm-node/localisedUrls/resolve.mjs +38 -0
  101. package/dist/esm-node/localisedUrls/routeIdentity.mjs +55 -0
  102. package/dist/esm-node/localisedUrls/routes.mjs +177 -0
  103. package/dist/esm-node/localisedUrls/types.mjs +1 -0
  104. package/dist/esm-node/prefetchPolicy.mjs +12 -0
  105. package/dist/esm-node/rendererHead.mjs +14 -5
  106. package/dist/esm-node/request-policy/browser.mjs +12 -0
  107. package/dist/esm-node/request-policy/client.mjs +196 -0
  108. package/dist/esm-node/request-policy/node.mjs +22 -0
  109. package/dist/esm-node/request-policy/policyCore.mjs +172 -0
  110. package/dist/esm-node/request-policy/requestContext.mjs +76 -0
  111. package/dist/esm-node/request-policy/traceparent.mjs +19 -0
  112. package/dist/esm-node/request-policy/transport.mjs +197 -0
  113. package/dist/esm-node/request-policy/types.mjs +11 -0
  114. package/dist/esm-node/routeHydration.mjs +78 -0
  115. package/dist/esm-node/routerPrefetchPolicy.mjs +116 -0
  116. package/dist/esm-node/routerProvider.mjs +59 -0
  117. package/dist/esm-node/routerState.mjs +114 -0
  118. package/dist/esm-node/routerStatePlugin.mjs +76 -0
  119. package/dist/esm-node/routerStateTypes.mjs +1 -0
  120. package/dist/esm-node/rsc-disabled/rscDisabledClientBrowserRuntime.mjs +11 -0
  121. package/dist/esm-node/rsc-disabled/rscDisabledClientEdgeRuntime.mjs +9 -0
  122. package/dist/esm-node/rsc-disabled/rscDisabledClientNodeRuntime.mjs +10 -0
  123. package/dist/esm-node/rsc-disabled/rscDisabledRuntimeError.mjs +5 -0
  124. package/dist/esm-node/rsc-disabled/rscDisabledServerEdgeRuntime.mjs +17 -0
  125. package/dist/esm-node/rsc-disabled/rscDisabledServerNodeRuntime.mjs +19 -0
  126. package/dist/esm-node/runtimeContextProjection.mjs +75 -0
  127. package/dist/esm-node/server-config/bffRuntime.mjs +1 -0
  128. package/dist/esm-node/server-config/index.mjs +1 -0
  129. package/dist/esm-node/server-config/serverTelemetry.mjs +1 -0
  130. package/dist/esm-node/shellStream.mjs +34 -2
  131. package/dist/esm-node/workspaceEvents.mjs +67 -0
  132. package/dist/types/buildIdentity.d.ts +37 -0
  133. package/dist/types/contextExtensions.d.ts +34 -0
  134. package/dist/types/headRuntime.d.ts +12 -0
  135. package/dist/types/helmetCompat.d.ts +1 -1
  136. package/dist/types/helmetContext.d.ts +10 -0
  137. package/dist/types/index.d.ts +2 -0
  138. package/dist/types/localisedLoaderIdentity.d.ts +10 -0
  139. package/dist/types/localisedUrls/config.d.ts +9 -0
  140. package/dist/types/localisedUrls/index.d.ts +10 -0
  141. package/dist/types/localisedUrls/normalise.d.ts +10 -0
  142. package/dist/types/localisedUrls/pathname.d.ts +4 -0
  143. package/dist/types/localisedUrls/patterns.d.ts +11 -0
  144. package/dist/types/localisedUrls/redirect.d.ts +19 -0
  145. package/dist/types/localisedUrls/resolve.d.ts +7 -0
  146. package/dist/types/localisedUrls/routeIdentity.d.ts +22 -0
  147. package/dist/types/localisedUrls/routes.d.ts +3 -0
  148. package/dist/types/localisedUrls/types.d.ts +14 -0
  149. package/dist/types/prefetchPolicy.d.ts +4 -0
  150. package/dist/types/rendererHead.d.ts +5 -3
  151. package/dist/types/request-policy/browser.d.ts +10 -0
  152. package/dist/types/request-policy/client.d.ts +14 -0
  153. package/dist/types/request-policy/node.d.ts +10 -0
  154. package/dist/types/request-policy/policyCore.d.ts +107 -0
  155. package/dist/types/request-policy/requestContext.d.ts +22 -0
  156. package/dist/types/request-policy/traceparent.d.ts +10 -0
  157. package/dist/types/request-policy/transport.d.ts +12 -0
  158. package/dist/types/request-policy/types.d.ts +144 -0
  159. package/dist/types/routeHydration.d.ts +24 -0
  160. package/dist/types/routerPrefetchPolicy.d.ts +28 -0
  161. package/dist/types/routerProvider.d.ts +55 -0
  162. package/dist/types/routerState.d.ts +17 -0
  163. package/dist/types/routerStatePlugin.d.ts +19 -0
  164. package/dist/types/routerStateTypes.d.ts +70 -0
  165. package/dist/types/rsc-disabled/rscDisabledClientBrowserRuntime.d.ts +9 -0
  166. package/dist/types/rsc-disabled/rscDisabledClientEdgeRuntime.d.ts +7 -0
  167. package/dist/types/rsc-disabled/rscDisabledClientNodeRuntime.d.ts +8 -0
  168. package/dist/types/rsc-disabled/rscDisabledRuntimeError.d.ts +1 -0
  169. package/dist/types/rsc-disabled/rscDisabledServerEdgeRuntime.d.ts +15 -0
  170. package/dist/types/rsc-disabled/rscDisabledServerNodeRuntime.d.ts +17 -0
  171. package/dist/types/runtimeContextProjection.d.ts +30 -0
  172. package/dist/types/server-config/bffRuntime.d.ts +122 -0
  173. package/dist/types/server-config/index.d.ts +2 -0
  174. package/dist/types/server-config/serverTelemetry.d.ts +322 -0
  175. package/dist/types/workspaceEvents.d.ts +62 -0
  176. package/package.json +240 -15
  177. package/rslib.config.mts +17 -2
  178. package/rstest.config.mts +33 -2
  179. package/src/buildIdentity.ts +61 -0
  180. package/src/contextExtensions.ts +81 -0
  181. package/src/headRuntime.tsx +77 -0
  182. package/src/helmetCompat.ts +1 -1
  183. package/src/helmetContext.ts +31 -0
  184. package/src/index.ts +2 -0
  185. package/src/localisedLoaderIdentity.ts +30 -0
  186. package/src/localisedUrls/config.ts +18 -0
  187. package/src/localisedUrls/index.ts +25 -0
  188. package/src/localisedUrls/normalise.ts +62 -0
  189. package/src/localisedUrls/pathname.ts +60 -0
  190. package/src/localisedUrls/patterns.ts +283 -0
  191. package/src/localisedUrls/redirect.ts +107 -0
  192. package/src/localisedUrls/resolve.ts +106 -0
  193. package/src/localisedUrls/routeIdentity.ts +83 -0
  194. package/src/localisedUrls/routes.ts +451 -0
  195. package/src/localisedUrls/types.ts +18 -0
  196. package/src/prefetchPolicy.ts +33 -0
  197. package/src/rendererHead.ts +25 -8
  198. package/src/request-policy/browser.ts +22 -0
  199. package/src/request-policy/client.ts +404 -0
  200. package/src/request-policy/node.ts +41 -0
  201. package/src/request-policy/policyCore.ts +424 -0
  202. package/src/request-policy/requestContext.ts +149 -0
  203. package/src/request-policy/traceparent.ts +43 -0
  204. package/src/request-policy/transport.ts +349 -0
  205. package/src/request-policy/types.ts +229 -0
  206. package/src/routeHydration.ts +208 -0
  207. package/src/routerPrefetchPolicy.ts +195 -0
  208. package/src/routerProvider.ts +160 -0
  209. package/src/routerState.ts +220 -0
  210. package/src/routerStatePlugin.ts +125 -0
  211. package/src/routerStateTypes.ts +76 -0
  212. package/src/rsc-disabled/rscDisabledClientBrowserRuntime.ts +10 -0
  213. package/src/rsc-disabled/rscDisabledClientEdgeRuntime.ts +8 -0
  214. package/src/rsc-disabled/rscDisabledClientNodeRuntime.ts +9 -0
  215. package/src/rsc-disabled/rscDisabledRuntimeError.ts +3 -0
  216. package/src/rsc-disabled/rscDisabledServerEdgeRuntime.ts +16 -0
  217. package/src/rsc-disabled/rscDisabledServerNodeRuntime.ts +18 -0
  218. package/src/runtimeContextProjection.ts +105 -0
  219. package/src/server-config/bffRuntime.ts +136 -0
  220. package/src/server-config/index.ts +2 -0
  221. package/src/server-config/serverTelemetry.ts +337 -0
  222. package/src/shellStream.ts +55 -25
  223. package/src/workspaceEvents.ts +324 -0
  224. package/tests/buildIdentity.test.ts +51 -0
  225. package/tests/contextExtensions.test.ts +27 -0
  226. package/tests/headRuntime.test.tsx +229 -0
  227. package/tests/localisedUrls.test.ts +560 -0
  228. package/tests/rendererHead.test.ts +52 -121
  229. package/tests/request-policy/browser-policy.test.ts +108 -0
  230. package/tests/request-policy/client-isolation.test.ts +109 -0
  231. package/tests/request-policy/node-policy.test.ts +447 -0
  232. package/tests/request-policy/policyCore.test.ts +129 -0
  233. package/tests/request-policy/requestContext.test.ts +66 -0
  234. package/tests/request-policy/requestFactory.outbound-contract.test.ts +290 -0
  235. package/tests/request-policy/traceparent.test.ts +52 -0
  236. package/tests/request-policy/transport-retry.test.ts +367 -0
  237. package/tests/routeHydration.test.ts +180 -0
  238. package/tests/routerPrefetchPolicy.test.ts +102 -0
  239. package/tests/routerProvider.test.ts +29 -0
  240. package/tests/routerState.test.ts +77 -0
  241. package/tests/routerStatePlugin.test.ts +63 -0
  242. package/tests/runtimeContextProjection.test.ts +83 -0
  243. package/tests/safeFailure.test.ts +10 -28
  244. package/tests/shellStream.test.ts +39 -2
  245. package/tests/workspaceEvents.test.ts +221 -0
  246. package/dist/cjs/rscHtmlStream.js +0 -128
  247. package/dist/esm/rscHtmlStream.mjs +0 -86
  248. package/dist/esm-node/rscHtmlStream.mjs +0 -87
  249. package/dist/types/rscHtmlStream.d.ts +0 -4
  250. package/src/rscHtmlStream.ts +0 -142
  251. package/tests/rscHtmlStream.test.ts +0 -153
@@ -0,0 +1,560 @@
1
+ import { describe, expect, test } from '@rstest/core';
2
+ import type { LocalisedRoute } from '../src/localisedUrls/index';
3
+ import {
4
+ applyLocalisedUrlsToRoutes,
5
+ canonicalTargetPathname,
6
+ localiseTargetPathname,
7
+ matchPathPattern,
8
+ resolveCanonicalLocalisedPath,
9
+ resolveLocalisedPath,
10
+ validateLocalisedUrls,
11
+ } from '../src/localisedUrls/index';
12
+
13
+ const createRoute = (
14
+ path: string,
15
+ children?: LocalisedRoute[],
16
+ ): LocalisedRoute => ({
17
+ id: path,
18
+ path,
19
+ type: 'nested',
20
+ origin: 'file-system',
21
+ routeType: children ? 'layout' : 'page',
22
+ _component: `${path}.tsx`,
23
+ children,
24
+ });
25
+
26
+ describe('localisedUrls', () => {
27
+ test('leaves framework-internal Module Federation routes unlocalised', () => {
28
+ const internalRoute = createRoute('_mf', [
29
+ createRoute('fragment', [createRoute('product-card')]),
30
+ ]);
31
+ const publicRoute = createRoute(':lang', [createRoute('products')]);
32
+ const routes = [internalRoute, publicRoute];
33
+
34
+ expect(() =>
35
+ validateLocalisedUrls(routes, ['en', 'cs'], {
36
+ '/products': { en: '/products', cs: '/produkty' },
37
+ }),
38
+ ).not.toThrow();
39
+ expect(
40
+ applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
41
+ '/products': { en: '/products', cs: '/produkty' },
42
+ })[0],
43
+ ).toEqual(internalRoute);
44
+ });
45
+
46
+ test('validates nested translations before producing canonical routes', () => {
47
+ const routes = [
48
+ createRoute(':lang', [createRoute('products', [createRoute(':slug?')])]),
49
+ ];
50
+ const map = {
51
+ '/products': { en: '/products', cs: '/produkty' },
52
+ '/products/:slug?': { en: '/products/:slug?', cs: '/wrong/:slug?' },
53
+ };
54
+ expect(() =>
55
+ applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], map, 'canonical'),
56
+ ).toThrow('must be nested under');
57
+ map['/products/:slug?'].cs = '/produkty/:slug?';
58
+ const canonical = applyLocalisedUrlsToRoutes(
59
+ routes,
60
+ ['en', 'cs'],
61
+ map,
62
+ 'canonical',
63
+ );
64
+ expect(canonical[0].children).toHaveLength(1);
65
+ expect(canonical[0].children?.[0].children).toMatchObject([
66
+ {
67
+ id: ':slug?',
68
+ path: ':slug?',
69
+ modernLocalisedRoute: { canonicalPath: '/products/:slug?' },
70
+ },
71
+ ]);
72
+ });
73
+
74
+ test('requires every localisable route path to define every language', () => {
75
+ const routes = [createRoute(':lang', [createRoute('terms-of-service')])];
76
+
77
+ expect(() =>
78
+ validateLocalisedUrls(routes, ['en', 'cs'], {
79
+ '/terms-of-service': {
80
+ en: '/terms-of-service',
81
+ },
82
+ }),
83
+ ).toThrow('missing languages: cs');
84
+ });
85
+
86
+ test('requires canonical and localised patterns to keep the same parameter signature', () => {
87
+ const routes = [createRoute('/:lang/products/:slug')];
88
+
89
+ expect(() =>
90
+ validateLocalisedUrls(routes, ['en', 'cs'], {
91
+ '/products/:slug': {
92
+ en: '/products/:slug',
93
+ cs: '/produkty/:produkt',
94
+ },
95
+ }),
96
+ ).toThrow(
97
+ 'localisedUrls["/products/:slug"].cs must use the same parameter signature',
98
+ );
99
+ });
100
+
101
+ test('allows locales to reorder the same named parameters', () => {
102
+ const routes = [createRoute('/:lang/catalog/:category/:slug')];
103
+ const localisedUrls = {
104
+ '/catalog/:category/:slug': {
105
+ en: '/catalog/:category/:slug',
106
+ cs: '/katalog/:slug/kategorie/:category',
107
+ },
108
+ };
109
+
110
+ expect(() =>
111
+ validateLocalisedUrls(routes, ['en', 'cs'], localisedUrls),
112
+ ).not.toThrow();
113
+ expect(
114
+ resolveLocalisedPath(
115
+ '/catalog/shoes/red',
116
+ 'cs',
117
+ ['en', 'cs'],
118
+ localisedUrls,
119
+ ),
120
+ ).toBe('/katalog/red/kategorie/shoes');
121
+ });
122
+
123
+ test('rejects reordering optional parameters whose position affects matching', () => {
124
+ const routes = [createRoute('/:lang/catalog/:category?/:slug?')];
125
+
126
+ expect(() =>
127
+ validateLocalisedUrls(routes, ['en', 'cs'], {
128
+ '/catalog/:category?/:slug?': {
129
+ en: '/catalog/:category?/:slug?',
130
+ cs: '/katalog/:slug?/:category?',
131
+ },
132
+ }),
133
+ ).toThrow('must keep optional and splat parameters in canonical order');
134
+ });
135
+
136
+ test('allows languages of one canonical route to share a physical alias', () => {
137
+ const routes = [createRoute('/:lang/about')];
138
+
139
+ expect(
140
+ applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
141
+ '/about': {
142
+ en: '/about',
143
+ cs: '/about',
144
+ },
145
+ }).map(route => route.path),
146
+ ).toEqual([':lang/about']);
147
+ });
148
+
149
+ test('rejects distinct canonical routes that generate the same physical path', () => {
150
+ const routes = [
151
+ createRoute('/:lang/products/:slug'),
152
+ createRoute('/:lang/articles/:article'),
153
+ ];
154
+
155
+ expect(() =>
156
+ validateLocalisedUrls(routes, ['en', 'cs'], {
157
+ '/products/:slug': {
158
+ en: '/catalog/:slug',
159
+ cs: '/produkty/:slug',
160
+ },
161
+ '/articles/:article': {
162
+ en: '/catalog/:article',
163
+ cs: '/clanky/:article',
164
+ },
165
+ }),
166
+ ).toThrow('generate the same physical route pattern');
167
+ });
168
+
169
+ test('rejects an alias owned by another route canonical pattern', () => {
170
+ const routes = [createRoute('/:lang/about'), createRoute('/:lang/company')];
171
+
172
+ expect(() =>
173
+ validateLocalisedUrls(routes, ['en', 'cs'], {
174
+ '/about': {
175
+ en: '/about-en',
176
+ cs: '/about-cs',
177
+ },
178
+ '/company': {
179
+ en: '/company-en',
180
+ cs: '/about',
181
+ },
182
+ }),
183
+ ).toThrow('generate the same physical route pattern "/about"');
184
+ });
185
+
186
+ test('rejects equal-specificity aliases that can match the same path', () => {
187
+ const routes = [
188
+ createRoute('/:lang/alpha/:slug'),
189
+ createRoute('/:lang/beta/:section'),
190
+ ];
191
+
192
+ expect(() =>
193
+ validateLocalisedUrls(routes, ['en', 'cs'], {
194
+ '/alpha/:slug': {
195
+ en: '/x/:slug',
196
+ cs: '/alfa/:slug',
197
+ },
198
+ '/beta/:section': {
199
+ en: '/:section/y',
200
+ cs: '/beta/:section',
201
+ },
202
+ }),
203
+ ).toThrow('overlapping route patterns');
204
+ });
205
+
206
+ test('disambiguates lossy route ID suffixes without rejecting valid URLs', () => {
207
+ const routes = [createRoute('/:lang/about')];
208
+ const localisedUrls = {
209
+ '/about': {
210
+ en: '/about',
211
+ cs: '/o.nas',
212
+ de: '/o_nas',
213
+ },
214
+ };
215
+
216
+ const localisedRoutes = applyLocalisedUrlsToRoutes(
217
+ routes,
218
+ ['en', 'cs', 'de'],
219
+ localisedUrls,
220
+ );
221
+
222
+ expect(localisedRoutes.map(route => route.path)).toEqual([
223
+ ':lang/about',
224
+ ':lang/o.nas',
225
+ ':lang/o_nas',
226
+ ]);
227
+ expect(new Set(localisedRoutes.map(route => route.id)).size).toBe(3);
228
+ });
229
+
230
+ test('rejects a generated route ID that collides with an existing route', () => {
231
+ const collidingRoute = {
232
+ ...createRoute('_mf/health'),
233
+ id: '/:lang/about__localised_lang_o_nas',
234
+ };
235
+
236
+ expect(() =>
237
+ applyLocalisedUrlsToRoutes(
238
+ [createRoute('/:lang/about'), collidingRoute],
239
+ ['en', 'cs'],
240
+ {
241
+ '/about': {
242
+ en: '/about',
243
+ cs: '/o.nas',
244
+ },
245
+ },
246
+ ),
247
+ ).toThrow('generated duplicate route ID');
248
+ });
249
+
250
+ test('rejects duplicate params and non-terminal splats', () => {
251
+ expect(() =>
252
+ validateLocalisedUrls(
253
+ [createRoute('/:lang/pairs/:id/:id')],
254
+ ['en', 'cs'],
255
+ {
256
+ '/pairs/:id/:id': {
257
+ en: '/pairs/:id/:id',
258
+ cs: '/dvojice/:id/:id',
259
+ },
260
+ },
261
+ ),
262
+ ).toThrow('duplicate path parameter ":id"');
263
+
264
+ expect(() =>
265
+ validateLocalisedUrls([createRoute('/:lang/docs/*/edit')], ['en', 'cs'], {
266
+ '/docs/*/edit': {
267
+ en: '/docs/*/edit',
268
+ cs: '/dokumenty/*/upravit',
269
+ },
270
+ }),
271
+ ).toThrow('splat parameter "*" must be the final segment');
272
+ });
273
+
274
+ test('expands route paths to localised aliases', () => {
275
+ const routes = [
276
+ createRoute(':lang', [
277
+ createRoute('terms-of-service'),
278
+ createRoute('products', [createRoute(':slug')]),
279
+ ]),
280
+ ];
281
+
282
+ const localisedRoutes = applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
283
+ '/terms-of-service': {
284
+ en: '/terms-of-service',
285
+ cs: '/podminky-pouzivani',
286
+ },
287
+ '/products': {
288
+ en: '/products',
289
+ cs: '/produkty',
290
+ },
291
+ '/products/:slug': {
292
+ en: '/products/:slug',
293
+ cs: '/produkty/:slug',
294
+ },
295
+ });
296
+
297
+ const localeRoute = localisedRoutes[0] as LocalisedRoute;
298
+ expect(localeRoute.children?.map(route => route.path)).toEqual([
299
+ 'terms-of-service',
300
+ 'podminky-pouzivani',
301
+ 'products',
302
+ 'produkty',
303
+ ]);
304
+
305
+ const productRoutes = localeRoute.children?.filter(
306
+ route => route.path === 'products' || route.path === 'produkty',
307
+ );
308
+ expect(productRoutes?.[0].children?.map(route => route.path)).toEqual([
309
+ ':slug',
310
+ ]);
311
+ expect(productRoutes?.[1].children?.map(route => route.path)).toEqual([
312
+ ':slug',
313
+ ]);
314
+ });
315
+
316
+ test('resolves optional route params', () => {
317
+ const localisedUrls = {
318
+ '/products/:slug?': {
319
+ en: '/products/:slug?',
320
+ cs: '/produkty/:slug?',
321
+ },
322
+ };
323
+
324
+ expect(
325
+ resolveLocalisedPath('/products', 'cs', ['en', 'cs'], localisedUrls),
326
+ ).toBe('/produkty');
327
+ expect(
328
+ resolveLocalisedPath(
329
+ '/produkty/cervena-bota',
330
+ 'en',
331
+ ['en', 'cs'],
332
+ localisedUrls,
333
+ ),
334
+ ).toBe('/products/cervena-bota');
335
+ });
336
+
337
+ test('empty splats round-trip and exact static routes keep precedence', () => {
338
+ expect(matchPathPattern('/docs', '/docs/*')).toEqual({ '*': '' });
339
+ expect(
340
+ resolveLocalisedPath('/docs', 'cs', ['en', 'cs'], {
341
+ '/docs/*': {
342
+ en: '/docs/*',
343
+ cs: '/dokumenty/*',
344
+ },
345
+ }),
346
+ ).toBe('/dokumenty');
347
+
348
+ expect(
349
+ resolveLocalisedPath('/docs', 'cs', ['en', 'cs'], {
350
+ '/docs/*': {
351
+ en: '/docs/*',
352
+ cs: '/dokumenty/*',
353
+ },
354
+ '/docs': {
355
+ en: '/docs',
356
+ cs: '/dokumenty-domu',
357
+ },
358
+ }),
359
+ ).toBe('/dokumenty-domu');
360
+ });
361
+
362
+ test('encodes wildcard params while preserving path separators', () => {
363
+ const localisedUrls = {
364
+ '/docs/*': {
365
+ en: '/docs/*',
366
+ cs: '/dokumenty/*',
367
+ },
368
+ };
369
+
370
+ expect(
371
+ resolveLocalisedPath(
372
+ '/docs/a%20b/c%23d',
373
+ 'cs',
374
+ ['en', 'cs'],
375
+ localisedUrls,
376
+ ),
377
+ ).toBe('/dokumenty/a%20b/c%23d');
378
+ expect(
379
+ resolveCanonicalLocalisedPath(
380
+ '/dokumenty/a%20b/c%23d',
381
+ ['en', 'cs'],
382
+ localisedUrls,
383
+ ),
384
+ ).toBe('/docs/a%20b/c%23d');
385
+ });
386
+
387
+ test('resolves static patterns before param patterns', () => {
388
+ const localisedUrls = {
389
+ '/products/:slug': {
390
+ en: '/products/:slug',
391
+ cs: '/produkty/:slug',
392
+ },
393
+ '/products/new': {
394
+ en: '/products/new',
395
+ cs: '/produkty/novinka',
396
+ },
397
+ };
398
+
399
+ expect(
400
+ resolveLocalisedPath('/products/new', 'cs', ['en', 'cs'], localisedUrls),
401
+ ).toBe('/produkty/novinka');
402
+ expect(
403
+ resolveCanonicalLocalisedPath(
404
+ '/produkty/novinka',
405
+ ['en', 'cs'],
406
+ localisedUrls,
407
+ ),
408
+ ).toBe('/products/new');
409
+ });
410
+
411
+ test('reverse resolution prefers the most specific localised source pattern', () => {
412
+ const localisedUrls = {
413
+ '/catalog/:section/:item/:detail': {
414
+ en: '/:section/:item/:detail',
415
+ cs: '/katalog/:section/:item/:detail',
416
+ },
417
+ '/:category/:slug': {
418
+ en: '/store/:category/:slug',
419
+ cs: '/obchod/:category/:slug',
420
+ },
421
+ };
422
+
423
+ expect(
424
+ resolveCanonicalLocalisedPath(
425
+ '/store/shoes/red',
426
+ ['en', 'cs'],
427
+ localisedUrls,
428
+ ),
429
+ ).toBe('/shoes/red');
430
+ });
431
+
432
+ test('missing required target params never collapse into empty segments', () => {
433
+ expect(() =>
434
+ resolveLocalisedPath('/products/red-shoe', 'cs', ['en', 'cs'], {
435
+ '/products/:slug': {
436
+ en: '/products/:slug',
437
+ cs: '/produkty/:product',
438
+ },
439
+ }),
440
+ ).toThrow('Missing required path parameter "product"');
441
+
442
+ expect(() =>
443
+ resolveLocalisedPath('/products/red-shoe', 'cs', ['en', 'cs'], {
444
+ '/products/:slug': {
445
+ en: '/products/:slug',
446
+ cs: '/produkty/:toString',
447
+ },
448
+ }),
449
+ ).toThrow('Missing required path parameter "toString"');
450
+ });
451
+
452
+ test('strips case-insensitive locale prefixes before relocalising full pathnames', () => {
453
+ const localisedUrls = {
454
+ '/products/:slug': {
455
+ en: '/products/:slug',
456
+ cs: '/produkty/:slug',
457
+ },
458
+ };
459
+
460
+ expect(
461
+ localiseTargetPathname(
462
+ '/CS/produkty/cervena-bota',
463
+ 'en',
464
+ ['en', 'cs'],
465
+ localisedUrls,
466
+ ),
467
+ ).toBe('/en/products/cervena-bota');
468
+ expect(
469
+ canonicalTargetPathname(
470
+ '/CS/produkty/cervena-bota',
471
+ ['en', 'cs'],
472
+ localisedUrls,
473
+ ),
474
+ ).toBe('/products/cervena-bota');
475
+ });
476
+
477
+ test('resolves nested optional route params with translated ancestors', () => {
478
+ const localisedUrls = {
479
+ '/checkout': {
480
+ en: '/checkout',
481
+ cs: '/pokladna',
482
+ },
483
+ '/checkout/thank-you': {
484
+ en: '/checkout/thank-you',
485
+ cs: '/pokladna/dekujeme',
486
+ },
487
+ '/checkout/thank-you/:orderId?': {
488
+ en: '/checkout/thank-you/:orderId?',
489
+ cs: '/pokladna/dekujeme/:orderId?',
490
+ },
491
+ };
492
+
493
+ expect(
494
+ resolveLocalisedPath(
495
+ '/checkout/thank-you',
496
+ 'cs',
497
+ ['en', 'cs'],
498
+ localisedUrls,
499
+ ),
500
+ ).toBe('/pokladna/dekujeme');
501
+ expect(
502
+ resolveLocalisedPath(
503
+ '/pokladna/dekujeme/ABC-123',
504
+ 'en',
505
+ ['en', 'cs'],
506
+ localisedUrls,
507
+ ),
508
+ ).toBe('/checkout/thank-you/ABC-123');
509
+ });
510
+ });
511
+
512
+ describe('matchPathPattern decoding', () => {
513
+ test('keeps prototype-named params as ordinary own values', () => {
514
+ const params = matchPathPattern('/products/red', '/products/:__proto__');
515
+
516
+ expect(params && Object.hasOwn(params, '__proto__')).toBe(true);
517
+ expect(
518
+ params && Object.getOwnPropertyDescriptor(params, '__proto__')?.value,
519
+ ).toBe('red');
520
+ });
521
+
522
+ test('decodes valid percent-encoded params', () => {
523
+ expect(
524
+ matchPathPattern('/produkty/%C4%8Derven%C3%A1', '/produkty/:slug'),
525
+ ).toEqual({ slug: 'červená' });
526
+ });
527
+
528
+ test('treats malformed percent-encoding as no match instead of throwing', () => {
529
+ expect(() =>
530
+ matchPathPattern('/produkty/%E0%A4%A', '/produkty/:slug'),
531
+ ).not.toThrow();
532
+ expect(
533
+ matchPathPattern('/produkty/%E0%A4%A', '/produkty/:slug'),
534
+ ).toBeNull();
535
+ });
536
+
537
+ test('keeps literal bracket segments in pathnames (no pattern rewrite)', () => {
538
+ expect(matchPathPattern('/produkty/[x]', '/produkty/:slug')).toEqual({
539
+ slug: '[x]',
540
+ });
541
+ });
542
+
543
+ test('resolveLocalisedPath returns malformed paths unchanged', () => {
544
+ const localisedUrls = {
545
+ '/products/:slug': {
546
+ en: '/products/:slug',
547
+ cs: '/produkty/:slug',
548
+ },
549
+ };
550
+
551
+ expect(
552
+ resolveLocalisedPath(
553
+ '/produkty/%E0%A4%A',
554
+ 'en',
555
+ ['en', 'cs'],
556
+ localisedUrls,
557
+ ),
558
+ ).toBe('/produkty/%E0%A4%A');
559
+ });
560
+ });