diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index e73feee62d0955..e6985780e8a9ec 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -1,6 +1,7 @@ import path from 'node:path' import fs from 'node:fs' import { performance } from 'node:perf_hooks' +import type { Session } from 'node:inspector' import { cac } from 'cac' import colors from 'picocolors' import type { BuildOptions } from './build' @@ -30,9 +31,10 @@ interface GlobalCLIOptions { force?: boolean } +// @ts-ignore +const profileSession: Session | undefined = global.__vite_profile_session + export const stopProfiler = (log: (message: string) => void): void => { - // @ts-ignore - const profileSession = global.__vite_profile_session if (profileSession) { profileSession.post('Profiler.stop', (err: any, { profile }: any) => { // Write profile to disk, upload, etc. @@ -148,7 +150,18 @@ cli ) server.printUrls() - stopProfiler((message) => server.config.logger.info(` ${message}`)) + server.bindShortcuts({ + print: true, + additionalShortCuts: [ + profileSession && { + key: 's', + description: 'stop the profiler', + action(server) { + stopProfiler(server.config.logger.info) + }, + }, + ], + }) } catch (e) { const logger = createLogger(options.logLevel) logger.error(colors.red(`error when starting dev server:\n${e.stack}`), { diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index a73f8175884b5a..57e81fd4e4eaba 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -41,6 +41,8 @@ import { initDepsOptimizer, initDevSsrDepsOptimizer, } from '../optimizer' +import { bindShortcuts } from '../shortcuts' +import type { BindShortcutsOptions } from '../shortcuts' import { CLIENT_DIR } from '../constants' import type { Logger } from '../logger' import { printServerUrls } from '../logger' @@ -262,6 +264,10 @@ export interface ViteDevServer { * Print server urls */ printUrls(): void + /** + * Listen to `process.stdin` for keyboard shortcuts. + */ + bindShortcuts(opts?: BindShortcutsOptions): void /** * Restart the server. * @@ -300,6 +306,10 @@ export interface ViteDevServer { * @internal */ _fsDenyGlob: Matcher + /** + * @internal + */ + _shortcutsBound: boolean } export interface ResolvedServerUrls { @@ -435,6 +445,10 @@ export async function createServer( ) } }, + bindShortcuts(opts = {}) { + bindShortcuts(server, opts) + server._shortcutsBound = true + }, async restart(forceOptimize?: boolean) { if (!server._restartPromise) { server._forceOptimizeOnRestart = !!forceOptimize @@ -452,6 +466,7 @@ export async function createServer( _forceOptimizeOnRestart: false, _pendingRequests: new Map(), _fsDenyGlob: picomatch(config.server.fs.deny, { matchBase: true }), + _shortcutsBound: false, } server.transformIndexHtml = createDevHtmlTransformFn(server) @@ -771,6 +786,7 @@ async function restartServer(server: ViteDevServer) { // @ts-ignore global.__vite_start_time = performance.now() const { port: prevPort, host: prevHost } = server.config.server + const bindShortcuts = server._shortcutsBound await server.close() @@ -819,6 +835,10 @@ async function restartServer(server: ViteDevServer) { logger.info('server restarted.', { timestamp: true }) } + if (bindShortcuts) { + newServer.bindShortcuts() + } + // new server (the current server) can restart now newServer._restartPromise = null } diff --git a/packages/vite/src/node/shortcuts.ts b/packages/vite/src/node/shortcuts.ts new file mode 100644 index 00000000000000..03863c357ed37f --- /dev/null +++ b/packages/vite/src/node/shortcuts.ts @@ -0,0 +1,136 @@ +import colors from 'picocolors' +import type { ViteDevServer } from './server' +import { openBrowser } from './server/openBrowser' +import type { HmrOptions } from './server/hmr' +import { isDefined } from './utils' + +export type BindShortcutsOptions = { + /** + * Print a one line hint to the terminal. + */ + print?: boolean + additionalShortCuts?: (CLIShortcut | undefined | null)[] +} + +export type CLIShortcut = { + key: string + description: string + action(server: ViteDevServer): void | Promise +} + +export function bindShortcuts( + server: ViteDevServer, + opts: BindShortcutsOptions, +): void { + if (!server.httpServer) return + + if (opts.print) { + server.config.logger.info( + colors.dim(colors.green(' ➜')) + + colors.dim(' press ') + + colors.bold('h') + + colors.dim(' to show help'), + ) + } + + const shortcuts = (opts.additionalShortCuts ?? []) + .filter(isDefined) + .concat(BASE_SHORTCUTS) + + let actionRunning = false + + const onInput = async (input: string) => { + // ctrl+c or ctrl+d + if (input === '\x03' || input === '\x04') { + process.emit('SIGTERM') + return + } + + if (actionRunning) return + + if (input === 'h') { + server.config.logger.info( + shortcuts + .map( + (shortcut) => + colors.dim(' press ') + + colors.bold(shortcut.key) + + colors.dim(` to ${shortcut.description}`), + ) + .join('\n'), + ) + } + + const shortcut = shortcuts.find((shortcut) => shortcut.key === input) + if (!shortcut) 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() + }) +} + +let initialHmrOptions: HmrOptions | boolean + +const BASE_SHORTCUTS: CLIShortcut[] = [ + { + key: 'r', + description: 'restart the server', + async action(server) { + await server.restart() + }, + }, + { + key: 'o', + description: 'open in browser', + action(server) { + const url = server.resolvedUrls?.local[0] + + if (!url) { + server.config.logger.warn('No URL available to open in browser') + return + } + + openBrowser(url, true, server.config.logger) + }, + }, + { + key: 'm', + description: 'toggle hmr on/off', + action({ config }: ViteDevServer): void { + if (initialHmrOptions === undefined) { + initialHmrOptions = config.server.hmr ?? true + } + /** + * Mutating the server config works because Vite reads from + * it on every file change, instead of caching its value. + */ + config.server.hmr = + config.server.hmr === false + ? initialHmrOptions === false + ? true + : initialHmrOptions + : false + config.logger.info( + colors.cyan(`hmr ${config.server.hmr ? `enabled` : `disabled`}`), + ) + }, + }, + { + key: 'q', + description: 'quit', + async action(server) { + await server.close().finally(() => process.exit()) + }, + }, +]