From a25028aa30bf0f3b89a9a7c99192e1a14267fc97 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Mon, 11 Sep 2023 14:52:07 +0200 Subject: Initial commit --- .gitignore | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 22 +++++++ bun.lockb | Bin 0 -> 2354 bytes package.json | 15 +++++ src/index.ts | 10 ++++ test/index.test.ts | 21 +++++++ tsconfig.json | 22 +++++++ 7 files changed, 259 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 test/index.test.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f81d56e --- /dev/null +++ b/.gitignore @@ -0,0 +1,169 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* diff --git a/README.md b/README.md new file mode 100644 index 0000000..3430625 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# homestead + +## Goals + +1. To be a near-drop-in replacement for Zola +2. More indieweb features + +## Installing + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run src/index.ts +``` + +This project was created using `bun init` in bun v0.8.1. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..2ab9dd9 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..96a8252 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "homestead", + "module": "src/index.ts", + "devDependencies": { + "bun-html-live-reload": "^0.1.1", + "bun-types": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "type": "module", + "dependencies": { + "siopao": "^0.4.0" + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8c0a3ba --- /dev/null +++ b/src/index.ts @@ -0,0 +1,10 @@ +import { withHtmlLiveReload } from "bun-html-live-reload"; +import Siopao from "siopao"; + +const router = new Siopao(); + +router.get("/status", () => new Response("OK")); + +export default withHtmlLiveReload({ + fetch: router.fetch.bind(router), +}) diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..d82cb9e --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,21 @@ +import { type Server } from "bun" +import { expect, test, beforeAll, afterAll } from "bun:test" + +import app from "../src/index" + +const port = 33000; +let server: Server + +beforeAll(async function () { + server = Bun.serve(Object.assign({}, app, { port })) +}) + +afterAll(function () { + server.stop() +}) + +test("/status returns 200 OK", async function () { + const res = await fetch(`http://localhost:${port}/status`) + expect(res.status).toBe(200) + expect(await res.text()).toBe("OK") +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1c542d7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ES2017", "ES2019", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "preserve", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" // add Bun global + ] + } +} -- cgit 1.4.1 From 51cc4389f6dc7947ee34d1b3367876941e8a8fbc Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Mon, 11 Sep 2023 19:43:06 +0200 Subject: Serve static files --- README.md | 5 +++-- bun.lockb | Bin 2354 -> 2042 bytes package.json | 2 +- src/index.ts | 12 ++++++------ test/index.test.ts | 9 ++++++++- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3430625..34f3f42 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ ## Goals -1. To be a near-drop-in replacement for Zola -2. More indieweb features +1. Static web server with prometheus-based analytics +2. Dynamic web server capable of generating Zola-based websites +3. More indieweb features ## Installing diff --git a/bun.lockb b/bun.lockb index 2ab9dd9..f724266 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 96a8252..d0e61be 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,6 @@ }, "type": "module", "dependencies": { - "siopao": "^0.4.0" + "serve-static-bun": "^0.5.3" } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 8c0a3ba..59913b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ -import { withHtmlLiveReload } from "bun-html-live-reload"; -import Siopao from "siopao"; +import { withHtmlLiveReload } from "bun-html-live-reload" +import serveStatic from "serve-static-bun" -const router = new Siopao(); - -router.get("/status", () => new Response("OK")); +const dir = Bun.argv.length > 2 ? Bun.argv[Bun.argv.length - 1] : "./" export default withHtmlLiveReload({ - fetch: router.fetch.bind(router), + fetch: serveStatic(dir, { + dotfiles: "allow" + }), }) diff --git a/test/index.test.ts b/test/index.test.ts index d82cb9e..8a32e47 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -4,6 +4,7 @@ import { expect, test, beforeAll, afterAll } from "bun:test" import app from "../src/index" const port = 33000; +const base = `http://localhost:${port}/`; let server: Server beforeAll(async function () { @@ -15,7 +16,13 @@ afterAll(function () { }) test("/status returns 200 OK", async function () { - const res = await fetch(`http://localhost:${port}/status`) + const res = await fetch(new URL("/status", base)) expect(res.status).toBe(200) expect(await res.text()).toBe("OK") }) + +test("/ returns 200 and says Hello world", async function () { + const res = await fetch(base) + expect(res.status).toBe(200) + expect(await res.text()).toBe("Hello world") +}) -- cgit 1.4.1 From 7fc8048d3104cf9e129920326b30aaefaaaeb89b Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 10:55:15 +0200 Subject: Init flake --- .envrc | 1 + flake.lock | 27 +++++++++++++++++++++++++++ flake.nix | 31 +++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..0f56f8b --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1689261696, + "narHash": "sha256-LzfUtFs9MQRvIoQ3MfgSuipBVMXslMPH/vZ+nM40LkA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "df1eee2aa65052a18121ed4971081576b25d6b5c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..04bef10 --- /dev/null +++ b/flake.nix @@ -0,0 +1,31 @@ +{ + description = "A Nix-flake-based Node.js development environment"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + outputs = { self, nixpkgs }: + let + overlays = [ + (final: prev: rec { + nodejs = prev.nodejs-18_x; + bun = (prev.bun.override { inherit nodejs; }); + }) + ]; + supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { + pkgs = import nixpkgs { inherit overlays system; }; + }); + in + { + devShells = forEachSupportedSystem ({ pkgs }: { + default = pkgs.mkShell { + packages = with pkgs; [ + node2nix + nodejs + ] ++ (with pkgs.nodePackages; [ + prettier + ]); + }; + }); + }; +} -- cgit 1.4.1 From b45b4e37af14d94726b0c5d2691289886c0527cf Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 10:56:10 +0200 Subject: Reformat with prettier --- src/index.ts | 10 +++++----- test/index.test.ts | 32 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/index.ts b/src/index.ts index 59913b3..4887dd6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ -import { withHtmlLiveReload } from "bun-html-live-reload" -import serveStatic from "serve-static-bun" +import { withHtmlLiveReload } from "bun-html-live-reload"; +import serveStatic from "serve-static-bun"; -const dir = Bun.argv.length > 2 ? Bun.argv[Bun.argv.length - 1] : "./" +const dir = Bun.argv.length > 2 ? Bun.argv[Bun.argv.length - 1] : "./"; export default withHtmlLiveReload({ fetch: serveStatic(dir, { - dotfiles: "allow" + dotfiles: "allow", }), -}) +}); diff --git a/test/index.test.ts b/test/index.test.ts index 8a32e47..234bd1e 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,28 +1,28 @@ -import { type Server } from "bun" -import { expect, test, beforeAll, afterAll } from "bun:test" +import { type Server } from "bun"; +import { expect, test, beforeAll, afterAll } from "bun:test"; -import app from "../src/index" +import app from "../src/index"; const port = 33000; const base = `http://localhost:${port}/`; -let server: Server +let server: Server; beforeAll(async function () { - server = Bun.serve(Object.assign({}, app, { port })) -}) + server = Bun.serve(Object.assign({}, app, { port })); +}); afterAll(function () { - server.stop() -}) + server.stop(); +}); test("/status returns 200 OK", async function () { - const res = await fetch(new URL("/status", base)) - expect(res.status).toBe(200) - expect(await res.text()).toBe("OK") -}) + const res = await fetch(new URL("/status", base)); + expect(res.status).toBe(200); + expect(await res.text()).toBe("OK"); +}); test("/ returns 200 and says Hello world", async function () { - const res = await fetch(base) - expect(res.status).toBe(200) - expect(await res.text()).toBe("Hello world") -}) + const res = await fetch(base); + expect(res.status).toBe(200); + expect(await res.text()).toBe("Hello world"); +}); -- cgit 1.4.1 From 78439b16cc66532225e75c9aa40cf7c49cddc22d Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 10:56:39 +0200 Subject: Read config from TOML file --- bun.lockb | Bin 2042 -> 2386 bytes package.json | 3 ++- src/config.ts | 9 +++++++++ src/index.ts | 10 ++++++++-- 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 src/config.ts diff --git a/bun.lockb b/bun.lockb index f724266..c8b2651 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d0e61be..27f95e0 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "type": "module", "dependencies": { - "serve-static-bun": "^0.5.3" + "serve-static-bun": "^0.5.3", + "toml": "^3.0.0" } } \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..064b038 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,9 @@ +import path from "node:path"; +import fs from "node:fs"; +import toml from "toml"; + +export default function readConfig(base: string) { + const filename = path.join(base, "config.toml"); + + return toml.parse(fs.readFileSync(filename, "utf-8")); +} diff --git a/src/index.ts b/src/index.ts index 4887dd6..d8c9bf1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,16 @@ +import path from "node:path"; import { withHtmlLiveReload } from "bun-html-live-reload"; import serveStatic from "serve-static-bun"; -const dir = Bun.argv.length > 2 ? Bun.argv[Bun.argv.length - 1] : "./"; +import readConfig from "./config"; + +const base = Bun.argv.length > 2 ? Bun.argv[Bun.argv.length - 1] : "."; + +const config = readConfig(base); export default withHtmlLiveReload({ - fetch: serveStatic(dir, { + fetch: serveStatic(path.join(base, "public"), { + headers: config.extra.headers, dotfiles: "allow", }), }); -- cgit 1.4.1 From 2f6152539c540697290ec73a7c1b50b9f2db88c6 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 16:54:29 +0200 Subject: Use own logic for static file serving --- bun.lockb | Bin 2386 -> 3923 bytes package.json | 3 ++- src/index.ts | 71 ++++++++++++++++++++++++++++++++++++++++++++++++----- test/index.test.ts | 12 ++++----- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/bun.lockb b/bun.lockb index c8b2651..e8b79d3 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 27f95e0..0eb65a3 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "homestead", "module": "src/index.ts", "devDependencies": { + "@types/contains-path": "^1.0.2", "bun-html-live-reload": "^0.1.1", "bun-types": "latest" }, @@ -10,7 +11,7 @@ }, "type": "module", "dependencies": { - "serve-static-bun": "^0.5.3", + "contains-path": "^1.0.0", "toml": "^3.0.0" } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index d8c9bf1..ec1c296 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,75 @@ import path from "node:path"; +import fs, { Stats } from "node:fs"; +import fsp from "node:fs/promises"; +import util from "node:util"; + import { withHtmlLiveReload } from "bun-html-live-reload"; -import serveStatic from "serve-static-bun"; +import containsPath from "contains-path"; import readConfig from "./config"; -const base = Bun.argv.length > 2 ? Bun.argv[Bun.argv.length - 1] : "."; +const base = "../website/"; +const publicDir = path.resolve(base, "public") + path.sep; const config = readConfig(base); +const defaultHeaders = config.extra.headers; + +function getFilename(name: string): string { + return path.join(publicDir, `${name}`); +} + +let files: Map = new Map(); + +function registerFile(pathname: string, absPath: string, stat: Stats): void { + pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname); + + if (files.get(pathname) !== undefined) { + console.warn("File already registered:", pathname); + } + files.set(pathname, { + filename: absPath, + size: stat.size, + mtime: stat.mtime.toUTCString(), + }); +} + +function walkDirectory(root: string, dir: string) { + const absDir = path.join(root, dir); + for (let pathname of fs.readdirSync(absDir)) { + const relPath = path.join(dir, pathname); + const absPath = path.join(absDir, pathname); + const stat = fs.statSync(absPath); + if (stat.isDirectory()) { + walkDirectory(root, relPath + path.sep); + } else if (stat.isFile()) { + if (pathname === "index.html") { + registerFile(path.dirname(relPath), absPath, stat); + registerFile(path.dirname(relPath) + path.sep, absPath, stat); + } + registerFile(relPath, absPath, stat); + } + } +} + +walkDirectory(publicDir, ""); export default withHtmlLiveReload({ - fetch: serveStatic(path.join(base, "public"), { - headers: config.extra.headers, - dotfiles: "allow", - }), + fetch: async function (request) { + const pathname = new URL(request.url).pathname; + if (files.has(pathname)) { + const file = files.get(pathname); + console.info("filename", file.filename); + return new Response(Bun.file(file.filename), { + headers: defaultHeaders, + status: 200, + }); + } + return new Response(Bun.file(getFilename("404.html")), { + headers: Object.assign({}, defaultHeaders, { + "cache-control": "max-age=5, no-cache", + }), + status: 404, + statusText: "Not Found", + }); + }, }); diff --git a/test/index.test.ts b/test/index.test.ts index 234bd1e..c1f5f64 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -15,14 +15,12 @@ afterAll(function () { server.stop(); }); -test("/status returns 200 OK", async function () { - const res = await fetch(new URL("/status", base)); +test("/ returns 200", async function () { + const res = await fetch(base); expect(res.status).toBe(200); - expect(await res.text()).toBe("OK"); }); -test("/ returns 200 and says Hello world", async function () { - const res = await fetch(base); - expect(res.status).toBe(200); - expect(await res.text()).toBe("Hello world"); +test("/asdf returns 404", async function () { + const res = await fetch(`${base}asdf`); + expect(res.status).toBe(404); }); -- cgit 1.4.1 From 0ded4a2bf46a7c0e00523bf2f216fbfba12ba4d5 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 17:16:58 +0200 Subject: Give files map a proper type --- src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index ec1c296..e35368d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,13 @@ function getFilename(name: string): string { return path.join(publicDir, `${name}`); } -let files: Map = new Map(); +type File = { + filename: string; + size: number; + mtime: string; +}; + +let files = new Map(); function registerFile(pathname: string, absPath: string, stat: Stats): void { pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname); -- cgit 1.4.1 From aa663a1c5308c2035e1aa73836b932eba040abfc Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 17:17:23 +0200 Subject: Avoid duplicate registration of initial directory --- src/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index e35368d..10e9da1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,8 +49,11 @@ function walkDirectory(root: string, dir: string) { walkDirectory(root, relPath + path.sep); } else if (stat.isFile()) { if (pathname === "index.html") { - registerFile(path.dirname(relPath), absPath, stat); - registerFile(path.dirname(relPath) + path.sep, absPath, stat); + const dir = path.dirname(relPath); + registerFile(dir, absPath, stat); + if (dir !== ".") { + registerFile(dir + path.sep, absPath, stat); + } } registerFile(relPath, absPath, stat); } -- cgit 1.4.1 From 2ea9c80f1599398e122e3531c6e9be6b091e60af Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 17:17:50 +0200 Subject: wtf typescript --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 10e9da1..1b181a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,14 +65,14 @@ walkDirectory(publicDir, ""); export default withHtmlLiveReload({ fetch: async function (request) { const pathname = new URL(request.url).pathname; - if (files.has(pathname)) { - const file = files.get(pathname); - console.info("filename", file.filename); + const file = files.get(pathname); + if (file) { return new Response(Bun.file(file.filename), { headers: defaultHeaders, status: 200, }); } + return new Response(Bun.file(getFilename("404.html")), { headers: Object.assign({}, defaultHeaders, { "cache-control": "max-age=5, no-cache", -- cgit 1.4.1 From 817164e34248c559d4a883bf991e48407111473a Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 18:09:08 +0200 Subject: Remove unused dependency --- bun.lockb | Bin 3923 -> 2002 bytes package.json | 4 +--- src/index.ts | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/bun.lockb b/bun.lockb index e8b79d3..5ccd483 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 0eb65a3..314e9f9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "homestead", "module": "src/index.ts", "devDependencies": { - "@types/contains-path": "^1.0.2", "bun-html-live-reload": "^0.1.1", "bun-types": "latest" }, @@ -11,7 +10,6 @@ }, "type": "module", "dependencies": { - "contains-path": "^1.0.0", "toml": "^3.0.0" } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 1b181a1..8ae1d0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,6 @@ import fsp from "node:fs/promises"; import util from "node:util"; import { withHtmlLiveReload } from "bun-html-live-reload"; -import containsPath from "contains-path"; import readConfig from "./config"; -- cgit 1.4.1 From 14e6b232704e9358a2df181b2db0858f794f67de Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 18:09:19 +0200 Subject: Return 404 if file no longer exists --- src/index.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8ae1d0d..e524527 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ function getFilename(name: string): string { type File = { filename: string; + headers: Record; size: number; mtime: string; }; @@ -33,6 +34,10 @@ function registerFile(pathname: string, absPath: string, stat: Stats): void { } files.set(pathname, { filename: absPath, + headers: + pathname === "/404.html" + ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" }) + : defaultHeaders, size: stat.size, mtime: stat.mtime.toUTCString(), }); @@ -61,23 +66,21 @@ function walkDirectory(root: string, dir: string) { walkDirectory(publicDir, ""); +async function serveFile(file: File | undefined): Promise { + if (file && (await fsp.exists(file.filename))) { + return new Response(Bun.file(file.filename), { + headers: file.headers, + status: 200, + }); + } else { + return serveFile(files.get("/404.html")); + } +} + export default withHtmlLiveReload({ fetch: async function (request) { const pathname = new URL(request.url).pathname; const file = files.get(pathname); - if (file) { - return new Response(Bun.file(file.filename), { - headers: defaultHeaders, - status: 200, - }); - } - - return new Response(Bun.file(getFilename("404.html")), { - headers: Object.assign({}, defaultHeaders, { - "cache-control": "max-age=5, no-cache", - }), - status: 404, - statusText: "Not Found", - }); + return serveFile(file); }, }); -- cgit 1.4.1 From d4c067d0baff81a308c01ea62a900075b0326b7d Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 18:33:47 +0200 Subject: Rename variable --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index e524527..27af7a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,14 +26,14 @@ type File = { let files = new Map(); -function registerFile(pathname: string, absPath: string, stat: Stats): void { +function registerFile(pathname: string, filename: string, stat: Stats): void { pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname); if (files.get(pathname) !== undefined) { console.warn("File already registered:", pathname); } files.set(pathname, { - filename: absPath, + filename, headers: pathname === "/404.html" ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" }) -- cgit 1.4.1 From b89c78af528b400d31ce59576fc4238b902a9cfe Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 12 Sep 2023 18:41:59 +0200 Subject: Send status code 404 with /404.html --- src/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 27af7a7..030b0bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,14 +66,17 @@ function walkDirectory(root: string, dir: string) { walkDirectory(publicDir, ""); -async function serveFile(file: File | undefined): Promise { +async function serveFile( + file: File | undefined, + statusCode: number = 200, +): Promise { if (file && (await fsp.exists(file.filename))) { return new Response(Bun.file(file.filename), { headers: file.headers, - status: 200, + status: statusCode, }); } else { - return serveFile(files.get("/404.html")); + return serveFile(files.get("/404.html"), 404); } } -- cgit 1.4.1 From 7c64360212a54cf259c503929c74c6c92c4d875d Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 06:04:08 +0200 Subject: Reduce memory usage by not duplicating default headers --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 030b0bd..418af5c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ function getFilename(name: string): string { type File = { filename: string; - headers: Record; + headers?: Record; size: number; mtime: string; }; @@ -37,7 +37,7 @@ function registerFile(pathname: string, filename: string, stat: Stats): void { headers: pathname === "/404.html" ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" }) - : defaultHeaders, + : undefined, size: stat.size, mtime: stat.mtime.toUTCString(), }); @@ -72,7 +72,7 @@ async function serveFile( ): Promise { if (file && (await fsp.exists(file.filename))) { return new Response(Bun.file(file.filename), { - headers: file.headers, + headers: file.headers || defaultHeaders, status: statusCode, }); } else { -- cgit 1.4.1 From a5e05c9241c8c6e7f0a8ce54fecdbb7df3c1ecec Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 06:21:16 +0200 Subject: Remove unused function --- src/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 418af5c..f423b01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,6 @@ import path from "node:path"; import fs, { Stats } from "node:fs"; import fsp from "node:fs/promises"; -import util from "node:util"; - import { withHtmlLiveReload } from "bun-html-live-reload"; import readConfig from "./config"; @@ -13,10 +11,6 @@ const publicDir = path.resolve(base, "public") + path.sep; const config = readConfig(base); const defaultHeaders = config.extra.headers; -function getFilename(name: string): string { - return path.join(publicDir, `${name}`); -} - type File = { filename: string; headers?: Record; -- cgit 1.4.1 From 7c376d43ed17ffb44933f3ac8c1c8b8590dd675e Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 06:36:39 +0200 Subject: Send last-modified header --- src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index f423b01..8b8ab0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,7 +66,10 @@ async function serveFile( ): Promise { if (file && (await fsp.exists(file.filename))) { return new Response(Bun.file(file.filename), { - headers: file.headers || defaultHeaders, + headers: { + "last-modified": file.mtime, + ...(file.headers || defaultHeaders), + }, status: statusCode, }); } else { -- cgit 1.4.1 From c48a92fdd4d9ab9ec96c1cff73af84b9cdd1a96d Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 06:52:32 +0200 Subject: Send 304 when file time not greater than if-modified-since header --- src/index.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8b8ab0b..790c856 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ type File = { filename: string; headers?: Record; size: number; - mtime: string; + mtime: Date; }; let files = new Map(); @@ -33,7 +33,7 @@ function registerFile(pathname: string, filename: string, stat: Stats): void { ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" }) : undefined, size: stat.size, - mtime: stat.mtime.toUTCString(), + mtime: stat.mtime, }); } @@ -67,7 +67,7 @@ async function serveFile( if (file && (await fsp.exists(file.filename))) { return new Response(Bun.file(file.filename), { headers: { - "last-modified": file.mtime, + "last-modified": file.mtime.toUTCString(), ...(file.headers || defaultHeaders), }, status: statusCode, @@ -77,10 +77,22 @@ async function serveFile( } } +function parseIfModifiedSinceHeader(header: string | null): number { + return header ? new Date(header).getTime() + 999 : 0; +} + export default withHtmlLiveReload({ fetch: async function (request) { const pathname = new URL(request.url).pathname; const file = files.get(pathname); + if (file) { + const ims = parseIfModifiedSinceHeader( + request.headers.get("if-modified-since"), + ); + if (ims >= file?.mtime.getTime()) { + return new Response("", { status: 304 }); + } + } return serveFile(file); }, }); -- cgit 1.4.1 From 8e7a851d2d5372f6f36afec731bef2af5d6ee602 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 07:02:32 +0200 Subject: Inline variable --- src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 790c856..b6d0713 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,10 +86,10 @@ export default withHtmlLiveReload({ const pathname = new URL(request.url).pathname; const file = files.get(pathname); if (file) { - const ims = parseIfModifiedSinceHeader( - request.headers.get("if-modified-since"), - ); - if (ims >= file?.mtime.getTime()) { + if ( + parseIfModifiedSinceHeader(request.headers.get("if-modified-since")) >= + file?.mtime.getTime() + ) { return new Response("", { status: 304 }); } } -- cgit 1.4.1 From 37ee14f5472a9bb4688238fb89bd1d9f2658e66d Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 09:24:11 +0200 Subject: Return precompressed files, if they exist --- bun.lockb | Bin 2002 -> 3084 bytes package.json | 2 ++ src/index.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/bun.lockb b/bun.lockb index 5ccd483..2bb84c6 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --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; size: number; mtime: Date; @@ -20,7 +24,12 @@ type File = { let files = new Map(); -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 = {}, ): Promise { - 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 = {}, +): Promise { + 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); }, -- cgit 1.4.1 From c738123d55b8c9418a76afcbfd4480cc90de0e94 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 10:16:53 +0200 Subject: Use bun's builtin mime logic --- bun.lockb | Bin 3084 -> 2002 bytes package.json | 2 -- src/index.ts | 11 +++-------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/bun.lockb b/bun.lockb index 2bb84c6..5af2fa2 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 7798b26..314e9f9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "homestead", "module": "src/index.ts", "devDependencies": { - "@types/mime-types": "^2.1.1", "bun-html-live-reload": "^0.1.1", "bun-types": "latest" }, @@ -11,7 +10,6 @@ }, "type": "module", "dependencies": { - "mime-types": "^2.1.35", "toml": "^3.0.0" } } diff --git a/src/index.ts b/src/index.ts index 1616df7..4382aa4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ 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"; @@ -105,10 +104,6 @@ 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; @@ -130,7 +125,7 @@ export default withHtmlLiveReload({ ); return serveEncodedFile(files.get(file.relPath + ".br"), 200, { "content-encoding": "br", - "content-type": getMIME(file.filename), + "content-type": file.handle.type, }); } else if ( encodings.includes("zstd") && @@ -138,7 +133,7 @@ export default withHtmlLiveReload({ ) { return serveEncodedFile(files.get(file.relPath + ".zst"), 200, { "content-encoding": "zstd", - "content-type": getMIME(file.filename), + "content-type": file.handle.type, }); } else if ( encodings.includes("gzip") && @@ -146,7 +141,7 @@ export default withHtmlLiveReload({ ) { return serveEncodedFile(files.get(file.relPath + ".gz"), 200, { "content-encoding": "gzip", - "content-type": getMIME(file.filename), + "content-type": file.handle.type, }); } } -- cgit 1.4.1 From 93197c349e19f1e5b1cb5244827eab64aa4ebba4 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 10:19:40 +0200 Subject: Remove defunct live-reload Doesn't work when serving static files --- bun.lockb | Bin 2002 -> 1630 bytes package.json | 1 - src/index.ts | 7 +++---- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bun.lockb b/bun.lockb index 5af2fa2..9d90bd9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 314e9f9..354305b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "homestead", "module": "src/index.ts", "devDependencies": { - "bun-html-live-reload": "^0.1.1", "bun-types": "latest" }, "peerDependencies": { diff --git a/src/index.ts b/src/index.ts index 4382aa4..69c9b1a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,7 @@ 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 type { BunFile } from "bun-types"; +import type { BunFile, Serve } from "bun"; import readConfig from "./config"; @@ -104,7 +103,7 @@ function parseIfModifiedSinceHeader(header: string | null): number { return header ? new Date(header).getTime() + 999 : 0; } -export default withHtmlLiveReload({ +export default { fetch: async function (request) { const pathname = new URL(request.url).pathname; const file = files.get(pathname); @@ -147,4 +146,4 @@ export default withHtmlLiveReload({ } return serveFile(file); }, -}); +} satisfies Serve; -- cgit 1.4.1 From 2c41c45c48761ee8d7bb503c6966edb00b82457d Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 11:12:50 +0200 Subject: Fix incorrect file existence check --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 69c9b1a..7188e6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,7 +74,7 @@ async function serveFile( statusCode: number = 200, extraHeaders: Record = {}, ): Promise { - if (file && file.handle.exists()) { + if (file && (await file.handle.exists())) { return new Response(file.handle, { headers: { "last-modified": file.mtime.toUTCString(), -- cgit 1.4.1 From bdaab2de153d5835c34445f607a9c8d649ba08f4 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 12:19:56 +0200 Subject: Remove console.log --- src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7188e6c..60d7bb7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -118,10 +118,6 @@ export default { .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": file.handle.type, -- cgit 1.4.1 From b52a1c8420fafd52fbc115a67e56db6e6a2d3875 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 14:45:22 +0200 Subject: Add Vary: Accept-Encoding header (only header keys are re-cased) --- src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 60d7bb7..076b7ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,10 @@ const base = "../website/"; const publicDir = path.resolve(base, "public") + path.sep; const config = readConfig(base); -const defaultHeaders = config.extra.headers; +const defaultHeaders = { + ...config.extra.headers, + vary: "Accept-Encoding", +}; type File = { filename: string; -- cgit 1.4.1 From da0355160e9603f097438c446ef1f9c992758ddb Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 14:45:51 +0200 Subject: Send headers with 304 responses --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 076b7ee..db06d3b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -115,7 +115,7 @@ export default { parseIfModifiedSinceHeader(request.headers.get("if-modified-since")) >= file?.mtime.getTime() ) { - return new Response("", { status: 304 }); + return new Response("", { status: 304, headers: defaultHeaders }); } const encodings = (request.headers.get("accept-encoding") || "") .split(",") -- cgit 1.4.1 From 2574e38f6241bfa4dd5193fcb636e8b76f5c6437 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 14:46:08 +0200 Subject: Add tests --- test/index.test.ts | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/test/index.test.ts b/test/index.test.ts index c1f5f64..2f682a9 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -24,3 +24,74 @@ test("/asdf returns 404", async function () { const res = await fetch(`${base}asdf`); expect(res.status).toBe(404); }); + +test("/ returns 304 with newer if-modified-since header", async function () { + const res = await fetch(base, { + headers: { + "if-modified-since": new Date().toUTCString(), + }, + }); + expect(res.status).toBe(304); + expect(res.headers.get("vary")).toBe("Accept-Encoding"); +}); + +test("/ returns 200 with older if-modified-since header", async function () { + const res = await fetch(base, { + headers: { + "if-modified-since": new Date(0).toUTCString(), + }, + }); + expect(res.status).toBe(200); +}); + +test("/ returns gzipped content with accept-encoding: gzip", async function () { + const res = await fetch(base, { + headers: { + "accept-encoding": "gzip", + }, + }); + expect(res.status).toBe(200); + // Bun 0.8.1 this doesn't work, but `verbose` shows it's there + // expect(res.headers.get("content-encoding")).toBe("gzip"); + // response is automatically gunzipped + const body = await res.text(); + expect(body.length).toBeGreaterThan( + Number(res.headers.get("content-length")), + ); +}); + +test("/ returns uncompressed content with accept-encoding: identity", async function () { + const res = await fetch(base, { + headers: { + "accept-encoding": "identity", + }, + }); + expect(res.status).toBe(200); + const body = await res.text(); + expect(body.length).toBe(Number(res.headers.get("content-length"))); +}); + +test("/ returns brotli-compressed content with accept-encoding: br", async function () { + const res = await fetch(base, { + headers: { + "accept-encoding": "br", + }, + }); + expect(res.status).toBe(200); + expect(res.headers.get("content-encoding")).toBe("br"); + const body = await res.text(); + expect(body.length).toBeLessThan(Number(res.headers.get("content-length"))); +}); + +test("/ returns zstd-compressed content with accept-encoding: zstd", async function () { + const res = await fetch(base, { + headers: { + "accept-encoding": "zstd", + }, + }); + expect(res.status).toBe(200); + expect(res.headers.get("content-encoding")).toBe("zstd"); + expect(res.headers.get("vary")).toBe("Accept-Encoding"); + const body = await res.text(); + expect(body.length).toBeLessThan(Number(res.headers.get("content-length"))); +}); -- cgit 1.4.1 From ae9de0eaaf7f6f5aa6114671aefe297ce6e8f3f1 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 15:20:32 +0200 Subject: Remove unused import --- src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index db06d3b..ff154fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import path from "node:path"; import fs, { Stats } from "node:fs"; -import fsp from "node:fs/promises"; import type { BunFile, Serve } from "bun"; import readConfig from "./config"; -- cgit 1.4.1 From 627aec8448ca075ea4bf87f85229c67a7374eac0 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Wed, 13 Sep 2023 15:55:48 +0200 Subject: Collect metrics for prometheus --- bun.lockb | Bin 1630 -> 2683 bytes package.json | 1 + src/index.ts | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/bun.lockb b/bun.lockb index 9d90bd9..bea9f9d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 354305b..a0b6a73 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "type": "module", "dependencies": { + "bun-prometheus-client": "^0.0.2", "toml": "^3.0.0" } } diff --git a/src/index.ts b/src/index.ts index ff154fa..6699a10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import path from "node:path"; import fs, { Stats } from "node:fs"; import type { BunFile, Serve } from "bun"; +import prom from "bun-prometheus-client"; import readConfig from "./config"; @@ -22,6 +23,26 @@ type File = { mtime: Date; }; +const collectDefaultMetrics = prom.collectDefaultMetrics; +collectDefaultMetrics({ + labels: { + FLY_APP_NAME: Bun.env.FLY_APP_NAME, + FLY_ALLOC_ID: Bun.env.FLY_ALLOC_ID, + FLY_REGION: Bun.env.FLY_REGION, + }, +}); +const counters = { + requestsByStatus: new prom.Counter({ + name: "requests_by_status", + help: "Number of requests by status code", + labelNames: ["status_code"], + }), + requestsByPath: new prom.Counter({ + name: "requests_by_path", + help: "Number of requests by path", + }), +}; + let files = new Map(); function registerFile( @@ -77,6 +98,7 @@ async function serveFile( extraHeaders: Record = {}, ): Promise { if (file && (await file.handle.exists())) { + counters.requestsByStatus.inc({ status_code: statusCode }); return new Response(file.handle, { headers: { "last-modified": file.mtime.toUTCString(), @@ -86,6 +108,7 @@ async function serveFile( status: statusCode, }); } else { + counters.requestsByStatus.inc({ status_code: 404 }); // TODO return encoded return serveFile(files.get("/404.html"), 404); } @@ -105,15 +128,30 @@ function parseIfModifiedSinceHeader(header: string | null): number { return header ? new Date(header).getTime() + 999 : 0; } +Bun.serve({ + port: 9091, + fetch: async function (request) { + const pathname = new URL(request.url).pathname; + switch (pathname) { + case "/metrics": + return new Response(await prom.register.metrics()); + default: + return new Response("", { status: 404 }); + } + }, +}); + export default { fetch: async function (request) { const pathname = new URL(request.url).pathname; const file = files.get(pathname); + counters.requestsByPath.inc({ path: pathname }); if (file) { if ( parseIfModifiedSinceHeader(request.headers.get("if-modified-since")) >= file?.mtime.getTime() ) { + counters.requestsByStatus.inc({ status_code: 304 }); return new Response("", { status: 304, headers: defaultHeaders }); } const encodings = (request.headers.get("accept-encoding") || "") -- cgit 1.4.1 From 4ea39c5997bfea97b322d8b503e202db005317dd Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Thu, 14 Sep 2023 11:39:20 +0200 Subject: Fix error in requestsByPath counter --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 6699a10..69a7d86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,7 @@ const counters = { requestsByPath: new prom.Counter({ name: "requests_by_path", help: "Number of requests by path", + labelNames: ["path"], }), }; -- cgit 1.4.1 From 75041215b306fd5b7e7a86c2b0f35e5590010dea Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Thu, 14 Sep 2023 11:39:56 +0200 Subject: Log server info at startup --- src/index.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 69a7d86..2818682 100644 --- a/src/index.ts +++ b/src/index.ts @@ -129,7 +129,7 @@ function parseIfModifiedSinceHeader(header: string | null): number { return header ? new Date(header).getTime() + 999 : 0; } -Bun.serve({ +const metricsServer = Bun.serve({ port: 9091, fetch: async function (request) { const pathname = new URL(request.url).pathname; @@ -142,7 +142,11 @@ Bun.serve({ }, }); -export default { +console.info( + `Serving metrics on http://${metricsServer.hostname}:${metricsServer.port}/metrics`, +); + +const server = Bun.serve({ fetch: async function (request) { const pathname = new URL(request.url).pathname; const file = files.get(pathname); @@ -183,4 +187,6 @@ export default { } return serveFile(file); }, -} satisfies Serve; +}); + +console.info(`Serving website on http://${server.hostname}:${server.port}/`); -- cgit 1.4.1 From 02abf6ebb5ac4979537ee52ccdc93a4f29820cea Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Thu, 14 Sep 2023 11:40:17 +0200 Subject: Build with docker --- .dockerignore | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 60 +++++++++++++++++++++ package.json | 3 ++ src/index.ts | 2 +- 4 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f81d56e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,169 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..79533e9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +# syntax = docker/dockerfile:1 + +# Adjust BUN_VERSION as desired +ARG BUN_VERSION=0.8.1 +ARG ZOLA_VERSION=0.17.1 +FROM oven/bun:${BUN_VERSION} as base + +LABEL fly_launch_runtime="Bun" + +# Bun app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" + +# Throw-away build stage to reduce size of final image +FROM base as build + +# # Install packages needed to build node modules +# RUN apt-get update -qq && \ +# apt-get install -y build-essential pkg-config python-is-python3 + +# Install node modules +COPY --link bun.lockb package.json ./ +RUN bun install --ci + +# Copy application code +COPY --link src src + +FROM ghcr.io/getzola/zola:v${ZOLA_VERSION} as ssg + +WORKDIR /web + +COPY --link website ./ + +RUN [ "zola", "build", "--force" ] + +FROM alpine:edge as postprocess + +WORKDIR /web + +RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories + +RUN apk add --no-cache prettier@testing make fd brotli gzip zstd + +COPY --from=ssg /web ./ + +RUN make -j4 format compress + +# Final stage for app image +FROM base + +# Copy built application +COPY --from=build /app /app +COPY --from=postprocess /web/ /app/website + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +EXPOSE 9091 +CMD [ "bun", "run", "src/index.ts" ] diff --git a/package.json b/package.json index a0b6a73..f51b68d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "homestead", "module": "src/index.ts", + "scripts": { + "start": "bun run ." + }, "devDependencies": { "bun-types": "latest" }, diff --git a/src/index.ts b/src/index.ts index 2818682..f968fda 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import prom from "bun-prometheus-client"; import readConfig from "./config"; -const base = "../website/"; +const base = "./website/"; const publicDir = path.resolve(base, "public") + path.sep; const config = readConfig(base); -- cgit 1.4.1 From a9f9c066c69f3312946cf18834ea86bd51db23fb Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Thu, 14 Sep 2023 12:01:21 +0200 Subject: Deploy to fly.io --- fly.toml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 fly.toml diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..4316797 --- /dev/null +++ b/fly.toml @@ -0,0 +1,21 @@ +# fly.toml app configuration file generated for homestead on 2023-09-14T11:40:37+02:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = "homestead" +primary_region = "ams" + +[build] + +[metrics] + port = 9091 + path = "/metrics" + +[http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + processes = ["app"] -- cgit 1.4.1 From 7bb417f923bdaac08d0d48ef1df6d191e5030bd6 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Thu, 14 Sep 2023 12:07:05 +0200 Subject: Catch errors and log to sentry --- bun.lockb | Bin 2683 -> 7240 bytes package.json | 1 + src/index.ts | 82 +++++++++++++++++++++++++++++++++-------------------------- 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/bun.lockb b/bun.lockb index bea9f9d..2608647 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index f51b68d..acb0224 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "type": "module", "dependencies": { + "@sentry/node": "^7.69.0", "bun-prometheus-client": "^0.0.2", "toml": "^3.0.0" } diff --git a/src/index.ts b/src/index.ts index f968fda..d979f43 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,13 @@ import path from "node:path"; import fs, { Stats } from "node:fs"; import type { BunFile, Serve } from "bun"; +import * as Sentry from "@sentry/node"; import prom from "bun-prometheus-client"; import readConfig from "./config"; +Sentry.init({}); + const base = "./website/"; const publicDir = path.resolve(base, "public") + path.sep; @@ -148,44 +151,51 @@ console.info( const server = Bun.serve({ fetch: async function (request) { - const pathname = new URL(request.url).pathname; - const file = files.get(pathname); - counters.requestsByPath.inc({ path: pathname }); - if (file) { - if ( - parseIfModifiedSinceHeader(request.headers.get("if-modified-since")) >= - file?.mtime.getTime() - ) { - counters.requestsByStatus.inc({ status_code: 304 }); - return new Response("", { status: 304, headers: defaultHeaders }); - } - const encodings = (request.headers.get("accept-encoding") || "") - .split(",") - .map((x) => x.trim().toLowerCase()); - if (encodings.includes("br") && files.has(file.relPath + ".br")) { - return serveEncodedFile(files.get(file.relPath + ".br"), 200, { - "content-encoding": "br", - "content-type": file.handle.type, - }); - } else if ( - encodings.includes("zstd") && - files.has(file.relPath + ".zst") - ) { - return serveEncodedFile(files.get(file.relPath + ".zst"), 200, { - "content-encoding": "zstd", - "content-type": file.handle.type, - }); - } else if ( - encodings.includes("gzip") && - files.has(file.relPath + ".gz") - ) { - return serveEncodedFile(files.get(file.relPath + ".gz"), 200, { - "content-encoding": "gzip", - "content-type": file.handle.type, - }); + try { + const pathname = new URL(request.url).pathname; + const file = files.get(pathname); + counters.requestsByPath.inc({ path: pathname }); + if (file) { + if ( + parseIfModifiedSinceHeader( + request.headers.get("if-modified-since"), + ) >= file?.mtime.getTime() + ) { + counters.requestsByStatus.inc({ status_code: 304 }); + return new Response("", { status: 304, headers: defaultHeaders }); + } + const encodings = (request.headers.get("accept-encoding") || "") + .split(",") + .map((x) => x.trim().toLowerCase()); + if (encodings.includes("br") && files.has(file.relPath + ".br")) { + return serveEncodedFile(files.get(file.relPath + ".br"), 200, { + "content-encoding": "br", + "content-type": file.handle.type, + }); + } else if ( + encodings.includes("zstd") && + files.has(file.relPath + ".zst") + ) { + return serveEncodedFile(files.get(file.relPath + ".zst"), 200, { + "content-encoding": "zstd", + "content-type": file.handle.type, + }); + } else if ( + encodings.includes("gzip") && + files.has(file.relPath + ".gz") + ) { + return serveEncodedFile(files.get(file.relPath + ".gz"), 200, { + "content-encoding": "gzip", + "content-type": file.handle.type, + }); + } } + return serveFile(file); + } catch (error) { + counters.requestsByStatus.inc({ status_code: 503 }); + Sentry.captureException(error); + return new Response("Something went wrong", { status: 503 }); } - return serveFile(file); }, }); -- cgit 1.4.1 From 4218b3b54104cf3318943fcfb0a05b5f0b6cfe8e Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Thu, 14 Sep 2023 19:52:30 +0200 Subject: Add namespace to prometheus metrics --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index d979f43..a377985 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,12 +36,12 @@ collectDefaultMetrics({ }); const counters = { requestsByStatus: new prom.Counter({ - name: "requests_by_status", + name: "homestead_requests_by_status", help: "Number of requests by status code", labelNames: ["status_code"], }), requestsByPath: new prom.Counter({ - name: "requests_by_path", + name: "homestead_requests_by_path", help: "Number of requests by path", labelNames: ["path"], }), -- cgit 1.4.1 From e718339d93e2f60b6df55330aafb2d9536820ce4 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Fri, 15 Sep 2023 10:34:33 +0200 Subject: Add request_duration_seconds histogram --- src/index.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index a377985..ee092f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,7 @@ collectDefaultMetrics({ FLY_REGION: Bun.env.FLY_REGION, }, }); -const counters = { +const metrics = { requestsByStatus: new prom.Counter({ name: "homestead_requests_by_status", help: "Number of requests by status code", @@ -45,6 +45,11 @@ const counters = { help: "Number of requests by path", labelNames: ["path"], }), + requestDuration: new prom.Histogram({ + name: "homestead_request_duration_seconds", + help: "Request duration in seconds", + labelNames: ["path"], + }), }; let files = new Map(); @@ -102,7 +107,7 @@ async function serveFile( extraHeaders: Record = {}, ): Promise { if (file && (await file.handle.exists())) { - counters.requestsByStatus.inc({ status_code: statusCode }); + metrics.requestsByStatus.inc({ status_code: statusCode }); return new Response(file.handle, { headers: { "last-modified": file.mtime.toUTCString(), @@ -112,7 +117,7 @@ async function serveFile( status: statusCode, }); } else { - counters.requestsByStatus.inc({ status_code: 404 }); + metrics.requestsByStatus.inc({ status_code: 404 }); // TODO return encoded return serveFile(files.get("/404.html"), 404); } @@ -151,17 +156,18 @@ console.info( const server = Bun.serve({ fetch: async function (request) { + const pathname = new URL(request.url).pathname; + const endTimer = metrics.requestDuration.startTimer({ path: pathname }); try { - const pathname = new URL(request.url).pathname; const file = files.get(pathname); - counters.requestsByPath.inc({ path: pathname }); + metrics.requestsByPath.inc({ path: pathname }); if (file) { if ( parseIfModifiedSinceHeader( request.headers.get("if-modified-since"), ) >= file?.mtime.getTime() ) { - counters.requestsByStatus.inc({ status_code: 304 }); + metrics.requestsByStatus.inc({ status_code: 304 }); return new Response("", { status: 304, headers: defaultHeaders }); } const encodings = (request.headers.get("accept-encoding") || "") @@ -192,9 +198,12 @@ const server = Bun.serve({ } return serveFile(file); } catch (error) { - counters.requestsByStatus.inc({ status_code: 503 }); + metrics.requestsByStatus.inc({ status_code: 503 }); Sentry.captureException(error); return new Response("Something went wrong", { status: 503 }); + } finally { + const seconds = endTimer(); + metrics.requestDuration.observe(seconds); } }, }); -- cgit 1.4.1