diff --git a/packages/cli/create-strapi-app/src/create-strapi.ts b/packages/cli/create-strapi-app/src/create-strapi.ts index caf93aaf30..96613c1805 100644 --- a/packages/cli/create-strapi-app/src/create-strapi.ts +++ b/packages/cli/create-strapi-app/src/create-strapi.ts @@ -4,7 +4,6 @@ import chalk from 'chalk'; import execa from 'execa'; import fse from 'fs-extra'; -import { stopProcess } from './utils/stop-process'; import { copyTemplate } from './utils/template'; import { tryGitInit } from './utils/git'; import { trackUsage } from './utils/usage'; @@ -13,6 +12,7 @@ import { generateDotEnv } from './utils/dot-env'; import { isStderrError } from './types'; import type { Scope } from './types'; +import { logger } from './utils/logger'; async function createStrapi(scope: Scope) { const { rootPath } = scope; @@ -41,7 +41,7 @@ async function createApp(scope: Scope) { await trackUsage({ event: 'willCreateProject', scope }); - console.log(`Creating a new Strapi application at ${chalk.green(rootPath)}.`); + logger.title('Strapi', `Creating a new application at ${chalk.green(rootPath)}`); if (!isQuickstart) { await trackUsage({ event: 'didChooseCustomDatabase', scope }); @@ -62,19 +62,21 @@ async function createApp(scope: Scope) { } } else { try { - console.log(`Using template: ${chalk.green(template)}`); + logger.info(`${chalk.cyan('Installing template')} ${template}`); + await copyTemplate(scope, rootPath); - console.log('Template copied successfully.'); + + logger.success('Template copied successfully.'); } catch (error) { if (error instanceof Error) { - throw new Error(`⛔️ Template installation failed: ${error.message}`); + logger.fatal(`Template installation failed: ${error.message}`); } throw error; } if (!fse.existsSync(join(rootPath, 'package.json'))) { - throw new Error('Missing package.json in template'); + logger.fatal(`Missing ${chalk.bold('package.json')} in template`); } } @@ -99,13 +101,15 @@ async function createApp(scope: Scope) { if (installDependencies) { try { + logger.title('deps', `Installing dependencies with ${chalk.cyan(packageManager)}`); + await trackUsage({ event: 'willInstallProjectDependencies', scope }); - console.log(`Installing dependencies with ${chalk.bold(packageManager)}\n`); await runInstall(scope); - console.log(`Dependencies installed ${chalk.green('successfully')}.`); await trackUsage({ event: 'didInstallProjectDependencies', scope }); + + logger.success(`Dependencies installed`); } catch (error) { const stderr = isStderrError(error) ? error.stderr : ''; @@ -115,20 +119,17 @@ async function createApp(scope: Scope) { error: stderr.slice(-1024), }); - console.log( + logger.fatal([ chalk.bold( - 'Oh, it seems that you encountered errors while installing dependencies in your project.' - ) - ); - console.log(`Don't give up, your project was created correctly.`); - console.log( - `Fix the issues mentioned in the installation errors and try to run the following command` - ); - console.log(); - console.log(`cd ${chalk.green(rootPath)} && ${chalk.cyan(packageManager)} install`); - console.log(); - - stopProcess(); + 'Oh, it seems that you encountered an error while installing dependencies in your project' + ), + '', + `Don't give up, your project was created correctly`, + '', + `Fix the issues mentioned in the installation errors and try to run the following command:`, + '', + `cd ${chalk.green(rootPath)} && ${chalk.cyan(packageManager)} install`, + ]); } } @@ -136,54 +137,48 @@ async function createApp(scope: Scope) { // Init git if (gitInit) { - console.log('Initializing git repository.'); + logger.title('git', 'Initializing git repository.'); await tryGitInit(rootPath); - console.log('Initialized a git repository.'); - console.log(); + logger.success('Initialized a git repository.'); } - console.log(); - console.log(`Your application was created at ${chalk.green(rootPath)}.\n`); + logger.title('Strapi', `Your application was created!`); const cmd = chalk.cyan(`${packageManager} run`); - console.log('Available commands in your project:'); - console.log(); - console.log(` ${cmd} develop`); - console.log( - ' Start Strapi in watch mode. (Changes in Strapi project files will trigger a server restart)' - ); - console.log(); - console.log(` ${cmd} start`); - console.log(' Start Strapi without watch mode.'); - console.log(); - console.log(` ${cmd} build`); - console.log(' Build Strapi admin panel.'); - console.log(); - console.log(` ${cmd} deploy`); - console.log(' Deploy Strapi project.'); - console.log(); - console.log(` ${cmd} strapi`); - console.log(` Display all available commands.`); - console.log(); + logger.log([ + 'Available commands in your project:', + '', + 'Start Strapi in watch mode. (Changes in Strapi project files will trigger a server restart)', + `${cmd} develop`, + '', + 'Start Strapi without watch mode.', + `${cmd} start`, + '', + 'Build Strapi admin panel.', + `${cmd} build`, + '', + 'Deploy Strapi project.', + `${cmd} deploy`, + '', + 'Display all available commands.', + `${cmd} strapi\n`, + ]); if (installDependencies) { - console.log('You can start by doing:'); - console.log(); - console.log(` ${chalk.cyan('cd')} ${rootPath}`); - console.log(` ${cmd} develop`); - console.log(); + logger.log(['To get start run', '', `${chalk.cyan('cd')} ${rootPath}`, `${cmd} develop`]); } else { - console.log('You can start by doing:'); - console.log(); - console.log(` ${chalk.cyan('cd')} ${rootPath}`); - console.log(` ${chalk.cyan(packageManager)} install`); - console.log(` ${cmd} develop`); - console.log(); + logger.log([ + 'To get start run', + '', + `${chalk.cyan('cd')} ${rootPath}`, + `${chalk.cyan(packageManager)} install`, + `${cmd} develop`, + ]); } if (runApp && installDependencies) { - console.log(`Running your Strapi application.`); + logger.title('Starting', 'Running your Strapi application'); try { await trackUsage({ event: 'willStartServer', scope }); @@ -204,7 +199,7 @@ async function createApp(scope: Scope) { }); } - stopProcess(); + logger.fatal('Failed to start your Strapi application'); } } } @@ -231,7 +226,9 @@ function runInstall({ rootPath, packageManager }: Scope) { installArguments.push(...(installArgumentsMap[packageManager] ?? [])); } - return execa(packageManager, installArguments, options); + const proc = execa(packageManager, installArguments, options); + + return proc; } export { createStrapi }; diff --git a/packages/cli/create-strapi-app/src/index.ts b/packages/cli/create-strapi-app/src/index.ts index 04d59e23c0..44b1bf0f5a 100644 --- a/packages/cli/create-strapi-app/src/index.ts +++ b/packages/cli/create-strapi-app/src/index.ts @@ -15,7 +15,7 @@ import { trackError } from './utils/usage'; import { addDatabaseDependencies, getDatabaseInfos } from './utils/database'; import type { Options, Scope } from './types'; -import { stopProcess } from './utils/stop-process'; +import { logger } from './utils/logger'; const command = new commander.Command('create-strapi-app') .version(version) @@ -69,18 +69,49 @@ async function run(args: string[]): Promise { const options = command.parse(args).opts(); const directory = command.args[0]; - console.log( - `\n${chalk.bgBlueBright(` ${chalk.black('Strapi')} `)} ${chalk.green( - chalk.bold(`v${version}`) - )} ${chalk.bold("🚀 Let's create your new project")}` + logger.title( + 'Strapi', + `${chalk.green(chalk.bold(`v${version}`))} ${chalk.bold("🚀 Let's create your new project")}\n` ); - checkNodeRequirements(); + if ( + (options.javascript !== undefined || options.typescript !== undefined) && + options.template !== undefined + ) { + logger.fatal( + `You cannot use ${chalk.bold('--javascript')} or ${chalk.bold('--typescript')} with ${chalk.bold('--template')}` + ); + } + + if (options.javascript === true && options.typescript === true) { + logger.fatal( + `You cannot use both ${chalk.bold('--typescript')} (--ts) and ${chalk.bold('--javascript')} (--js) flags together` + ); + } + + // Only prompt the example app option if there is no template option + if (options.example === true && options.template !== undefined) { + logger.fatal(`You cannot use ${chalk.bold('--example')} with ${chalk.bold('--template')}`); + } + + if (options.template !== undefined && options.template.startsWith('-')) { + logger.fatal(`Template name ${chalk.bold(`"${options.template}"`)} is invalid`); + } + + if ([options.useNpm, options.usePnpm, options.useYarn].filter(Boolean).length > 1) { + logger.fatal( + `You cannot specify multiple package managers at the same time ${chalk.bold('(--use-npm, --use-pnpm, --use-yarn)')}` + ); + } if (options.quickstart && !directory) { - stopProcess('Please specify the of your project when using --quickstart'); + logger.fatal( + `Please specify the ${chalk.bold('')} of your project when using ${chalk.bold('--quickstart')}` + ); } + checkNodeRequirements(); + const appDirectory = directory || (await prompts.directory()); const rootPath = await checkInstallPath(appDirectory); @@ -123,27 +154,6 @@ async function run(args: string[]): Promise { }, }; - if ((options.javascript !== undefined || options.typescript) && options.template !== undefined) { - stopProcess('You cannot use --javascript or --typescript with --template'); - } - - if (options.javascript === true && options.typescript === true) { - stopProcess('You cannot use both --typescript (--ts) and --javascript (--js) flags together'); - } - - // Only prompt the example app option if there is no template option - if (options.example === true && options.template !== undefined) { - stopProcess('You cannot use --example with --template'); - } - - // Only prompt the example app option if there is no template option - if ( - (options.javascript === true || options.typescript === true) && - options.template !== undefined - ) { - stopProcess('You cannot use --javascript or --typescript with --template'); - } - if (options.template !== undefined) { scope.useExampleApp = false; } else if (options.example === true || options.quickstart) { @@ -199,17 +209,11 @@ async function run(args: string[]): Promise { await trackError({ scope, error }); - stopProcess(`Error: ${error.message}`); + logger.fatal(error.message); } } function getPkgManager(options: Options) { - if ([options.useNpm, options.usePnpm, options.useYarn].filter(Boolean).length > 1) { - stopProcess( - 'You cannot specify multiple package managers at the same time (--use-npm, --use-pnpm, --use-yarn)' - ); - } - if (options.useNpm === true) { return 'npm'; } diff --git a/packages/cli/create-strapi-app/src/utils/check-install-path.ts b/packages/cli/create-strapi-app/src/utils/check-install-path.ts index 468bfa2405..cdd1f0222a 100644 --- a/packages/cli/create-strapi-app/src/utils/check-install-path.ts +++ b/packages/cli/create-strapi-app/src/utils/check-install-path.ts @@ -1,23 +1,18 @@ import { resolve } from 'node:path'; import chalk from 'chalk'; import fse from 'fs-extra'; - -import { stopProcess } from './stop-process'; +import { logger } from './logger'; // Checks if the an empty directory exists at rootPath export async function checkInstallPath(directory: string): Promise { - if (!directory) { - stopProcess(`⛔️ Please provide a project name.`); - } - const rootPath = resolve(directory); if (await fse.pathExists(rootPath)) { const stat = await fse.stat(rootPath); if (!stat.isDirectory()) { - stopProcess( - `⛔️ ${chalk.green( + logger.fatal( + `${chalk.green( rootPath )} is not a directory. Make sure to create a Strapi application in an empty directory.` ); @@ -25,11 +20,10 @@ export async function checkInstallPath(directory: string): Promise { const files = await fse.readdir(rootPath); if (files.length > 1) { - stopProcess( - `⛔️ You can only create a Strapi app in an empty directory.\nMake sure ${chalk.green( - rootPath - )} is empty.` - ); + logger.fatal([ + 'You can only create a Strapi app in an empty directory', + `Make sure ${chalk.green(rootPath)} is empty.`, + ]); } } diff --git a/packages/cli/create-strapi-app/src/utils/check-requirements.ts b/packages/cli/create-strapi-app/src/utils/check-requirements.ts index 3c4800ba48..9061eb9890 100644 --- a/packages/cli/create-strapi-app/src/utils/check-requirements.ts +++ b/packages/cli/create-strapi-app/src/utils/check-requirements.ts @@ -2,26 +2,25 @@ import chalk from 'chalk'; import semver from 'semver'; import { engines } from './engines'; -import { stopProcess } from './stop-process'; +import { logger } from './logger'; export function checkNodeRequirements() { const currentNodeVersion = process.versions.node; // error if the node version isn't supported if (!semver.satisfies(currentNodeVersion, engines.node)) { - console.error(chalk.red(`You are running ${chalk.bold(`Node.js ${currentNodeVersion}`)}`)); - console.error(`Strapi requires ${chalk.bold(chalk.green(`Node.js ${engines.node}`))}`); - console.error('Please make sure to use the right version of Node.'); - stopProcess(); + logger.fatal([ + chalk.red(`You are running ${chalk.bold(`Node.js ${currentNodeVersion}`)}`), + `Strapi requires ${chalk.bold(chalk.green(`Node.js ${engines.node}`))}`, + 'Please make sure to use the right version of Node.', + ]); } // warn if not using a LTS version else if (semver.major(currentNodeVersion) % 2 !== 0) { - console.warn(chalk.yellow(`You are running ${chalk.bold(`Node.js ${currentNodeVersion}`)}`)); - console.warn( - `Strapi only supports ${chalk.bold( - chalk.green('LTS versions of Node.js') - )}, other versions may not be compatible.` - ); + logger.warn([ + chalk.yellow(`You are running ${chalk.bold(`Node.js ${currentNodeVersion}`)}`), + `Strapi only supports ${chalk.bold(chalk.green('LTS versions of Node.js'))}, other versions may not be compatible.`, + ]); } } diff --git a/packages/cli/create-strapi-app/src/utils/database.ts b/packages/cli/create-strapi-app/src/utils/database.ts index fb8fa1868f..41cfd05274 100644 --- a/packages/cli/create-strapi-app/src/utils/database.ts +++ b/packages/cli/create-strapi-app/src/utils/database.ts @@ -2,7 +2,7 @@ import inquirer from 'inquirer'; import type { Question } from 'inquirer'; import type { Scope, Options, DBClient, DBConfig } from '../types'; -import { stopProcess } from './stop-process'; +import { logger } from './logger'; const DBOptions = ['dbclient', 'dbhost', 'dbport', 'dbname', 'dbusername', 'dbpassword']; @@ -55,7 +55,7 @@ export async function getDatabaseInfos(options: Options): Promise { } if (options.dbclient && !VALID_CLIENTS.includes(options.dbclient)) { - stopProcess( + logger.fatal( `Invalid --dbclient: ${options.dbclient}, expected one of ${VALID_CLIENTS.join(', ')}` ); } @@ -68,7 +68,7 @@ export async function getDatabaseInfos(options: Options): Promise { matchingArgs.length !== DBOptions.length && options.dbclient !== 'sqlite' ) { - stopProcess(`Required database arguments are missing: ${missingArgs.join(', ')}.`); + logger.fatal(`Required database arguments are missing: ${missingArgs.join(', ')}.`); } const hasDBOptions = DBOptions.some((key) => key in options); @@ -82,7 +82,7 @@ export async function getDatabaseInfos(options: Options): Promise { } if (!options.dbclient) { - stopProcess('Please specify the database client'); + return logger.fatal('Please specify the database client'); } const database: DBConfig = { diff --git a/packages/cli/create-strapi-app/src/utils/logger.ts b/packages/cli/create-strapi-app/src/utils/logger.ts new file mode 100644 index 0000000000..b388a0dc0b --- /dev/null +++ b/packages/cli/create-strapi-app/src/utils/logger.ts @@ -0,0 +1,61 @@ +import chalk from 'chalk'; +import type { ChalkFunction } from 'chalk'; + +const MAX_PREFIX_LENGTH = 8; + +const badge = (text: string, bgColor: ChalkFunction, textColor: ChalkFunction = chalk.black) => { + const wrappedText = ` ${text} `; + + return ' '.repeat(MAX_PREFIX_LENGTH - wrappedText.length) + bgColor(textColor(wrappedText)); +}; + +const textIndent = ( + text: string | string[], + indentFirst = true, + indent: number = MAX_PREFIX_LENGTH + 2 +) => { + const parts = Array.isArray(text) ? text : [text]; + + return parts + .map((part, i) => { + if (i === 0 && !indentFirst) { + return part; + } + + return ' '.repeat(indent) + part; + }) + .join('\n'); +}; + +export const logger = { + log(message: string | string[]): void { + console.log(textIndent(message)); + }, + title(title: string, message: string): void { + const prefix = badge(title, chalk.bgBlueBright); + console.log(`\n${prefix} ${message}`); + }, + info(message: string): void { + console.log(`${' '.repeat(7)}${chalk.cyan('●')} ${message}`); + }, + success(message: string): void { + console.log(`\n${' '.repeat(7)}${chalk.green('✓')} ${chalk.green(message)}`); + }, + fatal(message?: string | string[]): never { + const prefix = badge('Error', chalk.bgRed); + + if (message) { + console.error(`\n${prefix} ${textIndent(message, false)}\n`); + } + + process.exit(1); + }, + error(message: string | string[]): void { + const prefix = badge('Error', chalk.bgRed); + console.error(`\n${prefix} ${textIndent(message, false)}\n`); + }, + warn(message: string | string[]): void { + const prefix = badge('Warn', chalk.bgYellow); + console.warn(`\n${prefix} ${textIndent(message, false)}\n`); + }, +}; diff --git a/packages/cli/create-strapi-app/src/utils/stop-process.ts b/packages/cli/create-strapi-app/src/utils/stop-process.ts deleted file mode 100644 index 443bbdce97..0000000000 --- a/packages/cli/create-strapi-app/src/utils/stop-process.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function stopProcess(message?: string): never { - if (message) { - console.error(message); - } - - process.exit(1); -}