Skip to content

Commit e3464e7

Browse files
authored
feat(request): add watch option to request command (#50)
* feat(request): add watch option to request command * docs(request): tweak description of `watch` option
1 parent e2b816d commit e3464e7

File tree

8 files changed

+265
-169
lines changed

8 files changed

+265
-169
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ hono request [file] [options]
156156
- `-X, --method <method>` - HTTP method (default: GET)
157157
- `-d, --data <data>` - Request body data
158158
- `-H, --header <header>` - Custom headers (can be used multiple times)
159+
- `-w, --watch` - Watch for changes and resend request
159160

160161
**Examples:**
161162

src/commands/optimize/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ export function optimizeCommand(program: Command) {
2929
}
3030

3131
const appFilePath = realpathSync(appPath)
32-
const app: Hono = await buildAndImportApp(appFilePath, {
32+
const buildIterator = buildAndImportApp(appFilePath, {
3333
external: ['@hono/node-server'],
3434
})
35+
const app: Hono = (await buildIterator.next()).value
3536

3637
let routerName
3738
let importStatement

src/commands/request/index.test.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,27 @@ describe('requestCommand', () => {
2424
let mockModules: any
2525
let mockBuildAndImportApp: any
2626

27+
const createBuildIterator = (app: Hono) => {
28+
const iterator = {
29+
next: vi
30+
.fn()
31+
.mockResolvedValueOnce({ value: app, done: false })
32+
.mockResolvedValueOnce({ value: undefined, done: true }),
33+
return: vi.fn().mockResolvedValue({ value: undefined, done: true }),
34+
[Symbol.asyncIterator]() {
35+
return this
36+
},
37+
}
38+
return iterator
39+
}
40+
2741
const setupBasicMocks = (appPath: string, mockApp: Hono) => {
2842
mockModules.existsSync.mockReturnValue(true)
2943
mockModules.realpathSync.mockReturnValue(appPath)
3044
mockModules.resolve.mockImplementation((cwd: string, path: string) => {
3145
return `${cwd}/${path}`
3246
})
33-
mockBuildAndImportApp.mockResolvedValue(mockApp)
47+
mockBuildAndImportApp.mockReturnValue(createBuildIterator(mockApp))
3448
}
3549

3650
beforeEach(async () => {
@@ -67,6 +81,41 @@ describe('requestCommand', () => {
6781
// Verify resolve was called with correct arguments
6882
expect(mockModules.resolve).toHaveBeenCalledWith(process.cwd(), 'test-app.js')
6983

84+
expect(mockBuildAndImportApp).toHaveBeenCalledWith(expectedPath, {
85+
external: ['@hono/node-server'],
86+
watch: false,
87+
})
88+
89+
expect(consoleLogSpy).toHaveBeenCalledWith(
90+
JSON.stringify(
91+
{
92+
status: 200,
93+
body: '{"message":"Hello"}',
94+
headers: { 'content-type': 'application/json' },
95+
},
96+
null,
97+
2
98+
)
99+
)
100+
})
101+
102+
it('should handle GET request to specific file', async () => {
103+
const mockApp = new Hono()
104+
mockApp.get('/', (c) => c.json({ message: 'Hello' }))
105+
106+
const expectedPath = 'test-app.js'
107+
setupBasicMocks(expectedPath, mockApp)
108+
109+
await program.parseAsync(['node', 'test', 'request', '-w', '-P', '/', 'test-app.js'])
110+
111+
// Verify resolve was called with correct arguments
112+
expect(mockModules.resolve).toHaveBeenCalledWith(process.cwd(), 'test-app.js')
113+
114+
expect(mockBuildAndImportApp).toHaveBeenCalledWith(expectedPath, {
115+
external: ['@hono/node-server'],
116+
watch: true,
117+
})
118+
70119
expect(consoleLogSpy).toHaveBeenCalledWith(
71120
JSON.stringify(
72121
{
@@ -134,7 +183,7 @@ describe('requestCommand', () => {
134183
mockModules.resolve.mockImplementation((cwd: string, path: string) => {
135184
return `${cwd}/${path}`
136185
})
137-
mockBuildAndImportApp.mockResolvedValue(mockApp)
186+
mockBuildAndImportApp.mockReturnValue(createBuildIterator(mockApp))
138187

139188
await program.parseAsync(['node', 'test', 'request'])
140189

src/commands/request/index.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface RequestOptions {
1111
data?: string
1212
header?: string[]
1313
path?: string
14+
watch: boolean
1415
}
1516

1617
export function requestCommand(program: Command) {
@@ -21,6 +22,7 @@ export function requestCommand(program: Command) {
2122
.option('-P, --path <path>', 'Request path', '/')
2223
.option('-X, --method <method>', 'HTTP method', 'GET')
2324
.option('-d, --data <data>', 'Request body data')
25+
.option('-w, --watch', 'Watch for changes and resend request', false)
2426
.option(
2527
'-H, --header <header>',
2628
'Custom headers',
@@ -31,16 +33,19 @@ export function requestCommand(program: Command) {
3133
)
3234
.action(async (file: string | undefined, options: RequestOptions) => {
3335
const path = options.path || '/'
34-
const result = await executeRequest(file, path, options)
35-
console.log(JSON.stringify(result, null, 2))
36+
const watch = options.watch
37+
const buildIterator = getBuildIterator(file, watch)
38+
for await (const app of buildIterator) {
39+
const result = await executeRequest(app, path, options)
40+
console.log(JSON.stringify(result, null, 2))
41+
}
3642
})
3743
}
3844

39-
export async function executeRequest(
45+
export function getBuildIterator(
4046
appPath: string | undefined,
41-
requestPath: string,
42-
options: RequestOptions
43-
): Promise<{ status: number; body: string; headers: Record<string, string> }> {
47+
watch: boolean
48+
): AsyncGenerator<Hono> {
4449
// Determine entry file path
4550
let entry: string
4651
let resolvedAppPath: string
@@ -62,14 +67,17 @@ export async function executeRequest(
6267
}
6368

6469
const appFilePath = realpathSync(resolvedAppPath)
65-
const app: Hono = await buildAndImportApp(appFilePath, {
70+
return buildAndImportApp(appFilePath, {
6671
external: ['@hono/node-server'],
72+
watch,
6773
})
74+
}
6875

69-
if (!app || typeof app.request !== 'function') {
70-
throw new Error('No valid Hono app exported from the file')
71-
}
72-
76+
export async function executeRequest(
77+
app: Hono,
78+
requestPath: string,
79+
options: RequestOptions
80+
): Promise<{ status: number; body: string; headers: Record<string, string> }> {
7381
// Build request
7482
const url = new URL(requestPath, 'http://localhost')
7583
const requestInit: RequestInit = {

src/commands/serve/index.test.ts

Lines changed: 36 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
11
import { Command } from 'commander'
2-
import { Hono } from 'hono'
32
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
3+
import { execFile } from 'node:child_process'
4+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
5+
import { tmpdir } from 'node:os'
6+
import { join } from 'node:path'
47

58
// Mock dependencies
6-
vi.mock('node:fs', () => ({
7-
existsSync: vi.fn(),
8-
realpathSync: vi.fn(),
9-
}))
10-
11-
vi.mock('node:path', () => ({
12-
extname: vi.fn(),
13-
resolve: vi.fn(),
14-
}))
15-
16-
vi.mock('node:url', () => ({
17-
pathToFileURL: vi.fn(),
18-
}))
19-
20-
vi.mock('esbuild', () => ({
21-
build: vi.fn(),
22-
}))
23-
249
vi.mock('@hono/node-server', () => ({
2510
serve: vi.fn(),
2611
}))
@@ -44,6 +29,7 @@ import { serveCommand } from './index.js'
4429

4530
describe('serveCommand', () => {
4631
let program: Command
32+
let mockEsbuild: any
4733
let mockModules: any
4834
let mockServe: any
4935
let mockShowRoutes: any
@@ -53,15 +39,6 @@ describe('serveCommand', () => {
5339
program = new Command()
5440
serveCommand(program)
5541

56-
// Get mocked modules
57-
mockModules = {
58-
existsSync: vi.mocked((await import('node:fs')).existsSync),
59-
realpathSync: vi.mocked((await import('node:fs')).realpathSync),
60-
extname: vi.mocked((await import('node:path')).extname),
61-
resolve: vi.mocked((await import('node:path')).resolve),
62-
pathToFileURL: vi.mocked((await import('node:url')).pathToFileURL),
63-
}
64-
6542
mockServe = vi.mocked((await import('@hono/node-server')).serve)
6643
mockShowRoutes = vi.mocked((await import('hono/dev')).showRoutes)
6744

@@ -81,11 +58,6 @@ describe('serveCommand', () => {
8158
})
8259

8360
it('should start server with default port', async () => {
84-
mockModules.existsSync.mockReturnValue(false)
85-
mockModules.resolve.mockImplementation((cwd: string, path: string) => {
86-
return `${cwd}/${path}`
87-
})
88-
8961
await program.parseAsync(['node', 'test', 'serve'])
9062

9163
// Verify serve was called with default port 7070
@@ -99,11 +71,6 @@ describe('serveCommand', () => {
9971
})
10072

10173
it('should start server with custom port', async () => {
102-
mockModules.existsSync.mockReturnValue(false)
103-
mockModules.resolve.mockImplementation((cwd: string, path: string) => {
104-
return `${cwd}/${path}`
105-
})
106-
10774
await program.parseAsync(['node', 'test', 'serve', '-p', '8080'])
10875

10976
// Verify serve was called with custom port
@@ -117,25 +84,39 @@ describe('serveCommand', () => {
11784
})
11885

11986
it('should serve app that responds correctly to requests', async () => {
120-
const mockApp = new Hono()
121-
mockApp.get('/', (c) => c.text('Hello World'))
122-
mockApp.get('/api', (c) => c.json({ message: 'API response' }))
123-
124-
const expectedPath = 'app.js'
125-
const absolutePath = `${process.cwd()}/${expectedPath}`
126-
127-
mockModules.existsSync.mockReturnValue(true)
128-
mockModules.realpathSync.mockReturnValue(absolutePath)
129-
mockModules.extname.mockReturnValue('.js')
130-
mockModules.resolve.mockImplementation((cwd: string, path: string) => {
131-
return `${cwd}/${path}`
132-
})
133-
mockModules.pathToFileURL.mockReturnValue(new URL(`file://${absolutePath}`))
87+
const appDir = mkdtempSync(join(tmpdir(), 'hono-cli-serve-test'))
88+
mkdirSync(appDir, { recursive: true })
89+
const appFile = join(appDir, 'app.ts')
90+
writeFileSync(
91+
appFile,
92+
`
93+
import { Hono } from 'hono'
94+
95+
const app = new Hono()
96+
app.get('/', (c) => c.text('Hello World'))
97+
app.get('/api', (c) => c.json({ message: 'API response' }))
13498
135-
// Mock the import of JS file
136-
vi.doMock(absolutePath, () => ({ default: mockApp }))
99+
export default app
100+
`
101+
)
102+
writeFileSync(
103+
join(appDir, 'package.json'),
104+
JSON.stringify({
105+
type: 'module',
106+
dependencies: {
107+
hono: 'latest',
108+
},
109+
})
110+
)
111+
process.chdir(appDir)
112+
await new Promise<void>((resolve) => {
113+
const child = execFile('npm', ['install'])
114+
child.on('exit', () => {
115+
resolve()
116+
})
117+
})
137118

138-
await program.parseAsync(['node', 'test', 'serve', 'app.js'])
119+
await program.parseAsync(['node', 'test', 'serve', appFile])
139120

140121
// Test the captured fetch function
141122
const rootRequest = new Request('http://localhost:7070/')
@@ -149,11 +130,6 @@ describe('serveCommand', () => {
149130
})
150131

151132
it('should return 404 for non-existent routes when no app file exists', async () => {
152-
mockModules.existsSync.mockReturnValue(false)
153-
mockModules.resolve.mockImplementation((cwd: string, path: string) => {
154-
return `${cwd}/${path}`
155-
})
156-
157133
await program.parseAsync(['node', 'test', 'serve'])
158134

159135
// Test 404 behavior with default empty app
@@ -181,11 +157,6 @@ describe('serveCommand', () => {
181157
})
182158

183159
it('should handle typical use case: basicAuth + proxy to ramen-api.dev', async () => {
184-
mockModules.existsSync.mockReturnValue(false)
185-
mockModules.resolve.mockImplementation((cwd: string, path: string) => {
186-
return `${cwd}/${path}`
187-
})
188-
189160
// Mock basicAuth middleware
190161
const mockBasicAuth = vi.fn().mockImplementation(() => {
191162
return async (c: any, next: any) => {

src/commands/serve/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ export function serveCommand(program: Command) {
4848
app = new Hono()
4949
} else {
5050
const appFilePath = realpathSync(appPath)
51-
app = await buildAndImportApp(appFilePath, {
51+
const buildIterator = buildAndImportApp(appFilePath, {
5252
external: ['@hono/node-server'],
5353
})
54+
app = (await buildIterator.next()).value
5455
}
5556
}
5657

0 commit comments

Comments
 (0)