Files
next.js/test/integration/create-next-app/prompts.test.ts
Jude GaoandGitHub 9a6346302a [create-next-app] Skip interactive prompts when CLI flags are provided (#91840)
When AI agents run `create-next-app` with explicit flags like
`--typescript --tailwind --eslint --app --src-dir`, the CLI still enters
interactive mode and prompts for any unspecified options:

```
➜ npx create-next-app my-app --typescript --tailwind --eslint --app --src-dir --use-pnpm
✔ Would you like to use React Compiler? … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
? Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? › No / Yes
```

Agents can sometimes answer these interactive prompts correctly, but
often they can't. When they fail to navigate the prompts, they fall back
to scaffolding the project themselves from scratch — generating files
based on stale training data. This means they might initialize an app
using deprecated patterns or pin to an older version of Next.js (e.g.
15) instead of the latest. Using `create-next-app` non-interactively is
the best way to ensure agents always produce up-to-date scaffolding.

Previously, `hasProvidedOptions` only skipped the initial "use
recommended defaults?" meta-prompt but still showed individual prompts
for each missing option. Now when any config flags are provided, all
remaining options use the recommended defaults without prompting.

The resolved defaults are printed to stdout so the caller knows exactly
what was assumed and which flags to pass to override:

```
Using defaults for unprovided options:

  --eslint                ESLint (use --biome for Biome, --no-eslint for None)
  --no-react-compiler     No React Compiler (use --react-compiler for React Compiler)
  --agents-md             AGENTS.md (use --no-agents-md for No AGENTS.md)
  --import-alias          "@/*"

To customize, re-run with explicit flags.
```

The `displayConfig` array is extended with a `flags` field so this
output is auto-generated from the same source of truth used for the
interactive prompts. Existing behavior for `--yes`, CI mode, and fully
interactive mode (no flags) is unchanged.
2026-03-25 11:36:01 -07:00

347 lines
9.0 KiB
TypeScript

import { check } from 'next-test-utils'
import { join } from 'path'
import { createNextApp, projectFilesShouldExist, useTempDir } from './utils'
describe('create-next-app prompts', () => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
})
it('should prompt user for choice if directory name is absent', async () => {
await useTempDir(async (cwd) => {
const projectName = 'no-dir-name'
const childProcess = createNextApp(
[
'--ts',
'--app',
'--eslint',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: ['package.json'],
})
resolve()
})
// enter project name
childProcess.stdin.write(`${projectName}\n`)
})
const pkg = require(join(cwd, projectName, 'package.json'))
expect(pkg.name).toBe(projectName)
})
})
it('should use default for --ts when other flags are provided', async () => {
await useTempDir(async (cwd) => {
const projectName = 'ts-js'
const childProcess = createNextApp(
[
projectName,
'--app',
'--eslint',
'--no-tailwind',
'--no-src-dir',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
{
cwd,
},
nextTgzFilename
)
// No stdin interaction needed - defaults are used automatically
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
// Default is TypeScript
projectFilesShouldExist({
cwd,
projectName,
files: ['tsconfig.json'],
})
resolve()
})
})
})
})
it('should use default for --tailwind when other flags are provided', async () => {
await useTempDir(async (cwd) => {
const projectName = 'tw'
const childProcess = createNextApp(
[
projectName,
'--ts',
'--app',
'--eslint',
'--no-src-dir',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
{
cwd,
},
nextTgzFilename
)
// No stdin interaction needed - defaults are used automatically
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
// Default is Tailwind enabled
projectFilesShouldExist({
cwd,
projectName,
files: ['postcss.config.mjs'],
})
resolve()
})
})
})
})
it('should use default import alias when other flags are provided', async () => {
await useTempDir(async (cwd) => {
const projectName = 'import-alias'
const childProcess = createNextApp(
[
projectName,
'--ts',
'--app',
'--eslint',
'--no-tailwind',
'--no-src-dir',
'--no-react-compiler',
'--no-agents-md',
],
{
cwd,
},
nextTgzFilename
)
// No stdin interaction needed - default import alias @/* is used
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
resolve()
})
})
const tsConfig = require(join(cwd, projectName, 'tsconfig.json'))
expect(tsConfig.compilerOptions.paths).toMatchInlineSnapshot(`
{
"@/*": [
"./*",
],
}
`)
})
})
it('should not prompt user for choice and use defaults if --yes is defined', async () => {
await useTempDir(async (cwd) => {
const projectName = 'yes-we-can'
const childProcess = createNextApp(
[projectName, '--yes'],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'app',
'package.json',
'postcss.config.mjs',
'tsconfig.json',
'AGENTS.md',
'CLAUDE.md',
],
})
resolve()
})
})
const pkg = require(join(cwd, projectName, 'package.json'))
expect(pkg.name).toBe(projectName)
const tsConfig = require(join(cwd, projectName, 'tsconfig.json'))
expect(tsConfig.compilerOptions.paths).toMatchInlineSnapshot(`
{
"@/*": [
"./*",
],
}
`)
})
})
it('should use recommended defaults when user selects that option', async () => {
await useTempDir(async (cwd) => {
const projectName = 'recommended-defaults'
const childProcess = createNextApp(
[projectName],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'app',
'package.json',
'postcss.config.mjs', // tailwind
'tsconfig.json', // typescript
'AGENTS.md', // agent files
'CLAUDE.md',
],
})
resolve()
})
// Select "Yes, use recommended defaults" (default option, just press enter)
childProcess.stdin.write('\n')
})
const pkg = require(join(cwd, projectName, 'package.json'))
expect(pkg.name).toBe(projectName)
})
})
it('should show reuse previous settings option when preferences exist', async () => {
const Conf = require('next/dist/compiled/conf')
await useTempDir(async (cwd) => {
// Manually set preferences to simulate a previous run
const conf = new Conf({ projectName: 'create-next-app' })
conf.set('preferences', {
typescript: false,
eslint: true,
linter: 'eslint',
tailwind: false,
app: false,
srcDir: false,
importAlias: '@/*',
customizeImportAlias: false,
reactCompiler: false,
})
const projectName = 'reuse-prefs-project'
const childProcess = createNextApp(
[projectName],
{
cwd,
},
nextTgzFilename,
false // Don't clear preferences
)
await new Promise<void>(async (resolve) => {
let output = ''
childProcess.stdout.on('data', (data) => {
output += data
process.stdout.write(data)
})
// Select "reuse previous settings" (cursor down once, then enter)
childProcess.stdin.write('\u001b[B\n')
// Wait for the prompt to appear with "reuse previous settings"
await check(() => output, /No, reuse previous settings/)
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'pages', // pages router (not app)
'package.json',
'jsconfig.json', // javascript
],
})
resolve()
})
})
const pkg = require(join(cwd, projectName, 'package.json'))
expect(pkg.name).toBe(projectName)
})
})
it('should prompt user to confirm reset preferences', async () => {
await useTempDir(async (cwd) => {
const childProcess = createNextApp(
['--reset'],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>(async (resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
resolve()
})
let output = ''
childProcess.stdout.on('data', (data) => {
output += data
process.stdout.write(data)
})
await check(
() => output,
/Would you like to reset the saved preferences/
)
// cursor forward, choose 'Yes' for reset preferences
childProcess.stdin.write('\u001b[C\n')
await check(
() => output,
/The preferences have been reset successfully/
)
})
})
})
})