diff --git a/packages/core/src/mergeConfig.ts b/packages/core/src/mergeConfig.ts index ac1a8264b4..625c75ffc4 100644 --- a/packages/core/src/mergeConfig.ts +++ b/packages/core/src/mergeConfig.ts @@ -1,4 +1,4 @@ -import { type RsbuildConfig, castArray } from '@rsbuild/shared'; +import { type RsbuildConfig, castArray, cloneDeep } from '@rsbuild/shared'; import { isFunction, isPlainObject } from './helpers'; const OVERRIDE_PATHS = [ @@ -32,10 +32,10 @@ const merge = (x: unknown, y: unknown, path = '') => { // ignore undefined property if (x === undefined) { - return y; + return isPlainObject(y) ? cloneDeep(y) : y; } if (y === undefined) { - return x; + return isPlainObject(x) ? cloneDeep(x) : x; } const pair = [x, y]; diff --git a/packages/core/src/plugins/moduleFederation.ts b/packages/core/src/plugins/moduleFederation.ts index 27583b37c7..4fbcf0d193 100644 --- a/packages/core/src/plugins/moduleFederation.ts +++ b/packages/core/src/plugins/moduleFederation.ts @@ -88,54 +88,51 @@ export function pluginModuleFederation(): RsbuildPlugin { name: 'rsbuild:module-federation', setup(api) { - api.modifyRsbuildConfig({ - order: 'post', - handler: (config) => { - /** - * Currently, splitChunks will take precedence over module federation shared modules. - * So we need to disable the default split chunks rules to make shared modules to work properly. - * @see https://github.com/module-federation/module-federation-examples/issues/3161 - */ - if ( - config.moduleFederation?.options && - config.performance?.chunkSplit?.strategy === 'split-by-experience' - ) { - config.performance.chunkSplit = { - ...config.performance.chunkSplit, - strategy: 'custom', - }; - } - - return config; - }, + api.modifyEnvironmentConfig((config) => { + /** + * Currently, splitChunks will take precedence over module federation shared modules. + * So we need to disable the default split chunks rules to make shared modules to work properly. + * @see https://github.com/module-federation/module-federation-examples/issues/3161 + */ + if ( + config.moduleFederation?.options && + config.performance?.chunkSplit?.strategy === 'split-by-experience' + ) { + config.performance.chunkSplit = { + ...config.performance.chunkSplit, + strategy: 'custom', + }; + } }); - api.modifyBundlerChain(async (chain, { CHAIN_ID, target }) => { - const config = api.getNormalizedConfig(); + api.modifyBundlerChain( + async (chain, { CHAIN_ID, target, environment }) => { + const config = api.getNormalizedConfig({ environment }); - if (!config.moduleFederation?.options || target !== 'web') { - return; - } - - const { options } = config.moduleFederation; + if (!config.moduleFederation?.options || target !== 'web') { + return; + } - chain - .plugin(CHAIN_ID.PLUGIN.MODULE_FEDERATION) - .use(rspack.container.ModuleFederationPlugin, [options]); + const { options } = config.moduleFederation; - if (options.name) { chain - .plugin('mf-patch-split-chunks') - .use(PatchSplitChunksPlugin, [options.name]); - } + .plugin(CHAIN_ID.PLUGIN.MODULE_FEDERATION) + .use(rspack.container.ModuleFederationPlugin, [options]); - const publicPath = chain.output.get('publicPath'); + if (options.name) { + chain + .plugin('mf-patch-split-chunks') + .use(PatchSplitChunksPlugin, [options.name]); + } - // set the default publicPath to 'auto' to make MF work - if (publicPath === DEFAULT_ASSET_PREFIX) { - chain.output.set('publicPath', 'auto'); - } - }); + const publicPath = chain.output.get('publicPath'); + + // set the default publicPath to 'auto' to make MF work + if (publicPath === DEFAULT_ASSET_PREFIX) { + chain.output.set('publicPath', 'auto'); + } + }, + ); }, }; } diff --git a/packages/core/tests/__snapshots__/moduleFederation.test.ts.snap b/packages/core/tests/__snapshots__/moduleFederation.test.ts.snap new file mode 100644 index 0000000000..6df4f6334f --- /dev/null +++ b/packages/core/tests/__snapshots__/moduleFederation.test.ts.snap @@ -0,0 +1,170 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`plugin-module-federation > should set environment module federation config correctly 1`] = ` +[ + { + "optimization": { + "splitChunks": { + "cacheGroups": {}, + "chunks": "all", + "enforceSizeThreshold": 50000, + }, + }, + "plugins": [ + ModuleFederationPlugin { + "_options": { + "exposes": { + "Button": "src/Button", + }, + "filename": "remoteEntry.js", + "name": "remote", + "shared": { + "react": { + "requiredVersion": "^18.2.0", + "singleton": true, + }, + "react-dom": { + "requiredVersion": "^18.2.0", + "singleton": true, + }, + }, + }, + }, + PatchSplitChunksPlugin { + "name": "remote", + }, + ], + }, + { + "optimization": { + "splitChunks": { + "cacheGroups": { + "lib-axios": { + "name": "lib-axios", + "priority": 0, + "reuseExistingChunk": true, + "test": /\\[\\\\\\\\/\\]node_modules\\[\\\\\\\\/\\]\\(axios\\|axios-\\.\\+\\)\\[\\\\\\\\/\\]/, + }, + "lib-lodash": { + "name": "lib-lodash", + "priority": 0, + "reuseExistingChunk": true, + "test": /\\[\\\\\\\\/\\]node_modules\\[\\\\\\\\/\\]\\(lodash\\|lodash-es\\)\\[\\\\\\\\/\\]/, + }, + "lib-polyfill": { + "name": "lib-polyfill", + "priority": 0, + "reuseExistingChunk": true, + "test": /\\[\\\\\\\\/\\]node_modules\\[\\\\\\\\/\\]\\(tslib\\|core-js\\|@babel\\\\/runtime\\|@swc\\\\/helpers\\)\\[\\\\\\\\/\\]/, + }, + }, + "chunks": "all", + "enforceSizeThreshold": 50000, + }, + }, + }, +] +`; + +exports[`plugin-module-federation > should set module federation and environment chunkSplit config correctly 1`] = ` +[ + { + "optimization": { + "splitChunks": { + "cacheGroups": {}, + "chunks": "all", + "enforceSizeThreshold": 50000, + }, + }, + "plugins": [ + ModuleFederationPlugin { + "_options": { + "exposes": { + "Button": "src/Button", + }, + "filename": "remoteEntry.js", + "name": "remote", + "shared": { + "react": { + "requiredVersion": "^18.2.0", + "singleton": true, + }, + "react-dom": { + "requiredVersion": "^18.2.0", + "singleton": true, + }, + }, + }, + }, + PatchSplitChunksPlugin { + "name": "remote", + }, + ], + }, + { + "optimization": { + "splitChunks": false, + }, + "plugins": [ + ModuleFederationPlugin { + "_options": { + "exposes": { + "Button": "src/Button", + }, + "filename": "remoteEntry.js", + "name": "remote", + "shared": { + "react": { + "requiredVersion": "^18.2.0", + "singleton": true, + }, + "react-dom": { + "requiredVersion": "^18.2.0", + "singleton": true, + }, + }, + }, + }, + PatchSplitChunksPlugin { + "name": "remote", + }, + ], + }, +] +`; + +exports[`plugin-module-federation > should set module federation config 1`] = ` +{ + "optimization": { + "splitChunks": { + "cacheGroups": {}, + "chunks": "all", + "enforceSizeThreshold": 50000, + }, + }, + "plugins": [ + ModuleFederationPlugin { + "_options": { + "exposes": { + "Button": "src/Button", + }, + "filename": "remoteEntry.js", + "name": "remote", + "shared": { + "react": { + "requiredVersion": "^18.2.0", + "singleton": true, + }, + "react-dom": { + "requiredVersion": "^18.2.0", + "singleton": true, + }, + }, + }, + }, + PatchSplitChunksPlugin { + "name": "remote", + }, + ], +} +`; diff --git a/packages/core/tests/moduleFederation.test.ts b/packages/core/tests/moduleFederation.test.ts new file mode 100644 index 0000000000..51e644d1ef --- /dev/null +++ b/packages/core/tests/moduleFederation.test.ts @@ -0,0 +1,126 @@ +import { createStubRsbuild } from '@scripts/test-helper'; +import { pluginModuleFederation } from '../src/plugins/moduleFederation'; +import { pluginSplitChunks } from '../src/plugins/splitChunks'; + +describe('plugin-module-federation', () => { + it('should set module federation config', async () => { + const rsbuild = await createStubRsbuild({ + plugins: [pluginSplitChunks(), pluginModuleFederation()], + rsbuildConfig: { + performance: { + chunkSplit: { + strategy: 'split-by-experience', + }, + }, + moduleFederation: { + options: { + name: 'remote', + exposes: { + './Button': './src/Button', + }, + filename: 'remoteEntry.js', + shared: { + react: { + singleton: true, + requiredVersion: '^18.2.0', + }, + 'react-dom': { + singleton: true, + requiredVersion: '^18.2.0', + }, + }, + }, + }, + }, + }); + + const config = await rsbuild.unwrapConfig(); + expect(config).toMatchSnapshot(); + }); + + it('should set environment module federation config correctly', async () => { + const rsbuild = await createStubRsbuild({ + plugins: [pluginSplitChunks(), pluginModuleFederation()], + rsbuildConfig: { + performance: { + chunkSplit: { + strategy: 'split-by-experience', + }, + }, + environments: { + web: { + moduleFederation: { + options: { + name: 'remote', + exposes: { + './Button': './src/Button', + }, + filename: 'remoteEntry.js', + shared: { + react: { + singleton: true, + requiredVersion: '^18.2.0', + }, + 'react-dom': { + singleton: true, + requiredVersion: '^18.2.0', + }, + }, + }, + }, + }, + web1: {}, + }, + }, + }); + + const configs = await rsbuild.initConfigs(); + expect(configs).toMatchSnapshot(); + }); + + it('should set module federation and environment chunkSplit config correctly', async () => { + const rsbuild = await createStubRsbuild({ + plugins: [pluginSplitChunks(), pluginModuleFederation()], + rsbuildConfig: { + moduleFederation: { + options: { + name: 'remote', + exposes: { + './Button': './src/Button', + }, + filename: 'remoteEntry.js', + shared: { + react: { + singleton: true, + requiredVersion: '^18.2.0', + }, + 'react-dom': { + singleton: true, + requiredVersion: '^18.2.0', + }, + }, + }, + }, + environments: { + web: { + performance: { + chunkSplit: { + strategy: 'split-by-experience', + }, + }, + }, + web1: { + performance: { + chunkSplit: { + strategy: 'all-in-one', + }, + }, + }, + }, + }, + }); + + const configs = await rsbuild.initConfigs(); + expect(configs).toMatchSnapshot(); + }); +}); diff --git a/packages/plugin-assets-retry/src/index.ts b/packages/plugin-assets-retry/src/index.ts index bf4cceb871..39fa471126 100644 --- a/packages/plugin-assets-retry/src/index.ts +++ b/packages/plugin-assets-retry/src/index.ts @@ -12,7 +12,7 @@ export const pluginAssetsRetry = ( setup(api) { api.modifyBundlerChain( async (chain, { CHAIN_ID, HtmlPlugin, isProd, environment }) => { - const config = api.getNormalizedConfig(); + const config = api.getNormalizedConfig({ environment }); const htmlPaths = api.getHTMLPaths({ environment }); if (!options || Object.keys(htmlPaths).length === 0) {