mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
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.
347 lines
9.0 KiB
TypeScript
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/
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|