UNPKG

@nestjs/cli

Version:

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

203 lines (202 loc) 8.11 kB
import { confirm, input, select } from '@inquirer/prompts'; import ansis from 'ansis'; import { execSync } from 'child_process'; import * as fs from 'fs'; import { join } from 'path'; import { defaultGitIgnore } from '../lib/configuration/defaults.js'; import { PackageManager, PackageManagerFactory, } from '../lib/package-managers/index.js'; import { generateInput, generateSelect } from '../lib/questions/questions.js'; import { GitRunner } from '../lib/runners/git.runner.js'; import { Collection, CollectionFactory, SchematicOption, } from '../lib/schematics/index.js'; import { EMOJIS, MESSAGES } from '../lib/ui/index.js'; import { normalizeToKebabOrSnakeCase } from '../lib/utils/formatting.js'; import { gracefullyExitOnPromptError } from '../lib/utils/gracefully-exit-on-prompt-error.js'; import { isInteractive } from '../lib/utils/is-interactive.js'; import { AbstractAction } from './abstract.action.js'; export class NewAction extends AbstractAction { async handle(context) { await askForMissingInformation(context); await generateApplicationFiles(context).catch(exit); const projectDirectory = getProjectDirectory(context); if (!context.skipInstall) { await installPackages(context, projectDirectory); } if (!context.dryRun) { if (!context.skipGit) { await initializeGitRepository(projectDirectory); await createGitIgnoreFile(projectDirectory); } if (context.observe) { printObservabilityNextSteps(); } printCollective(); } process.exit(0); } } const getProjectDirectory = (context) => { return (context.directory || normalizeToKebabOrSnakeCase(context.name)); }; const askForMissingInformation = async (context) => { console.info(MESSAGES.PROJECT_INFORMATION_START); console.info(); if (!context.name) { const message = MESSAGES.PROJECT_NAME_QUESTION; const question = generateInput('name', message)('nest-app'); context.name = (await input(question).catch(gracefullyExitOnPromptError)); } if (!context.packageManager) { context.packageManager = (await askForPackageManager()); } if (context.observe === undefined) { context.observe = await askForObservability(); } }; const generateApplicationFiles = async (context) => { const collection = CollectionFactory.create(context.collection || Collection.NESTJS); const schematicOptions = mapContextToSchematicOptions(context); await collection.execute('application', schematicOptions); console.info(); }; const mapContextToSchematicOptions = (context) => { const options = []; if (context.name !== undefined) options.push(new SchematicOption('name', context.name)); if (context.directory !== undefined) options.push(new SchematicOption('directory', context.directory)); if (context.dryRun) options.push(new SchematicOption('dry-run', true)); options.push(new SchematicOption('skip-git', context.skipGit)); options.push(new SchematicOption('strict', context.strict)); if (context.skipTests) { options.push(new SchematicOption('spec', false)); } if (context.packageManager !== undefined) options.push(new SchematicOption('packageManager', context.packageManager)); if (context.collection !== undefined) options.push(new SchematicOption('collection', context.collection)); options.push(new SchematicOption('language', context.language)); options.push(new SchematicOption('format', context.format)); if (context.observe) { options.push(new SchematicOption('observe', true)); } // note: skip-install is intentionally excluded — not sent to schematics return options; }; const installPackages = async (context, installDirectory) => { const inputPackageManager = context.packageManager; let packageManager; if (context.dryRun) { console.info(); console.info(ansis.green(MESSAGES.DRY_RUN_MODE)); console.info(); return; } try { packageManager = PackageManagerFactory.create(inputPackageManager); await packageManager.install(installDirectory, inputPackageManager); } catch (error) { if (error instanceof Error) { console.error(ansis.red(error.message)); } } }; const askForObservability = async () => { // The prompt below defaults to yes, but that only holds where someone is // there to say no. Without a TTY (CI, piped input, `execSync`) nobody // consented to the extra dependency, so stay off rather than block or assume. if (!isInteractive()) { return false; } return (await confirm({ message: MESSAGES.OBSERVABILITY_QUESTION, default: true, }).catch(gracefullyExitOnPromptError)); }; const askForPackageManager = async () => { const question = generateSelect('packageManager')(MESSAGES.PACKAGE_MANAGER_QUESTION)([ PackageManager.NPM, PackageManager.YARN, PackageManager.PNPM, PackageManager.BUN, ]); return select(question).catch(gracefullyExitOnPromptError); }; const initializeGitRepository = async (dir) => { const runner = new GitRunner(); await runner.run('init', true, join(process.cwd(), dir)).catch(() => { console.error(ansis.red(MESSAGES.GIT_INITIALIZATION_ERROR)); }); }; /** * Write a file `.gitignore` in the root of the newly created project. * `.gitignore` available in `@nestjs/schematics` cannot be published to * NPM (needs to be investigated). * * @param dir Relative path to the project. * @param content (optional) Content written in the `.gitignore`. * * @return Resolves when succeeds, or rejects with any error from `fn.writeFile`. */ const createGitIgnoreFile = (dir, content) => { const fileContent = content || defaultGitIgnore; const filePath = join(process.cwd(), dir, '.gitignore'); if (fileExists(filePath)) { return; } return fs.promises.writeFile(filePath, fileContent); }; const printObservabilityNextSteps = () => { console.info(); console.info(MESSAGES.OBSERVABILITY_NEXT_STEPS); console.info(); console.info(ansis.underline(MESSAGES.OBSERVABILITY_SIGN_UP_URL)); console.info(ansis.gray(MESSAGES.OBSERVABILITY_KEYS_HINT)); console.info(); }; const printCollective = () => { const dim = print('dim'); const yellow = print('yellow'); const emptyLine = print(); emptyLine(); yellow(`Thanks for installing Nest ${EMOJIS.PRAY}`); dim('Please consider donating to our open collective'); dim('to help us maintain this package.'); emptyLine(); emptyLine(); print()(`${ansis.bold `${EMOJIS.WINE} Donate:`} ${ansis.underline('https://opencollective.com/nest')}`); emptyLine(); }; const print = (color = null) => (str = '') => { const terminalCols = retrieveCols(); // eslint-disable-next-line no-control-regex const strLength = str.replace(/\x1b\[[0-9]+m/g, '').length; const leftPaddingLength = Math.floor((terminalCols - strLength) / 2); const leftPadding = ' '.repeat(Math.max(leftPaddingLength, 0)); if (color) { str = ansis[color](str); } console.log(leftPadding, str); }; export const retrieveCols = () => { const defaultCols = 80; // Prefer process.stdout.columns: it works on every platform (including // Windows, where `tput` is not available by default) and reflects the // actual terminal size instead of always falling back to the default. const stdoutCols = process.stdout.columns; if (typeof stdoutCols === 'number' && stdoutCols > 0) { return stdoutCols; } try { const terminalCols = execSync('tput cols', { stdio: ['pipe', 'pipe', 'ignore'], }); return parseInt(terminalCols.toString(), 10) || defaultCols; } catch { return defaultCols; } }; const fileExists = (path) => fs.existsSync(path); export const exit = () => process.exit(1);