gale/server.ts
2025-10-08 12:50:23 -04:00

186 lines
6.0 KiB
TypeScript

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("./");
console.log(RootRunning);
console.log(RootSiblings);
const bakeConfigPackage:Deno.bundle.Options =
{
entrypoints:[""],
platform: "browser",
format:"esm",
write: false,
minify: true,
}
const bakeConfigLocal:Deno.bundle.Options = {...bakeConfigPackage, minify:false, sourcemap:"inline", inlineImports:false };
async function BakeForce(path:string, type?:"package")
{
const config = type ? bakeConfigPackage : bakeConfigLocal;
config.entrypoints[0] = type ? path : RootRunning+path;
console.log("baking", config.entrypoints);
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;
return save;
}
return undefined;
};
async function BakeCheck(path:string, type?:"package")
{
const lookup:CachedTranspile = await BakeCache[path];
if(!lookup)
{
return BakeForce(path, type);
}
return lookup;
}
type CachedTranspile = [file:string, profile:string]
const BakeCache:Record<string, CachedTranspile> = {}
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;
}
}
denoBody.imports["react-jsx-runtime-original"] = "/>/npm:react/jsx-runtime";
denoBody.imports["react/jsx-runtime"] = "/^/jsx-runtime.tsx";
// denoBody.imports["react-original"] = denoBody.imports["react"];
// denoBody.imports["react-jsx-runtime-original"] = denoBody.imports[denoBody.compilerOptions.jsxImportSource]+"/jsx-runtime";
// denoBody.imports["signals-original"] = denoBody.imports["@preact/signals"];
// denoBody.imports["@preact/signals"] = "/^/hmr/hmr-signal.tsx";
// denoBody.imports["react"] = "/^/hmr/hmr-react.tsx";
// denoBody.imports["react/jsx-runtime"] = "/^/hmr/hmr-react.tsx";
console.log(denoBody.imports);
const importMap = `<script type="importmap">{"imports":${JSON.stringify(denoBody.imports)}}</script>`;
let htmlPageBody = await fetch(RootRunning+"/index.html").then(resp=>resp.text());
htmlPageBody = htmlPageBody.replace("<head>", "<head>"+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)=>new Response(body, JSHead);
Deno.serve(async(req:Request)=>
{
if(req.headers.get("upgrade") == "websocket")
{
console.log(" --serve socket", req.url)
try
{
const { response, socket } = Deno.upgradeWebSocket(req);
socket.onopen = () => SocketsLive.add(socket);
socket.onclose = () => SocketsLive.delete(socket);
socket.onmessage = () => {};
socket.onerror = (e) => console.log("Socket errored:", e);
return response;
}
catch(e){ console.log("Socket errored:", e); }
}
const url = new URL(req.url);
const parts = url.pathname.split("/").filter(part=>part);
const lastPart = parts.at(-1);
const extension = extractExtension(lastPart);
console.log("REQUEST:", parts);
if(parts[0] == keyBundle)
{
const proxiedPath = parts.slice(1).join("/");
const transpiled = await BakeCheck(proxiedPath, "package");
console.log(" --serve bundle", proxiedPath);
return JSResponse(transpiled[0]);
}
if(parts[0] == keyAdjacent)
{
const proxiedPath = "/"+parts.slice(1).join("/");
const transpiled = await BakeCheck(proxiedPath);
console.log(" --serve adjacent", proxiedPath);
return JSResponse(transpiled[0]);
}
if(keysExtension.includes(extension))
{
console.log(" --serve local file", parts)
const transpiled = await BakeCheck(url.pathname);
return JSResponse(transpiled[url.searchParams.has(keyReload) ? 0 : 1]);
}
if(!extension)
{
console.log(" --serving html page", parts);
return IndexResponse();
}
return new Response();
});
const SocketsLive:Set<WebSocket> = new Set();
const SocketsSend =(inData:string)=>{ for (const socket of SocketsLive){ socket.send(inData); } }
const Watcher =async()=>
{
let blocking = false;
const filesChanged:Map<string, string> = 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()=>
{
for await (const [path, action] of filesChanged)
{
const extension = extractExtension(path);
if(keysExtension.includes(extension))
{
console.log("File change", path);
const key = path.substring(Deno.cwd().length).replaceAll("\\", "/");
if(action != "remove")
{
BakeForce(path);
SocketsSend(key);
}
else
{
delete BakeCache[key];
}
}
}
filesChanged.clear();
blocking = false;
}
, 1000);
}
}
}
Watcher();