@nestjs/cli
Version:
Nest - modern, fast, powerful node.js web framework (@cli)
203 lines (202 loc) • 8.11 kB
JavaScript
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);