Skip to content

Commit 0dc7498

Browse files
authored
[create-next-app]: prompt to use recommended options (#84570)
`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
1 parent ed2e69d commit 0dc7498

File tree

3 files changed

+225
-5
lines changed

3 files changed

+225
-5
lines changed

packages/create-next-app/index.ts

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,8 @@ async function run(): Promise<void> {
227227
* If the user does not provide the necessary flags, prompt them for their
228228
* preferences, unless `--yes` option was specified, or when running in CI.
229229
*/
230-
const skipPrompt = ciInfo.isCI || opts.yes
230+
let skipPrompt = ciInfo.isCI || opts.yes
231+
let useRecommendedDefaults = false
231232

232233
if (!example) {
233234
const defaults: typeof preferences = {
@@ -244,8 +245,124 @@ async function run(): Promise<void> {
244245
disableGit: false,
245246
reactCompiler: false,
246247
}
247-
const getPrefOrDefault = (field: string) =>
248-
preferences[field] ?? defaults[field]
248+
249+
type DisplayConfigItem = {
250+
key: keyof typeof defaults
251+
values?: Record<string, string>
252+
}
253+
254+
const displayConfig: DisplayConfigItem[] = [
255+
{
256+
key: 'typescript',
257+
values: { true: 'TypeScript', false: 'JavaScript' },
258+
},
259+
{ key: 'linter', values: { eslint: 'ESLint', biome: 'Biome' } },
260+
{ key: 'reactCompiler', values: { true: 'React Compiler' } },
261+
{ key: 'tailwind', values: { true: 'Tailwind CSS' } },
262+
{ key: 'srcDir', values: { true: 'src/ dir' } },
263+
{ key: 'app', values: { true: 'App Router', false: 'Pages Router' } },
264+
{ key: 'turbopack', values: { true: 'Turbopack' } },
265+
]
266+
267+
// Helper to format settings for display based on displayConfig
268+
const formatSettingsDescription = (
269+
settings: Record<string, boolean | string>
270+
) => {
271+
const descriptions: string[] = []
272+
273+
for (const config of displayConfig) {
274+
const value = settings[config.key]
275+
276+
if (config.values) {
277+
// Look up the display label for this value
278+
const label = config.values[String(value)]
279+
if (label) {
280+
descriptions.push(label)
281+
}
282+
}
283+
}
284+
285+
return descriptions.join(', ')
286+
}
287+
288+
// Check if we have saved preferences
289+
const hasSavedPreferences = Object.keys(preferences).length > 0
290+
291+
// Check if user provided any configuration flags
292+
// If they did, skip the "recommended defaults" prompt and go straight to
293+
// individual prompts for any missing options
294+
const hasProvidedOptions = process.argv.some((arg) => arg.startsWith('--'))
295+
296+
// Only show the "recommended defaults" prompt if:
297+
// - Not in CI and not using --yes flag
298+
// - User hasn't provided any custom options
299+
if (!skipPrompt && !hasProvidedOptions) {
300+
const choices: Array<{
301+
title: string
302+
value: string
303+
description?: string
304+
}> = [
305+
{
306+
title: 'Yes, use recommended defaults',
307+
value: 'recommended',
308+
description: formatSettingsDescription(defaults),
309+
},
310+
{
311+
title: 'No, customize settings',
312+
value: 'customize',
313+
description: 'Choose your own preferences',
314+
},
315+
]
316+
317+
// Add "reuse previous settings" option if we have saved preferences
318+
if (hasSavedPreferences) {
319+
const prefDescription = formatSettingsDescription(preferences)
320+
choices.splice(1, 0, {
321+
title: 'No, reuse previous settings',
322+
value: 'reuse',
323+
description: prefDescription,
324+
})
325+
}
326+
327+
const { setupChoice } = await prompts(
328+
{
329+
type: 'select',
330+
name: 'setupChoice',
331+
message: 'Would you like to use the recommended Next.js defaults?',
332+
choices,
333+
initial: 0,
334+
},
335+
{
336+
onCancel: () => {
337+
console.error('Exiting.')
338+
process.exit(1)
339+
},
340+
}
341+
)
342+
343+
if (setupChoice === 'recommended') {
344+
useRecommendedDefaults = true
345+
skipPrompt = true
346+
} else if (setupChoice === 'reuse') {
347+
skipPrompt = true
348+
}
349+
}
350+
351+
// If using recommended defaults, populate preferences with defaults
352+
// This ensures they are saved for reuse next time
353+
if (useRecommendedDefaults) {
354+
Object.assign(preferences, defaults)
355+
}
356+
357+
const getPrefOrDefault = (field: string) => {
358+
// If using recommended defaults, always use hardcoded defaults
359+
if (useRecommendedDefaults) {
360+
return defaults[field]
361+
}
362+
363+
// If not using the recommended template, we prefer saved preferences, otherwise defaults.
364+
return preferences[field] ?? defaults[field]
365+
}
249366

250367
if (!opts.typescript && !opts.javascript) {
251368
if (skipPrompt) {

test/integration/create-next-app/lib/utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ const cli = require.resolve('create-next-app/dist/index.js')
3030
export const createNextApp = (
3131
args: string[],
3232
options?: SpawnOptions,
33-
testVersion?: string
33+
testVersion?: string,
34+
clearPreferences: boolean = true
3435
) => {
3536
const conf = new Conf({ projectName: 'create-next-app' })
36-
conf.clear()
37+
if (clearPreferences) {
38+
conf.clear()
39+
}
3740

3841
console.log(`[TEST] $ ${cli} ${args.join(' ')}`, { options })
3942

test/integration/create-next-app/prompts.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,106 @@ describe('create-next-app prompts', () => {
220220
})
221221
})
222222

223+
it('should use recommended defaults when user selects that option', async () => {
224+
await useTempDir(async (cwd) => {
225+
const projectName = 'recommended-defaults'
226+
const childProcess = createNextApp(
227+
[projectName],
228+
{
229+
cwd,
230+
},
231+
nextTgzFilename
232+
)
233+
234+
await new Promise<void>((resolve) => {
235+
childProcess.on('exit', async (exitCode) => {
236+
expect(exitCode).toBe(0)
237+
projectFilesShouldExist({
238+
cwd,
239+
projectName,
240+
files: [
241+
'app',
242+
'package.json',
243+
'postcss.config.mjs', // tailwind
244+
'tsconfig.json', // typescript
245+
],
246+
})
247+
resolve()
248+
})
249+
250+
// Select "Yes, use recommended defaults" (default option, just press enter)
251+
childProcess.stdin.write('\n')
252+
})
253+
254+
const pkg = require(join(cwd, projectName, 'package.json'))
255+
expect(pkg.name).toBe(projectName)
256+
// Verify turbopack is in dev script
257+
expect(pkg.scripts.dev).toContain('--turbo')
258+
})
259+
})
260+
261+
it('should show reuse previous settings option when preferences exist', async () => {
262+
const Conf = require('next/dist/compiled/conf')
263+
264+
await useTempDir(async (cwd) => {
265+
// Manually set preferences to simulate a previous run
266+
const conf = new Conf({ projectName: 'create-next-app' })
267+
conf.set('preferences', {
268+
typescript: false,
269+
eslint: true,
270+
linter: 'eslint',
271+
tailwind: false,
272+
app: false,
273+
srcDir: false,
274+
importAlias: '@/*',
275+
customizeImportAlias: false,
276+
turbopack: false,
277+
reactCompiler: false,
278+
})
279+
280+
const projectName = 'reuse-prefs-project'
281+
const childProcess = createNextApp(
282+
[projectName],
283+
{
284+
cwd,
285+
},
286+
nextTgzFilename,
287+
false // Don't clear preferences
288+
)
289+
290+
await new Promise<void>(async (resolve) => {
291+
let output = ''
292+
childProcess.stdout.on('data', (data) => {
293+
output += data
294+
process.stdout.write(data)
295+
})
296+
297+
// Select "reuse previous settings" (cursor down once, then enter)
298+
childProcess.stdin.write('\u001b[B\n')
299+
300+
// Wait for the prompt to appear with "reuse previous settings"
301+
await check(() => output, /No, reuse previous settings/)
302+
303+
childProcess.on('exit', async (exitCode) => {
304+
expect(exitCode).toBe(0)
305+
projectFilesShouldExist({
306+
cwd,
307+
projectName,
308+
files: [
309+
'pages', // pages router (not app)
310+
'package.json',
311+
'jsconfig.json', // javascript
312+
],
313+
})
314+
resolve()
315+
})
316+
})
317+
318+
const pkg = require(join(cwd, projectName, 'package.json'))
319+
expect(pkg.name).toBe(projectName)
320+
})
321+
})
322+
223323
it('should prompt user to confirm reset preferences', async () => {
224324
await useTempDir(async (cwd) => {
225325
const childProcess = createNextApp(

0 commit comments

Comments
 (0)