Skip to content

Commit 925cefe

Browse files
Copilotsapphi-redsun0daybluwy
authored
feat(create-vite): support auto install dependencies and start dev (#20468)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: sapphi-red <[email protected]> Co-authored-by: ivan <[email protected]> Co-authored-by: Bjorn Lu <[email protected]>
1 parent b3227d5 commit 925cefe

File tree

2 files changed

+124
-30
lines changed

2 files changed

+124
-30
lines changed

packages/create-vite/__tests__/cli.spec.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
3-
import type { SyncOptions, SyncResult } from 'execa'
3+
import type { SyncOptions } from 'execa'
44
import { execaCommandSync } from 'execa'
55
import { afterEach, beforeAll, expect, test } from 'vitest'
66

@@ -10,11 +10,11 @@ const projectName = 'test-app'
1010
const genPath = path.join(__dirname, projectName)
1111
const genPathWithSubfolder = path.join(__dirname, 'subfolder', projectName)
1212

13-
const run = <SO extends SyncOptions>(
14-
args: string[],
15-
options?: SO,
16-
): SyncResult<SO> => {
17-
return execaCommandSync(`node ${CLI_PATH} ${args.join(' ')}`, options)
13+
const run = (args: string[], options?: SyncOptions) => {
14+
return execaCommandSync(`node ${CLI_PATH} ${args.join(' ')}`, {
15+
env: { ...process.env, _VITE_TEST_CLI: 'true' },
16+
...options,
17+
})
1818
}
1919

2020
// Helper to create a non-empty directory
@@ -111,7 +111,14 @@ test('asks to overwrite non-empty current directory', () => {
111111

112112
test('successfully scaffolds a project based on vue starter template', () => {
113113
const { stdout } = run(
114-
[projectName, '--interactive', '--template', 'vue', '--no-rolldown'],
114+
[
115+
projectName,
116+
'--interactive',
117+
'--no-immediate',
118+
'--template',
119+
'vue',
120+
'--no-rolldown',
121+
],
115122
{
116123
cwd: __dirname,
117124
},
@@ -128,6 +135,7 @@ test('successfully scaffolds a project with subfolder based on react starter tem
128135
[
129136
`subfolder/${projectName}`,
130137
'--interactive',
138+
'--no-immediate',
131139
'--template',
132140
'react',
133141
'--no-rolldown',
@@ -164,7 +172,14 @@ test('successfully scaffolds a project with subfolder based on react starter tem
164172

165173
test('works with the -t alias', () => {
166174
const { stdout } = run(
167-
[projectName, '--interactive', '-t', 'vue', '--no-rolldown'],
175+
[
176+
projectName,
177+
'--interactive',
178+
'--no-immediate',
179+
'-t',
180+
'vue',
181+
'--no-rolldown',
182+
],
168183
{
169184
cwd: __dirname,
170185
},
@@ -214,3 +229,21 @@ test('sets index.html title to project name', () => {
214229
expect(stdout).toContain(`Scaffolding project in ${genPath}`)
215230
expect(indexHtmlContent).toContain(`<title>${projectName}</title>`)
216231
})
232+
233+
test('accepts immediate flag', () => {
234+
const { stdout } = run([projectName, '--template', 'vue', '--immediate'], {
235+
cwd: __dirname,
236+
})
237+
expect(stdout).not.toContain('Install and start now?')
238+
expect(stdout).toContain(`Scaffolding project in ${genPath}`)
239+
expect(stdout).toContain('Installing dependencies')
240+
})
241+
242+
test('accepts immediate flag and skips install prompt', () => {
243+
const { stdout } = run([projectName, '--template', 'vue', '--no-immediate'], {
244+
cwd: __dirname,
245+
})
246+
expect(stdout).not.toContain('Install and start now?')
247+
expect(stdout).not.toContain('Installing dependencies')
248+
expect(stdout).toContain(`Scaffolding project in ${genPath}`)
249+
})

packages/create-vite/src/index.ts

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ const argv = mri<{
2323
template?: string
2424
help?: boolean
2525
overwrite?: boolean
26+
immediate?: boolean
2627
rolldown?: boolean
2728
interactive?: boolean
2829
}>(process.argv.slice(2), {
29-
alias: { h: 'help', t: 'template' },
30-
boolean: ['help', 'overwrite', 'rolldown', 'interactive'],
30+
boolean: ['help', 'overwrite', 'immediate', 'rolldown', 'interactive'],
31+
alias: { h: 'help', t: 'template', i: 'immediate' },
3132
string: ['template'],
3233
})
3334
const cwd = process.cwd()
@@ -41,6 +42,7 @@ When running in TTY, the CLI will start in interactive mode.
4142
4243
Options:
4344
-t, --template NAME use a specific template
45+
-i, --immediate install dependencies and start dev
4446
--rolldown / --no-rolldown use / do not use rolldown-vite (Experimental)
4547
--interactive / --no-interactive force interactive / non-interactive mode
4648
@@ -344,12 +346,52 @@ const renameFiles: Record<string, string | undefined> = {
344346

345347
const defaultTargetDir = 'vite-project'
346348

349+
function run(...params: Parameters<typeof spawn.sync>) {
350+
const { status, error } = spawn.sync(...params)
351+
if (status != null && status > 0) {
352+
process.exit(status)
353+
}
354+
355+
if (error) {
356+
console.error(`\n${params.slice(0, -1).join(' ')} error!`)
357+
console.error(error)
358+
process.exit(1)
359+
}
360+
}
361+
362+
function install(root: string, agent: string) {
363+
if (process.env._VITE_TEST_CLI) {
364+
prompts.log.step(
365+
`Installing dependencies with ${agent}... (skipped in test)`,
366+
)
367+
return
368+
}
369+
prompts.log.step(`Installing dependencies with ${agent}...`)
370+
run(agent, agent === 'yarn' ? [] : ['install'], {
371+
stdio: 'inherit',
372+
cwd: root,
373+
})
374+
}
375+
376+
function start(root: string, agent: string) {
377+
if (process.env._VITE_TEST_CLI) {
378+
prompts.log.step('Starting dev server... (skipped in test)')
379+
return
380+
}
381+
prompts.log.step('Starting dev server...')
382+
run(agent, agent === 'npm' ? ['run', 'dev'] : ['dev'], {
383+
stdio: 'inherit',
384+
cwd: root,
385+
})
386+
}
387+
347388
async function init() {
348389
const argTargetDir = argv._[0]
349390
? formatTargetDir(String(argv._[0]))
350391
: undefined
351392
const argTemplate = argv.template
352393
const argOverwrite = argv.overwrite
394+
const argImmediate = argv.immediate
353395
const argRolldown = argv.rolldown
354396
const argInteractive = argv.interactive
355397

@@ -499,6 +541,22 @@ async function init() {
499541
}
500542
}
501543

544+
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
545+
546+
// 5. Ask about immediate install and package manager
547+
let immediate = argImmediate
548+
if (immediate === undefined) {
549+
if (interactive) {
550+
const immediateResult = await prompts.confirm({
551+
message: `Install with ${pkgManager} and start now?`,
552+
})
553+
if (prompts.isCancel(immediateResult)) return cancel()
554+
immediate = immediateResult
555+
} else {
556+
immediate = false
557+
}
558+
}
559+
502560
const root = path.join(cwd, targetDir)
503561
fs.mkdirSync(root, { recursive: true })
504562

@@ -509,8 +567,6 @@ async function init() {
509567
template = template.replace('-swc', '')
510568
}
511569

512-
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
513-
514570
const { customCommand } =
515571
FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {}
516572

@@ -613,25 +669,30 @@ async function init() {
613669
setupReactSwc(root, template.endsWith('-ts'))
614670
}
615671

616-
let doneMessage = ''
617-
const cdProjectName = path.relative(cwd, root)
618-
doneMessage += `Done. Now run:\n`
619-
if (root !== cwd) {
620-
doneMessage += `\n cd ${
621-
cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName
622-
}`
623-
}
624-
switch (pkgManager) {
625-
case 'yarn':
626-
doneMessage += '\n yarn'
627-
doneMessage += '\n yarn dev'
628-
break
629-
default:
630-
doneMessage += `\n ${pkgManager} install`
631-
doneMessage += `\n ${pkgManager} run dev`
632-
break
672+
if (immediate) {
673+
install(root, pkgManager)
674+
start(root, pkgManager)
675+
} else {
676+
let doneMessage = ''
677+
const cdProjectName = path.relative(cwd, root)
678+
doneMessage += `Done. Now run:\n`
679+
if (root !== cwd) {
680+
doneMessage += `\n cd ${
681+
cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName
682+
}`
683+
}
684+
switch (pkgManager) {
685+
case 'yarn':
686+
doneMessage += '\n yarn'
687+
doneMessage += '\n yarn dev'
688+
break
689+
default:
690+
doneMessage += `\n ${pkgManager} install`
691+
doneMessage += `\n ${pkgManager} run dev`
692+
break
693+
}
694+
prompts.outro(doneMessage)
633695
}
634-
prompts.outro(doneMessage)
635696
}
636697

637698
function formatTargetDir(targetDir: string) {

0 commit comments

Comments
 (0)