From 40cf21b10c78b7ce1eab4e6c38261b85b8181bc6 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Fri, 13 Jun 2025 08:53:14 -0400 Subject: [PATCH] feat(react): support port option for react app generator (#31552) This PR adds the ability to set the port of the React application when using the generator. e.g. ```shell npx nx g @nx/react:app --port 8080 ``` This is useful when generating multiple apps and then running them in parallel. --- .../react/generators/application.json | 10 + .../vite/generators/configuration.json | 4 + e2e/react/src/react-rspack.test.ts | 20 ++ e2e/react/src/react-vite.test.ts | 23 ++ e2e/react/src/react-webpack.test.ts | 22 ++ .../application/application.spec.ts | 287 ++++++++++++++++++ .../base-rspack/rspack.config.js__tmpl__ | 2 +- .../base-webpack/webpack.config.js__tmpl__ | 2 +- .../src/generators/application/lib/add-e2e.ts | 10 +- .../application/lib/bundlers/add-vite.ts | 3 + .../lib/create-application-files.ts | 1 + .../application/lib/normalize-options.ts | 2 +- .../src/generators/application/schema.d.ts | 1 + .../src/generators/application/schema.json | 10 + .../src/utils/e2e-web-server-info-utils.ts | 2 +- packages/rspack/src/plugins/plugin.spec.ts | 6 +- packages/rspack/src/plugins/plugin.ts | 6 + .../src/utils/e2e-web-server-info-utils.ts | 2 +- .../generators/configuration/configuration.ts | 1 + .../src/generators/configuration/schema.d.ts | 2 + .../src/generators/configuration/schema.json | 4 + .../src/utils/e2e-web-server-info-utils.ts | 10 +- packages/vite/src/utils/generator-utils.ts | 6 +- .../plugins/__snapshots__/plugin.spec.ts.snap | 138 --------- packages/webpack/src/plugins/plugin.spec.ts | 141 ++++++++- packages/webpack/src/plugins/plugin.ts | 6 + .../src/utils/e2e-web-server-info-utils.ts | 2 +- 27 files changed, 568 insertions(+), 155 deletions(-) delete mode 100644 packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap diff --git a/docs/generated/packages/react/generators/application.json b/docs/generated/packages/react/generators/application.json index c96ff9c043..ff0b70938a 100644 --- a/docs/generated/packages/react/generators/application.json +++ b/docs/generated/packages/react/generators/application.json @@ -19,6 +19,10 @@ { "command": "nx g app apps/myapp --routing", "description": "Set up React Router" + }, + { + "command": "nx g app apps/myapp --port=3000", + "description": "Set up the dev server to use port 3000" } ], "type": "object", @@ -190,6 +194,12 @@ "useProjectJson": { "type": "boolean", "description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file." + }, + "port": { + "type": "number", + "description": "The port to use for the development server", + "x-prompt": "Which port would you like to use for the dev server?", + "default": 4200 } }, "required": ["directory"], diff --git a/docs/generated/packages/vite/generators/configuration.json b/docs/generated/packages/vite/generators/configuration.json index 121a388157..764bb1ca69 100644 --- a/docs/generated/packages/vite/generators/configuration.json +++ b/docs/generated/packages/vite/generators/configuration.json @@ -55,6 +55,10 @@ "type": "string", "enum": ["node", "jsdom", "happy-dom", "edge-runtime"], "default": "jsdom" + }, + "port": { + "type": "number", + "description": "The port to use for the development server" } }, "examplesFile": "---\ntitle: Examples for the Vite configuration generator\ndescription: This page contains examples for the Vite @nx/vite:configuration generator, which helps you set up Vite on your Nx workspace, or convert an existing project to use Vite.\n---\n\nThis generator is used for converting an existing React or Web project to use [Vite.js](https://vitejs.dev/).\n\nIt will create a `vite.config.ts` file at the root of your project with the correct settings, or if there's already a `vite.config.ts` file, it will modify it to include the correct settings.\n\n{% callout type=\"caution\" title=\"Your code will be modified!\" %}\nThis generator will modify your code, so make sure to commit your changes before running it.\n{% /callout %}\n\n```bash\nnx g @nx/vite:configuration\n```\n\nWhen running this generator, you will be prompted to provide the following:\n\n- The `project`, as the name of the project you want to generate the configuration for.\n- The `uiFramework` you want to use. Supported values are: `react` and `none`.\n\nYou must provide a `project` and a `uiFramework` for the generator to work.\n\nYou may also pass the `includeVitest` flag. This will also configure your project for testing with [Vitest](https://vitest.dev/), by adding the `test` configuration in your `vite.config.ts` file.\n\n## How to use\n\nIf you have an existing project that does not use Vite, you may want to convert it to use Vite. This can be a `webpack` project, a buildable JS library that uses the `@nx/js:babel`, the `@nx/js:swc` or the `@nx/rollup:rollup` executor, or even a non-buildable library.\nBy default, the `@nx/vite:configuration` generator will search your project to find the relevant configuration (either a `webpack.config.ts` file for example, or the `@nx/js` executors). If it determines that your project can be converted, then Nx will generate the configuration for you. If it cannot determine that your project can be converted, it will ask you if you want to convert it anyway or throw an error if it determines that it cannot be converted.\n\nYou can then test on your own if the result works or not, and modify the configuration as needed. It's suggested that you commit your changes before running the generator, so you can revert the changes if needed.\n\n## Projects that can be converted to use the `@nx/vite` executors\n\nUsually, React and Web projects generated with the `@nx/react` and the `@nx/web` generators can be converted to use the `@nx/vite` executors without any issues.\n\nThe list of executors for building, testing and serving that can be converted to use the `@nx/vite` executors is:\n\n### Supported `build` executors\n\n- `@nxext/vite:build`\n- `@nx/js:babel`\n- `@nx/js:swc`\n- `@nx/rollup:rollup`\n- `@nx/webpack:webpack`\n- `@nx/web:rollup`\n\n### Unsupported executors\n\n- `@nx/angular:ng-packagr-lite`\n- `@nx/angular:package`\n- `@nx/angular:webpack-browser`\n- `@angular-devkit/build-angular:browser`\n- `@angular-devkit/build-angular:dev-server`\n- `@nx/esbuild:esbuild`\n- `@nx/react-native:start`\n- `@nx/next:build`\n- `@nx/next:server`\n- `@nx/js:tsc`\n- any executor _not_ listed in the lists of \"supported executors\"\n- any project that does _not_ have a target for building, serving or testing\n\nWe **cannot** guarantee that projects using unsupported executors - _or any executor that is NOT listed in the list of \"supported executors\"_ - for either building, testing or serving will work correctly when converted to use Vite.\n\nYou can read more in the [Vite package overview page](/nx-api/vite).\n\n## Examples\n\n### Convert a React app to use Vite\n\n```bash\nnx g @nx/vite:configuration --project=my-react-app --uiFramework=react --includeVitest\n```\n\nThis will configure the `my-react-app` project to use Vite.\n\n### Convert a Web app to use Vite\n\n```bash\nnx g @nx/vite:configuration --project=my-web-app --uiFramework=none --includeVitest\n```\n\nThis will configure the `my-web-app` project to use Vite.\n", diff --git a/e2e/react/src/react-rspack.test.ts b/e2e/react/src/react-rspack.test.ts index 13a24f5df5..c3fa9c9ccf 100644 --- a/e2e/react/src/react-rspack.test.ts +++ b/e2e/react/src/react-rspack.test.ts @@ -22,6 +22,26 @@ describe('Build React applications and libraries with Rspack', () => { cleanupProject(); }); + it('should generate app with custom port', async () => { + const appName = uniq('app'); + const customPort = 8081; + + runCLI( + `generate @nx/react:app ${appName} --bundler=rspack --port=${customPort} --unit-test-runner=vitest --no-interactive --skipFormat --linter=eslint --e2eTestRunner=playwright` + ); + + const rspackConfig = readFile(`${appName}/rspack.config.js`); + expect(rspackConfig).toContain(`port: ${customPort}`); + + if (runE2ETests()) { + const e2eResults = runCLI(`e2e ${appName}-e2e`, { + verbose: true, + }); + expect(e2eResults).toContain('Successfully ran target e2e for project'); + expect(await killPorts()).toBeTruthy(); + } + }, 300_000); + it('should be able to use Rspack to build and test apps', async () => { const appName = uniq('app'); const libName = uniq('lib'); diff --git a/e2e/react/src/react-vite.test.ts b/e2e/react/src/react-vite.test.ts index fb71af58ad..15f5cbb5db 100644 --- a/e2e/react/src/react-vite.test.ts +++ b/e2e/react/src/react-vite.test.ts @@ -1,9 +1,12 @@ import { checkFilesExist, cleanupProject, + killPorts, newProject, + readFile, runCLI, runCLIAsync, + runE2ETests, uniq, } from '@nx/e2e/utils'; @@ -60,6 +63,26 @@ describe('Build React applications and libraries with Vite', () => { checkFilesExist(`dist/apps/${viteApp}/index.html`); }, 300_000); + it('should generate app with custom port', async () => { + const viteApp = uniq('viteapp'); + const customPort = 8081; + + runCLI( + `generate @nx/react:app apps/${viteApp} --bundler=vite --port=${customPort} --unitTestRunner=vitest --no-interactive --linter=eslint --e2eTestRunner=playwright` + ); + + const viteConfig = readFile(`apps/${viteApp}/vite.config.ts`); + expect(viteConfig).toContain(`port: ${customPort}`); + + if (runE2ETests()) { + const e2eResults = runCLI(`e2e ${viteApp}-e2e`, { + verbose: true, + }); + expect(e2eResults).toContain('Successfully ran target e2e for project'); + expect(await killPorts()).toBeTruthy(); + } + }, 300_000); + it('should test and lint app with bundler=vite and inSourceTests', async () => { const viteApp = uniq('viteapp'); const viteLib = uniq('vitelib'); diff --git a/e2e/react/src/react-webpack.test.ts b/e2e/react/src/react-webpack.test.ts index b3576f209d..84e1742d26 100644 --- a/e2e/react/src/react-webpack.test.ts +++ b/e2e/react/src/react-webpack.test.ts @@ -1,11 +1,13 @@ import { cleanupProject, createFile, + killPorts, listFiles, newProject, readFile, runCLI, runCLIAsync, + runE2ETests, uniq, updateFile, } from '@nx/e2e/utils'; @@ -21,6 +23,26 @@ describe('Build React applications and libraries with Webpack', () => { cleanupProject(); }); + it('should generate app with custom port', async () => { + const appName = uniq('app'); + const customPort = 8081; + + runCLI( + `generate @nx/react:app apps/${appName} --bundler=webpack --port=${customPort} --unitTestRunner=none --no-interactive --e2eTestRunner=playwright` + ); + + const webpackConfig = readFile(`apps/${appName}/webpack.config.js`); + expect(webpackConfig).toContain(`port: ${customPort}`); + + if (runE2ETests()) { + const e2eResults = runCLI(`e2e ${appName}-e2e`, { + verbose: true, + }); + expect(e2eResults).toContain('Successfully ran target e2e for project'); + expect(await killPorts()).toBeTruthy(); + } + }, 300_000); + // Regression test: https://github.com/nrwl/nx/issues/21773 it('should support SVGR and SVG asset in the same project', async () => { const appName = uniq('app'); diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index cbce937112..f934aa0012 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -1866,4 +1866,291 @@ describe('app', () => { ); }); }); + + describe('--port', () => { + it('should generate app with custom port for vite', async () => { + await applicationGenerator(appTree, { + ...schema, + directory: 'my-app', + bundler: 'vite', + port: 9000, + }); + + const viteConfig = appTree.read('my-app/vite.config.ts', 'utf-8'); + expect(viteConfig).toMatchInlineSnapshot(` + "/// + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + + export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../node_modules/.vite/my-app', + server:{ + port: 9000, + host: 'localhost', + }, + preview:{ + port: 9000, + host: 'localhost', + }, + plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + build: { + outDir: '../dist/my-app', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + })); + " + `); + }); + + it('should generate app with custom port for webpack', async () => { + await applicationGenerator(appTree, { + ...schema, + directory: 'my-app', + bundler: 'webpack', + port: 9000, + }); + + const webpackConfig = appTree.read('my-app/webpack.config.js', 'utf-8'); + expect(webpackConfig).toMatchInlineSnapshot(` + "const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); + const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin'); + const { join } = require('path'); + + module.exports = { + output: { + path: join(__dirname, '../dist/my-app'), + }, + devServer: { + port: 9000, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + plugins: [ + new NxAppWebpackPlugin({ + tsConfig: './tsconfig.app.json', + compiler: 'babel', + main: './src/main.tsx', + index: './src/index.html', + baseHref: '/', + assets: ["./src/favicon.ico","./src/assets"], + styles: ["./src/styles.css"], + outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none', + optimization: process.env['NODE_ENV'] === 'production', + }), + new NxReactWebpackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + ], + }; + " + `); + }); + + it('should generate app with custom port for rspack', async () => { + await applicationGenerator(appTree, { + ...schema, + directory: 'my-app', + bundler: 'rspack', + port: 9000, + }); + + const rspackConfig = appTree.read('my-app/rspack.config.js', 'utf-8'); + expect(rspackConfig).toMatchInlineSnapshot(` + "const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); + const { join } = require('path'); + + module.exports = { + output: { + path: join(__dirname, '../dist/my-app'), + }, + devServer: { + port: 9000, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + plugins: [ + new NxAppRspackPlugin({ + tsConfig: './tsconfig.app.json', + main: './src/main.tsx', + index: './src/index.html', + baseHref: '/', + assets: ["./src/favicon.ico","./src/assets"], + styles: ["./src/styles.css"], + outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none', + optimization: process.env['NODE_ENV'] === 'production', + }), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + ], + }; + " + `); + }); + + it('should use default port when not specified', async () => { + await applicationGenerator(appTree, { + ...schema, + directory: 'my-app', + bundler: 'vite', + }); + + const viteConfig = appTree.read('my-app/vite.config.ts', 'utf-8'); + expect(viteConfig).toMatchInlineSnapshot(` + "/// + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + + export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../node_modules/.vite/my-app', + server:{ + port: 4200, + host: 'localhost', + }, + preview:{ + port: 4300, + host: 'localhost', + }, + plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + build: { + outDir: '../dist/my-app', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + })); + " + `); + }); + + it('should generate vite app with cypress using custom port', async () => { + await applicationGenerator(appTree, { + ...schema, + directory: 'my-app', + bundler: 'vite', + e2eTestRunner: 'cypress', + port: 9000, + }); + + const cypressConfig = appTree.read( + 'my-app-e2e/cypress.config.ts', + 'utf-8' + ); + expect(cypressConfig).toContain("baseUrl: 'http://localhost:9000'"); + }); + + it('should generate vite app with playwright using custom port', async () => { + await applicationGenerator(appTree, { + ...schema, + directory: 'my-app', + bundler: 'vite', + e2eTestRunner: 'playwright', + port: 9000, + }); + + const playwrightConfig = appTree.read( + 'my-app-e2e/playwright.config.ts', + 'utf-8' + ); + expect(playwrightConfig).toContain("|| 'http://localhost:9000'"); + expect(playwrightConfig).toContain("url: 'http://localhost:9000'"); + }); + + it('should generate webpack app with cypress using custom port', async () => { + await applicationGenerator(appTree, { + ...schema, + directory: 'my-app', + bundler: 'webpack', + e2eTestRunner: 'cypress', + port: 9000, + }); + + const cypressConfig = appTree.read( + 'my-app-e2e/cypress.config.ts', + 'utf-8' + ); + expect(cypressConfig).toContain("baseUrl: 'http://localhost:9000'"); + }); + + it('should generate webpack app with playwright using custom port', async () => { + await applicationGenerator(appTree, { + ...schema, + directory: 'my-app', + bundler: 'webpack', + e2eTestRunner: 'playwright', + port: 9000, + }); + + const playwrightConfig = appTree.read( + 'my-app-e2e/playwright.config.ts', + 'utf-8' + ); + expect(playwrightConfig).toContain("|| 'http://localhost:9000'"); + expect(playwrightConfig).toContain("url: 'http://localhost:9000'"); + }); + + it('should generate rspack app with cypress using custom port', async () => { + await applicationGenerator(appTree, { + ...schema, + directory: 'my-app', + bundler: 'rspack', + e2eTestRunner: 'cypress', + port: 9000, + }); + + const cypressConfig = appTree.read( + 'my-app-e2e/cypress.config.ts', + 'utf-8' + ); + expect(cypressConfig).toContain("baseUrl: 'http://localhost:9000'"); + }); + + it('should generate rspack app with playwright using custom port', async () => { + await applicationGenerator(appTree, { + ...schema, + directory: 'my-app', + bundler: 'rspack', + e2eTestRunner: 'playwright', + port: 9000, + }); + + const playwrightConfig = appTree.read( + 'my-app-e2e/playwright.config.ts', + 'utf-8' + ); + expect(playwrightConfig).toContain("|| 'http://localhost:9000'"); + expect(playwrightConfig).toContain("url: 'http://localhost:9000'"); + }); + }); }); diff --git a/packages/react/src/generators/application/files/base-rspack/rspack.config.js__tmpl__ b/packages/react/src/generators/application/files/base-rspack/rspack.config.js__tmpl__ index b6fc7fad8b..d6b9bff0f5 100644 --- a/packages/react/src/generators/application/files/base-rspack/rspack.config.js__tmpl__ +++ b/packages/react/src/generators/application/files/base-rspack/rspack.config.js__tmpl__ @@ -8,7 +8,7 @@ module.exports = { path: join(__dirname, '<%= rspackPluginOptions.outputPath %>'), }, devServer: { - port: 4200, + port: <%= port %>, historyApiFallback: { index: '/index.html', disableDotRule: true, diff --git a/packages/react/src/generators/application/files/base-webpack/webpack.config.js__tmpl__ b/packages/react/src/generators/application/files/base-webpack/webpack.config.js__tmpl__ index 8da4f74349..1950345dd0 100644 --- a/packages/react/src/generators/application/files/base-webpack/webpack.config.js__tmpl__ +++ b/packages/react/src/generators/application/files/base-webpack/webpack.config.js__tmpl__ @@ -8,7 +8,7 @@ module.exports = { path: join(__dirname, '<%= webpackPluginOptions.outputPath %>'), }, devServer: { - port: 4200, + port: <%= port %>, historyApiFallback: { index: '/index.html', disableDotRule: true, diff --git a/packages/react/src/generators/application/lib/add-e2e.ts b/packages/react/src/generators/application/lib/add-e2e.ts index 2b7af6a9e8..63fd411cee 100644 --- a/packages/react/src/generators/application/lib/add-e2e.ts +++ b/packages/react/src/generators/application/lib/add-e2e.ts @@ -37,7 +37,7 @@ export async function addE2e( e2eCiWebServerCommand: `${getPackageManagerCommand().exec} nx run ${ options.projectName }:serve-static`, - e2eCiBaseUrl: `http://localhost:4200`, + e2eCiBaseUrl: `http://localhost:${options.port ?? 4300}`, e2eDevServerTarget: `${options.projectName}:serve`, }; @@ -81,7 +81,9 @@ export async function addE2e( `vite.config.${options.js ? 'js' : 'ts'}` ), options.addPlugin, - options.devServerPort ?? 4200 + options.devServerPort ?? 4200, + // If the user manually sets the port, then use it for dev and preview + options.port ) : await getViteE2EWebServerInfo( tree, @@ -91,7 +93,9 @@ export async function addE2e( `vite.config.${options.js ? 'js' : 'ts'}` ), options.addPlugin, - options.devServerPort ?? 4200 + options.devServerPort ?? 4200, + // If the user manually sets the port, then use it for dev and preview + options.port ); } else if (options.bundler === 'rsbuild') { ensurePackage('@nx/rsbuild', nxVersion); diff --git a/packages/react/src/generators/application/lib/bundlers/add-vite.ts b/packages/react/src/generators/application/lib/bundlers/add-vite.ts index eff8a0380e..17de4e46d8 100644 --- a/packages/react/src/generators/application/lib/bundlers/add-vite.ts +++ b/packages/react/src/generators/application/lib/bundlers/add-vite.ts @@ -42,6 +42,7 @@ export async function setupViteConfiguration( skipFormat: true, addPlugin: options.addPlugin, projectType: 'application', + port: options.port, }); tasks.push(viteTask); createOrEditViteConfig( @@ -52,6 +53,8 @@ export async function setupViteConfiguration( includeVitest: options.unitTestRunner === 'vitest', inSourceTests: options.inSourceTests, rollupOptionsExternal: ["'react'", "'react-dom'", "'react/jsx-runtime'"], + port: options.port, + previewPort: options.port, ...(options.useReactRouter ? reactRouterFrameworkConfig : baseReactConfig), diff --git a/packages/react/src/generators/application/lib/create-application-files.ts b/packages/react/src/generators/application/lib/create-application-files.ts index 6127bc28ce..b50a1b792c 100644 --- a/packages/react/src/generators/application/lib/create-application-files.ts +++ b/packages/react/src/generators/application/lib/create-application-files.ts @@ -57,6 +57,7 @@ export function getDefaultTemplateVariables( style: options.style === 'tailwind' ? 'css' : options.style, hasStyleFile, isUsingTsSolutionSetup: isUsingTsSolutionSetup(host), + port: options.port ?? 4200, }; } diff --git a/packages/react/src/generators/application/lib/normalize-options.ts b/packages/react/src/generators/application/lib/normalize-options.ts index e78161b1c6..cc965b6c53 100644 --- a/packages/react/src/generators/application/lib/normalize-options.ts +++ b/packages/react/src/generators/application/lib/normalize-options.ts @@ -87,7 +87,7 @@ export async function normalizeOptions( normalized.unitTestRunner = normalized.unitTestRunner ?? 'jest'; normalized.e2eTestRunner = normalized.e2eTestRunner ?? 'playwright'; normalized.inSourceTests = normalized.minimal || normalized.inSourceTests; - normalized.devServerPort ??= findFreePort(host); + normalized.devServerPort ??= options.port ?? findFreePort(host); normalized.minimal = normalized.minimal ?? false; return normalized; diff --git a/packages/react/src/generators/application/schema.d.ts b/packages/react/src/generators/application/schema.d.ts index be678c4405..112541454a 100644 --- a/packages/react/src/generators/application/schema.d.ts +++ b/packages/react/src/generators/application/schema.d.ts @@ -33,6 +33,7 @@ export interface Schema { useTsSolution?: boolean; formatter?: 'prettier' | 'none'; useProjectJson?: boolean; + port?: number; } export interface NormalizedSchema extends T { diff --git a/packages/react/src/generators/application/schema.json b/packages/react/src/generators/application/schema.json index 74ddd1db4e..bda8574b97 100644 --- a/packages/react/src/generators/application/schema.json +++ b/packages/react/src/generators/application/schema.json @@ -16,6 +16,10 @@ { "command": "nx g app apps/myapp --routing", "description": "Set up React Router" + }, + { + "command": "nx g app apps/myapp --port=3000", + "description": "Set up the dev server to use port 3000" } ], "type": "object", @@ -196,6 +200,12 @@ "useProjectJson": { "type": "boolean", "description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file." + }, + "port": { + "type": "number", + "description": "The port to use for the development server", + "x-prompt": "Which port would you like to use for the dev server?", + "default": 4200 } }, "required": ["directory"], diff --git a/packages/rsbuild/src/utils/e2e-web-server-info-utils.ts b/packages/rsbuild/src/utils/e2e-web-server-info-utils.ts index d3d4f7a7da..a69d847a51 100644 --- a/packages/rsbuild/src/utils/e2e-web-server-info-utils.ts +++ b/packages/rsbuild/src/utils/e2e-web-server-info-utils.ts @@ -31,7 +31,7 @@ export async function getRsbuildE2EWebServerInfo( defaultServeTargetName: 'dev', defaultServeStaticTargetName: 'preview', defaultE2EWebServerAddress: `http://localhost:${e2ePort}`, - defaultE2ECiBaseUrl: 'http://localhost:4200', + defaultE2ECiBaseUrl: `http://localhost:${e2ePort}`, defaultE2EPort: e2ePort, }, isPluginBeingAdded diff --git a/packages/rspack/src/plugins/plugin.spec.ts b/packages/rspack/src/plugins/plugin.spec.ts index e6efaea454..83ab03204e 100644 --- a/packages/rspack/src/plugins/plugin.spec.ts +++ b/packages/rspack/src/plugins/plugin.spec.ts @@ -33,7 +33,10 @@ describe('@nx/rspack', () => { 'my-app/project.json', JSON.stringify({ name: 'my-app' }) ); - tempFs.createFileSync('my-app/rspack.config.ts', `export default {};`); + tempFs.createFileSync( + 'my-app/rspack.config.ts', + `export default { devServer: { port: 9000 } };` + ); tempFs.createFileSync('package-lock.json', `{}`); }); @@ -132,6 +135,7 @@ describe('@nx/rspack', () => { "executor": "@nx/web:file-server", "options": { "buildTarget": "build", + "port": 9000, "spa": true, }, }, diff --git a/packages/rspack/src/plugins/plugin.ts b/packages/rspack/src/plugins/plugin.ts index 4ebe664499..a0c65063ef 100644 --- a/packages/rspack/src/plugins/plugin.ts +++ b/packages/rspack/src/plugins/plugin.ts @@ -247,6 +247,12 @@ async function createRspackTargets( }, }; + // for `convert-to-inferred` we need to leave the port undefined or the options will not match + if (rspackConfig.devServer?.port && rspackConfig.devServer?.port !== 4200) { + targets[options.serveStaticTargetName].options.port = + rspackConfig.devServer.port; + } + if (isTsSolutionSetup) { targets[options.buildTargetName].syncGenerators = [ '@nx/js:typescript-sync', diff --git a/packages/rspack/src/utils/e2e-web-server-info-utils.ts b/packages/rspack/src/utils/e2e-web-server-info-utils.ts index 717a57c2c4..de4406e00a 100644 --- a/packages/rspack/src/utils/e2e-web-server-info-utils.ts +++ b/packages/rspack/src/utils/e2e-web-server-info-utils.ts @@ -31,7 +31,7 @@ export async function getRspackE2EWebServerInfo( defaultServeTargetName: 'serve', defaultServeStaticTargetName: 'preview', defaultE2EWebServerAddress: `http://localhost:${e2ePort}`, - defaultE2ECiBaseUrl: 'http://localhost:4200', + defaultE2ECiBaseUrl: `http://localhost:${e2ePort}`, defaultE2EPort: e2ePort, }, isPluginBeingAdded diff --git a/packages/vite/src/generators/configuration/configuration.ts b/packages/vite/src/generators/configuration/configuration.ts index 06846ed3fc..c574c6028a 100644 --- a/packages/vite/src/generators/configuration/configuration.ts +++ b/packages/vite/src/generators/configuration/configuration.ts @@ -152,6 +152,7 @@ export async function viteConfigurationGeneratorInternal( : `import react from '@vitejs/plugin-react'`, ], plugins: ['react()'], + port: schema.port, }, false, undefined diff --git a/packages/vite/src/generators/configuration/schema.d.ts b/packages/vite/src/generators/configuration/schema.d.ts index 3b9d843556..e80b3f2d61 100644 --- a/packages/vite/src/generators/configuration/schema.d.ts +++ b/packages/vite/src/generators/configuration/schema.d.ts @@ -8,7 +8,9 @@ export interface ViteConfigurationGeneratorSchema { includeLib?: boolean; skipFormat?: boolean; testEnvironment?: 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' | string; + port?: number; // Internal options addPlugin?: boolean; projectType?: 'application' | 'library'; + previewPort?: number; } diff --git a/packages/vite/src/generators/configuration/schema.json b/packages/vite/src/generators/configuration/schema.json index f19908c15d..93f4207adb 100644 --- a/packages/vite/src/generators/configuration/schema.json +++ b/packages/vite/src/generators/configuration/schema.json @@ -55,6 +55,10 @@ "type": "string", "enum": ["node", "jsdom", "happy-dom", "edge-runtime"], "default": "jsdom" + }, + "port": { + "type": "number", + "description": "The port to use for the development server" } }, "examplesFile": "../../../docs/configuration-examples.md" diff --git a/packages/vite/src/utils/e2e-web-server-info-utils.ts b/packages/vite/src/utils/e2e-web-server-info-utils.ts index cdeb280764..906c594ac0 100644 --- a/packages/vite/src/utils/e2e-web-server-info-utils.ts +++ b/packages/vite/src/utils/e2e-web-server-info-utils.ts @@ -6,7 +6,8 @@ export async function getViteE2EWebServerInfo( projectName: string, configFilePath: string, isPluginBeingAdded: boolean, - e2ePortOverride?: number + e2ePortOverride?: number, + e2eCIPortOverride?: number ) { const nxJson = readNxJson(tree); let e2ePort = e2ePortOverride ?? 4200; @@ -35,7 +36,7 @@ export async function getViteE2EWebServerInfo( defaultServeTargetName: 'dev', defaultServeStaticTargetName: 'preview', defaultE2EWebServerAddress: `http://localhost:${e2ePort}`, - defaultE2ECiBaseUrl: 'http://localhost:4300', + defaultE2ECiBaseUrl: `http://localhost:${e2eCIPortOverride ?? 4300}`, defaultE2EPort: e2ePort, }, isPluginBeingAdded @@ -47,7 +48,8 @@ export async function getReactRouterE2EWebServerInfo( projectName: string, configFilePath: string, isPluginBeingAdded: boolean, - e2ePortOverride?: number + e2ePortOverride?: number, + e2eCIPortOverride?: number ) { const e2ePort = e2ePortOverride ?? parseInt(process.env.PORT) ?? 4200; @@ -64,7 +66,7 @@ export async function getReactRouterE2EWebServerInfo( defaultServeTargetName: 'dev', defaultServeStaticTargetName: 'dev', defaultE2EWebServerAddress: `http://localhost:${e2ePort}`, - defaultE2ECiBaseUrl: 'http://localhost:4200', + defaultE2ECiBaseUrl: `http://localhost:${e2eCIPortOverride ?? 4300}`, defaultE2EPort: e2ePort, }, isPluginBeingAdded diff --git a/packages/vite/src/utils/generator-utils.ts b/packages/vite/src/utils/generator-utils.ts index fc98f30c3e..a4b15374f1 100644 --- a/packages/vite/src/utils/generator-utils.ts +++ b/packages/vite/src/utils/generator-utils.ts @@ -378,6 +378,8 @@ export interface ViteConfigFileOptions { coverageProvider?: 'v8' | 'istanbul' | 'custom'; setupFile?: string; useEsmExtension?: boolean; + port?: number; + previewPort?: number; } export function createOrEditViteConfig( @@ -501,7 +503,7 @@ ${ : options.includeLib ? '' : ` server:{ - port: 4200, + port: ${options.port ?? 4200}, host: 'localhost', },`; @@ -510,7 +512,7 @@ ${ : options.includeLib ? '' : ` preview:{ - port: 4300, + port: ${options.previewPort ?? 4300}, host: 'localhost', },`; diff --git a/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap deleted file mode 100644 index 426dfe51e8..0000000000 --- a/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap +++ /dev/null @@ -1,138 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`@nx/webpack/plugin should create nodes 1`] = ` -[ - [ - "my-app/webpack.config.js", - { - "projects": { - "my-app": { - "metadata": {}, - "projectType": "application", - "targets": { - "build-deps": { - "dependsOn": [ - "^build", - ], - }, - "build-something": { - "cache": true, - "command": "webpack-cli build", - "dependsOn": [ - "^build-something", - ], - "inputs": [ - "production", - "^production", - { - "externalDependencies": [ - "webpack-cli", - ], - }, - ], - "metadata": { - "description": "Runs Webpack build", - "help": { - "command": "npx webpack-cli build --help", - "example": { - "args": [ - "--profile", - ], - "options": { - "json": "stats.json", - }, - }, - }, - "technologies": [ - "webpack", - ], - }, - "options": { - "args": [ - "--node-env=production", - ], - "cwd": "my-app", - }, - "outputs": [ - "{projectRoot}/dist/foo", - ], - }, - "my-serve": { - "command": "webpack-cli serve", - "continuous": true, - "metadata": { - "description": "Starts Webpack dev server", - "help": { - "command": "npx webpack-cli serve --help", - "example": { - "options": { - "args": [ - "--client-progress", - "--history-api-fallback ", - ], - }, - }, - }, - "technologies": [ - "webpack", - ], - }, - "options": { - "args": [ - "--node-env=development", - ], - "cwd": "my-app", - }, - }, - "preview-site": { - "command": "webpack-cli serve", - "continuous": true, - "metadata": { - "description": "Starts Webpack dev server in production mode", - "help": { - "command": "npx webpack-cli serve --help", - "example": { - "options": { - "args": [ - "--client-progress", - "--history-api-fallback ", - ], - }, - }, - }, - "technologies": [ - "webpack", - ], - }, - "options": { - "args": [ - "--node-env=production", - ], - "cwd": "my-app", - }, - }, - "serve-static": { - "continuous": true, - "dependsOn": [ - "build-something", - ], - "executor": "@nx/web:file-server", - "options": { - "buildTarget": "build-something", - "spa": true, - }, - }, - "watch-deps": { - "command": "npx nx watch --projects my-app --includeDependentProjects -- npx nx build-deps my-app", - "continuous": true, - "dependsOn": [ - "build-deps", - ], - }, - }, - }, - }, - }, - ], -] -`; diff --git a/packages/webpack/src/plugins/plugin.spec.ts b/packages/webpack/src/plugins/plugin.spec.ts index 9d944036be..f4fac89941 100644 --- a/packages/webpack/src/plugins/plugin.spec.ts +++ b/packages/webpack/src/plugins/plugin.spec.ts @@ -38,6 +38,9 @@ describe('@nx/webpack/plugin', () => { output: { path: 'dist/foo', }, + devServer: { + port: 9000, + }, }); const nodes = await createNodesFunction( ['my-app/webpack.config.js'], @@ -50,7 +53,143 @@ describe('@nx/webpack/plugin', () => { context ); - expect(nodes).toMatchSnapshot(); + expect(nodes).toMatchInlineSnapshot(` + [ + [ + "my-app/webpack.config.js", + { + "projects": { + "my-app": { + "metadata": {}, + "projectType": "application", + "targets": { + "build-deps": { + "dependsOn": [ + "^build", + ], + }, + "build-something": { + "cache": true, + "command": "webpack-cli build", + "dependsOn": [ + "^build-something", + ], + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "webpack-cli", + ], + }, + ], + "metadata": { + "description": "Runs Webpack build", + "help": { + "command": "npx webpack-cli build --help", + "example": { + "args": [ + "--profile", + ], + "options": { + "json": "stats.json", + }, + }, + }, + "technologies": [ + "webpack", + ], + }, + "options": { + "args": [ + "--node-env=production", + ], + "cwd": "my-app", + }, + "outputs": [ + "{projectRoot}/dist/foo", + ], + }, + "my-serve": { + "command": "webpack-cli serve", + "continuous": true, + "metadata": { + "description": "Starts Webpack dev server", + "help": { + "command": "npx webpack-cli serve --help", + "example": { + "options": { + "args": [ + "--client-progress", + "--history-api-fallback ", + ], + }, + }, + }, + "technologies": [ + "webpack", + ], + }, + "options": { + "args": [ + "--node-env=development", + ], + "cwd": "my-app", + }, + }, + "preview-site": { + "command": "webpack-cli serve", + "continuous": true, + "metadata": { + "description": "Starts Webpack dev server in production mode", + "help": { + "command": "npx webpack-cli serve --help", + "example": { + "options": { + "args": [ + "--client-progress", + "--history-api-fallback ", + ], + }, + }, + }, + "technologies": [ + "webpack", + ], + }, + "options": { + "args": [ + "--node-env=production", + ], + "cwd": "my-app", + }, + }, + "serve-static": { + "continuous": true, + "dependsOn": [ + "build-something", + ], + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "build-something", + "port": 9000, + "spa": true, + }, + }, + "watch-deps": { + "command": "npx nx watch --projects my-app --includeDependentProjects -- npx nx build-deps my-app", + "continuous": true, + "dependsOn": [ + "build-deps", + ], + }, + }, + }, + }, + }, + ], + ] + `); }); function mockWebpackConfig(config: any) { diff --git a/packages/webpack/src/plugins/plugin.ts b/packages/webpack/src/plugins/plugin.ts index 32b21feb97..af3d48fb26 100644 --- a/packages/webpack/src/plugins/plugin.ts +++ b/packages/webpack/src/plugins/plugin.ts @@ -268,6 +268,12 @@ async function createWebpackTargets( }, }; + // for `convert-to-inferred` we need to leave the port undefined or the options will not match + if (webpackConfig.devServer?.port && webpackConfig.devServer?.port !== 4200) { + targets[options.serveStaticTargetName].options.port = + webpackConfig.devServer.port; + } + if (isTsSolutionSetup) { targets[options.buildTargetName].syncGenerators = [ '@nx/js:typescript-sync', diff --git a/packages/webpack/src/utils/e2e-web-server-info-utils.ts b/packages/webpack/src/utils/e2e-web-server-info-utils.ts index 03599526df..bd14479b88 100644 --- a/packages/webpack/src/utils/e2e-web-server-info-utils.ts +++ b/packages/webpack/src/utils/e2e-web-server-info-utils.ts @@ -31,7 +31,7 @@ export async function getWebpackE2EWebServerInfo( defaultServeTargetName: 'serve', defaultServeStaticTargetName: 'serve-static', defaultE2EWebServerAddress: `http://localhost:${e2ePort}`, - defaultE2ECiBaseUrl: 'http://localhost:4200', + defaultE2ECiBaseUrl: `http://localhost:${e2ePort}`, defaultE2EPort: e2ePort, }, isPluginBeingAdded