+2
package.json
+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
+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
+4
wrangler.toml
+6
-2
src/worker.js
+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
+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
+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
+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
}