Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add CLI keyboard shortcuts #9673

Closed
wants to merge 14 commits into from
Closed
1 change: 1 addition & 0 deletions packages/vite/src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ cli
)

server.printUrls()
server.bindShortcuts()
} catch (e) {
createLogger(options.logLevel).error(
colors.red(`error when starting dev server:\n${e.stack}`),
Expand Down
18 changes: 18 additions & 0 deletions packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { openBrowser } from './openBrowser'
import type { TransformOptions, TransformResult } from './transformRequest'
import { transformRequest } from './transformRequest'
import { searchForWorkspaceRoot } from './searchRoot'
import { bindShortcuts } from './shortcuts'

export { searchForWorkspaceRoot } from './searchRoot'

Expand Down Expand Up @@ -114,6 +115,13 @@ export interface ServerOptions extends CommonServerOptions {
* in a future minor version without following semver
*/
force?: boolean
/**
* Disable key bindings for the server by setting this to `false`. This can be
* useful if you need the `process.stdin` stream for another purpose.
*
* @default true in CLI, `false` in programmatic usage
*/
bindShortcuts?: boolean
Comment on lines +118 to +124
Copy link
Member

@ArnaudBarre ArnaudBarre Nov 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this is not called at all in programmatic usage. (like printURLs)

There is currently no option to disable print of server urls, I would do the same here.

If people want more complex use case, they can use the pragmatic usage.

}

export interface ResolvedServerOptions extends ServerOptions {
Expand Down Expand Up @@ -255,6 +263,11 @@ export interface ViteDevServer {
* @param forceOptimize - force the optimizer to re-bundle, same as --force cli flag
*/
restart(forceOptimize?: boolean): Promise<void>
/**
* Listen to `process.stdin` for pre-defined keyboard shortcuts, which are
* printed to the terminal by this method.
Comment on lines +267 to +268
Copy link
Member

@ArnaudBarre ArnaudBarre Nov 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What can be useful for non standard custom usage is separating biding from help display with a little boolean flag on the method

*/
bindShortcuts(): void
/**
* @internal
*/
Expand Down Expand Up @@ -421,6 +434,11 @@ export async function createServer(
}
return server._restartPromise
},
bindShortcuts() {
if (serverConfig.bindShortcuts !== false) {
bindShortcuts(server)
}
},

_ssrExternals: null,
_restartPromise: null,
Expand Down
124 changes: 124 additions & 0 deletions packages/vite/src/node/server/shortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import colors from 'picocolors'
import type { ViteDevServer } from '..'
import { openBrowser } from './openBrowser'

export function bindShortcuts(server: ViteDevServer): void {
if (!server.httpServer) return

const helpInfo =
colors.dim('press ') +
colors.reset(colors.bold('h')) +
colors.dim(' to show help')
const quitInfo =
colors.dim('press ') +
colors.reset(colors.bold('q')) +
colors.dim(' to quit')

server.config.logger.info(
colors.dim(` ${colors.green('➜')} ${helpInfo}, ${quitInfo}`)
)

let actionRunning = false

const onInput = async (input: string) => {
// ctrl+c or ctrl+d
if (input === '\x03' || input === '\x04') {
return process.kill(process.pid, 'SIGINT')
}

if (input === 'h') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be itself a shortcut. This is common for CLI to print the help option in the help itself

return server.config.logger.info(
SHORTCUTS.map(
(shortcut) =>
colors.dim(' press ') +
colors.reset(colors.bold(shortcut.key)) +
colors.dim(` to ${shortcut.description}`)
).join('\n')
)
}

const shortcut = SHORTCUTS.find((shortcut) => shortcut.key === input)

if (!shortcut) {
return
}

if (actionRunning) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this should be the second check

return
}

actionRunning = true
await shortcut.action(server)
actionRunning = false
}

if (process.stdin.isTTY) {
process.stdin.setRawMode(true)
}

process.stdin.on('data', onInput).setEncoding('utf8').resume()

server.httpServer.on('close', () => {
process.stdin.off('data', onInput).pause()
})
}

export interface Shortcut {
key: string
description: string
action(server: ViteDevServer): void | Promise<void>
}

export const SHORTCUTS: Shortcut[] = [
Comment on lines +66 to +72
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be exported?

{
key: 'r',
description: 'restart the server',
async action(server: ViteDevServer): Promise<void> {
await server.restart()
server.bindShortcuts()
}
},
{
key: 'o',
description: 'open in browser',
action(server: ViteDevServer): void {
const url = server.resolvedUrls?.local[0]

if (!url) {
return server.config.logger.warn(
colors.yellow(`cannot open in browser; no server URLs registered.`)
)
}

openBrowser(url, true, server.config.logger)
}
},
{
key: 'm',
description: 'toggle hmr on/off',
action({ config }: ViteDevServer): void {
/**
* Mutating the server config works because Vite reads from
* it on every file change, instead of caching its value.
*
* Since `undefined` is treated as `true`, we have to
* use `!== true` to flip the boolean value.
*/
config.server.hmr = config.server.hmr !== true
config.logger.info(
colors.cyan(` hmr ${config.server.hmr ? `enabled` : `disabled`}`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to test but I think the two whitespaces at the beginning will look strange in the middle of Vite dev server logs

)
}
},
{
key: 'q',
description: 'quit',
async action(server: ViteDevServer): Promise<void> {
try {
await server.close()
} finally {
process.exit()
}
}
}
]