diff --git a/e2e/angular/src/projects.test.ts b/e2e/angular/src/projects.test.ts index 03e8d37f2ecd1a..d25513cb3fdfca 100644 --- a/e2e/angular/src/projects.test.ts +++ b/e2e/angular/src/projects.test.ts @@ -118,7 +118,7 @@ describe('Angular Projects', () => { console.log( `The current es2015 bundle size is ${es2015BundleSize / 1000} KB` ); - expect(es2015BundleSize).toBeLessThanOrEqual(220000); + expect(es2015BundleSize).toBeLessThanOrEqual(221000); // check unit tests runCLI( diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json index 4e212c1f475ccd..b9bfbdce2d90c5 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 58fefc28c907ea..e81c257c19448b 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 7310db9dc17e59..00635d5873119f 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 8c41564ed02308..4cd42dfea615fb 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 ba8928796ac8e7..68a77408ba50a6 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 36e2ed54205377..2495afbdbca59c 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 a125470e800edf..c504a08db038e8 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,75 @@ export async function withModuleFederationForSSR( if (global.NX_GRAPH_CREATION) { return (config) => config; } + 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.disableNxRuntimeLibraryControlPlugin + ? [ + ...(configOverride?.runtimePlugins ?? []), + require.resolve( + '@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js' + ), + ] + : configOverride?.runtimePlugins, + }, + {} + ), + sharedLibraries.getReplacementPlugin(), + ], + }; + + // 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 91d3a575c06a85..bd8774fad6c283 100644 --- a/packages/angular/src/utils/mf/with-module-federation.ts +++ b/packages/angular/src/utils/mf/with-module-federation.ts @@ -12,50 +12,73 @@ export async function withModuleFederation( if (global.NX_GRAPH_CREATION) { return (config) => config; } + 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.disableNxRuntimeLibraryControlPlugin + ? [ + ...(configOverride?.runtimePlugins ?? []), + require.resolve( + '@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js' + ), + ] + : configOverride?.runtimePlugins, + }), + sharedLibraries.getReplacementPlugin(), + ], + }; + + // 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 11542dc6e1548a..8dc8b75e27cff6 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 e637287551b239..77db2451444bd1 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 8bc815a804645e..a5164c0203414d 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 c1ca0f6065b93e..01eee220b8ce60 100644 --- a/packages/react/src/module-federation/with-module-federation-ssr.ts +++ b/packages/react/src/module-federation/with-module-federation-ssr.ts @@ -42,12 +42,30 @@ export async function withModuleFederationForSSR( * Apply user-defined config overrides */ ...(configOverride ? configOverride : {}), + runtimePlugins: + process.env.NX_MF_DEV_REMOTES && + !options.disableNxRuntimeLibraryControlPlugin + ? [ + ...(configOverride?.runtimePlugins ?? []), + require.resolve( + '@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js' + ), + ] + : configOverride?.runtimePlugins, }, {} ), sharedLibraries.getReplacementPlugin() ); + // 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 c336882583250c..b6acd19f105670 100644 --- a/packages/react/src/module-federation/with-module-federation.ts +++ b/packages/react/src/module-federation/with-module-federation.ts @@ -20,6 +20,7 @@ export async function withModuleFederation( if (global.NX_GRAPH_CREATION) { return (config) => config; } + const { sharedDependencies, sharedLibraries, mappedRemotes } = await getModuleFederationConfig(options); const isGlobal = isVarOrWindow(options.library?.type); @@ -70,10 +71,28 @@ export async function withModuleFederation( * Apply user-defined config overrides */ ...(configOverride ? configOverride : {}), + runtimePlugins: + process.env.NX_MF_DEV_REMOTES && + !options.disableNxRuntimeLibraryControlPlugin + ? [ + ...(configOverride?.runtimePlugins ?? []), + require.resolve( + '@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js' + ), + ] + : configOverride?.runtimePlugins, }), sharedLibraries.getReplacementPlugin() ); + // 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 0a899eb95317d2..5738317ac01eb9 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 5547b48e6355ae..b14c8422fd2509 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 2a1150a9cfe929..351c39e8e19033 100644 --- a/packages/webpack/src/utils/module-federation/models/index.ts +++ b/packages/webpack/src/utils/module-federation/models/index.ts @@ -45,6 +45,12 @@ export interface ModuleFederationConfig { exposes?: Record; shared?: SharedFunction; additionalShared?: AdditionalSharedConfig; + /** + * `nxRuntimeLibraryControlPlugin` is a runtime module federation plugin to ensure + * that shared libraries are resolved from a remote with live reload capabilities. + * If you run into any issues with loading shared libraries, try disabling this option. + */ + disableNxRuntimeLibraryControlPlugin?: 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 00000000000000..4f5b57d2d0c5a4 --- /dev/null +++ b/packages/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.ts @@ -0,0 +1,71 @@ +import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; + +const runtimeStore: { + name?: string; + devRemotes?: string[]; + sharedPackagesFromDev: Record; +} = { + sharedPackagesFromDev: {}, +}; + +if (process.env.NX_MF_DEV_REMOTES) { + // process.env.NX_MF_DEV_REMOTES is replaced by an array value via DefinePlugin, even though the original value is a stringified array. + runtimeStore.devRemotes = process.env + .NX_MF_DEV_REMOTES as unknown as string[]; +} + +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; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87a92f9b8361ea..2b17a3ed0df62d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13530,7 +13530,7 @@ packages: '@use-gesture/react': 10.3.1(react@18.3.1) camera-controls: 2.8.5(three@0.166.1) cross-env: 7.0.3 - detect-gpu: 5.0.39 + detect-gpu: 5.0.38 glsl-noise: 0.0.0 hls.js: 1.3.5 maath: 0.10.8(@types/three@0.166.0)(three@0.166.1) @@ -21760,8 +21760,8 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - /detect-gpu@5.0.39: - resolution: {integrity: sha512-qs+7gnNNxsH4RN1IPpQieU2XNO+RhgemuaRhcawiUug6oXb0Glup90H1YGSjslPO30Sw0E4yfjRoGtSEURwVPQ==} + /detect-gpu@5.0.38: + resolution: {integrity: sha512-36QeGHSXYcJ/RfrnPEScR8GDprbXFG4ZhXsfVNVHztZr38+fRxgHnJl3CjYXXjbeRUhu3ZZBJh6Lg0A9v0Qd8A==} dependencies: webgl-constants: 1.1.1 dev: false @@ -27273,7 +27273,7 @@ packages: /launch-editor@2.6.1: resolution: {integrity: sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==} dependencies: - picocolors: 1.0.0 + picocolors: 1.0.1 shell-quote: 1.8.1 dev: true