Skip to content

Commit 3d1f7f2

Browse files
committed
[create-next-app]: prompt to use recommended options
1 parent 4c4a5bc commit 3d1f7f2

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

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