UNPKG

@nestjs/schematics

Version:

Nest - modern, fast, powerful node.js web framework (@schematics)

309 lines (308 loc) 11.6 kB
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); }; }