diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json
index 2d04847817a34b..956a1946b54b08 100644
--- a/docs/generated/manifests/menus.json
+++ b/docs/generated/manifests/menus.json
@@ -9835,6 +9835,14 @@
"children": [],
"isExternal": false,
"disableCollapsible": false
+ },
+ {
+ "id": "convert-config-to-webpack-plugin",
+ "path": "/nx-api/webpack/generators/convert-config-to-webpack-plugin",
+ "name": "convert-config-to-webpack-plugin",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
}
],
"isExternal": false,
diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json
index 5acb369ee33d04..d7ba32aad33a8d 100644
--- a/docs/generated/manifests/nx-api.json
+++ b/docs/generated/manifests/nx-api.json
@@ -3179,6 +3179,15 @@
"originalFilePath": "/packages/webpack/src/generators/configuration/schema.json",
"path": "/nx-api/webpack/generators/configuration",
"type": "generator"
+ },
+ "/nx-api/webpack/generators/convert-config-to-webpack-plugin": {
+ "description": "Convert the project to use the `NxAppWebpackPlugin` and `NxReactWebpackPlugin`.",
+ "file": "generated/packages/webpack/generators/convert-config-to-webpack-plugin.json",
+ "hidden": false,
+ "name": "convert-config-to-webpack-plugin",
+ "originalFilePath": "/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json",
+ "path": "/nx-api/webpack/generators/convert-config-to-webpack-plugin",
+ "type": "generator"
}
},
"path": "/nx-api/webpack"
diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json
index 20da3c9e981883..a446900bcb9538 100644
--- a/docs/generated/packages-metadata.json
+++ b/docs/generated/packages-metadata.json
@@ -3144,6 +3144,15 @@
"originalFilePath": "/packages/webpack/src/generators/configuration/schema.json",
"path": "webpack/generators/configuration",
"type": "generator"
+ },
+ {
+ "description": "Convert the project to use the `NxAppWebpackPlugin` and `NxReactWebpackPlugin`.",
+ "file": "generated/packages/webpack/generators/convert-config-to-webpack-plugin.json",
+ "hidden": false,
+ "name": "convert-config-to-webpack-plugin",
+ "originalFilePath": "/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json",
+ "path": "webpack/generators/convert-config-to-webpack-plugin",
+ "type": "generator"
}
],
"githubRoot": "https://github.com/nrwl/nx/blob/master",
diff --git a/docs/generated/packages/webpack/generators/convert-config-to-webpack-plugin.json b/docs/generated/packages/webpack/generators/convert-config-to-webpack-plugin.json
new file mode 100644
index 00000000000000..a632cce3f1d376
--- /dev/null
+++ b/docs/generated/packages/webpack/generators/convert-config-to-webpack-plugin.json
@@ -0,0 +1,30 @@
+{
+ "name": "convert-config-to-webpack-plugin",
+ "factory": "./src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin",
+ "schema": {
+ "$schema": "https://json-schema.org/schema",
+ "$id": "NxWebpackConvertConfigToWebpackPlugin",
+ "description": "Convert existing Webpack project(s) using `@nx/webpack:webpack` executor that uses `withNx` to use `NxAppWebpackPlugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
+ "title": "Convert Webpack project using withNx to NxAppWebpackPlugin",
+ "type": "object",
+ "properties": {
+ "project": {
+ "type": "string",
+ "description": "The project to convert from using the `@nx/webpack:webpack` executor and `withNx` plugin to use `NxAppWebpackPlugin`.",
+ "x-priority": "important"
+ },
+ "skipFormat": {
+ "type": "boolean",
+ "description": "Whether to format files at the end of the migration.",
+ "default": false
+ }
+ },
+ "presets": []
+ },
+ "description": "Convert the project to use the `NxAppWebpackPlugin` and `NxReactWebpackPlugin`.",
+ "implementation": "/packages/webpack/src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin.ts",
+ "aliases": [],
+ "hidden": false,
+ "path": "/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json",
+ "type": "generator"
+}
diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md
index a2943de2cdf2a6..5810efbe669592 100644
--- a/docs/shared/reference/sitemap.md
+++ b/docs/shared/reference/sitemap.md
@@ -705,6 +705,7 @@
- [generators](/nx-api/webpack/generators)
- [init](/nx-api/webpack/generators/init)
- [configuration](/nx-api/webpack/generators/configuration)
+ - [convert-config-to-webpack-plugin](/nx-api/webpack/generators/convert-config-to-webpack-plugin)
- [workspace](/nx-api/workspace)
- [documents](/nx-api/workspace/documents)
- [Overview](/nx-api/workspace/documents/overview)
diff --git a/e2e/webpack/src/webpack.legacy.test.ts b/e2e/webpack/src/webpack.legacy.test.ts
index b626383e3cf441..e2c29fd85060ab 100644
--- a/e2e/webpack/src/webpack.legacy.test.ts
+++ b/e2e/webpack/src/webpack.legacy.test.ts
@@ -3,6 +3,7 @@ import {
cleanupProject,
killProcessAndPorts,
newProject,
+ readFile,
runCLI,
runCommandUntil,
runE2ETests,
@@ -146,4 +147,37 @@ describe('Webpack Plugin (legacy)', () => {
}).not.toThrow();
}
});
+
+ it('should convert withNx webpack config to a standard config using NxWebpackPlugin', () => {
+ const appName = uniq('app');
+ runCLI(
+ `generate @nx/web:app ${appName} --bundler webpack --e2eTestRunner=playwright`
+ );
+ updateFile(
+ `${appName}/src/main.ts`,
+ `
+ const root = document.querySelector('proj-root');
+ if(root) {
+ root.innerHTML = '
Welcome
'
+ }
+ `
+ );
+
+ runCLI(`generate @nx/webpack:convert-config-to-webpack-plugin ${appName}`);
+
+ const webpackConfig = readFile(`${appName}/webpack.config.js`);
+
+ checkFilesExist(`${appName}/webpack.config.old.js`);
+ expect(webpackConfig).toMatchSnapshot();
+
+ expect(() => {
+ runCLI(`build ${appName}`);
+ }).not.toThrow();
+
+ if (runE2ETests()) {
+ expect(() => {
+ runCLI(`e2e ${appName}-e2e`);
+ }).not.toThrow();
+ }
+ });
});
diff --git a/packages/webpack/generators.json b/packages/webpack/generators.json
index 9e3b52de760876..91d3104042f14d 100644
--- a/packages/webpack/generators.json
+++ b/packages/webpack/generators.json
@@ -15,6 +15,11 @@
"schema": "./src/generators/configuration/schema.json",
"description": "Add webpack configuration to a project.",
"hidden": true
+ },
+ "convert-config-to-webpack-plugin": {
+ "factory": "./src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin",
+ "schema": "./src/generators/convert-config-to-webpack-plugin/schema.json",
+ "description": "Convert the project to use the `NxAppWebpackPlugin` and `NxReactWebpackPlugin`."
}
}
}
diff --git a/packages/webpack/index.ts b/packages/webpack/index.ts
index 8bd0d2d58331cb..11d268a0172f77 100644
--- a/packages/webpack/index.ts
+++ b/packages/webpack/index.ts
@@ -1,8 +1,9 @@
import { configurationGenerator } from './src/generators/configuration/configuration';
import { NxAppWebpackPlugin } from './src/plugins/nx-webpack-plugin/nx-app-webpack-plugin';
import { NxTsconfigPathsWebpackPlugin as _NxTsconfigPathsWebpackPlugin } from './src/plugins/nx-typescript-webpack-plugin/nx-tsconfig-paths-webpack-plugin';
+import { convertConfigToWebpackPluginGenerator } from './src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin';
-export { configurationGenerator };
+export { configurationGenerator, convertConfigToWebpackPluginGenerator };
// Exported for backwards compatibility in case a plugin is using the old name.
/** @deprecated Use `configurationGenerator` instead. */
diff --git a/packages/webpack/src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin.spec.ts b/packages/webpack/src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin.spec.ts
new file mode 100644
index 00000000000000..e68c6f1f5d45d3
--- /dev/null
+++ b/packages/webpack/src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin.spec.ts
@@ -0,0 +1,332 @@
+import {
+ ProjectConfiguration,
+ Tree,
+ addProjectConfiguration,
+} from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import convertConfigToWebpackPluginGenerator from './convert-config-to-webpack-plugin';
+
+interface CreateProjectOptions {
+ name: string;
+ root: string;
+ targetName: string;
+ targetOptions: Record;
+ additionalTargets?: Record;
+}
+
+const defaultOptions: CreateProjectOptions = {
+ name: 'my-app',
+ root: 'my-app',
+ targetName: 'build',
+ targetOptions: {},
+};
+
+function createProject(tree: Tree, options: Partial) {
+ const projectOpts = {
+ ...defaultOptions,
+ ...options,
+ targetOptions: {
+ ...defaultOptions.targetOptions,
+ ...options?.targetOptions,
+ },
+ };
+ const project: ProjectConfiguration = {
+ name: projectOpts.name,
+ root: projectOpts.root,
+ targets: {
+ build: {
+ executor: '@nx/webpack:webpack',
+ options: {
+ webpackConfig: `${projectOpts.root}/webpack.config.js`,
+ ...projectOpts.targetOptions,
+ },
+ },
+ ...options.additionalTargets,
+ },
+ };
+
+ addProjectConfiguration(tree, project.name, project);
+
+ return project;
+}
+
+describe('convertConfigToWebpackPluginGenerator', () => {
+ let tree: Tree;
+
+ beforeEach(() => {
+ tree = createTreeWithEmptyWorkspace();
+ });
+
+ it('should migrate the webpack config of the specified project', async () => {
+ const project = createProject(tree, {
+ name: 'my-app',
+ root: 'my-app',
+ });
+
+ createProject(tree, {
+ name: 'another-app',
+ root: 'another-app',
+ });
+
+ tree.write(
+ 'another-app/webpack.config.js',
+ `
+ const { composePlugins, withNx } = require('@nx/webpack');
+ const { withReact } = require('@nx/react');
+
+ // Nx plugins for webpack.
+ module.exports = composePlugins(
+ withNx(),
+ withReact({
+ // Uncomment this line if you don't want to use SVGR
+ // See: https://react-svgr.com/
+ // svgr: false
+ }),
+ (config) => {
+ return config;
+ }
+ );
+ `
+ );
+
+ tree.write(
+ `${project.name}/webpack.config.js`,
+ `
+ const { composePlugins, withNx } = require('@nx/webpack');
+ const { withReact } = require('@nx/react');
+
+ // Nx plugins for webpack.
+ module.exports = composePlugins(
+ withNx(),
+ withReact({
+ // Uncomment this line if you don't want to use SVGR
+ // See: https://react-svgr.com/
+ // svgr: false
+ }),
+ (config) => {
+ return config;
+ }
+ );
+ `
+ );
+
+ await convertConfigToWebpackPluginGenerator(tree, {
+ project: project.name,
+ });
+ expect(tree.read(`${project.name}/webpack.config.js`, 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
+ const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
+
+ // This file was migrated using @nx/webpack:convert-config-to-webpack-plugin
+
+ module.exports = {
+ plugins: [
+ new NxAppWebpackPlugin(),
+ new NxReactWebpackPlugin({
+ // Uncomment this line if you don't want to use SVGR
+ // See: https://react-svgr.com/
+ // svgr: false
+ }),
+ ],
+ };
+ "
+ `);
+
+ expect(tree.read(`another-app/webpack.config.js`, 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "const { composePlugins, withNx } = require('@nx/webpack');
+ const { withReact } = require('@nx/react');
+
+ // Nx plugins for webpack.
+ module.exports = composePlugins(
+ withNx(),
+ withReact({
+ // Uncomment this line if you don't want to use SVGR
+ // See: https://react-svgr.com/
+ // svgr: false
+ }),
+ (config) => {
+ return config;
+ }
+ );
+ "
+ `);
+
+ expect(tree.exists(`${project.name}/webpack.config.old.js`)).toBe(true);
+ expect(tree.exists(`another-app/webpack.config.old.js`)).toBe(false);
+ });
+
+ it('should throw an error if no projects are found', async () => {
+ const project = createProject(tree, {
+ name: 'my-app',
+ root: 'my-app',
+ });
+
+ await expect(
+ convertConfigToWebpackPluginGenerator(tree, {
+ project: project.name,
+ })
+ ).rejects.toThrowError('Could not find any projects to migrate.');
+ });
+
+ it('should not migrate a webpack config that does not use withNx', async () => {
+ const project = createProject(tree, {
+ name: 'my-app',
+ root: 'my-app',
+ });
+
+ tree.write(`${project.name}/webpack.config.js`, `module.exports = {};`);
+
+ await expect(
+ convertConfigToWebpackPluginGenerator(tree, {
+ project: project.name,
+ })
+ ).rejects.toThrowError('Could not find any projects to migrate.');
+
+ expect(
+ tree.read(`${project.name}/webpack.config.js`, 'utf-8')
+ ).toMatchInlineSnapshot(`"module.exports = {};"`);
+ });
+
+ it('should throw an error if the project is using Module federation', async () => {
+ const project = createProject(tree, {
+ name: 'my-app',
+ root: 'my-app',
+ additionalTargets: {
+ serve: {
+ executor: '@nx/react:module-federation-dev-server',
+ options: {
+ buildTarget: 'my-app:build',
+ },
+ },
+ },
+ });
+
+ await expect(
+ convertConfigToWebpackPluginGenerator(tree, { project: project.name })
+ ).rejects.toThrowError(
+ `The project ${project.name} is using Module Federation. At the moment, we don't support migrating projects that use Module Federation.`
+ );
+ });
+
+ it('should not migrate a webpack config that is already using NxAppWebpackPlugin', async () => {
+ const project = createProject(tree, {
+ name: 'my-app',
+ root: 'my-app',
+ });
+
+ tree.write(
+ `${project.name}/webpack.config.js`,
+ `
+ const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
+
+ module.exports = {
+ plugins: [
+ new NxAppWebpackPlugin(),
+ ],
+ };
+ `
+ );
+
+ await expect(
+ convertConfigToWebpackPluginGenerator(tree, { project: project.name })
+ ).rejects.toThrowError(`Could not find any projects to migrate.`);
+ expect(tree.read(`${project.name}/webpack.config.js`, 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "
+ const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
+
+ module.exports = {
+ plugins: [
+ new NxAppWebpackPlugin(),
+ ],
+ };
+ "
+ `);
+ expect(tree.exists(`${project.name}/webpack.config.old.js`)).toBe(false);
+ });
+
+ it('should convert absolute options paths to relative paths during the conversion', async () => {
+ const project = createProject(tree, {
+ name: 'my-app',
+ root: 'apps/my-app',
+ });
+
+ tree.write(
+ `${project.root}/webpack.config.js`,
+ `
+ const { composePlugins, withNx } = require('@nx/webpack');
+ const { withReact } = require('@nx/react');
+
+ // Nx plugins for webpack.
+ module.exports = composePlugins(
+ withNx({
+ assets: ["apps/${project.name}/src/favicon.ico","apps/${project.name}/src/assets"],
+ styles: ["apps/${project.name}/src/styles.scss"],
+ scripts: ["apps/${project.name}/src/scripts.js"],
+ tsConfig: "apps/${project.name}/tsconfig.app.json",
+ fileReplacements: [
+ {
+ replace: "apps/${project.name}/src/environments/environment.ts",
+ with: "apps/${project.name}/src/environments/environment.prod.ts"
+ }
+ ],
+ additionalEntryPoints: [
+ {
+ entryPath: "apps/${project.name}/src/polyfills.ts",
+ }
+ ]
+ }),
+ withReact({
+ // Uncomment this line if you don't want to use SVGR
+ // See: https://react-svgr.com/
+ // svgr: false
+ }),
+ (config) => {
+ return config;
+ }
+ );
+ `
+ );
+
+ await convertConfigToWebpackPluginGenerator(tree, {
+ project: project.name,
+ });
+ expect(tree.read(`${project.root}/webpack.config.js`, 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
+ const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
+
+ // This file was migrated using @nx/webpack:convert-config-to-webpack-plugin
+
+ module.exports = {
+ plugins: [
+ new NxAppWebpackPlugin({
+ assets: ['./src/favicon.ico', './src/assets'],
+ styles: ['./src/styles.scss'],
+ scripts: ['./src/scripts.js'],
+ tsConfig: './tsconfig.app.json',
+ fileReplacements: [
+ {
+ replace: './src/environments/environment.ts',
+ with: './src/environments/environment.prod.ts',
+ },
+ ],
+ additionalEntryPoints: [
+ {
+ entryPath: './src/polyfills.ts',
+ },
+ ],
+ }),
+ new NxReactWebpackPlugin({
+ // Uncomment this line if you don't want to use SVGR
+ // See: https://react-svgr.com/
+ // svgr: false
+ }),
+ ],
+ };
+ "
+ `);
+ });
+});
diff --git a/packages/webpack/src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin.ts b/packages/webpack/src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin.ts
new file mode 100644
index 00000000000000..26083e35ef49de
--- /dev/null
+++ b/packages/webpack/src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin.ts
@@ -0,0 +1,127 @@
+import {
+ formatFiles,
+ getProjects,
+ stripIndents,
+ Tree,
+ joinPathFragments,
+} from '@nx/devkit';
+import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
+import { WebpackExecutorOptions } from '../../executors/webpack/schema';
+import { extractWebpackOptions } from './lib/extract-webpack-options';
+import { normalizePathOptions } from './lib/normalize-path-options';
+import { parse } from 'path';
+
+interface Schema {
+ project?: string;
+ skipFormat?: boolean;
+}
+
+const preprocessText = (text: string) => {
+ return text
+ .replace(/(\w+):/g, '"$1":') // Quote property names
+ .replace(/'/g, '"') // Convert single quotes to double quotes
+ .replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas
+ .replace(/(\r\n|\n|\r|\t)/gm, ''); // Remove newlines and tabs
+};
+
+export async function convertConfigToWebpackPluginGenerator(
+ tree: Tree,
+ options: Schema
+) {
+ let migrated = 0;
+
+ const projects = getProjects(tree);
+ forEachExecutorOptions(
+ tree,
+ '@nx/webpack:webpack',
+ (currentTargetOptions, projectName, targetName, configurationName) => {
+ if (options.project && projectName !== options.project) {
+ return;
+ }
+ if (!configurationName) {
+ const project = projects.get(projectName);
+ const containsMfeExecutor = Object.keys(project.targets).some(
+ (target) => {
+ return [
+ '@nx/react:module-federation-dev-server',
+ '@nx/angular:module-federation-dev-server',
+ ].includes(project.targets[target].executor);
+ }
+ );
+
+ if (containsMfeExecutor) {
+ throw new Error(
+ `The project ${projectName} is using Module Federation. At the moment, we don't support migrating projects that use Module Federation.`
+ );
+ }
+
+ const webpackConfigPath = currentTargetOptions?.webpackConfig || '';
+
+ if (webpackConfigPath && tree.exists(webpackConfigPath)) {
+ let { withNxConfig: webpackOptions, withReactConfig } =
+ extractWebpackOptions(tree, webpackConfigPath);
+
+ if (webpackOptions !== undefined) {
+ let parsedOptions = {};
+ if (webpackOptions) {
+ parsedOptions = JSON.parse(
+ preprocessText(webpackOptions.getText())
+ );
+ parsedOptions = normalizePathOptions(project.root, parsedOptions);
+ }
+
+ const { dir, name, ext } = parse(webpackConfigPath);
+
+ tree.rename(
+ webpackConfigPath,
+ `${joinPathFragments(dir, `${name}.old${ext}`)}`
+ );
+
+ tree.write(
+ webpackConfigPath,
+ stripIndents`
+ const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
+ const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
+
+ // This file was migrated using @nx/webpack:convert-config-to-webpack-plugin
+
+ module.exports = {
+ plugins: [
+ ${
+ webpackOptions
+ ? `new NxAppWebpackPlugin(${JSON.stringify(
+ parsedOptions,
+ null,
+ 2
+ )})`
+ : 'new NxAppWebpackPlugin()'
+ },
+ ${
+ withReactConfig
+ ? `new NxReactWebpackPlugin(${withReactConfig.getText()})`
+ : `new NxReactWebpackPlugin({
+ // Uncomment this line if you don't want to use SVGR
+ // See: https://react-svgr.com/
+ // svgr: false
+ }),`
+ },
+ ],
+ };
+ `
+ );
+ migrated++;
+ }
+ }
+ }
+ }
+ );
+ if (migrated === 0) {
+ throw new Error('Could not find any projects to migrate.');
+ }
+
+ if (!options.skipFormat) {
+ await formatFiles(tree);
+ }
+}
+
+export default convertConfigToWebpackPluginGenerator;
diff --git a/packages/webpack/src/generators/convert-config-to-webpack-plugin/lib/extract-webpack-options.ts b/packages/webpack/src/generators/convert-config-to-webpack-plugin/lib/extract-webpack-options.ts
new file mode 100644
index 00000000000000..1bd00d984ca8fe
--- /dev/null
+++ b/packages/webpack/src/generators/convert-config-to-webpack-plugin/lib/extract-webpack-options.ts
@@ -0,0 +1,35 @@
+import { Tree } from '@nx/devkit';
+import { tsquery } from '@phenomnomnominal/tsquery';
+import * as ts from 'typescript';
+export function extractWebpackOptions(tree: Tree, webpackConfigPath: string) {
+ const source = tree.read(webpackConfigPath).toString('utf-8');
+ const ast = tsquery.ast(source);
+
+ const withNxCall = tsquery(
+ ast,
+ 'CallExpression:has(Identifier[name="withNx"])'
+ );
+ const withReactCall = tsquery(
+ ast,
+ 'CallExpression:has(Identifier[name="withReact"])'
+ );
+
+ let withNxConfig: ts.Node | '' | undefined,
+ withReactConfig: ts.Node | '' | undefined;
+
+ withNxCall.forEach((node) => {
+ if (ts.isCallExpression(node)) {
+ const argument = node.arguments[0] || ''; // assuming the first argument is the config object
+ withNxConfig = argument;
+ }
+ });
+
+ withReactCall.forEach((node) => {
+ if (ts.isCallExpression(node)) {
+ const argument = node.arguments[0] || '';
+ withReactConfig = argument;
+ }
+ });
+
+ return { withNxConfig, withReactConfig };
+}
diff --git a/packages/webpack/src/generators/convert-config-to-webpack-plugin/lib/normalize-path-options.ts b/packages/webpack/src/generators/convert-config-to-webpack-plugin/lib/normalize-path-options.ts
new file mode 100644
index 00000000000000..4db8cff68d4db4
--- /dev/null
+++ b/packages/webpack/src/generators/convert-config-to-webpack-plugin/lib/normalize-path-options.ts
@@ -0,0 +1,92 @@
+import { WebpackExecutorOptions } from '@nx/webpack';
+import { toProjectRelativePath } from './utils';
+
+const executorFieldsToNormalize: Array = [
+ 'outputPath',
+ 'index',
+ 'main',
+ 'assets',
+ 'tsConfig',
+ 'styles',
+ 'babelConfig',
+ 'additionalEntryPoints',
+ 'scripts',
+ 'fileReplacements',
+ 'postcssConfig',
+ 'stylePreprocessorOptions',
+ 'publicPath',
+];
+
+export function normalizePathOptions(
+ projectRoot: string,
+ options: Partial
+) {
+ for (const [key, value] of Object.entries(options)) {
+ if (
+ !executorFieldsToNormalize.includes(key as keyof WebpackExecutorOptions)
+ ) {
+ continue;
+ }
+ options[key] = normalizePath(
+ projectRoot,
+ key as keyof WebpackExecutorOptions,
+ value
+ );
+ }
+ return options;
+}
+
+function normalizePath(
+ projectRoot: string,
+ key: K,
+ value: WebpackExecutorOptions[K]
+) {
+ if (!value) return value;
+
+ switch (key) {
+ case 'assets':
+ return value.map((asset) => {
+ if (typeof asset === 'string') {
+ return toProjectRelativePath(asset, projectRoot);
+ }
+ return {
+ ...asset,
+ input: toProjectRelativePath(asset.input, projectRoot),
+ output: toProjectRelativePath(asset.output, projectRoot),
+ };
+ });
+
+ case 'styles':
+ case 'scripts':
+ return value.map((item) => {
+ if (typeof item === 'string') {
+ return toProjectRelativePath(item, projectRoot);
+ }
+ return {
+ ...item,
+ input: toProjectRelativePath(item.input, projectRoot),
+ };
+ });
+
+ case 'additionalEntryPoints':
+ return value.map((entry) => {
+ return {
+ ...entry,
+ entryPath: toProjectRelativePath(entry.entryPath, projectRoot),
+ };
+ });
+
+ case 'fileReplacements':
+ return value.map((replacement) => {
+ return {
+ replace: toProjectRelativePath(replacement.replace, projectRoot),
+ with: toProjectRelativePath(replacement.with, projectRoot),
+ };
+ });
+
+ default:
+ return Array.isArray(value)
+ ? value.map((item) => toProjectRelativePath(item, projectRoot))
+ : toProjectRelativePath(value, projectRoot);
+ }
+}
diff --git a/packages/webpack/src/generators/convert-config-to-webpack-plugin/lib/utils.ts b/packages/webpack/src/generators/convert-config-to-webpack-plugin/lib/utils.ts
new file mode 100644
index 00000000000000..701daa4629f70d
--- /dev/null
+++ b/packages/webpack/src/generators/convert-config-to-webpack-plugin/lib/utils.ts
@@ -0,0 +1,19 @@
+import { relative, resolve } from 'path/posix';
+import { workspaceRoot } from '@nx/devkit';
+
+export function toProjectRelativePath(
+ path: string,
+ projectRoot: string
+): string {
+ if (projectRoot === '.') {
+ // workspace and project root are the same, we normalize it to ensure it
+ return path.startsWith('.') ? path : `./${path}`;
+ }
+
+ const relativePath = relative(
+ resolve(workspaceRoot, projectRoot),
+ resolve(workspaceRoot, path)
+ );
+
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
+}
diff --git a/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json b/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json
new file mode 100644
index 00000000000000..222e2683f51da0
--- /dev/null
+++ b/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://json-schema.org/schema",
+ "$id": "NxWebpackConvertConfigToWebpackPlugin",
+ "description": "Convert existing Webpack project(s) using `@nx/webpack:webpack` executor that uses `withNx` to use `NxAppWebpackPlugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
+ "title": "Convert Webpack project using withNx to NxAppWebpackPlugin",
+ "type": "object",
+ "properties": {
+ "project": {
+ "type": "string",
+ "description": "The project to convert from using the `@nx/webpack:webpack` executor and `withNx` plugin to use `NxAppWebpackPlugin`.",
+ "x-priority": "important"
+ },
+ "skipFormat": {
+ "type": "boolean",
+ "description": "Whether to format files at the end of the migration.",
+ "default": false
+ }
+ }
+}
diff --git a/packages/webpack/src/plugins/nx-webpack-plugin/lib/normalize-options.ts b/packages/webpack/src/plugins/nx-webpack-plugin/lib/normalize-options.ts
index 061ca0ed182a1f..8bf4f04ea7183e 100644
--- a/packages/webpack/src/plugins/nx-webpack-plugin/lib/normalize-options.ts
+++ b/packages/webpack/src/plugins/nx-webpack-plugin/lib/normalize-options.ts
@@ -76,7 +76,7 @@ export function normalizeOptions(
const sourceRoot = projectNode.data.sourceRoot ?? projectNode.data.root;
- if (!options.main) {
+ if (!combinedPluginAndMaybeExecutorOptions.main) {
throw new Error(
`Missing "main" option for the entry file. Set this option in your Nx webpack plugin.`
);