@bleedingdev/modern-js-federation-runtime 3.9.3-ultramodern.7 → 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/module-federation/native-browser-dev-client.js +320 -0
- package/dist/cjs/module-federation/native-browser-dev-refresh.js +226 -0
- package/dist/cjs/module-federation/native-browser-dev-runtime-plugin.js +233 -0
- package/dist/cjs/module-federation/native-dev-refresh-runtime-plugin.js +267 -0
- package/dist/cjs/module-federation/renderer-runtime-plugin.js +5 -2
- package/dist/esm/module-federation/native-browser-dev-client.mjs +276 -0
- package/dist/esm/module-federation/native-browser-dev-refresh.mjs +182 -0
- package/dist/esm/module-federation/native-browser-dev-runtime-plugin.mjs +183 -0
- package/dist/esm/module-federation/native-dev-refresh-runtime-plugin.mjs +229 -0
- package/dist/esm/module-federation/renderer-runtime-plugin.mjs +5 -2
- package/dist/esm-node/module-federation/native-browser-dev-client.mjs +277 -0
- package/dist/esm-node/module-federation/native-browser-dev-refresh.mjs +183 -0
- package/dist/esm-node/module-federation/native-browser-dev-runtime-plugin.mjs +184 -0
- package/dist/esm-node/module-federation/native-dev-refresh-runtime-plugin.mjs +230 -0
- package/dist/esm-node/module-federation/renderer-runtime-plugin.mjs +5 -2
- package/dist/types/module-federation/native-browser-dev-client.d.ts +41 -0
- package/dist/types/module-federation/native-browser-dev-refresh.d.ts +86 -0
- package/dist/types/module-federation/native-browser-dev-runtime-plugin.d.ts +40 -0
- package/dist/types/module-federation/native-dev-refresh-runtime-plugin.d.ts +90 -0
- package/package.json +60 -13
- package/src/module-federation/native-browser-dev-client.ts +424 -0
- package/src/module-federation/native-browser-dev-refresh.ts +373 -0
- package/src/module-federation/native-browser-dev-runtime-plugin.ts +276 -0
- package/src/module-federation/native-dev-refresh-runtime-plugin.ts +445 -0
- package/src/module-federation/renderer-runtime-plugin.ts +7 -2
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/** The normal MF instance surface used by a browser development replacement. */
|
|
2
|
+
export interface NativeBrowserDevHost {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly options: Readonly<Record<string, unknown>>;
|
|
5
|
+
readonly shareScopeMap: Record<
|
|
6
|
+
string,
|
|
7
|
+
Record<string, Record<string, Shared>>
|
|
8
|
+
>;
|
|
9
|
+
readonly moduleCache: Map<string, unknown>;
|
|
10
|
+
readonly loaderHook?: {
|
|
11
|
+
readonly lifecycle: {
|
|
12
|
+
readonly fetch: {
|
|
13
|
+
emit(
|
|
14
|
+
url: string,
|
|
15
|
+
init: RequestInit,
|
|
16
|
+
remote: unknown,
|
|
17
|
+
context: {
|
|
18
|
+
readonly initiator: 'loadRemote';
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly resourceType: 'manifest';
|
|
21
|
+
readonly url: string;
|
|
22
|
+
},
|
|
23
|
+
): Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
initShareScopeMap(
|
|
28
|
+
name: string,
|
|
29
|
+
scope: Record<string, Record<string, Shared>>,
|
|
30
|
+
options: { hostShareScopeMap: NativeBrowserDevHost['shareScopeMap'] },
|
|
31
|
+
): void;
|
|
32
|
+
loadRemote<T>(id: string, options: { loadFactory: false }): Promise<T | null>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Shared {
|
|
36
|
+
readonly loaded?: boolean;
|
|
37
|
+
readonly loading?: unknown;
|
|
38
|
+
readonly lib?: unknown;
|
|
39
|
+
readonly get?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NativeBrowserDevGraph {
|
|
43
|
+
/** Existing completed graph revision, also used in immutable asset URLs. */
|
|
44
|
+
readonly revision: string;
|
|
45
|
+
/** Native JSON manifest whose entry AND chunks belong to this revision. */
|
|
46
|
+
readonly remote: Readonly<{ name: string; alias: string; entry: string }>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface StagingHost {
|
|
50
|
+
readonly host: NativeBrowserDevHost;
|
|
51
|
+
/** Rearm cached, unexecuted runtime hot listeners without starting a socket. */
|
|
52
|
+
prepareFactory?(): void;
|
|
53
|
+
/** Retire only this attempt's transport/listeners; never shared providers. */
|
|
54
|
+
release(): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface PublisherReplacement {
|
|
58
|
+
readonly commitStarted: boolean;
|
|
59
|
+
execute<T>(factory: () => T): T;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const COMMIT_ERROR = Symbol.for(
|
|
63
|
+
'ultramodern.federation.browser-dev-commit-error.v1',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
/** Propagates an observed native mutation; it does not grant native authority. */
|
|
67
|
+
class NativeBrowserDevCommitError extends Error {
|
|
68
|
+
readonly [COMMIT_ERROR] = true;
|
|
69
|
+
declare readonly revision: string;
|
|
70
|
+
|
|
71
|
+
constructor(revision: string, cause: unknown) {
|
|
72
|
+
super(
|
|
73
|
+
`Native browser refresh failed after ownership transfer began: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
74
|
+
{ cause },
|
|
75
|
+
);
|
|
76
|
+
this.name = 'NativeBrowserDevCommitError';
|
|
77
|
+
Object.defineProperty(this, 'revision', {
|
|
78
|
+
value: revision,
|
|
79
|
+
enumerable: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A remote runtime can receive this error from a different host runtime copy. */
|
|
85
|
+
export function isNativeBrowserDevCommitError(
|
|
86
|
+
error: unknown,
|
|
87
|
+
revision: string,
|
|
88
|
+
): error is NativeBrowserDevCommitError {
|
|
89
|
+
return (
|
|
90
|
+
error instanceof Error &&
|
|
91
|
+
Reflect.get(error, COMMIT_ERROR) === true &&
|
|
92
|
+
Reflect.get(error, 'revision') === revision
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Trusted coordinators use this only after observing an actual native commit. */
|
|
97
|
+
export function nativeBrowserDevCommitFailure(
|
|
98
|
+
revision: string,
|
|
99
|
+
cause: unknown,
|
|
100
|
+
): Error {
|
|
101
|
+
return new NativeBrowserDevCommitError(revision, cause);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface SelectedShare {
|
|
105
|
+
readonly scope: string;
|
|
106
|
+
readonly name: string;
|
|
107
|
+
readonly version: string;
|
|
108
|
+
readonly provider: Shared;
|
|
109
|
+
readonly lib: unknown;
|
|
110
|
+
readonly get: unknown;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const selectedShares = (host: NativeBrowserDevHost): SelectedShare[] =>
|
|
114
|
+
Object.entries(host.shareScopeMap).flatMap(([scope, packages]) =>
|
|
115
|
+
Object.entries(packages).flatMap(([name, versions]) =>
|
|
116
|
+
Object.entries(versions)
|
|
117
|
+
.filter(([, provider]) => provider.loaded || provider.loading)
|
|
118
|
+
.map(([version, provider]) => ({
|
|
119
|
+
scope,
|
|
120
|
+
name,
|
|
121
|
+
version,
|
|
122
|
+
provider,
|
|
123
|
+
lib: provider.lib,
|
|
124
|
+
get: provider.get,
|
|
125
|
+
})),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
function assertSelectedShares(
|
|
130
|
+
host: NativeBrowserDevHost,
|
|
131
|
+
shares: readonly SelectedShare[],
|
|
132
|
+
): void {
|
|
133
|
+
for (const share of shares) {
|
|
134
|
+
const provider =
|
|
135
|
+
host.shareScopeMap[share.scope]?.[share.name]?.[share.version];
|
|
136
|
+
if (
|
|
137
|
+
provider !== share.provider ||
|
|
138
|
+
provider.lib !== share.lib ||
|
|
139
|
+
provider.get !== share.get
|
|
140
|
+
)
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Native browser refresh changed the selected shared provider ${share.name}.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Load a new container without evicting the working host. MF admission and async
|
|
149
|
+
* chunk loading finish before Octane executes the synchronous exposed graph.
|
|
150
|
+
* Native execution owns declaration rollback and retirement of the old graph.
|
|
151
|
+
* It cannot undo arbitrary module side effects or the browser's ESM cache: a
|
|
152
|
+
* consumed, failed graph needs a newer completed revision for another attempt.
|
|
153
|
+
*
|
|
154
|
+
* The compiler supplies a public ModuleFederation constructor adapter and an
|
|
155
|
+
* attempt-owned transport release function. No global MF cache is deleted here.
|
|
156
|
+
*/
|
|
157
|
+
export function createNativeBrowserDevRefresh<Boundary>(options: {
|
|
158
|
+
readonly host: NativeBrowserDevHost;
|
|
159
|
+
readonly id: string;
|
|
160
|
+
readonly boundary: Boundary;
|
|
161
|
+
readonly prepareReplacement: (boundary: Boundary) => PublisherReplacement;
|
|
162
|
+
readonly createHost: (
|
|
163
|
+
options: Readonly<Record<string, unknown>>,
|
|
164
|
+
) => StagingHost;
|
|
165
|
+
}) {
|
|
166
|
+
let active: StagingHost | undefined;
|
|
167
|
+
let pending: Promise<unknown> | undefined;
|
|
168
|
+
let disposed = false;
|
|
169
|
+
const consumed = new Set<string>();
|
|
170
|
+
const recovery = new Set<StagingHost>();
|
|
171
|
+
const released = new WeakSet<StagingHost>();
|
|
172
|
+
|
|
173
|
+
const release = (stage: StagingHost) => {
|
|
174
|
+
if (released.has(stage)) return;
|
|
175
|
+
released.add(stage);
|
|
176
|
+
try {
|
|
177
|
+
stage.release();
|
|
178
|
+
} finally {
|
|
179
|
+
stage.host.moduleCache.clear();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
replace<T>(graph: NativeBrowserDevGraph): Promise<T> {
|
|
185
|
+
if (disposed)
|
|
186
|
+
return Promise.reject(new Error('Native browser refresh is disposed.'));
|
|
187
|
+
if (pending)
|
|
188
|
+
return Promise.reject(
|
|
189
|
+
new Error(
|
|
190
|
+
'Native browser refresh already has an active replacement.',
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
if (!/^[a-f0-9]{64}$/u.test(graph.revision))
|
|
194
|
+
return Promise.reject(
|
|
195
|
+
new Error('Native browser graph revision is invalid.'),
|
|
196
|
+
);
|
|
197
|
+
if (consumed.has(graph.revision))
|
|
198
|
+
return Promise.reject(
|
|
199
|
+
new Error(
|
|
200
|
+
'Native browser graph was already consumed; rebuild before retrying.',
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
// A cross-runtime outcome must come from this attempt's actual native
|
|
204
|
+
// commit, never from arbitrary objects thrown by a factory or loader.
|
|
205
|
+
const commitErrors = new WeakSet<Error>();
|
|
206
|
+
const task = (async (): Promise<T> => {
|
|
207
|
+
const host = options.host;
|
|
208
|
+
const shares = selectedShares(host);
|
|
209
|
+
// Prepare from the actual live boundary before async loading. execute
|
|
210
|
+
// revalidates all captured native owners after that loading completes.
|
|
211
|
+
const replacement = options.prepareReplacement(options.boundary);
|
|
212
|
+
const stage = options.createHost({
|
|
213
|
+
...host.options,
|
|
214
|
+
name: `${host.name}:native-dev:${graph.revision}`,
|
|
215
|
+
id: `${host.name}:native-dev:${graph.revision}`,
|
|
216
|
+
remotes: [{ ...graph.remote }],
|
|
217
|
+
// Reuse actual initialized scopes, not reconstructed shared configs.
|
|
218
|
+
shared: {},
|
|
219
|
+
});
|
|
220
|
+
if (stage.host === host || stage.host === active?.host)
|
|
221
|
+
throw new Error(
|
|
222
|
+
'Native browser refresh requires an isolated staging host.',
|
|
223
|
+
);
|
|
224
|
+
let accepted = false;
|
|
225
|
+
let module!: T;
|
|
226
|
+
let failed = false;
|
|
227
|
+
let failure: unknown;
|
|
228
|
+
try {
|
|
229
|
+
for (const [name, scope] of Object.entries(host.shareScopeMap)) {
|
|
230
|
+
stage.host.initShareScopeMap(name, scope, {
|
|
231
|
+
hostShareScopeMap: host.shareScopeMap,
|
|
232
|
+
});
|
|
233
|
+
if (stage.host.shareScopeMap[name] !== scope)
|
|
234
|
+
throw new Error(
|
|
235
|
+
'Native browser refresh lost the actual shared scope.',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const factory = await stage.host.loadRemote<() => T>(options.id, {
|
|
239
|
+
loadFactory: false,
|
|
240
|
+
});
|
|
241
|
+
if (disposed)
|
|
242
|
+
throw new Error(
|
|
243
|
+
'Native browser refresh was disposed while loading.',
|
|
244
|
+
);
|
|
245
|
+
if (typeof factory !== 'function')
|
|
246
|
+
throw new Error(
|
|
247
|
+
'Native browser refresh did not receive an exposed factory.',
|
|
248
|
+
);
|
|
249
|
+
assertSelectedShares(host, shares);
|
|
250
|
+
stage.prepareFactory?.();
|
|
251
|
+
consumed.add(graph.revision);
|
|
252
|
+
try {
|
|
253
|
+
module = replacement.execute(factory);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (replacement.commitStarted) {
|
|
256
|
+
// Native callbacks can fail after the first irreversible owner
|
|
257
|
+
// mutation. Retain both graphs for native recovery, surface the
|
|
258
|
+
// failure, and never describe this as an old-owner rollback.
|
|
259
|
+
if (active) recovery.add(active);
|
|
260
|
+
active = stage;
|
|
261
|
+
accepted = true;
|
|
262
|
+
}
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
const previous = active;
|
|
266
|
+
active = stage;
|
|
267
|
+
accepted = true;
|
|
268
|
+
// Native commit has completed. Never pretend later observer or
|
|
269
|
+
// transport errors rolled that commit back or discard its owner.
|
|
270
|
+
assertSelectedShares(host, shares);
|
|
271
|
+
// A failed retirement must not release the newly accepted owner.
|
|
272
|
+
const retired = new Set(recovery);
|
|
273
|
+
recovery.clear();
|
|
274
|
+
if (previous) retired.add(previous);
|
|
275
|
+
const errors: unknown[] = [];
|
|
276
|
+
for (const retained of retired) {
|
|
277
|
+
try {
|
|
278
|
+
release(retained);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
errors.push(error);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (errors.length)
|
|
284
|
+
throw new AggregateError(
|
|
285
|
+
errors,
|
|
286
|
+
'Native browser refresh retirement failed.',
|
|
287
|
+
);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
failed = true;
|
|
290
|
+
failure = error;
|
|
291
|
+
}
|
|
292
|
+
const retired = new Set<StagingHost>();
|
|
293
|
+
if (disposed && active === stage) {
|
|
294
|
+
active = undefined;
|
|
295
|
+
for (const retained of recovery) retired.add(retained);
|
|
296
|
+
recovery.clear();
|
|
297
|
+
retired.add(stage);
|
|
298
|
+
} else if (!accepted) retired.add(stage);
|
|
299
|
+
const cleanupErrors: unknown[] = [];
|
|
300
|
+
for (const retained of retired) {
|
|
301
|
+
try {
|
|
302
|
+
release(retained);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
cleanupErrors.push(error);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (cleanupErrors.length) {
|
|
308
|
+
failure = new AggregateError(
|
|
309
|
+
failed ? [failure, ...cleanupErrors] : cleanupErrors,
|
|
310
|
+
'Native browser refresh cleanup failed.',
|
|
311
|
+
);
|
|
312
|
+
failed = true;
|
|
313
|
+
}
|
|
314
|
+
if (failed) {
|
|
315
|
+
if (accepted) {
|
|
316
|
+
const committedFailure = nativeBrowserDevCommitFailure(
|
|
317
|
+
graph.revision,
|
|
318
|
+
failure,
|
|
319
|
+
);
|
|
320
|
+
commitErrors.add(committedFailure);
|
|
321
|
+
throw committedFailure;
|
|
322
|
+
}
|
|
323
|
+
throw failure;
|
|
324
|
+
}
|
|
325
|
+
return module;
|
|
326
|
+
})().catch(error => {
|
|
327
|
+
if (
|
|
328
|
+
error instanceof Error &&
|
|
329
|
+
Reflect.get(error, COMMIT_ERROR) === true &&
|
|
330
|
+
!commitErrors.has(error)
|
|
331
|
+
)
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Native browser graph failed before its own ownership transfer: ${error.message}`,
|
|
334
|
+
{ cause: error },
|
|
335
|
+
);
|
|
336
|
+
throw error;
|
|
337
|
+
});
|
|
338
|
+
pending = task;
|
|
339
|
+
void task
|
|
340
|
+
.finally(() => {
|
|
341
|
+
if (pending === task) pending = undefined;
|
|
342
|
+
})
|
|
343
|
+
.catch(() => {});
|
|
344
|
+
return task;
|
|
345
|
+
},
|
|
346
|
+
/** The original host remains untouched, including sibling remote loaders. */
|
|
347
|
+
get activeHost(): NativeBrowserDevHost {
|
|
348
|
+
return active?.host ?? options.host;
|
|
349
|
+
},
|
|
350
|
+
dispose(): void {
|
|
351
|
+
if (disposed) return;
|
|
352
|
+
disposed = true;
|
|
353
|
+
const previous = active;
|
|
354
|
+
active = undefined;
|
|
355
|
+
const retained = new Set(recovery);
|
|
356
|
+
recovery.clear();
|
|
357
|
+
if (previous) retained.add(previous);
|
|
358
|
+
const errors: unknown[] = [];
|
|
359
|
+
for (const stage of retained) {
|
|
360
|
+
try {
|
|
361
|
+
release(stage);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
errors.push(error);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (errors.length)
|
|
367
|
+
throw new AggregateError(
|
|
368
|
+
errors,
|
|
369
|
+
'Native browser refresh cleanup failed.',
|
|
370
|
+
);
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createNativeBrowserDevRefresh,
|
|
3
|
+
isNativeBrowserDevCommitError,
|
|
4
|
+
type NativeBrowserDevGraph,
|
|
5
|
+
type NativeBrowserDevHost,
|
|
6
|
+
nativeBrowserDevCommitFailure,
|
|
7
|
+
} from './native-browser-dev-refresh';
|
|
8
|
+
import { readRendererFederationContract } from './renderer-contract';
|
|
9
|
+
|
|
10
|
+
export const NATIVE_BROWSER_DEV_BOUNDARY = Symbol.for(
|
|
11
|
+
'ultramodern.federation.browser-dev-boundary.v1',
|
|
12
|
+
);
|
|
13
|
+
const REGISTRY = Symbol.for('ultramodern.federation.browser-dev-routing.v1');
|
|
14
|
+
|
|
15
|
+
interface Replacement {
|
|
16
|
+
readonly commitStarted: boolean;
|
|
17
|
+
execute<T>(factory: () => T): T;
|
|
18
|
+
}
|
|
19
|
+
interface Reference {
|
|
20
|
+
readonly host: NativeBrowserDevHost;
|
|
21
|
+
readonly remote: NativeBrowserDevGraph['remote'];
|
|
22
|
+
readonly boundary: object;
|
|
23
|
+
readonly prepare: (boundary: object) => Replacement;
|
|
24
|
+
}
|
|
25
|
+
interface Binding {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly remoteName: string;
|
|
28
|
+
readonly references: Set<Reference>;
|
|
29
|
+
readonly coordinator: ReturnType<typeof createNativeBrowserDevRefresh>;
|
|
30
|
+
}
|
|
31
|
+
interface Registry {
|
|
32
|
+
readonly sources: WeakMap<object, Map<string, Binding>>;
|
|
33
|
+
readonly bindings: Set<Binding>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function registry(): Registry {
|
|
37
|
+
const root = globalThis as typeof globalThis & { [REGISTRY]?: Registry };
|
|
38
|
+
if (!root[REGISTRY])
|
|
39
|
+
Object.defineProperty(root, REGISTRY, {
|
|
40
|
+
value: { sources: new WeakMap(), bindings: new Set() } satisfies Registry,
|
|
41
|
+
});
|
|
42
|
+
return root[REGISTRY]!;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function firstReference(binding: Binding): Reference {
|
|
46
|
+
const reference = binding.references.values().next().value;
|
|
47
|
+
if (!reference)
|
|
48
|
+
throw new Error('Native browser refresh has no mounted boundary owner.');
|
|
49
|
+
return reference;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* This registry routes work to actual mounted boundary references; its names
|
|
54
|
+
* grant no native authority. Octane revalidates the exact compiled root, lease,
|
|
55
|
+
* physical document and every surviving owner inside the opaque capability.
|
|
56
|
+
*/
|
|
57
|
+
export function createNativeBrowserDevRuntimePlugin(options: {
|
|
58
|
+
readonly remotes: readonly NativeBrowserDevGraph['remote'][];
|
|
59
|
+
readonly prepareReplacement: Reference['prepare'];
|
|
60
|
+
readonly createHost: Parameters<
|
|
61
|
+
typeof createNativeBrowserDevRefresh
|
|
62
|
+
>[0]['createHost'];
|
|
63
|
+
}) {
|
|
64
|
+
return {
|
|
65
|
+
name: 'ultramodern-native-browser-dev-refresh',
|
|
66
|
+
beforeInit<T extends { origin: NativeBrowserDevHost }>(args: T): T {
|
|
67
|
+
const host = args.origin;
|
|
68
|
+
if (Object.hasOwn(host, NATIVE_BROWSER_DEV_BOUNDARY)) return args;
|
|
69
|
+
Object.defineProperty(host, NATIVE_BROWSER_DEV_BOUNDARY, {
|
|
70
|
+
value(id: string, source: object, boundary: object): () => void {
|
|
71
|
+
const remote = options.remotes.find(remote =>
|
|
72
|
+
id.startsWith(`${remote.alias}/`),
|
|
73
|
+
);
|
|
74
|
+
if (!remote)
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Native browser refresh has no registered remote for ${id}.`,
|
|
77
|
+
);
|
|
78
|
+
const state = registry();
|
|
79
|
+
let sources = state.sources.get(source);
|
|
80
|
+
if (!sources) state.sources.set(source, (sources = new Map()));
|
|
81
|
+
let binding = sources.get(id);
|
|
82
|
+
if (binding && binding.remoteName !== remote.name)
|
|
83
|
+
throw new Error(
|
|
84
|
+
'Native browser refresh source has conflicting remote ownership.',
|
|
85
|
+
);
|
|
86
|
+
if (!binding) {
|
|
87
|
+
const references = new Set<Reference>();
|
|
88
|
+
const current = (): Reference => firstReference(created);
|
|
89
|
+
const created: Binding = {
|
|
90
|
+
id,
|
|
91
|
+
remoteName: remote.name,
|
|
92
|
+
references,
|
|
93
|
+
coordinator: createNativeBrowserDevRefresh({
|
|
94
|
+
get host() {
|
|
95
|
+
return current().host;
|
|
96
|
+
},
|
|
97
|
+
id,
|
|
98
|
+
boundary: references,
|
|
99
|
+
prepareReplacement() {
|
|
100
|
+
const reference = current();
|
|
101
|
+
return reference.prepare(reference.boundary);
|
|
102
|
+
},
|
|
103
|
+
createHost: options.createHost,
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
binding = created;
|
|
107
|
+
sources.set(id, binding);
|
|
108
|
+
state.bindings.add(binding);
|
|
109
|
+
}
|
|
110
|
+
const reference: Reference = {
|
|
111
|
+
host,
|
|
112
|
+
remote,
|
|
113
|
+
boundary,
|
|
114
|
+
prepare: options.prepareReplacement,
|
|
115
|
+
};
|
|
116
|
+
binding.references.add(reference);
|
|
117
|
+
const owned = binding;
|
|
118
|
+
let released = false;
|
|
119
|
+
return () => {
|
|
120
|
+
if (released) return;
|
|
121
|
+
released = true;
|
|
122
|
+
owned.references.delete(reference);
|
|
123
|
+
if (owned.references.size) return;
|
|
124
|
+
sources!.delete(id);
|
|
125
|
+
state.bindings.delete(owned);
|
|
126
|
+
owned.coordinator.dispose();
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
return args;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** One synchronous exposed graph execution serves all of its mounted instances. */
|
|
136
|
+
export async function replaceNativeBrowserDevRemote(
|
|
137
|
+
graph: NativeBrowserDevGraph,
|
|
138
|
+
): Promise<number> {
|
|
139
|
+
const bindings = [...registry().bindings].filter(
|
|
140
|
+
binding => binding.remoteName === graph.remote.name,
|
|
141
|
+
);
|
|
142
|
+
if (!bindings.length)
|
|
143
|
+
throw new Error(
|
|
144
|
+
'Native browser refresh has no mounted publisher for this remote.',
|
|
145
|
+
);
|
|
146
|
+
let committed = 0;
|
|
147
|
+
try {
|
|
148
|
+
for (const binding of bindings) {
|
|
149
|
+
await binding.coordinator.replace(graph);
|
|
150
|
+
committed++;
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Separate exposed roots are not an atomic remote-wide transaction. If an
|
|
154
|
+
// earlier root committed, its runtime must survive a later root's failure.
|
|
155
|
+
if (committed && !isNativeBrowserDevCommitError(error, graph.revision))
|
|
156
|
+
throw nativeBrowserDevCommitFailure(graph.revision, error);
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
return bindings.length;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface NativeBrowserDevPublication extends NativeBrowserDevGraph {
|
|
163
|
+
readonly buildId: string;
|
|
164
|
+
readonly runtimeHash: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const record = (value: unknown): value is Record<string, unknown> =>
|
|
168
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
169
|
+
|
|
170
|
+
/** Discover the latest completed graph through this mounted host's transport. */
|
|
171
|
+
export async function readNativeBrowserDevPublication(
|
|
172
|
+
remoteName: string,
|
|
173
|
+
options: { readonly signal: AbortSignal; readonly revisionHeader: string },
|
|
174
|
+
): Promise<NativeBrowserDevPublication> {
|
|
175
|
+
const binding = [...registry().bindings].find(
|
|
176
|
+
binding => binding.remoteName === remoteName,
|
|
177
|
+
);
|
|
178
|
+
if (!binding)
|
|
179
|
+
throw new Error(
|
|
180
|
+
'Native browser refresh has no mounted publisher for discovery.',
|
|
181
|
+
);
|
|
182
|
+
const reference = firstReference(binding);
|
|
183
|
+
const remote = reference.remote;
|
|
184
|
+
const host = reference.host;
|
|
185
|
+
const init: RequestInit = { cache: 'no-store', signal: options.signal };
|
|
186
|
+
const context = {
|
|
187
|
+
initiator: 'loadRemote' as const,
|
|
188
|
+
id: binding.id,
|
|
189
|
+
resourceType: 'manifest' as const,
|
|
190
|
+
url: remote.entry,
|
|
191
|
+
};
|
|
192
|
+
const hook = host.loaderHook?.lifecycle.fetch;
|
|
193
|
+
const request = (async () => {
|
|
194
|
+
const hooked = await hook?.emit(remote.entry, init, remote, context);
|
|
195
|
+
const response =
|
|
196
|
+
hooked instanceof Response
|
|
197
|
+
? hooked
|
|
198
|
+
: await globalThis.fetch(remote.entry, init);
|
|
199
|
+
if (options.signal.aborted) {
|
|
200
|
+
void response.body?.cancel().catch(() => {});
|
|
201
|
+
throw options.signal.reason;
|
|
202
|
+
}
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
void response.body?.cancel().catch(() => {});
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Native browser manifest discovery failed: ${response.status}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const manifest: unknown = await response.json();
|
|
210
|
+
const revision = response.headers.get(options.revisionHeader);
|
|
211
|
+
const links = [
|
|
212
|
+
...(response.headers.get('link') ?? '').matchAll(
|
|
213
|
+
/<([^>]+)>\s*;([^,]*)/gu,
|
|
214
|
+
),
|
|
215
|
+
].filter(([, , parameters]) =>
|
|
216
|
+
/\brel\s*=\s*"?ultramodern-completed-graph"?(?:\s|;|$)/u.test(
|
|
217
|
+
parameters!,
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
if (!revision || !/^[a-f0-9]{64}$/u.test(revision) || links.length !== 1)
|
|
221
|
+
throw new Error(
|
|
222
|
+
'Native browser manifest has no unique completed graph coordinate.',
|
|
223
|
+
);
|
|
224
|
+
const entry = new URL(links[0]![1]!, remote.entry);
|
|
225
|
+
if (
|
|
226
|
+
!['http:', 'https:'].includes(entry.protocol) ||
|
|
227
|
+
entry.username ||
|
|
228
|
+
entry.password ||
|
|
229
|
+
entry.hash
|
|
230
|
+
)
|
|
231
|
+
throw new Error('Native browser completed graph URL is invalid.');
|
|
232
|
+
const namespace = /\/__ultramodern_mf__\/([a-f0-9]+)\//u.exec(
|
|
233
|
+
entry.pathname,
|
|
234
|
+
);
|
|
235
|
+
if (!namespace)
|
|
236
|
+
throw new Error(
|
|
237
|
+
'Native browser graph is not in the immutable runtime namespace.',
|
|
238
|
+
);
|
|
239
|
+
const metadata =
|
|
240
|
+
record(manifest) && record(manifest.metaData)
|
|
241
|
+
? manifest.metaData.ultramodernRenderer
|
|
242
|
+
: undefined;
|
|
243
|
+
const contract = readRendererFederationContract(metadata);
|
|
244
|
+
const buildIds = new Set(
|
|
245
|
+
Object.values(contract.identities).map(identity => identity.buildId),
|
|
246
|
+
);
|
|
247
|
+
if (buildIds.size !== 1)
|
|
248
|
+
throw new Error(
|
|
249
|
+
'Native browser graph has conflicting renderer build identities.',
|
|
250
|
+
);
|
|
251
|
+
return {
|
|
252
|
+
revision,
|
|
253
|
+
runtimeHash: namespace[1]!,
|
|
254
|
+
buildId: [...buildIds][0]!,
|
|
255
|
+
remote: { ...remote, entry: entry.href },
|
|
256
|
+
};
|
|
257
|
+
})();
|
|
258
|
+
let removeAbort!: () => void;
|
|
259
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
260
|
+
const abort = () => reject(options.signal.reason);
|
|
261
|
+
removeAbort = () => options.signal.removeEventListener('abort', abort);
|
|
262
|
+
if (options.signal.aborted) abort();
|
|
263
|
+
else options.signal.addEventListener('abort', abort, { once: true });
|
|
264
|
+
});
|
|
265
|
+
try {
|
|
266
|
+
return await Promise.race([request, aborted]);
|
|
267
|
+
} finally {
|
|
268
|
+
removeAbort();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function hasNativeBrowserDevPublisher(remoteName: string): boolean {
|
|
273
|
+
return [...registry().bindings].some(
|
|
274
|
+
binding => binding.remoteName === remoteName,
|
|
275
|
+
);
|
|
276
|
+
}
|