@squide/firefly-webpack-configs 4.2.1 → 4.2.3

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,514 @@
1
+ import { ModuleFederationPlugin } from "@module-federation/enhanced/webpack";
2
+ import type { SwcConfig } from "@workleap/swc-configs";
3
+ import { defineBuildConfig, defineBuildHtmlWebpackPluginConfig, defineDevConfig, defineDevHtmlWebpackPluginConfig, type DefineBuildConfigOptions, type DefineDevConfigOptions, type WebpackConfig, type WebpackConfigTransformer } from "@workleap/webpack-configs";
4
+ import merge from "deepmerge";
5
+ import type HtmlWebpackPlugin from "html-webpack-plugin";
6
+ import fs from "node:fs";
7
+ import path, { dirname } from "node:path";
8
+ import url, { fileURLToPath } from "node:url";
9
+ import type webpack from "webpack";
10
+ import { HostApplicationName } from "./shared.ts";
11
+
12
+ // Using import.meta.url instead of import.meta.dirname because Jest is throwing the following error:
13
+ // SyntaxError: Cannot use 'import.meta' outside a module
14
+ const applicationDirectory = dirname(fileURLToPath(import.meta.url));
15
+ const packageDirectory = url.fileURLToPath(new URL(".", import.meta.url));
16
+
17
+ // Must be similar to the module name defined in @workleap/module-federation.
18
+ const RemoteRegisterModuleName = "./register";
19
+ const RemoteEntryPoint = "remoteEntry.js";
20
+
21
+ // Webpack doesn't export ModuleFederationPlugin typings.
22
+ export type ModuleFederationPluginOptions = ConstructorParameters<typeof ModuleFederationPlugin>[0];
23
+ export type ModuleFederationRemotesOption = ModuleFederationPluginOptions["remotes"];
24
+
25
+ export type ModuleFederationRuntimePlugins = NonNullable<ModuleFederationPluginOptions["runtimePlugins"]>;
26
+ export type ModuleFederationShared = NonNullable<ModuleFederationPluginOptions["shared"]>;
27
+
28
+ // Generally, only the host application should have eager dependencies.
29
+ // For more informations about shared dependencies refer to: https://github.com/patricklafrance/wmf-versioning
30
+ function getDefaultSharedDependencies(features: Features, isHost: boolean): ModuleFederationShared {
31
+ return {
32
+ "react": {
33
+ singleton: true,
34
+ eager: isHost ? true : undefined,
35
+ // Fixed the warning when `react-i18next` is imported in any remote modules.
36
+ // For more information, refer to: https://github.com/i18next/react-i18next/issues/1697#issuecomment-1821748226.
37
+ requiredVersion: features.i18next ? false : undefined
38
+ },
39
+ "react-dom": {
40
+ singleton: true,
41
+ eager: isHost ? true : undefined
42
+ },
43
+ "@squide/core": {
44
+ singleton: true,
45
+ eager: isHost ? true : undefined
46
+ },
47
+ "@squide/module-federation": {
48
+ singleton: true,
49
+ eager: isHost ? true : undefined
50
+ }
51
+ };
52
+ }
53
+
54
+ export type Router = "react-router";
55
+
56
+ export interface Features {
57
+ router?: Router;
58
+ msw?: boolean;
59
+ i18next?: boolean;
60
+ environmentVariables?: boolean;
61
+ honeycomb?: boolean;
62
+ }
63
+
64
+ // Generally, only the host application should have eager dependencies.
65
+ // For more informations about shared dependencies refer to: https://github.com/patricklafrance/wmf-versioning
66
+ function getReactRouterSharedDependencies(isHost: boolean): ModuleFederationShared {
67
+ return {
68
+ "react-router-dom": {
69
+ singleton: true,
70
+ eager: isHost ? true : undefined
71
+ },
72
+ "@squide/react-router": {
73
+ singleton: true,
74
+ eager: isHost ? true : undefined
75
+ }
76
+ };
77
+ }
78
+
79
+ function getMswSharedDependency(isHost: boolean): ModuleFederationShared {
80
+ return {
81
+ "@squide/msw": {
82
+ singleton: true,
83
+ eager: isHost ? true : undefined
84
+ }
85
+ };
86
+ }
87
+
88
+ function getI18nextSharedDependency(isHost: boolean): ModuleFederationShared {
89
+ return {
90
+ "i18next": {
91
+ singleton: true,
92
+ eager: isHost ? true : undefined
93
+ },
94
+ // Not adding as a shared dependency for the moment because it causes the following error:
95
+ // Uncaught (in promise) TypeError: i18next_browser_languagedetector__WEBPACK_IMPORTED_MODULE_3__ is not a constructor
96
+ // "i18next-browser-languagedetector": {
97
+ // singleton: true,
98
+ // eager: isHost ? true : undefined
99
+ // },
100
+ "react-i18next": {
101
+ singleton: true,
102
+ eager: isHost ? true : undefined
103
+ },
104
+ "@squide/i18next": {
105
+ singleton: true,
106
+ eager: isHost ? true : undefined
107
+ }
108
+ };
109
+ }
110
+
111
+ function getEnvironmentVariablesSharedDependencies(isHost: boolean): ModuleFederationShared {
112
+ return {
113
+ "@squide/env-vars": {
114
+ singleton: true,
115
+ eager: isHost ? true : undefined
116
+ }
117
+ };
118
+ }
119
+
120
+ function getHoneycombSharedDependencies(isHost: boolean): ModuleFederationShared {
121
+ return {
122
+ "@honeycombio/opentelemetry-web": {
123
+ singleton: true,
124
+ eager: isHost ? true : undefined
125
+ },
126
+ "@opentelemetry/api": {
127
+ singleton: true,
128
+ eager: isHost ? true : undefined
129
+ },
130
+ "@opentelemetry/auto-instrumentations-web": {
131
+ singleton: true,
132
+ eager: isHost ? true : undefined
133
+ },
134
+ "@squide/firefly-honeycomb": {
135
+ singleton: true,
136
+ eager: isHost ? true : undefined
137
+ }
138
+ };
139
+ }
140
+
141
+ function getFeaturesDependencies(features: Features, isHost: boolean) {
142
+ const {
143
+ router = "react-router",
144
+ msw = true,
145
+ i18next,
146
+ environmentVariables,
147
+ honeycomb
148
+ } = features;
149
+
150
+ return {
151
+ ...(router === "react-router" ? getReactRouterSharedDependencies(isHost) : {}),
152
+ ...(msw ? getMswSharedDependency(isHost) : {}),
153
+ ...(i18next ? getI18nextSharedDependency(isHost) : {}),
154
+ ...(environmentVariables ? getEnvironmentVariablesSharedDependencies(isHost) : {}),
155
+ ...(honeycomb ? getHoneycombSharedDependencies(isHost) : {})
156
+ };
157
+ }
158
+
159
+ function resolveDefaultSharedDependencies(features: Features, isHost: boolean) {
160
+ return {
161
+ ...getDefaultSharedDependencies(features, isHost),
162
+ ...getFeaturesDependencies(features, isHost)
163
+ };
164
+ }
165
+
166
+ const forceNamedChunkIdsTransformer: WebpackConfigTransformer = (config: WebpackConfig) => {
167
+ config.optimization = {
168
+ ...(config.optimization ?? {}),
169
+ // Without named chunk ids, there are some Webpack features that do not work
170
+ // when used with Module Federation. One of these feature is using a dynamic import in
171
+ // a remote module.
172
+ chunkIds: "named"
173
+ };
174
+
175
+ return config;
176
+ };
177
+
178
+ function createSetUniqueNameTransformer(uniqueName: string) {
179
+ const transformer: WebpackConfigTransformer = (config: WebpackConfig) => {
180
+ config.output = {
181
+ ...(config.output ?? {}),
182
+ // Every host and remotes must have a "uniqueName" for React Refresh to work
183
+ // with Module Federation.
184
+ uniqueName
185
+ };
186
+
187
+ return config;
188
+ };
189
+
190
+ return transformer;
191
+ }
192
+
193
+ function resolveEntryFilePath(entryPaths: string[]) {
194
+ for (const entryPath in entryPaths) {
195
+ if (fs.existsSync(path.resolve(applicationDirectory, entryPath))) {
196
+ return entryPath;
197
+ }
198
+ }
199
+
200
+ return entryPaths[0];
201
+ }
202
+
203
+ //////////////////////////// Host /////////////////////////////
204
+
205
+ export interface RemoteDefinition {
206
+ // The name of the remote module.
207
+ name: string;
208
+ // The URL of the remote, ex: http://localhost:8081.
209
+ url: string;
210
+ }
211
+
212
+ const HostEntryFilePaths = [
213
+ "./src/index.ts",
214
+ "./src/index.tsx"
215
+ ];
216
+
217
+ export interface DefineHostModuleFederationPluginOptions extends ModuleFederationPluginOptions {
218
+ features?: Features;
219
+ }
220
+
221
+ // The function return type is mandatory, otherwise we got an error TS4058.
222
+ export function defineHostModuleFederationPluginOptions(remotes: RemoteDefinition[], options: DefineHostModuleFederationPluginOptions): ModuleFederationPluginOptions {
223
+ const {
224
+ features = {},
225
+ shared = {},
226
+ runtimePlugins = [],
227
+ ...rest
228
+ } = options;
229
+
230
+ const defaultSharedDependencies = resolveDefaultSharedDependencies(features, true);
231
+
232
+ return {
233
+ name: HostApplicationName,
234
+ // Since Squide modules are only exporting a register function with a standardized API
235
+ // it doesn't requires any typing.
236
+ dts: false,
237
+ // Currently only supporting .js remotes.
238
+ manifest: false,
239
+ remotes: remotes.reduce((acc, x) => {
240
+ // Object reduce are always a mess for typings.
241
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
242
+ // @ts-ignore
243
+ acc[x.name] = `${x.name}@${x.url}/${RemoteEntryPoint}`;
244
+
245
+ return acc;
246
+ }, {}) as ModuleFederationRemotesOption,
247
+ // Deep merging the default shared dependencies with the provided shared dependencies
248
+ // to allow the consumer to easily override a default option of a shared dependency
249
+ // without extending the whole default shared dependencies object.
250
+ shared: merge.all([
251
+ defaultSharedDependencies,
252
+ shared
253
+ ]) as ModuleFederationShared,
254
+ runtimePlugins: [
255
+ path.resolve(packageDirectory, "./sharedDependenciesResolutionPlugin.js"),
256
+ path.resolve(packageDirectory, "./nonCacheableRemoteEntryPlugin.js"),
257
+ ...runtimePlugins
258
+ ],
259
+ // Commented because it doesn't seems to work, the runtime is still embedded into remotes.
260
+ // experiments: {
261
+ // // The runtime is 100kb minified.
262
+ // federationRuntime: "hoisted"
263
+ // },
264
+ ...rest
265
+ };
266
+ }
267
+
268
+ // Fixing HMR and page reloads when using `publicPath: auto` either in the host or remotes webpack configuration.
269
+ // Otherwise, when a nested page that belongs to a remote module is reloaded, an "Unexpected token" error will be thrown.
270
+ function trySetHtmlWebpackPluginPublicPath(options: HtmlWebpackPlugin.Options) {
271
+ if (!options.publicPath) {
272
+ options.publicPath = "/";
273
+ }
274
+
275
+ return options;
276
+ }
277
+
278
+ export interface DefineDevHostConfigOptions extends Omit<DefineDevConfigOptions, "htmlWebpackPlugin" | "port"> {
279
+ htmlWebpackPluginOptions?: HtmlWebpackPlugin.Options;
280
+ features?: Features;
281
+ sharedDependencies?: ModuleFederationShared;
282
+ runtimePlugins?: ModuleFederationRuntimePlugins;
283
+ moduleFederationPluginOptions?: ModuleFederationPluginOptions;
284
+ }
285
+
286
+ // The function return type is mandatory, otherwise we got an error TS4058.
287
+ export function defineDevHostConfig(swcConfig: SwcConfig, port: number, remotes: RemoteDefinition[], options: DefineDevHostConfigOptions = {}): webpack.Configuration {
288
+ const {
289
+ entry = resolveEntryFilePath(HostEntryFilePaths),
290
+ publicPath = "auto",
291
+ cache,
292
+ plugins = [],
293
+ htmlWebpackPluginOptions,
294
+ transformers = [],
295
+ features,
296
+ sharedDependencies,
297
+ runtimePlugins,
298
+ moduleFederationPluginOptions = defineHostModuleFederationPluginOptions(remotes, { features, shared: sharedDependencies, runtimePlugins }),
299
+ ...webpackOptions
300
+ } = options;
301
+
302
+ return defineDevConfig(swcConfig, {
303
+ entry,
304
+ port,
305
+ publicPath,
306
+ cache,
307
+ htmlWebpackPlugin: trySetHtmlWebpackPluginPublicPath(htmlWebpackPluginOptions ?? defineBuildHtmlWebpackPluginConfig()),
308
+ plugins: [
309
+ ...plugins,
310
+ new ModuleFederationPlugin(moduleFederationPluginOptions)
311
+ ],
312
+ ...webpackOptions,
313
+ transformers: [
314
+ createSetUniqueNameTransformer(HostApplicationName),
315
+ ...transformers
316
+ ]
317
+ });
318
+ }
319
+
320
+ export interface DefineBuildHostConfigOptions extends Omit<DefineBuildConfigOptions, "htmlWebpackPlugin"> {
321
+ htmlWebpackPluginOptions?: HtmlWebpackPlugin.Options;
322
+ features?: Features;
323
+ sharedDependencies?: ModuleFederationShared;
324
+ runtimePlugins?: ModuleFederationRuntimePlugins;
325
+ moduleFederationPluginOptions?: ModuleFederationPluginOptions;
326
+ }
327
+
328
+ // The function return type is mandatory, otherwise we got an error TS4058.
329
+ export function defineBuildHostConfig(swcConfig: SwcConfig, remotes: RemoteDefinition[], options: DefineBuildHostConfigOptions = {}): webpack.Configuration {
330
+ const {
331
+ entry = resolveEntryFilePath(HostEntryFilePaths),
332
+ publicPath = "auto",
333
+ plugins = [],
334
+ htmlWebpackPluginOptions,
335
+ transformers = [],
336
+ features,
337
+ sharedDependencies,
338
+ runtimePlugins,
339
+ moduleFederationPluginOptions = defineHostModuleFederationPluginOptions(remotes, { features, shared: sharedDependencies, runtimePlugins }),
340
+ ...webpackOptions
341
+ } = options;
342
+
343
+ return defineBuildConfig(swcConfig, {
344
+ entry,
345
+ publicPath,
346
+ htmlWebpackPlugin: trySetHtmlWebpackPluginPublicPath(htmlWebpackPluginOptions ?? defineDevHtmlWebpackPluginConfig()),
347
+ plugins: [
348
+ ...plugins,
349
+ new ModuleFederationPlugin(moduleFederationPluginOptions)
350
+ ],
351
+ transformers: [
352
+ forceNamedChunkIdsTransformer,
353
+ createSetUniqueNameTransformer(HostApplicationName),
354
+ ...transformers
355
+ ],
356
+ ...webpackOptions
357
+ });
358
+ }
359
+
360
+ //////////////////////////// Remote /////////////////////////////
361
+
362
+ const RemoteEntryFilePaths = [
363
+ "./src/register.tsx",
364
+ "./src/register.ts"
365
+ ];
366
+
367
+ export interface DefineRemoteModuleFederationPluginOptions extends ModuleFederationPluginOptions {
368
+ features?: Features;
369
+ }
370
+
371
+ // The function return type is mandatory, otherwise we got an error TS4058.
372
+ export function defineRemoteModuleFederationPluginOptions(applicationName: string, options: DefineRemoteModuleFederationPluginOptions): ModuleFederationPluginOptions {
373
+ const {
374
+ features = {},
375
+ exposes = {},
376
+ shared = {},
377
+ runtimePlugins = [],
378
+ ...rest
379
+ } = options;
380
+
381
+ const defaultSharedDependencies = resolveDefaultSharedDependencies(features, false);
382
+
383
+ return {
384
+ name: applicationName,
385
+ // Since Squide modules are only exporting a register function with a standardized API
386
+ // it doesn't requires any typing.
387
+ dts: false,
388
+ // Currently only supporting .js remotes.
389
+ manifest: false,
390
+ filename: RemoteEntryPoint,
391
+ exposes: {
392
+ [RemoteRegisterModuleName]: "./src/register",
393
+ ...exposes
394
+ },
395
+ // Deep merging the default shared dependencies with the provided shared dependencies
396
+ // to allow the consumer to easily override a default option of a shared dependency
397
+ // without extending the whole default shared dependencies object.
398
+ shared: merge.all([
399
+ defaultSharedDependencies,
400
+ shared
401
+ ]) as ModuleFederationShared,
402
+ runtimePlugins: [
403
+ path.resolve(packageDirectory, "./sharedDependenciesResolutionPlugin.js"),
404
+ path.resolve(packageDirectory, "./nonCacheableRemoteEntryPlugin.js"),
405
+ ...runtimePlugins
406
+ ],
407
+ // Commented because it doesn't seems to work, the runtime is still embedded into remotes.
408
+ // experiments: {
409
+ // // The runtime is 100kb minified.
410
+ // federationRuntime: "hoisted"
411
+ // },
412
+ ...rest
413
+ };
414
+ }
415
+
416
+ const devRemoteModuleTransformer: WebpackConfigTransformer = (config: WebpackConfig) => {
417
+ // "config.devServer" does exist but webpack types are a messed. It could probably be solved by importing "webpack-dev-server"
418
+ // but I would prefer not adding it as a project dependency.
419
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
420
+ // @ts-ignore
421
+ config.devServer.headers = {
422
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
423
+ // @ts-ignore
424
+ ...(config.devServer.headers ?? {}),
425
+ // Otherwise hot reload in the host failed with a CORS error.
426
+ "Access-Control-Allow-Origin": "*"
427
+ };
428
+
429
+ return config;
430
+ };
431
+
432
+ export interface DefineDevRemoteModuleConfigOptions extends Omit<DefineDevConfigOptions, "port" | "overlay"> {
433
+ features?: Features;
434
+ sharedDependencies?: ModuleFederationShared;
435
+ runtimePlugins?: ModuleFederationRuntimePlugins;
436
+ moduleFederationPluginOptions?: ModuleFederationPluginOptions;
437
+ }
438
+
439
+ // The function return type is mandatory, otherwise we got an error TS4058.
440
+ export function defineDevRemoteModuleConfig(swcConfig: SwcConfig, applicationName: string, port: number, options: DefineDevRemoteModuleConfigOptions = {}): webpack.Configuration {
441
+ const {
442
+ entry = resolveEntryFilePath(RemoteEntryFilePaths),
443
+ publicPath = "auto",
444
+ cache,
445
+ plugins = [],
446
+ htmlWebpackPlugin = false,
447
+ transformers = [],
448
+ features,
449
+ sharedDependencies,
450
+ runtimePlugins,
451
+ moduleFederationPluginOptions = defineRemoteModuleFederationPluginOptions(applicationName, { features, shared: sharedDependencies, runtimePlugins }),
452
+ ...webpackOptions
453
+ } = options;
454
+
455
+ return defineDevConfig(swcConfig, {
456
+ entry,
457
+ port,
458
+ publicPath,
459
+ cache,
460
+ htmlWebpackPlugin,
461
+ // Disable the error overlay by default for remotes as otherwise every remote module's error overlay will be
462
+ // stack on top of the host application's error overlay.
463
+ overlay: false,
464
+ plugins: [
465
+ ...plugins,
466
+ new ModuleFederationPlugin(moduleFederationPluginOptions)
467
+ ],
468
+ transformers: [
469
+ devRemoteModuleTransformer,
470
+ createSetUniqueNameTransformer(applicationName),
471
+ ...transformers
472
+ ],
473
+ ...webpackOptions
474
+ });
475
+ }
476
+
477
+ export interface DefineBuildRemoteModuleConfigOptions extends DefineBuildConfigOptions {
478
+ features?: Features;
479
+ sharedDependencies?: ModuleFederationShared;
480
+ runtimePlugins?: ModuleFederationRuntimePlugins;
481
+ moduleFederationPluginOptions?: ModuleFederationPluginOptions;
482
+ }
483
+
484
+ // The function return type is mandatory, otherwise we got an error TS4058.
485
+ export function defineBuildRemoteModuleConfig(swcConfig: SwcConfig, applicationName: string, options: DefineBuildRemoteModuleConfigOptions = {}): webpack.Configuration {
486
+ const {
487
+ entry = resolveEntryFilePath(RemoteEntryFilePaths),
488
+ publicPath = "auto",
489
+ plugins = [],
490
+ htmlWebpackPlugin = false,
491
+ transformers = [],
492
+ features,
493
+ sharedDependencies,
494
+ runtimePlugins,
495
+ moduleFederationPluginOptions = defineRemoteModuleFederationPluginOptions(applicationName, { features, shared: sharedDependencies, runtimePlugins }),
496
+ ...webpackOptions
497
+ } = options;
498
+
499
+ return defineBuildConfig(swcConfig, {
500
+ entry,
501
+ publicPath,
502
+ htmlWebpackPlugin,
503
+ plugins: [
504
+ ...plugins,
505
+ new ModuleFederationPlugin(moduleFederationPluginOptions)
506
+ ],
507
+ transformers: [
508
+ forceNamedChunkIdsTransformer,
509
+ createSetUniqueNameTransformer(applicationName),
510
+ ...transformers
511
+ ],
512
+ ...webpackOptions
513
+ });
514
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "@workleap/webpack-configs";
2
+ export * from "./defineConfig.ts";
3
+
@@ -0,0 +1,20 @@
1
+ import type { FederationRuntimePlugin } from "@module-federation/enhanced/runtime";
2
+
3
+ const plugin: () => FederationRuntimePlugin = () => {
4
+ return {
5
+ name: "non-cacheable-remote-entry-plugin",
6
+ createScript: function({ url }) {
7
+ const element = document.createElement("script");
8
+
9
+ // Adding a timestamp to make sure the remote entry points are never cached.
10
+ // View: https://github.com/module-federation/module-federation-examples/issues/566.
11
+ element.src = `${url}?t=${Date.now()}`;
12
+ element.type = "text/javascript";
13
+ element.async = true;
14
+
15
+ return element;
16
+ }
17
+ };
18
+ };
19
+
20
+ export default plugin;
package/src/shared.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Hardcoded to "host" because of the "sharedDependenciesResolutionPlugin" plugin.
2
+ export const HostApplicationName = "host";