Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

Create Next App

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

create-next-app serves as the official scaffolding utility for initializing Next.js applications, automating the end-to-end setup process from command-line parsing to a ready-to-run repository packages/create-next-app/index.ts, packages/create-next-app/create-app.ts. By combining interactive prompts with declarative CLI flags via commander and prompts, it resolves project configurations for TypeScript or JavaScript, Tailwind CSS, App or Pages routers, and alternative bundlers like Turbopack and Rspack packages/create-next-app/index.ts. The orchestrator validates target directories, pulls template structures or remote GitHub examples, executes package installations across npm, pnpm, yarn, or bun, and finalizes the workspace with optional Git repository initialization packages/create-next-app/create-app.ts. Sources: packages/create-next-app/index.ts:4-10, packages/create-next-app/create-app.ts:28-70

CLI Entry Point and Interactive Prompts

Argument Parsing and Command Configuration

The entry point script packages/create-next-app/index.ts executes as a Node.js CLI tool via the shebang #!/usr/bin/env node. It initializes command-line parsing using commander configured with the package name and version from packages/create-next-app/package.json. Signal handlers for SIGINT and SIGTERM invoke process.exit(0), while a prompt state handler monitors prompt cancellation to restore the terminal cursor using the ANSI escape sequence \x1B[?25h before terminating with process.exit(1).

#!/usr/bin/env node
/* eslint-disable import/no-extraneous-dependencies */
import ciInfo from 'ci-info'
import { Command } from 'commander'
import Conf from 'conf'
import { existsSync } from 'node:fs'
import { basename, resolve } from 'node:path'
import { blue, bold, cyan, green, red, yellow } from 'picocolors'
import type { InitialReturnValue } from 'prompts'
import prompts from 'prompts'
import updateCheck from 'update-check'
import { createApp, DownloadError } from './create-app'
import type { PackageManager } from './helpers/get-pkg-manager'
import { getPkgManager } from './helpers/get-pkg-manager'
import { isFolderEmpty } from './helpers/is-folder-empty'
import { validateNpmName } from './helpers/validate-pkg'
import packageJson from './package.json'
import { Bundler } from './templates'
 
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)
  }
}

Sources: packages/create-next-app/index.ts:1-39

The CLI defines a positional argument [directory] and accepts numerous boolean flags and options to configure scaffolding preferences without interactive prompts.

