@bleedingdev/modern-js-renderer-octane 0.0.0-stage → 3.9.3-ultramodern.1
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/LICENSE +21 -0
- package/bin/octane-tsc.mjs +4 -0
- package/dist/cjs/bootstrap.js +68 -0
- package/dist/cjs/client.js +371 -0
- package/dist/cjs/entry-application.js +83 -0
- package/dist/cjs/entry-client.js +92 -0
- package/dist/cjs/entry-server.js +127 -0
- package/dist/cjs/federation-context.js +72 -0
- package/dist/cjs/federation.js +103 -0
- package/dist/cjs/i18n/I18nProvider.js +67 -0
- package/dist/cjs/i18n/LocalizedLink.js +89 -0
- package/dist/cjs/i18n/context.js +41 -0
- package/dist/cjs/i18n/index.js +86 -0
- package/dist/cjs/i18n/languageSync.js +99 -0
- package/dist/cjs/i18n/types.js +18 -0
- package/dist/cjs/i18n/useI18n.js +110 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/manifest.js +167 -0
- package/dist/cjs/plugin/compiler.js +125 -0
- package/dist/cjs/plugin/create.js +265 -0
- package/dist/cjs/plugin/index.js +151 -0
- package/dist/cjs/plugin/profile.js +89 -0
- package/dist/cjs/plugin/source.js +56 -0
- package/dist/cjs/router-client.js +139 -0
- package/dist/cjs/router-handler.js +168 -0
- package/dist/cjs/router-injection.js +139 -0
- package/dist/cjs/router-server-cleanup.js +46 -0
- package/dist/cjs/router-server-snapshot.js +405 -0
- package/dist/cjs/router-server.js +108 -0
- package/dist/cjs/router-snapshot.js +270 -0
- package/dist/cjs/router.js +218 -0
- package/dist/cjs/routes.js +187 -0
- package/dist/cjs/server.js +224 -0
- package/dist/cjs/typecheck.js +615 -0
- package/dist/esm/bootstrap.mjs +24 -0
- package/dist/esm/client.mjs +327 -0
- package/dist/esm/entry-application.mjs +39 -0
- package/dist/esm/entry-client.mjs +54 -0
- package/dist/esm/entry-server.mjs +86 -0
- package/dist/esm/federation-context.mjs +25 -0
- package/dist/esm/federation.mjs +65 -0
- package/dist/esm/i18n/I18nProvider.mjs +29 -0
- package/dist/esm/i18n/LocalizedLink.mjs +48 -0
- package/dist/esm/i18n/context.mjs +3 -0
- package/dist/esm/i18n/index.mjs +8 -0
- package/dist/esm/i18n/languageSync.mjs +61 -0
- package/dist/esm/i18n/types.mjs +0 -0
- package/dist/esm/i18n/useI18n.mjs +72 -0
- package/dist/esm/index.mjs +8 -0
- package/dist/esm/manifest.mjs +117 -0
- package/dist/esm/router-client.mjs +98 -0
- package/dist/esm/router-handler.mjs +130 -0
- package/dist/esm/router-injection.mjs +101 -0
- package/dist/esm/router-server-cleanup.mjs +8 -0
- package/dist/esm/router-server-snapshot.mjs +367 -0
- package/dist/esm/router-server.mjs +12 -0
- package/dist/esm/router-snapshot.mjs +199 -0
- package/dist/esm/router.mjs +94 -0
- package/dist/esm/routes.mjs +134 -0
- package/dist/esm/server.mjs +183 -0
- package/dist/esm-node/bootstrap.mjs +24 -0
- package/dist/esm-node/manifest.mjs +117 -0
- package/dist/esm-node/plugin/compiler.mjs +70 -0
- package/dist/esm-node/plugin/create.mjs +227 -0
- package/dist/esm-node/plugin/index.mjs +103 -0
- package/dist/esm-node/plugin/profile.mjs +51 -0
- package/dist/esm-node/plugin/source.mjs +7 -0
- package/dist/esm-node/typecheck.mjs +560 -0
- package/dist/types/bootstrap.d.ts +10 -0
- package/dist/types/client.d.ts +36 -0
- package/dist/types/entry-application.d.ts +19 -0
- package/dist/types/entry-client.d.ts +8 -0
- package/dist/types/entry-server.d.ts +11 -0
- package/dist/types/federation-context.d.ts +28 -0
- package/dist/types/federation.d.ts +18 -0
- package/dist/types/i18n/I18nProvider.d.ts +8 -0
- package/dist/types/i18n/LocalizedLink.d.ts +41 -0
- package/dist/types/i18n/context.d.ts +9 -0
- package/dist/types/i18n/index.d.ts +9 -0
- package/dist/types/i18n/languageSync.d.ts +26 -0
- package/dist/types/i18n/types.d.ts +34 -0
- package/dist/types/i18n/useI18n.d.ts +21 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/manifest.d.ts +34 -0
- package/dist/types/plugin/compiler-manifest.d.cts +10 -0
- package/dist/types/plugin/compiler.d.ts +5 -0
- package/dist/types/plugin/create.d.ts +23 -0
- package/dist/types/plugin/index.d.ts +3 -0
- package/dist/types/plugin/profile.d.ts +2 -0
- package/dist/types/plugin/source.d.ts +1 -0
- package/dist/types/plugin/svg-component-template.d.cts +6 -0
- package/dist/types/router-client.d.ts +12 -0
- package/dist/types/router-handler.d.ts +13 -0
- package/dist/types/router-injection.d.ts +8 -0
- package/dist/types/router-server-cleanup.d.ts +3 -0
- package/dist/types/router-server-snapshot.d.ts +15 -0
- package/dist/types/router-server.d.ts +13 -0
- package/dist/types/router-snapshot.d.ts +14 -0
- package/dist/types/router.d.ts +30 -0
- package/dist/types/routes.d.ts +18 -0
- package/dist/types/server.d.ts +41 -0
- package/dist/types/typecheck.d.ts +31 -0
- package/package.json +216 -4
- package/src/bootstrap.ts +60 -0
- package/src/client.ts +485 -0
- package/src/entry-application.ts +72 -0
- package/src/entry-client.ts +80 -0
- package/src/entry-server.ts +142 -0
- package/src/federation-context.ts +53 -0
- package/src/federation.ts +158 -0
- package/src/i18n/I18nProvider.ts +56 -0
- package/src/i18n/LocalizedLink.ts +112 -0
- package/src/i18n/context.ts +12 -0
- package/src/i18n/index.ts +35 -0
- package/src/i18n/languageSync.ts +132 -0
- package/src/i18n/types.ts +36 -0
- package/src/i18n/useI18n.ts +119 -0
- package/src/index.ts +10 -0
- package/src/manifest.ts +253 -0
- package/src/plugin/compiler-manifest.cjs +178 -0
- package/src/plugin/compiler.ts +112 -0
- package/src/plugin/create.ts +272 -0
- package/src/plugin/index.ts +105 -0
- package/src/plugin/profile.ts +46 -0
- package/src/plugin/source.ts +16 -0
- package/src/plugin/svg-component-template.cjs +17 -0
- package/src/router-client.ts +144 -0
- package/src/router-handler.ts +182 -0
- package/src/router-injection.ts +145 -0
- package/src/router-server-cleanup.ts +11 -0
- package/src/router-server-snapshot.ts +564 -0
- package/src/router-server.ts +23 -0
- package/src/router-snapshot.ts +251 -0
- package/src/router.ts +192 -0
- package/src/routes.ts +263 -0
- package/src/server.ts +300 -0
- package/src/typecheck.ts +723 -0
- package/README.md +0 -3
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
|
|
2
|
+
import {
|
|
3
|
+
collectDataHeaders,
|
|
4
|
+
dataMetadataToDocumentPolicy,
|
|
5
|
+
mergeDataResponseIntoResponse,
|
|
6
|
+
mergeDataResponseMetadata,
|
|
7
|
+
mergeHeaderFields,
|
|
8
|
+
} from '@modern-js/renderer-core/data';
|
|
9
|
+
import {
|
|
10
|
+
createNativeServerEntry as createServerEntry,
|
|
11
|
+
type NativeI18nInstance,
|
|
12
|
+
type NativeServerEntry,
|
|
13
|
+
type NativeServerEntryOptions,
|
|
14
|
+
} from '@modern-js/renderer-core/entry-server';
|
|
15
|
+
import { createNativeRouter } from '@modern-js/renderer-core/router';
|
|
16
|
+
import type { ResponsePolicy } from '@modern-js/renderer-core/session';
|
|
17
|
+
import type { ServerRenderNode } from 'octane/server';
|
|
18
|
+
import {
|
|
19
|
+
componentView,
|
|
20
|
+
localizedRouterRoot,
|
|
21
|
+
octaneRouterFactory,
|
|
22
|
+
} from './entry-application';
|
|
23
|
+
import { validateOctaneModuleManifest } from './manifest';
|
|
24
|
+
import {
|
|
25
|
+
createOctaneRequestHandler,
|
|
26
|
+
createOctaneRouterInjection,
|
|
27
|
+
createSsrStreamResponse,
|
|
28
|
+
OctaneRouterServer,
|
|
29
|
+
} from './router-server';
|
|
30
|
+
import { renderOctaneApplication, renderOctaneCSRDocument } from './server';
|
|
31
|
+
|
|
32
|
+
/** Document-describing fields an Octane route's `headers()` may set. */
|
|
33
|
+
const OCTANE_DOCUMENT_FIELDS = new Set([
|
|
34
|
+
'content-language',
|
|
35
|
+
'content-location',
|
|
36
|
+
'content-disposition',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The loader projection strips representation fields; the ones an Octane route
|
|
41
|
+
* declares for the document itself are kept, as Solid document headers are.
|
|
42
|
+
*/
|
|
43
|
+
export function withOctaneDocumentFields(
|
|
44
|
+
policy: ResponsePolicy,
|
|
45
|
+
responseHeaders: Headers,
|
|
46
|
+
): ResponsePolicy {
|
|
47
|
+
const fields = collectDataHeaders(responseHeaders).filter(([name]) =>
|
|
48
|
+
OCTANE_DOCUMENT_FIELDS.has(name.toLowerCase()),
|
|
49
|
+
);
|
|
50
|
+
return fields.length
|
|
51
|
+
? { ...policy, headers: [...policy.headers, ...fields] }
|
|
52
|
+
: policy;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type OctaneServerEntryOptions = NativeServerEntryOptions<
|
|
56
|
+
NativeI18nInstance,
|
|
57
|
+
LocalisedUrlsOption
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
const LocalizedRouterServer = localizedRouterRoot(OctaneRouterServer);
|
|
61
|
+
|
|
62
|
+
/** The native transport handlers of a generated Octane server entry. */
|
|
63
|
+
export function createNativeServerEntry(
|
|
64
|
+
options: OctaneServerEntryOptions,
|
|
65
|
+
): NativeServerEntry {
|
|
66
|
+
return createServerEntry(options, {
|
|
67
|
+
document: (document, nativeManifest, identity) => ({
|
|
68
|
+
...document,
|
|
69
|
+
nativeHydrationBuildId: validateOctaneModuleManifest(
|
|
70
|
+
nativeManifest,
|
|
71
|
+
identity,
|
|
72
|
+
).nativeHydrationBuildId,
|
|
73
|
+
}),
|
|
74
|
+
run: (_session, callback) => callback(),
|
|
75
|
+
respond: (_session, response) => response,
|
|
76
|
+
createRouter: (application, routerOptions) =>
|
|
77
|
+
createNativeRouter(application, routerOptions, octaneRouterFactory),
|
|
78
|
+
renderCSR: ({ session }, document) =>
|
|
79
|
+
renderOctaneCSRDocument({ session, document }),
|
|
80
|
+
renderComponent: ({ session }, component, document, i18n) =>
|
|
81
|
+
renderOctaneApplication({
|
|
82
|
+
session,
|
|
83
|
+
App: componentView(component, i18n) as ServerRenderNode,
|
|
84
|
+
document,
|
|
85
|
+
federation: options.federation,
|
|
86
|
+
}),
|
|
87
|
+
async renderRoutes({
|
|
88
|
+
request,
|
|
89
|
+
context: { session },
|
|
90
|
+
router,
|
|
91
|
+
outcomes,
|
|
92
|
+
document,
|
|
93
|
+
forbiddenValues,
|
|
94
|
+
i18n,
|
|
95
|
+
}) {
|
|
96
|
+
const response = await createOctaneRequestHandler({
|
|
97
|
+
request,
|
|
98
|
+
session,
|
|
99
|
+
createRouter: () => router,
|
|
100
|
+
serialization: { forbiddenValues },
|
|
101
|
+
})(async ({ router, responseHeaders }) => {
|
|
102
|
+
const metadata = mergeDataResponseMetadata(outcomes, {
|
|
103
|
+
status: router.state.statusCode ?? 200,
|
|
104
|
+
});
|
|
105
|
+
// Route headers() take precedence over loader singleton fields; list
|
|
106
|
+
// fields keep both.
|
|
107
|
+
const policy = dataMetadataToDocumentPolicy({
|
|
108
|
+
...metadata,
|
|
109
|
+
headers: mergeHeaderFields(
|
|
110
|
+
collectDataHeaders(responseHeaders),
|
|
111
|
+
metadata.headers,
|
|
112
|
+
),
|
|
113
|
+
});
|
|
114
|
+
session.resolveResponse(
|
|
115
|
+
withOctaneDocumentFields(policy, responseHeaders),
|
|
116
|
+
);
|
|
117
|
+
const rendered = await renderOctaneApplication({
|
|
118
|
+
session,
|
|
119
|
+
...(i18n
|
|
120
|
+
? {
|
|
121
|
+
App: LocalizedRouterServer as ServerRenderNode,
|
|
122
|
+
props: { router, i18n },
|
|
123
|
+
}
|
|
124
|
+
: {
|
|
125
|
+
App: OctaneRouterServer as ServerRenderNode,
|
|
126
|
+
props: { router },
|
|
127
|
+
}),
|
|
128
|
+
document,
|
|
129
|
+
federation: options.federation,
|
|
130
|
+
injection: createOctaneRouterInjection(router, session),
|
|
131
|
+
});
|
|
132
|
+
return createSsrStreamResponse(router, rendered);
|
|
133
|
+
});
|
|
134
|
+
return session.committedPolicy
|
|
135
|
+
? response
|
|
136
|
+
: mergeDataResponseIntoResponse(
|
|
137
|
+
response,
|
|
138
|
+
mergeDataResponseMetadata(outcomes, { status: response.status }),
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FederationInstance,
|
|
3
|
+
getFederationHost,
|
|
4
|
+
type NativeFederationBinding,
|
|
5
|
+
} from '@modern-js/renderer-core/federation';
|
|
6
|
+
import { type ComponentBody, createContext, createElement } from 'octane';
|
|
7
|
+
|
|
8
|
+
/** One application owns its host runtime; each response owns its render scope. */
|
|
9
|
+
export interface OctaneFederationScope {
|
|
10
|
+
readonly instance: FederationInstance | undefined;
|
|
11
|
+
readonly server: boolean;
|
|
12
|
+
readonly nonce?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const FederationContext = createContext<OctaneFederationScope | null>(
|
|
16
|
+
null,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export function createFederationScope(
|
|
20
|
+
binding: NativeFederationBinding | undefined,
|
|
21
|
+
server: boolean,
|
|
22
|
+
nonce?: string,
|
|
23
|
+
): OctaneFederationScope {
|
|
24
|
+
return {
|
|
25
|
+
instance: getFederationHost(binding),
|
|
26
|
+
server,
|
|
27
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function FederationRoot(props: {
|
|
32
|
+
readonly scope: OctaneFederationScope;
|
|
33
|
+
readonly children?: unknown;
|
|
34
|
+
}) {
|
|
35
|
+
return createElement(FederationContext, {
|
|
36
|
+
value: props.scope,
|
|
37
|
+
children: props.children,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A stable root component retains native state when its application updates. */
|
|
42
|
+
export function FederationApplication(props: {
|
|
43
|
+
readonly scope: OctaneFederationScope;
|
|
44
|
+
readonly application: {
|
|
45
|
+
readonly default: ComponentBody;
|
|
46
|
+
readonly props?: unknown;
|
|
47
|
+
};
|
|
48
|
+
}) {
|
|
49
|
+
return createElement(FederationRoot, {
|
|
50
|
+
scope: props.scope,
|
|
51
|
+
children: createElement(props.application.default, props.application.props),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadFederatedModule,
|
|
3
|
+
remoteBrowserAssets,
|
|
4
|
+
} from '@modern-js/renderer-core/federation';
|
|
5
|
+
import {
|
|
6
|
+
type ComponentBody,
|
|
7
|
+
createElement,
|
|
8
|
+
lazy,
|
|
9
|
+
type OctaneNode,
|
|
10
|
+
preinit,
|
|
11
|
+
Suspense,
|
|
12
|
+
useContext,
|
|
13
|
+
} from 'octane';
|
|
14
|
+
import {
|
|
15
|
+
FederationContext,
|
|
16
|
+
type OctaneFederationScope,
|
|
17
|
+
} from './federation-context';
|
|
18
|
+
|
|
19
|
+
/** The native component shape exposed by a same-renderer Octane remote. */
|
|
20
|
+
export interface FederatedModule<P extends object> {
|
|
21
|
+
readonly default: ComponentBody<P>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FederatedComponentOptions {
|
|
25
|
+
/** Native content shown while loading, or after a failed server load. */
|
|
26
|
+
readonly fallback?: () => OctaneNode;
|
|
27
|
+
/** A positive load limit; the server defaults to 3000ms. */
|
|
28
|
+
readonly timeout?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function withTimeout<T>(
|
|
32
|
+
promise: Promise<T>,
|
|
33
|
+
timeout: number | undefined,
|
|
34
|
+
label: string,
|
|
35
|
+
): Promise<T> {
|
|
36
|
+
if (timeout === undefined) return promise;
|
|
37
|
+
return new Promise<T>((resolve, reject) => {
|
|
38
|
+
const timer = setTimeout(
|
|
39
|
+
() =>
|
|
40
|
+
reject(
|
|
41
|
+
new Error(`Remote module ${label} did not load within ${timeout}ms`),
|
|
42
|
+
),
|
|
43
|
+
timeout,
|
|
44
|
+
);
|
|
45
|
+
(timer as { unref?: () => void }).unref?.();
|
|
46
|
+
promise.then(
|
|
47
|
+
value => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
resolve(value);
|
|
50
|
+
},
|
|
51
|
+
error => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
reject(error);
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Render a native Octane remote by its `remote/Expose` id or a browser loader.
|
|
61
|
+
* Native Suspense owns server fallbacks, streamed styles and hydration adoption;
|
|
62
|
+
* browser failures reach the nearest native ErrorBoundary. A loader function is
|
|
63
|
+
* browser-only; use an id to render through the host's server federation runtime.
|
|
64
|
+
*/
|
|
65
|
+
export function federatedComponent<P extends object = Record<string, never>>(
|
|
66
|
+
remote: string | (() => Promise<FederatedModule<P>>),
|
|
67
|
+
options: FederatedComponentOptions = {},
|
|
68
|
+
): ComponentBody<P> {
|
|
69
|
+
if (
|
|
70
|
+
typeof remote === 'string'
|
|
71
|
+
? !/^[^/]+\/.+/u.test(remote)
|
|
72
|
+
: typeof remote !== 'function'
|
|
73
|
+
) {
|
|
74
|
+
throw new TypeError(
|
|
75
|
+
"federatedComponent requires a remote id such as 'remote/Widget' or a loader",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
options.timeout !== undefined &&
|
|
80
|
+
(!Number.isFinite(options.timeout) || options.timeout <= 0)
|
|
81
|
+
) {
|
|
82
|
+
throw new TypeError(
|
|
83
|
+
'federatedComponent timeout must be a positive number of milliseconds',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const id = typeof remote === 'string' ? remote : undefined;
|
|
87
|
+
const standalone: OctaneFederationScope = {
|
|
88
|
+
instance: undefined,
|
|
89
|
+
server: typeof document === 'undefined',
|
|
90
|
+
};
|
|
91
|
+
// Native lazy caches its module. Scope the cache to one application/response,
|
|
92
|
+
// so two hosts using the same component definition cannot share a remote.
|
|
93
|
+
const components = new WeakMap<OctaneFederationScope, ComponentBody<P>>();
|
|
94
|
+
|
|
95
|
+
return props => {
|
|
96
|
+
const context = useContext(FederationContext) ?? standalone;
|
|
97
|
+
let Remote = components.get(context);
|
|
98
|
+
if (!Remote) {
|
|
99
|
+
Remote = lazy(async () => {
|
|
100
|
+
try {
|
|
101
|
+
if (context.server && !id) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
'A federatedComponent loader renders in the browser only; use a remote id to server-render it',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const module = await withTimeout(
|
|
107
|
+
Promise.resolve().then(() =>
|
|
108
|
+
id
|
|
109
|
+
? loadFederatedModule<FederatedModule<P>>(context.instance, id)
|
|
110
|
+
: (remote as () => Promise<FederatedModule<P>>)(),
|
|
111
|
+
),
|
|
112
|
+
options.timeout ?? (context.server ? 3000 : undefined),
|
|
113
|
+
id ?? 'loader',
|
|
114
|
+
);
|
|
115
|
+
if (typeof module?.default !== 'function') {
|
|
116
|
+
throw new TypeError(
|
|
117
|
+
'A federated Octane module must default-export a native component',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const assets =
|
|
121
|
+
id && context.instance
|
|
122
|
+
? remoteBrowserAssets(context.instance, id)
|
|
123
|
+
: undefined;
|
|
124
|
+
if (context.server && !assets) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Cannot server-render ${id}: the host has no browser assets for this remote`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const component = module.default;
|
|
130
|
+
const Loaded: ComponentBody<P> = props => {
|
|
131
|
+
// Resource APIs require an active native render. Late styles travel
|
|
132
|
+
// in Octane's resource carrier ahead of the remote's HTML segment.
|
|
133
|
+
for (const href of assets?.css ?? []) {
|
|
134
|
+
preinit(href, {
|
|
135
|
+
as: 'style',
|
|
136
|
+
precedence: 'federated',
|
|
137
|
+
...(context.nonce === undefined
|
|
138
|
+
? {}
|
|
139
|
+
: { nonce: context.nonce }),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return createElement(component, props);
|
|
143
|
+
};
|
|
144
|
+
return { default: Loaded };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// A later mount or ErrorBoundary reset may retry a failed load.
|
|
147
|
+
if (!context.server) components.delete(context);
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
components.set(context, Remote);
|
|
152
|
+
}
|
|
153
|
+
return createElement(Suspense, {
|
|
154
|
+
fallback: options.fallback,
|
|
155
|
+
children: createElement(Remote, props),
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createElement, hookSlots, useEffect, useState } from 'octane';
|
|
2
|
+
import { I18nContext } from './context';
|
|
3
|
+
import type { I18nContextValue, I18nInstanceLike } from './types';
|
|
4
|
+
|
|
5
|
+
export interface I18nProviderProps extends Omit<I18nContextValue, 'language'> {
|
|
6
|
+
children?: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Provides a per-request i18next-shaped instance to the Octane tree. The
|
|
11
|
+
* instance must come from the caller (created with `cloneInstance` per
|
|
12
|
+
* request on the server, and the same instance reused on hydration) — this
|
|
13
|
+
* provider holds no module-level state of its own beyond the one reactive
|
|
14
|
+
* `language` state it derives from `instance` and shares with every
|
|
15
|
+
* consumer through context, matching `@modern-js/plugin-i18n`'s React
|
|
16
|
+
* `ModernI18nProvider` shape (one subscription, pushed through context,
|
|
17
|
+
* rather than every consumer subscribing to `languageChanged` itself).
|
|
18
|
+
*
|
|
19
|
+
* No JSX: built with `createElement`, matching this package's existing
|
|
20
|
+
* style (see `src/routes.ts`), and avoiding any dependency on Octane's
|
|
21
|
+
* JSX/compiler toolchain for a file that needs none of its features.
|
|
22
|
+
*/
|
|
23
|
+
// Plain TypeScript is not rewritten by the Octane compiler, so each hook call
|
|
24
|
+
// site carries its own stable slot, like `OctaneRouterRoot`.
|
|
25
|
+
const languageStateSlot = Symbol(hookSlots(1));
|
|
26
|
+
const languageSubscriptionSlot = Symbol(hookSlots(1));
|
|
27
|
+
|
|
28
|
+
export function I18nProvider(props: I18nProviderProps) {
|
|
29
|
+
const [language, setLanguage] = useState(
|
|
30
|
+
() => props.instance.language,
|
|
31
|
+
languageStateSlot,
|
|
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
|
+
};
|
|
43
|
+
},
|
|
44
|
+
[props.instance],
|
|
45
|
+
languageSubscriptionSlot,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const value: I18nContextValue = {
|
|
49
|
+
instance: props.instance,
|
|
50
|
+
languages: props.languages,
|
|
51
|
+
localisedUrls: props.localisedUrls,
|
|
52
|
+
language,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return createElement(I18nContext, { value, children: props.children });
|
|
56
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalPath,
|
|
3
|
+
configuredLanguage,
|
|
4
|
+
localizePath,
|
|
5
|
+
} from '@modern-js/i18n-runtime-extensions/paths';
|
|
6
|
+
import { createElement, hookSlots, type OctaneNode, useContext } from 'octane';
|
|
7
|
+
import {
|
|
8
|
+
Link,
|
|
9
|
+
type LinkComponentProps,
|
|
10
|
+
useLocation,
|
|
11
|
+
useRouter,
|
|
12
|
+
} from '../router';
|
|
13
|
+
import { I18nContext } from './context';
|
|
14
|
+
|
|
15
|
+
// Plain TypeScript is not rewritten by the Octane compiler: router hooks take
|
|
16
|
+
// their stable slot explicitly.
|
|
17
|
+
const locationSlot = Symbol(hookSlots(1));
|
|
18
|
+
|
|
19
|
+
export interface LocalizedLinkProps {
|
|
20
|
+
/** A canonical, language-agnostic target, e.g. `/products/red-shoe`. */
|
|
21
|
+
to: string;
|
|
22
|
+
/** Localize for a specific language instead of the current one. */
|
|
23
|
+
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
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A thin wrapper over the native Octane router `Link`
|
|
35
|
+
* (`@octanejs/tanstack-router`'s precompiled `Link.tsrx`) that localizes a
|
|
36
|
+
* canonical `to` for the current (or an explicit) language before handing it
|
|
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`).
|
|
52
|
+
*/
|
|
53
|
+
export function LocalizedLink(props: LocalizedLinkProps): OctaneNode {
|
|
54
|
+
const context = useContext(I18nContext);
|
|
55
|
+
if (!context) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
'[renderer-octane] LocalizedLink was used outside of an I18nProvider.',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const router = useRouter();
|
|
61
|
+
|
|
62
|
+
// An explicit language must be configured: it becomes the locale prefix.
|
|
63
|
+
const language =
|
|
64
|
+
props.language === undefined
|
|
65
|
+
? context.language
|
|
66
|
+
: configuredLanguage(props.language, context.languages);
|
|
67
|
+
const href = localizePath(props.to, language, {
|
|
68
|
+
languages: [...context.languages],
|
|
69
|
+
localisedUrls: context.localisedUrls,
|
|
70
|
+
});
|
|
71
|
+
const crossLanguage = language !== context.language;
|
|
72
|
+
const rewrite = Boolean(router.options.rewrite);
|
|
73
|
+
|
|
74
|
+
return createElement(
|
|
75
|
+
Link,
|
|
76
|
+
{
|
|
77
|
+
// Octane's `Link` builds its location from `to` alone. Under the i18n
|
|
78
|
+
// router rewrite the router matches canonical paths and localizes the
|
|
79
|
+
// public href itself; without it, the localized path is the route path.
|
|
80
|
+
to: rewrite ? props.to : href,
|
|
81
|
+
...(rewrite && crossLanguage ? { mask: { to: href } } : {}),
|
|
82
|
+
...(crossLanguage ? { hreflang: language } : {}),
|
|
83
|
+
class: props.class,
|
|
84
|
+
activeProps: props.activeProps,
|
|
85
|
+
inactiveProps: props.inactiveProps,
|
|
86
|
+
activeOptions: props.activeOptions,
|
|
87
|
+
replace: props.replace,
|
|
88
|
+
preload: props.preload,
|
|
89
|
+
},
|
|
90
|
+
props.children,
|
|
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
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext } from 'octane';
|
|
2
|
+
import type { I18nContextValue } from './types';
|
|
3
|
+
|
|
4
|
+
export type { I18nContextValue, I18nInstanceLike } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Octane's `createContext<T>(defaultValue: T)` requires a default (unlike
|
|
8
|
+
* Solid 2's default-less form), so a missing provider is detected by
|
|
9
|
+
* checking for `null` in `useI18n`/`useIsLocalizedActive` instead — the same
|
|
10
|
+
* guard shape `@modern-js/plugin-i18n`'s React `ModernI18nContext` uses.
|
|
11
|
+
*/
|
|
12
|
+
export const I18nContext = createContext<I18nContextValue | null>(null);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createRequestLanguageRedirect,
|
|
3
|
+
detectRequestLanguage,
|
|
4
|
+
languageFromPathname,
|
|
5
|
+
type RequestLanguageDetection,
|
|
6
|
+
type ResolvedRequestLanguage,
|
|
7
|
+
type ResolveRequestLanguageOptions,
|
|
8
|
+
resolveRequestLanguage,
|
|
9
|
+
} from '@modern-js/i18n-runtime-extensions/request-language';
|
|
10
|
+
export {
|
|
11
|
+
createI18nSsrHandoffInlineData,
|
|
12
|
+
I18N_SSR_HANDOFF_ELEMENT_ID,
|
|
13
|
+
type I18nSsrHandoffPayload,
|
|
14
|
+
type I18nSsrHandoffResources,
|
|
15
|
+
readI18nSsrHandoff,
|
|
16
|
+
} from '@modern-js/i18n-runtime-extensions/ssrLanguageHandoff';
|
|
17
|
+
// Renderer-neutral pieces an application entry composes with these bindings.
|
|
18
|
+
export {
|
|
19
|
+
type CreateI18nUrlRewriteOptions,
|
|
20
|
+
createI18nUrlRewrite,
|
|
21
|
+
type I18nLocationRewrite,
|
|
22
|
+
} from '@modern-js/i18n-runtime-extensions/urlRewrite';
|
|
23
|
+
export { I18nContext } from './context';
|
|
24
|
+
export { I18nProvider, type I18nProviderProps } from './I18nProvider';
|
|
25
|
+
export {
|
|
26
|
+
LocalizedLink,
|
|
27
|
+
type LocalizedLinkProps,
|
|
28
|
+
useIsLocalizedActive,
|
|
29
|
+
} from './LocalizedLink';
|
|
30
|
+
export {
|
|
31
|
+
type OctaneLatestLanguageSyncOptions,
|
|
32
|
+
useLatestLanguageSync,
|
|
33
|
+
} from './languageSync';
|
|
34
|
+
export type { I18nContextValue, I18nInstanceLike } from './types';
|
|
35
|
+
export { type UseI18nReturn, useI18n } from './useI18n';
|