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.
314 lines
7.1 KiB
TypeScript
314 lines
7.1 KiB
TypeScript
import {
|
|
EXAMPLE_PATH,
|
|
EXAMPLE_REPO,
|
|
FULL_EXAMPLE_PATH,
|
|
projectFilesShouldExist,
|
|
projectFilesShouldNotExist,
|
|
shouldBeTemplateProject,
|
|
run,
|
|
useTempDir,
|
|
} from './utils'
|
|
|
|
describe('create-next-app --example', () => {
|
|
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 create on valid Next.js example name', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'valid-example'
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--example',
|
|
'basic-css',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
}
|
|
)
|
|
expect(res.exitCode).toBe(0)
|
|
projectFilesShouldExist({
|
|
cwd,
|
|
projectName,
|
|
files: [
|
|
'.gitignore',
|
|
'package.json',
|
|
'app/page.tsx',
|
|
'app/layout.tsx',
|
|
'node_modules/next',
|
|
],
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should create with GitHub URL', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'github-url'
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--example',
|
|
FULL_EXAMPLE_PATH,
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
}
|
|
)
|
|
|
|
expect(res.exitCode).toBe(0)
|
|
projectFilesShouldExist({
|
|
cwd,
|
|
projectName,
|
|
files: [
|
|
'.gitignore',
|
|
'package.json',
|
|
'app/page.tsx',
|
|
'app/layout.tsx',
|
|
'node_modules/next',
|
|
],
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should create with GitHub URL trailing slash', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'github-url-trailing-slash'
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--example',
|
|
// since vercel/examples is not a template repo, we use the following
|
|
// GH#39665
|
|
'https://github.com/vercel/nextjs-portfolio-starter/',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
}
|
|
)
|
|
|
|
expect(res.exitCode).toBe(0)
|
|
projectFilesShouldExist({
|
|
cwd,
|
|
projectName,
|
|
files: [
|
|
'.gitignore',
|
|
'package.json',
|
|
'pages/index.mdx',
|
|
'node_modules/next',
|
|
],
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should create with GitHub URL and --example-path', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'github-url-and-example-path'
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--js',
|
|
'--no-tailwind',
|
|
'--eslint',
|
|
'--example',
|
|
EXAMPLE_REPO,
|
|
'--example-path',
|
|
EXAMPLE_PATH,
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
}
|
|
)
|
|
|
|
expect(res.exitCode).toBe(0)
|
|
projectFilesShouldExist({
|
|
cwd,
|
|
projectName,
|
|
files: [
|
|
'.gitignore',
|
|
'package.json',
|
|
'app/page.tsx',
|
|
'app/layout.tsx',
|
|
'node_modules/next',
|
|
],
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should use --example-path over the GitHub URL', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'example-path-over-github-url'
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--js',
|
|
'--no-tailwind',
|
|
'--eslint',
|
|
'--example',
|
|
FULL_EXAMPLE_PATH,
|
|
'--example-path',
|
|
EXAMPLE_PATH,
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
}
|
|
)
|
|
|
|
expect(res.exitCode).toBe(0)
|
|
projectFilesShouldExist({
|
|
cwd,
|
|
projectName,
|
|
files: [
|
|
'.gitignore',
|
|
'package.json',
|
|
'app/page.tsx',
|
|
'app/layout.tsx',
|
|
'node_modules/next',
|
|
],
|
|
})
|
|
})
|
|
})
|
|
|
|
// TODO: investigate why this test stalls on yarn install when
|
|
// stdin is piped instead of inherited on windows
|
|
if (process.platform !== 'win32') {
|
|
it('should fall back to default template if failed to download', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'fallback-to-default'
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--js',
|
|
'--no-tailwind',
|
|
'--eslint',
|
|
'--app',
|
|
'--example',
|
|
'__internal-testing-retry',
|
|
'--import-alias=@/*',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
input: '\n', // 'Yes' to retry
|
|
stdio: 'pipe',
|
|
}
|
|
)
|
|
|
|
expect(res.exitCode).toBe(0)
|
|
shouldBeTemplateProject({
|
|
cwd,
|
|
projectName,
|
|
template: 'app',
|
|
mode: 'js',
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
it('should create if --example value is default', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'example-default'
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--js',
|
|
'--no-app',
|
|
'--no-tailwind',
|
|
'--eslint',
|
|
'--no-src-dir',
|
|
'--no-react-compiler',
|
|
'--no-agents-md',
|
|
'--example',
|
|
'default',
|
|
'--import-alias=@/*',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
}
|
|
)
|
|
|
|
expect(res.exitCode).toBe(0)
|
|
shouldBeTemplateProject({
|
|
cwd,
|
|
projectName,
|
|
template: 'default',
|
|
mode: 'js',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should not create if --example flag value is invalid', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'invalid-example'
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--example',
|
|
'not a real example',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
reject: false,
|
|
}
|
|
)
|
|
|
|
expect(res.exitCode).toBe(1)
|
|
projectFilesShouldNotExist({
|
|
cwd,
|
|
projectName,
|
|
files: ['package.json'],
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should not create if --example flag value is absent', async () => {
|
|
await useTempDir(async (cwd) => {
|
|
const projectName = 'no-example'
|
|
const res = await run(
|
|
[
|
|
projectName,
|
|
'--ts',
|
|
'--app',
|
|
'--eslint',
|
|
'--no-src-dir',
|
|
'--no-tailwind',
|
|
'--example',
|
|
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
|
|
],
|
|
nextTgzFilename,
|
|
{
|
|
cwd,
|
|
reject: false,
|
|
}
|
|
)
|
|
|
|
expect(res.exitCode).toBe(1)
|
|
})
|
|
})
|
|
})
|