esbuild-wasm-bundler/serve.tsx
2024-08-13 10:25:16 -04:00

153 lines
4.8 KiB
TypeScript

import * as mime from "jsr:@std/media-types";
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<void>
{
// 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.contentType(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<string, string> = 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<string|void>=>
{
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)=>
{
console.log("new request", req.url)
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