488 lines
13 KiB
TypeScript
488 lines
13 KiB
TypeScript
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<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,
|
|
});
|
|
|
|
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<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())
|
|
: [];
|
|
|
|
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;
|