@bleedingdev/modern-js-renderer-octane 3.9.3-ultramodern.8 → 3.9.3-ultramodern.9
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 +11 -3
- package/dist/cjs/federation.js +108 -6
- 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/plugin/index.js +1 -1
- package/dist/cjs/plugin/profile.js +5 -4
- package/dist/cjs/router-external-snapshot.js +263 -0
- package/dist/cjs/server.js +15 -2
- 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 +8 -3
- package/dist/esm/federation.mjs +110 -8
- 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/router-external-snapshot.mjs +216 -0
- package/dist/esm/server.mjs +15 -2
- 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/plugin/index.mjs +1 -1
- package/dist/esm-node/plugin/profile.mjs +5 -4
- 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/router-external-snapshot.d.ts +10 -0
- package/dist/types/server.d.ts +3 -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 +69 -12
- 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 +17 -6
- package/src/federation.ts +159 -15
- 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/plugin/compiler-manifest.cjs +27 -1
- package/src/plugin/index.ts +1 -1
- package/src/plugin/profile.ts +6 -4
- package/src/router-external-snapshot.ts +306 -0
- package/src/server.ts +29 -4
- package/src/worker-federation-context.ts +51 -0
- package/src/worker-federation-contexts.ts +91 -0
- package/src/worker-federation.ts +120 -0
|
@@ -107,6 +107,29 @@ class OctaneCompilerManifestPlugin {
|
|
|
107
107
|
sources.sort((left, right) =>
|
|
108
108
|
left.resource.localeCompare(right.resource),
|
|
109
109
|
);
|
|
110
|
+
// Module Federation publishes exposed native modules through a
|
|
111
|
+
// separate container entry in this same compilation. Its browser
|
|
112
|
+
// closure belongs to this application's publication, even when the
|
|
113
|
+
// document itself never imports an exposed component.
|
|
114
|
+
const containerChunks = new Set();
|
|
115
|
+
for (const [entryName, entry] of compilation.entries) {
|
|
116
|
+
if (
|
|
117
|
+
!entry.dependencies.some(
|
|
118
|
+
dependency => dependency.type === 'container entry',
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
continue;
|
|
122
|
+
const container = compilation.entrypoints.get(entryName);
|
|
123
|
+
if (!container)
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Octane compiler has no federation container entry for ${entryName}.`,
|
|
126
|
+
);
|
|
127
|
+
for (const chunk of container.chunks) {
|
|
128
|
+
containerChunks.add(chunk);
|
|
129
|
+
for (const referenced of chunk.getAllReferencedChunks())
|
|
130
|
+
containerChunks.add(referenced);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
110
133
|
const identities = this.options.rendererIdentities();
|
|
111
134
|
if (!identities || !Object.keys(identities).length) {
|
|
112
135
|
throw new Error(
|
|
@@ -129,7 +152,10 @@ class OctaneCompilerManifestPlugin {
|
|
|
129
152
|
throw new Error(
|
|
130
153
|
`Octane compiler has no client entry for ${entryName}.`,
|
|
131
154
|
);
|
|
132
|
-
const entryChunks = new Set(
|
|
155
|
+
const entryChunks = new Set([
|
|
156
|
+
...entrypoint.chunks,
|
|
157
|
+
...containerChunks,
|
|
158
|
+
]);
|
|
133
159
|
for (const chunk of entrypoint.chunks) {
|
|
134
160
|
for (const referenced of chunk.getAllReferencedChunks())
|
|
135
161
|
entryChunks.add(referenced);
|
package/src/plugin/index.ts
CHANGED
|
@@ -81,7 +81,7 @@ export const rendererAdapter: NativeRendererAdapter<'octane'> =
|
|
|
81
81
|
artifacts: compilerArtifacts,
|
|
82
82
|
assertSupportedSource: assertOctaneEntrySource,
|
|
83
83
|
create: {
|
|
84
|
-
templates: { federation: true, workers:
|
|
84
|
+
templates: { federation: true, workers: true },
|
|
85
85
|
dependencies: (profile, { typescriptVersion }) => ({
|
|
86
86
|
frameworkDependencies: [
|
|
87
87
|
'@modern-js/renderer-core',
|
package/src/plugin/profile.ts
CHANGED
|
@@ -21,18 +21,20 @@ export const octaneProfile: RendererBuildProfile<'octane'> = {
|
|
|
21
21
|
name: '@octanejs/tanstack-router',
|
|
22
22
|
version: '0.2.10',
|
|
23
23
|
coreName: '@tanstack/router-core',
|
|
24
|
-
coreVersion: '1.171.
|
|
24
|
+
coreVersion: '1.171.38',
|
|
25
25
|
},
|
|
26
26
|
sourceExtensions: ['.tsx', '.tsrx', '.ts', '.js'],
|
|
27
27
|
jsxImportSource: 'octane',
|
|
28
28
|
dependencies: {
|
|
29
29
|
'@tsrx/oxc': '0.20.0',
|
|
30
30
|
octane:
|
|
31
|
-
'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-
|
|
31
|
+
'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-0bf2313bbdfb/octane-0.13.8.tgz',
|
|
32
32
|
'@octanejs/tanstack-router':
|
|
33
|
-
'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-
|
|
33
|
+
'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-0bf2313bbdfb/octanejs-tanstack-router-0.2.10.tgz',
|
|
34
34
|
'@octanejs/rspack-plugin':
|
|
35
|
-
'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-
|
|
35
|
+
'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-0bf2313bbdfb/octanejs-rspack-plugin-0.2.4.tgz',
|
|
36
|
+
'@tanstack/router-core':
|
|
37
|
+
'https://github.com/BleedingDev/router/releases/download/ultramodern-router-core-585d3233f7f5/tanstack-router-core-1.171.38.tgz',
|
|
36
38
|
'@modern-js/renderer-octane': '3.8.3',
|
|
37
39
|
seroval: '1.6.9',
|
|
38
40
|
'seroval-plugins': '1.6.9',
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
|
|
2
|
+
import {
|
|
3
|
+
type AnyRouter,
|
|
4
|
+
assertDefaultRouterRenderHost,
|
|
5
|
+
associateRouterRenderProfile,
|
|
6
|
+
captureRouterRenderSnapshot,
|
|
7
|
+
createRouterRenderRouteTree,
|
|
8
|
+
defaultParseSearch,
|
|
9
|
+
defaultStringifySearch,
|
|
10
|
+
disposeRouterRenderSnapshot,
|
|
11
|
+
getRouterRenderProfile,
|
|
12
|
+
matchContext,
|
|
13
|
+
type RouterRenderJsonRecord,
|
|
14
|
+
type RouterRenderSnapshot,
|
|
15
|
+
registerRouterRenderProfile,
|
|
16
|
+
restoreRouterRenderSnapshot,
|
|
17
|
+
routerContext,
|
|
18
|
+
} from '@octanejs/tanstack-router';
|
|
19
|
+
import {
|
|
20
|
+
getOctaneWorkerRouterRewrite,
|
|
21
|
+
OCTANE_WORKER_CONTEXT_KEYS,
|
|
22
|
+
registerOctaneWorkerContext,
|
|
23
|
+
} from './worker-federation-contexts';
|
|
24
|
+
|
|
25
|
+
export const OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE = Object.freeze({
|
|
26
|
+
key: 'ultramodern.file-routes',
|
|
27
|
+
version: '1',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
interface GeneratedRouterOrigin {
|
|
31
|
+
readonly publicContext: RouterRenderJsonRecord;
|
|
32
|
+
readonly rewrite: AnyRouter['options']['rewrite'];
|
|
33
|
+
}
|
|
34
|
+
const origins = new WeakMap<AnyRouter, GeneratedRouterOrigin>();
|
|
35
|
+
let profileRegistered = false;
|
|
36
|
+
|
|
37
|
+
function plainRecord(value: unknown): value is Record<string, unknown> {
|
|
38
|
+
return (
|
|
39
|
+
value !== null &&
|
|
40
|
+
typeof value === 'object' &&
|
|
41
|
+
[Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function record(
|
|
46
|
+
value: unknown,
|
|
47
|
+
fields?: readonly string[],
|
|
48
|
+
): Record<string, unknown> {
|
|
49
|
+
if (!plainRecord(value))
|
|
50
|
+
throw new TypeError('Worker router context requires a plain record');
|
|
51
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
52
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
53
|
+
if (
|
|
54
|
+
typeof key !== 'string' ||
|
|
55
|
+
(fields && !fields.includes(key)) ||
|
|
56
|
+
!descriptor ||
|
|
57
|
+
!descriptor.enumerable ||
|
|
58
|
+
!('value' in descriptor)
|
|
59
|
+
)
|
|
60
|
+
throw new TypeError('Unknown Worker router context field or accessor');
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function nonempty(value: unknown): string {
|
|
66
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 1024)
|
|
67
|
+
throw new TypeError('Invalid Worker router identity');
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Only the host entry's public logical identity crosses the Worker boundary. */
|
|
72
|
+
function publicContext(value: unknown): RouterRenderJsonRecord {
|
|
73
|
+
const context = record(value, ['ultramodern']);
|
|
74
|
+
const metadata = record(context.ultramodern, ['rendererIdentity']);
|
|
75
|
+
const identity = record(metadata.rendererIdentity, [
|
|
76
|
+
'renderer',
|
|
77
|
+
'appId',
|
|
78
|
+
'entryName',
|
|
79
|
+
'protocolVersion',
|
|
80
|
+
'buildId',
|
|
81
|
+
]);
|
|
82
|
+
if (identity.renderer !== 'octane' || identity.protocolVersion !== 1)
|
|
83
|
+
throw new TypeError('Unsupported Worker router identity');
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
ultramodern: Object.freeze({
|
|
86
|
+
rendererIdentity: Object.freeze({
|
|
87
|
+
renderer: 'octane',
|
|
88
|
+
appId: nonempty(identity.appId),
|
|
89
|
+
entryName: nonempty(identity.entryName),
|
|
90
|
+
protocolVersion: 1,
|
|
91
|
+
buildId: nonempty(identity.buildId),
|
|
92
|
+
}),
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function localizedUrls(value: unknown): LocalisedUrlsOption | undefined {
|
|
98
|
+
if (value === undefined || typeof value === 'boolean') return value;
|
|
99
|
+
const input = record(value);
|
|
100
|
+
const result: Exclude<LocalisedUrlsOption, boolean> = {};
|
|
101
|
+
for (const [path, translations] of Object.entries(input)) {
|
|
102
|
+
const names: Record<string, string> = {};
|
|
103
|
+
for (const [language, target] of Object.entries(record(translations)))
|
|
104
|
+
names[nonempty(language)] = nonempty(target);
|
|
105
|
+
result[path] = names;
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function rewriteDescriptor(value: unknown):
|
|
111
|
+
| {
|
|
112
|
+
readonly language: string;
|
|
113
|
+
readonly languages: readonly string[];
|
|
114
|
+
readonly localisedUrls?: LocalisedUrlsOption;
|
|
115
|
+
}
|
|
116
|
+
| undefined {
|
|
117
|
+
if (value === undefined) return undefined;
|
|
118
|
+
const input = record(value, ['language', 'languages', 'localisedUrls']);
|
|
119
|
+
if (
|
|
120
|
+
!Array.isArray(input.languages) ||
|
|
121
|
+
!input.languages.length ||
|
|
122
|
+
input.languages.length > 64
|
|
123
|
+
)
|
|
124
|
+
throw new TypeError('Invalid Worker router i18n languages');
|
|
125
|
+
const languages = input.languages.map(nonempty);
|
|
126
|
+
const language = nonempty(input.language);
|
|
127
|
+
if (
|
|
128
|
+
new Set(languages).size !== languages.length ||
|
|
129
|
+
!languages.includes(language)
|
|
130
|
+
)
|
|
131
|
+
throw new TypeError('Invalid Worker router i18n language');
|
|
132
|
+
const urls = localizedUrls(input.localisedUrls);
|
|
133
|
+
return {
|
|
134
|
+
language,
|
|
135
|
+
languages,
|
|
136
|
+
...(urls === undefined ? {} : { localisedUrls: urls }),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function nativeRewriteDescriptor(
|
|
141
|
+
router: AnyRouter,
|
|
142
|
+
): RouterRenderJsonRecord | undefined {
|
|
143
|
+
if (!router.options.rewrite) return undefined;
|
|
144
|
+
const known = getOctaneWorkerRouterRewrite()?.capture(router.options.rewrite);
|
|
145
|
+
if (!known)
|
|
146
|
+
throw new TypeError(
|
|
147
|
+
'Custom router rewrite requires an exact Worker profile',
|
|
148
|
+
);
|
|
149
|
+
const descriptor = rewriteDescriptor(known);
|
|
150
|
+
if (!descriptor)
|
|
151
|
+
throw new TypeError('Missing native router rewrite provenance');
|
|
152
|
+
return {
|
|
153
|
+
language: descriptor.language,
|
|
154
|
+
languages: [...descriptor.languages],
|
|
155
|
+
...(descriptor.localisedUrls === undefined
|
|
156
|
+
? {}
|
|
157
|
+
: { localisedUrls: descriptor.localisedUrls }),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function validateDefaultSnapshot(snapshot: RouterRenderSnapshot): void {
|
|
162
|
+
publicContext(snapshot.publicContext);
|
|
163
|
+
rewriteDescriptor(snapshot.i18n);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ensureDefaultProfile(): void {
|
|
167
|
+
if (profileRegistered) return;
|
|
168
|
+
registerRouterRenderProfile({
|
|
169
|
+
id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
|
|
170
|
+
assertHost: (router, snapshot) => {
|
|
171
|
+
const origin = origins.get(router);
|
|
172
|
+
if (
|
|
173
|
+
!origin ||
|
|
174
|
+
origin.rewrite !== router.options.rewrite ||
|
|
175
|
+
JSON.stringify(publicContext(router.options.context)) !==
|
|
176
|
+
JSON.stringify(origin.publicContext)
|
|
177
|
+
)
|
|
178
|
+
throw new TypeError('Generated Worker router origin changed');
|
|
179
|
+
assertDefaultRouterRenderHost(router, snapshot.i18n !== undefined);
|
|
180
|
+
validateDefaultSnapshot(snapshot);
|
|
181
|
+
if (
|
|
182
|
+
JSON.stringify(nativeRewriteDescriptor(router)) !==
|
|
183
|
+
JSON.stringify(snapshot.i18n)
|
|
184
|
+
)
|
|
185
|
+
throw new TypeError(
|
|
186
|
+
'Worker router rewrite differs from its native provenance',
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
createRoutingTree: snapshot => {
|
|
190
|
+
validateDefaultSnapshot(snapshot);
|
|
191
|
+
return createRouterRenderRouteTree(snapshot.routes);
|
|
192
|
+
},
|
|
193
|
+
routingOptions: snapshot => {
|
|
194
|
+
const defaults = {
|
|
195
|
+
parseSearch: defaultParseSearch,
|
|
196
|
+
stringifySearch: defaultStringifySearch,
|
|
197
|
+
};
|
|
198
|
+
const i18n = rewriteDescriptor(snapshot.i18n);
|
|
199
|
+
if (!i18n) return defaults;
|
|
200
|
+
const codec = getOctaneWorkerRouterRewrite();
|
|
201
|
+
if (!codec)
|
|
202
|
+
throw new TypeError('Missing owning Worker router i18n codec');
|
|
203
|
+
return { ...defaults, rewrite: codec.restore(i18n) };
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
profileRegistered = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Associate the genuine router produced by the generated file-route factory. */
|
|
210
|
+
export function associateOctaneFileRouteSnapshot(router: AnyRouter): void {
|
|
211
|
+
ensureDefaultProfile();
|
|
212
|
+
const context = publicContext(router.options.context);
|
|
213
|
+
origins.set(router, {
|
|
214
|
+
publicContext: context,
|
|
215
|
+
rewrite: router.options.rewrite,
|
|
216
|
+
});
|
|
217
|
+
associateRouterRenderProfile(router, {
|
|
218
|
+
id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
|
|
219
|
+
publicContext: context,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function captureOctaneRouterSnapshot(
|
|
224
|
+
router: AnyRouter,
|
|
225
|
+
): RouterRenderSnapshot {
|
|
226
|
+
const origin = origins.get(router);
|
|
227
|
+
const declared = getRouterRenderProfile(router);
|
|
228
|
+
if (
|
|
229
|
+
origin &&
|
|
230
|
+
declared?.key === OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE.key &&
|
|
231
|
+
declared.version === OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE.version
|
|
232
|
+
) {
|
|
233
|
+
const i18n = nativeRewriteDescriptor(router);
|
|
234
|
+
associateRouterRenderProfile(router, {
|
|
235
|
+
id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
|
|
236
|
+
publicContext: origin.publicContext,
|
|
237
|
+
...(i18n === undefined ? {} : { i18n }),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return captureRouterRenderSnapshot(router);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let registrations = 0;
|
|
244
|
+
let releaseContexts: (() => void) | undefined;
|
|
245
|
+
|
|
246
|
+
/** Startup registration uses the actual physical native router/match Contexts. */
|
|
247
|
+
export function registerOctaneRouterWorkerContexts(): () => void {
|
|
248
|
+
ensureDefaultProfile();
|
|
249
|
+
if (!releaseContexts) {
|
|
250
|
+
const releaseRouter = registerOctaneWorkerContext(routerContext, {
|
|
251
|
+
key: OCTANE_WORKER_CONTEXT_KEYS.router,
|
|
252
|
+
encode: value => {
|
|
253
|
+
if (!value) throw new TypeError('Missing native router provider');
|
|
254
|
+
return captureOctaneRouterSnapshot(value);
|
|
255
|
+
},
|
|
256
|
+
decode: (value, signal) => restoreRouterRenderSnapshot(value, { signal }),
|
|
257
|
+
dispose: value => {
|
|
258
|
+
if (value) disposeRouterRenderSnapshot(value);
|
|
259
|
+
},
|
|
260
|
+
isProvided: value => value !== undefined,
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
const releaseMatch = registerOctaneWorkerContext(matchContext, {
|
|
264
|
+
key: OCTANE_WORKER_CONTEXT_KEYS.match,
|
|
265
|
+
encode: nonempty,
|
|
266
|
+
decode: nonempty,
|
|
267
|
+
validate: (value, readContext) => {
|
|
268
|
+
const selected = readContext(routerContext);
|
|
269
|
+
const router = selected.present ? selected.value : undefined;
|
|
270
|
+
// Native Match and useMatch share the active route store key, not
|
|
271
|
+
// the location-specific match identity (which includes parameters).
|
|
272
|
+
const current = router?.state.matches.find(
|
|
273
|
+
match => match.routeId === value,
|
|
274
|
+
);
|
|
275
|
+
if (
|
|
276
|
+
!router ||
|
|
277
|
+
!current ||
|
|
278
|
+
typeof value !== 'string' ||
|
|
279
|
+
router.stores.getMatchStore(value).get() !== current
|
|
280
|
+
)
|
|
281
|
+
throw new TypeError(
|
|
282
|
+
'Worker match context requires its selected native router and a current match',
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
isProvided: value => value !== undefined,
|
|
286
|
+
});
|
|
287
|
+
releaseContexts = () => {
|
|
288
|
+
releaseMatch();
|
|
289
|
+
releaseRouter();
|
|
290
|
+
};
|
|
291
|
+
} catch (error) {
|
|
292
|
+
releaseRouter();
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
registrations++;
|
|
297
|
+
let released = false;
|
|
298
|
+
return () => {
|
|
299
|
+
if (released) return;
|
|
300
|
+
released = true;
|
|
301
|
+
if (--registrations === 0) {
|
|
302
|
+
releaseContexts?.();
|
|
303
|
+
releaseContexts = undefined;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -9,6 +9,7 @@ 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';
|
|
12
13
|
import type { AnyRouter } from '@octanejs/tanstack-router';
|
|
13
14
|
import { transformReadableStreamWithRouter } from '@octanejs/tanstack-router/ssr/server';
|
|
14
15
|
import {
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
assertOctaneIdentity,
|
|
24
25
|
} from './bootstrap';
|
|
25
26
|
import { createFederationScope, FederationRoot } from './federation-context';
|
|
27
|
+
import { WorkerFederationRoot } from './worker-federation-context';
|
|
26
28
|
|
|
27
29
|
export interface OctaneDocumentOptions {
|
|
28
30
|
/** One identity for this response, shared with the client's signal receiver. */
|
|
@@ -62,6 +64,8 @@ export interface RenderOctaneApplicationOptions<
|
|
|
62
64
|
readonly routerSerializationTimeoutMs?: number;
|
|
63
65
|
/** This server compilation's native Module Federation runtime. */
|
|
64
66
|
readonly federation?: NativeFederationBinding;
|
|
67
|
+
readonly workerFederation?: WorkerFederationTransport;
|
|
68
|
+
readonly workerFederationLocale?: string;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
function prepareOctaneDocument<Bindings extends object>(
|
|
@@ -170,9 +174,12 @@ export async function renderOctaneApplication<Bindings extends object>(
|
|
|
170
174
|
session.startRendering();
|
|
171
175
|
|
|
172
176
|
let head = '';
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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;
|
|
176
183
|
const App = federation
|
|
177
184
|
? () =>
|
|
178
185
|
createElement(FederationRoot, {
|
|
@@ -183,7 +190,24 @@ export async function renderOctaneApplication<Bindings extends object>(
|
|
|
183
190
|
: input.App,
|
|
184
191
|
})
|
|
185
192
|
: input.App;
|
|
186
|
-
|
|
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, {
|
|
187
211
|
signal: session.signal,
|
|
188
212
|
...(document.nonce === undefined ? {} : { nonce: document.nonce }),
|
|
189
213
|
earlySignalBootstrap: 'external',
|
|
@@ -192,6 +216,7 @@ export async function renderOctaneApplication<Bindings extends object>(
|
|
|
192
216
|
buildId: document.nativeHydrationBuildId,
|
|
193
217
|
documentId: document.documentId,
|
|
194
218
|
},
|
|
219
|
+
externalSnapshots: { documentId: document.documentId },
|
|
195
220
|
headChannel: 'separate',
|
|
196
221
|
onHeadReady(value) {
|
|
197
222
|
head = value;
|
|
@@ -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
|
+
}
|