import { addDependenciesToPackageJson, addProjectConfiguration, ensurePackage, formatFiles, generateFiles, GeneratorCallback, joinPathFragments, logger, names, offsetFromRoot, ProjectConfiguration, readProjectConfiguration, runTasksInSerial, TargetConfiguration, toJS, Tree, updateJson, updateProjectConfiguration, updateTsConfigsToJs, } from '@nx/devkit'; import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { configurationGenerator } from '@nx/jest'; import { getRelativePathToRootTsConfig, tsConfigBaseOptions } from '@nx/js'; import { esbuildVersion } from '@nx/js/src/utils/versions'; import { Linter, lintProjectGenerator } from '@nx/eslint'; import { join } from 'path'; import { expressTypingsVersion, expressVersion, fastifyAutoloadVersion, fastifyPluginVersion, fastifySensibleVersion, fastifyVersion, koaTypingsVersion, koaVersion, 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'; export interface NormalizedSchema extends Schema { appProjectRoot: string; parsedTags: string[]; } function getWebpackBuildConfig( project: ProjectConfiguration, options: NormalizedSchema ): TargetConfiguration { return { executor: `@nx/webpack:webpack`, outputs: ['{options.outputPath}'], defaultConfiguration: 'production', options: { target: 'node', compiler: 'tsc', outputPath: joinPathFragments( 'dist', options.rootProject ? options.name : options.appProjectRoot ), main: joinPathFragments( project.sourceRoot, 'main' + (options.js ? '.js' : '.ts') ), tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'), assets: [joinPathFragments(project.sourceRoot, 'assets')], isolatedConfig: true, webpackConfig: joinPathFragments( options.appProjectRoot, 'webpack.config.js' ), }, configurations: { development: {}, 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: joinPathFragments( 'dist', options.rootProject ? options.name : options.appProjectRoot ), // 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: 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 { executor: '@nx/js:node', defaultConfiguration: 'development', options: { buildTarget: `${options.name}:build`, }, configurations: { development: { buildTarget: `${options.name}:build:development`, }, production: { buildTarget: `${options.name}:build:production`, }, }, }; } function addProject(tree: Tree, options: NormalizedSchema) { const project: ProjectConfiguration = { root: options.appProjectRoot, sourceRoot: joinPathFragments(options.appProjectRoot, 'src'), projectType: 'application', targets: {}, tags: options.parsedTags, }; project.targets.build = options.bundler === 'esbuild' ? getEsBuildConfig(project, options) : getWebpackBuildConfig(project, options); project.targets.serve = getServeConfig(options); addProjectConfiguration( tree, options.name, project, options.standaloneConfig ); } 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 ), } ); 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); } if (options.pascalCaseFiles) { logger.warn('NOTE: --pascalCaseFiles is a noop'); } } function addProxy(tree: Tree, options: NormalizedSchema) { const projectConfig = readProjectConfiguration(tree, options.frontendProject); if (projectConfig.targets && projectConfig.targets.serve) { const pathToProxyFile = `${projectConfig.root}/proxy.conf.json`; projectConfig.targets.serve.options = { ...projectConfig.targets.serve.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); } } 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, }); 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], }, { ...frameworkDevDependencies[options.framework], ...bundlers[options.bundler], } ); } function updateTsConfigOptions(tree: Tree, options: NormalizedSchema) { updateJson(tree, `${options.appProjectRoot}/tsconfig.json`, (json) => { if (options.rootProject) { return { compilerOptions: { ...tsConfigBaseOptions, ...json.compilerOptions, esModuleInterop: true, }, ...json, extends: undefined, exclude: ['node_modules', 'tmp'], }; } else { return { ...json, compilerOptions: { ...json.compilerOptions, esModuleInterop: true, }, }; } }); } export async function applicationGenerator(tree: Tree, schema: Schema) { return await applicationGeneratorInternal(tree, { projectNameAndRootFormat: 'derived', ...schema, }); } export async function applicationGeneratorInternal(tree: Tree, schema: Schema) { const options = await normalizeOptions(tree, schema); const tasks: GeneratorCallback[] = []; if (options.framework === 'nest') { const { applicationGenerator } = ensurePackage('@nx/nest', nxVersion); return await applicationGenerator(tree, { ...options, skipFormat: true }); } const initTask = await initGenerator(tree, { ...schema, skipFormat: true, }); tasks.push(initTask); const installTask = addProjectDependencies(tree, options); tasks.push(installTask); addAppFiles(tree, options); addProject(tree, options); updateTsConfigOptions(tree, options); if (options.linter === Linter.EsLint) { const lintTask = await addLintingToApplication(tree, options); tasks.push(lintTask); } if (options.unitTestRunner === 'jest') { const jestTask = await configurationGenerator(tree, { ...options, project: options.name, setupFile: 'none', skipSerializers: true, supportTsx: options.js, testEnvironment: 'node', compiler: options.swcJest ? 'swc' : 'tsc', skipFormat: true, }); tasks.push(jestTask); } else { // No need for default spec file if unit testing is not setup. tree.delete( joinPathFragments(options.appProjectRoot, 'src/app/app.spec.ts') ); } if (options.e2eTestRunner === 'jest') { const e2eTask = await e2eProjectGenerator(tree, { ...options, projectType: options.framework === 'none' ? 'cli' : 'server', name: options.rootProject ? 'e2e' : `${options.name}-e2e`, directory: options.rootProject ? 'e2e' : `${options.appProjectRoot}-e2e`, projectNameAndRootFormat: 'as-provided', project: options.name, port: options.port, isNest: options.isNest, skipFormat: true, }); tasks.push(e2eTask); } if (options.js) { updateTsConfigsToJs(tree, { projectRoot: options.appProjectRoot }); } if (options.frontendProject) { addProxy(tree, options); } if (options.docker) { const dockerTask = await setupDockerGenerator(tree, { ...options, project: options.name, skipFormat: true, }); tasks.push(dockerTask); } if (!options.skipFormat) { await formatFiles(tree); } return runTasksInSerial(...tasks); } async function normalizeOptions( host: Tree, options: Schema ): Promise { const { projectName: appProjectName, projectRoot: appProjectRoot, projectNameAndRootFormat, } = await determineProjectNameAndRootOptions(host, { name: options.name, projectType: 'application', directory: options.directory, projectNameAndRootFormat: options.projectNameAndRootFormat, rootProject: options.rootProject, callingGenerator: '@nx/node:application', }); options.rootProject = appProjectRoot === '.'; options.projectNameAndRootFormat = projectNameAndRootFormat; options.bundler = options.bundler ?? 'esbuild'; options.e2eTestRunner = options.e2eTestRunner ?? 'jest'; const parsedTags = options.tags ? options.tags.split(',').map((s) => s.trim()) : []; return { ...options, name: appProjectName, frontendProject: options.frontendProject ? names(options.frontendProject).fileName : undefined, appProjectRoot, parsedTags, linter: options.linter ?? Linter.EsLint, unitTestRunner: options.unitTestRunner ?? 'jest', rootProject: options.rootProject ?? false, port: options.port ?? 3000, }; } export default applicationGenerator;