Skip to content

Commit 393ef4b

Browse files
committed
[create-next-app]: prompt to use recommended options
1 parent 86e006b commit 393ef4b

File tree

3 files changed

+228
-5
lines changed

3 files changed

+228
-5
lines changed

packages/create-next-app/index.ts

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

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

248368
if (!opts.typescript && !opts.javascript) {
249369
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)