Option FlagDescription
-v, --versionOutput the current version of create-next-app
-h, --helpDisplay this help message
--ts, --typescriptInitialize as a TypeScript project (default)
--js, --javascriptInitialize as a JavaScript project
--tailwindInitialize with Tailwind CSS config (default)
--react-compilerInitialize with React Compiler enabled
--cache-componentsInitialize with Cache Components enabled
--eslintInitialize with ESLint config
--biomeInitialize with Biome config
--appInitialize as an App Router project
--src-dirInitialize inside a src/ directory
--rspackEnable Rspack as the bundler
--import-alias <prefix/*>Specify import alias to use (default @/*)
--apiInitialize a headless API using the App Router
--emptyInitialize an empty project
--use-npmBootstrap using npm
--use-pnpmBootstrap using pnpm
--use-yarnBootstrap using Yarn
--use-bunBootstrap using Bun
--reset, --reset-preferencesReset saved preferences
--skip-installSkip installing packages
--yesUse saved preferences or defaults for unprovided options
-e, --example <name|url>Bootstrap with an example name or public GitHub URL
--example-path <path>Specify path to example for complex URLs
--agents-mdInclude AGENTS.md to guide coding agents (default)
--agent-feedbackPrepare anonymized Next.js feedback
--no-agent-feedbackDo not prepare anonymized Next.js feedback
--disable-gitSkip initializing a git repository
const program = new Command(packageJson.name)
  .version(
    packageJson.version,
    '-v, --version',
    'Output the current version of create-next-app.'
  )
  .argument('[directory]')
  .usage('[directory] [options]')
  .helpOption('-h, --help', 'Display this help message.')
  .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('--react-compiler', 'Initialize with React Compiler enabled.')
  .option('--cache-components', 'Initialize with Cache Components enabled.')
  .option('--eslint', 'Initialize with ESLint config.')
  .option('--biome', 'Initialize with Biome config.')
  .option('--app', 'Initialize as an App Router project.')
  .option('--src-dir', "Initialize inside a 'src/' directory.")
  .option('--rspack', 'Enable Rspack as the bundler.')
  .option(
    '--import-alias <prefix/*>',
    'Specify import alias to use (default "@/*").'
  )
  .option('--api', 'Initialize a headless API using the App Router.')
  .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(
    '--reset, --reset-preferences',
    'Reset the preferences saved for create-next-app.'
  )
  .option(
    '--skip-install',
    'Explicitly tell the CLI to skip installing packages.'
  )
  .option('--yes', 'Use saved preferences or defaults for unprovided options.')
  .option(
    '-e, --example <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 public GitHub URL. The URL can use
  any branch and/or subdirectory.
`
  )
  .option(
    '--example-path <path-to-example>',
    `
 
  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(
    '--agents-md',
    'Include AGENTS.md to guide coding agents to write up-to-date Next.js code. (default)'
  )
  .option(
    '--agent-feedback',
    'Prepare anonymized Next.js feedback for your review.'
  )
  .option(
    '--no-agent-feedback',
    'Do not prepare anonymized Next.js feedback for your review.'

Sources: packages/create-next-app/index.ts:41-133

Note

Commander does not natively support negated flags like --no-agent-feedback. The action handler explicitly checks if positional arguments start with --no- to correctly capture user intent without treating negated options as project directory names.

.action((name) => {
  // Commander does not implicitly support negated options. When they are used
  // by the user they will be interpreted as the positional argument (name) in
  // the action handler. See https://github.com/tj/commander.js/pull/1355
  if (name && !name.startsWith('--no-')) {
    projectPath = name
  }
})

Sources: packages/create-next-app/index.ts:123-130

Package Manager Resolution and Preference Management

The package manager is determined by inspecting explicit CLI flags (--use-npm, --use-pnpm, --use-yarn, or --use-bun) in order. If none of these flags are provided, the CLI falls back to detecting the invoking package manager via getPkgManager().

const packageManager: PackageManager = !!opts.useNpm
  ? 'npm'
  : !!opts.usePnpm
    ? 'pnpm'
    : !!opts.useYarn
      ? 'yarn'
      : !!opts.useBun
        ? 'bun'
        : getPkgManager()
 

Sources: packages/create-next-app/index.ts:137-146

The run asynchronous function initializes a Conf instance with projectName: 'create-next-app' to persist user preferences across executions. If --reset or --reset-preferences is passed, an interactive toggle prompt asks the user whether to clear stored configurations, calling conf.clear() upon confirmation.

async function run(): Promise<void> {
  const conf = new Conf({ projectName: 'create-next-app' })
 
  if (opts.resetPreferences) {
    const { resetPreferences } = await prompts({
      onState: onPromptState,
      type: 'toggle',
      name: 'resetPreferences',
      message: 'Would you like to reset the saved preferences?',
      initial: false,
      active: 'Yes',
      inactive: 'No',
    })
    if (resetPreferences) {
      conf.clear()
      console.log('The preferences have been reset successfully!')
    }
    process.exit(0)
  }

Sources: packages/create-next-app/index.ts:147-165

If no project path is provided via command-line arguments, prompts requests a project name interactively, validating input directory names using validateNpmName from packages/create-next-app/helpers/validate-pkg.ts alongside validate-npm-package-name.

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()
  }
}

Sources: packages/create-next-app/index.ts:167-190

export function validateNpmName(name: string): ValidateNpmNameResult {
  const nameValidation = validateProjectName(name)
  if (nameValidation.validForNewPackages) {
    return { valid: true }
  }
 
  return {
    valid: false,
    problems: [
      ...(nameValidation.errors || []),
      ...(nameValidation.warnings || []),
    ],
  }
}

Sources: packages/create-next-app/helpers/validate-pkg.ts:13-26

Directory Validation and Preflight Checks

Naming Verification and Destination Resolution

Before scaffolding begins, create-next-app evaluates the target project path and package identity to verify compatibility with npm naming conventions. The application extracts the base directory name from the resolved path and invokes validateNpmName, which delegates package name evaluation to validate-npm-package-name. If validForNewPackages is false, the CLI gathers all validation errors and warnings into a problem array, prints detailed error diagnostics, and exits with status code 1.

export function validateNpmName(name: string): ValidateNpmNameResult {
  const nameValidation = validateProjectName(name)
  if (nameValidation.validForNewPackages) {
    return { valid: true }
  }
 
  return {
    valid: false,
    problems: [
      ...(nameValidation.errors || []),
      ...(nameValidation.warnings || []),
    ],
  }
}

Sources: packages/create-next-app/helpers/validate-pkg.ts:13-26

Warning

If a user passes an empty project path argument or omits it entirely without answering the interactive prompt, the CLI terminates immediately after displaying usage instructions via opts.name() rather than proceeding with an undefined or current working directory target.

if (!projectPath) {
  console.log(
    '\nPlease specify the project directory:\n' +
      `  ${cyan(opts.name())} ${green('<project-directory>')}\n` +
      'For example:\n' +
      `  ${cyan(opts.name())} ${green('my-next-app')}\n\n` +
      `Run ${cyan(`${opts.name()} --help`)} to see all options.`
  )
  process.exit(1)
}

Sources: packages/create-next-app/index.ts:192-201

Write Permissions and Folder Emptiness Checks

Once the project path is resolved, createApp validates that the parent directory of the target root is writable by testing W_OK access via node:fs/promises. If the parent directory lacks write permissions, an error message is written to stderr and the process exits.

export async function isWriteable(directory: string): Promise<boolean> {
  try {
    await access(directory, W_OK)
    return true
  } catch (err) {
    return false
  }
}

Sources: packages/create-next-app/helpers/is-writeable.ts:4-11

The CLI recursively creates the destination directory using mkdirSync(root, { recursive: true }) and inspects its contents with isFolderEmpty. This helper function reads the directory contents and filters out standard non-conflicting configuration artifacts, temporary files, and editor directories.

File / Folder PatternStatusPurpose / Rationale
.claude, .cursor, .vscode, .zedWhitelistedAI coding assistant and editor configuration directories
.git, .hg, .hgcheck, .hgignoreWhitelistedVersion control directories and ignore rules
.DS_Store, Thumbs.bg, Thumbs.dbWhitelistedOperating system thumbnail and metadata files
.gitignore, .gitattributes, .npmignoreWhitelistedGit and npm tracking exclusion rules
LICENSE, docs, mkdocs.ymlWhitelistedDocumentation and licensing assets
npm-debug.log, yarn-debug.log, yarn-error.logWhitelistedPackage manager diagnostic and error logs
yarnrc.yml, .yarnWhitelistedYarn workspace configuration and cache folders
*.imlWhitelisted (Regex)IntelliJ IDEA project module files

Sources: packages/create-next-app/helpers/is-folder-empty.ts:7-39

Caution

If any conflicting files or subdirectories not present on the whitelist are found inside the target directory, isFolderEmpty prints each conflicting path (appending a trailing slash to directories) and returns false, causing createApp to terminate the process before any template files are written.

if (conflicts.length > 0) {
  console.log(
    `The directory ${green(name)} contains files that could conflict:`
  )
  console.log()
  for (const file of conflicts) {
    try {
      const stats = lstatSync(join(root, file))
      if (stats.isDirectory()) {
        console.log(`  ${blue(file)}/`)
      } else {
        console.log(`  ${file}`)
      }
    } catch {
      console.log(`  ${file}`)
    }
  }
  console.log()
  console.log(
    'Either try using a new directory name, or remove the files listed above.'
  )
  console.log()
  return false
}

Sources: packages/create-next-app/helpers/is-folder-empty.ts:41-64

Template Composition and File Generation

Template Selection and Scaffold Copying

The generator determines the initial template variant by combining the application routing structure, styling selection, and emptiness flags into a TemplateType string. The template mode (ts or js) is established based on the TypeScript boolean flag, which directs path resolution to the appropriate subdirectory within packages/create-next-app/templates/.

  const mode: TemplateMode = typescript ? 'ts' : 'js'
  const template: TemplateType = `${app ? 'app' : 'default'}${tailwind ? '-tw' : ''}${empty ? '-empty' : ''}`

Sources: packages/create-next-app/create-app.ts:72-73

When installing a template, installTemplate builds an exclusion array for the copy operation based on enabled features and bundler configuration. Specifically, configuration files such as eslint.config.mjs, biome.json, and postcss.config.mjs are excluded when their respective flags or bundlers are omitted or set to Turbopack.

Excluded File PatternCondition for Exclusion
!eslint.config.mjs!eslint (ESLint disabled)
!biome.json!biome (Biome disabled)
!postcss.config.mjs`!tailwind

Sources: packages/create-next-app/templates/index.ts:74-79

Note

During file copying, the rename transform intercepts specific filenames to normalize them for project creation. Notably, gitignore is renamed to .gitignore to prevent npm publish omissions, and README-template.md is output as README.md.

rename(name) {
  switch (name) {
    case "gitignore": {
      return `.${name}`;
    }
    // README.md is ignored by webpack-asset-relocator-loader used by ncc:
    // https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
    case "README-template.md": {
      return "README.md";
    }
    default: {
      return name;
    }
  }
},

Sources: packages/create-next-app/templates/index.ts:84-98

Bundler Integration and Config Patching

After copying template files, the generator updates configuration files to support alternative bundlers, experimental features, and custom import paths. For Rspack, next-rspack integration is injected into the Next.js configuration file by wrapping the exported configuration.

if (bundler === Bundler.Rspack) {
  const nextConfigFile = path.join(
    root,
    mode === "js" ? "next.config.mjs" : "next.config.ts",
  );
  await fs.writeFile(
    nextConfigFile,
    `import withRspack from "next-rspack";\n\n` +
      (await fs.readFile(nextConfigFile, "utf8")).replace(
        "export default nextConfig;",
        "export default withRspack(nextConfig);",
      ),
  );
}

Sources: packages/create-next-app/templates/index.ts:101-114

Config mutations are also applied when feature flags are enabled during template instantiation. The following options modify next.config.mjs or next.config.ts by searching for the /* config options here */ placeholder.

