@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.
Files changed (100) hide show
  1. package/dist/cjs/client.js +56 -6
  2. package/dist/cjs/entry-application.js +7 -2
  3. package/dist/cjs/entry-client.js +3 -0
  4. package/dist/cjs/entry-server.js +13 -5
  5. package/dist/cjs/federation.js +108 -6
  6. package/dist/cjs/i18n/I18nProvider.js +6 -14
  7. package/dist/cjs/i18n/LocalizedLink.js +24 -33
  8. package/dist/cjs/i18n/index.js +0 -3
  9. package/dist/cjs/i18n-external-snapshot.js +645 -0
  10. package/dist/cjs/index.js +4 -4
  11. package/dist/cjs/manifest.js +2 -2
  12. package/dist/cjs/plugin/compiler.js +2 -5
  13. package/dist/cjs/plugin/create.js +27 -26
  14. package/dist/cjs/plugin/index.js +2 -2
  15. package/dist/cjs/plugin/profile.js +12 -10
  16. package/dist/cjs/router-client.js +1 -1
  17. package/dist/cjs/router-external-snapshot.js +291 -0
  18. package/dist/cjs/router-handler.js +11 -8
  19. package/dist/cjs/router-server.js +3 -11
  20. package/dist/cjs/router.js +1 -1
  21. package/dist/cjs/routes.js +20 -17
  22. package/dist/cjs/server.js +28 -7
  23. package/dist/cjs/worker-federation-context.js +63 -0
  24. package/dist/cjs/worker-federation-contexts.js +97 -0
  25. package/dist/cjs/worker-federation.js +97 -0
  26. package/dist/esm/client.mjs +56 -6
  27. package/dist/esm/entry-application.mjs +7 -2
  28. package/dist/esm/entry-client.mjs +3 -0
  29. package/dist/esm/entry-server.mjs +11 -6
  30. package/dist/esm/federation.mjs +110 -8
  31. package/dist/esm/i18n/I18nProvider.mjs +7 -15
  32. package/dist/esm/i18n/LocalizedLink.mjs +24 -30
  33. package/dist/esm/i18n/index.mjs +1 -1
  34. package/dist/esm/i18n-external-snapshot.mjs +576 -0
  35. package/dist/esm/index.mjs +4 -4
  36. package/dist/esm/manifest.mjs +2 -2
  37. package/dist/esm/router-client.mjs +1 -1
  38. package/dist/esm/router-external-snapshot.mjs +244 -0
  39. package/dist/esm/router-handler.mjs +11 -8
  40. package/dist/esm/router-server.mjs +0 -1
  41. package/dist/esm/router.mjs +2 -2
  42. package/dist/esm/routes.mjs +18 -18
  43. package/dist/esm/server.mjs +24 -3
  44. package/dist/esm/worker-federation-context.mjs +19 -0
  45. package/dist/esm/worker-federation-contexts.mjs +47 -0
  46. package/dist/esm/worker-federation.mjs +59 -0
  47. package/dist/esm-node/manifest.mjs +2 -2
  48. package/dist/esm-node/plugin/compiler.mjs +2 -5
  49. package/dist/esm-node/plugin/create.mjs +27 -26
  50. package/dist/esm-node/plugin/index.mjs +2 -2
  51. package/dist/esm-node/plugin/profile.mjs +12 -10
  52. package/dist/types/client.d.ts +5 -1
  53. package/dist/types/entry-client.d.ts +2 -0
  54. package/dist/types/entry-server.d.ts +4 -1
  55. package/dist/types/federation.d.ts +2 -0
  56. package/dist/types/i18n/LocalizedLink.d.ts +5 -32
  57. package/dist/types/i18n/context.d.ts +1 -1
  58. package/dist/types/i18n/index.d.ts +1 -1
  59. package/dist/types/i18n-external-snapshot.d.ts +43 -0
  60. package/dist/types/index.d.ts +4 -4
  61. package/dist/types/manifest.d.ts +2 -2
  62. package/dist/types/router-external-snapshot.d.ts +10 -0
  63. package/dist/types/router-server.d.ts +0 -2
  64. package/dist/types/routes.d.ts +2 -0
  65. package/dist/types/server.d.ts +6 -0
  66. package/dist/types/worker-federation-context.d.ts +34 -0
  67. package/dist/types/worker-federation-contexts.d.ts +25 -0
  68. package/dist/types/worker-federation.d.ts +20 -0
  69. package/package.json +84 -22
  70. package/src/client.ts +99 -16
  71. package/src/entry-application.ts +7 -3
  72. package/src/entry-client.ts +10 -0
  73. package/src/entry-server.ts +20 -9
  74. package/src/federation.ts +164 -15
  75. package/src/i18n/I18nProvider.ts +11 -20
  76. package/src/i18n/LocalizedLink.ts +46 -72
  77. package/src/i18n/context.ts +1 -1
  78. package/src/i18n/index.ts +1 -5
  79. package/src/i18n-external-snapshot.ts +803 -0
  80. package/src/index.ts +4 -4
  81. package/src/manifest.ts +2 -2
  82. package/src/plugin/compiler-manifest.cjs +27 -1
  83. package/src/plugin/compiler.ts +6 -14
  84. package/src/plugin/create.ts +27 -26
  85. package/src/plugin/index.ts +4 -2
  86. package/src/plugin/profile.ts +14 -10
  87. package/src/router-client.ts +1 -1
  88. package/src/router-external-snapshot.ts +363 -0
  89. package/src/router-handler.ts +11 -8
  90. package/src/router-server.ts +0 -2
  91. package/src/router.ts +2 -2
  92. package/src/routes.ts +37 -26
  93. package/src/server.ts +45 -5
  94. package/src/worker-federation-context.ts +51 -0
  95. package/src/worker-federation-contexts.ts +91 -0
  96. package/src/worker-federation.ts +120 -0
  97. package/dist/cjs/router-injection.js +0 -139
  98. package/dist/esm/router-injection.mjs +0 -101
  99. package/dist/types/router-injection.d.ts +0 -8
  100. package/src/router-injection.ts +0 -145
@@ -91,8 +91,9 @@ function requestHeaders(router: AnyRouter): Headers {
91
91
  );
92
92
  appendHeaders(headers, field?.value);
93
93
  }
94
- const redirect = router.stores.redirect.get();
95
- if (redirect) appendHeaders(headers, redirect.headers);
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 redirect = router.stores.redirect.get();
135
- if (redirect) {
136
- const resolved = router.resolveRedirect(redirect);
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(serverSsr.dehydrate(), session.signal);
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
@@ -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, useEffect, useRef } from 'octane';
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
- useEffect(
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 match = [
85
- ...router.stores.pendingMatches.get(),
86
- ...router.stores.matches.get(),
87
- ...router.stores.cachedMatches.get(),
88
- ].find(candidate => candidate.abortController === controller);
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
- // Native successful loads have already preloaded their route components.
133
+ // The error component is ready before publishing a late transport failure.
129
134
  if (signal.aborted) return;
130
- router.updateMatch(match.id, previous =>
131
- previous.abortController === controller &&
132
- previous.status === 'success' &&
133
- !previous.isFetching
134
- ? {
135
- ...previous,
136
- error: failure,
137
- status: 'error',
138
- isFetching: false,
139
- }
140
- : previous,
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
- const federation = input.federation
170
- ? createFederationScope(input.federation, true, document.nonce)
171
- : undefined;
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
- const nativeStream = await renderToReadableStream(App, input.props, {
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(body);
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
+ }