Skip to content

Commit e54fde6

Browse files
committed
fix(vite): share an https dev server for the hub WebSocket
1 parent cac6900 commit e54fde6

2 files changed

Lines changed: 87 additions & 6 deletions

File tree

‎packages/vite/src/hub.ts‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { DevframeHubUi, DockRendererRegistration, HubDevframeEntry, HubInst
22
import type { DevframeHubContext } from '@devframes/hub/node'
33
import type { ClientScriptEntry } from '@devframes/hub/types'
44
import type { DevframeDefinition } from 'devframe'
5+
import type { Server as NodeHttpServer } from 'node:http'
56
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
6-
import { Server as NodeHttpServer } from 'node:http'
77
import process from 'node:process'
88
import { DEVFRAMES_HUB_BASE, normalizeHubBase } from '@devframes/hub/constants'
99
import { initHub } from '@devframes/hub/initiate'
@@ -180,7 +180,10 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin {
180180
// the hub client runtime imports it into the host page.
181181
const devframes = attachClientScripts(options.devframes, options.clientScripts)
182182

183-
const httpServer = server.httpServer instanceof NodeHttpServer ? server.httpServer : undefined
183+
// Vite's https dev server is an `Http2SecureServer` (or an
184+
// `https.Server` with `server.proxy`). Both emit `upgrade` for HTTP/1.1
185+
// requests, as Vite's own HMR socket relies on, so share it too.
186+
const httpServer = (server.httpServer ?? undefined) as NodeHttpServer | undefined
184187

185188
const hub = initHub({
186189
base,
@@ -198,8 +201,8 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin {
198201
auth: options.auth,
199202
/**
200203
* Share Vite's own HTTP server for the WS upgrade at `<base>__ws`, with no
201-
* side-car port to discover. A pinned `port` uses a side-car instead;
202-
* an https/http2 dev server (non-`node:http`) asks for an auto-port
204+
* side-car port to discover. A pinned `port` uses a side-car instead,
205+
* and a middleware-mode Vite (no `httpServer`) asks for an auto-port
203206
* side-car. Clients discover either via `__connection.json`.
204207
*/
205208
server: httpServer,
@@ -267,8 +270,8 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin {
267270

268271
/**
269272
* Share Vite's own HTTP server for the WS upgrade unless a `port` pins a
270-
* side-car, or the dev server isn't a plain `node:http` server (https/http2),
271-
* which needs an auto-port side-car.
273+
* side-car, or there is no server to share (middleware mode), which needs an
274+
* auto-port side-car.
272275
*/
273276
function resolveWsBinding(port: number | undefined, httpServer: NodeHttpServer | undefined): { ws?: { port: number } | { sidecar: true } } {
274277
if (port != null)

‎packages/vite/test/hub.test.ts‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http'
2+
import type { Http2SecureServer } from 'node:http2'
3+
import type { ViteDevServer } from 'vite'
4+
import { mkdtempSync } from 'node:fs'
5+
import { createServer } from 'node:http'
6+
import { createSecureServer } from 'node:http2'
7+
import { tmpdir } from 'node:os'
8+
import { join } from 'node:path'
9+
import { getPort } from 'get-port-please'
10+
import { afterEach, describe, expect, it } from 'vitest'
11+
import { viteDevframeHub } from '../src/hub'
12+
13+
type ConnectMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void
14+
15+
/**
16+
* Vite on `server.https` hands plugins an `Http2SecureServer` (with
17+
* `allowHTTP1`), which is not a `node:http` `Server`. Requests are served
18+
* here over a plain HTTP server running the same middleware stack, so the
19+
* test needs no certificate.
20+
*/
21+
function fakeHttpsViteServer() {
22+
const stack: ConnectMiddleware[] = []
23+
const requestServer: NodeHttpServer = createServer((req, res) => {
24+
let i = 0
25+
const next = (): void => {
26+
const handler = stack[i++]
27+
if (!handler) {
28+
res.statusCode = 404
29+
res.end()
30+
return
31+
}
32+
handler(req, res, next)
33+
}
34+
next()
35+
})
36+
const httpServer: Http2SecureServer = createSecureServer({ allowHTTP1: true })
37+
const server = {
38+
httpServer,
39+
resolvedUrls: null,
40+
middlewares: { use: (handler: ConnectMiddleware) => stack.push(handler) },
41+
}
42+
return { server, httpServer, requestServer }
43+
}
44+
45+
describe('viteDevframeHub', () => {
46+
let cleanup: (() => Promise<void>) | undefined
47+
48+
afterEach(async () => {
49+
await cleanup?.()
50+
cleanup = undefined
51+
})
52+
53+
it('shares an https (http2) dev server for the WebSocket upgrade', async () => {
54+
const host = '127.0.0.1'
55+
const port = await getPort({ port: 19800, host })
56+
const { server, httpServer, requestServer } = fakeHttpsViteServer()
57+
await new Promise<void>(resolve => requestServer.listen(port, host, resolve))
58+
59+
const plugin = viteDevframeHub({
60+
ui: false,
61+
auth: false,
62+
quiet: true,
63+
cwd: mkdtempSync(join(tmpdir(), 'devframe-vite-hub-')),
64+
})
65+
cleanup = async () => {
66+
httpServer.emit('close')
67+
await (plugin.closeBundle as () => Promise<void>)()
68+
requestServer.close()
69+
requestServer.closeAllConnections()
70+
}
71+
await (plugin.configureServer as (s: ViteDevServer) => Promise<void>)(server as any)
72+
73+
const res = await fetch(`http://${host}:${port}/__devframes/__connection.json`)
74+
const meta = await res.json() as { websocket?: unknown }
75+
expect(meta.websocket).toEqual({ path: '/__devframes/__ws' })
76+
expect(httpServer.listenerCount('upgrade')).toBe(1)
77+
})
78+
})

0 commit comments

Comments
 (0)