Feature FlagConfiguration Property Injected
tailwind (with Turbopack)turbopack: { rules: { "*.css": { loaders: ["@tailwindcss/turbopack"], as: "*.css" } } }
reactCompilerreactCompiler: true
cacheComponentscacheComponents: true, partialPrefetching: true
agentFeedbackexperimental: { agentFeedback: true }

Sources: packages/create-next-app/templates/index.ts:116-186

Warning

When cacheComponents is enabled, partialPrefetching is automatically enabled alongside it in the generated Next.js configuration because the framework design couples these two options.

if (cacheComponents) {
  const nextConfigFile = path.join(
    root,
    mode === "js" ? "next.config.mjs" : "next.config.ts",
  );
  let configContent = await fs.readFile(nextConfigFile, "utf8");
 
  // `partialPrefetching` is enabled alongside `cacheComponents` because the
  // only reason to disable it is when adopting `cacheComponents` from before
  // the option existed.
  configContent = configContent.replace(
    "/* config options here */\n",
    "/* config options here */\n  cacheComponents: true,\n  partialPrefetching: true,\n",
  );
 
  await fs.writeFile(nextConfigFile, configContent);
}

Sources: packages/create-next-app/templates/index.ts:155-171

Directory Structure and Import Aliases

When the srcDir option is active, the installer creates a src/ directory and relocates standard folders into it. The source directory migration moves specific directories defined in SRC_DIR_NAMES and updates default page template imports.

