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.
282 lines
7.3 KiB
TypeScript
282 lines
7.3 KiB
TypeScript
import { mkdir, writeFile } from 'fs/promises'
|
|
import { join } from 'path'
|
|
import {
|
|
run,
|
|
useTempDir,
|
|
projectFilesShouldExist,
|
|
projectFilesShouldNotExist,
|
|
} from './utils'
|
|
|
|
describe('create-next-app', () => {
|
|
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 not create if the target directory is not empty', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'non-empty-dir'
|
|
await mkdir(join(cwd, projectName))
|
|
const pkg = join(cwd, projectName, 'package.json')
|
|
await writeFile(pkg, `{ "name": "${projectName}" }`)
|
|
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--ts',
|
|
'--app',
|
|
'--no-linter',
|
|
'--no-tailwind',
|
|
'--no-src-dir',
|
|
'--no-import-alias',
|
|
'--no-react-compiler',
|
|
'--no-agents-md',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
reject: false,
|
|
}
|
|
)
|
|
expect(res.exitCode).toBe(1)
|
|
expect(res.stdout).toMatch(/contains files that could conflict/)
|
|
})
|
|
})
|
|
|
|
it('should not create if the target directory is not writable', async () => {
|
|
const expectedErrorMessage =
|
|
/you do not have write permissions for this folder|EPERM: operation not permitted/
|
|
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'dir-not-writable'
|
|
|
|
// if the folder isn't able to be write restricted we can't test so skip
|
|
if (
|
|
await writeFile(join(cwd, 'test'), 'hello')
|
|
.then(() => true)
|
|
.catch(() => false)
|
|
) {
|
|
console.warn(
|
|
`Test folder is not write restricted skipping write permission test`
|
|
)
|
|
return
|
|
}
|
|
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--ts',
|
|
'--app',
|
|
'--eslint',
|
|
'--no-tailwind',
|
|
'--no-src-dir',
|
|
'--no-import-alias',
|
|
'--no-react-compiler',
|
|
'--no-agents-md',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
reject: false,
|
|
}
|
|
)
|
|
|
|
expect(res.stderr).toMatch(expectedErrorMessage)
|
|
expect(res.exitCode).toBe(1)
|
|
}, 0o500).catch((err) => {
|
|
if (!expectedErrorMessage.test(err.message)) {
|
|
throw err
|
|
}
|
|
})
|
|
})
|
|
it('should create AGENTS.md and CLAUDE.md with --agents-md flag', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'with-agents-md'
|
|
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--ts',
|
|
'--app',
|
|
'--no-linter',
|
|
'--no-tailwind',
|
|
'--no-src-dir',
|
|
'--no-import-alias',
|
|
'--no-react-compiler',
|
|
'--agents-md',
|
|
'--skip-install',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
}
|
|
)
|
|
expect(res.exitCode).toBe(0)
|
|
projectFilesShouldExist({
|
|
cwd,
|
|
projectName,
|
|
files: ['AGENTS.md', 'CLAUDE.md'],
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should not create AGENTS.md and CLAUDE.md with --no-agents-md flag', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'without-agents-md'
|
|
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--ts',
|
|
'--app',
|
|
'--no-linter',
|
|
'--no-tailwind',
|
|
'--no-src-dir',
|
|
'--no-import-alias',
|
|
'--no-react-compiler',
|
|
'--no-agents-md',
|
|
'--skip-install',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
}
|
|
)
|
|
expect(res.exitCode).toBe(0)
|
|
projectFilesShouldNotExist({
|
|
cwd,
|
|
projectName,
|
|
files: ['AGENTS.md', 'CLAUDE.md'],
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should print assumed defaults when flags are partially provided', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'partial-flags'
|
|
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--ts',
|
|
'--tailwind',
|
|
'--app',
|
|
'--skip-install',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
stdio: 'pipe',
|
|
}
|
|
)
|
|
expect(res.exitCode).toBe(0)
|
|
|
|
// Extract the defaults block from stdout
|
|
const defaultsMatch = res.stdout.match(
|
|
/Using defaults for unprovided options:\n\n([\s\S]*?)\n\nCreating/
|
|
)
|
|
expect(defaultsMatch).not.toBeNull()
|
|
expect(defaultsMatch[1]).toMatchInlineSnapshot(`
|
|
" --eslint ESLint (use --biome for Biome, --no-eslint for None)
|
|
--no-react-compiler No React Compiler (use --react-compiler for React Compiler)
|
|
--no-src-dir No src/ directory (use --src-dir for src/ directory)
|
|
--agents-md AGENTS.md (use --no-agents-md for No AGENTS.md)
|
|
--import-alias "@/*""
|
|
`)
|
|
})
|
|
})
|
|
|
|
it('should not print assumed defaults when all flags are provided', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'all-flags'
|
|
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--ts',
|
|
'--app',
|
|
'--eslint',
|
|
'--tailwind',
|
|
'--no-src-dir',
|
|
'--no-import-alias',
|
|
'--no-react-compiler',
|
|
'--no-agents-md',
|
|
'--skip-install',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
stdio: 'pipe',
|
|
}
|
|
)
|
|
expect(res.exitCode).toBe(0)
|
|
expect(res.stdout).not.toContain('Using defaults for unprovided options')
|
|
})
|
|
})
|
|
|
|
it('should not print assumed defaults with --yes flag', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'yes-flag'
|
|
|
|
const res = await run(
|
|
[projectName, '--yes', '--skip-install'],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
stdio: 'pipe',
|
|
}
|
|
)
|
|
expect(res.exitCode).toBe(0)
|
|
expect(res.stdout).not.toContain('Using defaults for unprovided options')
|
|
})
|
|
})
|
|
|
|
it('should not install dependencies if --skip-install', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'empty-dir'
|
|
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--ts',
|
|
'--app',
|
|
'--no-linter',
|
|
'--no-tailwind',
|
|
'--no-src-dir',
|
|
'--no-import-alias',
|
|
'--skip-install',
|
|
'--no-react-compiler',
|
|
'--no-agents-md',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
}
|
|
)
|
|
expect(res.exitCode).toBe(0)
|
|
projectFilesShouldExist({
|
|
cwd,
|
|
projectName,
|
|
files: ['.gitignore', 'package.json'],
|
|
})
|
|
projectFilesShouldNotExist({ cwd, projectName, files: ['node_modules'] })
|
|
})
|
|
})
|
|
})
|