import { contentType } from "jsr:@std/media-types"; import { ModuleProxy } from "./hmr/hmr-static.tsx"; const keyBundle = encodeURI(">"); const keyAdjacent = encodeURI("^"); const keyReload = "reload"; const keysExtension = ["ts", "tsx"]; const extractExtension =(path:string)=> { const ind = path.lastIndexOf("."); return ind === -1 ? "" : path.substring(ind+1); } const RootRunning = new URL(`file://${Deno.cwd()}`).toString(); const RootSiblings = import.meta.resolve("./"); const bakeConfigPackage:Deno.bundle.Options = { entrypoints:[""], platform: "browser", format:"esm", write: false, minify: false, } const bakeConfigLocal:Deno.bundle.Options = {...bakeConfigPackage, sourcemap:"inline", inlineImports:false }; async function BakeForce(path:string, type?:"package") { // If already baking, return the in-flight promise. (Caller may also call BakeCheck which handles this.) if (BakeCache[path] && typeof (BakeCache[path] as any)?.then === "function") { return await BakeCache[path] as CachedTranspile | undefined; } // Create a fresh config per bake to avoid shared mutation. const config = {...(type ? bakeConfigPackage : bakeConfigLocal)}; config.entrypoints = [...config.entrypoints]; config.entrypoints[0] = type ? path : RootRunning+path; console.log("baking", config.entrypoints); // store the in-flight promise immediately so concurrent callers reuse it const inflight = (async () => { try { const result = await Deno.bundle(config); if (result.outputFiles) { const body = result.outputFiles.map(file=>file.text()).join("\n"); const save:CachedTranspile = [body, type ? "" : ModuleProxy(body, path)]; BakeCache[path] = save; // replace promise with resolved value return save; } } catch (e) { console.log("BakeForce error for", path, e); } // failed - remove cache entry so next attempt can retry delete BakeCache[path]; return undefined; })(); BakeCache[path] = inflight; return await inflight; }; async function BakeCheck(path:string, type?:"package") { const lookup = BakeCache[path]; if(!lookup) { return await BakeForce(path, type); } // if an in-flight promise is stored, await it if (typeof (lookup as any)?.then === "function") { return await lookup as CachedTranspile | undefined; } return lookup as CachedTranspile; } type CachedTranspile = [file:string, profile:string] // BakeCache may hold a resolved cached tuple or an in-flight Promise that resolves to one. const BakeCache:Record | undefined> = {} const denoBody = await fetch(RootRunning+"/deno.json").then(resp=>resp.json()); for(const key in denoBody.imports) { const value = denoBody.imports[key]; if(value.startsWith("npm:")) { denoBody.imports[key] = "/>/"+value; } } const react = denoBody.compilerOptions.jsxImportSource || "react"; denoBody.imports["react-original"] = denoBody.imports[react]; denoBody.imports[react] = "/^/hmr/hmr-react.tsx"; denoBody.imports[react+"/jsx-runtime"] = "/^/hmr/hmr-react.tsx"; console.log(denoBody.imports); const importMap = ``; let htmlPageBody = await fetch(RootRunning+"/index.html").then(resp=>resp.text()); htmlPageBody = htmlPageBody.replace("", ""+importMap); const htmlPageHead = {headers:{"content-type":"text/html"}} const IndexResponse =()=> new Response(htmlPageBody, htmlPageHead); const JSHead = {headers:{"content-type":"application/javascript"}}; const JSResponse =(body:string)=> { const encoder = new TextEncoder(); const stream = new ReadableStream({ start(controller) { controller.enqueue(encoder.encode(body)); controller.close(); } }); return new Response(stream, JSHead); } Deno.serve(async(req:Request)=> { if(req.headers.get("upgrade") == "websocket") { try { const { response, socket } = Deno.upgradeWebSocket(req); socket.onopen = () => SocketsLive.add(socket); socket.onclose = () => SocketsLive.delete(socket); socket.onerror = (e) => { console.log("socket error", e); SocketsLive.delete(socket); }; socket.onmessage = () => {}; return response; } catch(e){ console.log("upgradeWebSocket failed", e); } } const url = new URL(req.url); const parts = url.pathname.split("/").filter(part=>part); // if there are no path segments, serve index immediately (avoid calling extractExtension on undefined) if(parts.length === 0) { return IndexResponse(); } const lastPart = parts.at(-1) ?? ""; const extension = extractExtension(lastPart); if(parts[0] == keyBundle) { const proxiedPath = parts.slice(1).join("/"); const transpiled = await BakeCheck(proxiedPath, "package"); return JSResponse(transpiled[0]); } if(parts[0] == keyAdjacent) { const proxiedPath = "/"+parts.slice(1).join("/"); const transpiled = await BakeCheck(proxiedPath); return JSResponse(transpiled[0]); } if(keysExtension.includes(extension)) { const transpiled = await BakeCheck(url.pathname); //return JSResponse(transpiled[0]); return JSResponse(transpiled[url.searchParams.has(keyReload) ? 0 : 1]); } if(!extension) { return IndexResponse(); } return new Response(); }); const SocketsLive = new Set(); const SocketsSend = (inData: string) => { for (const socket of Array.from(SocketsLive)) { try { if (socket.readyState === WebSocket.OPEN) { socket.send(inData); } else { SocketsLive.delete(socket); } } catch (e) { console.log("socket send failed:", e); SocketsLive.delete(socket); } } }; const Watcher =async()=> { let blocking = false; const filesChanged:Map = new Map(); for await (const event of Deno.watchFs(Deno.cwd())) { event.paths.forEach( path => filesChanged.set(path, event.kind) ); if(!blocking) { blocking = true; setTimeout(async()=> { // filesChanged is a Map, iterate normally for (const [path, action] of filesChanged) { const extension = extractExtension(path); if(keysExtension.includes(extension)) { const key = path.substring(Deno.cwd().length).replaceAll("\\", "/"); console.log("File change", path, key); if(action != "remove") { await BakeForce(key); SocketsSend(key); } else { delete BakeCache[key]; } } } filesChanged.clear(); blocking = false; } , 1000); } } } Watcher();