export const SRC_DIR_NAMES = ["app", "pages", "styles"];

Sources: packages/create-next-app/templates/index.ts:43-43

if (srcDir) {
  await fs.mkdir(path.join(root, "src"), { recursive: true });
  await Promise.all(
    SRC_DIR_NAMES.map(async (file) => {
      await fs
        .rename(path.join(root, file), path.join(root, "src", file))
        .catch((err) => {
          if (err.code !== "ENOENT") {
            throw err;
          }
        });
    }),
  );
 

Sources: packages/create-next-app/templates/index.ts:240-253

Import aliases are configured in tsconfig.json or jsconfig.json by replacing default paths with the specified importAlias string. If a custom alias other than @/* is chosen, a concurrent file walker using an async-sema concurrency pool of size 8 scans project files while ignoring version control folders, font assets, and lockfiles to rewrite module imports.

if (importAlias !== "@/*") {
  const files = await glob("**/*", {
    cwd: root,
    dot: true,
    stats: false,
    // We don't want to modify compiler options in [ts/js]config.json
    // and none of the files in the .git folder
    // TODO: Refactor this to be an allowlist, rather than a denylist,
    // to avoid corrupting files that weren't intended to be replaced
 
    ignore: [
      "tsconfig.json",
      "jsconfig.json",
      ".git/**/*",
      "**/fonts/**",
      "**/favicon.ico",
    ],
  });
  const writeSema = new Sema(8, { capacity: files.length });
  await Promise.all(
    files.map(async (file) => {
      await writeSema.acquire();
      const filePath = path.join(root, file);
      if ((await fs.stat(filePath)).isFile()) {
        await fs.writeFile(
          filePath,
          (await fs.readFile(filePath, "utf8")).replace(
            `@/`,
            `${importAlias.replace(/\*/g, "")}`,
          ),
        );
      }
      writeSema.release();
    }),
  );
}

