11import react from '@vitejs/plugin-react' ;
2- import { defineConfig , loadEnv } from 'vite' ;
2+ import type { IncomingMessage } from 'node:http' ;
3+ import net from 'node:net' ;
4+ import tls from 'node:tls' ;
5+ import { defineConfig , loadEnv , Plugin } from 'vite' ;
36import svgr from 'vite-plugin-svgr' ;
47
8+ /**
9+ * Vite-only plugin that creates a dynamic WebSocket reverse-proxy for the
10+ * debug session endpoint. Because the backend picks a random port in the
11+ * range 65435-65535 for each session, a static proxy target is not possible.
12+ *
13+ * Flow (dev only):
14+ * 1. Browser calls POST /debug/ws-register with the wss:// URL returned by
15+ * the debugSession API, telling the plugin which host:port to tunnel to.
16+ * 2. Browser dials ws://localhost:<vite-port>/debug/join (same origin, no
17+ * cert issues).
18+ * 3. This plugin intercepts the WebSocket upgrade, opens a TLS tunnel to
19+ * the registered target, and splices the two sockets together.
20+ */
21+ function dynamicWsProxyPlugin ( ) : Plugin {
22+ let registeredTarget : URL | null = null ;
23+
24+ return {
25+ name : 'dynamic-ws-proxy' ,
26+ configureServer ( server ) {
27+ // ── Registration endpoint ─────────────────────────────────────────────
28+ server . middlewares . use ( '/debug/ws-register' , ( req , res ) => {
29+ if ( req . method !== 'POST' ) {
30+ res . statusCode = 405 ;
31+ res . end ( ) ;
32+ return ;
33+ }
34+ let body = '' ;
35+ req . on ( 'data' , ( chunk : Buffer ) => ( body += chunk . toString ( ) ) ) ;
36+ req . on ( 'end' , ( ) => {
37+ try {
38+ const { url } = JSON . parse ( body ) as { url : string } ;
39+ registeredTarget = new URL ( url ) ;
40+ res . statusCode = 200 ;
41+ res . setHeader ( 'Content-Type' , 'application/json' ) ;
42+ res . end ( JSON . stringify ( { ok : true } ) ) ;
43+ } catch {
44+ res . statusCode = 400 ;
45+ res . end ( JSON . stringify ( { error : 'Invalid request body' } ) ) ;
46+ }
47+ } ) ;
48+ } ) ;
49+
50+ // ── Dynamic WebSocket tunnel ──────────────────────────────────────────
51+ server . httpServer ?. prependListener (
52+ 'upgrade' ,
53+ ( req : IncomingMessage , socket : any , head : Buffer ) => {
54+ if ( ! req . url ?. startsWith ( '/debug/join' ) || ! registeredTarget ) return ;
55+
56+ const target = registeredTarget ;
57+ const useSecure =
58+ target . protocol === 'wss:' || target . protocol === 'https:' ;
59+ const port = target . port
60+ ? parseInt ( target . port , 10 )
61+ : useSecure ? 443 : 80 ;
62+
63+ const upstream : net . Socket = useSecure
64+ ? tls . connect ( { host : target . hostname , port, rejectUnauthorized : false } )
65+ : net . createConnection ( { host : target . hostname , port } ) ;
66+
67+ upstream . once ( useSecure ? 'secureConnect' : 'connect' , ( ) => {
68+ // Re-emit the full HTTP Upgrade request to the upstream server
69+ const headerLines = [
70+ `GET ${ req . url } HTTP/1.1` ,
71+ `Host: ${ target . host } ` ,
72+ ...Object . entries ( req . headers )
73+ . filter ( ( [ k ] ) => k !== 'host' )
74+ . map ( ( [ k , v ] ) => `${ k } : ${ Array . isArray ( v ) ? v . join ( ', ' ) : ( v ?? '' ) } ` ) ,
75+ '' ,
76+ '' ,
77+ ] ;
78+ upstream . write ( headerLines . join ( '\r\n' ) ) ;
79+ if ( head ?. length ) upstream . write ( head ) ;
80+
81+ socket . pipe ( upstream ) ;
82+ upstream . pipe ( socket ) ;
83+ } ) ;
84+
85+ upstream . on ( 'error' , ( err : Error ) => {
86+ console . error ( '[dynamic-ws-proxy] upstream error:' , err . message ) ;
87+ socket . destroy ( ) ;
88+ } ) ;
89+ socket . on ( 'error' , ( ) => upstream . destroy ( ) ) ;
90+ socket . on ( 'close' , ( ) => upstream . destroy ( ) ) ;
91+ upstream . on ( 'close' , ( ) => socket . destroy ( ) ) ;
92+ } ,
93+ ) ;
94+ } ,
95+ } ;
96+ }
97+
598export default defineConfig ( ( { mode } ) => {
699 const env = loadEnv ( mode , process . cwd ( ) , '' ) ;
7100
@@ -12,7 +105,7 @@ export default defineConfig(({ mode }) => {
12105
13106 return {
14107 base : '/debug/' ,
15- plugins : [ react ( ) , svgr ( { include : '**/*.svg' , svgrOptions : { exportType : 'named' } } ) ] ,
108+ plugins : [ react ( ) , svgr ( { include : '**/*.svg' , svgrOptions : { exportType : 'named' } } ) , dynamicWsProxyPlugin ( ) ] ,
16109 server : {
17110 proxy : programHost
18111 ? {
0 commit comments