From 8daad98992421ffde84473b4b47ee61c1d0eb05f Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 10 Jun 2025 16:57:46 +0100 Subject: [PATCH] chore(node): refactor application generator for more clarity (#31523) ## Current Behavior The Node.js application generator (`packages/node/src/generators/application/application.ts`) is implemented as a single large file containing ~469 lines of code. All generator logic is mixed together in one file including: - Option normalization and validation - Project configuration creation - File generation - Build/serve target setup - Dependency management - ESLint configuration - Proxy setup This makes the code harder to maintain, test, and understand as the file handles multiple responsibilities. ## Expected Behavior The generator is now refactored into smaller, focused modules organized in a `lib/` directory: - `normalize-options.ts` - handles option normalization and validation - `normalized-schema.ts` - defines the normalized schema interface - `create-project.ts` - handles project configuration creation (exported as `addProject`) - `create-files.ts` - handles file generation (exported as `addAppFiles`) - `create-targets.ts` - handles build/serve target configuration - `add-dependencies.ts` - handles dependency management (exported as `addProjectDependencies`) - `add-linting.ts` - handles ESLint setup (exported as `addLintingToApplication`) - `add-proxy.ts` - handles proxy configuration (exported as `addProxy`) - `index.ts` - exports all the functions The main `application.ts` file is now much cleaner at ~15 lines, focusing on orchestrating the generator workflow by calling the extracted functions. This separation of concerns improves: - **Maintainability**: Each file has a single responsibility - **Testability**: Individual functions can be tested in isolation - **Readability**: Easier to understand what each part does - **Reusability**: Functions can be potentially reused by other generators --- .../src/generators/application/application.ts | 484 +----------------- .../application/lib/add-dependencies.ts | 71 +++ .../generators/application/lib/add-linting.ts | 23 + .../generators/application/lib/add-proxy.ts | 61 +++ .../application/lib/create-files.ts | 71 +++ .../application/lib/create-project.ts | 71 +++ .../application/lib/create-targets.ts | 122 +++++ .../src/generators/application/lib/index.ts | 8 + .../application/lib/normalize-options.ts | 70 +++ .../application/lib/normalized-schema.ts | 9 + 10 files changed, 521 insertions(+), 469 deletions(-) create mode 100644 packages/node/src/generators/application/lib/add-dependencies.ts create mode 100644 packages/node/src/generators/application/lib/add-linting.ts create mode 100644 packages/node/src/generators/application/lib/add-proxy.ts create mode 100644 packages/node/src/generators/application/lib/create-files.ts create mode 100644 packages/node/src/generators/application/lib/create-project.ts create mode 100644 packages/node/src/generators/application/lib/create-targets.ts create mode 100644 packages/node/src/generators/application/lib/index.ts create mode 100644 packages/node/src/generators/application/lib/normalize-options.ts create mode 100644 packages/node/src/generators/application/lib/normalized-schema.ts diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index 04408251c2..9c04c25617 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -1,429 +1,37 @@ import { - addDependenciesToPackageJson, - addProjectConfiguration, ensurePackage, formatFiles, - generateFiles, GeneratorCallback, joinPathFragments, - logger, - names, - offsetFromRoot, - ProjectConfiguration, - readNxJson, readProjectConfiguration, runTasksInSerial, - TargetConfiguration, - toJS, Tree, updateJson, updateProjectConfiguration, updateTsConfigsToJs, - writeJson, } from '@nx/devkit'; -import { - determineProjectNameAndRootOptions, - ensureRootProjectName, -} from '@nx/devkit/src/generators/project-name-and-root-utils'; import { configurationGenerator } from '@nx/jest'; +import { initGenerator as jsInitGenerator, tsConfigBaseOptions } from '@nx/js'; import { - getRelativePathToRootTsConfig, - initGenerator as jsInitGenerator, - tsConfigBaseOptions, -} from '@nx/js'; -import { esbuildVersion } from '@nx/js/src/utils/versions'; -import { lintProjectGenerator } from '@nx/eslint'; -import { join } from 'path'; -import { - expressTypingsVersion, - expressVersion, - fastifyAutoloadVersion, - fastifyPluginVersion, - fastifySensibleVersion, - fastifyVersion, - koaTypingsVersion, - koaVersion, - nxVersion, - tslibVersion, - typesNodeVersion, -} from '../../utils/versions'; + addProjectToTsSolutionWorkspace, + updateTsconfigFiles, +} from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields'; +import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; +import { nxVersion } from '../../utils/versions'; import { e2eProjectGenerator } from '../e2e-project/e2e-project'; import { initGenerator } from '../init/init'; import { setupDockerGenerator } from '../setup-docker/setup-docker'; import { Schema } from './schema'; -import { hasWebpackPlugin } from '../../utils/has-webpack-plugin'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; -import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { - addProjectToTsSolutionWorkspace, - isUsingTsSolutionSetup, - updateTsconfigFiles, -} from '@nx/js/src/utils/typescript/ts-solution-setup'; -import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields'; -import type { PackageJson } from 'nx/src/utils/package-json'; - -export interface NormalizedSchema extends Omit { - appProjectRoot: string; - parsedTags: string[]; - outputPath: string; - importPath: string; - isUsingTsSolutionConfig: boolean; -} - -function getWebpackBuildConfig( - project: ProjectConfiguration, - options: NormalizedSchema -): TargetConfiguration { - return { - executor: `@nx/webpack:webpack`, - outputs: ['{options.outputPath}'], - defaultConfiguration: 'production', - options: { - target: 'node', - compiler: 'tsc', - outputPath: options.outputPath, - main: joinPathFragments( - project.sourceRoot, - 'main' + (options.js ? '.js' : '.ts') - ), - tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'), - assets: [joinPathFragments(project.sourceRoot, 'assets')], - webpackConfig: joinPathFragments( - options.appProjectRoot, - 'webpack.config.js' - ), - generatePackageJson: options.isUsingTsSolutionConfig ? undefined : true, - }, - configurations: { - development: { - outputHashing: 'none', - }, - production: { - ...(options.docker && { generateLockfile: true }), - }, - }, - }; -} - -function getEsBuildConfig( - project: ProjectConfiguration, - options: NormalizedSchema -): TargetConfiguration { - return { - executor: '@nx/esbuild:esbuild', - outputs: ['{options.outputPath}'], - defaultConfiguration: 'production', - options: { - platform: 'node', - outputPath: options.outputPath, - // Use CJS for Node apps for widest compatibility. - format: ['cjs'], - bundle: false, - main: joinPathFragments( - project.sourceRoot, - 'main' + (options.js ? '.js' : '.ts') - ), - tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'), - assets: [joinPathFragments(project.sourceRoot, 'assets')], - generatePackageJson: options.isUsingTsSolutionConfig ? undefined : true, - esbuildOptions: { - sourcemap: true, - // Generate CJS files as .js so imports can be './foo' rather than './foo.cjs'. - outExtension: { '.js': '.js' }, - }, - }, - configurations: { - development: {}, - production: { - ...(options.docker && { generateLockfile: true }), - esbuildOptions: { - sourcemap: false, - // Generate CJS files as .js so imports can be './foo' rather than './foo.cjs'. - outExtension: { '.js': '.js' }, - }, - }, - }, - }; -} - -function getServeConfig(options: NormalizedSchema): TargetConfiguration { - return { - continuous: true, - executor: '@nx/js:node', - defaultConfiguration: 'development', - // Run build, which includes dependency on "^build" by default, so the first run - // won't error out due to missing build artifacts. - dependsOn: ['build'], - options: { - buildTarget: `${options.name}:build`, - // Even though `false` is the default, set this option so users know it - // exists if they want to always run dependencies during each rebuild. - runBuildTargetDependencies: false, - }, - configurations: { - development: { - buildTarget: `${options.name}:build:development`, - }, - production: { - buildTarget: `${options.name}:build:production`, - }, - }, - }; -} - -function getNestWebpackBuildConfig(): TargetConfiguration { - return { - executor: 'nx:run-commands', - options: { - command: 'webpack-cli build', - args: ['node-env=production'], - }, - configurations: { - development: { - args: ['node-env=development'], - }, - }, - }; -} - -function addProject(tree: Tree, options: NormalizedSchema) { - const project: ProjectConfiguration = { - root: options.appProjectRoot, - sourceRoot: joinPathFragments(options.appProjectRoot, 'src'), - projectType: 'application', - targets: {}, - tags: options.parsedTags, - }; - - if (options.bundler === 'esbuild') { - addBuildTargetDefaults(tree, '@nx/esbuild:esbuild'); - project.targets.build = getEsBuildConfig(project, options); - } else if (options.bundler === 'webpack') { - if (!hasWebpackPlugin(tree) && options.addPlugin === false) { - addBuildTargetDefaults(tree, `@nx/webpack:webpack`); - project.targets.build = getWebpackBuildConfig(project, options); - } else if (options.isNest) { - // If we are using Nest that has the webpack plugin we need to override the - // build target so that node-env can be set to production or development so the serve target can be run in development mode - project.targets.build = getNestWebpackBuildConfig(); - } - } - project.targets.serve = getServeConfig(options); - - const packageJson: PackageJson = { - name: options.importPath, - version: '0.0.1', - private: true, - }; - - if (!options.useProjectJson) { - packageJson.nx = { - name: options.name !== options.importPath ? options.name : undefined, - targets: project.targets, - tags: project.tags?.length ? project.tags : undefined, - }; - } else { - addProjectConfiguration( - tree, - options.name, - project, - options.standaloneConfig - ); - } - - if (!options.useProjectJson || options.isUsingTsSolutionConfig) { - writeJson( - tree, - joinPathFragments(options.appProjectRoot, 'package.json'), - packageJson - ); - } -} - -function addAppFiles(tree: Tree, options: NormalizedSchema) { - generateFiles( - tree, - join(__dirname, './files/common'), - options.appProjectRoot, - { - ...options, - tmpl: '', - name: options.name, - root: options.appProjectRoot, - offset: offsetFromRoot(options.appProjectRoot), - rootTsConfigPath: getRelativePathToRootTsConfig( - tree, - options.appProjectRoot - ), - webpackPluginOptions: - hasWebpackPlugin(tree) && options.addPlugin !== false - ? { - outputPath: options.isUsingTsSolutionConfig - ? 'dist' - : joinPathFragments( - offsetFromRoot(options.appProjectRoot), - 'dist', - options.rootProject ? options.name : options.appProjectRoot - ), - main: './src/main' + (options.js ? '.js' : '.ts'), - tsConfig: './tsconfig.app.json', - assets: ['./src/assets'], - } - : null, - } - ); - - if (options.bundler !== 'webpack') { - tree.delete(joinPathFragments(options.appProjectRoot, 'webpack.config.js')); - } - - if (options.framework && options.framework !== 'none') { - generateFiles( - tree, - join(__dirname, `./files/${options.framework}`), - options.appProjectRoot, - { - ...options, - tmpl: '', - name: options.name, - root: options.appProjectRoot, - offset: offsetFromRoot(options.appProjectRoot), - rootTsConfigPath: getRelativePathToRootTsConfig( - tree, - options.appProjectRoot - ), - } - ); - } - if (options.js) { - toJS(tree); - } -} - -function addProxy(tree: Tree, options: NormalizedSchema) { - const projectConfig = readProjectConfiguration(tree, options.frontendProject); - if ( - projectConfig.targets && - ['serve', 'dev'].find((t) => !!projectConfig.targets[t]) - ) { - const targetName = ['serve', 'dev'].find((t) => !!projectConfig.targets[t]); - projectConfig.targets[targetName].dependsOn = [ - ...(projectConfig.targets[targetName].dependsOn ?? []), - `${options.name}:serve`, - ]; - const pathToProxyFile = `${projectConfig.root}/proxy.conf.json`; - projectConfig.targets[targetName].options = { - ...projectConfig.targets[targetName].options, - proxyConfig: pathToProxyFile, - }; - - if (!tree.exists(pathToProxyFile)) { - tree.write( - pathToProxyFile, - JSON.stringify( - { - '/api': { - target: `http://localhost:${options.port}`, - secure: false, - }, - }, - null, - 2 - ) - ); - } else { - //add new entry to existing config - const proxyFileContent = tree.read(pathToProxyFile).toString(); - - const proxyModified = { - ...JSON.parse(proxyFileContent), - [`/${options.name}-api`]: { - target: `http://localhost:${options.port}`, - secure: false, - }, - }; - - tree.write(pathToProxyFile, JSON.stringify(proxyModified, null, 2)); - } - - updateProjectConfiguration(tree, options.frontendProject, projectConfig); - } else { - logger.warn( - `Skip updating proxy for frontend project "${options.frontendProject}" since "serve" target is not found in project.json. For more information, see: https://nx.dev/recipes/node/application-proxies.` - ); - } -} - -export async function addLintingToApplication( - tree: Tree, - options: NormalizedSchema -): Promise { - const lintTask = await lintProjectGenerator(tree, { - linter: options.linter, - project: options.name, - tsConfigPaths: [ - joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'), - ], - unitTestRunner: options.unitTestRunner, - skipFormat: true, - setParserOptionsProject: options.setParserOptionsProject, - rootProject: options.rootProject, - addPlugin: options.addPlugin, - }); - - return lintTask; -} - -function addProjectDependencies( - tree: Tree, - options: NormalizedSchema -): GeneratorCallback { - const bundlers = { - webpack: { - '@nx/webpack': nxVersion, - }, - esbuild: { - '@nx/esbuild': nxVersion, - esbuild: esbuildVersion, - }, - }; - - const frameworkDependencies = { - express: { - express: expressVersion, - }, - koa: { - koa: koaVersion, - }, - fastify: { - fastify: fastifyVersion, - 'fastify-plugin': fastifyPluginVersion, - '@fastify/autoload': fastifyAutoloadVersion, - '@fastify/sensible': fastifySensibleVersion, - }, - }; - const frameworkDevDependencies = { - express: { - '@types/express': expressTypingsVersion, - }, - koa: { - '@types/koa': koaTypingsVersion, - }, - fastify: {}, - }; - return addDependenciesToPackageJson( - tree, - { - ...frameworkDependencies[options.framework], - tslib: tslibVersion, - }, - { - ...frameworkDevDependencies[options.framework], - ...bundlers[options.bundler], - '@types/node': typesNodeVersion, - } - ); -} + addAppFiles, + addLintingToApplication, + addProject, + addProjectDependencies, + addProxy, + normalizeOptions, + NormalizedSchema, +} from './lib'; function updateTsConfigOptions(tree: Tree, options: NormalizedSchema) { if (options.isUsingTsSolutionConfig) { @@ -635,66 +243,4 @@ export async function applicationGeneratorInternal(tree: Tree, schema: Schema) { return runTasksInSerial(...tasks); } -async function normalizeOptions( - host: Tree, - options: Schema -): Promise { - await ensureRootProjectName(options, 'application'); - const { - projectName, - projectRoot: appProjectRoot, - importPath, - } = await determineProjectNameAndRootOptions(host, { - name: options.name, - projectType: 'application', - directory: options.directory, - rootProject: options.rootProject, - }); - options.rootProject = appProjectRoot === '.'; - - options.bundler = options.bundler ?? 'esbuild'; - options.e2eTestRunner = options.e2eTestRunner ?? 'jest'; - - const parsedTags = options.tags - ? options.tags.split(',').map((s) => s.trim()) - : []; - - const nxJson = readNxJson(host); - const addPlugin = - process.env.NX_ADD_PLUGINS !== 'false' && - nxJson.useInferencePlugins !== false; - - const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host); - const swcJest = options.swcJest ?? isUsingTsSolutionConfig; - - const appProjectName = - !isUsingTsSolutionConfig || options.name ? projectName : importPath; - const useProjectJson = options.useProjectJson ?? !isUsingTsSolutionConfig; - - return { - addPlugin, - ...options, - name: appProjectName, - frontendProject: options.frontendProject - ? names(options.frontendProject).fileName - : undefined, - appProjectRoot, - importPath, - parsedTags, - linter: options.linter ?? 'eslint', - unitTestRunner: options.unitTestRunner ?? 'jest', - rootProject: options.rootProject ?? false, - port: options.port ?? 3000, - outputPath: isUsingTsSolutionConfig - ? joinPathFragments(appProjectRoot, 'dist') - : joinPathFragments( - 'dist', - options.rootProject ? appProjectName : appProjectRoot - ), - isUsingTsSolutionConfig, - swcJest, - useProjectJson, - }; -} - export default applicationGenerator; diff --git a/packages/node/src/generators/application/lib/add-dependencies.ts b/packages/node/src/generators/application/lib/add-dependencies.ts new file mode 100644 index 0000000000..b70a66e0c2 --- /dev/null +++ b/packages/node/src/generators/application/lib/add-dependencies.ts @@ -0,0 +1,71 @@ +import { + addDependenciesToPackageJson, + GeneratorCallback, + Tree, +} from '@nx/devkit'; +import { esbuildVersion } from '@nx/js/src/utils/versions'; +import { + expressTypingsVersion, + expressVersion, + fastifyAutoloadVersion, + fastifyPluginVersion, + fastifySensibleVersion, + fastifyVersion, + koaTypingsVersion, + koaVersion, + nxVersion, + tslibVersion, + typesNodeVersion, +} from '../../../utils/versions'; +import { NormalizedSchema } from './normalized-schema'; + +export function addProjectDependencies( + tree: Tree, + options: NormalizedSchema +): GeneratorCallback { + const bundlers = { + webpack: { + '@nx/webpack': nxVersion, + }, + esbuild: { + '@nx/esbuild': nxVersion, + esbuild: esbuildVersion, + }, + }; + + const frameworkDependencies = { + express: { + express: expressVersion, + }, + koa: { + koa: koaVersion, + }, + fastify: { + fastify: fastifyVersion, + 'fastify-plugin': fastifyPluginVersion, + '@fastify/autoload': fastifyAutoloadVersion, + '@fastify/sensible': fastifySensibleVersion, + }, + }; + const frameworkDevDependencies = { + express: { + '@types/express': expressTypingsVersion, + }, + koa: { + '@types/koa': koaTypingsVersion, + }, + fastify: {}, + }; + return addDependenciesToPackageJson( + tree, + { + ...frameworkDependencies[options.framework], + tslib: tslibVersion, + }, + { + ...frameworkDevDependencies[options.framework], + ...bundlers[options.bundler], + '@types/node': typesNodeVersion, + } + ); +} diff --git a/packages/node/src/generators/application/lib/add-linting.ts b/packages/node/src/generators/application/lib/add-linting.ts new file mode 100644 index 0000000000..a894606d5c --- /dev/null +++ b/packages/node/src/generators/application/lib/add-linting.ts @@ -0,0 +1,23 @@ +import { GeneratorCallback, joinPathFragments, Tree } from '@nx/devkit'; +import { lintProjectGenerator } from '@nx/eslint'; +import { NormalizedSchema } from './normalized-schema'; + +export async function addLintingToApplication( + tree: Tree, + options: NormalizedSchema +): Promise { + const lintTask = await lintProjectGenerator(tree, { + linter: options.linter, + project: options.name, + tsConfigPaths: [ + joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'), + ], + unitTestRunner: options.unitTestRunner, + skipFormat: true, + setParserOptionsProject: options.setParserOptionsProject, + rootProject: options.rootProject, + addPlugin: options.addPlugin, + }); + + return lintTask; +} diff --git a/packages/node/src/generators/application/lib/add-proxy.ts b/packages/node/src/generators/application/lib/add-proxy.ts new file mode 100644 index 0000000000..ddf87b85fe --- /dev/null +++ b/packages/node/src/generators/application/lib/add-proxy.ts @@ -0,0 +1,61 @@ +import { + logger, + readProjectConfiguration, + Tree, + updateProjectConfiguration, +} from '@nx/devkit'; +import { NormalizedSchema } from './normalized-schema'; + +export function addProxy(tree: Tree, options: NormalizedSchema) { + const projectConfig = readProjectConfiguration(tree, options.frontendProject); + if ( + projectConfig.targets && + ['serve', 'dev'].find((t) => !!projectConfig.targets[t]) + ) { + const targetName = ['serve', 'dev'].find((t) => !!projectConfig.targets[t]); + projectConfig.targets[targetName].dependsOn = [ + ...(projectConfig.targets[targetName].dependsOn ?? []), + `${options.name}:serve`, + ]; + const pathToProxyFile = `${projectConfig.root}/proxy.conf.json`; + projectConfig.targets[targetName].options = { + ...projectConfig.targets[targetName].options, + proxyConfig: pathToProxyFile, + }; + + if (!tree.exists(pathToProxyFile)) { + tree.write( + pathToProxyFile, + JSON.stringify( + { + '/api': { + target: `http://localhost:${options.port}`, + secure: false, + }, + }, + null, + 2 + ) + ); + } else { + //add new entry to existing config + const proxyFileContent = tree.read(pathToProxyFile).toString(); + + const proxyModified = { + ...JSON.parse(proxyFileContent), + [`/${options.name}-api`]: { + target: `http://localhost:${options.port}`, + secure: false, + }, + }; + + tree.write(pathToProxyFile, JSON.stringify(proxyModified, null, 2)); + } + + updateProjectConfiguration(tree, options.frontendProject, projectConfig); + } else { + logger.warn( + `Skip updating proxy for frontend project "${options.frontendProject}" since "serve" target is not found in project.json. For more information, see: https://nx.dev/recipes/node/application-proxies.` + ); + } +} diff --git a/packages/node/src/generators/application/lib/create-files.ts b/packages/node/src/generators/application/lib/create-files.ts new file mode 100644 index 0000000000..f8af60e83a --- /dev/null +++ b/packages/node/src/generators/application/lib/create-files.ts @@ -0,0 +1,71 @@ +import { + generateFiles, + joinPathFragments, + offsetFromRoot, + toJS, + Tree, +} from '@nx/devkit'; +import { getRelativePathToRootTsConfig } from '@nx/js'; +import { join } from 'path'; +import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin'; +import { NormalizedSchema } from './normalized-schema'; + +export function addAppFiles(tree: Tree, options: NormalizedSchema) { + generateFiles( + tree, + join(__dirname, '../files/common'), + options.appProjectRoot, + { + ...options, + tmpl: '', + name: options.name, + root: options.appProjectRoot, + offset: offsetFromRoot(options.appProjectRoot), + rootTsConfigPath: getRelativePathToRootTsConfig( + tree, + options.appProjectRoot + ), + webpackPluginOptions: + hasWebpackPlugin(tree) && options.addPlugin !== false + ? { + outputPath: options.isUsingTsSolutionConfig + ? 'dist' + : joinPathFragments( + offsetFromRoot(options.appProjectRoot), + 'dist', + options.rootProject ? options.name : options.appProjectRoot + ), + main: './src/main' + (options.js ? '.js' : '.ts'), + tsConfig: './tsconfig.app.json', + assets: ['./src/assets'], + } + : null, + } + ); + + if (options.bundler !== 'webpack') { + tree.delete(joinPathFragments(options.appProjectRoot, 'webpack.config.js')); + } + + if (options.framework && options.framework !== 'none') { + generateFiles( + tree, + join(__dirname, `../files/${options.framework}`), + options.appProjectRoot, + { + ...options, + tmpl: '', + name: options.name, + root: options.appProjectRoot, + offset: offsetFromRoot(options.appProjectRoot), + rootTsConfigPath: getRelativePathToRootTsConfig( + tree, + options.appProjectRoot + ), + } + ); + } + if (options.js) { + toJS(tree); + } +} diff --git a/packages/node/src/generators/application/lib/create-project.ts b/packages/node/src/generators/application/lib/create-project.ts new file mode 100644 index 0000000000..3efd71b477 --- /dev/null +++ b/packages/node/src/generators/application/lib/create-project.ts @@ -0,0 +1,71 @@ +import { + addProjectConfiguration, + joinPathFragments, + ProjectConfiguration, + Tree, + writeJson, +} from '@nx/devkit'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; +import type { PackageJson } from 'nx/src/utils/package-json'; +import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin'; +import { NormalizedSchema } from './normalized-schema'; +import { + getEsBuildConfig, + getNestWebpackBuildConfig, + getServeConfig, + getWebpackBuildConfig, +} from './create-targets'; + +export function addProject(tree: Tree, options: NormalizedSchema) { + const project: ProjectConfiguration = { + root: options.appProjectRoot, + sourceRoot: joinPathFragments(options.appProjectRoot, 'src'), + projectType: 'application', + targets: {}, + tags: options.parsedTags, + }; + + if (options.bundler === 'esbuild') { + addBuildTargetDefaults(tree, '@nx/esbuild:esbuild'); + project.targets.build = getEsBuildConfig(project, options); + } else if (options.bundler === 'webpack') { + if (!hasWebpackPlugin(tree) && options.addPlugin === false) { + addBuildTargetDefaults(tree, `@nx/webpack:webpack`); + project.targets.build = getWebpackBuildConfig(project, options); + } else if (options.isNest) { + // If we are using Nest that has the webpack plugin we need to override the + // build target so that node-env can be set to production or development so the serve target can be run in development mode + project.targets.build = getNestWebpackBuildConfig(); + } + } + project.targets.serve = getServeConfig(options); + + const packageJson: PackageJson = { + name: options.importPath, + version: '0.0.1', + private: true, + }; + + if (!options.useProjectJson) { + packageJson.nx = { + name: options.name !== options.importPath ? options.name : undefined, + targets: project.targets, + tags: project.tags?.length ? project.tags : undefined, + }; + } else { + addProjectConfiguration( + tree, + options.name, + project, + options.standaloneConfig + ); + } + + if (!options.useProjectJson || options.isUsingTsSolutionConfig) { + writeJson( + tree, + joinPathFragments(options.appProjectRoot, 'package.json'), + packageJson + ); + } +} diff --git a/packages/node/src/generators/application/lib/create-targets.ts b/packages/node/src/generators/application/lib/create-targets.ts new file mode 100644 index 0000000000..e3a65260c5 --- /dev/null +++ b/packages/node/src/generators/application/lib/create-targets.ts @@ -0,0 +1,122 @@ +import { + joinPathFragments, + ProjectConfiguration, + TargetConfiguration, +} from '@nx/devkit'; +import { NormalizedSchema } from './normalized-schema'; + +export function getWebpackBuildConfig( + project: ProjectConfiguration, + options: NormalizedSchema +): TargetConfiguration { + return { + executor: `@nx/webpack:webpack`, + outputs: ['{options.outputPath}'], + defaultConfiguration: 'production', + options: { + target: 'node', + compiler: 'tsc', + outputPath: options.outputPath, + main: joinPathFragments( + project.sourceRoot, + 'main' + (options.js ? '.js' : '.ts') + ), + tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'), + assets: [joinPathFragments(project.sourceRoot, 'assets')], + webpackConfig: joinPathFragments( + options.appProjectRoot, + 'webpack.config.js' + ), + generatePackageJson: options.isUsingTsSolutionConfig ? undefined : true, + }, + configurations: { + development: { + outputHashing: 'none', + }, + production: { + ...(options.docker && { generateLockfile: true }), + }, + }, + }; +} + +export function getEsBuildConfig( + project: ProjectConfiguration, + options: NormalizedSchema +): TargetConfiguration { + return { + executor: '@nx/esbuild:esbuild', + outputs: ['{options.outputPath}'], + defaultConfiguration: 'production', + options: { + platform: 'node', + outputPath: options.outputPath, + // Use CJS for Node apps for widest compatibility. + format: ['cjs'], + bundle: false, + main: joinPathFragments( + project.sourceRoot, + 'main' + (options.js ? '.js' : '.ts') + ), + tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'), + assets: [joinPathFragments(project.sourceRoot, 'assets')], + generatePackageJson: options.isUsingTsSolutionConfig ? undefined : true, + esbuildOptions: { + sourcemap: true, + // Generate CJS files as .js so imports can be './foo' rather than './foo.cjs'. + outExtension: { '.js': '.js' }, + }, + }, + configurations: { + development: {}, + production: { + ...(options.docker && { generateLockfile: true }), + esbuildOptions: { + sourcemap: false, + // Generate CJS files as .js so imports can be './foo' rather than './foo.cjs'. + outExtension: { '.js': '.js' }, + }, + }, + }, + }; +} + +export function getServeConfig(options: NormalizedSchema): TargetConfiguration { + return { + continuous: true, + executor: '@nx/js:node', + defaultConfiguration: 'development', + // Run build, which includes dependency on "^build" by default, so the first run + // won't error out due to missing build artifacts. + dependsOn: ['build'], + options: { + buildTarget: `${options.name}:build`, + // Even though `false` is the default, set this option so users know it + // exists if they want to always run dependencies during each rebuild. + runBuildTargetDependencies: false, + }, + configurations: { + development: { + buildTarget: `${options.name}:build:development`, + }, + production: { + buildTarget: `${options.name}:build:production`, + }, + }, + }; +} + +export function getNestWebpackBuildConfig(): TargetConfiguration { + return { + executor: 'nx:run-commands', + options: { + command: 'webpack-cli build', + args: ['node-env=production'], + }, + configurations: { + development: { + args: ['node-env=development'], + }, + }, + }; +} diff --git a/packages/node/src/generators/application/lib/index.ts b/packages/node/src/generators/application/lib/index.ts new file mode 100644 index 0000000000..c1ed90c0ed --- /dev/null +++ b/packages/node/src/generators/application/lib/index.ts @@ -0,0 +1,8 @@ +export * from './normalized-schema'; +export * from './normalize-options'; +export * from './create-targets'; +export * from './create-project'; +export * from './create-files'; +export * from './add-dependencies'; +export * from './add-linting'; +export * from './add-proxy'; diff --git a/packages/node/src/generators/application/lib/normalize-options.ts b/packages/node/src/generators/application/lib/normalize-options.ts new file mode 100644 index 0000000000..f3bbe5731c --- /dev/null +++ b/packages/node/src/generators/application/lib/normalize-options.ts @@ -0,0 +1,70 @@ +import { joinPathFragments, names, readNxJson, Tree } from '@nx/devkit'; +import { + determineProjectNameAndRootOptions, + ensureRootProjectName, +} from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { Schema } from '../schema'; +import { NormalizedSchema } from './normalized-schema'; + +export async function normalizeOptions( + host: Tree, + options: Schema +): Promise { + await ensureRootProjectName(options, 'application'); + const { + projectName, + projectRoot: appProjectRoot, + importPath, + } = await determineProjectNameAndRootOptions(host, { + name: options.name, + projectType: 'application', + directory: options.directory, + rootProject: options.rootProject, + }); + options.rootProject = appProjectRoot === '.'; + + options.bundler = options.bundler ?? 'esbuild'; + options.e2eTestRunner = options.e2eTestRunner ?? 'jest'; + + const parsedTags = options.tags + ? options.tags.split(',').map((s) => s.trim()) + : []; + + const nxJson = readNxJson(host); + const addPlugin = + process.env.NX_ADD_PLUGINS !== 'false' && + nxJson.useInferencePlugins !== false; + + const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host); + const swcJest = options.swcJest ?? isUsingTsSolutionConfig; + + const appProjectName = + !isUsingTsSolutionConfig || options.name ? projectName : importPath; + const useProjectJson = options.useProjectJson ?? !isUsingTsSolutionConfig; + + return { + addPlugin, + ...options, + name: appProjectName, + frontendProject: options.frontendProject + ? names(options.frontendProject).fileName + : undefined, + appProjectRoot, + importPath, + parsedTags, + linter: options.linter ?? 'eslint', + unitTestRunner: options.unitTestRunner ?? 'jest', + rootProject: options.rootProject ?? false, + port: options.port ?? 3000, + outputPath: isUsingTsSolutionConfig + ? joinPathFragments(appProjectRoot, 'dist') + : joinPathFragments( + 'dist', + options.rootProject ? appProjectName : appProjectRoot + ), + isUsingTsSolutionConfig, + swcJest, + useProjectJson, + }; +} diff --git a/packages/node/src/generators/application/lib/normalized-schema.ts b/packages/node/src/generators/application/lib/normalized-schema.ts new file mode 100644 index 0000000000..152b3ae868 --- /dev/null +++ b/packages/node/src/generators/application/lib/normalized-schema.ts @@ -0,0 +1,9 @@ +import { Schema } from '../schema'; + +export interface NormalizedSchema extends Omit { + appProjectRoot: string; + parsedTags: string[]; + outputPath: string; + importPath: string; + isUsingTsSolutionConfig: boolean; +}