This repository has been archived by the owner on Apr 6, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 1k
/
dev.ts
179 lines (158 loc) · 5 KB
/
dev.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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import { Worker } from 'worker_threads'
import { IncomingMessage, ServerResponse } from 'http'
import { existsSync, promises as fsp } from 'fs'
import { loading as loadingTemplate } from '@nuxt/design'
import chokidar, { FSWatcher } from 'chokidar'
import debounce from 'p-debounce'
import { promisifyHandle, createApp, Middleware, useBase } from 'h3'
import httpProxy from 'http-proxy'
import { listen, Listener, ListenOptions } from 'listhen'
import servePlaceholder from 'serve-placeholder'
import serveStatic from 'serve-static'
import { resolve } from 'pathe'
import connect from 'connect'
import type { NitroContext } from '../context'
import { handleVfs } from './vfs'
export interface NitroWorker {
worker: Worker,
address: string
}
function initWorker (filename): Promise<NitroWorker> {
return new Promise((resolve, reject) => {
const worker = new Worker(filename)
worker.once('exit', (code) => {
if (code) {
reject(new Error('[worker] exited with code: ' + code))
}
})
worker.on('error', (err) => {
err.message = '[worker] ' + err.message
reject(err)
})
worker.on('message', (event) => {
if (event && event.address) {
resolve({
worker,
address: event.address
} as NitroWorker)
}
})
})
}
async function killWorker (worker?: NitroWorker) {
if (!worker) {
return
}
await worker.worker?.terminate()
worker.worker = null
if (worker.address && existsSync(worker.address)) {
await fsp.rm(worker.address).catch(() => {})
}
}
export function createDevServer (nitroContext: NitroContext) {
// Worker
const workerEntry = resolve(nitroContext.output.dir, nitroContext.output.serverDir, 'index.mjs')
let currentWorker: NitroWorker
async function reload () {
// Create a new worker
const newWorker = await initWorker(workerEntry)
// Kill old worker in background
killWorker(currentWorker).catch(err => console.error(err))
// Replace new worker as current
currentWorker = newWorker
}
// App
const app = createApp()
// _nuxt and static
app.use(nitroContext._nuxt.publicPath, serveStatic(resolve(nitroContext._nuxt.buildDir, 'dist/client')))
app.use(nitroContext._nuxt.routerBase, serveStatic(resolve(nitroContext._nuxt.publicDir)))
// debugging endpoint to view vfs
app.use('/_vfs', useBase('/_vfs', handleVfs(nitroContext)))
// Dynamic Middlwware
const legacyMiddleware = createDynamicMiddleware()
const devMiddleware = createDynamicMiddleware()
app.use(legacyMiddleware.middleware)
app.use(devMiddleware.middleware)
// serve placeholder 404 assets instead of hitting SSR
app.use(nitroContext._nuxt.publicPath, servePlaceholder())
// SSR Proxy
const proxy = httpProxy.createProxy()
const proxyHandle = promisifyHandle((req: IncomingMessage, res: ServerResponse) => {
proxy.web(req, res, { target: currentWorker.address }, (error: unknown) => {
console.error('[proxy]', error)
})
})
app.use((req, res) => {
if (currentWorker?.address) {
// Workaround to pass legacy req.spa to proxy
// @ts-ignore
if (req.spa) {
req.headers['x-nuxt-no-ssr'] = 'true'
}
return proxyHandle(req, res)
} else {
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
res.end(loadingTemplate({}))
}
})
// Listen
let listeners: Listener[] = []
const _listen = async (port: ListenOptions['port'], opts?: Partial<ListenOptions>) => {
const listener = await listen(app, { port, ...opts })
listeners.push(listener)
return listener
}
// Watch for dist and reload worker
const pattern = '**/*.{js,json,cjs,mjs}'
const events = ['add', 'change']
let watcher: FSWatcher
function watch () {
if (watcher) { return }
const dReload = debounce(() => reload().catch(console.warn), 200, { before: true })
watcher = chokidar.watch([
resolve(nitroContext.output.serverDir, pattern),
resolve(nitroContext._nuxt.buildDir, 'dist/server', pattern)
]).on('all', event => events.includes(event) && dReload())
}
// Close handler
async function close () {
if (watcher) {
await watcher.close()
}
await killWorker(currentWorker)
await Promise.all(listeners.map(l => l.close()))
listeners = []
}
nitroContext._internal.hooks.hook('close', close)
return {
reload,
listen: _listen,
app,
close,
watch,
setLegacyMiddleware: legacyMiddleware.set,
setDevMiddleware: devMiddleware.set
}
}
interface DynamicMiddleware {
set: (input: Middleware) => void
middleware: Middleware
}
function createDynamicMiddleware (): DynamicMiddleware {
let middleware: Middleware
return {
set: (input) => {
if (!Array.isArray(input)) {
middleware = input
return
}
const app = connect()
for (const m of input) {
app.use(m.path || m.route || '/', m.handler || m.handle!)
}
middleware = app
},
middleware: (req, res, next) =>
middleware ? middleware(req, res, next) : next()
}
}