@bleedingdev/modern-js-renderer-octane 3.9.3-ultramodern.1 → 3.9.3-ultramodern.11
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/client.js +56 -6
- package/dist/cjs/entry-application.js +7 -2
- package/dist/cjs/entry-client.js +3 -0
- package/dist/cjs/entry-server.js +13 -5
- package/dist/cjs/federation.js +108 -6
- package/dist/cjs/i18n/I18nProvider.js +6 -14
- package/dist/cjs/i18n/LocalizedLink.js +24 -33
- package/dist/cjs/i18n/index.js +0 -3
- package/dist/cjs/i18n-external-snapshot.js +645 -0
- package/dist/cjs/index.js +4 -4
- package/dist/cjs/manifest.js +2 -2
- package/dist/cjs/plugin/compiler.js +2 -5
- package/dist/cjs/plugin/create.js +27 -26
- package/dist/cjs/plugin/index.js +2 -2
- package/dist/cjs/plugin/profile.js +12 -10
- package/dist/cjs/router-client.js +1 -1
- package/dist/cjs/router-external-snapshot.js +291 -0
- package/dist/cjs/router-handler.js +11 -8
- package/dist/cjs/router-server.js +3 -11
- package/dist/cjs/router.js +1 -1
- package/dist/cjs/routes.js +20 -17
- package/dist/cjs/server.js +28 -7
- package/dist/cjs/worker-federation-context.js +63 -0
- package/dist/cjs/worker-federation-contexts.js +97 -0
- package/dist/cjs/worker-federation.js +97 -0
- package/dist/esm/client.mjs +56 -6
- package/dist/esm/entry-application.mjs +7 -2
- package/dist/esm/entry-client.mjs +3 -0
- package/dist/esm/entry-server.mjs +11 -6
- package/dist/esm/federation.mjs +110 -8
- package/dist/esm/i18n/I18nProvider.mjs +7 -15
- package/dist/esm/i18n/LocalizedLink.mjs +24 -30
- package/dist/esm/i18n/index.mjs +1 -1
- package/dist/esm/i18n-external-snapshot.mjs +576 -0
- package/dist/esm/index.mjs +4 -4
- package/dist/esm/manifest.mjs +2 -2
- package/dist/esm/router-client.mjs +1 -1
- package/dist/esm/router-external-snapshot.mjs +244 -0
- package/dist/esm/router-handler.mjs +11 -8
- package/dist/esm/router-server.mjs +0 -1
- package/dist/esm/router.mjs +2 -2
- package/dist/esm/routes.mjs +18 -18
- package/dist/esm/server.mjs +24 -3
- package/dist/esm/worker-federation-context.mjs +19 -0
- package/dist/esm/worker-federation-contexts.mjs +47 -0
- package/dist/esm/worker-federation.mjs +59 -0
- package/dist/esm-node/manifest.mjs +2 -2
- package/dist/esm-node/plugin/compiler.mjs +2 -5
- package/dist/esm-node/plugin/create.mjs +27 -26
- package/dist/esm-node/plugin/index.mjs +2 -2
- package/dist/esm-node/plugin/profile.mjs +12 -10
- package/dist/types/client.d.ts +5 -1
- package/dist/types/entry-client.d.ts +2 -0
- package/dist/types/entry-server.d.ts +4 -1
- package/dist/types/federation.d.ts +2 -0
- package/dist/types/i18n/LocalizedLink.d.ts +5 -32
- package/dist/types/i18n/context.d.ts +1 -1
- package/dist/types/i18n/index.d.ts +1 -1
- package/dist/types/i18n-external-snapshot.d.ts +43 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/manifest.d.ts +2 -2
- package/dist/types/router-external-snapshot.d.ts +10 -0
- package/dist/types/router-server.d.ts +0 -2
- package/dist/types/routes.d.ts +2 -0
- package/dist/types/server.d.ts +6 -0
- package/dist/types/worker-federation-context.d.ts +34 -0
- package/dist/types/worker-federation-contexts.d.ts +25 -0
- package/dist/types/worker-federation.d.ts +20 -0
- package/package.json +84 -22
- package/src/client.ts +99 -16
- package/src/entry-application.ts +7 -3
- package/src/entry-client.ts +10 -0
- package/src/entry-server.ts +20 -9
- package/src/federation.ts +164 -15
- package/src/i18n/I18nProvider.ts +11 -20
- package/src/i18n/LocalizedLink.ts +46 -72
- package/src/i18n/context.ts +1 -1
- package/src/i18n/index.ts +1 -5
- package/src/i18n-external-snapshot.ts +803 -0
- package/src/index.ts +4 -4
- package/src/manifest.ts +2 -2
- package/src/plugin/compiler-manifest.cjs +27 -1
- package/src/plugin/compiler.ts +6 -14
- package/src/plugin/create.ts +27 -26
- package/src/plugin/index.ts +4 -2
- package/src/plugin/profile.ts +14 -10
- package/src/router-client.ts +1 -1
- package/src/router-external-snapshot.ts +363 -0
- package/src/router-handler.ts +11 -8
- package/src/router-server.ts +0 -2
- package/src/router.ts +2 -2
- package/src/routes.ts +37 -26
- package/src/server.ts +45 -5
- package/src/worker-federation-context.ts +51 -0
- package/src/worker-federation-contexts.ts +91 -0
- package/src/worker-federation.ts +120 -0
- package/dist/cjs/router-injection.js +0 -139
- package/dist/esm/router-injection.mjs +0 -101
- package/dist/types/router-injection.d.ts +0 -8
- package/src/router-injection.ts +0 -145
package/src/federation.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
loadFederatedModule,
|
|
3
|
-
remoteBrowserAssets,
|
|
4
|
-
} from '@modern-js/renderer-core/federation';
|
|
1
|
+
import { loadFederatedModule } from '@modern-js/renderer-core/federation';
|
|
5
2
|
import {
|
|
6
3
|
type ComponentBody,
|
|
7
4
|
createElement,
|
|
5
|
+
decodeExternalSnapshot,
|
|
6
|
+
externalSnapshotBoundary,
|
|
7
|
+
hookSlots,
|
|
8
8
|
lazy,
|
|
9
9
|
type OctaneNode,
|
|
10
10
|
preinit,
|
|
11
|
+
publisherBoundary,
|
|
11
12
|
Suspense,
|
|
13
|
+
serializeExternalSnapshotRequest,
|
|
12
14
|
useContext,
|
|
15
|
+
useEffect,
|
|
13
16
|
} from 'octane';
|
|
14
17
|
import {
|
|
15
18
|
FederationContext,
|
|
16
19
|
type OctaneFederationScope,
|
|
17
20
|
} from './federation-context';
|
|
18
21
|
|
|
22
|
+
import { I18nContext } from './i18n/context';
|
|
23
|
+
import {
|
|
24
|
+
type OctaneWorkerFederationScope,
|
|
25
|
+
WorkerFederationContext,
|
|
26
|
+
} from './worker-federation-context';
|
|
27
|
+
import { useOctaneWorkerContextKeys } from './worker-federation-contexts';
|
|
28
|
+
|
|
19
29
|
/** The native component shape exposed by a same-renderer Octane remote. */
|
|
20
30
|
export interface FederatedModule<P extends object> {
|
|
21
31
|
readonly default: ComponentBody<P>;
|
|
@@ -26,6 +36,8 @@ export interface FederatedComponentOptions {
|
|
|
26
36
|
readonly fallback?: () => OctaneNode;
|
|
27
37
|
/** A positive load limit; the server defaults to 3000ms. */
|
|
28
38
|
readonly timeout?: number;
|
|
39
|
+
/** Explicit registered server projections required by an application remote. */
|
|
40
|
+
readonly contextKeys?: readonly string[];
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
function withTimeout<T>(
|
|
@@ -56,6 +68,9 @@ function withTimeout<T>(
|
|
|
56
68
|
});
|
|
57
69
|
}
|
|
58
70
|
|
|
71
|
+
// This library wrapper is published as plain JavaScript, outside app compilation.
|
|
72
|
+
const publisherRegistrationEffectSlot = Symbol(hookSlots(1));
|
|
73
|
+
|
|
59
74
|
/**
|
|
60
75
|
* Render a native Octane remote by its `remote/Expose` id or a browser loader.
|
|
61
76
|
* Native Suspense owns server fallbacks, streamed styles and hydration adoption;
|
|
@@ -90,11 +105,38 @@ export function federatedComponent<P extends object = Record<string, never>>(
|
|
|
90
105
|
};
|
|
91
106
|
// Native lazy caches its module. Scope the cache to one application/response,
|
|
92
107
|
// so two hosts using the same component definition cannot share a remote.
|
|
93
|
-
const components = new WeakMap<
|
|
108
|
+
const components = new WeakMap<object, ComponentBody<P>>();
|
|
109
|
+
const workerOwners = new WeakMap<
|
|
110
|
+
OctaneFederationScope,
|
|
111
|
+
WeakMap<OctaneWorkerFederationScope, Map<string, object>>
|
|
112
|
+
>();
|
|
94
113
|
|
|
95
114
|
return props => {
|
|
96
115
|
const context = useContext(FederationContext) ?? standalone;
|
|
97
|
-
|
|
116
|
+
const worker = useContext(WorkerFederationContext);
|
|
117
|
+
const language = worker
|
|
118
|
+
? (useContext(I18nContext)?.language ?? worker.locale)
|
|
119
|
+
: undefined;
|
|
120
|
+
const contextKeys = worker
|
|
121
|
+
? useOctaneWorkerContextKeys(options.contextKeys)
|
|
122
|
+
: undefined;
|
|
123
|
+
let owner: object = context;
|
|
124
|
+
if (worker) {
|
|
125
|
+
let scopes = workerOwners.get(context);
|
|
126
|
+
if (!scopes) workerOwners.set(context, (scopes = new WeakMap()));
|
|
127
|
+
let variants = scopes.get(worker);
|
|
128
|
+
if (!variants) scopes.set(worker, (variants = new Map()));
|
|
129
|
+
// Server snapshots are locale-specific. The hydrated browser publisher
|
|
130
|
+
// keeps its native owner and receives language changes through context.
|
|
131
|
+
const key = JSON.stringify([
|
|
132
|
+
context.server ? language : undefined,
|
|
133
|
+
contextKeys,
|
|
134
|
+
]);
|
|
135
|
+
let variant = variants.get(key);
|
|
136
|
+
if (!variant) variants.set(key, (variant = {}));
|
|
137
|
+
owner = variant;
|
|
138
|
+
}
|
|
139
|
+
let Remote = components.get(owner);
|
|
98
140
|
if (!Remote) {
|
|
99
141
|
Remote = lazy(async () => {
|
|
100
142
|
try {
|
|
@@ -103,11 +145,101 @@ export function federatedComponent<P extends object = Record<string, never>>(
|
|
|
103
145
|
'A federatedComponent loader renders in the browser only; use a remote id to server-render it',
|
|
104
146
|
);
|
|
105
147
|
}
|
|
106
|
-
|
|
148
|
+
if (worker) {
|
|
149
|
+
if (!id || !language) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
'Worker federation requires a remote id and the owning entry locale',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const separator = id.indexOf('/');
|
|
155
|
+
const remoteName = id.slice(0, separator);
|
|
156
|
+
const expose = `./${id.slice(separator + 1)}`;
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const signal = worker.signal
|
|
159
|
+
? AbortSignal.any([worker.signal, controller.signal])
|
|
160
|
+
: controller.signal;
|
|
161
|
+
const timeout =
|
|
162
|
+
options.timeout ?? (context.server ? 3000 : undefined);
|
|
163
|
+
let admitted;
|
|
164
|
+
try {
|
|
165
|
+
admitted = await withTimeout(
|
|
166
|
+
worker.transport.admit({
|
|
167
|
+
remote: remoteName,
|
|
168
|
+
expose,
|
|
169
|
+
locale: language,
|
|
170
|
+
signal,
|
|
171
|
+
}),
|
|
172
|
+
timeout,
|
|
173
|
+
id,
|
|
174
|
+
);
|
|
175
|
+
} finally {
|
|
176
|
+
// Release this admission waiter, including after its own deadline.
|
|
177
|
+
controller.abort();
|
|
178
|
+
}
|
|
179
|
+
const BrowserRemote = lazy(async () => {
|
|
180
|
+
const { module, assets } = await loadFederatedModule<
|
|
181
|
+
FederatedModule<P>
|
|
182
|
+
>(context.instance, id);
|
|
183
|
+
if (typeof module?.default !== 'function') {
|
|
184
|
+
throw new TypeError(
|
|
185
|
+
'A federated Octane module must default-export a native component',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const component = module.default;
|
|
189
|
+
const Loaded: ComponentBody<P> = (props, scope, extra) => {
|
|
190
|
+
for (const href of assets?.css ?? []) {
|
|
191
|
+
preinit(href, {
|
|
192
|
+
as: 'style',
|
|
193
|
+
precedence: 'federated',
|
|
194
|
+
...(context.nonce === undefined
|
|
195
|
+
? {}
|
|
196
|
+
: { nonce: context.nonce }),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return component(props, scope, extra);
|
|
200
|
+
};
|
|
201
|
+
return { default: Loaded };
|
|
202
|
+
});
|
|
203
|
+
const boundary = externalSnapshotBoundary({
|
|
204
|
+
authority: admitted.coordinate.native,
|
|
205
|
+
component: BrowserRemote,
|
|
206
|
+
contextKeys,
|
|
207
|
+
timeoutMs: timeout,
|
|
208
|
+
async snapshot(request, signal) {
|
|
209
|
+
const response = await worker.transport.request({
|
|
210
|
+
remote: remoteName,
|
|
211
|
+
expose,
|
|
212
|
+
locale: language,
|
|
213
|
+
payload: serializeExternalSnapshotRequest(request),
|
|
214
|
+
signal,
|
|
215
|
+
});
|
|
216
|
+
return decodeExternalSnapshot(response.payload);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
const Loaded: ComponentBody<P> = props => {
|
|
220
|
+
// A Worker server does not execute BrowserRemote. Register the
|
|
221
|
+
// exact admitted expose's extracted CSS in this active native
|
|
222
|
+
// render; nested snapshots carry these hints through native head.
|
|
223
|
+
for (const href of admitted.styles) {
|
|
224
|
+
preinit(href, {
|
|
225
|
+
as: 'style',
|
|
226
|
+
precedence: 'federated',
|
|
227
|
+
...(context.nonce === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: { nonce: context.nonce }),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return createElement(boundary, props);
|
|
233
|
+
};
|
|
234
|
+
return { default: Loaded };
|
|
235
|
+
}
|
|
236
|
+
const { module, assets } = await withTimeout(
|
|
107
237
|
Promise.resolve().then(() =>
|
|
108
238
|
id
|
|
109
239
|
? loadFederatedModule<FederatedModule<P>>(context.instance, id)
|
|
110
|
-
: (remote as () => Promise<FederatedModule<P>>)()
|
|
240
|
+
: (remote as () => Promise<FederatedModule<P>>)().then(
|
|
241
|
+
module => ({ module, assets: undefined }),
|
|
242
|
+
),
|
|
111
243
|
),
|
|
112
244
|
options.timeout ?? (context.server ? 3000 : undefined),
|
|
113
245
|
id ?? 'loader',
|
|
@@ -117,17 +249,34 @@ export function federatedComponent<P extends object = Record<string, never>>(
|
|
|
117
249
|
'A federated Octane module must default-export a native component',
|
|
118
250
|
);
|
|
119
251
|
}
|
|
120
|
-
const assets =
|
|
121
|
-
id && context.instance
|
|
122
|
-
? remoteBrowserAssets(context.instance, id)
|
|
123
|
-
: undefined;
|
|
124
252
|
if (context.server && !assets) {
|
|
125
253
|
throw new Error(
|
|
126
254
|
`Cannot server-render ${id}: the host has no browser assets for this remote`,
|
|
127
255
|
);
|
|
128
256
|
}
|
|
129
|
-
|
|
257
|
+
// Native publisher ownership admits this exact compiled component's
|
|
258
|
+
// signal generations. Keep the boundary in the context-scoped lazy
|
|
259
|
+
// cache so props and compatible hot updates retain its live owner.
|
|
260
|
+
const component = publisherBoundary(module.default, {
|
|
261
|
+
publisherKey: id ?? 'ultramodern:browser-loader',
|
|
262
|
+
});
|
|
130
263
|
const Loaded: ComponentBody<P> = props => {
|
|
264
|
+
// A development runtime installs this hook on the actual owning
|
|
265
|
+
// host. Registration follows the native mounted lifetime; routing
|
|
266
|
+
// alone grants no replacement authority to a remote name.
|
|
267
|
+
useEffect(
|
|
268
|
+
() => {
|
|
269
|
+
if (context.server || !id || !context.instance) return;
|
|
270
|
+
const register = Reflect.get(
|
|
271
|
+
context.instance,
|
|
272
|
+
Symbol.for('ultramodern.federation.browser-dev-boundary.v1'),
|
|
273
|
+
);
|
|
274
|
+
if (typeof register !== 'function') return;
|
|
275
|
+
return register(id, module.default, component);
|
|
276
|
+
},
|
|
277
|
+
[],
|
|
278
|
+
publisherRegistrationEffectSlot,
|
|
279
|
+
);
|
|
131
280
|
// Resource APIs require an active native render. Late styles travel
|
|
132
281
|
// in Octane's resource carrier ahead of the remote's HTML segment.
|
|
133
282
|
for (const href of assets?.css ?? []) {
|
|
@@ -144,11 +293,11 @@ export function federatedComponent<P extends object = Record<string, never>>(
|
|
|
144
293
|
return { default: Loaded };
|
|
145
294
|
} catch (error) {
|
|
146
295
|
// A later mount or ErrorBoundary reset may retry a failed load.
|
|
147
|
-
if (!context.server) components.delete(
|
|
296
|
+
if (!context.server) components.delete(owner);
|
|
148
297
|
throw error;
|
|
149
298
|
}
|
|
150
299
|
});
|
|
151
|
-
components.set(
|
|
300
|
+
components.set(owner, Remote);
|
|
152
301
|
}
|
|
153
302
|
return createElement(Suspense, {
|
|
154
303
|
fallback: options.fallback,
|
package/src/i18n/I18nProvider.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createElement, hookSlots,
|
|
1
|
+
import { createElement, hookSlots, useSyncExternalStore } from 'octane';
|
|
2
2
|
import { I18nContext } from './context';
|
|
3
|
-
import type { I18nContextValue
|
|
3
|
+
import type { I18nContextValue } from './types';
|
|
4
4
|
|
|
5
5
|
export interface I18nProviderProps extends Omit<I18nContextValue, 'language'> {
|
|
6
6
|
children?: unknown;
|
|
@@ -22,27 +22,18 @@ export interface I18nProviderProps extends Omit<I18nContextValue, 'language'> {
|
|
|
22
22
|
*/
|
|
23
23
|
// Plain TypeScript is not rewritten by the Octane compiler, so each hook call
|
|
24
24
|
// site carries its own stable slot, like `OctaneRouterRoot`.
|
|
25
|
-
const
|
|
26
|
-
const languageSubscriptionSlot = Symbol(hookSlots(1));
|
|
25
|
+
const languageStoreSlot = Symbol(hookSlots(1));
|
|
27
26
|
|
|
28
27
|
export function I18nProvider(props: I18nProviderProps) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
useEffect(
|
|
35
|
-
() => {
|
|
36
|
-
const instance: I18nInstanceLike = props.instance;
|
|
37
|
-
setLanguage(instance.language);
|
|
38
|
-
const handleLanguageChanged = (lng: string) => setLanguage(lng);
|
|
39
|
-
instance.on?.('languageChanged', handleLanguageChanged);
|
|
40
|
-
return () => {
|
|
41
|
-
instance.off?.('languageChanged', handleLanguageChanged);
|
|
42
|
-
};
|
|
28
|
+
const instance = props.instance;
|
|
29
|
+
const language = useSyncExternalStore(
|
|
30
|
+
notify => {
|
|
31
|
+
instance.on?.('languageChanged', notify);
|
|
32
|
+
return () => instance.off?.('languageChanged', notify);
|
|
43
33
|
},
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
() => instance.language,
|
|
35
|
+
() => instance.language,
|
|
36
|
+
languageStoreSlot,
|
|
46
37
|
);
|
|
47
38
|
|
|
48
39
|
const value: I18nContextValue = {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
canonicalPath,
|
|
3
2
|
configuredLanguage,
|
|
4
3
|
localizePath,
|
|
5
4
|
} from '@modern-js/i18n-runtime-extensions/paths';
|
|
6
5
|
import { createElement, hookSlots, type OctaneNode, useContext } from 'octane';
|
|
7
6
|
import {
|
|
7
|
+
type AnyRouter,
|
|
8
8
|
Link,
|
|
9
9
|
type LinkComponentProps,
|
|
10
10
|
useLocation,
|
|
@@ -14,41 +14,23 @@ import { I18nContext } from './context';
|
|
|
14
14
|
|
|
15
15
|
// Plain TypeScript is not rewritten by the Octane compiler: router hooks take
|
|
16
16
|
// their stable slot explicitly.
|
|
17
|
-
const
|
|
17
|
+
const targetLocationSlot = Symbol(hookSlots(1));
|
|
18
18
|
|
|
19
|
-
export interface LocalizedLinkProps
|
|
19
|
+
export interface LocalizedLinkProps
|
|
20
|
+
extends Omit<
|
|
21
|
+
LinkComponentProps<'a', AnyRouter, string, string>,
|
|
22
|
+
'to' | 'mask' | 'href' | 'publicHref' | '_fromLocation'
|
|
23
|
+
> {
|
|
20
24
|
/** A canonical, language-agnostic target, e.g. `/products/red-shoe`. */
|
|
21
25
|
to: string;
|
|
22
26
|
/** Localize for a specific language instead of the current one. */
|
|
23
27
|
language?: string;
|
|
24
|
-
children?: unknown;
|
|
25
|
-
class?: string;
|
|
26
|
-
activeProps?: LinkComponentProps<'a'>['activeProps'];
|
|
27
|
-
inactiveProps?: LinkComponentProps<'a'>['inactiveProps'];
|
|
28
|
-
activeOptions?: LinkComponentProps<'a'>['activeOptions'];
|
|
29
|
-
replace?: boolean;
|
|
30
|
-
preload?: LinkComponentProps<'a'>['preload'];
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* to the real link — the same contract as `@modern-js/plugin-i18n`'s React
|
|
38
|
-
* `Link`. It resolves the href itself via the shared pathname helpers, so it
|
|
39
|
-
* works with or without the router's i18n `rewrite`. Every link, including
|
|
40
|
-
* one to another language, is the native `Link`: navigation, preloading and
|
|
41
|
-
* active state stay with the router.
|
|
42
|
-
*
|
|
43
|
-
* Under the i18n `rewrite` the router matches canonical paths and localizes
|
|
44
|
-
* outgoing locations to the current language, so a link to another language
|
|
45
|
-
* routes to the canonical `to` and publishes the target language's URL as a
|
|
46
|
-
* route mask (the rewrite keeps a mask's explicit language). The entry's
|
|
47
|
-
* router language synchronization (`syncWithRouter`) switches the i18next
|
|
48
|
-
* instance from the navigated URL, as it does for history navigation.
|
|
49
|
-
*
|
|
50
|
-
* No JSX: built with `createElement`, matching this package's existing
|
|
51
|
-
* style (see `src/routes.ts`).
|
|
31
|
+
* The native router owns client navigation, preloading and active state. Its mask
|
|
32
|
+
* keeps the canonical route while publishing the selected language URL;
|
|
33
|
+
* the entry's i18n router subscription synchronizes the instance on navigation.
|
|
52
34
|
*/
|
|
53
35
|
export function LocalizedLink(props: LocalizedLinkProps): OctaneNode {
|
|
54
36
|
const context = useContext(I18nContext);
|
|
@@ -58,55 +40,47 @@ export function LocalizedLink(props: LocalizedLinkProps): OctaneNode {
|
|
|
58
40
|
);
|
|
59
41
|
}
|
|
60
42
|
const router = useRouter();
|
|
61
|
-
|
|
62
|
-
|
|
43
|
+
const location = useLocation(undefined, targetLocationSlot);
|
|
44
|
+
const { language: requestedLanguage, to, children, ...options } = props;
|
|
63
45
|
const language =
|
|
64
|
-
|
|
46
|
+
requestedLanguage === undefined
|
|
65
47
|
? context.language
|
|
66
|
-
: configuredLanguage(
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
48
|
+
: configuredLanguage(requestedLanguage, context.languages);
|
|
49
|
+
const externalTarget = to.startsWith('//') || URL.canParse(to);
|
|
50
|
+
const pathname = externalTarget
|
|
51
|
+
? to
|
|
52
|
+
: localizePath(
|
|
53
|
+
router.buildLocation({
|
|
54
|
+
to,
|
|
55
|
+
from: options.from,
|
|
56
|
+
params: options.params,
|
|
57
|
+
unsafeRelative: options.unsafeRelative,
|
|
58
|
+
_fromLocation: location,
|
|
59
|
+
}).pathname,
|
|
60
|
+
language,
|
|
61
|
+
{
|
|
62
|
+
languages: [...context.languages],
|
|
63
|
+
localisedUrls: context.localisedUrls,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
73
66
|
|
|
74
67
|
return createElement(
|
|
75
|
-
Link,
|
|
68
|
+
Link<AnyRouter, string, string, string, string>,
|
|
76
69
|
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
70
|
+
...options,
|
|
71
|
+
to: router.options.rewrite ? to : pathname,
|
|
72
|
+
...(router.options.rewrite && !externalTarget
|
|
73
|
+
? {
|
|
74
|
+
mask: {
|
|
75
|
+
to: pathname,
|
|
76
|
+
inheritSearch: true,
|
|
77
|
+
hash: options.hash,
|
|
78
|
+
state: options.state,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
: {}),
|
|
82
|
+
...(language === context.language ? {} : { hreflang: language }),
|
|
89
83
|
},
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Language-invariant active check: `to="/products"` is active whether the
|
|
96
|
-
* current location is `/en/products` or `/produkty` under a mapped locale.
|
|
97
|
-
*/
|
|
98
|
-
export function useIsLocalizedActive(canonicalTarget: string): boolean {
|
|
99
|
-
const context = useContext(I18nContext);
|
|
100
|
-
if (!context) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
'[renderer-octane] useIsLocalizedActive was used outside of an I18nProvider.',
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
const location = useLocation(undefined, locationSlot);
|
|
106
|
-
return (
|
|
107
|
-
canonicalPath(location.pathname, {
|
|
108
|
-
languages: [...context.languages],
|
|
109
|
-
localisedUrls: context.localisedUrls,
|
|
110
|
-
}) === canonicalTarget
|
|
84
|
+
children,
|
|
111
85
|
);
|
|
112
86
|
}
|
package/src/i18n/context.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type { I18nContextValue, I18nInstanceLike } from './types';
|
|
|
6
6
|
/**
|
|
7
7
|
* Octane's `createContext<T>(defaultValue: T)` requires a default (unlike
|
|
8
8
|
* Solid 2's default-less form), so a missing provider is detected by
|
|
9
|
-
* checking for `null` in `useI18n`/`
|
|
9
|
+
* checking for `null` in `useI18n`/`LocalizedLink` instead — the same
|
|
10
10
|
* guard shape `@modern-js/plugin-i18n`'s React `ModernI18nContext` uses.
|
|
11
11
|
*/
|
|
12
12
|
export const I18nContext = createContext<I18nContextValue | null>(null);
|
package/src/i18n/index.ts
CHANGED
|
@@ -22,11 +22,7 @@ export {
|
|
|
22
22
|
} from '@modern-js/i18n-runtime-extensions/urlRewrite';
|
|
23
23
|
export { I18nContext } from './context';
|
|
24
24
|
export { I18nProvider, type I18nProviderProps } from './I18nProvider';
|
|
25
|
-
export {
|
|
26
|
-
LocalizedLink,
|
|
27
|
-
type LocalizedLinkProps,
|
|
28
|
-
useIsLocalizedActive,
|
|
29
|
-
} from './LocalizedLink';
|
|
25
|
+
export { LocalizedLink, type LocalizedLinkProps } from './LocalizedLink';
|
|
30
26
|
export {
|
|
31
27
|
type OctaneLatestLanguageSyncOptions,
|
|
32
28
|
useLatestLanguageSync,
|