Nicholas Cunningham bff23d1b75
fix(node): Ensure docker file is generated when nest framework is supplied (#27153)
When we generate a nest application and supply the `--docker` flag it
should generate a DockerFile.

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

Currently, no DockerFile is created.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

A DockerFile to be created.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #17343
2024-07-26 10:31:37 -06:00

581 lines
16 KiB
TypeScript

import {
addDependenciesToPackageJson,
addProjectConfiguration,
ensurePackage,
formatFiles,
generateFiles,
GeneratorCallback,
joinPathFragments,
logger,
names,
offsetFromRoot,
ProjectConfiguration,
readNxJson,
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,
initGenerator as jsInitGenerator,
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,
tslibVersion,
typesNodeVersion,
} 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/add-build-target-defaults';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
export interface NormalizedSchema extends Schema {
appProjectRoot: string;
parsedTags: string[];
outputPath: string;
}
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'
),
},
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: 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: 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',
// 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 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)) {
addBuildTargetDefaults(tree, `@nx/webpack:webpack`);
project.targets.build = getWebpackBuildConfig(project, options);
}
}
project.targets.serve = getServeConfig(options);
addProjectConfiguration(
tree,
options.name,
project,
options.standaloneConfig
);
}
function addAppFiles(tree: Tree, options: NormalizedSchema) {
const sourceRoot = joinPathFragments(options.appProjectRoot, 'src');
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)
? {
outputPath: options.outputPath,
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);
}
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<GeneratorCallback> {
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,
}
);
}
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, {
addPlugin: false,
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function applicationGeneratorInternal(tree: Tree, schema: Schema) {
const options = await normalizeOptions(tree, schema);
const tasks: GeneratorCallback[] = [];
if (options.framework === 'nest') {
// nx-ignore-next-line
const { applicationGenerator } = ensurePackage('@nx/nest', nxVersion);
const nestTasks = await applicationGenerator(tree, {
...options,
skipFormat: true,
});
tasks.push(nestTasks);
if (options.docker) {
const dockerTask = await setupDockerGenerator(tree, {
...options,
project: options.name,
skipFormat: true,
});
tasks.push(dockerTask);
}
return runTasksInSerial(
...[
...tasks,
() => {
logShowProjectCommand(options.name);
},
]
);
}
const jsInitTask = await jsInitGenerator(tree, {
...schema,
tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json',
skipFormat: true,
});
tasks.push(jsInitTask);
const initTask = await initGenerator(tree, {
...schema,
skipFormat: true,
});
tasks.push(initTask);
const installTask = addProjectDependencies(tree, options);
tasks.push(installTask);
if (options.bundler === 'webpack') {
const { webpackInitGenerator } = ensurePackage<
typeof import('@nx/webpack')
>('@nx/webpack', nxVersion);
const webpackInitTask = await webpackInitGenerator(tree, {
skipPackageJson: options.skipPackageJson,
skipFormat: true,
addPlugin: options.addPlugin,
});
tasks.push(webpackInitTask);
if (!options.skipPackageJson) {
const { ensureDependencies } = await import(
'@nx/webpack/src/utils/ensure-dependencies'
);
tasks.push(
ensureDependencies(tree, {
uiFramework: options.isNest ? 'none' : 'react',
})
);
}
}
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);
}
tasks.push(() => {
logShowProjectCommand(options.name);
});
return runTasksInSerial(...tasks);
}
async function normalizeOptions(
host: Tree,
options: Schema
): Promise<NormalizedSchema> {
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())
: [];
const nxJson = readNxJson(host);
const addPlugin =
process.env.NX_ADD_PLUGINS !== 'false' &&
nxJson.useInferencePlugins !== false;
return {
addPlugin,
...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,
outputPath: joinPathFragments(
'dist',
options.rootProject ? options.name : appProjectRoot
),
};
}
export default applicationGenerator;