Skip to content

Commit 5755244

Browse files
ArnaudBarrefuturGH
authored andcommitted
feat: add CLI keyboard shortcuts (vitejs#11228)
1 parent 59018d3 commit 5755244

File tree

3 files changed

+175
-9
lines changed

3 files changed

+175
-9
lines changed

packages/vite/src/node/cli.ts

+47-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import path from 'node:path'
22
import fs from 'node:fs'
33
import { performance } from 'node:perf_hooks'
4+
import type { Session } from 'node:inspector'
45
import { cac } from 'cac'
56
import colors from 'picocolors'
67
import type { BuildOptions } from './build'
78
import type { ServerOptions } from './server'
89
import type { LogLevel } from './logger'
910
import { createLogger } from './logger'
1011
import { VERSION } from './constants'
12+
import { bindShortcuts } from './shortcuts'
1113
import { resolveConfig } from '.'
1214

1315
const cli = cac('vite')
@@ -30,25 +32,34 @@ interface GlobalCLIOptions {
3032
force?: boolean
3133
}
3234

33-
export const stopProfiler = (log: (message: string) => void): void => {
34-
// @ts-ignore
35-
const profileSession = global.__vite_profile_session
36-
if (profileSession) {
37-
profileSession.post('Profiler.stop', (err: any, { profile }: any) => {
35+
// @ts-ignore
36+
let profileSession: Session | undefined = global.__vite_profile_session
37+
let profileCount = 0
38+
39+
export const stopProfiler = (
40+
log: (message: string) => void,
41+
): void | Promise<void> => {
42+
if (!profileSession) return
43+
return new Promise((res, rej) => {
44+
profileSession!.post('Profiler.stop', (err: any, { profile }: any) => {
3845
// Write profile to disk, upload, etc.
3946
if (!err) {
40-
const outPath = path.resolve('./vite-profile.cpuprofile')
47+
const outPath = path.resolve(
48+
`./vite-profile-${profileCount++}.cpuprofile`,
49+
)
4150
fs.writeFileSync(outPath, JSON.stringify(profile))
4251
log(
4352
colors.yellow(
4453
`CPU profile written to ${colors.white(colors.dim(outPath))}`,
4554
),
4655
)
56+
profileSession = undefined
57+
res()
4758
} else {
48-
throw err
59+
rej(err)
4960
}
5061
})
51-
}
62+
})
5263
}
5364

5465
const filterDuplicateOptions = <T extends object>(options: T) => {
@@ -148,7 +159,34 @@ cli
148159
)
149160

150161
server.printUrls()
151-
stopProfiler((message) => server.config.logger.info(` ${message}`))
162+
bindShortcuts(server, {
163+
print: true,
164+
customShortcuts: [
165+
profileSession && {
166+
key: 'p',
167+
description: 'start/stop the profiler',
168+
async action(server) {
169+
if (profileSession) {
170+
await stopProfiler(server.config.logger.info)
171+
} else {
172+
const inspector = await import('node:inspector').then(
173+
(r) => r.default,
174+
)
175+
await new Promise<void>((res) => {
176+
profileSession = new inspector.Session()
177+
profileSession.connect()
178+
profileSession.post('Profiler.enable', () => {
179+
profileSession!.post('Profiler.start', () => {
180+
server.config.logger.info('Profiler started')
181+
res()
182+
})
183+
})
184+
})
185+
}
186+
},
187+
},
188+
],
189+
})
152190
} catch (e) {
153191
const logger = createLogger(options.logLevel)
154192
logger.error(colors.red(`error when starting dev server:\n${e.stack}`), {

packages/vite/src/node/server/index.ts

+16
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import {
4141
initDepsOptimizer,
4242
initDevSsrDepsOptimizer,
4343
} from '../optimizer'
44+
import { bindShortcuts } from '../shortcuts'
45+
import type { BindShortcutsOptions } from '../shortcuts'
4446
import { CLIENT_DIR } from '../constants'
4547
import type { Logger } from '../logger'
4648
import { printServerUrls } from '../logger'
@@ -300,6 +302,13 @@ export interface ViteDevServer {
300302
* @internal
301303
*/
302304
_fsDenyGlob: Matcher
305+
/**
306+
* @internal
307+
* Actually BindShortcutsOptions | undefined but api-extractor checks for
308+
* export before trimming internal types :(
309+
* And I don't want to complexity prePatchTypes for that
310+
*/
311+
_shortcutsOptions: any | undefined
303312
}
304313

305314
export interface ResolvedServerUrls {
@@ -452,6 +461,7 @@ export async function createServer(
452461
_forceOptimizeOnRestart: false,
453462
_pendingRequests: new Map(),
454463
_fsDenyGlob: picomatch(config.server.fs.deny, { matchBase: true }),
464+
_shortcutsOptions: undefined,
455465
}
456466

457467
server.transformIndexHtml = createDevHtmlTransformFn(server)
@@ -771,6 +781,7 @@ async function restartServer(server: ViteDevServer) {
771781
// @ts-ignore
772782
global.__vite_start_time = performance.now()
773783
const { port: prevPort, host: prevHost } = server.config.server
784+
const shortcutsOptions: BindShortcutsOptions = server._shortcutsOptions
774785

775786
await server.close()
776787

@@ -819,6 +830,11 @@ async function restartServer(server: ViteDevServer) {
819830
logger.info('server restarted.', { timestamp: true })
820831
}
821832

833+
if (shortcutsOptions) {
834+
shortcutsOptions.print = false
835+
bindShortcuts(newServer, shortcutsOptions)
836+
}
837+
822838
// new server (the current server) can restart now
823839
newServer._restartPromise = null
824840
}

packages/vite/src/node/shortcuts.ts

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import colors from 'picocolors'
2+
import type { ViteDevServer } from './server'
3+
import { openBrowser } from './server/openBrowser'
4+
import { isDefined } from './utils'
5+
6+
export type BindShortcutsOptions = {
7+
/**
8+
* Print a one line hint to the terminal.
9+
*/
10+
print?: boolean
11+
customShortcuts?: (CLIShortcut | undefined | null)[]
12+
}
13+
14+
export type CLIShortcut = {
15+
key: string
16+
description: string
17+
action(server: ViteDevServer): void | Promise<void>
18+
}
19+
20+
export function bindShortcuts(
21+
server: ViteDevServer,
22+
opts: BindShortcutsOptions,
23+
): void {
24+
if (!server.httpServer) return
25+
server._shortcutsOptions = opts
26+
27+
if (opts.print) {
28+
server.config.logger.info(
29+
colors.dim(colors.green(' ➜')) +
30+
colors.dim(' press ') +
31+
colors.bold('h') +
32+
colors.dim(' to show help'),
33+
)
34+
}
35+
36+
const shortcuts = (opts.customShortcuts ?? [])
37+
.filter(isDefined)
38+
.concat(BASE_SHORTCUTS)
39+
40+
let actionRunning = false
41+
42+
const onInput = async (input: string) => {
43+
// ctrl+c or ctrl+d
44+
if (input === '\x03' || input === '\x04') {
45+
process.emit('SIGTERM')
46+
return
47+
}
48+
49+
if (actionRunning) return
50+
51+
if (input === 'h') {
52+
server.config.logger.info(
53+
shortcuts
54+
.map(
55+
(shortcut) =>
56+
colors.dim(' press ') +
57+
colors.bold(shortcut.key) +
58+
colors.dim(` to ${shortcut.description}`),
59+
)
60+
.join('\n'),
61+
)
62+
}
63+
64+
const shortcut = shortcuts.find((shortcut) => shortcut.key === input)
65+
if (!shortcut) return
66+
67+
actionRunning = true
68+
await shortcut.action(server)
69+
actionRunning = false
70+
}
71+
72+
if (process.stdin.isTTY) {
73+
process.stdin.setRawMode(true)
74+
}
75+
76+
process.stdin.on('data', onInput).setEncoding('utf8').resume()
77+
78+
server.httpServer.on('close', () => {
79+
process.stdin.off('data', onInput).pause()
80+
})
81+
}
82+
83+
const BASE_SHORTCUTS: CLIShortcut[] = [
84+
{
85+
key: 'r',
86+
description: 'restart the server',
87+
async action(server) {
88+
await server.restart()
89+
},
90+
},
91+
{
92+
key: 'o',
93+
description: 'open in browser',
94+
action(server) {
95+
const url = server.resolvedUrls?.local[0]
96+
97+
if (!url) {
98+
server.config.logger.warn('No URL available to open in browser')
99+
return
100+
}
101+
102+
openBrowser(url, true, server.config.logger)
103+
},
104+
},
105+
{
106+
key: 'q',
107+
description: 'quit',
108+
async action(server) {
109+
await server.close().finally(() => process.exit())
110+
},
111+
},
112+
]

0 commit comments

Comments
 (0)