@bleedingdev/modern-js-i18n-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/index.js +10 -10
- package/dist/cjs/localeRedirectPolicy.js +6 -6
- package/dist/cjs/paths.js +3 -3
- package/dist/cjs/router-navigation/adapter.js +24 -166
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/localeRedirectPolicy.mjs +1 -1
- package/dist/esm/paths.mjs +1 -1
- package/dist/esm/router-navigation/adapter.mjs +26 -168
- package/dist/esm-node/index.mjs +1 -1
- package/dist/esm-node/localeRedirectPolicy.mjs +1 -1
- package/dist/esm-node/paths.mjs +1 -1
- package/dist/esm-node/router-navigation/adapter.mjs +26 -168
- package/dist/types/index.d.ts +1 -1
- package/dist/types/localeRedirectPolicy.d.ts +1 -1
- package/dist/types/paths.d.ts +1 -1
- package/dist/types/router-navigation/adapter.d.ts +7 -55
- package/package.json +9 -9
- package/src/index.ts +1 -1
- package/src/localeRedirectPolicy.ts +2 -2
- package/src/paths.ts +2 -2
- package/src/router-navigation/adapter.tsx +62 -478
- package/tests/routerNavigation.test.tsx +80 -0
- package/dist/cjs/localisedUrls/config.js +0 -49
- package/dist/cjs/localisedUrls/index.js +0 -98
- package/dist/cjs/localisedUrls/normalise.js +0 -87
- package/dist/cjs/localisedUrls/pathname.js +0 -68
- package/dist/cjs/localisedUrls/patterns.js +0 -214
- package/dist/cjs/localisedUrls/redirect.js +0 -100
- package/dist/cjs/localisedUrls/resolve.js +0 -78
- package/dist/cjs/localisedUrls/routeIdentity.js +0 -118
- package/dist/cjs/localisedUrls/routes.js +0 -210
- package/dist/cjs/localisedUrls/types.js +0 -18
- package/dist/esm/localisedUrls/config.mjs +0 -11
- package/dist/esm/localisedUrls/index.mjs +0 -8
- package/dist/esm/localisedUrls/normalise.mjs +0 -37
- package/dist/esm/localisedUrls/pathname.mjs +0 -24
- package/dist/esm/localisedUrls/patterns.mjs +0 -152
- package/dist/esm/localisedUrls/redirect.mjs +0 -53
- package/dist/esm/localisedUrls/resolve.mjs +0 -37
- package/dist/esm/localisedUrls/routeIdentity.mjs +0 -77
- package/dist/esm/localisedUrls/routes.mjs +0 -169
- package/dist/esm/localisedUrls/types.mjs +0 -0
- package/dist/esm-node/localisedUrls/config.mjs +0 -12
- package/dist/esm-node/localisedUrls/index.mjs +0 -9
- package/dist/esm-node/localisedUrls/normalise.mjs +0 -38
- package/dist/esm-node/localisedUrls/pathname.mjs +0 -25
- package/dist/esm-node/localisedUrls/patterns.mjs +0 -153
- package/dist/esm-node/localisedUrls/redirect.mjs +0 -54
- package/dist/esm-node/localisedUrls/resolve.mjs +0 -38
- package/dist/esm-node/localisedUrls/routeIdentity.mjs +0 -78
- package/dist/esm-node/localisedUrls/routes.mjs +0 -170
- package/dist/esm-node/localisedUrls/types.mjs +0 -1
- package/dist/types/localisedUrls/config.d.ts +0 -9
- package/dist/types/localisedUrls/index.d.ts +0 -10
- package/dist/types/localisedUrls/normalise.d.ts +0 -10
- package/dist/types/localisedUrls/pathname.d.ts +0 -4
- package/dist/types/localisedUrls/patterns.d.ts +0 -11
- package/dist/types/localisedUrls/redirect.d.ts +0 -19
- package/dist/types/localisedUrls/resolve.d.ts +0 -7
- package/dist/types/localisedUrls/routeIdentity.d.ts +0 -24
- package/dist/types/localisedUrls/routes.d.ts +0 -3
- package/dist/types/localisedUrls/types.d.ts +0 -14
- package/src/localisedUrls/config.ts +0 -18
- package/src/localisedUrls/index.ts +0 -28
- package/src/localisedUrls/normalise.ts +0 -62
- package/src/localisedUrls/pathname.ts +0 -60
- package/src/localisedUrls/patterns.ts +0 -283
- package/src/localisedUrls/redirect.ts +0 -107
- package/src/localisedUrls/resolve.ts +0 -106
- package/src/localisedUrls/routeIdentity.ts +0 -107
- package/src/localisedUrls/routes.ts +0 -432
- package/src/localisedUrls/types.ts +0 -18
- package/tests/localisedUrls.test.ts +0 -532
|
@@ -1,40 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
InternalRuntimeContext,
|
|
4
|
-
type TInternalRuntimeContext,
|
|
5
|
-
type TRuntimeContext,
|
|
6
|
-
} from '@modern-js/runtime/context';
|
|
1
|
+
import { RuntimeContext } from '@modern-js/runtime';
|
|
2
|
+
import { InternalRuntimeContext } from '@modern-js/runtime/context';
|
|
7
3
|
import {
|
|
8
4
|
getRouterRuntimeState,
|
|
5
|
+
type RouterLinkTarget,
|
|
6
|
+
type RouterNavigationCapability,
|
|
7
|
+
type RouterNavigationSnapshot,
|
|
9
8
|
subscribeRouterRuntimeState,
|
|
10
9
|
} from '@modern-js/runtime-extensions/router-state';
|
|
11
10
|
import type React from 'react';
|
|
12
11
|
import { useCallback, useContext, useSyncExternalStore } from 'react';
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
* Fork-owned: the selected-router navigation adapter.
|
|
16
|
-
*
|
|
17
|
-
* `useNativeI18nRouterAdapter` only knows react-router's own hooks, so a bare
|
|
18
|
-
* `appTools() + i18nPlugin()` app running the TanStack router would fall back
|
|
19
|
-
* to plain `<a>` links and a non-reactive `window.location`. That breaks
|
|
20
|
-
* language-invariant active state and, worse, leaves the i18n language stuck
|
|
21
|
-
* after a client-side navigation to a localized URL.
|
|
22
|
-
*
|
|
23
|
-
* The subsystem lives in this fork-owned package rather than inside
|
|
24
|
-
* upstream-owned `@modern-js/plugin-i18n`: that package only wires it in
|
|
25
|
-
* through its existing `NavigationProvider` extension point, so the upstream
|
|
26
|
-
* package keeps a seam instead of gaining a fork subsystem. The primitives the
|
|
27
|
-
* adapter composes with - the navigation context provider, the native
|
|
28
|
-
* react-router adapter and the adapter shape - are injected by that seam, so
|
|
29
|
-
* nothing here imports `@modern-js/plugin-i18n` and no dependency cycle is
|
|
30
|
-
* created.
|
|
31
|
-
*
|
|
32
|
-
* The adapter reads the actual selected router out of the fork-owned router
|
|
33
|
-
* runtime-state slot, so it serves both routers without either plugin having
|
|
34
|
-
* to know about i18n.
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
/** The primitives the host plugin injects; see `createI18nRouterNavigation`. */
|
|
38
13
|
export interface I18nRouterNavigationDependencies<
|
|
39
14
|
Adapter extends I18nRouterAdapterShape,
|
|
40
15
|
> {
|
|
@@ -44,491 +19,100 @@ export interface I18nRouterNavigationDependencies<
|
|
|
44
19
|
useNativeI18nRouterAdapter: () => Adapter;
|
|
45
20
|
}
|
|
46
21
|
|
|
47
|
-
export
|
|
48
|
-
pathname: string;
|
|
49
|
-
href: string;
|
|
50
|
-
search?: Record<string, unknown>;
|
|
51
|
-
hash?: string;
|
|
52
|
-
hashScrollIntoView?: boolean | ScrollIntoViewOptions;
|
|
53
|
-
prefetch?: 'intent' | 'render' | 'viewport' | 'none';
|
|
54
|
-
preload?: unknown;
|
|
55
|
-
}
|
|
22
|
+
export type I18nRouterLinkTarget = RouterLinkTarget;
|
|
56
23
|
|
|
57
24
|
export interface I18nRouterAdapterShape {
|
|
58
25
|
framework?: string;
|
|
59
26
|
hasRouter: boolean;
|
|
60
|
-
location:
|
|
61
|
-
navigate:
|
|
62
|
-
|
|
63
|
-
href: string,
|
|
64
|
-
options?: { replace?: boolean; state?: unknown },
|
|
65
|
-
) => void | Promise<void>)
|
|
66
|
-
| null;
|
|
67
|
-
Link: React.ComponentType<{
|
|
68
|
-
to: string;
|
|
69
|
-
children?: React.ReactNode;
|
|
70
|
-
[key: string]: unknown;
|
|
71
|
-
}> | null;
|
|
27
|
+
location: RouterNavigationSnapshot['location'] | null;
|
|
28
|
+
navigate: RouterNavigationCapability['navigate'] | null;
|
|
29
|
+
Link: RouterNavigationCapability['Link'] | null;
|
|
72
30
|
params: Record<string, string>;
|
|
73
|
-
createLinkProps?:
|
|
74
|
-
to: string;
|
|
75
|
-
[key: string]: unknown;
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
type I18nRouterFramework = 'react-router' | 'tanstack' | string;
|
|
80
|
-
|
|
81
|
-
interface I18nRouterLocation {
|
|
82
|
-
pathname: string;
|
|
83
|
-
search: string;
|
|
84
|
-
hash: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface I18nRouterNavigateOptions {
|
|
88
|
-
replace?: boolean;
|
|
89
|
-
state?: unknown;
|
|
31
|
+
createLinkProps?: RouterNavigationCapability['createLinkProps'];
|
|
90
32
|
}
|
|
91
33
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
options?: I18nRouterNavigateOptions,
|
|
95
|
-
) => void | Promise<void>;
|
|
96
|
-
|
|
97
|
-
type I18nRouterLink = React.ComponentType<{
|
|
98
|
-
to: string;
|
|
99
|
-
children?: React.ReactNode;
|
|
100
|
-
[key: string]: unknown;
|
|
101
|
-
}>;
|
|
102
|
-
|
|
103
|
-
type RuntimeContextWithRouter = TRuntimeContext & {
|
|
104
|
-
router?: {
|
|
105
|
-
useInRouterContext?: () => boolean;
|
|
106
|
-
useRouter?: (options?: { warn?: boolean }) => unknown;
|
|
107
|
-
useLocation?: () => unknown;
|
|
108
|
-
useNavigate?: () => I18nRouterNavigate;
|
|
109
|
-
useParams?: () => Record<string, string>;
|
|
110
|
-
useHref?: () => unknown;
|
|
111
|
-
Link?: I18nRouterLink;
|
|
112
|
-
};
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
type InternalRuntimeContextWithRouter = TInternalRuntimeContext & {
|
|
116
|
-
router?: RuntimeContextWithRouter['router'];
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
type RouterInstance = {
|
|
120
|
-
navigate?: (...args: any[]) => unknown;
|
|
121
|
-
state?: {
|
|
122
|
-
location?: unknown;
|
|
123
|
-
matches?: Array<{ params?: Record<string, string> }>;
|
|
124
|
-
};
|
|
125
|
-
stores?: {
|
|
126
|
-
location?: {
|
|
127
|
-
get?: () => unknown;
|
|
128
|
-
subscribe?: (listener: () => void) => () => void;
|
|
129
|
-
};
|
|
130
|
-
matches?: {
|
|
131
|
-
get?: () => Array<{ params?: Record<string, string> }>;
|
|
132
|
-
};
|
|
133
|
-
};
|
|
134
|
-
subscribe?:
|
|
135
|
-
| ((listener: () => void) => () => void)
|
|
136
|
-
| ((eventType: string, listener: () => void) => () => void);
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const normalizeUrlPart = (value: unknown, prefix: '?' | '#'): string => {
|
|
140
|
-
if (typeof value !== 'string' || !value) {
|
|
141
|
-
return '';
|
|
142
|
-
}
|
|
143
|
-
return value.startsWith(prefix) ? value : `${prefix}${value}`;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const normalizeLocation = (location: unknown): I18nRouterLocation | null => {
|
|
147
|
-
if (!location || typeof location !== 'object') {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const locationValue = location as {
|
|
152
|
-
pathname?: unknown;
|
|
153
|
-
search?: unknown;
|
|
154
|
-
searchStr?: unknown;
|
|
155
|
-
hash?: unknown;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
if (typeof locationValue.pathname !== 'string') {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
pathname: locationValue.pathname,
|
|
164
|
-
search: normalizeUrlPart(
|
|
165
|
-
typeof locationValue.search === 'string'
|
|
166
|
-
? locationValue.search
|
|
167
|
-
: locationValue.searchStr,
|
|
168
|
-
'?',
|
|
169
|
-
),
|
|
170
|
-
hash: normalizeUrlPart(locationValue.hash, '#'),
|
|
171
|
-
};
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const getWindowLocation = (): I18nRouterLocation | null => {
|
|
175
|
-
if (!isBrowser()) {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
pathname: window.location.pathname,
|
|
181
|
-
search: window.location.search,
|
|
182
|
-
hash: window.location.hash,
|
|
183
|
-
};
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const getRouterFramework = (
|
|
187
|
-
runtimeContext: RuntimeContextWithRouter,
|
|
188
|
-
internalContext: InternalRuntimeContextWithRouter,
|
|
189
|
-
inReactRouter: boolean,
|
|
190
|
-
): I18nRouterFramework | undefined => {
|
|
191
|
-
const framework =
|
|
192
|
-
getRouterRuntimeState(internalContext)?.framework ||
|
|
193
|
-
getRouterRuntimeState(runtimeContext)?.framework;
|
|
194
|
-
|
|
195
|
-
if (framework) {
|
|
196
|
-
return framework;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) {
|
|
200
|
-
return 'tanstack';
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
internalContext.router?.useLocation ||
|
|
205
|
-
internalContext.router?.useHref ||
|
|
206
|
-
runtimeContext.router?.useLocation ||
|
|
207
|
-
runtimeContext.router?.useHref
|
|
208
|
-
) {
|
|
209
|
-
return 'react-router';
|
|
210
|
-
}
|
|
34
|
+
const subscribeNone = () => () => {};
|
|
35
|
+
const getEmptySnapshot = () => null;
|
|
211
36
|
|
|
212
|
-
|
|
213
|
-
return 'react-router';
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return undefined;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const getRouterInstance = (
|
|
220
|
-
internalContext: InternalRuntimeContextWithRouter,
|
|
221
|
-
contextRouter?: RouterInstance | null,
|
|
222
|
-
): RouterInstance | null => {
|
|
223
|
-
if (contextRouter) {
|
|
224
|
-
return contextRouter;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const router = getRouterRuntimeState(internalContext)?.instance;
|
|
228
|
-
if (!router || typeof router !== 'object') {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
return router as RouterInstance;
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const getRouterStateLocation = (
|
|
235
|
-
internalContext: InternalRuntimeContextWithRouter,
|
|
236
|
-
contextRouter?: RouterInstance | null,
|
|
237
|
-
): I18nRouterLocation | null => {
|
|
238
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
239
|
-
return (
|
|
240
|
-
normalizeLocation(router?.stores?.location?.get?.()) ||
|
|
241
|
-
normalizeLocation(router?.state?.location)
|
|
242
|
-
);
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const getRouterParams = (
|
|
246
|
-
internalContext: InternalRuntimeContextWithRouter,
|
|
247
|
-
contextRouter?: RouterInstance | null,
|
|
248
|
-
): Record<string, string> => {
|
|
249
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
250
|
-
const matches = router?.stores?.matches?.get?.() || router?.state?.matches;
|
|
251
|
-
if (!Array.isArray(matches)) {
|
|
252
|
-
return {};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return matches.reduce<Record<string, string>>((params, match) => {
|
|
256
|
-
if (match?.params) {
|
|
257
|
-
Object.assign(params, match.params);
|
|
258
|
-
}
|
|
259
|
-
return params;
|
|
260
|
-
}, {});
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const getRouterSnapshot = (
|
|
264
|
-
internalContext: InternalRuntimeContextWithRouter,
|
|
265
|
-
contextRouter?: RouterInstance | null,
|
|
266
|
-
) => {
|
|
267
|
-
const location = getRouterStateLocation(internalContext, contextRouter);
|
|
268
|
-
const params = getRouterParams(internalContext, contextRouter);
|
|
269
|
-
return JSON.stringify([
|
|
270
|
-
location?.pathname ?? '',
|
|
271
|
-
location?.search ?? '',
|
|
272
|
-
location?.hash ?? '',
|
|
273
|
-
Object.entries(params).sort(([left], [right]) => left.localeCompare(right)),
|
|
274
|
-
]);
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Build the selected-router navigation hook and provider from the host
|
|
279
|
-
* plugin's own primitives. Injecting them is what keeps this fork-owned
|
|
280
|
-
* subsystem free of any import edge back into the plugin that installs it.
|
|
281
|
-
*/
|
|
37
|
+
/** The router provider owns navigation; this seam only observes its capability. */
|
|
282
38
|
export const createI18nRouterNavigation = <
|
|
283
39
|
Adapter extends I18nRouterAdapterShape,
|
|
284
40
|
>({
|
|
285
41
|
I18nNavigationProvider,
|
|
286
42
|
useNativeI18nRouterAdapter,
|
|
287
43
|
}: I18nRouterNavigationDependencies<Adapter>) => {
|
|
44
|
+
// The injected context accepts null to preserve native-router fallback.
|
|
45
|
+
const NavigationProvider = I18nNavigationProvider as React.ComponentType<
|
|
46
|
+
React.PropsWithChildren<{ value: Adapter | null }>
|
|
47
|
+
>;
|
|
288
48
|
const useIntegratedRouterAdapter = (): Adapter => {
|
|
289
|
-
const runtimeContext = useContext(
|
|
290
|
-
|
|
291
|
-
) as RuntimeContextWithRouter;
|
|
292
|
-
const internalContext = useContext(
|
|
293
|
-
InternalRuntimeContext,
|
|
294
|
-
) as InternalRuntimeContextWithRouter;
|
|
295
|
-
const routerApi = internalContext.router || runtimeContext.router;
|
|
49
|
+
const runtimeContext = useContext(RuntimeContext);
|
|
50
|
+
const internalContext = useContext(InternalRuntimeContext);
|
|
296
51
|
const native = useNativeI18nRouterAdapter();
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
runtimeContext,
|
|
303
|
-
internalContext,
|
|
304
|
-
inReactRouter,
|
|
52
|
+
const getCapability = useCallback(
|
|
53
|
+
() =>
|
|
54
|
+
getRouterRuntimeState(internalContext)?.navigation ??
|
|
55
|
+
getRouterRuntimeState(runtimeContext)?.navigation,
|
|
56
|
+
[internalContext, runtimeContext],
|
|
305
57
|
);
|
|
306
|
-
const
|
|
307
|
-
!inReactRouter && framework === 'tanstack'
|
|
308
|
-
? internalContext.router?.useRouter || runtimeContext.router?.useRouter
|
|
309
|
-
: undefined;
|
|
310
|
-
const contextRouter = contextUseRouter
|
|
311
|
-
? (contextUseRouter({ warn: false }) as RouterInstance | null)
|
|
312
|
-
: null;
|
|
313
|
-
// `hasRouter` means "this adapter can actually drive a router", not "a
|
|
314
|
-
// router framework was named". Two things can drive one: a live react-router
|
|
315
|
-
// context we are rendering inside, or an instance published in the
|
|
316
|
-
// runtime-state slot. A framework name alone is neither.
|
|
317
|
-
//
|
|
318
|
-
// This matters because router providers publish their name before their
|
|
319
|
-
// instance. `plugin-tanstack` publishes its hooks in `onBeforeRender` and
|
|
320
|
-
// only installs the instance when `RouterWrapper` renders - below anything
|
|
321
|
-
// wrapping the app, this provider included - so on a CSR boot the first
|
|
322
|
-
// evaluation here sees a name and no instance. Claiming a router there hands
|
|
323
|
-
// `changeLanguage()` a `navigate` that can only throw, instead of letting it
|
|
324
|
-
// fall back to a full-page load.
|
|
325
|
-
const slotRouter = getRouterInstance(internalContext, contextRouter);
|
|
326
|
-
// A slot instance counts only when `navigate` below can drive it: a named
|
|
327
|
-
// framework this adapter implements, with a navigate function. An unknown
|
|
328
|
-
// provider's instance would otherwise hand `changeLanguage()` a navigate
|
|
329
|
-
// that goes nowhere instead of the routerless full-page fallback.
|
|
330
|
-
const drivableSlotRouter =
|
|
331
|
-
(framework === 'tanstack' || framework === 'react-router') &&
|
|
332
|
-
typeof slotRouter?.navigate === 'function';
|
|
333
|
-
const hasRouter = Boolean(reactRouterNavigate) || drivableSlotRouter;
|
|
334
|
-
|
|
335
|
-
const subscribeToRouter = useCallback(
|
|
58
|
+
const subscribeCapability = useCallback(
|
|
336
59
|
(update: () => void) => {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
// The instance can arrive after this provider has already rendered, so
|
|
340
|
-
// watch the slot itself: without this the store subscription below would
|
|
341
|
-
// be a permanent no-op on a CSR boot and the adapter would never notice
|
|
342
|
-
// the router it is meant to drive.
|
|
343
|
-
const unsubscribeFromSlot = subscribeRouterRuntimeState(
|
|
60
|
+
const stopInternal = subscribeRouterRuntimeState(
|
|
344
61
|
internalContext,
|
|
345
62
|
update,
|
|
346
63
|
);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
352
|
-
if (!router) {
|
|
353
|
-
return () => {
|
|
354
|
-
for (const unsubscribe of unsubscribers) {
|
|
355
|
-
unsubscribe();
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
framework === 'react-router' &&
|
|
362
|
-
!inReactRouter &&
|
|
363
|
-
typeof router.subscribe === 'function'
|
|
364
|
-
) {
|
|
365
|
-
const subscribe = router.subscribe as (
|
|
366
|
-
this: RouterInstance,
|
|
367
|
-
listener: () => void,
|
|
368
|
-
) => () => void;
|
|
369
|
-
const unsubscribe = subscribe.call(router, update);
|
|
370
|
-
if (typeof unsubscribe === 'function') {
|
|
371
|
-
unsubscribers.push(unsubscribe);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (
|
|
376
|
-
framework === 'tanstack' &&
|
|
377
|
-
typeof router.stores?.location?.subscribe === 'function'
|
|
378
|
-
) {
|
|
379
|
-
const unsubscribe = router.stores.location.subscribe(update);
|
|
380
|
-
if (typeof unsubscribe === 'function') {
|
|
381
|
-
unsubscribers.push(unsubscribe);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (
|
|
386
|
-
framework === 'tanstack' &&
|
|
387
|
-
typeof router.subscribe === 'function'
|
|
388
|
-
) {
|
|
389
|
-
const subscribe = router.subscribe as (
|
|
390
|
-
this: RouterInstance,
|
|
391
|
-
eventType: string,
|
|
392
|
-
listener: () => void,
|
|
393
|
-
) => () => void;
|
|
394
|
-
for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
|
|
395
|
-
const unsubscribe = subscribe.call(router, eventType, update);
|
|
396
|
-
if (typeof unsubscribe === 'function') {
|
|
397
|
-
unsubscribers.push(unsubscribe);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
64
|
+
const stopPublic =
|
|
65
|
+
internalContext === runtimeContext
|
|
66
|
+
? undefined
|
|
67
|
+
: subscribeRouterRuntimeState(runtimeContext, update);
|
|
402
68
|
return () => {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
69
|
+
stopInternal();
|
|
70
|
+
stopPublic?.();
|
|
406
71
|
};
|
|
407
72
|
},
|
|
408
|
-
[
|
|
73
|
+
[internalContext, runtimeContext],
|
|
409
74
|
);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
75
|
+
// Separate subscriptions handle both delayed installation/replacement and
|
|
76
|
+
// navigation inside the currently selected provider, without hook guessing.
|
|
77
|
+
const capability = useSyncExternalStore(
|
|
78
|
+
subscribeCapability,
|
|
79
|
+
getCapability,
|
|
80
|
+
getCapability,
|
|
413
81
|
);
|
|
414
|
-
useSyncExternalStore(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
419
|
-
const activeFramework = getRouterFramework(
|
|
420
|
-
runtimeContext,
|
|
421
|
-
internalContext,
|
|
422
|
-
inReactRouter,
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
if (activeFramework === 'tanstack') {
|
|
426
|
-
if (typeof router?.navigate === 'function') {
|
|
427
|
-
return router.navigate({
|
|
428
|
-
to: href,
|
|
429
|
-
replace: options?.replace,
|
|
430
|
-
...(options?.state === undefined ? {} : { state: options.state }),
|
|
431
|
-
}) as void | Promise<void>;
|
|
432
|
-
}
|
|
433
|
-
throw new Error('TanStack router instance is not available.');
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (reactRouterNavigate) {
|
|
437
|
-
return reactRouterNavigate(href, options);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (activeFramework === 'react-router') {
|
|
441
|
-
if (typeof router?.navigate === 'function') {
|
|
442
|
-
return router.navigate(href, options) as void | Promise<void>;
|
|
443
|
-
}
|
|
444
|
-
throw new Error('React Router instance is not available.');
|
|
445
|
-
}
|
|
446
|
-
},
|
|
447
|
-
[
|
|
448
|
-
contextRouter,
|
|
449
|
-
internalContext,
|
|
450
|
-
inReactRouter,
|
|
451
|
-
reactRouterNavigate,
|
|
452
|
-
runtimeContext,
|
|
453
|
-
],
|
|
82
|
+
const snapshot = useSyncExternalStore(
|
|
83
|
+
capability?.subscribe ?? subscribeNone,
|
|
84
|
+
capability?.getSnapshot ?? getEmptySnapshot,
|
|
85
|
+
capability?.getSnapshot ?? getEmptySnapshot,
|
|
454
86
|
);
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
: getRouterStateLocation(internalContext, contextRouter)) ||
|
|
460
|
-
getWindowLocation();
|
|
461
|
-
const params = inReactRouter
|
|
462
|
-
? (reactRouterParams as Record<string, string>)
|
|
463
|
-
: getRouterParams(internalContext, contextRouter);
|
|
464
|
-
const Link =
|
|
465
|
-
framework === 'tanstack'
|
|
466
|
-
? internalContext.router?.Link || runtimeContext.router?.Link || null
|
|
467
|
-
: framework === 'react-router' || inReactRouter
|
|
468
|
-
? (routerApi?.Link ?? null)
|
|
469
|
-
: null;
|
|
470
|
-
|
|
87
|
+
if (native.hasRouter || !capability || !snapshot) {
|
|
88
|
+
return native;
|
|
89
|
+
}
|
|
90
|
+
const internalState = getRouterRuntimeState(internalContext);
|
|
471
91
|
return {
|
|
472
|
-
framework
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
92
|
+
framework:
|
|
93
|
+
internalState?.navigation === capability
|
|
94
|
+
? internalState.framework
|
|
95
|
+
: getRouterRuntimeState(runtimeContext)?.framework,
|
|
96
|
+
hasRouter: true,
|
|
97
|
+
...snapshot,
|
|
98
|
+
navigate: capability.navigate,
|
|
99
|
+
Link: capability.Link,
|
|
100
|
+
createLinkProps: capability.createLinkProps,
|
|
481
101
|
} as Adapter;
|
|
482
102
|
};
|
|
483
103
|
|
|
484
|
-
const createTanstackLinkProps = (target: I18nRouterLinkTarget) => {
|
|
485
|
-
const preload =
|
|
486
|
-
target.preload !== undefined
|
|
487
|
-
? target.preload
|
|
488
|
-
: target.prefetch === 'none'
|
|
489
|
-
? false
|
|
490
|
-
: target.prefetch;
|
|
491
|
-
return {
|
|
492
|
-
to: target.pathname,
|
|
493
|
-
...(target.search ? { search: target.search } : {}),
|
|
494
|
-
...(target.hash ? { hash: target.hash } : {}),
|
|
495
|
-
...(target.hashScrollIntoView === undefined
|
|
496
|
-
? {}
|
|
497
|
-
: { hashScrollIntoView: target.hashScrollIntoView }),
|
|
498
|
-
...(preload === undefined ? {} : { preload }),
|
|
499
|
-
};
|
|
500
|
-
};
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Supplies the actual selected router; native links still render its Link.
|
|
504
|
-
*
|
|
505
|
-
* This provider sits above the app, so it sits above the router element too.
|
|
506
|
-
* That is fine for a router this adapter reaches through the fork-owned
|
|
507
|
-
* runtime-state slot - TanStack hands out its instance, not a React context -
|
|
508
|
-
* but react-router publishes its location and navigate through context that
|
|
509
|
-
* only exists *below* the router. Reading it from up here yields an inert
|
|
510
|
-
* adapter, and publishing that would shadow the native one a consumer
|
|
511
|
-
* resolves at its own position, where the context is live. So only take over
|
|
512
|
-
* for the router reached through the slot, and only once its instance is
|
|
513
|
-
* actually installed; until then `useI18nRouterAdapter` falls back to the
|
|
514
|
-
* native adapter and `changeLanguage()` keeps its full-page behaviour.
|
|
515
|
-
*/
|
|
516
104
|
const I18nRouterNavigationProvider = ({
|
|
517
105
|
children,
|
|
518
106
|
}: React.PropsWithChildren) => {
|
|
519
107
|
const value = useIntegratedRouterAdapter();
|
|
520
108
|
const native = useNativeI18nRouterAdapter();
|
|
521
|
-
//
|
|
522
|
-
// live react-router context the native adapter - evaluated at the
|
|
523
|
-
// consumer's own position, where that context exists - is already the
|
|
524
|
-
// right answer, and shadowing it with one read from above the router
|
|
525
|
-
// yields an inert adapter. With no router to drive at all, staying absent
|
|
526
|
-
// is what preserves the full-page `changeLanguage()` fallback.
|
|
527
|
-
if (!value.hasRouter || native.hasRouter) {
|
|
528
|
-
return <>{children}</>;
|
|
529
|
-
}
|
|
109
|
+
// Keep the router subtree mounted when a capability arrives or disappears.
|
|
530
110
|
return (
|
|
531
|
-
<
|
|
111
|
+
<NavigationProvider
|
|
112
|
+
value={value.hasRouter && !native.hasRouter ? value : null}
|
|
113
|
+
>
|
|
114
|
+
{children}
|
|
115
|
+
</NavigationProvider>
|
|
532
116
|
);
|
|
533
117
|
};
|
|
534
118
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InternalRuntimeContext,
|
|
3
|
+
RuntimeContext,
|
|
4
|
+
} from '@modern-js/runtime/context';
|
|
5
|
+
import {
|
|
6
|
+
applyRouterRuntimeState,
|
|
7
|
+
type RouterNavigationCapability,
|
|
8
|
+
} from '@modern-js/runtime-extensions/router-state';
|
|
9
|
+
import React, { act, createContext, useContext, useEffect } from 'react';
|
|
10
|
+
import { createRoot } from 'react-dom/client';
|
|
11
|
+
import {
|
|
12
|
+
createI18nRouterNavigation,
|
|
13
|
+
type I18nRouterAdapterShape,
|
|
14
|
+
} from '../src/router-navigation/adapter';
|
|
15
|
+
|
|
16
|
+
test('late navigation installation preserves the mounted router and native fallback', async () => {
|
|
17
|
+
rstest.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
18
|
+
const context = {} as React.ContextType<typeof InternalRuntimeContext>;
|
|
19
|
+
const navigationContext = createContext<I18nRouterAdapterShape | null>(null);
|
|
20
|
+
const native: I18nRouterAdapterShape = {
|
|
21
|
+
hasRouter: false,
|
|
22
|
+
location: null,
|
|
23
|
+
navigate: null,
|
|
24
|
+
Link: null,
|
|
25
|
+
params: {},
|
|
26
|
+
};
|
|
27
|
+
const { I18nRouterNavigationProvider } = createI18nRouterNavigation({
|
|
28
|
+
I18nNavigationProvider: navigationContext.Provider,
|
|
29
|
+
useNativeI18nRouterAdapter: () => native,
|
|
30
|
+
});
|
|
31
|
+
const snapshot = {
|
|
32
|
+
location: { pathname: '/cs/produkty/bota', search: '', hash: '' },
|
|
33
|
+
params: { lang: 'cs', slug: 'bota' },
|
|
34
|
+
};
|
|
35
|
+
const navigation: RouterNavigationCapability = {
|
|
36
|
+
getSnapshot: () => snapshot,
|
|
37
|
+
subscribe: () => () => {},
|
|
38
|
+
navigate: () => {},
|
|
39
|
+
Link: () => null,
|
|
40
|
+
};
|
|
41
|
+
const mounted = rstest.fn();
|
|
42
|
+
const unmounted = rstest.fn();
|
|
43
|
+
const Router = () => {
|
|
44
|
+
const adapter = useContext(navigationContext);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
mounted();
|
|
47
|
+
return unmounted;
|
|
48
|
+
}, []);
|
|
49
|
+
return <p>{adapter?.location?.pathname ?? 'native fallback'}</p>;
|
|
50
|
+
};
|
|
51
|
+
const container = document.createElement('div');
|
|
52
|
+
const root = createRoot(container);
|
|
53
|
+
try {
|
|
54
|
+
await act(async () => {
|
|
55
|
+
root.render(
|
|
56
|
+
<InternalRuntimeContext.Provider value={context}>
|
|
57
|
+
<RuntimeContext.Provider value={context}>
|
|
58
|
+
<I18nRouterNavigationProvider>
|
|
59
|
+
<Router />
|
|
60
|
+
</I18nRouterNavigationProvider>
|
|
61
|
+
</RuntimeContext.Provider>
|
|
62
|
+
</InternalRuntimeContext.Provider>,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
expect(container.textContent).toBe('native fallback');
|
|
66
|
+
await act(async () => {
|
|
67
|
+
applyRouterRuntimeState(context, { framework: 'tanstack', navigation });
|
|
68
|
+
});
|
|
69
|
+
expect(container.textContent).toBe('/cs/produkty/bota');
|
|
70
|
+
await act(async () => {
|
|
71
|
+
applyRouterRuntimeState(context, { navigation: undefined });
|
|
72
|
+
});
|
|
73
|
+
expect(container.textContent).toBe('native fallback');
|
|
74
|
+
expect(mounted).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(unmounted).not.toHaveBeenCalled();
|
|
76
|
+
} finally {
|
|
77
|
+
await act(async () => root.unmount());
|
|
78
|
+
rstest.unstubAllGlobals();
|
|
79
|
+
}
|
|
80
|
+
});
|