feat(webpack, rspack): support multiple configurations (#29691)

This pull request includes changes to support multi-configuration mode
for both Rspack and Webpack.

## Currently
Currently our plugin only supports single configurations
```js
module.exports =  { 
  ...config
}
```
Which works in most cases but some applications can have mutliple
configs that serve different platforms.

## Changes
With these changes, the Webpack and Rspack plugins will also support
multi-configuration.
```js
module.exports = [ 
   { ...clientConfig },
   { ...serverConfig }
 ]
This commit is contained in:
Nicholas Cunningham 2025-01-22 18:50:03 -07:00 committed by GitHub
parent 123602c0d6
commit 7524356180
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 358 additions and 87 deletions

View File

@ -1,6 +1,7 @@
import { import {
checkFilesExist, checkFilesExist,
cleanupProject, cleanupProject,
createFile,
fileExists, fileExists,
listFiles, listFiles,
newProject, newProject,
@ -406,6 +407,202 @@ describe('Webpack Plugin', () => {
`Successfully ran target build for project ${appName}` `Successfully ran target build for project ${appName}`
); );
}); });
describe('config types', () => {
it('should support a standard config object', () => {
const appName = uniq('app');
runCLI(
`generate @nx/react:application --directory=apps/${appName} --bundler=webpack --e2eTestRunner=none`
);
updateFile(
`apps/${appName}/webpack.config.js`,
`
const path = require('path');
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
module.exports = {
target: 'node',
output: {
path: path.join(__dirname, '../../dist/${appName}')
},
plugins: [
new NxAppWebpackPlugin({
compiler: 'babel',
main: './src/main.tsx',
tsConfig: './tsconfig.app.json',
outputHashing: 'none',
optimization: false,
})
]
};`
);
const result = runCLI(`build ${appName}`);
expect(result).toContain(
`Successfully ran target build for project ${appName}`
);
});
it('should support a standard function that returns a config object', () => {
const appName = uniq('app');
runCLI(
`generate @nx/react:application --directory=apps/${appName} --bundler=webpack --e2eTestRunner=none`
);
updateFile(
`apps/${appName}/webpack.config.js`,
`
const path = require('path');
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
module.exports = () => {
return {
target: 'node',
output: {
path: path.join(__dirname, '../../dist/${appName}')
},
plugins: [
new NxAppWebpackPlugin({
compiler: 'tsc',
main: './src/main.tsx',
tsConfig: './tsconfig.app.json',
outputHashing: 'none',
optimization: false,
})
]
};
};`
);
const result = runCLI(`build ${appName}`);
expect(result).toContain(
`Successfully ran target build for project ${appName}`
);
});
it('should support an array of standard config objects', () => {
const appName = uniq('app');
const serverName = uniq('server');
runCLI(
`generate @nx/react:application --directory=apps/${appName} --bundler=webpack --e2eTestRunner=none`
);
// Create server index file
createFile(
`apps/${serverName}/index.js`,
`console.log('Hello from ${serverName}');\n`
);
updateFile(
`apps/${appName}/webpack.config.js`,
`
const path = require('path');
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
module.exports = [
{
name: 'client',
target: 'node',
output: {
path: path.join(__dirname, '../../dist/${appName}')
},
plugins: [
new NxAppWebpackPlugin({
compiler: 'tsc',
main: './src/main.tsx',
tsConfig: './tsconfig.app.json',
outputHashing: 'none',
optimization: false,
})
]
}, {
name: 'server',
target: 'node',
entry: '../${serverName}/index.js',
output: {
path: path.join(__dirname, '../../dist/${serverName}'),
filename: 'index.js',
},
}
];
`
);
const result = runCLI(`build ${appName}`);
checkFilesExist(`dist/${appName}/main.js`);
checkFilesExist(`dist/${serverName}/index.js`);
expect(result).toContain(
`Successfully ran target build for project ${appName}`
);
});
it('should support a function that returns an array of standard config objects', () => {
const appName = uniq('app');
const serverName = uniq('server');
runCLI(
`generate @nx/react:application --directory=apps/${appName} --bundler=webpack --e2eTestRunner=none`
);
// Create server index file
createFile(
`apps/${serverName}/index.js`,
`console.log('Hello from ${serverName}');\n`
);
updateFile(
`apps/${appName}/webpack.config.js`,
`
const path = require('path');
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
module.exports = () => {
return [
{
name: 'client',
target: 'node',
output: {
path: path.join(__dirname, '../../dist/${appName}')
},
plugins: [
new NxAppWebpackPlugin({
compiler: 'tsc',
main: './src/main.tsx',
tsConfig: './tsconfig.app.json',
outputHashing: 'none',
optimization: false,
})
]
},
{
name: 'server',
target: 'node',
entry: '../${serverName}/index.js',
output: {
path: path.join(__dirname, '../../dist/${serverName}'),
filename: 'index.js',
}
}
];
};`
);
const result = runCLI(`build ${appName}`);
checkFilesExist(`dist/${serverName}/index.js`);
checkFilesExist(`dist/${appName}/main.js`);
expect(result).toContain(
`Successfully ran target build for project ${appName}`
);
});
});
}); });
function readMainFile(dir: string): string { function readMainFile(dir: string): string {

View File

@ -149,10 +149,12 @@ async function createRspackTargets(
const rspackOptions = await readRspackOptions(rspackConfig); const rspackOptions = await readRspackOptions(rspackConfig);
const outputPath = normalizeOutputPath( const outputs = [];
rspackOptions.output?.path, for (const config of rspackOptions) {
projectRoot if (config.output?.path) {
); outputs.push(normalizeOutputPath(config.output.path, projectRoot));
}
}
const targets = {}; const targets = {};
@ -177,7 +179,7 @@ async function createRspackTargets(
externalDependencies: ['@rspack/cli'], externalDependencies: ['@rspack/cli'],
}, },
], ],
outputs: [outputPath], outputs,
}; };
targets[options.serveTargetName] = { targets[options.serveTargetName] = {

View File

@ -5,53 +5,88 @@ import { readNxJsonFromDisk } from 'nx/src/devkit-internals';
/** /**
* Reads the Rspack options from a give Rspack configuration. The configuration can be: * Reads the Rspack options from a give Rspack configuration. The configuration can be:
* 1. A standard config object * 1. A single standard config object
* 2. A standard function that returns a config object * 2. A standard function that returns a config object (standard Rspack)
* 3. A Nx-specific composable function that takes Nx context, rspack config, and returns the config object. * 3. An array of standard config objects (multi-configuration mode)
* 4. A Nx-specific composable function that takes Nx context, rspack config, and returns the config object.
* *
* @param rspackConfig * @param rspackConfig
*/ */
export async function readRspackOptions( export async function readRspackOptions(
rspackConfig: unknown rspackConfig: unknown
): Promise<Configuration> { ): Promise<Configuration[]> {
let config: Configuration; const configs: Configuration[] = [];
if (isNxRspackComposablePlugin(rspackConfig)) {
config = await rspackConfig( const resolveConfig = async (
{}, config: unknown
{ ): Promise<Configuration | Configuration[]> => {
// These values are only used during build-time, so passing stubs here just to read out let resolvedConfig: Configuration;
// the returned config object. if (isNxRspackComposablePlugin(config)) {
options: { resolvedConfig = await config(
root: workspaceRoot, {},
projectRoot: '', {
sourceRoot: '', // These values are only used during build-time, so passing stubs here just to read out
outputFileName: '', // the returned config object.
assets: [], options: {
main: '', root: workspaceRoot,
tsConfig: '', projectRoot: '',
outputPath: '', sourceRoot: '',
rspackConfig: '', outputFileName: '',
useTsconfigPaths: undefined, assets: [],
main: '',
tsConfig: '',
outputPath: '',
rspackConfig: '',
useTsconfigPaths: undefined,
},
context: {
root: workspaceRoot,
cwd: undefined,
isVerbose: false,
nxJsonConfiguration: readNxJsonFromDisk(workspaceRoot),
projectGraph: null,
projectsConfigurations: null,
},
}
);
} else if (typeof config === 'function') {
const resolved = await config(
{
production: true, // we want the production build options
}, },
context: { {}
root: workspaceRoot, );
cwd: undefined, // If the resolved configuration is an array, resolve each configuration
isVerbose: false, return Array.isArray(resolved)
nxJsonConfiguration: readNxJsonFromDisk(workspaceRoot), ? await Promise.all(resolved.map(resolveConfig))
projectGraph: null, : resolved;
projectsConfigurations: null, } else if (Array.isArray(config)) {
}, // If the config passed is an array, resolve each configuration
} const resolvedConfigs = await Promise.all(config.map(resolveConfig));
); return resolvedConfigs.flat();
} else if (typeof rspackConfig === 'function') { } else {
config = await rspackConfig( return config as Configuration;
{ }
production: true, // we want the production build options };
},
{} // Since configs can have nested arrays, we need to flatten them
); const flattenConfigs = (
resolvedConfigs: Configuration | Configuration[]
): Configuration[] => {
return Array.isArray(resolvedConfigs)
? resolvedConfigs.flatMap((cfg) => flattenConfigs(cfg))
: [resolvedConfigs];
};
if (Array.isArray(rspackConfig)) {
for (const config of rspackConfig) {
const resolved = await resolveConfig(config);
configs.push(...flattenConfigs(resolved));
}
} else { } else {
config = rspackConfig; const resolved = await resolveConfig(rspackConfig);
configs.push(...flattenConfigs(resolved));
} }
return config;
return configs;
} }

View File

@ -170,10 +170,12 @@ async function createWebpackTargets(
const webpackOptions = await readWebpackOptions(webpackConfig); const webpackOptions = await readWebpackOptions(webpackConfig);
const outputPath = normalizeOutputPath( const outputs = [];
webpackOptions.output?.path, for (const config of webpackOptions) {
projectRoot if (config.output?.path) {
); outputs.push(normalizeOutputPath(config.output.path, projectRoot));
}
}
const targets: Record<string, TargetConfiguration> = {}; const targets: Record<string, TargetConfiguration> = {};
@ -198,7 +200,7 @@ async function createWebpackTargets(
externalDependencies: ['webpack-cli'], externalDependencies: ['webpack-cli'],
}, },
], ],
outputs: [outputPath], outputs,
metadata: { metadata: {
technologies: ['webpack'], technologies: ['webpack'],
description: 'Runs Webpack build', description: 'Runs Webpack build',

View File

@ -7,48 +7,83 @@ import { readNxJsonFromDisk } from 'nx/src/devkit-internals';
* Reads the webpack options from a give webpack configuration. The configuration can be: * Reads the webpack options from a give webpack configuration. The configuration can be:
* 1. A standard config object * 1. A standard config object
* 2. A standard function that returns a config object (webpack.js.org/configuration/configuration-types/#exporting-a-function) * 2. A standard function that returns a config object (webpack.js.org/configuration/configuration-types/#exporting-a-function)
* 3. A Nx-specific composable function that takes Nx context, webpack config, and returns the config object. * 3. An array of standard config objects (multi-configuration mode)
* 4. A Nx-specific composable function that takes Nx context, webpack config, and returns the config object.
* *
* @param webpackConfig * @param webpackConfig
*/ */
export async function readWebpackOptions( export async function readWebpackOptions(
webpackConfig: unknown webpackConfig: unknown
): Promise<Configuration> { ): Promise<Configuration[]> {
let config: Configuration; const configs: Configuration[] = [];
if (isNxWebpackComposablePlugin(webpackConfig)) {
config = await webpackConfig( const resolveConfig = async (
{}, config: unknown
{ ): Promise<Configuration | Configuration[]> => {
// These values are only used during build-time, so passing stubs here just to read out if (isNxWebpackComposablePlugin(config)) {
// the returned config object. return await config(
options: { {},
root: workspaceRoot, {
projectRoot: '', // These values are only used during build-time, so passing stubs here just to read out
sourceRoot: '', options: {
outputFileName: undefined, root: workspaceRoot,
outputPath: undefined, projectRoot: '',
assets: undefined, sourceRoot: '',
useTsconfigPaths: undefined, outputFileName: undefined,
outputPath: undefined,
assets: undefined,
useTsconfigPaths: undefined,
},
context: {
root: workspaceRoot,
cwd: undefined,
isVerbose: false,
projectsConfigurations: null,
projectGraph: null,
nxJsonConfiguration: readNxJsonFromDisk(workspaceRoot),
},
}
);
} else if (typeof config === 'function') {
const resolved = await config(
{
production: true, // we want the production build options
}, },
context: { {}
root: workspaceRoot, );
cwd: undefined,
isVerbose: false, // If the resolved configuration is an array, resolve each configuration
projectsConfigurations: null, return Array.isArray(resolved)
projectGraph: null, ? await Promise.all(resolved.map(resolveConfig))
nxJsonConfiguration: readNxJsonFromDisk(workspaceRoot), : resolved;
}, } else if (Array.isArray(config)) {
} // If the config passed is an array, resolve each configuration
); const resolvedConfigs = await Promise.all(config.map(resolveConfig));
} else if (typeof webpackConfig === 'function') { return resolvedConfigs.flat();
config = await webpackConfig( } else {
{ // Return plain configuration
production: true, // we want the production build options return config as Configuration;
}, }
{} };
);
// Since configs can have nested arrays, we need to flatten them
const flattenConfigs = (
resolvedConfigs: Configuration | Configuration[]
): Configuration[] => {
return Array.isArray(resolvedConfigs)
? resolvedConfigs.flatMap((cfg) => flattenConfigs(cfg))
: [resolvedConfigs];
};
if (Array.isArray(webpackConfig)) {
for (const config of webpackConfig) {
const resolved = await resolveConfig(config);
configs.push(...flattenConfigs(resolved));
}
} else { } else {
config = webpackConfig; const resolved = await resolveConfig(webpackConfig);
configs.push(...flattenConfigs(resolved));
} }
return config;
return configs;
} }