Static site hosting via tangled

Compare changes

Choose any two refs to compare.

+2
package.json
···
},
"scripts": {
"start": "npx tangled-pages --config config.example.json",
+
"dev": "nodemon --watch src --watch config.example.json --exec 'node src/server.js --config config.example.json'",
+
"dev:multiple": "nodemon --watch src --watch config.multiple.example.json --exec 'node src/server.js --config config.multiple.example.json'",
"dev:worker": "wrangler dev --port 3000"
},
"dependencies": {
+26
src/helpers.js
···
+
export function joinurl(...segments) {
+
let url = segments[0];
+
for (const segment of segments.slice(1)) {
+
if (url.endsWith("/") && segment.startsWith("/")) {
+
url = url.slice(0, -1) + segment;
+
} else if (!url.endsWith("/") && !segment.startsWith("/")) {
+
url = url + "/" + segment;
+
} else {
+
url = url + segment;
+
}
+
}
+
return url;
+
}
+
+
export function extname(filename) {
+
if (!filename.includes(".")) {
+
return "";
+
}
+
return "." + filename.split(".").pop();
+
}
+
+
export function getContentTypeForFilename(filename) {
+
const extension = extname(filename).toLowerCase();
+
return getContentTypeForExtension(extension);
+
}
+
export function getContentTypeForExtension(extension, fallback = "text/plain") {
switch (extension) {
case ".html":
+4
wrangler.toml
···
main = "src/worker.js"
compatibility_flags = [ "nodejs_compat" ]
compatibility_date = "2024-09-23"
+
+
+
[observability.logs]
+
enabled = true
+6 -2
src/worker.js
···
import { Config } from "./config.js";
import configObj from "../config.worker.example.json"; // must be set at build time
+
const config = new Config(configObj);
+
if (config.cache) {
+
throw new Error("Cache is not supported in worker mode");
+
}
+
export default {
async fetch(request, env, ctx) {
-
const config = new Config(configObj);
-
const handler = await Handler.fromConfig(config);
const url = new URL(request.url);
const host = url.host;
const path = url.pathname;
+
const handler = await Handler.fromConfig(config);
const { status, content, contentType } = await handler.handleRequest({
host,
path,
+24 -4
src/knot-event-listener.js
···
import EventEmitter from "node:events";
export class KnotEventListener extends EventEmitter {
-
constructor({ knotDomain }) {
+
constructor({ knotDomain, reconnectTimeout = 10000 }) {
super();
this.knotDomain = knotDomain;
+
this.reconnectTimeout = reconnectTimeout;
+
this.connection = null;
}
async start() {
-
const ws = new WebSocket(`wss://${this.knotDomain}/events`);
-
ws.onmessage = (event) => this.handleMessage(event);
+
this.connection = new WebSocket(`wss://${this.knotDomain}/events`);
+
this.connection.onmessage = (event) => this.handleMessage(event);
+
this.connection.onerror = (event) => this.handleError(event);
+
this.connection.onclose = () => this.handleClose();
return new Promise((resolve) => {
-
ws.onopen = () => {
+
this.connection.onopen = () => {
console.log("Knot event listener connected to:", this.knotDomain);
resolve();
};
···
this.emit("refUpdate", event);
}
}
+
+
handleError(event) {
+
console.error("Knot event listener error:", event);
+
this.emit("error", event);
+
}
+
+
handleClose() {
+
console.log("Knot event listener closed");
+
this.emit("close");
+
if (this.reconnectTimeout) {
+
setTimeout(() => {
+
console.log("Knot event listener reconnecting...");
+
this.start();
+
}, this.reconnectTimeout).unref();
+
}
+
}
}
+5 -4
src/server.js
···
});
}
-
async start() {
-
this.app.listen(3000, () => {
-
console.log("Server is running on port 3000");
+
async start({ port }) {
+
this.app.listen(port, () => {
+
console.log(`Server is running on port ${port}`);
});
this.app.on("error", (error) => {
console.error("Server error:", error);
···
async function main() {
const args = yargs(process.argv.slice(2)).parse();
+
const port = args.port ?? args.p ?? 3000;
const configFilepath = args.config || "config.json";
const config = await Config.fromFile(configFilepath);
const handler = await Handler.fromConfig(config);
const server = new Server({
handler,
});
-
await server.start();
+
await server.start({ port });
}
main();
+30 -2
src/config.js
···
+
class SiteConfig {
+
constructor({
+
tangledUrl,
+
knotDomain,
+
ownerDid,
+
repoName,
+
branch,
+
baseDir,
+
notFoundFilepath,
+
}) {
+
if (tangledUrl) {
+
if ([ownerDid, repoName].some((v) => !!v)) {
+
throw new Error("Cannot use ownerDid and repoName with url");
+
}
+
}
+
this.tangledUrl = tangledUrl;
+
this.ownerDid = ownerDid;
+
this.repoName = repoName;
+
this.knotDomain = knotDomain;
+
this.branch = branch;
+
this.baseDir = baseDir;
+
this.notFoundFilepath = notFoundFilepath;
+
}
+
}
+
export class Config {
constructor({ site, sites, subdomainOffset, cache = false }) {
-
this.site = site;
-
this.sites = sites;
+
if (site && sites) {
+
throw new Error("Cannot use both site and sites in config");
+
}
+
this.site = site ? new SiteConfig(site) : null;
+
this.sites = sites ? sites.map((site) => new SiteConfig(site)) : null;
this.subdomainOffset = subdomainOffset;
this.cache = cache;
}
+2
README.md
···
When `cache: false`, the server fetches files from the repo on every request, so it might be slow.
+
This library fetches html from the repo directly, so there's no build step. As a workaround, you can add a commit hook to build your site locally and include the built files in your repo (or as a git submodule).
+
## To-do
- support `cache: true` in workers