import { path, fileServer, graph } from "./deps.ts";import { HttpError } from "./client.ts";import { NO_MATCH } from "./http.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;
export async function serveAsset( req: Request, opt: ServeAssetOptions,): Promise<Response> { let cwd = opt.cwd || "."; const dir = opt.dir || "assets"; const filePath = opt.path;
if (cwd.startsWith("file://")) { cwd = path.join(path.fromFileUrl(cwd), ".."); }
if ( typeof Deno.emit !== "undefined" && typeof Deno.writeTextFile !== "undefined" ) { await prepareAssets(path.join(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 NO_MATCH; } throw e1; }}
const watchingAssets = new Set<string>();
export async function prepareAssets(dir: string, opt: { watch: boolean;}) { if (opt.watch && watchingAssets.has(dir)) { return; }
const check = await Deno.stat(dir); if (!check.isDirectory) { throw new Error("path given was not a directory"); }
const modules: string[] = []; const findModules = async (dir: string) => { for await (const entry of Deno.readDir(dir)) { if ( entry.isFile && (entry.name.endsWith(".ts") || entry.name.endsWith(".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.endsWith(".ts") ? input.slice(0, -3) + ".bundle.js" : input.endsWith(".tsx") ? input.slice(0, -4) + ".bundle.js" : input + ".bundle.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); };
for (const m of modules) { try { await bundle(m); } catch (e) { if (!opt.watch) { throw e; } console.error("Bundle error:", e); } }
if (!opt.watch) { return; } watchingAssets.add(dir);
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) || !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)); for await (const _ of Deno.watchFs(deps)) { break; }
if (!await isFile(input)) { watching.delete(input); return; }
try { await bundle(input); } catch (e) { console.error("Failed to bundle -", input, "-", e); } watching.delete(input); watch(input); };
for (const m of modules) { watch(m); }
(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(".ts") || p.endsWith(".tsx")) { watch(p); } } } } })();}