@nestjs/schematics
Version:
Nest - modern, fast, powerful node.js web framework (@schematics)
309 lines (308 loc) • 11.6 kB
JavaScript
import { join, normalize, strings } from '@angular-devkit/core';
import { apply, branchAndMerge, chain, mergeWith, move, noop, SchematicsException, template, url, } from '@angular-devkit/schematics';
import { existsSync, readFileSync } from 'fs';
import { parse, stringify } from 'comment-json';
import { formatFiles } from '../../utils/format-files.rule.js';
import { inPlaceSortByKeys, normalizeToKebabOrSnakeCase, } from '../../utils/index.js';
import { DEFAULT_APPS_PATH, DEFAULT_APP_NAME, DEFAULT_DIR_ENTRY_APP, DEFAULT_LANGUAGE, DEFAULT_LIB_PATH, DEFAULT_PATH_NAME, PROJECT_TYPE, TEST_ENV, } from '../defaults.js';
import { isEsmProject } from '../../utils/source-root.helpers.js';
export function main(options) {
const appName = getAppNameFromPackageJson();
options = transform(options);
return chain([
updateTsConfig(options.path, appName),
updatePackageJson(options, appName),
(tree, context) => isMonorepo(tree)
? noop()(tree, context)
: chain([
branchAndMerge(mergeWith(generateWorkspace(options, appName))),
moveDefaultAppToApps(options.path, appName, options.sourceRoot),
])(tree, context),
addAppsToCliOptions(options.path, options.name, appName),
addTsConfigReference(options.path, options.name),
(tree) => {
options.isEsm = isEsmProject(tree);
return tree;
},
branchAndMerge(mergeWith(generate(options))),
options.format === true ? formatFiles() : noop(),
]);
}
function getAppNameFromPackageJson() {
try {
if (!existsSync('./package.json')) {
return DEFAULT_DIR_ENTRY_APP;
}
const packageJson = JSON.parse(stripBom(readFileSync('./package.json', 'utf-8')));
if (!packageJson.name) {
return DEFAULT_DIR_ENTRY_APP;
}
let name = packageJson.name;
name = name.replace(/[^\w.]+/g, '-').replace(/-+/g, '-');
return name[0] === '-' ? name.substr(1) : name;
}
catch {
return DEFAULT_DIR_ENTRY_APP;
}
}
function stripBom(value) {
if (value.charCodeAt(0) === 0xfeff) {
return value.slice(1);
}
return value;
}
function transform(options) {
const target = Object.assign({}, options);
const defaultSourceRoot = options.rootDir !== undefined ? options.rootDir : DEFAULT_APPS_PATH;
if (!target.name) {
target.name = DEFAULT_APP_NAME;
}
target.language = target.language ? target.language : DEFAULT_LANGUAGE;
target.name = normalizeToKebabOrSnakeCase(target.name);
target.path =
target.path !== undefined
? join(normalize(defaultSourceRoot), target.path)
: normalize(defaultSourceRoot);
return target;
}
function isMonorepo(host) {
const nestFileExists = host.exists('nest.json');
const nestCliFileExists = host.exists('nest-cli.json');
if (!nestFileExists && !nestCliFileExists) {
return false;
}
const filename = nestCliFileExists ? 'nest-cli.json' : 'nest.json';
const source = host.read(filename);
if (!source) {
return false;
}
const sourceText = source.toString('utf-8');
const optionsObj = parse(sourceText);
return !!optionsObj.monorepo;
}
function updateJsonFile(host, path, callback) {
const source = host.read(path);
if (source) {
const sourceText = source.toString('utf-8');
const json = parse(sourceText);
callback(json);
host.overwrite(path, stringify(json, null, 2));
}
return host;
}
function updateTsConfig(projectRoot, appName) {
return (host) => {
if (!host.exists('tsconfig.json')) {
return host;
}
return updateJsonFile(host, 'tsconfig.json', (tsconfig) => {
if (!tsconfig.compilerOptions) {
tsconfig.compilerOptions = {};
}
delete tsconfig.compilerOptions.baseUrl;
if (!tsconfig.files) {
tsconfig.files = [];
}
delete tsconfig.include;
delete tsconfig.exclude;
if (!tsconfig.references) {
tsconfig.references = [];
}
const workspaceAppTsConfigPath = join(projectRoot, appName, 'tsconfig.app.json');
const hasWorkspaceRef = tsconfig.references.some((ref) => ref.path === `./${workspaceAppTsConfigPath}`);
if (!hasWorkspaceRef) {
tsconfig.references.push({
path: `./${workspaceAppTsConfigPath}`,
});
}
});
};
}
function addTsConfigReference(projectRoot, projectName) {
return (host) => {
if (!host.exists('tsconfig.json')) {
return host;
}
return updateJsonFile(host, 'tsconfig.json', (tsconfig) => {
if (!tsconfig.references) {
tsconfig.references = [];
}
const refPath = `./${join(projectRoot, projectName, 'tsconfig.app.json')}`;
const hasRef = tsconfig.references.some((ref) => ref.path === refPath);
if (!hasRef) {
tsconfig.references.push({ path: refPath });
}
});
};
}
function updatePackageJson(options, defaultAppName) {
return (host) => {
if (!host.exists('package.json')) {
return host;
}
return updateJsonFile(host, 'package.json', (packageJson) => {
updateNpmScripts(packageJson.scripts, options, defaultAppName);
updateJestOptions(packageJson.jest, options);
});
};
}
function updateNpmScripts(scripts, options, defaultAppName) {
if (!scripts) {
return;
}
const defaultFormatScriptName = 'format';
const defaultStartScriptName = 'start:prod';
const defaultTestScriptName = 'test:e2e';
if (!scripts[defaultTestScriptName] &&
!scripts[defaultFormatScriptName] &&
!scripts[defaultStartScriptName]) {
return;
}
if (scripts[defaultTestScriptName] &&
scripts[defaultTestScriptName].indexOf(options.path) < 0) {
const defaultTestDir = 'test';
const newTestDir = join(options.path, defaultAppName, defaultTestDir);
scripts[defaultTestScriptName] = scripts[defaultTestScriptName].replace(defaultTestDir, newTestDir);
}
if (scripts[defaultFormatScriptName] &&
scripts[defaultFormatScriptName].indexOf(DEFAULT_PATH_NAME) >= 0) {
const defaultSourceRoot = options.rootDir !== undefined ? options.rootDir : DEFAULT_APPS_PATH;
scripts[defaultFormatScriptName] =
`prettier --write "${defaultSourceRoot}/**/*.ts" "${DEFAULT_LIB_PATH}/**/*.ts"`;
}
if (scripts[defaultStartScriptName] &&
scripts[defaultStartScriptName].indexOf('dist/main') >= 0) {
const defaultSourceRoot = options.rootDir !== undefined ? options.rootDir : DEFAULT_APPS_PATH;
scripts[defaultStartScriptName] =
`node dist/${defaultSourceRoot}/${defaultAppName}/main`;
}
}
function updateJestOptions(jestOptions, options) {
if (!jestOptions) {
return;
}
if (jestOptions.rootDir === DEFAULT_PATH_NAME) {
jestOptions.rootDir = '.';
jestOptions.coverageDirectory = './coverage';
}
const defaultSourceRoot = options.rootDir !== undefined ? options.rootDir : DEFAULT_APPS_PATH;
const jestSourceRoot = `<rootDir>/${defaultSourceRoot}/`;
if (!jestOptions.roots) {
jestOptions.roots = [jestSourceRoot];
}
else if (jestOptions.roots.indexOf(jestSourceRoot) < 0) {
jestOptions.roots.push(jestSourceRoot);
const originalSourceRoot = `<rootDir>/src/`;
const originalSourceRootIndex = jestOptions.roots.indexOf(originalSourceRoot);
if (originalSourceRootIndex >= 0) {
jestOptions.roots.splice(originalSourceRootIndex, 1);
}
}
}
function moveDefaultAppToApps(projectRoot, appName, sourceRoot = DEFAULT_PATH_NAME) {
return (host) => {
if (process.env.NODE_ENV === TEST_ENV) {
return host;
}
const appDestination = join(projectRoot, appName);
moveDirectoryTo(sourceRoot, appDestination, host);
moveDirectoryTo('test', appDestination, host);
return host;
};
}
function moveDirectoryTo(srcDir, destination, tree) {
let srcDirExists = false;
tree
.getDir(srcDir)
.visit((filePath, file) => {
if (!file)
return;
srcDirExists = true;
const newFilePath = join(destination, filePath);
tree.create(newFilePath, file.content);
});
if (srcDirExists) {
tree.delete(srcDir);
}
}
function addAppsToCliOptions(projectRoot, projectName, appName) {
const rootPath = join(projectRoot, projectName);
const project = {
type: PROJECT_TYPE.APPLICATION,
root: rootPath,
entryFile: 'main',
sourceRoot: join(rootPath, DEFAULT_PATH_NAME),
compilerOptions: {
tsConfigPath: join(rootPath, 'tsconfig.app.json'),
},
};
return (host) => {
const nestFileExists = host.exists('nest.json');
let nestCliFileExists = host.exists('nest-cli.json');
if (!nestCliFileExists && !nestFileExists) {
host.create('nest-cli.json', '{}');
nestCliFileExists = true;
}
return updateJsonFile(host, nestCliFileExists ? 'nest-cli.json' : 'nest.json', (optionsFile) => {
updateMainAppOptions(optionsFile, projectRoot, appName);
if (!optionsFile.projects) {
optionsFile.projects = {};
}
if (optionsFile.projects[projectName]) {
throw new SchematicsException(`Project "${projectName}" exists in this workspace already.`);
}
optionsFile.projects[projectName] = project;
inPlaceSortByKeys(optionsFile.projects);
});
};
}
function updateMainAppOptions(optionsFile, projectRoot, appName) {
if (optionsFile.monorepo) {
return;
}
const rootFilePath = join(projectRoot, appName);
const tsConfigPath = join(rootFilePath, 'tsconfig.app.json');
optionsFile.monorepo = true;
optionsFile.root = rootFilePath;
optionsFile.sourceRoot = join(projectRoot, appName, optionsFile.sourceRoot || DEFAULT_PATH_NAME);
if (!optionsFile.compilerOptions) {
optionsFile.compilerOptions = {};
}
optionsFile.compilerOptions.builder = 'rspack';
optionsFile.compilerOptions.tsConfigPath = tsConfigPath;
if (!optionsFile.projects) {
optionsFile.projects = {};
}
optionsFile.projects[appName] = {
type: PROJECT_TYPE.APPLICATION,
root: rootFilePath,
entryFile: optionsFile.entryFile || 'main',
sourceRoot: join(rootFilePath, DEFAULT_PATH_NAME),
compilerOptions: {
tsConfigPath,
},
};
}
function generateWorkspace(options, appName) {
const path = join(options.path, appName);
return apply(url(join('./workspace', options.language)), [
template({
...strings,
...options,
name: appName,
}),
move(path),
]);
}
function generate(options) {
return (context) => {
const path = join(options.path, options.name);
return apply(url(join('./files', options.language)), [
template({
...strings,
...options,
}),
move(path),
])(context);
};
}