@takodotid/circuit 0.34.2 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11347 -9018
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +17 -11
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -25
- package/src/adapters/devices/raisecom-ros/index.ts +0 -693
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
package/src/studio/server.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { execFile, spawn } from "node:child_process";
|
|
|
4
4
|
import { existsSync, readFileSync, watch } from "node:fs";
|
|
5
5
|
import { createServer, type ServerResponse } from "node:http";
|
|
6
6
|
import { extname, join } from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { StudioModel, type StudioData } from "./data";
|
|
8
|
+
import { Git } from "./git";
|
|
9
9
|
|
|
10
10
|
export type StudioOptions = {
|
|
11
11
|
/** The config file. */
|
|
@@ -21,207 +21,213 @@ export type StudioOptions = {
|
|
|
21
21
|
allowHosts: string[];
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Whether a request's Host header names Studio, so a web page elsewhere cannot read the network through a name it points here.
|
|
29
|
-
*
|
|
30
|
-
* Studio answers to this machine's own names at its port, and to each name it was told to, with or without a port.
|
|
31
|
-
*/
|
|
32
|
-
export function hostAllowed(header: string | undefined, port: number, allowHosts: readonly string[]): boolean {
|
|
33
|
-
if (!header) return false;
|
|
34
|
-
if (header === `127.0.0.1:${port}` || header === `localhost:${port}` || header === `[::1]:${port}`) return true;
|
|
35
|
-
|
|
36
|
-
const name = header.replace(/:\d+$/, "").toLowerCase();
|
|
37
|
-
return allowHosts.some((allowed) => {
|
|
38
|
-
const wanted = allowed.toLowerCase();
|
|
39
|
-
return wanted === header.toLowerCase() || wanted === name;
|
|
40
|
-
});
|
|
41
|
-
}
|
|
24
|
+
export class StudioServer {
|
|
25
|
+
/** Directories whose changes never change what Studio shows. */
|
|
26
|
+
private static readonly IGNORED = /(^|[\\/])(node_modules|\.git|\.circuit[\\/]cache)([\\/]|$)/;
|
|
42
27
|
|
|
43
|
-
|
|
44
|
-
|
|
28
|
+
private static readonly CONTENT_TYPES: Record<string, string> = {
|
|
29
|
+
".html": "text/html; charset=utf-8",
|
|
30
|
+
".css": "text/css; charset=utf-8",
|
|
31
|
+
".js": "text/javascript; charset=utf-8",
|
|
32
|
+
".svg": "image/svg+xml",
|
|
33
|
+
".woff2": "font/woff2",
|
|
34
|
+
};
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
/** Whether an address to listen on is this machine's own, which nothing else on the network can reach. */
|
|
37
|
+
static isLoopback(host: string): boolean {
|
|
38
|
+
return host === "localhost" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether a request's Host header names Studio, so a web page elsewhere cannot read the network through a name it points here.
|
|
43
|
+
*
|
|
44
|
+
* Studio answers to this machine's own names at its port, and to each name it was told to, with or without a port.
|
|
45
|
+
*/
|
|
46
|
+
static hostAllowed(header: string | undefined, port: number, allowHosts: readonly string[]): boolean {
|
|
47
|
+
if (!header) return false;
|
|
48
|
+
if (header === `127.0.0.1:${port}` || header === `localhost:${port}` || header === `[::1]:${port}`) return true;
|
|
49
|
+
|
|
50
|
+
const name = header.replace(/:\d+$/, "").toLowerCase();
|
|
51
|
+
return allowHosts.some((allowed) => {
|
|
52
|
+
const wanted = allowed.toLowerCase();
|
|
53
|
+
return wanted === header.toLowerCase() || wanted === name;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
53
56
|
|
|
54
|
-
/** What Studio shows, as JSON, from `circuit studio --json` run as a new process, or what stopped it. */
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
/** What Studio shows, as JSON, from `circuit studio --json` run as a new process, or what stopped it. */
|
|
58
|
+
private static gather(configPath: string): Promise<string> {
|
|
59
|
+
const cli = process.argv[1]!;
|
|
60
|
+
const args = [...process.execArgv, cli, "studio", "--json", `--config=${configPath}`];
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
return new Promise((done) => {
|
|
63
|
+
execFile(process.execPath, args, { maxBuffer: 256 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
64
|
+
if (!error) return done(stdout);
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
const message = (stderr || error.message).trim();
|
|
67
|
+
done(JSON.stringify({ error: message }));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Open the address in the browser. A machine without one only misses the convenience. */
|
|
73
|
+
private static openBrowser(url: string): void {
|
|
74
|
+
const [command, args] =
|
|
75
|
+
process.platform === "win32"
|
|
76
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
77
|
+
: process.platform === "darwin"
|
|
78
|
+
? ["open", [url]]
|
|
79
|
+
: ["xdg-open", [url]];
|
|
80
|
+
|
|
81
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
82
|
+
child.on("error", () => {});
|
|
83
|
+
child.unref();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Serve Studio until the process stops, and gather what it shows again whenever a file beside the config changes. */
|
|
87
|
+
static async start(options: StudioOptions): Promise<void> {
|
|
88
|
+
const { configPath, root, port } = options;
|
|
89
|
+
|
|
90
|
+
// The page's files ship in the package's sources, which Bun runs as they are and Node's build leaves in place.
|
|
91
|
+
const installed = StudioModel.packageRoot();
|
|
92
|
+
if (!installed) throw new Error("cannot find where Circuit is installed, so Studio has no page to serve");
|
|
93
|
+
const pages = join(installed, "src", "studio", "public");
|
|
94
|
+
|
|
95
|
+
let data = await StudioServer.gather(configPath);
|
|
96
|
+
|
|
97
|
+
// Another commit's network is read once: a commit does not change. Each is kept as it is being read, so two asks share one read.
|
|
98
|
+
const past = new Map<string, Promise<string>>();
|
|
99
|
+
|
|
100
|
+
/** Answer with JSON, or with an error the page shows. */
|
|
101
|
+
const answer = async (response: ServerResponse, work: () => Promise<unknown>) => {
|
|
102
|
+
let body: string;
|
|
103
|
+
try {
|
|
104
|
+
const result = await work();
|
|
105
|
+
body = typeof result === "string" ? result : JSON.stringify(result);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
body = JSON.stringify({ error: (error as Error).message });
|
|
108
|
+
}
|
|
109
|
+
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(body);
|
|
110
|
+
};
|
|
111
|
+
const listeners = new Set<ServerResponse>();
|
|
112
|
+
const tell = (message: string) => {
|
|
113
|
+
for (const listener of listeners) listener.write(`data: ${message}\n\n`);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const server = createServer((request, response) => {
|
|
117
|
+
// Studio shows one team's network: every answer asks search engines to leave it out, and robots.txt says so for the whole site.
|
|
118
|
+
response.setHeader("x-robots-tag", "noindex, nofollow, noarchive");
|
|
119
|
+
|
|
120
|
+
if (!StudioServer.hostAllowed(request.headers.host, port, options.allowHosts)) {
|
|
121
|
+
response.writeHead(403).end();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const url = new URL(request.url ?? "/", "http://studio");
|
|
126
|
+
const path = url.pathname;
|
|
127
|
+
|
|
128
|
+
// The commits that changed a device: its file and its snapshot.
|
|
129
|
+
if (path === "/history") {
|
|
130
|
+
void answer(response, async () => {
|
|
131
|
+
const name = url.searchParams.get("device") ?? "";
|
|
132
|
+
const files = (JSON.parse(data) as StudioData).files?.[name];
|
|
133
|
+
if (!files) throw new Error(`no device ${name}`);
|
|
134
|
+
return Git.history(
|
|
135
|
+
root,
|
|
136
|
+
[files.source, files.snapshot].filter((file): file is string => Boolean(file))
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// What Studio can compare with: branches, tags and the latest commits.
|
|
143
|
+
if (path === "/refs") {
|
|
144
|
+
void answer(response, () => Git.references(root));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The network as it was at a branch, tag or commit.
|
|
149
|
+
if (path === "/compare") {
|
|
150
|
+
void answer(response, async () => {
|
|
151
|
+
const asked = url.searchParams.get("ref") ?? "";
|
|
152
|
+
const commit = await Git.commitOf(root, asked);
|
|
153
|
+
if (!commit) throw new Error(`${asked} names no commit here`);
|
|
154
|
+
|
|
155
|
+
if (!past.has(commit.commit))
|
|
156
|
+
past.set(commit.commit, Git.networkAt(root, configPath, commit.commit, StudioServer.gather));
|
|
157
|
+
const network = await past.get(commit.commit)!;
|
|
158
|
+
if (network.startsWith('{"error"')) past.delete(commit.commit);
|
|
159
|
+
|
|
160
|
+
return `{"commit":${JSON.stringify(commit)},"network":${network}}`;
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (path === "/robots.txt") {
|
|
166
|
+
response.writeHead(200, { "content-type": "text/plain; charset=utf-8" }).end("User-agent: *\nDisallow: /\n");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (path === "/data") {
|
|
171
|
+
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(data);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Changes are told as they happen; a proxy such as nginx is asked not to hold them back.
|
|
176
|
+
if (path === "/events") {
|
|
177
|
+
response.writeHead(200, {
|
|
178
|
+
"content-type": "text/event-stream",
|
|
179
|
+
"cache-control": "no-store",
|
|
180
|
+
connection: "keep-alive",
|
|
181
|
+
"x-accel-buffering": "no",
|
|
182
|
+
});
|
|
183
|
+
response.write(": connected\n\n");
|
|
184
|
+
|
|
185
|
+
listeners.add(response);
|
|
186
|
+
request.on("close", () => listeners.delete(response));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Only a file directly in public/, by a plain name, is served: nothing above it can be reached.
|
|
191
|
+
const name = path === "/" ? "index.html" : path.slice(1);
|
|
192
|
+
const type = StudioServer.CONTENT_TYPES[extname(name)];
|
|
193
|
+
const file = join(pages, name);
|
|
194
|
+
|
|
195
|
+
if (!/^[a-z-]+\.[a-z0-9]+$/.test(name) || !type || !existsSync(file)) {
|
|
196
|
+
response.writeHead(404).end();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
response.writeHead(200, { "content-type": type, "cache-control": "no-store" }).end(readFileSync(file));
|
|
65
201
|
});
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
202
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const [command, args] =
|
|
72
|
-
process.platform === "win32"
|
|
73
|
-
? ["cmd", ["/c", "start", "", url]]
|
|
74
|
-
: process.platform === "darwin"
|
|
75
|
-
? ["open", [url]]
|
|
76
|
-
: ["xdg-open", [url]];
|
|
77
|
-
|
|
78
|
-
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
79
|
-
child.on("error", () => {});
|
|
80
|
-
child.unref();
|
|
81
|
-
}
|
|
203
|
+
// Several files often change at once, such as on a save or a checkout: the config is read again once they settle.
|
|
204
|
+
let timer: NodeJS.Timeout | undefined;
|
|
82
205
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// The page's files ship in the package's sources, which Bun runs as they are and Node's build leaves in place.
|
|
87
|
-
const installed = packageRoot();
|
|
88
|
-
if (!installed) throw new Error("cannot find where Circuit is installed, so Studio has no page to serve");
|
|
89
|
-
const pages = join(installed, "src", "studio", "public");
|
|
90
|
-
|
|
91
|
-
let data = await gather(configPath);
|
|
92
|
-
|
|
93
|
-
// Another commit's network is read once: a commit does not change. Each is kept as it is being read, so two asks share one read.
|
|
94
|
-
const past = new Map<string, Promise<string>>();
|
|
95
|
-
|
|
96
|
-
/** Answer with JSON, or with an error the page shows. */
|
|
97
|
-
const answer = async (response: ServerResponse, work: () => Promise<unknown>) => {
|
|
98
|
-
let body: string;
|
|
99
|
-
try {
|
|
100
|
-
const result = await work();
|
|
101
|
-
body = typeof result === "string" ? result : JSON.stringify(result);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
body = JSON.stringify({ error: (error as Error).message });
|
|
104
|
-
}
|
|
105
|
-
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(body);
|
|
106
|
-
};
|
|
107
|
-
const listeners = new Set<ServerResponse>();
|
|
108
|
-
const tell = (message: string) => {
|
|
109
|
-
for (const listener of listeners) listener.write(`data: ${message}\n\n`);
|
|
110
|
-
};
|
|
206
|
+
watch(root, { recursive: true }, (_event, filename) => {
|
|
207
|
+
if (filename && StudioServer.IGNORED.test(filename)) return;
|
|
111
208
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// What Studio can compare with: branches, tags and the latest commits.
|
|
139
|
-
if (path === "/refs") {
|
|
140
|
-
void answer(response, () => references(root));
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// The network as it was at a branch, tag or commit.
|
|
145
|
-
if (path === "/compare") {
|
|
146
|
-
void answer(response, async () => {
|
|
147
|
-
const asked = url.searchParams.get("ref") ?? "";
|
|
148
|
-
const commit = await commitOf(root, asked);
|
|
149
|
-
if (!commit) throw new Error(`${asked} names no commit here`);
|
|
150
|
-
|
|
151
|
-
if (!past.has(commit.commit)) past.set(commit.commit, networkAt(root, configPath, commit.commit, gather));
|
|
152
|
-
const network = await past.get(commit.commit)!;
|
|
153
|
-
if (network.startsWith('{"error"')) past.delete(commit.commit);
|
|
154
|
-
|
|
155
|
-
return `{"commit":${JSON.stringify(commit)},"network":${network}}`;
|
|
156
|
-
});
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (path === "/robots.txt") {
|
|
161
|
-
response.writeHead(200, { "content-type": "text/plain; charset=utf-8" }).end("User-agent: *\nDisallow: /\n");
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (path === "/data") {
|
|
166
|
-
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(data);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Changes are told as they happen; a proxy such as nginx is asked not to hold them back.
|
|
171
|
-
if (path === "/events") {
|
|
172
|
-
response.writeHead(200, {
|
|
173
|
-
"content-type": "text/event-stream",
|
|
174
|
-
"cache-control": "no-store",
|
|
175
|
-
connection: "keep-alive",
|
|
176
|
-
"x-accel-buffering": "no",
|
|
177
|
-
});
|
|
178
|
-
response.write(": connected\n\n");
|
|
179
|
-
|
|
180
|
-
listeners.add(response);
|
|
181
|
-
request.on("close", () => listeners.delete(response));
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Only a file directly in public/, by a plain name, is served: nothing above it can be reached.
|
|
186
|
-
const name = path === "/" ? "index.html" : path.slice(1);
|
|
187
|
-
const type = CONTENT_TYPES[extname(name)];
|
|
188
|
-
const file = join(pages, name);
|
|
189
|
-
|
|
190
|
-
if (!/^[a-z-]+\.[a-z0-9]+$/.test(name) || !type || !existsSync(file)) {
|
|
191
|
-
response.writeHead(404).end();
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
response.writeHead(200, { "content-type": type, "cache-control": "no-store" }).end(readFileSync(file));
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// Several files often change at once, such as on a save or a checkout: the config is read again once they settle.
|
|
199
|
-
let timer: NodeJS.Timeout | undefined;
|
|
200
|
-
|
|
201
|
-
watch(root, { recursive: true }, (_event, filename) => {
|
|
202
|
-
if (filename && IGNORED.test(filename)) return;
|
|
203
|
-
|
|
204
|
-
clearTimeout(timer);
|
|
205
|
-
timer = setTimeout(async () => {
|
|
206
|
-
data = await gather(configPath);
|
|
207
|
-
tell("changed");
|
|
208
|
-
console.log(`${new Date().toLocaleTimeString()} ${filename ?? "a file"} changed, read again`);
|
|
209
|
-
}, 300);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// A change to the page's own files, while working on Circuit, loads the page again.
|
|
213
|
-
watch(pages, () => tell("page"));
|
|
214
|
-
|
|
215
|
-
server.on("error", (error: NodeJS.ErrnoException) => {
|
|
216
|
-
const message = error.code === "EADDRINUSE" ? `port ${port} is in use; choose another with --port=N` : error.message;
|
|
217
|
-
console.error(message);
|
|
218
|
-
process.exit(1);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
server.listen(port, options.host, () => {
|
|
222
|
-
const local = isLoopback(options.host);
|
|
223
|
-
const url = local ? `http://127.0.0.1:${port}` : `http://${options.allowHosts[0] ?? options.host}`;
|
|
224
|
-
console.log(`Circuit Studio at ${url}, listening on ${options.host}:${port}, watching ${root}. Stop it with Ctrl+C.`);
|
|
225
|
-
if (options.open && local) openBrowser(url);
|
|
226
|
-
});
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
timer = setTimeout(async () => {
|
|
211
|
+
data = await StudioServer.gather(configPath);
|
|
212
|
+
tell("changed");
|
|
213
|
+
console.log(`${new Date().toLocaleTimeString()} ${filename ?? "a file"} changed, read again`);
|
|
214
|
+
}, 300);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// A change to the page's own files, while working on Circuit, loads the page again.
|
|
218
|
+
watch(pages, () => tell("page"));
|
|
219
|
+
|
|
220
|
+
server.on("error", (error: NodeJS.ErrnoException) => {
|
|
221
|
+
const message = error.code === "EADDRINUSE" ? `port ${port} is in use; choose another with --port=N` : error.message;
|
|
222
|
+
console.error(message);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
server.listen(port, options.host, () => {
|
|
227
|
+
const local = StudioServer.isLoopback(options.host);
|
|
228
|
+
const url = local ? `http://127.0.0.1:${port}` : `http://${options.allowHosts[0] ?? options.host}`;
|
|
229
|
+
console.log(`Circuit Studio at ${url}, listening on ${options.host}:${port}, watching ${root}. Stop it with Ctrl+C.`);
|
|
230
|
+
if (options.open && local) StudioServer.openBrowser(url);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
227
233
|
}
|