Sources: packages/create-next-app/templates/index.ts:203-238

Remote Example Extraction and Fallbacks

URL Parsing and Repository Validation

When an example argument is supplied to createApp, the input is first evaluated to determine if it represents a full GitHub repository URL or a built-in template name. If example can be successfully parsed as a URL object originating from https://github.com, the helper getRepoInfo breaks down the pathname into structural components.

export async function getRepoInfo(
  url: URL,
  examplePath?: string
): Promise<RepoInfo | undefined> {
  const [, username, name, t, _branch, ...file] = url.pathname.split('/')
  const filePath = examplePath ? examplePath.replace(/^\//, '') : file.join('/')
 
  if (
    // Support repos whose entire purpose is to be a Next.js example, e.g.
    // https://github.com/:username/:my-cool-nextjs-example-repo-name.
    t === undefined ||
    // Support GitHub URL that ends with a trailing slash, e.g.
    // https://github.com/:username/:my-cool-nextjs-example-repo-name/
    // In this case "t" will be an empty string while the next part "_branch" will be undefined
    (t === '' && _branch === undefined)
  ) {
    try {
      const infoResponse = await fetch(
        `https://api.github.com/repos/${username}/${name}`
      )
      if (infoResponse.status !== 200) {
        return
      }
 
      const info = await infoResponse.json()
      return { username, name, branch: info['default_branch'], filePath }
    } catch {
      return
    }
  }
 
  // If examplePath is available, the branch name takes the entire path
  const branch = examplePath
    ? `${_branch}/${file.join('/')}`.replace(new RegExp(`/${filePath}|/$`), '')
    : _branch
 
  if (username && name && branch && t === 'tree') {
    return { username, name, branch, filePath }
  }
}

Sources: packages/create-next-app/helpers/examples.ts:23-62

The RepoInfo data structure captures the parsed coordinates required for remote tarball extraction:

export type RepoInfo = {
  username: string
  name: string
  branch: string
  filePath: string
}

Sources: packages/create-next-app/helpers/examples.ts:7-12

Warning

Only repositories hosted on https://github.com are permitted; passing non-GitHub URLs triggers an immediate termination with process exit code 1.

if (repoUrl) {
  if (repoUrl.origin !== 'https://github.com') {
    console.error(
      `Invalid URL: ${red(
        `"${example}"`
      )}. Only GitHub repositories are supported. Please use a GitHub URL and try again.`
    )
    process.exit(1)
  }
 

Sources: packages/create-next-app/create-app.ts:89-98

Before any downloading commences, hasRepo checks whether a package.json file can be reached via the raw GitHub user content network to verify that the target directory and branch exist.

export function hasRepo({
  username,
  name,
  branch,
  filePath,
}: RepoInfo): Promise<boolean> {
  // Avoid GitHub's low unauthenticated API rate limit. create-next-app only
  // needs to verify that the example's package.json can be downloaded.
  const packagePath = `${filePath ? `${filePath}/` : ''}package.json`
 
  return isUrlOk(
    `https://raw.githubusercontent.com/${username}/${name}/${branch}/${packagePath}`
  )
}

Sources: packages/create-next-app/helpers/examples.ts:64-77

If the example string is not a valid URL, it is evaluated as a built-in example residing within the main vercel/next.js repository by querying the GitHub contents API or verifying an explicit URL status via isUrlOk.

export function existsInRepo(nameOrUrl: string): Promise<boolean> {
  try {
    const url = new URL(nameOrUrl)
    return isUrlOk(url.href)
  } catch {
    return isUrlOk(
      `https://api.github.com/repos/vercel/next.js/contents/examples/${encodeURIComponent(
        nameOrUrl
      )}`
    )
  }
}

Sources: packages/create-next-app/helpers/examples.ts:79-90

Tarball Streaming and Extraction Filters

Remote code extraction relies on streaming tarballs through downloadTarStream combined with the tar library's x extractor. Depending on whether a custom repository or a built-in framework example was requested, the extraction process invokes either downloadAndExtractRepo or downloadAndExtractExample.

async function downloadTarStream(url: string) {
  const res = await fetch(url)
 
  if (!res.body) {
    throw new Error(`Failed to download: ${url}`)
  }
 
  return Readable.fromWeb(res.body as import('stream/web').ReadableStream)
}

Sources: packages/create-next-app/helpers/examples.ts:92-100

export async function downloadAndExtractRepo(
  root: string,
  { username, name, branch, filePath }: RepoInfo
) {
  let rootPath: string | null = null
  await pipeline(
    await downloadTarStream(
      `https://codeload.github.com/${username}/${name}/tar.gz/${branch}`
    ),
    x({
      cwd: root,
      strip: filePath ? filePath.split('/').length + 1 : 1,
      filter: (p: string) => {
        // Convert Windows path separators to POSIX style
        const posixPath = p.split(sep).join(posix.sep)
 
        // Determine the unpacked root path dynamically instead of hardcoding to the fetched repo's name / branch.
        // This avoids the condition when the repository has been renamed, and the old repository name is used to fetch the example.
        // The tar download will work as it is redirected automatically, but the root directory of the extracted
        // example will be the new, renamed name instead of the name used to fetch the example, breaking the filter.
        if (rootPath === null) {
          const pathSegments = posixPath.split(posix.sep)
          rootPath = pathSegments.length ? pathSegments[0] : null
        }
 
        return posixPath.startsWith(
          `${rootPath}${filePath ? `/${filePath}/` : '/'}`
        )
      },
    })
  )
}

Sources: packages/create-next-app/helpers/examples.ts:102-133

Note

During custom repository extraction, the filter dynamically resolves the unpacked root directory from the incoming POSIX path segments rather than hardcoding the repository name or branch. This prevents path-matching failures if a GitHub repository was recently renamed while redirects were active.

export async function downloadAndExtractExample(root: string, name: string) {
  if (name === '__internal-testing-retry') {
    throw new Error('This is an internal example for testing the CLI.')
  }
 
  await pipeline(
    await downloadTarStream(
      'https://codeload.github.com/vercel/next.js/tar.gz/canary'
    ),
    x({
      cwd: root,
      strip: 2 + name.split('/').length,
      filter: (p) => p.includes(`next.js-canary/examples/${name}/`),
    })
  )
}

Sources: packages/create-next-app/helpers/examples.ts:135-150

Package Manager Detection and Installation

Package Manager Resolution

The tool resolves the active package manager by inspecting the npm_config_user_agent environment variable. If this string is not set or does not match a known prefix, it defaults to npm.

Package ManagerEnvironment Prefix / CommandVersion Resolution Fallback
yarnyarnyarn --version
pnpmpnpmpnpm --version
bunbunbun --version
npm(default fallback)npm --version
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
 
export function getPkgManager(): PackageManager {
  const userAgent = process.env.npm_config_user_agent || ''
 
  if (userAgent.startsWith('yarn')) {
    return 'yarn'
  }
 
  if (userAgent.startsWith('pnpm')) {
    return 'pnpm'
  }
 
  if (userAgent.startsWith('bun')) {
    return 'bun'
  }
 
  return 'npm'
}

Sources: packages/create-next-app/helpers/get-pkg-manager.ts:3-21

To acquire precise version metadata, getPackageManagerVersion searches npm_config_user_agent with a dynamic RegExp matching ${packageManager}/([\d.]+[\w-]*). If unpopulated, it executes <packageManager> --version via execSync with standard output suppressed on error.

export function getPackageManagerVersion(
  packageManager: PackageManager
): string | null {
  const userAgent = process.env.npm_config_user_agent || ''
  const userAgentMatch = userAgent.match(
    new RegExp(`${packageManager}/([\\d.]+[\\w.-]*)`)
  )
  if (userAgentMatch) {
    return userAgentMatch[1]
  }
 
  try {
    const version = execSync(`${packageManager} --version`, {
      encoding: 'utf8',
      stdio: ['pipe', 'pipe', 'ignore'],
    }).trim()
    if (/^\d+\.\d+\.\d+/.test(version)) {
      return version
    }
  } catch {
    // package manager not available or failed to run
  }
 
  return null
}

Sources: packages/create-next-app/helpers/get-pkg-manager.ts:30-54

Note

When evaluating pnpm, getPnpmMajorVersion queries getPackageManagerVersion('pnpm'), splits the resulting version string by dots, and parses the primary integer index to gate major-version-specific scaffolding behaviors.

export function getPnpmMajorVersion(): number | null {
  const version = getPackageManagerVersion('pnpm')
  if (!version) return null
  const major = parseInt(version.split('.')[0], 10)
  return Number.isNaN(major) ? null : major
}

Sources: packages/create-next-app/helpers/get-pkg-manager.ts:60-65

Dependency Installation and Network Fallbacks

Dependency installation is managed by the install helper, which spawns the chosen package manager with the install argument. When isOnline evaluates to false, the --offline flag is appended to force resolution from local caches.

export async function install(
  /** Indicate which package manager to use. */
  packageManager: PackageManager,
  /** Indicate whether there is an active Internet connection.*/
  isOnline: boolean
): Promise<void> {
  const args: string[] = ['install']
  if (!isOnline) {
    console.log(
      yellow('You appear to be offline.\nFalling back to the local cache.')
    )
    args.push('--offline')
  }

Sources: packages/create-next-app/helpers/install.ts:11-23

Warning

Spawning child processes forces NODE_ENV to development within the execution environment payload to prevent pnpm from prematurely pruning development dependencies required during project compilation.

const child = spawn(packageManager, args, {
  stdio: 'inherit',
  env: {
    ...process.env,
    ADBLOCK: '1',
    // we set NODE_ENV to development as pnpm skips dev
    // dependencies when production
    NODE_ENV: 'development',
    DISABLE_OPENCOLLECTIVE: '1',
  },
})

Sources: packages/create-next-app/helpers/install.ts:31-41

If the installation process exits with a non-zero code, the child handler rejects with an object encapsulating the failing command string.

child.on('close', (code) => {
  if (code !== 0) {
    reject({ command: `${packageManager} ${args.join(' ')}` })
    return
  }
  resolve()
})

Sources: packages/create-next-app/helpers/install.ts:42-48

Post-Scaffolding Type Generation

After remote example repositories complete their package installation phase, the orchestrator triggers runTypegen as a best-effort operation to synthesize initial TypeScript declaration linkages.

  try {
    console.log()
    await runTypegen(packageManager)
    console.log()
  } catch (err) {
    // Best effort: do not fail app creation if typegen fails
    console.error('Error running typegen:', err)
  }
}

Sources: packages/create-next-app/create-app.ts:233-241

Git Initialization and Lifecycle Finalization

Repository Initialization Checks

The tryGitInit helper function orchestrates local version control setup by checking for existing repository boundaries. Before running any commands, it verifies whether the destination path resides within an existing Git repository or a Mercurial repository by executing git rev-parse --is-inside-work-tree and hg --cwd . root respectively.

function isInGitRepository(): boolean {
  try {
    execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' })
    return true
  } catch (_) {}
  return false
}
 
function isInMercurialRepository(): boolean {
  try {
    execSync('hg --cwd . root', { stdio: 'ignore' })
    return true
  } catch (_) {}
  return false
}

Sources: packages/create-next-app/helpers/git.ts:5-19

Warning

If tryGitInit detects that the target directory is already part of an active Git or Mercurial working tree, initialization is bypassed entirely and the function returns false to prevent nested repository corruption.

if (isInGitRepository() || isInMercurialRepository()) {
  return false
}

Sources: packages/create-next-app/helpers/git.ts:33-35

Default Branch and Initial Commit

When no repository exists and Git is available on the host system, the orchestrator invokes git init. It checks whether a default branch name has been configured globally via git config init.defaultBranch. If unconfigured, it explicitly switches branches to main using git checkout -b main.

function isDefaultBranchSet(): boolean {
  try {
    execSync('git config init.defaultBranch', { stdio: 'ignore' })
    return true
  } catch (_) {}
  return false
}
 
export function tryGitInit(root: string): boolean {
  let didInit = false
  try {
    execSync('git --version', { stdio: 'ignore' })
    if (isInGitRepository() || isInMercurialRepository()) {
      return false
    }
 
    execSync('git init', { stdio: 'ignore' })
    didInit = true
 
    if (!isDefaultBranchSet()) {
      execSync('git checkout -b main', { stdio: 'ignore' })
    }

Sources: packages/create-next-app/helpers/git.ts:21-42

Following branch selection, all scaffolded project files are staged via git add -A and committed with the message "Initial commit from Create Next App". If any step in this sequence throws an exception after initialization has started, the helper cleans up by recursively removing the .git directory using rmSync.

  execSync('git add -A', { stdio: 'ignore' })
  execSync('git commit -m "Initial commit from Create Next App"', {
    stdio: 'ignore',
  })
  return true
} catch (e) {
  if (didInit) {
    try {
      rmSync(join(root, '.git'), { recursive: true, force: true })
    } catch (_) {}
  }
  return false
}

Sources: packages/create-next-app/helpers/git.ts:44-56

Completion Messaging and Next Steps

Once scaffolding, installation, and optional Git initialization complete, the orchestrator computes the correct relative path (cdpath) for navigating into the newly created directory. It outputs a success message followed by a list of available package manager commands tailored to the user's configuration.

let cdpath: string
if (join(originalDirectory, appName) === appPath) {
  cdpath = appName
} else {
  cdpath = appPath
}
 
console.log(`${green('Success!')} Created ${appName} at ${appPath}`)
 
if (hasPackageJson) {
  console.log('Inside that directory, you can run several commands:')
  console.log()
  console.log(cyan(`  ${packageManager} ${useYarn ? '' : 'run '}dev`))
  console.log('    Starts the development server.')
  console.log()
  console.log(cyan(`  ${packageManager} ${useYarn ? '' : 'run '}build`))
  console.log('    Builds the app for production.')
  console.log()
  console.log(cyan(`  ${packageManager} start`))
  console.log('    Runs the built app in production mode.')
  console.log()
  console.log('We suggest that you begin by typing:')
  console.log()
  console.log(cyan('  cd'), cdpath)
  console.log(`  ${cyan(`${packageManager} ${useYarn ? '' : 'run '}dev`)}`)
}

Sources: packages/create-next-app/create-app.ts:280-305

Was this page helpful?