Jack Hsu 40cf21b10c
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.
2025-06-13 08:53:14 -04:00

241 lines
7.6 KiB
TypeScript

import {
addProjectConfiguration,
ensurePackage,
GeneratorCallback,
getPackageManagerCommand,
joinPathFragments,
Tree,
writeJson,
} from '@nx/devkit';
import { webStaticServeGenerator } from '@nx/web';
import { nxVersion } from '../../../utils/versions';
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
import { hasVitePlugin } from '../../../utils/has-vite-plugin';
import { hasRspackPlugin } from '../../../utils/has-rspack-plugin';
import { hasRsbuildPlugin } from '../../../utils/has-rsbuild-plugin';
import { NormalizedSchema } from '../schema';
import { E2EWebServerDetails } from '@nx/devkit/src/generators/e2e-web-server-info-utils';
import type { PackageJson } from 'nx/src/utils/package-json';
export async function addE2e(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
const hasNxBuildPlugin =
(options.bundler === 'webpack' && hasWebpackPlugin(tree)) ||
(options.bundler === 'rspack' && hasRspackPlugin(tree)) ||
(options.bundler === 'rsbuild' &&
(await hasRsbuildPlugin(tree, options.appProjectRoot))) ||
(options.bundler === 'vite' && hasVitePlugin(tree));
let e2eWebServerInfo: E2EWebServerDetails = {
e2eWebServerAddress: `http://localhost:${options.devServerPort ?? 4200}`,
e2eWebServerCommand: `${getPackageManagerCommand().exec} nx run ${
options.projectName
}:serve`,
e2eCiWebServerCommand: `${getPackageManagerCommand().exec} nx run ${
options.projectName
}:serve-static`,
e2eCiBaseUrl: `http://localhost:${options.port ?? 4300}`,
e2eDevServerTarget: `${options.projectName}:serve`,
};
if (options.bundler === 'webpack') {
const { getWebpackE2EWebServerInfo } = ensurePackage<
typeof import('@nx/webpack')
>('@nx/webpack', nxVersion);
e2eWebServerInfo = await getWebpackE2EWebServerInfo(
tree,
options.projectName,
joinPathFragments(
options.appProjectRoot,
`webpack.config.${options.js ? 'js' : 'ts'}`
),
options.addPlugin,
options.devServerPort ?? 4200
);
} else if (options.bundler === 'rspack') {
const { getRspackE2EWebServerInfo } = ensurePackage<
typeof import('@nx/rspack')
>('@nx/rspack', nxVersion);
e2eWebServerInfo = await getRspackE2EWebServerInfo(
tree,
options.projectName,
joinPathFragments(
options.appProjectRoot,
`rspack.config.${options.js ? 'js' : 'ts'}`
),
options.addPlugin,
options.devServerPort ?? 4200
);
} else if (options.bundler === 'vite') {
const { getViteE2EWebServerInfo, getReactRouterE2EWebServerInfo } =
ensurePackage<typeof import('@nx/vite')>('@nx/vite', nxVersion);
e2eWebServerInfo = options.useReactRouter
? await getReactRouterE2EWebServerInfo(
tree,
options.projectName,
joinPathFragments(
options.appProjectRoot,
`vite.config.${options.js ? 'js' : 'ts'}`
),
options.addPlugin,
options.devServerPort ?? 4200,
// If the user manually sets the port, then use it for dev and preview
options.port
)
: await getViteE2EWebServerInfo(
tree,
options.projectName,
joinPathFragments(
options.appProjectRoot,
`vite.config.${options.js ? 'js' : 'ts'}`
),
options.addPlugin,
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);
const { getRsbuildE2EWebServerInfo } = await import(
'@nx/rsbuild/config-utils'
);
e2eWebServerInfo = await getRsbuildE2EWebServerInfo(
tree,
options.projectName,
joinPathFragments(
options.appProjectRoot,
`rsbuild.config.${options.js ? 'js' : 'ts'}`
),
options.addPlugin,
options.devServerPort ?? 4200
);
}
if (!hasNxBuildPlugin) {
await webStaticServeGenerator(tree, {
buildTarget: `${options.projectName}:build`,
targetName: 'serve-static',
spa: true,
});
}
switch (options.e2eTestRunner) {
case 'cypress': {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/cypress')
>('@nx/cypress', nxVersion);
const packageJson: PackageJson = {
name: options.e2eProjectName,
version: '0.0.1',
private: true,
};
if (!options.useProjectJson) {
packageJson.nx = {
implicitDependencies: [options.projectName],
};
} else {
addProjectConfiguration(tree, options.e2eProjectName, {
projectType: 'application',
root: options.e2eProjectRoot,
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
targets: {},
implicitDependencies: [options.projectName],
tags: [],
});
}
if (!options.useProjectJson || options.isUsingTsSolutionConfig) {
writeJson(
tree,
joinPathFragments(options.e2eProjectRoot, 'package.json'),
packageJson
);
}
const e2eTask = await configurationGenerator(tree, {
...options,
project: options.e2eProjectName,
directory: 'src',
// the name and root are already normalized, instruct the generator to use them as is
bundler:
options.bundler === 'rspack'
? 'webpack'
: options.bundler === 'rsbuild'
? 'none'
: options.bundler,
skipFormat: true,
devServerTarget: e2eWebServerInfo.e2eDevServerTarget,
baseUrl: e2eWebServerInfo.e2eWebServerAddress,
jsx: true,
rootProject: options.rootProject,
webServerCommands: {
default: e2eWebServerInfo.e2eWebServerCommand,
production: e2eWebServerInfo.e2eCiWebServerCommand,
},
ciWebServerCommand: e2eWebServerInfo.e2eCiWebServerCommand,
ciBaseUrl: e2eWebServerInfo.e2eCiBaseUrl,
});
return e2eTask;
}
case 'playwright': {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright')
>('@nx/playwright', nxVersion);
const packageJson: PackageJson = {
name: options.e2eProjectName,
version: '0.0.1',
private: true,
};
if (!options.useProjectJson) {
packageJson.nx = {
implicitDependencies: [options.projectName],
};
} else {
addProjectConfiguration(tree, options.e2eProjectName, {
projectType: 'application',
root: options.e2eProjectRoot,
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
targets: {},
implicitDependencies: [options.projectName],
tags: [],
});
}
if (!options.useProjectJson || options.isUsingTsSolutionConfig) {
writeJson(
tree,
joinPathFragments(options.e2eProjectRoot, 'package.json'),
packageJson
);
}
const e2eTask = await configurationGenerator(tree, {
project: options.e2eProjectName,
skipFormat: true,
skipPackageJson: options.skipPackageJson,
directory: 'src',
js: false,
linter: options.linter,
setParserOptionsProject: options.setParserOptionsProject,
webServerCommand: e2eWebServerInfo.e2eCiWebServerCommand,
webServerAddress: e2eWebServerInfo.e2eCiBaseUrl,
rootProject: options.rootProject,
addPlugin: options.addPlugin,
});
return e2eTask;
}
case 'none':
default:
return () => {};
}
}