Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/plugin-vite/demo/routes/tests/ws.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { FreshContext } from "fresh";

export const handler = {
GET(ctx: FreshContext) {
if (ctx.req.headers.get("upgrade") !== "websocket") {
return new Response("Not a WebSocket request", { status: 400 });
}

const { socket, response } = Deno.upgradeWebSocket(ctx.req);

socket.addEventListener("message", (event) => {
socket.send(`echo: ${event.data}`);
});

return response;
},
};
80 changes: 80 additions & 0 deletions packages/plugin-vite/src/plugins/dev_server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DevEnvironment, Plugin } from "vite";
import * as path from "@std/path";
import { connect } from "node:net";
import { ASSET_CACHE_BUST_KEY } from "fresh/internal";
import { createRequest, sendResponse } from "@remix-run/node-fetch-server";
import { hashCode } from "../shared.ts";
Expand All @@ -16,6 +17,85 @@ export function devServer(): Plugin[] {
configureServer(server) {
const IGNORE_URLS = /^\/(@(vite|fs|id)|\.vite)\//;

// Start a Deno HTTP server on a random port to handle WebSocket
// upgrades. Vite's Connect-based server fires 'upgrade' events
// on the underlying http.Server, but those never reach Connect
// middleware. We proxy upgrade requests to this Deno server
// where Deno.upgradeWebSocket() works natively.
let wsPort = 0;
const wsServer = Deno.serve(
{
port: 0,
onListen: ({ port }) => wsPort = port,
onError: () =>
new Response("Internal Server Error", { status: 500 }),
},
async (req) => {
try {
const mod = await server.ssrLoadModule("fresh:server_entry");
return (await mod.default.fetch(req)) as Response;
} catch {
return new Response("Internal Server Error", { status: 500 });
}
},
);

const originalClose = server.close;
server.close = async () => {
await wsServer.shutdown();
return originalClose.call(server);
};

server.httpServer?.on(
"upgrade",
(
req: {
url?: string;
method: string;
httpVersion: string;
rawHeaders: string[];
},
clientSocket: import("node:net").Socket,
head: Uint8Array,
) => {
// Let Vite handle its own HMR WebSocket upgrades
if (
req.url === "/__vite_hmr" ||
req.url === "/__vite_ping"
) {
return;
}

const proxySocket = connect(wsPort, "127.0.0.1", () => {
// Rebuild the HTTP upgrade request for the Deno server
let raw = `${req.method} ${req.url} HTTP/${req.httpVersion}\r\n`;
for (let i = 0; i < req.rawHeaders.length; i += 2) {
raw += `${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`;
}
raw += "\r\n";

proxySocket.write(raw);
if (head.length > 0) proxySocket.write(head);

clientSocket.pipe(proxySocket);
proxySocket.pipe(clientSocket);
});

proxySocket.on("error", () => {
try {
clientSocket.destroy();
} catch { /* ignore */ }
});
clientSocket.on("error", () => {
try {
proxySocket.destroy();
} catch { /* ignore */ }
});
clientSocket.on("close", () => proxySocket.destroy());
proxySocket.on("close", () => clientSocket.destroy());
},
);

server.middlewares.use(async (nodeReq, nodeRes, next) => {
const serverCfg = server.config.server;

Expand Down
33 changes: 33 additions & 0 deletions packages/plugin-vite/tests/dev_server_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,39 @@ Deno.test({
sanitizeResources: false,
});

// issue: https://github.com/denoland/fresh/issues/3350
Deno.test({
name: "vite dev - websocket upgrade",
fn: async () => {
const wsUrl = demoServer.address().replace("http", "ws");
const ws = new WebSocket(`${wsUrl}/tests/ws`);

const result = await new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => {
ws.close();
reject(new Error("WebSocket timed out"));
}, 5000);

ws.onopen = () => {
ws.send("hello");
};
ws.onmessage = (e) => {
clearTimeout(timeout);
resolve(e.data);
ws.close();
};
ws.onerror = (e) => {
clearTimeout(timeout);
reject(e);
};
});

expect(result).toEqual("echo: hello");
},
sanitizeResources: false,
sanitizeOps: false,
});

Deno.test({
name: "vite dev - source mapped stack traces",
fn: async () => {
Expand Down
Loading