summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xbun.lockbbin2002 -> 3084 bytes
-rw-r--r--package.json2
-rw-r--r--src/index.ts73
3 files changed, 67 insertions, 8 deletions
diff --git a/bun.lockb b/bun.lockb
index 5ccd483..2bb84c6 100755
--- a/bun.lockb
+++ b/bun.lockb
Binary files differdiff --git a/package.json b/package.json
index 314e9f9..7798b26 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
   "name": "homestead",
   "module": "src/index.ts",
   "devDependencies": {
+    "@types/mime-types": "^2.1.1",
     "bun-html-live-reload": "^0.1.1",
     "bun-types": "latest"
   },
@@ -10,6 +11,7 @@
   },
   "type": "module",
   "dependencies": {
+    "mime-types": "^2.1.35",
     "toml": "^3.0.0"
   }
 }
diff --git a/src/index.ts b/src/index.ts
index b6d0713..1616df7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,6 +2,8 @@ import path from "node:path";
 import fs, { Stats } from "node:fs";
 import fsp from "node:fs/promises";
 import { withHtmlLiveReload } from "bun-html-live-reload";
+import mime from "mime-types";
+import type { BunFile } from "bun-types";
 
 import readConfig from "./config";
 
@@ -13,6 +15,8 @@ const defaultHeaders = config.extra.headers;
 
 type File = {
   filename: string;
+  handle: BunFile;
+  relPath: string;
   headers?: Record<string, string>;
   size: number;
   mtime: Date;
@@ -20,7 +24,12 @@ type File = {
 
 let files = new Map<string, File>();
 
-function registerFile(pathname: string, filename: string, stat: Stats): void {
+function registerFile(
+  path: string,
+  pathname: string,
+  filename: string,
+  stat: Stats,
+): void {
   pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname);
 
   if (files.get(pathname) !== undefined) {
@@ -28,6 +37,8 @@ function registerFile(pathname: string, filename: string, stat: Stats): void {
   }
   files.set(pathname, {
     filename,
+    relPath: "/" + path,
+    handle: Bun.file(filename),
     headers:
       pathname === "/404.html"
         ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" })
@@ -46,14 +57,14 @@ function walkDirectory(root: string, dir: string) {
     if (stat.isDirectory()) {
       walkDirectory(root, relPath + path.sep);
     } else if (stat.isFile()) {
-      if (pathname === "index.html") {
-        const dir = path.dirname(relPath);
-        registerFile(dir, absPath, stat);
+      if (pathname.startsWith("index.html")) {
+        const dir = relPath.replace("index.html", "");
+        registerFile(relPath, dir, absPath, stat);
         if (dir !== ".") {
-          registerFile(dir + path.sep, absPath, stat);
+          registerFile(relPath, dir + path.sep, absPath, stat);
         }
       }
-      registerFile(relPath, absPath, stat);
+      registerFile(relPath, relPath, absPath, stat);
     }
   }
 }
@@ -63,24 +74,41 @@ walkDirectory(publicDir, "");
 async function serveFile(
   file: File | undefined,
   statusCode: number = 200,
+  extraHeaders: Record<string, string> = {},
 ): Promise<Response> {
-  if (file && (await fsp.exists(file.filename))) {
-    return new Response(Bun.file(file.filename), {
+  if (file && file.handle.exists()) {
+    return new Response(file.handle, {
       headers: {
         "last-modified": file.mtime.toUTCString(),
+        ...extraHeaders,
         ...(file.headers || defaultHeaders),
       },
       status: statusCode,
     });
   } else {
+    // TODO return encoded
     return serveFile(files.get("/404.html"), 404);
   }
 }
 
+async function serveEncodedFile(
+  file: File | undefined,
+  statusCode: number = 200,
+  extraHeaders: Record<string, string> = {},
+): Promise<Response> {
+  const res = await serveFile(file, statusCode, extraHeaders);
+  res.headers.delete("content-disposition");
+  return res;
+}
+
 function parseIfModifiedSinceHeader(header: string | null): number {
   return header ? new Date(header).getTime() + 999 : 0;
 }
 
+function getMIME(filename: string): string {
+  return mime.contentType(path.extname(filename)) || "text/plain";
+}
+
 export default withHtmlLiveReload({
   fetch: async function (request) {
     const pathname = new URL(request.url).pathname;
@@ -92,6 +120,35 @@ export default withHtmlLiveReload({
       ) {
         return new Response("", { status: 304 });
       }
+      const encodings = (request.headers.get("accept-encoding") || "")
+        .split(",")
+        .map((x) => x.trim().toLowerCase());
+      if (encodings.includes("br") && files.has(file.relPath + ".br")) {
+        console.log(
+          "Using br encoding for user agent",
+          request.headers.get("user-agent"),
+        );
+        return serveEncodedFile(files.get(file.relPath + ".br"), 200, {
+          "content-encoding": "br",
+          "content-type": getMIME(file.filename),
+        });
+      } else if (
+        encodings.includes("zstd") &&
+        files.has(file.relPath + ".zst")
+      ) {
+        return serveEncodedFile(files.get(file.relPath + ".zst"), 200, {
+          "content-encoding": "zstd",
+          "content-type": getMIME(file.filename),
+        });
+      } else if (
+        encodings.includes("gzip") &&
+        files.has(file.relPath + ".gz")
+      ) {
+        return serveEncodedFile(files.get(file.relPath + ".gz"), 200, {
+          "content-encoding": "gzip",
+          "content-type": getMIME(file.filename),
+        });
+      }
     }
     return serveFile(file);
   },