@bleedingdev/modern-js-renderer-octane 0.0.0-stage → 3.9.3-ultramodern.10
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 +421 -0
- package/dist/cjs/entry-application.js +88 -0
- package/dist/cjs/entry-client.js +95 -0
- package/dist/cjs/entry-server.js +135 -0
- package/dist/cjs/federation-context.js +72 -0
- package/dist/cjs/federation.js +205 -0
- package/dist/cjs/i18n/I18nProvider.js +59 -0
- package/dist/cjs/i18n/LocalizedLink.js +80 -0
- package/dist/cjs/i18n/context.js +41 -0
- package/dist/cjs/i18n/index.js +83 -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/i18n-external-snapshot.js +645 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/manifest.js +167 -0
- package/dist/cjs/plugin/compiler.js +122 -0
- package/dist/cjs/plugin/create.js +266 -0
- package/dist/cjs/plugin/index.js +151 -0
- package/dist/cjs/plugin/profile.js +91 -0
- package/dist/cjs/plugin/source.js +56 -0
- package/dist/cjs/router-client.js +139 -0
- package/dist/cjs/router-external-snapshot.js +291 -0
- package/dist/cjs/router-handler.js +171 -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 +100 -0
- package/dist/cjs/router-snapshot.js +270 -0
- package/dist/cjs/router.js +218 -0
- package/dist/cjs/routes.js +190 -0
- package/dist/cjs/server.js +245 -0
- package/dist/cjs/typecheck.js +615 -0
- 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/bootstrap.mjs +24 -0
- package/dist/esm/client.mjs +377 -0
- package/dist/esm/entry-application.mjs +44 -0
- package/dist/esm/entry-client.mjs +57 -0
- package/dist/esm/entry-server.mjs +91 -0
- package/dist/esm/federation-context.mjs +25 -0
- package/dist/esm/federation.mjs +167 -0
- package/dist/esm/i18n/I18nProvider.mjs +21 -0
- package/dist/esm/i18n/LocalizedLink.mjs +42 -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/i18n-external-snapshot.mjs +576 -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-external-snapshot.mjs +244 -0
- package/dist/esm/router-handler.mjs +133 -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 +11 -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 +204 -0
- 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/bootstrap.mjs +24 -0
- package/dist/esm-node/manifest.mjs +117 -0
- package/dist/esm-node/plugin/compiler.mjs +67 -0
- package/dist/esm-node/plugin/create.mjs +228 -0
- package/dist/esm-node/plugin/index.mjs +103 -0
- package/dist/esm-node/plugin/profile.mjs +53 -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 +40 -0
- package/dist/types/entry-application.d.ts +19 -0
- package/dist/types/entry-client.d.ts +10 -0
- package/dist/types/entry-server.d.ts +14 -0
- package/dist/types/federation-context.d.ts +28 -0
- package/dist/types/federation.d.ts +20 -0
- package/dist/types/i18n/I18nProvider.d.ts +8 -0
- package/dist/types/i18n/LocalizedLink.d.ts +14 -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/i18n-external-snapshot.d.ts +43 -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-external-snapshot.d.ts +10 -0
- package/dist/types/router-handler.d.ts +13 -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 +11 -0
- package/dist/types/router-snapshot.d.ts +14 -0
- package/dist/types/router.d.ts +30 -0
- package/dist/types/routes.d.ts +20 -0
- package/dist/types/server.d.ts +47 -0
- package/dist/types/typecheck.d.ts +31 -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 +278 -4
- package/src/bootstrap.ts +60 -0
- package/src/client.ts +568 -0
- package/src/entry-application.ts +76 -0
- package/src/entry-client.ts +90 -0
- package/src/entry-server.ts +153 -0
- package/src/federation-context.ts +53 -0
- package/src/federation.ts +307 -0
- package/src/i18n/I18nProvider.ts +47 -0
- package/src/i18n/LocalizedLink.ts +86 -0
- package/src/i18n/context.ts +12 -0
- package/src/i18n/index.ts +31 -0
- package/src/i18n/languageSync.ts +132 -0
- package/src/i18n/types.ts +36 -0
- package/src/i18n/useI18n.ts +119 -0
- package/src/i18n-external-snapshot.ts +803 -0
- package/src/index.ts +10 -0
- package/src/manifest.ts +253 -0
- package/src/plugin/compiler-manifest.cjs +204 -0
- package/src/plugin/compiler.ts +104 -0
- package/src/plugin/create.ts +273 -0
- package/src/plugin/index.ts +107 -0
- package/src/plugin/profile.ts +50 -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-external-snapshot.ts +363 -0
- package/src/router-handler.ts +185 -0
- package/src/router-server-cleanup.ts +11 -0
- package/src/router-server-snapshot.ts +564 -0
- package/src/router-server.ts +21 -0
- package/src/router-snapshot.ts +251 -0
- package/src/router.ts +192 -0
- package/src/routes.ts +274 -0
- package/src/server.ts +340 -0
- package/src/typecheck.ts +723 -0
- package/src/worker-federation-context.ts +51 -0
- package/src/worker-federation-contexts.ts +91 -0
- package/src/worker-federation.ts +120 -0
- package/README.md +0 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
|
|
2
|
+
import {
|
|
3
|
+
type NativeClientEntryOptions,
|
|
4
|
+
type NativeI18nInstance,
|
|
5
|
+
startNativeClientEntry,
|
|
6
|
+
} from '@modern-js/renderer-core/entry-client';
|
|
7
|
+
import { createNativeRouter } from '@modern-js/renderer-core/router';
|
|
8
|
+
import type { ComponentBody } from 'octane';
|
|
9
|
+
import {
|
|
10
|
+
hydrateOctaneApplication,
|
|
11
|
+
mountOctaneApplication,
|
|
12
|
+
type OctaneApplicationModule,
|
|
13
|
+
readOctaneDocumentBootstrap,
|
|
14
|
+
} from './client';
|
|
15
|
+
import {
|
|
16
|
+
componentView,
|
|
17
|
+
localizedRouterRoot,
|
|
18
|
+
octaneRouterFactory,
|
|
19
|
+
} from './entry-application';
|
|
20
|
+
import {
|
|
21
|
+
OctaneRouterRoot,
|
|
22
|
+
prepareOctaneRouterHydration,
|
|
23
|
+
} from './router-client';
|
|
24
|
+
import type {
|
|
25
|
+
OctaneWorkerFederationFactory,
|
|
26
|
+
OctaneWorkerFederationScope,
|
|
27
|
+
} from './worker-federation-context';
|
|
28
|
+
|
|
29
|
+
export interface OctaneClientEntryOptions
|
|
30
|
+
extends NativeClientEntryOptions<NativeI18nInstance, LocalisedUrlsOption> {
|
|
31
|
+
/** The native client compilation hash, `__webpack_hash__`. */
|
|
32
|
+
readonly nativeHydrationBuildId: string;
|
|
33
|
+
readonly workerFederation?:
|
|
34
|
+
| OctaneWorkerFederationScope
|
|
35
|
+
| OctaneWorkerFederationFactory;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const LocalizedRouterRoot = localizedRouterRoot(OctaneRouterRoot);
|
|
39
|
+
|
|
40
|
+
/** Start a generated Octane browser entry: mount, or hydrate the server document. */
|
|
41
|
+
export function startNativeClient(options: OctaneClientEntryOptions): void {
|
|
42
|
+
const { nativeHydrationBuildId } = options;
|
|
43
|
+
startNativeClientEntry(options, {
|
|
44
|
+
readBootstrap: (document, identity) =>
|
|
45
|
+
readOctaneDocumentBootstrap(document, identity, nativeHydrationBuildId),
|
|
46
|
+
createRouter: (application, routerOptions) =>
|
|
47
|
+
createNativeRouter(application, routerOptions, octaneRouterFactory),
|
|
48
|
+
disposeRouter: router => router.history.destroy(),
|
|
49
|
+
async prepareRouter(router, hydrating, signal) {
|
|
50
|
+
// Hydration adopts the server's matches; a fresh mount loads its own.
|
|
51
|
+
if (hydrating) await prepareOctaneRouterHydration(router, { signal });
|
|
52
|
+
else await router.load();
|
|
53
|
+
},
|
|
54
|
+
async start({ root, identity, bootstrap, signal, load }) {
|
|
55
|
+
const application = {
|
|
56
|
+
container: root,
|
|
57
|
+
identity,
|
|
58
|
+
nativeHydrationBuildId,
|
|
59
|
+
federation: options.federation,
|
|
60
|
+
workerFederation: options.workerFederation,
|
|
61
|
+
documentId: bootstrap?.documentId,
|
|
62
|
+
signal,
|
|
63
|
+
// Hydration installs the native signal bridge before this importer runs.
|
|
64
|
+
load: async (): Promise<OctaneApplicationModule> => {
|
|
65
|
+
const view = await load();
|
|
66
|
+
if (view.kind === 'component')
|
|
67
|
+
return { default: componentView(view.component, view.i18n) };
|
|
68
|
+
return view.i18n
|
|
69
|
+
? {
|
|
70
|
+
default: LocalizedRouterRoot as ComponentBody,
|
|
71
|
+
props: { router: view.router, i18n: view.i18n },
|
|
72
|
+
}
|
|
73
|
+
: {
|
|
74
|
+
default: OctaneRouterRoot as ComponentBody,
|
|
75
|
+
props: { router: view.router },
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const handle = bootstrap?.hydrating
|
|
80
|
+
? await hydrateOctaneApplication({
|
|
81
|
+
...application,
|
|
82
|
+
documentIdentity: bootstrap.identity,
|
|
83
|
+
documentId: bootstrap.documentId,
|
|
84
|
+
documentNativeHydrationBuildId: bootstrap.nativeHydrationBuildId,
|
|
85
|
+
})
|
|
86
|
+
: await mountOctaneApplication(application);
|
|
87
|
+
return () => handle.dispose();
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
createSsrStreamResponse,
|
|
27
|
+
getSsrStatus,
|
|
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 { createNativeWorkerFederationRuntime } from './worker-federation';
|
|
56
|
+
|
|
57
|
+
export interface OctaneServerEntryOptions
|
|
58
|
+
extends NativeServerEntryOptions<NativeI18nInstance, LocalisedUrlsOption> {
|
|
59
|
+
readonly workerFederationLocale?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const LocalizedRouterServer = localizedRouterRoot(OctaneRouterServer);
|
|
63
|
+
|
|
64
|
+
/** The native transport handlers of a generated Octane server entry. */
|
|
65
|
+
export function createNativeServerEntry(
|
|
66
|
+
options: OctaneServerEntryOptions,
|
|
67
|
+
): NativeServerEntry {
|
|
68
|
+
return createServerEntry(options, {
|
|
69
|
+
document: (document, nativeManifest, identity) => ({
|
|
70
|
+
...document,
|
|
71
|
+
nativeHydrationBuildId: validateOctaneModuleManifest(
|
|
72
|
+
nativeManifest,
|
|
73
|
+
identity,
|
|
74
|
+
).nativeHydrationBuildId,
|
|
75
|
+
}),
|
|
76
|
+
run: (_session, callback) => callback(),
|
|
77
|
+
respond: (_session, response) => response,
|
|
78
|
+
createRouter: (application, routerOptions) =>
|
|
79
|
+
createNativeRouter(application, routerOptions, octaneRouterFactory),
|
|
80
|
+
renderCSR: ({ session }, document) =>
|
|
81
|
+
renderOctaneCSRDocument({ session, document }),
|
|
82
|
+
renderComponent: (
|
|
83
|
+
{ session, workerFederation },
|
|
84
|
+
component,
|
|
85
|
+
document,
|
|
86
|
+
i18n,
|
|
87
|
+
) =>
|
|
88
|
+
renderOctaneApplication({
|
|
89
|
+
session,
|
|
90
|
+
App: componentView(component, i18n) as ServerRenderNode,
|
|
91
|
+
document,
|
|
92
|
+
federation: options.federation,
|
|
93
|
+
workerFederation,
|
|
94
|
+
workerFederationLocale: options.workerFederationLocale,
|
|
95
|
+
}),
|
|
96
|
+
async renderRoutes({
|
|
97
|
+
request,
|
|
98
|
+
context: { session, workerFederation },
|
|
99
|
+
router,
|
|
100
|
+
outcomes,
|
|
101
|
+
document,
|
|
102
|
+
forbiddenValues,
|
|
103
|
+
i18n,
|
|
104
|
+
}) {
|
|
105
|
+
const response = await createOctaneRequestHandler({
|
|
106
|
+
request,
|
|
107
|
+
session,
|
|
108
|
+
createRouter: () => router,
|
|
109
|
+
serialization: { forbiddenValues },
|
|
110
|
+
})(async ({ router, responseHeaders }) => {
|
|
111
|
+
const metadata = mergeDataResponseMetadata(outcomes, {
|
|
112
|
+
status: getSsrStatus(router),
|
|
113
|
+
});
|
|
114
|
+
// Route headers() take precedence over loader singleton fields; list
|
|
115
|
+
// fields keep both.
|
|
116
|
+
const policy = dataMetadataToDocumentPolicy({
|
|
117
|
+
...metadata,
|
|
118
|
+
headers: mergeHeaderFields(
|
|
119
|
+
collectDataHeaders(responseHeaders),
|
|
120
|
+
metadata.headers,
|
|
121
|
+
),
|
|
122
|
+
});
|
|
123
|
+
session.resolveResponse(
|
|
124
|
+
withOctaneDocumentFields(policy, responseHeaders),
|
|
125
|
+
);
|
|
126
|
+
const rendered = await renderOctaneApplication({
|
|
127
|
+
session,
|
|
128
|
+
...(i18n
|
|
129
|
+
? {
|
|
130
|
+
App: LocalizedRouterServer as ServerRenderNode,
|
|
131
|
+
props: { router, i18n },
|
|
132
|
+
}
|
|
133
|
+
: {
|
|
134
|
+
App: OctaneRouterServer as ServerRenderNode,
|
|
135
|
+
props: { router },
|
|
136
|
+
}),
|
|
137
|
+
document,
|
|
138
|
+
federation: options.federation,
|
|
139
|
+
workerFederation,
|
|
140
|
+
workerFederationLocale: options.workerFederationLocale,
|
|
141
|
+
router,
|
|
142
|
+
});
|
|
143
|
+
return createSsrStreamResponse(router, rendered);
|
|
144
|
+
});
|
|
145
|
+
return session.committedPolicy
|
|
146
|
+
? response
|
|
147
|
+
: mergeDataResponseIntoResponse(
|
|
148
|
+
response,
|
|
149
|
+
mergeDataResponseMetadata(outcomes, { status: response.status }),
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -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,307 @@
|
|
|
1
|
+
import { loadFederatedModule } from '@modern-js/renderer-core/federation';
|
|
2
|
+
import {
|
|
3
|
+
type ComponentBody,
|
|
4
|
+
createElement,
|
|
5
|
+
decodeExternalSnapshot,
|
|
6
|
+
externalSnapshotBoundary,
|
|
7
|
+
hookSlots,
|
|
8
|
+
lazy,
|
|
9
|
+
type OctaneNode,
|
|
10
|
+
preinit,
|
|
11
|
+
publisherBoundary,
|
|
12
|
+
Suspense,
|
|
13
|
+
serializeExternalSnapshotRequest,
|
|
14
|
+
useContext,
|
|
15
|
+
useEffect,
|
|
16
|
+
} from 'octane';
|
|
17
|
+
import {
|
|
18
|
+
FederationContext,
|
|
19
|
+
type OctaneFederationScope,
|
|
20
|
+
} from './federation-context';
|
|
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
|
+
|
|
29
|
+
/** The native component shape exposed by a same-renderer Octane remote. */
|
|
30
|
+
export interface FederatedModule<P extends object> {
|
|
31
|
+
readonly default: ComponentBody<P>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FederatedComponentOptions {
|
|
35
|
+
/** Native content shown while loading, or after a failed server load. */
|
|
36
|
+
readonly fallback?: () => OctaneNode;
|
|
37
|
+
/** A positive load limit; the server defaults to 3000ms. */
|
|
38
|
+
readonly timeout?: number;
|
|
39
|
+
/** Explicit registered server projections required by an application remote. */
|
|
40
|
+
readonly contextKeys?: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function withTimeout<T>(
|
|
44
|
+
promise: Promise<T>,
|
|
45
|
+
timeout: number | undefined,
|
|
46
|
+
label: string,
|
|
47
|
+
): Promise<T> {
|
|
48
|
+
if (timeout === undefined) return promise;
|
|
49
|
+
return new Promise<T>((resolve, reject) => {
|
|
50
|
+
const timer = setTimeout(
|
|
51
|
+
() =>
|
|
52
|
+
reject(
|
|
53
|
+
new Error(`Remote module ${label} did not load within ${timeout}ms`),
|
|
54
|
+
),
|
|
55
|
+
timeout,
|
|
56
|
+
);
|
|
57
|
+
(timer as { unref?: () => void }).unref?.();
|
|
58
|
+
promise.then(
|
|
59
|
+
value => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
resolve(value);
|
|
62
|
+
},
|
|
63
|
+
error => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
reject(error);
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// This library wrapper is published as plain JavaScript, outside app compilation.
|
|
72
|
+
const publisherRegistrationEffectSlot = Symbol(hookSlots(1));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Render a native Octane remote by its `remote/Expose` id or a browser loader.
|
|
76
|
+
* Native Suspense owns server fallbacks, streamed styles and hydration adoption;
|
|
77
|
+
* browser failures reach the nearest native ErrorBoundary. A loader function is
|
|
78
|
+
* browser-only; use an id to render through the host's server federation runtime.
|
|
79
|
+
*/
|
|
80
|
+
export function federatedComponent<P extends object = Record<string, never>>(
|
|
81
|
+
remote: string | (() => Promise<FederatedModule<P>>),
|
|
82
|
+
options: FederatedComponentOptions = {},
|
|
83
|
+
): ComponentBody<P> {
|
|
84
|
+
if (
|
|
85
|
+
typeof remote === 'string'
|
|
86
|
+
? !/^[^/]+\/.+/u.test(remote)
|
|
87
|
+
: typeof remote !== 'function'
|
|
88
|
+
) {
|
|
89
|
+
throw new TypeError(
|
|
90
|
+
"federatedComponent requires a remote id such as 'remote/Widget' or a loader",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
options.timeout !== undefined &&
|
|
95
|
+
(!Number.isFinite(options.timeout) || options.timeout <= 0)
|
|
96
|
+
) {
|
|
97
|
+
throw new TypeError(
|
|
98
|
+
'federatedComponent timeout must be a positive number of milliseconds',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const id = typeof remote === 'string' ? remote : undefined;
|
|
102
|
+
const standalone: OctaneFederationScope = {
|
|
103
|
+
instance: undefined,
|
|
104
|
+
server: typeof document === 'undefined',
|
|
105
|
+
};
|
|
106
|
+
// Native lazy caches its module. Scope the cache to one application/response,
|
|
107
|
+
// so two hosts using the same component definition cannot share a remote.
|
|
108
|
+
const components = new WeakMap<object, ComponentBody<P>>();
|
|
109
|
+
const workerOwners = new WeakMap<
|
|
110
|
+
OctaneFederationScope,
|
|
111
|
+
WeakMap<OctaneWorkerFederationScope, Map<string, object>>
|
|
112
|
+
>();
|
|
113
|
+
|
|
114
|
+
return props => {
|
|
115
|
+
const context = useContext(FederationContext) ?? standalone;
|
|
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);
|
|
140
|
+
if (!Remote) {
|
|
141
|
+
Remote = lazy(async () => {
|
|
142
|
+
try {
|
|
143
|
+
if (context.server && !id) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
'A federatedComponent loader renders in the browser only; use a remote id to server-render it',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
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(
|
|
237
|
+
Promise.resolve().then(() =>
|
|
238
|
+
id
|
|
239
|
+
? loadFederatedModule<FederatedModule<P>>(context.instance, id)
|
|
240
|
+
: (remote as () => Promise<FederatedModule<P>>)().then(
|
|
241
|
+
module => ({ module, assets: undefined }),
|
|
242
|
+
),
|
|
243
|
+
),
|
|
244
|
+
options.timeout ?? (context.server ? 3000 : undefined),
|
|
245
|
+
id ?? 'loader',
|
|
246
|
+
);
|
|
247
|
+
if (typeof module?.default !== 'function') {
|
|
248
|
+
throw new TypeError(
|
|
249
|
+
'A federated Octane module must default-export a native component',
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (context.server && !assets) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Cannot server-render ${id}: the host has no browser assets for this remote`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
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
|
+
});
|
|
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
|
+
);
|
|
280
|
+
// Resource APIs require an active native render. Late styles travel
|
|
281
|
+
// in Octane's resource carrier ahead of the remote's HTML segment.
|
|
282
|
+
for (const href of assets?.css ?? []) {
|
|
283
|
+
preinit(href, {
|
|
284
|
+
as: 'style',
|
|
285
|
+
precedence: 'federated',
|
|
286
|
+
...(context.nonce === undefined
|
|
287
|
+
? {}
|
|
288
|
+
: { nonce: context.nonce }),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return createElement(component, props);
|
|
292
|
+
};
|
|
293
|
+
return { default: Loaded };
|
|
294
|
+
} catch (error) {
|
|
295
|
+
// A later mount or ErrorBoundary reset may retry a failed load.
|
|
296
|
+
if (!context.server) components.delete(owner);
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
components.set(owner, Remote);
|
|
301
|
+
}
|
|
302
|
+
return createElement(Suspense, {
|
|
303
|
+
fallback: options.fallback,
|
|
304
|
+
children: createElement(Remote, props),
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createElement, hookSlots, useSyncExternalStore } from 'octane';
|
|
2
|
+
import { I18nContext } from './context';
|
|
3
|
+
import type { I18nContextValue } 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 languageStoreSlot = Symbol(hookSlots(1));
|
|
26
|
+
|
|
27
|
+
export function I18nProvider(props: I18nProviderProps) {
|
|
28
|
+
const instance = props.instance;
|
|
29
|
+
const language = useSyncExternalStore(
|
|
30
|
+
notify => {
|
|
31
|
+
instance.on?.('languageChanged', notify);
|
|
32
|
+
return () => instance.off?.('languageChanged', notify);
|
|
33
|
+
},
|
|
34
|
+
() => instance.language,
|
|
35
|
+
() => instance.language,
|
|
36
|
+
languageStoreSlot,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const value: I18nContextValue = {
|
|
40
|
+
instance: props.instance,
|
|
41
|
+
languages: props.languages,
|
|
42
|
+
localisedUrls: props.localisedUrls,
|
|
43
|
+
language,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return createElement(I18nContext, { value, children: props.children });
|
|
47
|
+
}
|