import mime from "https://esm.sh/mime@4.0.3"; import { parseArgs } from "jsr:@std/cli/parse-args"; import bundler, {type ESBuild, type ImportMap} from "./mod.ts"; import Introspect from "./introspect.ts"; if(Deno.mainModule == import.meta.url) { serve(parseArgs(Deno.args) as ServeArgs); } export type ServeArgs = {path?:string, html?:string, port?:string|number}; async function serve(settings:ServeArgs):Promise { // etag hash const ETag = new Date().getTime().toString(); // extra parsing const ExtensionsJS = ["tsx", "ts", "jsx", "js", "jsx", "mjs"]; const PathExtensionType=(inPath:string)=> { const path = inPath.endsWith("/") ? inPath.substring(0, inPath.length-1) : inPath; const posDot = path.lastIndexOf("."); const extension = (posDot > path.lastIndexOf("/")) ? path.substring(posDot+1) : null; let type:string|null = null if(extension) { type = (ExtensionsJS.includes(extension)) ? "application/javascript" : mime.getType(extension); } return {path, extension, type}; } // base path let basePath = ""; let DenoConfig:ImportMap const SetDirectory =async(inPath:string)=>{ if(inPath.startsWith("http://")||inPath.startsWith("https://")||inPath.startsWith("file://")) { basePath = inPath; } else { basePath = new URL(inPath, new URL(`file://${Deno.cwd().replaceAll("\\", "/")}/`).toString()).href; } if(!basePath.endsWith("/")) { basePath = basePath + "/"; } console.log("Base Path:", basePath); DenoConfig = await Introspect(basePath) as ImportMap; console.log("found config", DenoConfig); } await SetDirectory(settings.path||"./"); // bundler const transpiled:Map = new Map(); const defaultConfig:ESBuild.BuildOptions = { entryPoints:[] as string[], outdir:"/", entryNames: `[dir][name]`, splitting: true }; const bundle =async(pathName:string, pathBase:string, buildConfig:ESBuild.BuildOptions = {}):Promise=> { const lookupKey = pathName.substring(0, pathName.lastIndexOf("."))+".js"; const lookup = transpiled.get(lookupKey); if(lookup) { return lookup; } buildConfig = {...defaultConfig, ...buildConfig} buildConfig.entryPoints = [pathBase+pathName]; try { const results = await bundler(basePath, buildConfig, DenoConfig); if(results.outputFiles) { results.outputFiles.forEach(output=>{transpiled.set(output.path, output.text); console.log("building", output.path);}) return results.outputFiles[0].text; } } catch(_e){return;} }; // index html serving const resp404 = new Response("404", {status:404}); const resp304 = new Response(null, {status:304}); let respIndexBody:false|string = false; if(settings.html) { if(settings.html.indexOf("<") != -1) // if html is actual markup instead of a url { respIndexBody = settings.html; } else { const load = await fetch(basePath+settings.html); respIndexBody = await load.text(); } } ///// go Deno.serve({port:parseInt(settings.port as string)||8000}, async(req)=> { const checkHash = req.headers.get("if-none-match") if(checkHash === ETag){ return resp304; } const url = new URL(req.url); const {path, type} = PathExtensionType(url.pathname); const headers = {"content-type":type||"", ETag, "Cache-Control":"max-age=3600"} // serve javascript if(type === "application/javascript") { const checkBuild = await bundle(path, basePath); if(checkBuild) { return new Response(checkBuild, {headers}); } } // serve static if(type) { try{ const handle = await fetch(basePath+path.substring(1)); return new Response(handle.body, {headers}); } catch(_e){ return resp404; } } // serve routes/implied index html files if(respIndexBody) { return new Response(respIndexBody, {headers}); } else { try{ const handle = await fetch(basePath+path.substring(1)); headers["content-type"] = "text/html" return new Response(handle.body, {headers}); } catch(_e){ return resp404; } } }); } export default serve