import { path, fileServer, graph } from "./deps.ts";import { HttpError } from "./serial.ts";
export interface ServeAssetOptions { cwd?: string; dir?: string; path: string;}
const htmlRelativeLinks = /<[a-z\-]+(?:\s+[a-z\-]+(?:(?:=".*")|(?:='.*'))?\s*)*\s+((?:href|src)=(?:"\.\.?\/.*?"|'\.\.?\/.*?'))(?:\s+[a-z\-]+(?:(?:=".*")|(?:='.*'))?\s*)*\/?>/g;
function parseCwd(cwd: string): string { if (cwd.startsWith("file://")) { return path.join(path.fromFileUrl(cwd), ".."); } return cwd;}
export async function serveAsset( req: Request, opt: ServeAssetOptions,): Promise<Response> { const cwd = parseCwd(opt.cwd || "."); const dir = opt.dir || "assets"; const filePath = opt.path;
prepareAssets({ cwd, dir, watch: true, });
const process = async (filePath: string) => { filePath = path.join( cwd, dir, path.join("/", filePath), ); let fileInfo: Deno.FileInfo | null = null; try { if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) { throw new Error(".ts and .tsx files are always ignored"); }
fileInfo = await Deno.stat(filePath); } catch { try { const p = `${filePath}.html`; const info = await Deno.stat(p); if (info.isFile) { filePath = p; fileInfo = info; } } catch { } }
let wasAutoIndexed = false; if (fileInfo && fileInfo.isDirectory) { fileInfo = null; try { const p = path.join(filePath, "index.html"); const info = await Deno.stat(p); if (info.isFile) { filePath = p; fileInfo = info; wasAutoIndexed = true; } } catch { } }
if (fileInfo === null) { throw new HttpError("404 not found", { status: 404 }); }
return { servePath: filePath, wasAutoIndexed }; };
let servePath = ""; try { const p = await process(filePath); servePath = p.servePath; const originalResponse = await fileServer.serveFile(req, servePath);
const url = new URL(req.url); if (!p.wasAutoIndexed || url.pathname.endsWith("/")) { return originalResponse; }
const basename = path.basename(url.pathname); let content = await Deno.readTextFile(servePath);
content = content.replaceAll(htmlRelativeLinks, (match, group) => { const newGroup = group.replace( /^(?:src|href)=(?:"|')(\..*)(?:"|')$/g, (m: string, g: string) => m.replace(g, ( g.startsWith("./") ? `./${basename}/${g.slice(2)}` : g.startsWith("../") ? `./${g.slice(3)}` : g )), ); return match.replace(group, newGroup); });
originalResponse.headers.delete("content-length"); return new Response(content, { headers: originalResponse.headers }); } catch (e1) { if (e1.message === "404 not found") { throw new HttpError("404 not found", { status: 404 }); } throw e1; }}
const canEmit = typeof Deno.emit === "undefined";const watchingAssets = new Set<string>();
export async function prepareAssets(opt: { cwd?: string; dir?: string; watch?: boolean;}) { const cwd = parseCwd(opt.cwd || "."); const dir = opt.dir || "assets"; const assets = path.join(cwd, dir);
if ( !canEmit || (opt.watch && watchingAssets.has(assets)) ) { return; }
if ((await Deno.permissions.query({ name: "write", path: assets, })).state !== "granted") { return; }
const check = await Deno.stat(assets); if (!check.isDirectory) { throw new Error(`path given is not a directory: ${assets}`); }
const modules: string[] = []; const findModules = async (dir: string) => { for await (const entry of Deno.readDir(dir)) { if ( entry.isFile && (entry.name.endsWith("_bundle.ts") || entry.name.endsWith("_bundle.tsx")) ) { modules.push(path.join(dir, entry.name)); } else if (entry.isDirectory) { await findModules(path.join(dir, entry.name)); } } }; await findModules(dir);
const bundle = async (input: string) => { const output = input + ".js";
const js = (await Deno.emit(input, { bundle: "module", check: false, compilerOptions: { lib: [ "dom", "dom.iterable", "dom.asynciterable", "esnext", ], }, })).files["deno:///bundle.js"];
await Deno.writeTextFile(output, js); };
const isFile = async (path: string) => { try { const check = await Deno.stat(path); return check.isFile; } catch { return false; } };
const watching = new Set<string>(); const watch = async (input: string) => { input = path.resolve(input); if (watching.has(input)) { return; } if (!await isFile(input)) { watching.delete(input); return; } watching.add(input);
let inputGraph: graph.ModuleGraph; try { inputGraph = await graph.createGraph( path.toFileUrl(input).href ); } catch (e) { console.error("Failed to graph", input, "-", e); watching.delete(input); return; }
const deps = inputGraph.modules .filter(m => m.specifier.startsWith("file://")) .map(m => path.fromFileUrl(m.specifier)); try { await bundle(input); } catch (e) { console.error("Failed to bundle", input, "-", e); }
for await (const _ of Deno.watchFs(deps)) { break; }
if (!await isFile(input)) { watching.delete(input); return; }
watching.delete(input); watch(input); };
if (opt.watch) { watchingAssets.add(dir); }
for (const m of modules) { if (!opt.watch) { await bundle(m); } else { watch(m); } }
if (!opt.watch) { return; }
(async () => { for await (const event of Deno.watchFs(dir)) { if (event.kind === "create" || event.kind === "modify") { for (const p of event.paths) { if (p.endsWith("_bundle.ts") || p.endsWith("_bundle.tsx")) { watch(p); } } } } })();}