From 51699f95747f20dcdb2adc6eb2ae8046eaa6ec30 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Wed, 3 Jul 2024 17:09:39 +0100 Subject: [PATCH] feat(module-federation): add nx-runtime-library-control-plugin --- packages/angular/ng-package.json | 3 +- packages/angular/package.json | 1 + .../module-federation-dev-ssr.impl.ts | 3 + .../module-federation-dev-server.impl.ts | 5 + .../generators/utils/add-mf-env-to-inputs.ts | 2 +- .../add-mf-env-var-to-target-defaults.spec.ts | 4 +- .../utils/mf/with-module-federation-ssr.ts | 109 +++++++++++------- .../src/utils/mf/with-module-federation.ts | 109 +++++++++++------- .../module-federation-dev-server.impl.ts | 5 + .../module-federation-ssr-dev-server.impl.ts | 3 + .../add-mf-env-var-to-target-defaults.spec.ts | 4 +- .../with-module-federation-ssr.ts | 22 ++++ .../with-module-federation.ts | 23 ++++ .../react/src/utils/add-mf-env-to-inputs.ts | 2 +- packages/webpack/package.json | 1 + .../utils/module-federation/models/index.ts | 7 ++ .../plugins/runtime-library-control.plugin.ts | 67 +++++++++++ 17 files changed, 281 insertions(+), 89 deletions(-) create mode 100644 packages/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.ts diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json index 4e212c1f475cc..b9bfbdce2d90c 100644 --- a/packages/angular/ng-package.json +++ b/packages/angular/ng-package.json @@ -25,7 +25,8 @@ "magic-string", "enquirer", "find-cache-dir", - "piscina" + "piscina", + "webpack" ], "keepLifecycleScripts": true } diff --git a/packages/angular/package.json b/packages/angular/package.json index e2090b894d739..c1d7d3be2a75a 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -57,6 +57,7 @@ "semver": "^7.5.3", "tslib": "^2.3.0", "webpack-merge": "^5.8.0", + "webpack": "^5.88.0", "@module-federation/enhanced": "~0.2.3", "@nx/devkit": "file:../devkit", "@nx/js": "file:../js", diff --git a/packages/angular/src/builders/module-federation-dev-ssr/module-federation-dev-ssr.impl.ts b/packages/angular/src/builders/module-federation-dev-ssr/module-federation-dev-ssr.impl.ts index 7310db9dc17e5..00635d5873119 100644 --- a/packages/angular/src/builders/module-federation-dev-ssr/module-federation-dev-ssr.impl.ts +++ b/packages/angular/src/builders/module-federation-dev-ssr/module-federation-dev-ssr.impl.ts @@ -60,6 +60,9 @@ export function executeModuleFederationDevSSRBuilder( ? options.devRemotes : [options.devRemotes]; + // Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin + process.env.NX_MF_DEV_REMOTES = JSON.stringify(devServeRemotes); + validateDevRemotes({ devRemotes: devServeRemotes }, workspaceProjects); const remotesToSkip = new Set(options.skipRemotes ?? []); diff --git a/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts b/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts index 8c41564ed0230..4cd42dfea615f 100644 --- a/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts +++ b/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts @@ -123,6 +123,11 @@ export async function* moduleFederationDevServerExecutor( pathToManifestFile ); + // Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin + process.env.NX_MF_DEV_REMOTES = JSON.stringify( + remotes.devRemotes.map((r) => (typeof r === 'string' ? r : r.remoteName)) + ); + if (remotes.devRemotes.length > 0 && !schema.staticRemotesPort) { options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => { const remoteName = typeof r === 'string' ? r : r.remoteName; diff --git a/packages/angular/src/generators/utils/add-mf-env-to-inputs.ts b/packages/angular/src/generators/utils/add-mf-env-to-inputs.ts index ba8928796ac8e..68a77408ba50a 100644 --- a/packages/angular/src/generators/utils/add-mf-env-to-inputs.ts +++ b/packages/angular/src/generators/utils/add-mf-env-to-inputs.ts @@ -3,7 +3,7 @@ import { type Tree, readNxJson, updateNxJson } from '@nx/devkit'; export function addMfEnvToTargetDefaultInputs(tree: Tree) { const nxJson = readNxJson(tree); const webpackExecutor = '@nx/angular:webpack-browser'; - const mfEnvVar = 'NX_MF_DEV_SERVER_STATIC_REMOTES'; + const mfEnvVar = 'NX_MF_DEV_REMOTES'; nxJson.targetDefaults ??= {}; nxJson.targetDefaults[webpackExecutor] ??= {}; diff --git a/packages/angular/src/migrations/update-18-0-0/add-mf-env-var-to-target-defaults.spec.ts b/packages/angular/src/migrations/update-18-0-0/add-mf-env-var-to-target-defaults.spec.ts index 36e2ed5420537..2495afbdbca59 100644 --- a/packages/angular/src/migrations/update-18-0-0/add-mf-env-var-to-target-defaults.spec.ts +++ b/packages/angular/src/migrations/update-18-0-0/add-mf-env-var-to-target-defaults.spec.ts @@ -29,7 +29,7 @@ describe('addMfEnvVarToTargetDefaults', () => { "production", "^production", { - "env": "NX_MF_DEV_SERVER_STATIC_REMOTES", + "env": "NX_MF_DEV_REMOTES", }, ], }, @@ -109,7 +109,7 @@ describe('addMfEnvVarToTargetDefaults', () => { "inputs": [ "^build", { - "env": "NX_MF_DEV_SERVER_STATIC_REMOTES", + "env": "NX_MF_DEV_REMOTES", }, ], }, diff --git a/packages/angular/src/utils/mf/with-module-federation-ssr.ts b/packages/angular/src/utils/mf/with-module-federation-ssr.ts index a125470e800ed..b2cf3eaa2bcc2 100644 --- a/packages/angular/src/utils/mf/with-module-federation-ssr.ts +++ b/packages/angular/src/utils/mf/with-module-federation-ssr.ts @@ -11,52 +11,79 @@ export async function withModuleFederationForSSR( if (global.NX_GRAPH_CREATION) { return (config) => config; } + + options.useNxLibraryControlPlugin ??= true; + const { sharedLibraries, sharedDependencies, mappedRemotes } = await getModuleFederationConfig(options, { isServer: true, }); - return (config) => ({ - ...(config ?? {}), - target: false, - output: { - ...(config.output ?? {}), - uniqueName: options.name, - }, - optimization: { - ...(config.optimization ?? {}), - runtimeChunk: false, - }, - resolve: { - ...(config.resolve ?? {}), - alias: { - ...(config.resolve?.alias ?? {}), - ...sharedLibraries.getAliases(), + return (config) => { + const updatedConfig = { + ...(config ?? {}), + target: false, + output: { + ...(config.output ?? {}), + uniqueName: options.name, }, - }, - plugins: [ - ...(config.plugins ?? []), - new (require('@module-federation/node').UniversalFederationPlugin)( - { - name: options.name, - filename: 'remoteEntry.js', - exposes: options.exposes, - remotes: mappedRemotes, - shared: { - ...sharedDependencies, - }, - library: { - type: 'commonjs-module', - }, - isServer: true, - /** - * Apply user-defined config override - */ - ...(configOverride ? configOverride : {}), + optimization: { + ...(config.optimization ?? {}), + runtimeChunk: false, + }, + resolve: { + ...(config.resolve ?? {}), + alias: { + ...(config.resolve?.alias ?? {}), + ...sharedLibraries.getAliases(), }, - {} - ), - sharedLibraries.getReplacementPlugin(), - ], - }); + }, + plugins: [ + ...(config.plugins ?? []), + new (require('@module-federation/node').UniversalFederationPlugin)( + { + name: options.name, + filename: 'remoteEntry.js', + exposes: options.exposes, + remotes: mappedRemotes, + shared: { + ...sharedDependencies, + }, + library: { + type: 'commonjs-module', + }, + isServer: true, + /** + * Apply user-defined config override + */ + ...(configOverride ? configOverride : {}), + runtimePlugins: + process.env.NX_MF_DEV_REMOTES && options.useNxLibraryControlPlugin + ? [ + ...(configOverride?.runtimePlugins ?? []), + require.resolve( + '@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js' + ), + ] + : configOverride?.runtimePlugins, + }, + {} + ), + sharedLibraries.getReplacementPlugin(), + ], + }; + + if (process.env.NX_MF_DEV_REMOTES && options.useNxLibraryControlPlugin) { + // The env var is only set from the module-federation-dev-server + // Attach the runtime plugin + + updatedConfig.plugins.push( + new (require('webpack').DefinePlugin)({ + 'process.env.NX_MF_DEV_REMOTES': process.env.NX_MF_DEV_REMOTES, + }) + ); + } + + return updatedConfig; + }; } diff --git a/packages/angular/src/utils/mf/with-module-federation.ts b/packages/angular/src/utils/mf/with-module-federation.ts index 91d3a575c06a8..bea63bb34e534 100644 --- a/packages/angular/src/utils/mf/with-module-federation.ts +++ b/packages/angular/src/utils/mf/with-module-federation.ts @@ -12,50 +12,77 @@ export async function withModuleFederation( if (global.NX_GRAPH_CREATION) { return (config) => config; } + + options.useNxLibraryControlPlugin ??= true; + const { sharedLibraries, sharedDependencies, mappedRemotes } = await getModuleFederationConfig(options); - return (config) => ({ - ...(config ?? {}), - output: { - ...(config.output ?? {}), - uniqueName: options.name, - publicPath: 'auto', - }, - optimization: { - ...(config.optimization ?? {}), - runtimeChunk: false, - }, - resolve: { - ...(config.resolve ?? {}), - alias: { - ...(config.resolve?.alias ?? {}), - ...sharedLibraries.getAliases(), + return (config) => { + const updatedConfig = { + ...(config ?? {}), + output: { + ...(config.output ?? {}), + uniqueName: options.name, + publicPath: 'auto', }, - }, - experiments: { - ...(config.experiments ?? {}), - outputModule: true, - }, - plugins: [ - ...(config.plugins ?? []), - new ModuleFederationPlugin({ - name: options.name, - filename: 'remoteEntry.mjs', - exposes: options.exposes, - remotes: mappedRemotes, - shared: { - ...sharedDependencies, - }, - library: { - type: 'module', + optimization: { + ...(config.optimization ?? {}), + runtimeChunk: false, + }, + resolve: { + ...(config.resolve ?? {}), + alias: { + ...(config.resolve?.alias ?? {}), + ...sharedLibraries.getAliases(), }, - /** - * Apply user-defined config override - */ - ...(configOverride ? configOverride : {}), - }), - sharedLibraries.getReplacementPlugin(), - ], - }); + }, + experiments: { + ...(config.experiments ?? {}), + outputModule: true, + }, + plugins: [ + ...(config.plugins ?? []), + new ModuleFederationPlugin({ + name: options.name, + filename: 'remoteEntry.mjs', + exposes: options.exposes, + remotes: mappedRemotes, + shared: { + ...sharedDependencies, + }, + library: { + type: 'module', + }, + /** + * Apply user-defined config override + */ + ...(configOverride ? configOverride : {}), + runtimePlugins: + process.env.NX_MF_DEV_REMOTES && options.useNxLibraryControlPlugin + ? [ + ...(configOverride?.runtimePlugins ?? []), + require.resolve( + '@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js' + ), + ] + : configOverride?.runtimePlugins, + }), + sharedLibraries.getReplacementPlugin(), + ], + }; + + if (process.env.NX_MF_DEV_REMOTES && options.useNxLibraryControlPlugin) { + // The env var is only set from the module-federation-dev-server + // Attach the runtime plugin + + updatedConfig.plugins.push( + new (require('webpack').DefinePlugin)({ + 'process.env.NX_MF_DEV_REMOTES': process.env.NX_MF_DEV_REMOTES, + }) + ); + } + + return updatedConfig; + }; } diff --git a/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts b/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts index 11542dc6e1548..8dc8b75e27cff 100644 --- a/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts +++ b/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts @@ -345,6 +345,11 @@ export default async function* moduleFederationDevServer( pathToManifestFile ); + // Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin + process.env.NX_MF_DEV_REMOTES = JSON.stringify( + remotes.devRemotes.map((r) => (typeof r === 'string' ? r : r.remoteName)) + ); + if (remotes.devRemotes.length > 0 && !initialStaticRemotesPorts) { options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => { const remoteName = typeof r === 'string' ? r : r.remoteName; diff --git a/packages/react/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts b/packages/react/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts index e637287551b23..77db2451444bd 100644 --- a/packages/react/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts +++ b/packages/react/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts @@ -120,6 +120,9 @@ export default async function* moduleFederationSsrDevServer( ? options.devRemotes : [options.devRemotes]; + // Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin + process.env.NX_MF_DEV_REMOTES = JSON.stringify(devServeApps); + for (const app of knownRemotes) { const [appName] = Array.isArray(app) ? app : [app]; const isDev = devServeApps.includes(appName); diff --git a/packages/react/src/migrations/update-18-0-0/add-mf-env-var-to-target-defaults.spec.ts b/packages/react/src/migrations/update-18-0-0/add-mf-env-var-to-target-defaults.spec.ts index 8bc815a804645..a5164c0203414 100644 --- a/packages/react/src/migrations/update-18-0-0/add-mf-env-var-to-target-defaults.spec.ts +++ b/packages/react/src/migrations/update-18-0-0/add-mf-env-var-to-target-defaults.spec.ts @@ -28,7 +28,7 @@ describe('addMfEnvVarToTargetDefaults', () => { "production", "^production", { - "env": "NX_MF_DEV_SERVER_STATIC_REMOTES", + "env": "NX_MF_DEV_REMOTES", }, ], }, @@ -109,7 +109,7 @@ describe('addMfEnvVarToTargetDefaults', () => { "inputs": [ "^build", { - "env": "NX_MF_DEV_SERVER_STATIC_REMOTES", + "env": "NX_MF_DEV_REMOTES", }, ], }, diff --git a/packages/react/src/module-federation/with-module-federation-ssr.ts b/packages/react/src/module-federation/with-module-federation-ssr.ts index c1ca0f6065b93..994db9fab7af8 100644 --- a/packages/react/src/module-federation/with-module-federation-ssr.ts +++ b/packages/react/src/module-federation/with-module-federation-ssr.ts @@ -12,6 +12,8 @@ export async function withModuleFederationForSSR( return (config) => config; } + options.useNxLibraryControlPlugin ??= true; + const { sharedLibraries, sharedDependencies, mappedRemotes } = await getModuleFederationConfig(options, { isServer: true, @@ -42,12 +44,32 @@ export async function withModuleFederationForSSR( * Apply user-defined config overrides */ ...(configOverride ? configOverride : {}), + runtimePlugins: + process.env.NX_MF_DEV_REMOTES && options.useNxLibraryControlPlugin + ? [ + ...(configOverride?.runtimePlugins ?? []), + require.resolve( + '@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js' + ), + ] + : configOverride?.runtimePlugins, }, {} ), sharedLibraries.getReplacementPlugin() ); + if (process.env.NX_MF_DEV_REMOTES && options.useNxLibraryControlPlugin) { + // The env var is only set from the module-federation-dev-server + // Attach the runtime plugin + + config.plugins.push( + new (require('webpack').DefinePlugin)({ + 'process.env.NX_MF_DEV_REMOTES': process.env.NX_MF_DEV_REMOTES, + }) + ); + } + return config; }; } diff --git a/packages/react/src/module-federation/with-module-federation.ts b/packages/react/src/module-federation/with-module-federation.ts index c336882583250..26f072a8c1066 100644 --- a/packages/react/src/module-federation/with-module-federation.ts +++ b/packages/react/src/module-federation/with-module-federation.ts @@ -20,6 +20,9 @@ export async function withModuleFederation( if (global.NX_GRAPH_CREATION) { return (config) => config; } + + options.useNxLibraryControlPlugin ??= true; + const { sharedDependencies, sharedLibraries, mappedRemotes } = await getModuleFederationConfig(options); const isGlobal = isVarOrWindow(options.library?.type); @@ -70,10 +73,30 @@ export async function withModuleFederation( * Apply user-defined config overrides */ ...(configOverride ? configOverride : {}), + runtimePlugins: + process.env.NX_MF_DEV_REMOTES && options.useNxLibraryControlPlugin + ? [ + ...(configOverride?.runtimePlugins ?? []), + require.resolve( + '@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js' + ), + ] + : configOverride?.runtimePlugins, }), sharedLibraries.getReplacementPlugin() ); + if (process.env.NX_MF_DEV_REMOTES && options.useNxLibraryControlPlugin) { + // The env var is only set from the module-federation-dev-server + // Attach the runtime plugin + + config.plugins.push( + new (require('webpack').DefinePlugin)({ + 'process.env.NX_MF_DEV_REMOTES': process.env.NX_MF_DEV_REMOTES, + }) + ); + } + return config; }; } diff --git a/packages/react/src/utils/add-mf-env-to-inputs.ts b/packages/react/src/utils/add-mf-env-to-inputs.ts index 0a899eb95317d..5738317ac01eb 100644 --- a/packages/react/src/utils/add-mf-env-to-inputs.ts +++ b/packages/react/src/utils/add-mf-env-to-inputs.ts @@ -3,7 +3,7 @@ import { type Tree, readNxJson, updateNxJson } from '@nx/devkit'; export function addMfEnvToTargetDefaultInputs(tree: Tree) { const nxJson = readNxJson(tree); const webpackExecutor = '@nx/webpack:webpack'; - const mfEnvVar = 'NX_MF_DEV_SERVER_STATIC_REMOTES'; + const mfEnvVar = 'NX_MF_DEV_REMOTES'; nxJson.targetDefaults ??= {}; nxJson.targetDefaults[webpackExecutor] ??= {}; diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 5547b48e6355a..b14c8422fd250 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -33,6 +33,7 @@ "@babel/core": "^7.23.2", "@phenomnomnominal/tsquery": "~5.0.1", "@module-federation/sdk": "^0.2.3", + "@module-federation/enhanced": "^0.2.3", "ajv": "^8.12.0", "autoprefixer": "^10.4.9", "babel-loader": "^9.1.2", diff --git a/packages/webpack/src/utils/module-federation/models/index.ts b/packages/webpack/src/utils/module-federation/models/index.ts index 2a1150a9cfe92..6e9e9b19b7796 100644 --- a/packages/webpack/src/utils/module-federation/models/index.ts +++ b/packages/webpack/src/utils/module-federation/models/index.ts @@ -45,6 +45,13 @@ export interface ModuleFederationConfig { exposes?: Record; shared?: SharedFunction; additionalShared?: AdditionalSharedConfig; + /** + * NxLibraryControlPlugin is a runtime module federation plugin that + * ensures that shared packages are resolved from a remote with live reload + * + * Default: true + */ + useNxLibraryControlPlugin?: boolean; } export type NxModuleFederationConfigOverride = Omit< diff --git a/packages/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.ts b/packages/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.ts new file mode 100644 index 0000000000000..7575594a040ff --- /dev/null +++ b/packages/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.ts @@ -0,0 +1,67 @@ +import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; + +const runtimeStore: { + name?: string; + devRemotes?: string[]; + sharedPackagesFromDev: Record; +} = { + sharedPackagesFromDev: {}, +}; + +if (process.env && process.env.NX_MF_DEV_REMOTES) { + runtimeStore.devRemotes = process.env.NX_MF_DEV_REMOTES as any; +} +const nxRuntimeLibraryControlPlugin: () => FederationRuntimePlugin = + function () { + return { + name: 'nx-runtime-library-control-plugin', + beforeInit(args) { + runtimeStore.name = args.options.name; + return args; + }, + resolveShare: (args) => { + const { shareScopeMap, scope, pkgName, version, GlobalFederation } = + args; + + const originalResolver = args.resolver; + args.resolver = function () { + if (!runtimeStore.sharedPackagesFromDev[pkgName]) { + if (!GlobalFederation.__INSTANCES__) { + return originalResolver(); + } else if (!runtimeStore.devRemotes) { + return originalResolver(); + } + const devRemoteInstanceToUse = GlobalFederation.__INSTANCES__.find( + (instance) => + instance.options.shared[pkgName] && + runtimeStore.devRemotes.find((dr) => instance.name === dr) + ); + if (!devRemoteInstanceToUse) { + return originalResolver(); + } + runtimeStore.sharedPackagesFromDev[pkgName] = + devRemoteInstanceToUse.name; + } + + const remoteInstanceName = + runtimeStore.sharedPackagesFromDev[pkgName]; + const remoteInstance = GlobalFederation.__INSTANCES__.find( + (instance) => instance.name === remoteInstanceName + ); + try { + const remotePkgInfo = remoteInstance.options.shared[pkgName].find( + (shared) => shared.from === remoteInstanceName + ); + remotePkgInfo.useIn.push(runtimeStore.name); + remotePkgInfo.useIn = Array.from(new Set(remotePkgInfo.useIn)); + shareScopeMap[scope][pkgName][version] = remotePkgInfo; + return remotePkgInfo; + } catch { + return originalResolver(); + } + }; + return args; + }, + }; + }; +export default nxRuntimeLibraryControlPlugin;