Static site hosting via tangled

Compare changes

Choose any two refs to compare.

+2
package.json
··· 7 7 }, 8 8 "scripts": { 9 9 "start": "npx tangled-pages --config config.example.json", 10 + "dev": "nodemon --watch src --watch config.example.json --exec 'node src/server.js --config config.example.json'", 11 + "dev:multiple": "nodemon --watch src --watch config.multiple.example.json --exec 'node src/server.js --config config.multiple.example.json'", 10 12 "dev:worker": "wrangler dev --port 3000" 11 13 }, 12 14 "dependencies": {
+26
src/helpers.js
··· 1 + export function joinurl(...segments) { 2 + let url = segments[0]; 3 + for (const segment of segments.slice(1)) { 4 + if (url.endsWith("/") && segment.startsWith("/")) { 5 + url = url.slice(0, -1) + segment; 6 + } else if (!url.endsWith("/") && !segment.startsWith("/")) { 7 + url = url + "/" + segment; 8 + } else { 9 + url = url + segment; 10 + } 11 + } 12 + return url; 13 + } 14 + 15 + export function extname(filename) { 16 + if (!filename.includes(".")) { 17 + return ""; 18 + } 19 + return "." + filename.split(".").pop(); 20 + } 21 + 22 + export function getContentTypeForFilename(filename) { 23 + const extension = extname(filename).toLowerCase(); 24 + return getContentTypeForExtension(extension); 25 + } 26 + 1 27 export function getContentTypeForExtension(extension, fallback = "text/plain") { 2 28 switch (extension) { 3 29 case ".html":
+4
wrangler.toml
··· 2 2 main = "src/worker.js" 3 3 compatibility_flags = [ "nodejs_compat" ] 4 4 compatibility_date = "2024-09-23" 5 + 6 + 7 + [observability.logs] 8 + enabled = true
+6 -2
src/worker.js
··· 2 2 import { Config } from "./config.js"; 3 3 import configObj from "../config.worker.example.json"; // must be set at build time 4 4 5 + const config = new Config(configObj); 6 + if (config.cache) { 7 + throw new Error("Cache is not supported in worker mode"); 8 + } 9 + 5 10 export default { 6 11 async fetch(request, env, ctx) { 7 - const config = new Config(configObj); 8 - const handler = await Handler.fromConfig(config); 9 12 const url = new URL(request.url); 10 13 const host = url.host; 11 14 const path = url.pathname; 15 + const handler = await Handler.fromConfig(config); 12 16 const { status, content, contentType } = await handler.handleRequest({ 13 17 host, 14 18 path,
+24 -4
src/knot-event-listener.js
··· 1 1 import EventEmitter from "node:events"; 2 2 3 3 export class KnotEventListener extends EventEmitter { 4 - constructor({ knotDomain }) { 4 + constructor({ knotDomain, reconnectTimeout = 10000 }) { 5 5 super(); 6 6 this.knotDomain = knotDomain; 7 + this.reconnectTimeout = reconnectTimeout; 8 + this.connection = null; 7 9 } 8 10 9 11 async start() { 10 - const ws = new WebSocket(`wss://${this.knotDomain}/events`); 11 - ws.onmessage = (event) => this.handleMessage(event); 12 + this.connection = new WebSocket(`wss://${this.knotDomain}/events`); 13 + this.connection.onmessage = (event) => this.handleMessage(event); 14 + this.connection.onerror = (event) => this.handleError(event); 15 + this.connection.onclose = () => this.handleClose(); 12 16 return new Promise((resolve) => { 13 - ws.onopen = () => { 17 + this.connection.onopen = () => { 14 18 console.log("Knot event listener connected to:", this.knotDomain); 15 19 resolve(); 16 20 }; ··· 29 33 this.emit("refUpdate", event); 30 34 } 31 35 } 36 + 37 + handleError(event) { 38 + console.error("Knot event listener error:", event); 39 + this.emit("error", event); 40 + } 41 + 42 + handleClose() { 43 + console.log("Knot event listener closed"); 44 + this.emit("close"); 45 + if (this.reconnectTimeout) { 46 + setTimeout(() => { 47 + console.log("Knot event listener reconnecting..."); 48 + this.start(); 49 + }, this.reconnectTimeout).unref(); 50 + } 51 + } 32 52 }
+5 -4
src/server.js
··· 22 22 }); 23 23 } 24 24 25 - async start() { 26 - this.app.listen(3000, () => { 27 - console.log("Server is running on port 3000"); 25 + async start({ port }) { 26 + this.app.listen(port, () => { 27 + console.log(`Server is running on port ${port}`); 28 28 }); 29 29 this.app.on("error", (error) => { 30 30 console.error("Server error:", error); ··· 34 34 35 35 async function main() { 36 36 const args = yargs(process.argv.slice(2)).parse(); 37 + const port = args.port ?? args.p ?? 3000; 37 38 const configFilepath = args.config || "config.json"; 38 39 const config = await Config.fromFile(configFilepath); 39 40 const handler = await Handler.fromConfig(config); 40 41 const server = new Server({ 41 42 handler, 42 43 }); 43 - await server.start(); 44 + await server.start({ port }); 44 45 } 45 46 46 47 main();
+30 -2
src/config.js
··· 1 + class SiteConfig { 2 + constructor({ 3 + tangledUrl, 4 + knotDomain, 5 + ownerDid, 6 + repoName, 7 + branch, 8 + baseDir, 9 + notFoundFilepath, 10 + }) { 11 + if (tangledUrl) { 12 + if ([ownerDid, repoName].some((v) => !!v)) { 13 + throw new Error("Cannot use ownerDid and repoName with url"); 14 + } 15 + } 16 + this.tangledUrl = tangledUrl; 17 + this.ownerDid = ownerDid; 18 + this.repoName = repoName; 19 + this.knotDomain = knotDomain; 20 + this.branch = branch; 21 + this.baseDir = baseDir; 22 + this.notFoundFilepath = notFoundFilepath; 23 + } 24 + } 25 + 1 26 export class Config { 2 27 constructor({ site, sites, subdomainOffset, cache = false }) { 3 - this.site = site; 4 - this.sites = sites; 28 + if (site && sites) { 29 + throw new Error("Cannot use both site and sites in config"); 30 + } 31 + this.site = site ? new SiteConfig(site) : null; 32 + this.sites = sites ? sites.map((site) => new SiteConfig(site)) : null; 5 33 this.subdomainOffset = subdomainOffset; 6 34 this.cache = cache; 7 35 }