-
Notifications
You must be signed in to change notification settings - Fork 35
/
dev-server.ts
113 lines (100 loc) · 3.69 KB
/
dev-server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import type http from 'http'
import { getRequestListener } from '@hono/node-server'
import type { Miniflare, MiniflareOptions, WorkerOptions } from 'miniflare'
import type { Plugin, ViteDevServer, Connect } from 'vite'
export type DevServerOptions = {
entry?: string
injectClientScript?: boolean
exclude?: (string | RegExp)[]
cf?: Partial<
Omit<
WorkerOptions,
// We can ignore these properties:
'name' | 'script' | 'scriptPath' | 'modules' | 'modulesRoot' | 'modulesRules'
> &
Pick<
MiniflareOptions,
'cachePersist' | 'd1Persist' | 'durableObjectsPersist' | 'kvPersist' | 'r2Persist'
>
>
}
export const defaultOptions: Required<Omit<DevServerOptions, 'cf'>> = {
entry: './src/index.ts',
injectClientScript: true,
exclude: ['.*\\.ts', '.*\\.tsx', '/@.+', '\\/node_modules\\/.*'],
}
interface ExecutionContext {
waitUntil(promise: Promise<unknown>): void
passThroughOnException(): void
}
type Fetch = (request: Request, env: {}, executionContext: ExecutionContext) => Promise<Response>
const nullScript = 'export default { fetch: () => new Response(null, { status: 404 }) };'
export function devServer(options?: DevServerOptions): Plugin {
const entry = options?.entry ?? defaultOptions.entry
const plugin: Plugin = {
name: '@hono/vite-dev-server',
configureServer: async (server) => {
let mf: undefined | Miniflare = undefined
// Dynamic import Miniflare for environments like Bun.
if (options?.cf) {
const { Miniflare } = await import('miniflare')
mf = new Miniflare({
modules: true,
script: nullScript,
...options.cf,
})
}
async function createMiddleware(server: ViteDevServer): Promise<Connect.HandleFunction> {
return async function (
req: http.IncomingMessage,
res: http.ServerResponse,
next: Connect.NextFunction
): Promise<void> {
const exclude = options?.exclude ?? defaultOptions.exclude
for (const pattern of exclude) {
const regExp = new RegExp(`^${pattern}$`)
if (req.url && regExp.test(req.url)) {
return next()
}
}
const appModule = await server.ssrLoadModule(entry)
const app = appModule['default'] as { fetch: Fetch }
if (!app) {
console.error(`Failed to find a named export "default" from ${entry}`)
return next()
}
getRequestListener(async (request) => {
let bindings = {}
if (mf) {
bindings = await mf.getBindings()
}
const response = await app.fetch(request, bindings, {
waitUntil: async (fn) => fn,
passThroughOnException: () => {
throw new Error('`passThroughOnException` is not supported')
},
})
if (
options?.injectClientScript !== false &&
// If the response is a streaming, it does not inject the script:
!response.headers.get('transfer-encoding')?.match('chunked') &&
response.headers.get('content-type')?.match(/^text\/html/)
) {
const body =
(await response.text()) + '<script type="module" src="/@vite/client"></script>'
const headers = new Headers(response.headers)
headers.delete('content-length')
return new Response(body, {
status: response.status,
headers,
})
}
return response
})(req, res)
}
}
server.middlewares.use(await createMiddleware(server))
},
}
return plugin
}