@squide/firefly-module-federation 0.0.0

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.
@@ -0,0 +1,397 @@
1
+ import { isFunction, isNil, ModuleRegistrationError, registerModule, type DeferredRegistrationFunction, type ModuleRegistrationStatus, type ModuleRegistrationStatusChangedListener, type ModuleRegistry, type RegisterModulesOptions, type Runtime } from "@squide/core";
2
+ import type { Logger, RootLogger } from "@workleap/logging";
3
+ import type { RemoteDefinition } from "./RemoteDefinition.ts";
4
+
5
+ export const RemoteModuleRegistryId = "remote";
6
+
7
+ export const RemoteModulesRegistrationStartedEvent = "squide-remote-modules-registration-started";
8
+ export const RemoteModulesRegistrationCompletedEvent = "squide-remote-modules-registration-completed";
9
+ export const RemoteModuleRegistrationFailedEvent = "squide-remote-module-registration-failed";
10
+
11
+ export const RemoteModulesDeferredRegistrationStartedEvent = "squide-remote-modules-deferred-registration-started";
12
+ export const RemoteModulesDeferredRegistrationCompletedEvent = "squide-remote-modules-deferred-registration-completed";
13
+ export const RemoteModuleDeferredRegistrationFailedEvent = "squide-some-remote-module-deferred-registration-failed";
14
+
15
+ export const RemoteModulesDeferredRegistrationsUpdateStartedEvent = "squide-remote-modules-deferred-registrations-update-started";
16
+ export const RemoteModulesDeferredRegistrationsUpdateCompletedEvent = "squide-remote-modules-deferred-registrations-update-completed-started";
17
+ export const RemoteModuleDeferredRegistrationUpdateFailedEvent = "squide-remote-module-deferred-registration-update-failed";
18
+
19
+ export interface RemoteModulesRegistrationStartedEventPayload {
20
+ remoteCount: number;
21
+ }
22
+
23
+ export interface RemoteModulesRegistrationCompletedEventPayload {
24
+ remoteCount: number;
25
+ }
26
+
27
+ export interface RemoteModulesDeferredRegistrationStartedEventPayload {
28
+ registrationCount: number;
29
+ }
30
+
31
+ export interface RemoteModulesDeferredRegistrationCompletedEventPayload {
32
+ registrationCount: number;
33
+ }
34
+
35
+ export interface RemoteModulesDeferredRegistrationsUpdateStartedEventPayload {
36
+ registrationCount: number;
37
+ }
38
+
39
+ export interface RemoteModulesDeferredRegistrationsUpdateCompletedEventPayload {
40
+ registrationCount: number;
41
+ }
42
+
43
+ const RemoteRegisterModuleName = "register";
44
+
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ type LoadRemoteFunction = (remoteName: string, moduleName: string) => Promise<any>;
47
+
48
+ interface DeferredRegistration<TRuntime extends Runtime = Runtime, TData = unknown> {
49
+ remoteName: string;
50
+ index: string;
51
+ fct: DeferredRegistrationFunction<TRuntime, TData>;
52
+ }
53
+
54
+ export class RemoteModuleRegistrationError extends ModuleRegistrationError {
55
+ readonly #remoteName: string;
56
+ readonly #moduleName: string;
57
+
58
+ constructor(message: string, remoteName: string, moduleName: string, options?: ErrorOptions) {
59
+ super(message, options);
60
+
61
+ this.#remoteName = remoteName;
62
+ this.#moduleName = moduleName;
63
+ }
64
+
65
+ get remoteName() {
66
+ return this.#remoteName;
67
+ }
68
+
69
+ get moduleName() {
70
+ return this.#moduleName;
71
+ }
72
+ }
73
+
74
+ export class RemoteModuleRegistry implements ModuleRegistry {
75
+ #registrationStatus: ModuleRegistrationStatus = "none";
76
+
77
+ readonly #deferredRegistrations: DeferredRegistration[] = [];
78
+ readonly #loadRemote: LoadRemoteFunction;
79
+ readonly #statusChangedListeners = new Set<ModuleRegistrationStatusChangedListener>();
80
+
81
+ constructor(loadRemote: LoadRemoteFunction) {
82
+ this.#loadRemote = loadRemote;
83
+ }
84
+
85
+ get id() {
86
+ return RemoteModuleRegistryId;
87
+ }
88
+
89
+ #logSharedScope(logger: Logger) {
90
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
91
+ // @ts-ignore
92
+ if (__webpack_share_scopes__) {
93
+ logger.debug(
94
+ "[squide] Module Federation shared scope is available:",
95
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
96
+ // @ts-ignore
97
+ __webpack_share_scopes__.default
98
+ );
99
+ }
100
+ }
101
+
102
+ async registerModules<TRuntime extends Runtime = Runtime, TContext = unknown, TData = unknown>(remotes: RemoteDefinition[], runtime: TRuntime, { context }: RegisterModulesOptions<TContext> = {}) {
103
+ const errors: RemoteModuleRegistrationError[] = [];
104
+
105
+ if (this.#registrationStatus !== "none") {
106
+ throw new Error("[squide] The registerRemoteModules function can only be called once.");
107
+ }
108
+
109
+ if (remotes.length > 0) {
110
+ runtime.logger.information(`[squide] Found ${remotes.length} remote module${remotes.length !== 1 ? "s" : ""} to register.`);
111
+
112
+ this.#setRegistrationStatus("registering-modules");
113
+
114
+ runtime.eventBus.dispatch(RemoteModulesRegistrationStartedEvent, {
115
+ remoteCount: remotes.length
116
+ } satisfies RemoteModulesRegistrationStartedEventPayload);
117
+
118
+ let completedCount = 0;
119
+
120
+ await Promise.allSettled(remotes.map(async (x, index) => {
121
+ const remoteName = x.name;
122
+ const loggerScope = (runtime.logger as RootLogger).startScope(`[squide] ${index + 1}/${remotes.length} Loading module "${RemoteRegisterModuleName}" of remote "${remoteName}".`);
123
+ const runtimeScope = runtime.startScope(loggerScope);
124
+
125
+ try {
126
+ const module = await this.#loadRemote(remoteName, RemoteRegisterModuleName);
127
+
128
+ if (isNil(module.register)) {
129
+ throw new Error(`[squide] A "register" function is not available for module "${RemoteRegisterModuleName}" of remote "${remoteName}". Make sure your remote "./register.[js,jsx,ts.tsx]" file export a function named "register".`);
130
+ }
131
+
132
+ loggerScope.debug("[squide] Registering module...");
133
+
134
+ const optionalDeferredRegistration = await registerModule<TRuntime, TContext, TData>(module.register, runtimeScope as TRuntime, context);
135
+
136
+ if (isFunction(optionalDeferredRegistration)) {
137
+ this.#deferredRegistrations.push({
138
+ remoteName: x.name,
139
+ index: `${index + 1}/${remotes.length}`,
140
+ fct: optionalDeferredRegistration as DeferredRegistrationFunction
141
+ });
142
+ }
143
+
144
+ completedCount += 1;
145
+
146
+ loggerScope.information("[squide] Successfully registered remote module.", {
147
+ style: {
148
+ color: "green"
149
+ }
150
+ });
151
+
152
+ loggerScope.end({
153
+ labelStyle: {
154
+ color: "green"
155
+ }
156
+ });
157
+ } catch (error: unknown) {
158
+ loggerScope
159
+ .withText("[squide] An error occured while registering the remote module.")
160
+ .withError(error as Error)
161
+ .error();
162
+
163
+ loggerScope.end({
164
+ labelStyle: {
165
+ color: "red"
166
+ }
167
+ });
168
+
169
+ errors.push(
170
+ new RemoteModuleRegistrationError(
171
+ `An error occured while registering module "${RemoteRegisterModuleName}" of remote "${remoteName}".`,
172
+ remoteName,
173
+ RemoteRegisterModuleName,
174
+ { cause: error }
175
+ )
176
+ );
177
+ }
178
+ }));
179
+
180
+ if (errors.length > 0) {
181
+ errors.forEach(x => {
182
+ runtime.eventBus.dispatch(RemoteModuleRegistrationFailedEvent, x);
183
+ });
184
+ }
185
+
186
+ // Must be dispatched before updating the registration status to ensure bootstrapping events sequencing.
187
+ runtime.eventBus.dispatch(RemoteModulesRegistrationCompletedEvent, {
188
+ remoteCount: completedCount
189
+ } satisfies RemoteModulesRegistrationCompletedEventPayload);
190
+
191
+ this.#setRegistrationStatus(this.#deferredRegistrations.length > 0 ? "modules-registered" : "ready");
192
+
193
+ // After introducting the "setRegistrationStatus" method, TypeScript seems to think that the only possible
194
+ // values for registrationStatus is "none" and now complains about the lack of overlapping between "none" and "ready".
195
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
196
+ // @ts-ignore
197
+ if (this.#registrationStatus === "ready") {
198
+ this.#logSharedScope(runtime.logger);
199
+ }
200
+ } else {
201
+ // There's no modules to register, it can be considered as ready.
202
+ this.#setRegistrationStatus("ready");
203
+ }
204
+
205
+ return errors;
206
+ }
207
+
208
+ async registerDeferredRegistrations<TData = unknown, TRuntime extends Runtime = Runtime>(data: TData, runtime: TRuntime) {
209
+ const errors: RemoteModuleRegistrationError[] = [];
210
+
211
+ if (this.#registrationStatus === "ready" && this.#deferredRegistrations.length === 0) {
212
+ // No deferred registrations were returned by the remote modules, skip this phase.
213
+ return errors;
214
+ }
215
+
216
+ if (this.#registrationStatus === "none" || this.#registrationStatus === "registering-modules") {
217
+ throw new Error("[squide] The registerDeferredRegistrations function can only be called once the remote modules are registered.");
218
+ }
219
+
220
+ if (this.#registrationStatus !== "modules-registered") {
221
+ throw new Error("[squide] The registerDeferredRegistrations function can only be called once.");
222
+ }
223
+
224
+ this.#setRegistrationStatus("registering-deferred-registration");
225
+
226
+ runtime.eventBus.dispatch(RemoteModulesDeferredRegistrationStartedEvent, {
227
+ registrationCount: this.#deferredRegistrations.length
228
+ } satisfies RemoteModulesDeferredRegistrationStartedEventPayload);
229
+
230
+ let completedCount = 0;
231
+
232
+ await Promise.allSettled(this.#deferredRegistrations.map(async ({ remoteName, index, fct: deferredRegister }) => {
233
+ const loggerScope = (runtime.logger as RootLogger).startScope(`[squide] ${index} Registering the deferred registrations for module "${RemoteRegisterModuleName}" of remote "${remoteName}".`);
234
+ const runtimeScope = runtime.startScope(loggerScope);
235
+
236
+ loggerScope
237
+ .withText("Data:")
238
+ .withObject(data)
239
+ .debug();
240
+
241
+ try {
242
+ await deferredRegister(runtimeScope, data, "register");
243
+
244
+ completedCount += 1;
245
+ } catch (error: unknown) {
246
+ loggerScope
247
+ .withText("[squide] An error occured while registering the deferred registrations.")
248
+ .withError(error as Error)
249
+ .error();
250
+
251
+ loggerScope.end({
252
+ labelStyle: {
253
+ color: "red"
254
+ }
255
+ });
256
+
257
+ errors.push(
258
+ new RemoteModuleRegistrationError(
259
+ `An error occured while registering the deferred registrations for module "${RemoteRegisterModuleName}" of remote "${remoteName}".`,
260
+ remoteName,
261
+ RemoteRegisterModuleName,
262
+ { cause: error }
263
+ )
264
+ );
265
+ }
266
+
267
+ loggerScope.information("[squide] Successfully registered deferred registrations.", {
268
+ style: {
269
+ color: "green"
270
+ }
271
+ });
272
+
273
+ loggerScope.end({
274
+ labelStyle: {
275
+ color: "green"
276
+ }
277
+ });
278
+ }));
279
+
280
+ if (errors.length > 0) {
281
+ errors.forEach(x => {
282
+ runtime.eventBus.dispatch(RemoteModuleDeferredRegistrationFailedEvent, x);
283
+ });
284
+ }
285
+
286
+ // Must be dispatched before updating the registration status to ensure bootstrapping events sequencing.
287
+ runtime.eventBus.dispatch(RemoteModulesDeferredRegistrationCompletedEvent, {
288
+ registrationCount: completedCount
289
+ } satisfies RemoteModulesDeferredRegistrationCompletedEventPayload);
290
+
291
+ this.#setRegistrationStatus("ready");
292
+ this.#logSharedScope(runtime.logger);
293
+
294
+ return errors;
295
+ }
296
+
297
+ async updateDeferredRegistrations<TData = unknown, TRuntime extends Runtime = Runtime>(data: TData, runtime: TRuntime) {
298
+ const errors: RemoteModuleRegistrationError[] = [];
299
+
300
+ if (this.#registrationStatus !== "ready") {
301
+ throw new Error("[squide] The updateDeferredRegistrations function can only be called once the remote modules are ready.");
302
+ }
303
+
304
+ runtime.eventBus.dispatch(RemoteModulesDeferredRegistrationsUpdateStartedEvent, {
305
+ registrationCount: this.#deferredRegistrations.length
306
+ } satisfies RemoteModulesDeferredRegistrationsUpdateStartedEventPayload);
307
+
308
+ let completedCount = 0;
309
+
310
+ await Promise.allSettled(this.#deferredRegistrations.map(async ({ remoteName, index, fct: deferredRegister }) => {
311
+ const loggerScope = (runtime.logger as RootLogger).startScope(`[squide] ${index} Updating the deferred registrations for module "${RemoteRegisterModuleName}" of remote "${remoteName}".`);
312
+ const runtimeScope = runtime.startScope(loggerScope);
313
+
314
+ loggerScope
315
+ .withText("Data:")
316
+ .withObject(data)
317
+ .debug();
318
+
319
+ try {
320
+ await deferredRegister(runtimeScope, data, "update");
321
+
322
+ completedCount += 1;
323
+ } catch (error: unknown) {
324
+ loggerScope
325
+ .withText("[squide] An error occured while updating the deferred registrations.")
326
+ .withError(error as Error)
327
+ .error();
328
+
329
+ loggerScope.end({
330
+ labelStyle: {
331
+ color: "red"
332
+ }
333
+ });
334
+
335
+ errors.push(
336
+ new RemoteModuleRegistrationError(
337
+ `An error occured while updating the deferred registrations for module "${RemoteRegisterModuleName}" of remote "${remoteName}".`,
338
+ remoteName,
339
+ RemoteRegisterModuleName,
340
+ { cause: error }
341
+ )
342
+ );
343
+ }
344
+
345
+ loggerScope.information("[squide] Successfully updated the deferred registrations.", {
346
+ style: {
347
+ color: "green"
348
+ }
349
+ });
350
+
351
+ loggerScope.end({
352
+ labelStyle: {
353
+ color: "green"
354
+ }
355
+ });
356
+ }));
357
+
358
+ if (errors.length > 0) {
359
+ errors.forEach(x => {
360
+ runtime.eventBus.dispatch(RemoteModuleDeferredRegistrationUpdateFailedEvent, x);
361
+ });
362
+ }
363
+
364
+ runtime.eventBus.dispatch(RemoteModulesDeferredRegistrationsUpdateCompletedEvent, {
365
+ registrationCount: completedCount
366
+ } satisfies RemoteModulesDeferredRegistrationsUpdateCompletedEventPayload);
367
+
368
+ return errors;
369
+ }
370
+
371
+ registerStatusChangedListener(callback: ModuleRegistrationStatusChangedListener) {
372
+ this.#statusChangedListeners.add(callback);
373
+ }
374
+
375
+ removeStatusChangedListener(callback: ModuleRegistrationStatusChangedListener) {
376
+ this.#statusChangedListeners.delete(callback);
377
+ }
378
+
379
+ #setRegistrationStatus(status: ModuleRegistrationStatus) {
380
+ this.#registrationStatus = status;
381
+
382
+ this.#statusChangedListeners.forEach(x => {
383
+ x();
384
+ });
385
+ }
386
+
387
+ get registrationStatus() {
388
+ return this.#registrationStatus;
389
+ }
390
+ }
391
+
392
+ export function toRemoteModuleDefinitions(remotes: RemoteDefinition[]) {
393
+ return remotes.map(x => ({
394
+ definition: x,
395
+ registryId: RemoteModuleRegistryId
396
+ }));
397
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ export { initializeFirefly, type InitializeFireflyOptions } from "./initializeFirefly.ts";
2
+ export { getModuleFederationPlugin, ModuleFederationPluginName, type ModuleFederationPlugin } from "./ModuleFederationPlugin.ts";
3
+ export type { RemoteDefinition } from "./RemoteDefinition.ts";
4
+ export {
5
+ RemoteModuleDeferredRegistrationFailedEvent,
6
+ RemoteModuleDeferredRegistrationUpdateFailedEvent,
7
+ RemoteModuleRegistrationFailedEvent,
8
+ RemoteModuleRegistryId,
9
+ RemoteModulesDeferredRegistrationCompletedEvent,
10
+ RemoteModulesDeferredRegistrationStartedEvent,
11
+ RemoteModulesDeferredRegistrationsUpdateCompletedEvent,
12
+ RemoteModulesDeferredRegistrationsUpdateStartedEvent,
13
+ RemoteModulesRegistrationCompletedEvent,
14
+ RemoteModulesRegistrationStartedEvent,
15
+ type RemoteModuleRegistrationError,
16
+ type RemoteModulesDeferredRegistrationCompletedEventPayload,
17
+ type RemoteModulesDeferredRegistrationStartedEventPayload,
18
+ type RemoteModulesDeferredRegistrationsUpdateCompletedEventPayload,
19
+ type RemoteModulesDeferredRegistrationsUpdateStartedEventPayload,
20
+ type RemoteModulesRegistrationCompletedEventPayload,
21
+ type RemoteModulesRegistrationStartedEventPayload
22
+ } from "./RemoteModuleRegistry.ts";
23
+
@@ -0,0 +1,29 @@
1
+ import { FireflyRuntime, initializeFirefly as baseInitializeFirefly, type InitializeFireflyOptions as BaseInitializeFireflyOptions } from "@squide/firefly";
2
+ import { ModuleFederationPlugin } from "./ModuleFederationPlugin.ts";
3
+ import { RemoteDefinition } from "./RemoteDefinition.ts";
4
+ import { toRemoteModuleDefinitions } from "./RemoteModuleRegistry.ts";
5
+
6
+ export interface InitializeFireflyOptions<TRuntime extends FireflyRuntime, TContext = unknown, TData = unknown> extends BaseInitializeFireflyOptions<TRuntime, TContext, TData> {
7
+ remotes?: RemoteDefinition[];
8
+ }
9
+
10
+ export function initializeFirefly<TContext = unknown, TData = unknown>(options: InitializeFireflyOptions<FireflyRuntime, TContext, TData> = {}) {
11
+ const {
12
+ remotes = [],
13
+ moduleDefinitions = [],
14
+ plugins = [],
15
+ ...rest
16
+ } = options;
17
+
18
+ return baseInitializeFirefly({
19
+ moduleDefinitions: [
20
+ ...moduleDefinitions,
21
+ ...toRemoteModuleDefinitions(remotes)
22
+ ],
23
+ plugins: [
24
+ x => new ModuleFederationPlugin(x),
25
+ ...plugins
26
+ ],
27
+ ...rest
28
+ });
29
+ }