From 9a29cce2ce4ce8a5805d00e4b8bde54b444b0be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Wed, 7 Dec 2022 00:18:20 +0100 Subject: [PATCH 1/5] feat: add CLI keyboard shortcuts --- packages/vite/src/node/cli.ts | 19 +++- packages/vite/src/node/index.ts | 1 + packages/vite/src/node/server/index.ts | 20 ++++ packages/vite/src/node/shortcuts.ts | 136 +++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 packages/vite/src/node/shortcuts.ts 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/index.ts b/packages/vite/src/node/index.ts index 3ee329d8db6246..f27b99fc79a573 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -61,6 +61,7 @@ export type { LogType, LoggerOptions, } from './logger' +export type { BindShortcutsOptions, CLIShortcut } from './shortcuts' export type { IndexHtmlTransform, IndexHtmlTransformHook, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 795c9ea5246042..844dbbf9800cf1 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()) + }, + }, +] From 0f182332aef596c27070f451b934c1642aabab42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Wed, 7 Dec 2022 00:53:09 +0100 Subject: [PATCH 2/5] fix: additionalShortCuts -> customShortcuts --- packages/vite/src/node/cli.ts | 2 +- packages/vite/src/node/shortcuts.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index e6985780e8a9ec..156a64126e2701 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -152,7 +152,7 @@ cli server.printUrls() server.bindShortcuts({ print: true, - additionalShortCuts: [ + customShortcuts: [ profileSession && { key: 's', description: 'stop the profiler', diff --git a/packages/vite/src/node/shortcuts.ts b/packages/vite/src/node/shortcuts.ts index 03863c357ed37f..5e14b753399214 100644 --- a/packages/vite/src/node/shortcuts.ts +++ b/packages/vite/src/node/shortcuts.ts @@ -9,7 +9,7 @@ export type BindShortcutsOptions = { * Print a one line hint to the terminal. */ print?: boolean - additionalShortCuts?: (CLIShortcut | undefined | null)[] + customShortcuts?: (CLIShortcut | undefined | null)[] } export type CLIShortcut = { @@ -33,7 +33,7 @@ export function bindShortcuts( ) } - const shortcuts = (opts.additionalShortCuts ?? []) + const shortcuts = (opts.customShortcuts ?? []) .filter(isDefined) .concat(BASE_SHORTCUTS) From 70f006c88bf55debfd6994dd833126284a56779a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Wed, 7 Dec 2022 00:55:06 +0100 Subject: [PATCH 3/5] chore: use ??= --- packages/vite/src/node/shortcuts.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/vite/src/node/shortcuts.ts b/packages/vite/src/node/shortcuts.ts index 5e14b753399214..7b7772fa52b106 100644 --- a/packages/vite/src/node/shortcuts.ts +++ b/packages/vite/src/node/shortcuts.ts @@ -108,9 +108,7 @@ const BASE_SHORTCUTS: CLIShortcut[] = [ key: 'm', description: 'toggle hmr on/off', action({ config }: ViteDevServer): void { - if (initialHmrOptions === undefined) { - initialHmrOptions = config.server.hmr ?? true - } + initialHmrOptions ??= config.server.hmr ?? true /** * Mutating the server config works because Vite reads from * it on every file change, instead of caching its value. From 023f47d87d768395351c282ab23bed2a5f3cde42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Wed, 7 Dec 2022 17:01:03 +0100 Subject: [PATCH 4/5] fix: drop toggle hmr shortcut --- packages/vite/src/node/shortcuts.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/packages/vite/src/node/shortcuts.ts b/packages/vite/src/node/shortcuts.ts index 7b7772fa52b106..df981f4bcb7cfb 100644 --- a/packages/vite/src/node/shortcuts.ts +++ b/packages/vite/src/node/shortcuts.ts @@ -1,7 +1,6 @@ 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 = { @@ -80,8 +79,6 @@ export function bindShortcuts( }) } -let initialHmrOptions: HmrOptions | boolean - const BASE_SHORTCUTS: CLIShortcut[] = [ { key: 'r', @@ -104,26 +101,6 @@ const BASE_SHORTCUTS: CLIShortcut[] = [ openBrowser(url, true, server.config.logger) }, }, - { - key: 'm', - description: 'toggle hmr on/off', - action({ config }: ViteDevServer): void { - 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', From 3647c78ab5452f88dd6a894756c4e1dc7fe0875c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Wed, 7 Dec 2022 19:54:40 +0100 Subject: [PATCH 5/5] feat: restart profiler --- packages/vite/src/node/cli.ts | 49 +++++++++++++++++++------- packages/vite/src/node/index.ts | 1 - packages/vite/src/node/server/index.ts | 22 +++++------- packages/vite/src/node/shortcuts.ts | 1 + 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index 156a64126e2701..8863fd3cd42167 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -9,6 +9,7 @@ import type { ServerOptions } from './server' import type { LogLevel } from './logger' import { createLogger } from './logger' import { VERSION } from './constants' +import { bindShortcuts } from './shortcuts' import { resolveConfig } from '.' const cli = cac('vite') @@ -32,25 +33,33 @@ interface GlobalCLIOptions { } // @ts-ignore -const profileSession: Session | undefined = global.__vite_profile_session +let profileSession: Session | undefined = global.__vite_profile_session +let profileCount = 0 -export const stopProfiler = (log: (message: string) => void): void => { - if (profileSession) { - profileSession.post('Profiler.stop', (err: any, { profile }: any) => { +export const stopProfiler = ( + log: (message: string) => void, +): void | Promise => { + if (!profileSession) return + return new Promise((res, rej) => { + profileSession!.post('Profiler.stop', (err: any, { profile }: any) => { // Write profile to disk, upload, etc. if (!err) { - const outPath = path.resolve('./vite-profile.cpuprofile') + const outPath = path.resolve( + `./vite-profile-${profileCount++}.cpuprofile`, + ) fs.writeFileSync(outPath, JSON.stringify(profile)) log( colors.yellow( `CPU profile written to ${colors.white(colors.dim(outPath))}`, ), ) + profileSession = undefined + res() } else { - throw err + rej(err) } }) - } + }) } const filterDuplicateOptions = (options: T) => { @@ -150,14 +159,30 @@ cli ) server.printUrls() - server.bindShortcuts({ + bindShortcuts(server, { print: true, customShortcuts: [ profileSession && { - key: 's', - description: 'stop the profiler', - action(server) { - stopProfiler(server.config.logger.info) + key: 'p', + description: 'start/stop the profiler', + async action(server) { + if (profileSession) { + await stopProfiler(server.config.logger.info) + } else { + const inspector = await import('node:inspector').then( + (r) => r.default, + ) + await new Promise((res) => { + profileSession = new inspector.Session() + profileSession.connect() + profileSession.post('Profiler.enable', () => { + profileSession!.post('Profiler.start', () => { + server.config.logger.info('Profiler started') + res() + }) + }) + }) + } }, }, ], diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index f27b99fc79a573..3ee329d8db6246 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -61,7 +61,6 @@ export type { LogType, LoggerOptions, } from './logger' -export type { BindShortcutsOptions, CLIShortcut } from './shortcuts' export type { IndexHtmlTransform, IndexHtmlTransformHook, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 844dbbf9800cf1..4c26087d9ce2b8 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -264,10 +264,6 @@ export interface ViteDevServer { * Print server urls */ printUrls(): void - /** - * Listen to `process.stdin` for keyboard shortcuts. - */ - bindShortcuts(opts?: BindShortcutsOptions): void /** * Restart the server. * @@ -308,8 +304,11 @@ export interface ViteDevServer { _fsDenyGlob: Matcher /** * @internal + * Actually BindShortcutsOptions | undefined but api-extractor checks for + * export before trimming internal types :( + * And I don't want to complexity prePatchTypes for that */ - _shortcutsBound: boolean + _shortcutsOptions: any | undefined } export interface ResolvedServerUrls { @@ -445,10 +444,6 @@ export async function createServer( ) } }, - bindShortcuts(opts = {}) { - bindShortcuts(server, opts) - server._shortcutsBound = true - }, async restart(forceOptimize?: boolean) { if (!server._restartPromise) { server._forceOptimizeOnRestart = !!forceOptimize @@ -466,7 +461,7 @@ export async function createServer( _forceOptimizeOnRestart: false, _pendingRequests: new Map(), _fsDenyGlob: picomatch(config.server.fs.deny, { matchBase: true }), - _shortcutsBound: false, + _shortcutsOptions: undefined, } server.transformIndexHtml = createDevHtmlTransformFn(server) @@ -786,7 +781,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 + const shortcutsOptions: BindShortcutsOptions = server._shortcutsOptions await server.close() @@ -835,8 +830,9 @@ async function restartServer(server: ViteDevServer) { logger.info('server restarted.', { timestamp: true }) } - if (bindShortcuts) { - newServer.bindShortcuts() + if (shortcutsOptions) { + shortcutsOptions.print = false + bindShortcuts(newServer, shortcutsOptions) } // new server (the current server) can restart now diff --git a/packages/vite/src/node/shortcuts.ts b/packages/vite/src/node/shortcuts.ts index df981f4bcb7cfb..3d06d83fd93fed 100644 --- a/packages/vite/src/node/shortcuts.ts +++ b/packages/vite/src/node/shortcuts.ts @@ -22,6 +22,7 @@ export function bindShortcuts( opts: BindShortcutsOptions, ): void { if (!server.httpServer) return + server._shortcutsOptions = opts if (opts.print) { server.config.logger.info(