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.` );