import React from "react";
import { renderToString } from "react-dom/server";
import { Context, Next } from "../../core/types.ts";
const hmrScriptSource = `
(function() {
var __hmrConnected = false;
var reconnectAttempts = 0;
var reconnectDelay = 1000;
var ws = null;
function scheduleReconnect() {
reconnectAttempts++;
reconnectDelay = Math.min(30000, Math.round(reconnectDelay * 1.5));
setTimeout(connect, reconnectDelay);
}
function connect() {
console.log('[HMR] connecting to /hmr, attempt', reconnectAttempts + 1);
ws = new WebSocket((window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host + '/hmr');
ws.onopen = function() {
console.log('[HMR] connection open');
reconnectAttempts = 0;
reconnectDelay = 1000;
};
ws.onmessage = function(event) {
const data = event.data;
console.log('[HMR] message', data);
if (data === 'connected') {
if (__hmrConnected) {
console.log('[HMR] already connected, reloading');
window.location.reload();
return;
}
__hmrConnected = true;
console.log('[HMR] handshake complete');
}
if (data === 'reload') {
console.log('[HMR] reload message received, reloading now');
window.location.reload();
}
// heartbeat and other messages are ignored here
};
ws.onclose = function() {
console.log('[HMR] connection closed, scheduling reconnect');
scheduleReconnect();
};
ws.onerror = function(e) {
console.warn('[HMR] connection error', e);
try { ws.close(); } catch (_) {}
};
}
connect();
})();
`;
const rawHMRscript = `<script>${hmrScriptSource}</script>`;
const hmrClients = new Set<WebSocket>();
let __nextHmrClientId = 1;
const __hmrClientIds = new WeakMap<WebSocket, number>();
const heartbeatIntervals = new Set<number>();
let watcherStarted = false;
let watcherInterval: number | undefined;
let lastReloadAt = 0;
let pendingReload = false;
let lastMtime = 0;
export const _resetWatcherForTests = () => {
watcherStarted = false;
pendingReload = false;
lastReloadAt = 0;
lastMtime = 0;
hmrClients.clear();
for (const id of heartbeatIntervals) clearInterval(id);
heartbeatIntervals.clear();
if (watcherInterval) {
clearInterval(watcherInterval);
watcherInterval = undefined;
}
};
export const _getHmrClientsForTests = () => hmrClients;
async function _watchTickImpl(
statFn: (path: string) => Promise<Deno.FileInfo>,
) {
try {
const stat = await statFn("./.build_done");
const mtime = stat.mtime?.getTime() || 0;
if (mtime > lastMtime) {
lastMtime = mtime;
const now = Date.now();
if (now - lastReloadAt < 1500) return;
lastReloadAt = now;
pendingReload = true;
console.log(
"HMR: Detected change, sending reload to",
hmrClients.size,
"clients",
);
let sent = 0;
for (const client of hmrClients) {
const cid = __hmrClientIds.get(client) || -1;
try {
if (client.readyState === WebSocket.OPEN) {
client.send("reload");
sent++;
console.log("HMR: sent reload to client", cid);
} else {
hmrClients.delete(client);
}
} catch (e) {
console.warn("HMR: failed to send to client", cid, e);
hmrClients.delete(client);
}
}
if (sent > 0) {
pendingReload = false;
console.log("HMR: reload delivered to", sent, "clients");
} else {
console.log("HMR: no clients received reload, pending remains true");
}
}
} catch (e) {
if (Deno.env.get("ENV") === "coverage-throw") throw e;
void 0;
}
}
export function _watchTickForTestsWithStat(
statFn: (path: string) => Promise<Deno.FileInfo>,
) {
return _watchTickImpl(statFn);
}
export function _watchTickForTests() {
return _watchTickImpl(Deno.stat.bind(Deno));
}
export function _setLastMtimeForTests(v: number) {
lastMtime = v;
}
export function startComponentsWatcher(
opts: { startInterval?: boolean; immediate?: boolean } = {},
) {
const { startInterval = true, immediate } = opts;
void 0;
if (Deno.env.get("ENV") === "production") return;
if (watcherStarted) return;
watcherStarted = true;
try {
try {
Deno.statSync("./.build_done");
} catch {
try {
Deno.writeTextFileSync("./.build_done", Date.now().toString());
} catch (_) { }
}
try {
const stat = Deno.statSync("./.build_done");
lastMtime = stat.mtime?.getTime() || 0;
} catch (_) { }
void 0;
const __fastro_watcher_cb = () => {
_watchTickForTests().catch(() => {});
};
if (startInterval) {
watcherInterval = setInterval(__fastro_watcher_cb, 500);
}
const shouldImmediate = immediate === true ||
Deno.env.get("FASTRO_COVERAGE") === "1" ||
Deno.env.get("ENV") === "coverage";
if (shouldImmediate) {
try {
__fastro_watcher_cb();
} catch (_) {
void 0;
}
}
} catch (_e) {
}
}
type RenderToStringOptions = {
identifierPrefix?: string;
signal?: AbortSignal;
nonceProvider?: () => string;
onError?: (error: unknown) => void;
};
type RenderOptions = {
module?: string;
includeDoctype?: boolean;
includeHead?: boolean;
head?: string;
title?: string;
initialProps?: Record<string, unknown>;
} & RenderToStringOptions;
const createRenderToString = (_context: Context) => {
return (component: React.ReactElement, opts: RenderOptions = {}) => {
const {
module: moduleFromOpts,
identifierPrefix,
signal,
nonceProvider,
onError,
includeDoctype = false,
includeHead = true,
head,
title,
initialProps,
} = opts;
const renderOptions: RenderToStringOptions = {
identifierPrefix,
signal,
nonceProvider,
onError,
};
const componentWithProps = initialProps
? React.cloneElement(component, initialProps)
: component;
const bodyHtml = renderToString(componentWithProps, renderOptions);
if (!includeHead) {
return includeDoctype ? `<!DOCTYPE html>${bodyHtml}` : bodyHtml;
}
const headContent = head || `<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title || "Fastro App"}</title>
</head>`;
const initialPropsScript = initialProps
? `<script id="initial" type="application/json">${
JSON.stringify(initialProps).replace(/</g, "\\u003c")
}</script>`
: "";
const resolvedModule = moduleFromOpts ?? ((_context && _context.state &&
typeof _context.state.module === "string")
? _context.state.module
: undefined);
const isProd = Deno.env.get("ENV") === "production";
const timestamp = !isProd ? `?t=${Date.now()}` : "";
const clientScript = resolvedModule
? `<script src="/js/${resolvedModule}/client.js${timestamp}" defer></script>`
: "";
const hmrScript = !isProd ? rawHMRscript : "";
const html = `<html lang="en">
${headContent}
<body id="root">${bodyHtml}${initialPropsScript}${clientScript}${hmrScript}</body>
</html>`;
return includeDoctype ? `<!DOCTYPE html>${html}` : html;
};
};
export const createRenderMiddleware = (_options?: Record<string, unknown>) => {
return (req: Request, context: Context, next: Next) => {
let url: URL;
try {
const urlStr = req.url || "/";
url = new URL(urlStr);
} catch (_e) {
const urlStr = req.url || "/";
url = new URL(urlStr, "http://localhost");
}
if (Deno.env.get("ENV") !== "production") {
startComponentsWatcher();
}
if (
!context.renderToString ||
(context.renderToString as unknown as { __is_stub?: boolean }).__is_stub
) {
context.renderToString = createRenderToString(context);
}
if (url.pathname === "/hmr") {
const { socket, response } = Deno.upgradeWebSocket(req);
hmrClients.add(socket);
const cid = __nextHmrClientId++;
__hmrClientIds.set(socket, cid);
socket.onopen = () => {
socket.send("connected");
if (pendingReload) {
socket.send("reload");
pendingReload = false;
}
const heartbeatId = setInterval(() => {
try {
socket.send("heartbeat");
} catch (_) {
clearInterval(heartbeatId);
heartbeatIntervals.delete(heartbeatId);
hmrClients.delete(socket);
}
}, 10000);
heartbeatIntervals.add(heartbeatId);
socket.onclose = () => {
clearInterval(heartbeatId);
heartbeatIntervals.delete(heartbeatId);
hmrClients.delete(socket);
};
};
return response;
}
return next();
};
};