Repository: vercel/next.js
Pull request: [create-next-app]: prompt to use recommended options

`create-next-app` by default asks a lot of questions that new users might not be familiar with or necessarily know the answer to. This updates the flow to start by asking if you want to use the recommended defaults. If you already have preferences saved from previous runs, it becomes an option to re-use those preferences. Otherwise you can proceed with the step-by-step flow.

https://github.com/user-attachments/assets/92e815df-5484-4235-841b-eb231df8377c



Changed files:
filename: packages/create-next-app/index.ts
patch: @@ -227,7 +227,8 @@ async function run(): Promise<void> {
    * If the user does not provide the necessary flags, prompt them for their
    * preferences, unless `--yes` option was specified, or when running in CI.
    */
-  const skipPrompt = ciInfo.isCI || opts.yes
+  let skipPrompt = ciInfo.isCI || opts.yes
+  let useRecommendedDefaults = false
 
   if (!example) {
     const defaults: typeof preferences = {
@@ -244,8 +245,124 @@ async function run(): Promise<void> {
       disableGit: false,
       reactCompiler: false,
     }
-    const getPrefOrDefault = (field: string) =>
-      preferences[field] ?? defaults[field]
+
+    type DisplayConfigItem = {
+      key: keyof typeof defaults
+      values?: Record<string, string>
+    }
+
+    const displayConfig: DisplayConfigItem[] = [
+      {
+        key: 'typescript',
+        values: { true: 'TypeScript', false: 'JavaScript' },
+      },
+      { key: 'linter', values: { eslint: 'ESLint', biome: 'Biome' } },
+      { key: 'reactCompiler', values: { true: 'React Compiler' } },
+      { key: 'tailwind', values: { true: 'Tailwind CSS' } },
+      { key: 'srcDir', values: { true: 'src/ dir' } },
+      { key: 'app', values: { true: 'App Router', false: 'Pages Router' } },
+      { key: 'turbopack', values: { true: 'Turbopack' } },
+    ]
+
+    // Helper to format settings for display based on displayConfig
+    const formatSettingsDescription = (
+      settings: Record<string, boolean | string>
+    ) => {
+      const descriptions: string[] = []
+
+      for (const config of displayConfig) {
+        const value = settings[config.key]
+
+        if (config.values) {
+          // Look up the display label for this value
+          const label = config.values[String(value)]
+          if (label) {
+            descriptions.push(label)
+          }
+        }
+      }
+
+      return descriptions.join(', ')
+    }
+
+    // Check if we have saved preferences
+    const hasSavedPreferences = Object.keys(preferences).length > 0
+
+    // Check if user provided any configuration flags
+    // If they did, skip the "recommended defaults" prompt and go straight to
+    // individual prompts for any missing options
+    const hasProvidedOptions = process.argv.some((arg) => arg.startsWith('--'))
+
+    // Only show the "recommended defaults" prompt if:
+    // - Not in CI and not using --yes flag
+    // - User hasn't provided any custom options
+    if (!skipPrompt && !hasProvidedOptions) {
+      const choices: Array<{
+        title: string
+        value: string
+        description?: string
+      }> = [
+        {
+          title: 'Yes, use recommended defaults',
+          value: 'recommended',
+          description: formatSettingsDescription(defaults),
+        },
+        {
+          title: 'No, customize settings',
+          value: 'customize',
+          description: 'Choose your own preferences',
+        },
+      ]
+
+      // Add "reuse previous settings" option if we have saved preferences
+      if (hasSavedPreferences) {
+        const prefDescription = formatSettingsDescription(preferences)
+        choices.splice(1, 0, {
+          title: 'No, reuse previous settings',
+          value: 'reuse',
+          description: prefDescription,
+        })
+      }
+
+      const { setupChoice } = await prompts(
+        {
+          type: 'select',
+          name: 'setupChoice',
+          message: 'Would you like to use the recommended Next.js defaults?',
+          choices,
+          initial: 0,
+        },
+        {
+          onCancel: () => {
+            console.error('Exiting.')
+            process.exit(1)
+          },
+        }
+      )
+
+      if (setupChoice === 'recommended') {
+        useRecommendedDefaults = true
+        skipPrompt = true
+      } else if (setupChoice === 'reuse') {
+        skipPrompt = true
+      }
+    }
+
+    // If using recommended defaults, populate preferences with defaults
+    // This ensures they are saved for reuse next time
+    if (useRecommendedDefaults) {
+      Object.assign(preferences, defaults)
+    }
+
+    const getPrefOrDefault = (field: string) => {
+      // If using recommended defaults, always use hardcoded defaults
+      if (useRecommendedDefaults) {
+        return defaults[field]
+      }
+
+      // If not using the recommended template, we prefer saved preferences, otherwise defaults.
+      return preferences[field] ?? defaults[field]
+    }
 
     if (!opts.typescript && !opts.javascript) {
       if (skipPrompt) {

filename: test/integration/create-next-app/lib/utils.ts
patch: @@ -30,10 +30,13 @@ const cli = require.resolve('create-next-app/dist/index.js')
 export const createNextApp = (
   args: string[],
   options?: SpawnOptions,
-  testVersion?: string
+  testVersion?: string,
+  clearPreferences: boolean = true
 ) => {
   const conf = new Conf({ projectName: 'create-next-app' })
-  conf.clear()
+  if (clearPreferences) {
+    conf.clear()
+  }
 
   console.log(`[TEST] $ ${cli} ${args.join(' ')}`, { options })
 

filename: test/integration/create-next-app/prompts.test.ts
patch: @@ -220,6 +220,106 @@ describe('create-next-app prompts', () => {
     })
   })
 
+  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
+            ],
+          })
+          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)
+      // Verify turbopack is in dev script
+      expect(pkg.scripts.dev).toContain('--turbo')
+    })
+  })
+
+  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,
+        turbopack: 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(