Skip to content

Commit 5909efd

Browse files
authored
fix: allow multiple bindCLIShortcuts calls with shortcut merging (#21103)
1 parent 0ec8aeb commit 5909efd

File tree

4 files changed

+110
-7
lines changed

4 files changed

+110
-7
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, expect, test, vi } from 'vitest'
2+
import { createServer } from '../server'
3+
import { preview } from '../preview'
4+
import { bindCLIShortcuts } from '../shortcuts'
5+
6+
describe('bindCLIShortcuts', () => {
7+
test.each([
8+
['dev server', () => createServer()],
9+
['preview server', () => preview()],
10+
])('binding custom shortcuts with the %s', async (_, startServer) => {
11+
const server = await startServer()
12+
13+
try {
14+
const xAction = vi.fn()
15+
const yAction = vi.fn()
16+
17+
bindCLIShortcuts(
18+
server,
19+
{
20+
customShortcuts: [
21+
{ key: 'x', description: 'test x', action: xAction },
22+
{ key: 'y', description: 'test y', action: yAction },
23+
],
24+
},
25+
true,
26+
)
27+
28+
expect.assert(
29+
server._rl,
30+
'The readline interface should be defined after binding shortcuts.',
31+
)
32+
expect(xAction).not.toHaveBeenCalled()
33+
34+
server._rl.emit('line', 'x')
35+
await vi.waitFor(() => expect(xAction).toHaveBeenCalledOnce())
36+
37+
const xUpdatedAction = vi.fn()
38+
const zAction = vi.fn()
39+
40+
xAction.mockClear()
41+
bindCLIShortcuts(
42+
server,
43+
{
44+
customShortcuts: [
45+
{ key: 'x', description: 'test x updated', action: xUpdatedAction },
46+
{ key: 'z', description: 'test z', action: zAction },
47+
],
48+
},
49+
true,
50+
)
51+
52+
expect(xUpdatedAction).not.toHaveBeenCalled()
53+
server._rl.emit('line', 'x')
54+
await vi.waitFor(() => expect(xUpdatedAction).toHaveBeenCalledOnce())
55+
56+
// Ensure original xAction is not called again
57+
expect(xAction).not.toBeCalled()
58+
59+
expect(yAction).not.toHaveBeenCalled()
60+
server._rl.emit('line', 'y')
61+
await vi.waitFor(() => expect(yAction).toHaveBeenCalledOnce())
62+
63+
expect(zAction).not.toHaveBeenCalled()
64+
server._rl.emit('line', 'z')
65+
await vi.waitFor(() => expect(zAction).toHaveBeenCalledOnce())
66+
} finally {
67+
await server.close()
68+
}
69+
})
70+
})

packages/vite/src/node/preview.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
3+
import type readline from 'node:readline'
34
import sirv from 'sirv'
45
import compression from '@polka/compression'
56
import connect from 'connect'
@@ -107,6 +108,14 @@ export interface PreviewServer {
107108
* Bind CLI shortcuts
108109
*/
109110
bindCLIShortcuts(options?: BindCLIShortcutsOptions<PreviewServer>): void
111+
/**
112+
* @internal
113+
*/
114+
_shortcutsOptions?: BindCLIShortcutsOptions<PreviewServer>
115+
/**
116+
* @internal
117+
*/
118+
_rl?: readline.Interface | undefined
110119
}
111120

112121
export type PreviewServerHook = (

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { get as httpsGet } from 'node:https'
66
import type * as http from 'node:http'
77
import { performance } from 'node:perf_hooks'
88
import type { Http2SecureServer } from 'node:http2'
9+
import type readline from 'node:readline'
910
import connect from 'connect'
1011
import corsMiddleware from 'cors'
1112
import colors from 'picocolors'
@@ -408,6 +409,10 @@ export interface ViteDevServer {
408409
* @internal
409410
*/
410411
_shortcutsOptions?: BindCLIShortcutsOptions<ViteDevServer>
412+
/**
413+
* @internal
414+
*/
415+
_rl?: readline.Interface | undefined
411416
/**
412417
* @internal
413418
*/

packages/vite/src/node/shortcuts.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,28 @@ export type CLIShortcut<Server = ViteDevServer | PreviewServer> = {
2828
export function bindCLIShortcuts<Server extends ViteDevServer | PreviewServer>(
2929
server: Server,
3030
opts?: BindCLIShortcutsOptions<Server>,
31+
enabled: boolean = process.stdin.isTTY && !process.env.CI,
3132
): void {
32-
if (!server.httpServer || !process.stdin.isTTY || process.env.CI) {
33+
if (!server.httpServer || !enabled) {
3334
return
3435
}
3536

3637
const isDev = isDevServer(server)
3738

38-
if (isDev) {
39-
server._shortcutsOptions = opts as BindCLIShortcutsOptions<ViteDevServer>
39+
const customShortcuts: CLIShortcut<ViteDevServer | PreviewServer>[] =
40+
opts?.customShortcuts ?? []
41+
42+
// Merge custom shortcuts from existing options
43+
// with new shortcuts taking priority
44+
for (const shortcut of server._shortcutsOptions?.customShortcuts ?? []) {
45+
if (!customShortcuts.some((s) => s.key === shortcut.key)) {
46+
customShortcuts.push(shortcut)
47+
}
48+
}
49+
50+
server._shortcutsOptions = {
51+
...opts,
52+
customShortcuts,
4053
}
4154

4255
if (opts?.print) {
@@ -48,7 +61,7 @@ export function bindCLIShortcuts<Server extends ViteDevServer | PreviewServer>(
4861
)
4962
}
5063

51-
const shortcuts = (opts?.customShortcuts ?? []).concat(
64+
const shortcuts = customShortcuts.concat(
5265
(isDev
5366
? BASE_DEV_SHORTCUTS
5467
: BASE_PREVIEW_SHORTCUTS) as CLIShortcut<Server>[],
@@ -87,9 +100,15 @@ export function bindCLIShortcuts<Server extends ViteDevServer | PreviewServer>(
87100
actionRunning = false
88101
}
89102

90-
const rl = readline.createInterface({ input: process.stdin })
91-
rl.on('line', onInput)
92-
server.httpServer.on('close', () => rl.close())
103+
if (!server._rl) {
104+
const rl = readline.createInterface({ input: process.stdin })
105+
server._rl = rl
106+
server.httpServer.on('close', () => rl.close())
107+
} else {
108+
server._rl.removeAllListeners('line')
109+
}
110+
111+
server._rl.on('line', onInput)
93112
}
94113

95114
const BASE_DEV_SHORTCUTS: CLIShortcut<ViteDevServer>[] = [

0 commit comments

Comments
 (0)