Relevant source files
The following files were used as context for generating this wiki page:
- packages/create-next-app/index.ts
- packages/create-next-app/create-app.ts
- packages/create-next-app/templates/index.ts
- packages/next/src/cli/next-upgrade.ts
- packages/next/src/bin/next.ts
- packages/next-codemod/bin/upgrade.ts
- packages/next/src/cli/next-info.ts
- packages/next/src/cli/next-dev.ts
- packages/create-next-app/helpers/install.ts
- packages/create-next-app/helpers/examples.ts
- packages/next-codemod/bin/next-codemod.ts
- packages/next-codemod/bin/agents-md.ts
- packages/create-next-app/package.json
- packages/next/src/lib/helpers/install.ts
- packages/create-next-app/helpers/is-folder-empty.ts
- packages/next-codemod/lib/install.ts
- packages/create-next-app/helpers/git.ts
- packages/create-next-app/helpers/validate-pkg.ts
- packages/create-next-app/helpers/is-writeable.ts
- packages/create-next-app/helpers/get-pkg-manager.ts
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 Flag | Description |
|---|---|
-v, --version | Output the current version of create-next-app |
-h, --help | Display this help message |
--ts, --typescript | Initialize as a TypeScript project (default) |
--js, --javascript | Initialize as a JavaScript project |
--tailwind | Initialize with Tailwind CSS config (default) |
--react-compiler | Initialize with React Compiler enabled |
--cache-components | Initialize with Cache Components enabled |
--eslint | Initialize with ESLint config |
--biome | Initialize with Biome config |
--app | Initialize as an App Router project |
--src-dir | Initialize inside a src/ directory |
--rspack | Enable Rspack as the bundler |
--import-alias <prefix/*> | Specify import alias to use (default @/*) |
--api | Initialize a headless API using the App Router |
--empty | Initialize an empty project |
--use-npm | Bootstrap using npm |
--use-pnpm | Bootstrap using pnpm |
--use-yarn | Bootstrap using Yarn |
--use-bun | Bootstrap using Bun |
--reset, --reset-preferences | Reset saved preferences |
--skip-install | Skip installing packages |
--yes | Use 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-md | Include AGENTS.md to guide coding agents (default) |
--agent-feedback | Prepare anonymized Next.js feedback |
--no-agent-feedback | Do not prepare anonymized Next.js feedback |
--disable-git | Skip 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 Pattern | Status | Purpose / Rationale |
|---|---|---|
.claude, .cursor, .vscode, .zed | Whitelisted | AI coding assistant and editor configuration directories |
.git, .hg, .hgcheck, .hgignore | Whitelisted | Version control directories and ignore rules |
.DS_Store, Thumbs.bg, Thumbs.db | Whitelisted | Operating system thumbnail and metadata files |
.gitignore, .gitattributes, .npmignore | Whitelisted | Git and npm tracking exclusion rules |
LICENSE, docs, mkdocs.yml | Whitelisted | Documentation and licensing assets |
npm-debug.log, yarn-debug.log, yarn-error.log | Whitelisted | Package manager diagnostic and error logs |
yarnrc.yml, .yarn | Whitelisted | Yarn workspace configuration and cache folders |
*.iml | Whitelisted (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 Pattern | Condition 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 Flag | Configuration Property Injected |
|---|---|
tailwind (with Turbopack) | turbopack: { rules: { "*.css": { loaders: ["@tailwindcss/turbopack"], as: "*.css" } } } |
reactCompiler | reactCompiler: true |
cacheComponents | cacheComponents: true, partialPrefetching: true |
agentFeedback | experimental: { 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 Manager | Environment Prefix / Command | Version Resolution Fallback |
|---|---|---|
yarn | yarn | yarn --version |
pnpm | pnpm | pnpm --version |
bun | bun | bun --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
Related
On This Page
OverviewCLI Entry Point and Interactive PromptsArgument Parsing and Command ConfigurationPackage Manager Resolution and Preference ManagementDirectory Validation and Preflight ChecksNaming Verification and Destination ResolutionWrite Permissions and Folder Emptiness ChecksTemplate Composition and File GenerationTemplate Selection and Scaffold CopyingBundler Integration and Config PatchingDirectory Structure and Import AliasesRemote Example Extraction and FallbacksURL Parsing and Repository ValidationTarball Streaming and Extraction FiltersPackage Manager Detection and InstallationPackage Manager ResolutionDependency Installation and Network FallbacksPost-Scaffolding Type GenerationGit Initialization and Lifecycle FinalizationRepository Initialization ChecksDefault Branch and Initial CommitCompletion Messaging and Next StepsRelated