#!/usr/bin/env node /* eslint-disable import/no-extraneous-dependencies */ import { basename, resolve } from 'node:path' import { existsSync } from 'node:fs' import { cyan, green, red, yellow, bold, blue } from 'picocolors' import { Command } from 'commander' import Conf from 'conf' import prompts from 'prompts' import type { InitialReturnValue } from 'prompts' import checkForUpdate from 'update-check' import { createApp, DownloadError } from './create-app' import { getPkgManager } from './helpers/get-pkg-manager' import { validateNpmName } from './helpers/validate-pkg' import packageJson from './package.json' import ciInfo from 'ci-info' import { isFolderEmpty } from './helpers/is-folder-empty' let projectPath: string = '' const handleSigTerm = () => process.exit(0) process.on('SIGINT', handleSigTerm) process.on('SIGTERM', handleSigTerm) const onPromptState = (state: { value: InitialReturnValue aborted: boolean exited: boolean }) => { if (state.aborted) { // If we don't re-enable the terminal cursor before exiting // the program, the cursor will remain hidden process.stdout.write('\x1B[?25h') process.stdout.write('\n') process.exit(1) } } const program = new Command(packageJson.name) .version(packageJson.version) .argument('[project-directory]') .usage(`${green('[project-directory]')} [options]`) .action((name) => { projectPath = name }) .option( '--ts, --typescript', ` Initialize as a TypeScript project. (default) ` ) .option( '--js, --javascript', ` Initialize as a JavaScript project. ` ) .option( '--tailwind', ` Initialize with Tailwind CSS config. (default) ` ) .option( '--eslint', ` Initialize with eslint config. ` ) .option( '--app', ` Initialize as an App Router project. ` ) .option( '--src-dir', ` Initialize inside a \`src/\` directory. ` ) .option( '--turbo', ` Enable Turbopack by default for development. ` ) .option( '--import-alias ', ` Specify import alias to use (default "@/*"). ` ) .option( '--empty', ` Initialize an empty project. ` ) .option( '--use-npm', ` Explicitly tell the CLI to bootstrap the application using npm ` ) .option( '--use-pnpm', ` Explicitly tell the CLI to bootstrap the application using pnpm ` ) .option( '--use-yarn', ` Explicitly tell the CLI to bootstrap the application using Yarn ` ) .option( '--use-bun', ` Explicitly tell the CLI to bootstrap the application using Bun ` ) .option( '-e, --example [name]|[github-url]', ` An example to bootstrap the app with. You can use an example name from the official Next.js repo or a GitHub URL. The URL can use any branch and/or subdirectory ` ) .option( '--example-path ', ` In a rare case, your GitHub URL might contain a branch name with a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). In this case, you must specify the path to the example separately: --example-path foo/bar ` ) .option( '--reset-preferences', ` Explicitly tell the CLI to reset any stored preferences ` ) .option( '--skip-install', ` Explicitly tell the CLI to skip installing packages ` ) .allowUnknownOption() .parse(process.argv) .opts() const packageManager = !!program.useNpm ? 'npm' : !!program.usePnpm ? 'pnpm' : !!program.useYarn ? 'yarn' : !!program.useBun ? 'bun' : getPkgManager() async function run(): Promise { const conf = new Conf({ projectName: 'create-next-app' }) if (program.resetPreferences) { conf.clear() console.log(`Preferences reset successfully`) return } if (typeof projectPath === 'string') { projectPath = projectPath.trim() } if (!projectPath) { const res = await prompts({ onState: onPromptState, type: 'text', name: 'path', message: 'What is your project named?', initial: 'my-app', validate: (name) => { const validation = validateNpmName(basename(resolve(name))) if (validation.valid) { return true } return 'Invalid project name: ' + validation.problems[0] }, }) if (typeof res.path === 'string') { projectPath = res.path.trim() } } if (!projectPath) { console.log( '\nPlease specify the project directory:\n' + ` ${cyan(program.name())} ${green('')}\n` + 'For example:\n' + ` ${cyan(program.name())} ${green('my-next-app')}\n\n` + `Run ${cyan(`${program.name()} --help`)} to see all options.` ) process.exit(1) } const resolvedProjectPath = resolve(projectPath) const projectName = basename(resolvedProjectPath) const validation = validateNpmName(projectName) if (!validation.valid) { console.error( `Could not create a project called ${red( `"${projectName}"` )} because of npm naming restrictions:` ) validation.problems.forEach((p) => console.error(` ${red(bold('*'))} ${p}`) ) process.exit(1) } if (program.example === true) { console.error( 'Please provide an example name or url, otherwise remove the example option.' ) process.exit(1) } /** * Verify the project dir is empty or doesn't exist */ const root = resolve(resolvedProjectPath) const appName = basename(root) const folderExists = existsSync(root) if (folderExists && !isFolderEmpty(root, appName)) { process.exit(1) } const example = typeof program.example === 'string' && program.example.trim() const preferences = (conf.get('preferences') || {}) as Record< string, boolean | string > /** * If the user does not provide the necessary flags, prompt them for whether * to use TS or JS. */ if (!example) { const defaults: typeof preferences = { typescript: true, eslint: true, tailwind: true, app: true, srcDir: false, importAlias: '@/*', customizeImportAlias: false, empty: false, turbo: false, } const getPrefOrDefault = (field: string) => preferences[field] ?? defaults[field] if (!program.typescript && !program.javascript) { if (ciInfo.isCI) { // default to TypeScript in CI as we can't prompt to // prevent breaking setup flows program.typescript = getPrefOrDefault('typescript') } else { const styledTypeScript = blue('TypeScript') const { typescript } = await prompts( { type: 'toggle', name: 'typescript', message: `Would you like to use ${styledTypeScript}?`, initial: getPrefOrDefault('typescript'), active: 'Yes', inactive: 'No', }, { /** * User inputs Ctrl+C or Ctrl+D to exit the prompt. We should close the * process and not write to the file system. */ onCancel: () => { console.error('Exiting.') process.exit(1) }, } ) /** * Depending on the prompt response, set the appropriate program flags. */ program.typescript = Boolean(typescript) program.javascript = !Boolean(typescript) preferences.typescript = Boolean(typescript) } } if ( !process.argv.includes('--eslint') && !process.argv.includes('--no-eslint') ) { if (ciInfo.isCI) { program.eslint = getPrefOrDefault('eslint') } else { const styledEslint = blue('ESLint') const { eslint } = await prompts({ onState: onPromptState, type: 'toggle', name: 'eslint', message: `Would you like to use ${styledEslint}?`, initial: getPrefOrDefault('eslint'), active: 'Yes', inactive: 'No', }) program.eslint = Boolean(eslint) preferences.eslint = Boolean(eslint) } } if ( !process.argv.includes('--tailwind') && !process.argv.includes('--no-tailwind') ) { if (ciInfo.isCI) { program.tailwind = getPrefOrDefault('tailwind') } else { const tw = blue('Tailwind CSS') const { tailwind } = await prompts({ onState: onPromptState, type: 'toggle', name: 'tailwind', message: `Would you like to use ${tw}?`, initial: getPrefOrDefault('tailwind'), active: 'Yes', inactive: 'No', }) program.tailwind = Boolean(tailwind) preferences.tailwind = Boolean(tailwind) } } if ( !process.argv.includes('--src-dir') && !process.argv.includes('--no-src-dir') ) { if (ciInfo.isCI) { program.srcDir = getPrefOrDefault('srcDir') } else { const styledSrcDir = blue('`src/` directory') const { srcDir } = await prompts({ onState: onPromptState, type: 'toggle', name: 'srcDir', message: `Would you like your code inside a ${styledSrcDir}?`, initial: getPrefOrDefault('srcDir'), active: 'Yes', inactive: 'No', }) program.srcDir = Boolean(srcDir) preferences.srcDir = Boolean(srcDir) } } if (!process.argv.includes('--app') && !process.argv.includes('--no-app')) { if (ciInfo.isCI) { program.app = getPrefOrDefault('app') } else { const styledAppDir = blue('App Router') const { appRouter } = await prompts({ onState: onPromptState, type: 'toggle', name: 'appRouter', message: `Would you like to use ${styledAppDir}? (recommended)`, initial: getPrefOrDefault('app'), active: 'Yes', inactive: 'No', }) program.app = Boolean(appRouter) } } if (!program.turbo && !process.argv.includes('--no-turbo')) { if (ciInfo.isCI) { program.turbo = getPrefOrDefault('turbo') } else { const styledTurbo = blue('Turbopack') const { turbo } = await prompts({ onState: onPromptState, type: 'toggle', name: 'turbo', message: `Would you like to use ${styledTurbo} for ${`next dev`}?`, initial: getPrefOrDefault('turbo'), active: 'Yes', inactive: 'No', }) program.turbo = Boolean(turbo) preferences.turbo = Boolean(turbo) } } const importAliasPattern = /^[^*"]+\/\*\s*$/ if ( typeof program.importAlias !== 'string' || !importAliasPattern.test(program.importAlias) ) { if (ciInfo.isCI) { // We don't use preferences here because the default value is @/* regardless of existing preferences program.importAlias = defaults.importAlias } else if (process.argv.includes('--no-import-alias')) { program.importAlias = defaults.importAlias } else { const styledImportAlias = blue('import alias') const { customizeImportAlias } = await prompts({ onState: onPromptState, type: 'toggle', name: 'customizeImportAlias', message: `Would you like to customize the ${styledImportAlias} (${defaults.importAlias} by default)?`, initial: getPrefOrDefault('customizeImportAlias'), active: 'Yes', inactive: 'No', }) if (!customizeImportAlias) { // We don't use preferences here because the default value is @/* regardless of existing preferences program.importAlias = defaults.importAlias } else { const { importAlias } = await prompts({ onState: onPromptState, type: 'text', name: 'importAlias', message: `What ${styledImportAlias} would you like configured?`, initial: getPrefOrDefault('importAlias'), validate: (value) => importAliasPattern.test(value) ? true : 'Import alias must follow the pattern /*', }) program.importAlias = importAlias preferences.importAlias = importAlias } } } } try { await createApp({ appPath: resolvedProjectPath, packageManager, example: example && example !== 'default' ? example : undefined, examplePath: program.examplePath, typescript: program.typescript, tailwind: program.tailwind, eslint: program.eslint, appRouter: program.app, srcDir: program.srcDir, importAlias: program.importAlias, skipInstall: program.skipInstall, empty: program.empty, turbo: program.turbo, }) } catch (reason) { if (!(reason instanceof DownloadError)) { throw reason } const res = await prompts({ onState: onPromptState, type: 'confirm', name: 'builtin', message: `Could not download "${example}" because of a connectivity issue between your machine and GitHub.\n` + `Do you want to use the default template instead?`, initial: true, }) if (!res.builtin) { throw reason } await createApp({ appPath: resolvedProjectPath, packageManager, typescript: program.typescript, eslint: program.eslint, tailwind: program.tailwind, appRouter: program.app, srcDir: program.srcDir, importAlias: program.importAlias, skipInstall: program.skipInstall, empty: program.empty, turbo: program.turbo, }) } conf.set('preferences', preferences) } const update = checkForUpdate(packageJson).catch(() => null) async function notifyUpdate(): Promise { try { const res = await update if (res?.latest) { const updateMessage = packageManager === 'yarn' ? 'yarn global add create-next-app' : packageManager === 'pnpm' ? 'pnpm add -g create-next-app' : packageManager === 'bun' ? 'bun add -g create-next-app' : 'npm i -g create-next-app' console.log( yellow(bold('A new version of `create-next-app` is available!')) + '\n' + 'You can update by running: ' + cyan(updateMessage) + '\n' ) } process.exit() } catch { // ignore error } } run() .then(notifyUpdate) .catch(async (reason) => { console.log() console.log('Aborting installation.') if (reason.command) { console.log(` ${cyan(reason.command)} has failed.`) } else { console.log( red('Unexpected error. Please report it as a bug:') + '\n', reason ) } console.log() await notifyUpdate() process.exit(1) })