Skip to content

Commit fcef8f4

Browse files
authored
feat: add dev server shortcuts (#83)
1 parent 4a7558e commit fcef8f4

File tree

4 files changed

+186
-3
lines changed

4 files changed

+186
-3
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"fast-glob": "^3.3.3",
6969
"get-port": "^7.1.0",
7070
"junk": "^4.0.1",
71+
"open": "^10.1.2",
7172
"picomatch": "^4.0.2",
7273
"pretty-hrtime": "^1.0.3",
7374
"tmp-cache": "^1.1.0",

src/dev_server.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { type UnWrapLazyImport } from '@poppinss/utils/types'
2020

2121
import debug from './debug.ts'
2222
import { FileSystem } from './file_system.ts'
23+
import { ShortcutsManager } from './shortcuts_manager.ts'
2324
import type { DevServerOptions } from './types/common.ts'
2425
import { type DevServerHooks, type WatcherHooks } from './types/hooks.ts'
2526
import { getPort, loadHooks, parseConfig, runNode, throttle, watch } from './utils.ts'
@@ -68,6 +69,11 @@ export class DevServer {
6869
*/
6970
#httpServer?: ResultPromise
7071

72+
/**
73+
* Keyboard shortcuts manager
74+
*/
75+
#shortcutsManager?: ShortcutsManager
76+
7177
/**
7278
* Filesystem is used to decide which files to watch or entertain when
7379
* using hot-hook
@@ -103,6 +109,29 @@ export class DevServer {
103109
await this.#startHTTPServer(this.#stickyPort)
104110
}, 'restartHTTPServer')
105111

112+
/**
113+
* Sets up keyboard shortcuts
114+
*/
115+
#setupKeyboardShortcuts() {
116+
this.#shortcutsManager = new ShortcutsManager({
117+
logger: this.ui.logger,
118+
callbacks: {
119+
onRestart: () => this.#restartHTTPServer(),
120+
onClear: () => this.#clearScreen(),
121+
onQuit: () => this.close(),
122+
},
123+
})
124+
125+
this.#shortcutsManager.setup()
126+
}
127+
128+
/**
129+
* Cleanup keyboard shortcuts
130+
*/
131+
#cleanupKeyboardShortcuts() {
132+
this.#shortcutsManager?.cleanup()
133+
}
134+
106135
/**
107136
* CLI UI to log colorful messages
108137
*/
@@ -152,15 +181,20 @@ export class DevServer {
152181
*/
153182
async #postServerReady(message: { port: number; host: string; duration?: [number, number] }) {
154183
const host = message.host === '0.0.0.0' ? '127.0.0.1' : message.host
184+
const serverUrl = `http://${host}:${message.port}`
185+
this.#shortcutsManager?.setServerUrl(serverUrl)
186+
155187
const displayMessage = this.ui
156188
.sticker()
157-
.add(`Server address: ${this.ui.colors.cyan(`http://${host}:${message.port}`)}`)
189+
.add(`Server address: ${this.ui.colors.cyan(serverUrl)}`)
158190
.add(`Mode: ${this.ui.colors.cyan(this.mode)}`)
159191

160192
if (message.duration) {
161193
displayMessage.add(`Ready in: ${this.ui.colors.cyan(prettyHrtime(message.duration))}`)
162194
}
163195

196+
displayMessage.add(`Press ${this.ui.colors.dim('h')} to show help`)
197+
164198
/**
165199
* Run hooks before displaying the "displayMessage". It will allow hooks to add
166200
* custom lines to the display message.
@@ -392,6 +426,7 @@ export class DevServer {
392426
* Close watchers and the running child process
393427
*/
394428
async close() {
429+
this.#cleanupKeyboardShortcuts()
395430
await this.#watcher?.close()
396431
if (this.#httpServer) {
397432
this.#httpServer.removeAllListeners()
@@ -434,6 +469,7 @@ export class DevServer {
434469
}
435470

436471
this.#clearScreen()
472+
this.#setupKeyboardShortcuts()
437473
this.ui.logger.info('starting HTTP server...')
438474
await this.#startHTTPServer(this.#stickyPort)
439475
}
@@ -460,6 +496,7 @@ export class DevServer {
460496
this.#registerServerRestartHooks()
461497

462498
this.#clearScreen()
499+
this.#setupKeyboardShortcuts()
463500
this.ui.logger.info('starting HTTP server...')
464501
await this.#startHTTPServer(this.#stickyPort)
465502

src/shortcuts_manager.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* @adonisjs/assembler
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import type { Logger } from '@poppinss/cliui'
11+
12+
/**
13+
* Keyboard shortcut definition
14+
*/
15+
export interface KeyboardShortcut {
16+
key: string
17+
description: string
18+
handler: () => void
19+
}
20+
21+
/**
22+
* Callbacks for keyboard shortcuts actions
23+
*/
24+
export interface KeyboardShortcutsCallbacks {
25+
onRestart: () => void
26+
onClear: () => void
27+
onQuit: () => void
28+
}
29+
30+
/**
31+
* Shortcuts manager options
32+
*/
33+
export interface ShortcutsManagerOptions {
34+
logger: Logger
35+
callbacks: KeyboardShortcutsCallbacks
36+
}
37+
38+
/**
39+
* Manages keyboard shortcuts for development server
40+
*/
41+
export class ShortcutsManager {
42+
#logger: Logger
43+
#callbacks: KeyboardShortcutsCallbacks
44+
#serverUrl?: string
45+
#keyPressHandler?: (data: Buffer) => void
46+
47+
#shortcuts: KeyboardShortcut[] = [
48+
{
49+
key: 'r',
50+
description: 'restart server',
51+
handler: () => {
52+
this.#logger.log('')
53+
this.#logger.info('Manual restart triggered...')
54+
this.#callbacks.onRestart()
55+
},
56+
},
57+
{
58+
key: 'c',
59+
description: 'clear console',
60+
handler: () => {
61+
this.#callbacks.onClear()
62+
this.#logger.info('Console cleared')
63+
},
64+
},
65+
{
66+
key: 'o',
67+
description: 'open in browser',
68+
handler: () => this.#handleOpenBrowser(),
69+
},
70+
{
71+
key: 'h',
72+
description: 'show this help',
73+
handler: () => this.showHelp(),
74+
},
75+
]
76+
77+
constructor(options: ShortcutsManagerOptions) {
78+
this.#logger = options.logger
79+
this.#callbacks = options.callbacks
80+
}
81+
82+
/**
83+
* Set server url for opening in browser
84+
*/
85+
setServerUrl(url: string) {
86+
this.#serverUrl = url
87+
}
88+
89+
/**
90+
* Initialize keyboard shortcuts
91+
*/
92+
setup() {
93+
if (!process.stdin.isTTY) return
94+
95+
process.stdin.setRawMode(true)
96+
97+
this.#keyPressHandler = (data: Buffer) => this.#handleKeyPress(data.toString())
98+
process.stdin.on('data', this.#keyPressHandler)
99+
}
100+
101+
/**
102+
* Handle key press events
103+
*/
104+
#handleKeyPress(key: string) {
105+
// Handle Ctrl+C (0x03) and Ctrl+D (0x04)
106+
if (key === '\u0003' || key === '\u0004') return this.#callbacks.onQuit()
107+
108+
const shortcut = this.#shortcuts.find((s) => s.key === key)
109+
if (shortcut) shortcut.handler()
110+
}
111+
112+
/**
113+
* Handle opening browser
114+
*/
115+
async #handleOpenBrowser() {
116+
this.#logger.log('')
117+
this.#logger.info(`Opening ${this.#serverUrl}...`)
118+
119+
const { default: open } = await import('open')
120+
open(this.#serverUrl!)
121+
}
122+
123+
/**
124+
* Show available keyboard shortcuts
125+
*/
126+
showHelp() {
127+
this.#logger.log('')
128+
this.#logger.log('Available shortcuts:')
129+
130+
this.#shortcuts.forEach(({ key, description }) => this.#logger.log(${key}: ${description}`))
131+
}
132+
133+
/**
134+
* Cleanup keyboard shortcuts
135+
*/
136+
cleanup() {
137+
if (!process.stdin.isTTY) return
138+
139+
process.stdin.setRawMode(false)
140+
process.stdin.removeListener('data', this.#keyPressHandler!)
141+
this.#keyPressHandler = undefined
142+
}
143+
}

tests/dev_server.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ test.group('DevServer', () => {
8181
stream: 'stdout',
8282
},
8383
{
84-
message: 'Server address: cyan(http://localhost:3334)\nMode: cyan(static)',
84+
message:
85+
'Server address: cyan(http://localhost:3334)\nMode: cyan(static)\nPress dim(h) to show help',
8586
stream: 'stdout',
8687
},
8788
])
@@ -139,7 +140,8 @@ test.group('DevServer', () => {
139140
stream: 'stdout',
140141
},
141142
{
142-
message: 'Server address: cyan(http://localhost:3335)\nMode: cyan(watch)',
143+
message:
144+
'Server address: cyan(http://localhost:3335)\nMode: cyan(watch)\nPress dim(h) to show help',
143145
stream: 'stdout',
144146
},
145147
])

0 commit comments

Comments
 (0)