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 {
checkFilesExist,
cleanupProject,
createFile,
fileExists,
listFiles,
newProject,
@ -406,6 +407,202 @@ describe('Webpack Plugin', () => {
`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 {

View File

@ -149,10 +149,12 @@ async function createRspackTargets(
const rspackOptions = await readRspackOptions(rspackConfig);
const outputPath = normalizeOutputPath(
rspackOptions.output?.path,
projectRoot
);
const outputs = [];
for (const config of rspackOptions) {
if (config.output?.path) {
outputs.push(normalizeOutputPath(config.output.path, projectRoot));
}
}
const targets = {};
@ -177,7 +179,7 @@ async function createRspackTargets(
externalDependencies: ['@rspack/cli'],
},
],
outputs: [outputPath],
outputs,
};
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:
* 1. A standard config object
* 2. A standard function that returns a config object
* 3. A Nx-specific composable function that takes Nx context, rspack config, and returns the config object.
* 1. A single standard config object
* 2. A standard function that returns a config object (standard Rspack)
* 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
*/
export async function readRspackOptions(
rspackConfig: unknown
): Promise<Configuration> {
let config: Configuration;
if (isNxRspackComposablePlugin(rspackConfig)) {
config = await rspackConfig(
{},
{
// These values are only used during build-time, so passing stubs here just to read out
// the returned config object.
options: {
root: workspaceRoot,
projectRoot: '',
sourceRoot: '',
outputFileName: '',
assets: [],
main: '',
tsConfig: '',
outputPath: '',
rspackConfig: '',
useTsconfigPaths: undefined,
): Promise<Configuration[]> {
const configs: Configuration[] = [];
const resolveConfig = async (
config: unknown
): Promise<Configuration | Configuration[]> => {
let resolvedConfig: Configuration;
if (isNxRspackComposablePlugin(config)) {
resolvedConfig = await config(
{},
{
// These values are only used during build-time, so passing stubs here just to read out
// the returned config object.
options: {
root: workspaceRoot,
projectRoot: '',
sourceRoot: '',
outputFileName: '',
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,
isVerbose: false,
nxJsonConfiguration: readNxJsonFromDisk(workspaceRoot),
projectGraph: null,
projectsConfigurations: null,
},
}
);
} else if (typeof rspackConfig === 'function') {
config = await rspackConfig(
{
production: true, // we want the production build options
},
{}
);
{}
);
// If the resolved configuration is an array, resolve each configuration
return Array.isArray(resolved)
? await Promise.all(resolved.map(resolveConfig))
: resolved;
} 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 {
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(rspackConfig)) {
for (const config of rspackConfig) {
const resolved = await resolveConfig(config);
configs.push(...flattenConfigs(resolved));
}
} 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 outputPath = normalizeOutputPath(
webpackOptions.output?.path,
projectRoot
);
const outputs = [];
for (const config of webpackOptions) {
if (config.output?.path) {
outputs.push(normalizeOutputPath(config.output.path, projectRoot));
}
}
const targets: Record<string, TargetConfiguration> = {};
@ -198,7 +200,7 @@ async function createWebpackTargets(
externalDependencies: ['webpack-cli'],
},
],
outputs: [outputPath],
outputs,
metadata: {
technologies: ['webpack'],
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:
* 1. A standard config object
* 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
*/
export async function readWebpackOptions(
webpackConfig: unknown
): Promise<Configuration> {
let config: Configuration;
if (isNxWebpackComposablePlugin(webpackConfig)) {
config = await webpackConfig(
{},
{
// These values are only used during build-time, so passing stubs here just to read out
// the returned config object.
options: {
root: workspaceRoot,
projectRoot: '',
sourceRoot: '',
outputFileName: undefined,
outputPath: undefined,
assets: undefined,
useTsconfigPaths: undefined,
): Promise<Configuration[]> {
const configs: Configuration[] = [];
const resolveConfig = async (
config: unknown
): Promise<Configuration | Configuration[]> => {
if (isNxWebpackComposablePlugin(config)) {
return await config(
{},
{
// These values are only used during build-time, so passing stubs here just to read out
options: {
root: workspaceRoot,
projectRoot: '',
sourceRoot: '',
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,
projectsConfigurations: null,
projectGraph: null,
nxJsonConfiguration: readNxJsonFromDisk(workspaceRoot),
},
}
);
} else if (typeof webpackConfig === 'function') {
config = await webpackConfig(
{
production: true, // we want the production build options
},
{}
);
{}
);
// If the resolved configuration is an array, resolve each configuration
return Array.isArray(resolved)
? await Promise.all(resolved.map(resolveConfig))
: resolved;
} 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 {
// Return plain configuration
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 {
config = webpackConfig;
const resolved = await resolveConfig(webpackConfig);
configs.push(...flattenConfigs(resolved));
}
return config;
return configs;
}