@bleedingdev/modern-js-renderer-octane 3.9.3-ultramodern.1 → 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/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/router-handler.ts
CHANGED
|
@@ -91,8 +91,9 @@ function requestHeaders(router: AnyRouter): Headers {
|
|
|
91
91
|
);
|
|
92
92
|
appendHeaders(headers, field?.value);
|
|
93
93
|
}
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
94
|
+
const result = router._serverResult;
|
|
95
|
+
if (result?.type === 'redirect')
|
|
96
|
+
appendHeaders(headers, result.redirect.headers);
|
|
96
97
|
return headers;
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -117,7 +118,6 @@ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
|
|
|
117
118
|
...serialization,
|
|
118
119
|
forbiddenValues: [request, ...(serialization?.forbiddenValues ?? [])],
|
|
119
120
|
});
|
|
120
|
-
session.registerCleanup(() => router.cancelMatches());
|
|
121
121
|
|
|
122
122
|
const { url } = getNormalizedURL(request.url, 'http://localhost');
|
|
123
123
|
const href = url.href.replace(url.origin, '');
|
|
@@ -126,14 +126,14 @@ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
|
|
|
126
126
|
history: createMemoryHistory({ initialEntries: [href] }),
|
|
127
127
|
origin: router.options.origin ?? getOrigin(request),
|
|
128
128
|
});
|
|
129
|
-
await withAbort(router.load(), session.signal);
|
|
129
|
+
await withAbort(router.load({ _signal: session.signal }), session.signal);
|
|
130
130
|
guard.assertActive();
|
|
131
131
|
|
|
132
132
|
// A native redirect is a terminal HTTP Response. It must never enter
|
|
133
133
|
// the public serializer as a match error or document payload.
|
|
134
|
-
const
|
|
135
|
-
if (redirect) {
|
|
136
|
-
const resolved =
|
|
134
|
+
const loadResult = router._serverResult;
|
|
135
|
+
if (loadResult?.type === 'redirect') {
|
|
136
|
+
const resolved = loadResult.redirect;
|
|
137
137
|
const headers = requestHeaders(router);
|
|
138
138
|
return new Response(null, {
|
|
139
139
|
status: resolved.status,
|
|
@@ -146,7 +146,10 @@ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
|
|
|
146
146
|
const serverSsr = router.serverSsr;
|
|
147
147
|
if (!serverSsr)
|
|
148
148
|
throw new Error('Native Octane router SSR state is required');
|
|
149
|
-
await withAbort(
|
|
149
|
+
await withAbort(
|
|
150
|
+
serverSsr.dehydrate({ signal: session.signal }),
|
|
151
|
+
session.signal,
|
|
152
|
+
);
|
|
150
153
|
guard.assertActive();
|
|
151
154
|
const responseHeaders = requestHeaders(router);
|
|
152
155
|
// A callback that ignores cancellation must not hold the router SSR
|
package/src/router-server.ts
CHANGED
|
@@ -5,8 +5,6 @@ import { createElement, type ServerRenderNode } from 'octane/server';
|
|
|
5
5
|
export * from '@octanejs/tanstack-router/ssr/server';
|
|
6
6
|
export type { OctaneRequestHandlerOptions } from './router-handler';
|
|
7
7
|
export { createOctaneRequestHandler } from './router-handler';
|
|
8
|
-
export type { OctaneRouterInjectionOptions } from './router-injection';
|
|
9
|
-
export { createOctaneRouterInjection } from './router-injection';
|
|
10
8
|
export type {
|
|
11
9
|
OctaneRouterSerializationGuard,
|
|
12
10
|
OctaneRouterSerializationOptions,
|
package/src/router.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useMatch,
|
|
10
10
|
useRouter,
|
|
11
11
|
} from '@octanejs/tanstack-router';
|
|
12
|
-
import { createSubSlot, useCallback,
|
|
12
|
+
import { createSubSlot, useCallback, useLayoutEffect, useRef } from 'octane';
|
|
13
13
|
import { assertOctaneIdentity } from './bootstrap';
|
|
14
14
|
import { resolveRouteData } from './routes';
|
|
15
15
|
|
|
@@ -161,7 +161,7 @@ export function useOctaneRouteAction(...args: unknown[]) {
|
|
|
161
161
|
const identity = useApplicationIdentity();
|
|
162
162
|
const routeId = useApplicationRouteId();
|
|
163
163
|
const owner = useRef<AbortController | null>(null, actionSlot(slot, 'owner'));
|
|
164
|
-
|
|
164
|
+
useLayoutEffect(
|
|
165
165
|
() => {
|
|
166
166
|
const controller = new AbortController();
|
|
167
167
|
owner.current = controller;
|
package/src/routes.ts
CHANGED
|
@@ -29,6 +29,13 @@ import {
|
|
|
29
29
|
} from '@octanejs/tanstack-router';
|
|
30
30
|
import { createElement, Fragment } from 'octane';
|
|
31
31
|
|
|
32
|
+
const fileRouteHeads = new WeakMap<AnyRoute, AnyRoute['options']['head']>();
|
|
33
|
+
|
|
34
|
+
/** Head callbacks execute on the host; only the factory's original binding is admitted. */
|
|
35
|
+
export function hasUnchangedOctaneFileRouteHead(route: AnyRoute): boolean {
|
|
36
|
+
return fileRouteHeads.get(route) === route.options.head;
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
export type {
|
|
33
40
|
FileSystemDataModule,
|
|
34
41
|
FileSystemRouteOptions,
|
|
@@ -59,7 +66,7 @@ export function resolveRouteData(
|
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
interface ReactiveNativeMatch {
|
|
62
|
-
get(): AnyRouteMatch;
|
|
69
|
+
get(): AnyRouteMatch | undefined;
|
|
63
70
|
subscribe(callback: () => void): { unsubscribe(): void };
|
|
64
71
|
}
|
|
65
72
|
|
|
@@ -75,24 +82,18 @@ function isReactiveNativeMatch(value: unknown): value is ReactiveNativeMatch {
|
|
|
75
82
|
/** A transport failure belongs to the native generation that fetched it. */
|
|
76
83
|
function observeRouteCompletion(
|
|
77
84
|
router: AnyRouter,
|
|
85
|
+
routeId: string,
|
|
78
86
|
controller: AbortController,
|
|
79
87
|
signal: AbortSignal,
|
|
80
88
|
completion: Promise<void>,
|
|
81
89
|
) {
|
|
82
90
|
void completion.catch(error => {
|
|
83
91
|
if (signal.aborted) return;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!match) return;
|
|
90
|
-
const store = [
|
|
91
|
-
router.stores.pendingMatchStores.get(match.id),
|
|
92
|
-
router.stores.matchStores.get(match.id),
|
|
93
|
-
router.stores.cachedMatchStores.get(match.id),
|
|
94
|
-
].find(candidate => candidate?.get().abortController === controller);
|
|
95
|
-
if (!store) return;
|
|
92
|
+
const route = Object.values(router.routesById).find(
|
|
93
|
+
candidate => candidate.options.staticData?.ultramodernRouteId === routeId,
|
|
94
|
+
);
|
|
95
|
+
if (!route) return;
|
|
96
|
+
const store = router.stores.getMatchStore(route.id);
|
|
96
97
|
let subscription: { unsubscribe(): void } | undefined;
|
|
97
98
|
let settled = false;
|
|
98
99
|
const cleanup = () => {
|
|
@@ -103,9 +104,10 @@ function observeRouteCompletion(
|
|
|
103
104
|
settled = true;
|
|
104
105
|
cleanup();
|
|
105
106
|
};
|
|
106
|
-
const publish = () => {
|
|
107
|
+
const publish = async () => {
|
|
107
108
|
if (settled) return;
|
|
108
109
|
const current = store.get();
|
|
110
|
+
if (!current) return;
|
|
109
111
|
if (
|
|
110
112
|
current.abortController !== controller ||
|
|
111
113
|
signal.aborted ||
|
|
@@ -121,23 +123,29 @@ function observeRouteCompletion(
|
|
|
121
123
|
const route = router.routesById[current.routeId];
|
|
122
124
|
let failure = error;
|
|
123
125
|
try {
|
|
126
|
+
await route?.options.errorComponent?.preload?.();
|
|
127
|
+
if (signal.aborted || store.get()?.abortController !== controller)
|
|
128
|
+
return;
|
|
124
129
|
route?.options.onError?.(failure);
|
|
125
130
|
} catch (onErrorFailure) {
|
|
126
131
|
failure = onErrorFailure;
|
|
127
132
|
}
|
|
128
|
-
//
|
|
133
|
+
// The error component is ready before publishing a late transport failure.
|
|
129
134
|
if (signal.aborted) return;
|
|
130
|
-
router.
|
|
131
|
-
previous
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
router.stores.setMatches(
|
|
136
|
+
router.stores.matches.get().map(previous =>
|
|
137
|
+
previous.routeId === route.id &&
|
|
138
|
+
previous.abortController === controller &&
|
|
139
|
+
previous.status === 'success' &&
|
|
140
|
+
!previous.isFetching
|
|
141
|
+
? {
|
|
142
|
+
...previous,
|
|
143
|
+
error: failure,
|
|
144
|
+
status: 'error',
|
|
145
|
+
isFetching: false,
|
|
146
|
+
}
|
|
147
|
+
: previous,
|
|
148
|
+
),
|
|
141
149
|
);
|
|
142
150
|
};
|
|
143
151
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
@@ -203,6 +211,7 @@ export function createFileSystemRouteTree<Context = unknown>(
|
|
|
203
211
|
if (failed) throw failure;
|
|
204
212
|
observeRouteCompletion(
|
|
205
213
|
router,
|
|
214
|
+
route.id,
|
|
206
215
|
match.abortController,
|
|
207
216
|
signal,
|
|
208
217
|
outcome.completion,
|
|
@@ -239,6 +248,7 @@ export function createFileSystemRouteTree<Context = unknown>(
|
|
|
239
248
|
...rootBindings,
|
|
240
249
|
component: OctaneDocumentRoot,
|
|
241
250
|
});
|
|
251
|
+
fileRouteHeads.set(root, root.options.head);
|
|
242
252
|
|
|
243
253
|
function bind(
|
|
244
254
|
route: FileSystemRouteIR,
|
|
@@ -252,6 +262,7 @@ export function createFileSystemRouteTree<Context = unknown>(
|
|
|
252
262
|
...nativeRoutePath(route),
|
|
253
263
|
...bindings(route, errorComponent),
|
|
254
264
|
});
|
|
265
|
+
fileRouteHeads.set(native, native.options.head);
|
|
255
266
|
return native.addChildren(
|
|
256
267
|
route.children.map(child => bind(child, native, errorComponent)),
|
|
257
268
|
);
|
package/src/server.ts
CHANGED
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
type ResponsePolicy,
|
|
10
10
|
untilAborted,
|
|
11
11
|
} from '@modern-js/renderer-core/session';
|
|
12
|
+
import type { WorkerFederationTransport } from '@modern-js/renderer-core/worker-federation';
|
|
13
|
+
import type { AnyRouter } from '@octanejs/tanstack-router';
|
|
14
|
+
import { transformReadableStreamWithRouter } from '@octanejs/tanstack-router/ssr/server';
|
|
12
15
|
import {
|
|
13
16
|
createElement,
|
|
14
17
|
earlySignalBootstrapScript,
|
|
@@ -21,6 +24,7 @@ import {
|
|
|
21
24
|
assertOctaneIdentity,
|
|
22
25
|
} from './bootstrap';
|
|
23
26
|
import { createFederationScope, FederationRoot } from './federation-context';
|
|
27
|
+
import { WorkerFederationRoot } from './worker-federation-context';
|
|
24
28
|
|
|
25
29
|
export interface OctaneDocumentOptions {
|
|
26
30
|
/** One identity for this response, shared with the client's signal receiver. */
|
|
@@ -56,8 +60,12 @@ export interface RenderOctaneApplicationOptions<
|
|
|
56
60
|
readonly props?: unknown;
|
|
57
61
|
/** Native router serialization, owned by this request's router SSR context. */
|
|
58
62
|
readonly injection?: StreamOptions['injection'];
|
|
63
|
+
readonly router?: AnyRouter;
|
|
64
|
+
readonly routerSerializationTimeoutMs?: number;
|
|
59
65
|
/** This server compilation's native Module Federation runtime. */
|
|
60
66
|
readonly federation?: NativeFederationBinding;
|
|
67
|
+
readonly workerFederation?: WorkerFederationTransport;
|
|
68
|
+
readonly workerFederationLocale?: string;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
function prepareOctaneDocument<Bindings extends object>(
|
|
@@ -166,9 +174,12 @@ export async function renderOctaneApplication<Bindings extends object>(
|
|
|
166
174
|
session.startRendering();
|
|
167
175
|
|
|
168
176
|
let head = '';
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
// Worker SSR and browser activation must own the same provider ranges,
|
|
178
|
+
// even when only the browser needs a Module Federation loader.
|
|
179
|
+
const federation =
|
|
180
|
+
input.federation || input.workerFederation
|
|
181
|
+
? createFederationScope(input.federation, true, document.nonce)
|
|
182
|
+
: undefined;
|
|
172
183
|
const App = federation
|
|
173
184
|
? () =>
|
|
174
185
|
createElement(FederationRoot, {
|
|
@@ -179,7 +190,24 @@ export async function renderOctaneApplication<Bindings extends object>(
|
|
|
179
190
|
: input.App,
|
|
180
191
|
})
|
|
181
192
|
: input.App;
|
|
182
|
-
|
|
193
|
+
// Native SSR retries the application while lazy boundaries settle. Keep
|
|
194
|
+
// their request-owned context identity stable across those render passes.
|
|
195
|
+
const workerFederation = input.workerFederation
|
|
196
|
+
? {
|
|
197
|
+
transport: input.workerFederation,
|
|
198
|
+
locale: input.workerFederationLocale ?? 'default',
|
|
199
|
+
signal: session.signal,
|
|
200
|
+
}
|
|
201
|
+
: undefined;
|
|
202
|
+
const NativeApp = workerFederation
|
|
203
|
+
? () =>
|
|
204
|
+
createElement(WorkerFederationRoot, {
|
|
205
|
+
scope: workerFederation,
|
|
206
|
+
children:
|
|
207
|
+
typeof App === 'function' ? createElement(App, input.props) : App,
|
|
208
|
+
})
|
|
209
|
+
: App;
|
|
210
|
+
const nativeStream = await renderToReadableStream(NativeApp, input.props, {
|
|
183
211
|
signal: session.signal,
|
|
184
212
|
...(document.nonce === undefined ? {} : { nonce: document.nonce }),
|
|
185
213
|
earlySignalBootstrap: 'external',
|
|
@@ -188,6 +216,7 @@ export async function renderOctaneApplication<Bindings extends object>(
|
|
|
188
216
|
buildId: document.nativeHydrationBuildId,
|
|
189
217
|
documentId: document.documentId,
|
|
190
218
|
},
|
|
219
|
+
externalSnapshots: { documentId: document.documentId },
|
|
191
220
|
headChannel: 'separate',
|
|
192
221
|
onHeadReady(value) {
|
|
193
222
|
head = value;
|
|
@@ -292,7 +321,18 @@ export async function renderOctaneApplication<Bindings extends object>(
|
|
|
292
321
|
},
|
|
293
322
|
{ highWaterMark: 0 },
|
|
294
323
|
);
|
|
295
|
-
return session.respond(
|
|
324
|
+
return session.respond(
|
|
325
|
+
input.router
|
|
326
|
+
? transformReadableStreamWithRouter(input.router, body, {
|
|
327
|
+
signal: session.signal,
|
|
328
|
+
rendererSafePoint: 'script-close',
|
|
329
|
+
timeoutMs: input.routerSerializationTimeoutMs,
|
|
330
|
+
onAbort: reason => {
|
|
331
|
+
void session.fail(reason);
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
: body,
|
|
335
|
+
);
|
|
296
336
|
} catch (error) {
|
|
297
337
|
await session.fail(error);
|
|
298
338
|
throw error;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { WorkerFederationTransport } from '@modern-js/renderer-core/worker-federation';
|
|
2
|
+
import { createContext, createElement } from 'octane';
|
|
3
|
+
import {
|
|
4
|
+
FederationApplication,
|
|
5
|
+
type OctaneFederationScope,
|
|
6
|
+
} from './federation-context';
|
|
7
|
+
|
|
8
|
+
export interface OctaneWorkerFederationScope {
|
|
9
|
+
readonly transport: WorkerFederationTransport;
|
|
10
|
+
/** Compiled entry fallback; localized providers supply their live language. */
|
|
11
|
+
readonly locale: string;
|
|
12
|
+
/** Own document disposal; each lazy admission also owns its waiter deadline. */
|
|
13
|
+
readonly signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type OctaneWorkerFederationFactory = (
|
|
17
|
+
signal: AbortSignal,
|
|
18
|
+
) => OctaneWorkerFederationScope;
|
|
19
|
+
|
|
20
|
+
/** Each native document owns its transport and captured publisher authorities. */
|
|
21
|
+
export const WorkerFederationContext =
|
|
22
|
+
createContext<OctaneWorkerFederationScope | null>(null);
|
|
23
|
+
|
|
24
|
+
export function WorkerFederationRoot(props: {
|
|
25
|
+
readonly scope: OctaneWorkerFederationScope;
|
|
26
|
+
readonly children?: unknown;
|
|
27
|
+
}) {
|
|
28
|
+
return createElement(WorkerFederationContext, {
|
|
29
|
+
value: props.scope,
|
|
30
|
+
children: props.children,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Stable native root used for startup and application updates alike. */
|
|
35
|
+
export function WorkerFederationApplication(props: {
|
|
36
|
+
readonly scope: OctaneWorkerFederationScope;
|
|
37
|
+
readonly federation?: OctaneFederationScope;
|
|
38
|
+
readonly application: Parameters<
|
|
39
|
+
typeof FederationApplication
|
|
40
|
+
>[0]['application'];
|
|
41
|
+
}) {
|
|
42
|
+
return createElement(WorkerFederationRoot, {
|
|
43
|
+
scope: props.scope,
|
|
44
|
+
children: props.federation
|
|
45
|
+
? createElement(FederationApplication, {
|
|
46
|
+
scope: props.federation,
|
|
47
|
+
application: props.application,
|
|
48
|
+
})
|
|
49
|
+
: createElement(props.application.default, props.application.props),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { NativeLocationRewrite } from '@modern-js/renderer-core/router';
|
|
2
|
+
import {
|
|
3
|
+
type Context,
|
|
4
|
+
type ExternalSnapshotContextCodec,
|
|
5
|
+
registerExternalSnapshotContext,
|
|
6
|
+
useContext,
|
|
7
|
+
} from 'octane';
|
|
8
|
+
|
|
9
|
+
export const OCTANE_WORKER_CONTEXT_KEYS = Object.freeze({
|
|
10
|
+
router: 'ultramodern.octane.router-context.v1',
|
|
11
|
+
match: 'ultramodern.octane.match-context.v1',
|
|
12
|
+
i18n: 'ultramodern.octane.i18n-context.v1',
|
|
13
|
+
} as const);
|
|
14
|
+
|
|
15
|
+
type OctaneWorkerContextKey =
|
|
16
|
+
(typeof OCTANE_WORKER_CONTEXT_KEYS)[keyof typeof OCTANE_WORKER_CONTEXT_KEYS];
|
|
17
|
+
|
|
18
|
+
export interface OctaneWorkerContextCodec<T>
|
|
19
|
+
extends ExternalSnapshotContextCodec<T> {
|
|
20
|
+
readonly key: OctaneWorkerContextKey;
|
|
21
|
+
readonly dispose?: (value: T) => void | PromiseLike<void>;
|
|
22
|
+
readonly isProvided: (value: T) => boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const builtinKeys = Object.values(OCTANE_WORKER_CONTEXT_KEYS);
|
|
26
|
+
const contexts = new Map<OctaneWorkerContextKey, () => boolean>();
|
|
27
|
+
|
|
28
|
+
/** Optional renderer modules register their actual native context identities. */
|
|
29
|
+
export function registerOctaneWorkerContext<T>(
|
|
30
|
+
context: Context<T>,
|
|
31
|
+
codec: OctaneWorkerContextCodec<T>,
|
|
32
|
+
): () => void {
|
|
33
|
+
const { key, isProvided } = codec;
|
|
34
|
+
if (!builtinKeys.includes(key) || typeof isProvided !== 'function')
|
|
35
|
+
throw new TypeError('Invalid Octane Worker builtin context codec');
|
|
36
|
+
|
|
37
|
+
const unregister = registerExternalSnapshotContext(context, codec);
|
|
38
|
+
const hasProvider = () => isProvided(useContext(context));
|
|
39
|
+
contexts.set(key, hasProvider);
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
if (contexts.get(key) !== hasProvider) return;
|
|
43
|
+
unregister();
|
|
44
|
+
contexts.delete(key);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Read live providers on every render; explicit requirements retain native validation. */
|
|
49
|
+
export function useOctaneWorkerContextKeys(
|
|
50
|
+
explicit: readonly string[] = [],
|
|
51
|
+
): readonly string[] {
|
|
52
|
+
const selected: string[] = [];
|
|
53
|
+
for (const key of builtinKeys) {
|
|
54
|
+
const hasProvider = contexts.get(key);
|
|
55
|
+
if (hasProvider?.() && !explicit.includes(key)) selected.push(key);
|
|
56
|
+
}
|
|
57
|
+
return [...selected, ...explicit];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface OctaneWorkerRouterRewrite {
|
|
61
|
+
readonly capture: (rewrite: NativeLocationRewrite) => unknown;
|
|
62
|
+
readonly restore: (descriptor: unknown) => NativeLocationRewrite;
|
|
63
|
+
}
|
|
64
|
+
let routerRewrite: OctaneWorkerRouterRewrite | undefined;
|
|
65
|
+
|
|
66
|
+
/** The optional i18n owner supplies its own native rewrite provenance. */
|
|
67
|
+
export function registerOctaneWorkerRouterRewrite(
|
|
68
|
+
codec: OctaneWorkerRouterRewrite,
|
|
69
|
+
): () => void {
|
|
70
|
+
if (
|
|
71
|
+
routerRewrite ||
|
|
72
|
+
typeof codec.capture !== 'function' ||
|
|
73
|
+
typeof codec.restore !== 'function'
|
|
74
|
+
)
|
|
75
|
+
throw new TypeError(
|
|
76
|
+
'Invalid or duplicate Octane Worker router rewrite codec',
|
|
77
|
+
);
|
|
78
|
+
const captured = Object.freeze({
|
|
79
|
+
capture: codec.capture,
|
|
80
|
+
restore: codec.restore,
|
|
81
|
+
});
|
|
82
|
+
routerRewrite = captured;
|
|
83
|
+
return () => {
|
|
84
|
+
if (routerRewrite === captured) routerRewrite = undefined;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function getOctaneWorkerRouterRewrite():
|
|
88
|
+
| OctaneWorkerRouterRewrite
|
|
89
|
+
| undefined {
|
|
90
|
+
return routerRewrite;
|
|
91
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { NativeWorkerDispatchOptions } from '@modern-js/renderer-core/server';
|
|
2
|
+
import {
|
|
3
|
+
type AdmitWorkerRendererContract,
|
|
4
|
+
createWorkerFederationHandler,
|
|
5
|
+
createWorkerFederationTransport,
|
|
6
|
+
} from '@modern-js/renderer-core/worker-federation';
|
|
7
|
+
import {
|
|
8
|
+
type ExternalSnapshotRenderOptions,
|
|
9
|
+
prepareExternalSnapshotRequest,
|
|
10
|
+
releasePreparedExternalSnapshotRequest,
|
|
11
|
+
renderExternalSnapshot,
|
|
12
|
+
type ServerRenderNode,
|
|
13
|
+
serializeExternalSnapshot,
|
|
14
|
+
} from 'octane/server';
|
|
15
|
+
import { createFederationScope, FederationContext } from './federation-context';
|
|
16
|
+
import { WorkerFederationContext } from './worker-federation-context';
|
|
17
|
+
|
|
18
|
+
type NativeSnapshotRequest = Awaited<
|
|
19
|
+
ReturnType<typeof prepareExternalSnapshotRequest>
|
|
20
|
+
>;
|
|
21
|
+
type HandlerOptions = Parameters<
|
|
22
|
+
typeof createWorkerFederationHandler<NativeSnapshotRequest>
|
|
23
|
+
>[0];
|
|
24
|
+
|
|
25
|
+
function isNativeWorkerComponent(
|
|
26
|
+
value: unknown,
|
|
27
|
+
): value is Extract<ServerRenderNode, (props: never) => unknown> {
|
|
28
|
+
return typeof value === 'function';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The compiled registry pins its renderer publication and literal imports. */
|
|
32
|
+
export interface OctaneWorkerFederationRegistry {
|
|
33
|
+
initialize(
|
|
34
|
+
options: Omit<HandlerOptions, 'publication' | 'exposes'> & {
|
|
35
|
+
publication: unknown;
|
|
36
|
+
},
|
|
37
|
+
): (request: Request) => Promise<Response | undefined>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Created per dispatch; no publisher or native snapshot cache crosses documents. */
|
|
41
|
+
export function createNativeWorkerFederationRuntime<Bindings extends object>(
|
|
42
|
+
options: NativeWorkerDispatchOptions<Bindings>,
|
|
43
|
+
compiler: {
|
|
44
|
+
readonly admitRendererContract: AdmitWorkerRendererContract;
|
|
45
|
+
readonly registry?: OctaneWorkerFederationRegistry;
|
|
46
|
+
},
|
|
47
|
+
) {
|
|
48
|
+
const { resources } = options;
|
|
49
|
+
const bindings = options.bindings;
|
|
50
|
+
const transport = resources.workerFederationServices?.length
|
|
51
|
+
? createWorkerFederationTransport({
|
|
52
|
+
admitRendererContract: compiler.admitRendererContract,
|
|
53
|
+
services: resources.workerFederationServices.map(service => {
|
|
54
|
+
const binding: unknown =
|
|
55
|
+
bindings && Reflect.get(bindings, service.binding);
|
|
56
|
+
if (
|
|
57
|
+
!binding ||
|
|
58
|
+
(typeof binding !== 'object' && typeof binding !== 'function') ||
|
|
59
|
+
!('fetch' in binding) ||
|
|
60
|
+
typeof binding.fetch !== 'function'
|
|
61
|
+
)
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Native Worker federation requires service binding ${service.binding}.`,
|
|
64
|
+
);
|
|
65
|
+
return { ...service, fetch: binding.fetch.bind(binding) };
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
68
|
+
: undefined;
|
|
69
|
+
let handle: ((request: Request) => Promise<Response | undefined>) | undefined;
|
|
70
|
+
if (resources.workerFederation) {
|
|
71
|
+
if (!compiler.registry || !resources.workerFederationManifestUrl)
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Native Worker federation requires its compiled expose registry and pinned manifest URL.',
|
|
74
|
+
);
|
|
75
|
+
handle = compiler.registry.initialize({
|
|
76
|
+
publication: resources.workerFederation.worker,
|
|
77
|
+
manifestUrl: resources.workerFederationManifestUrl,
|
|
78
|
+
admitRendererContract: compiler.admitRendererContract,
|
|
79
|
+
admitNativeRequest(payload, { admitted, signal }) {
|
|
80
|
+
const initializeContexts: ExternalSnapshotRenderOptions['initializeContexts'] =
|
|
81
|
+
transport
|
|
82
|
+
? (provide, request) => {
|
|
83
|
+
provide(
|
|
84
|
+
FederationContext,
|
|
85
|
+
createFederationScope(undefined, true, request.nonce),
|
|
86
|
+
);
|
|
87
|
+
provide(WorkerFederationContext, {
|
|
88
|
+
transport,
|
|
89
|
+
locale: admitted.locale,
|
|
90
|
+
signal,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
: undefined;
|
|
94
|
+
return prepareExternalSnapshotRequest(payload, {
|
|
95
|
+
authority: admitted.coordinate.native,
|
|
96
|
+
signal,
|
|
97
|
+
...(initializeContexts ? { initializeContexts } : {}),
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
releaseNativeRequest: releasePreparedExternalSnapshotRequest,
|
|
101
|
+
async renderNativeSnapshot(module, request, { admitted }) {
|
|
102
|
+
const component =
|
|
103
|
+
module && typeof module === 'object' && 'default' in module
|
|
104
|
+
? module.default
|
|
105
|
+
: undefined;
|
|
106
|
+
if (!isNativeWorkerComponent(component))
|
|
107
|
+
throw new TypeError(
|
|
108
|
+
`Native Worker expose ${admitted.expose} requires a default Octane component.`,
|
|
109
|
+
);
|
|
110
|
+
return serializeExternalSnapshot(
|
|
111
|
+
await renderExternalSnapshot(component, request),
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
transport,
|
|
118
|
+
handleRequest: handle ?? (() => Promise.resolve(undefined)),
|
|
119
|
+
};
|
|
120
|
+
}
|