@takodotid/circuit 0.20.0 → 0.22.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/dist/index.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import {
2
- contains,
3
- familyOf,
4
- host,
5
- isAddress,
6
- isCidr,
7
- isGlobal,
8
- network
9
- } from "./chunks/index-xm9nma2t.js";
10
- import"./chunks/index-fq57hya4.js";
2
+ familyOf2,
3
+ isAddress2,
4
+ isCidr2,
5
+ network,
6
+ contains2,
7
+ host2,
8
+ isGlobal2
9
+ } from "./chunks/index-v8zgr7wm.js";
11
10
 
12
11
  // src/core/define.ts
13
12
  var secret = (name) => ({ secret: name });
@@ -19,7 +18,7 @@ var secretFile = (path) => {
19
18
  function defineDevice(device) {
20
19
  return device;
21
20
  }
22
- var defineNetwork = (network2) => network2;
21
+ var defineNetwork = (network) => network;
23
22
  // src/core/merge.ts
24
23
  function merge(...records) {
25
24
  const result = {};
@@ -33,16 +32,16 @@ function merge(...records) {
33
32
  return result;
34
33
  }
35
34
  export {
36
- secretFile,
37
- secret,
38
- network as prefixOf,
39
- merge,
40
- isGlobal,
41
- isCidr,
42
- isAddress,
43
- host,
44
- familyOf,
45
- defineNetwork,
35
+ contains2 as contains,
46
36
  defineDevice,
47
- contains
37
+ defineNetwork,
38
+ familyOf2 as familyOf,
39
+ host2 as host,
40
+ isAddress2 as isAddress,
41
+ isCidr2 as isCidr,
42
+ isGlobal2 as isGlobal,
43
+ merge,
44
+ network as prefixOf,
45
+ secret,
46
+ secretFile
48
47
  };
@@ -1,7 +1,6 @@
1
1
  import {
2
- contains
3
- } from "../chunks/index-xm9nma2t.js";
4
- import"../chunks/index-fq57hya4.js";
2
+ contains2
3
+ } from "../chunks/index-v8zgr7wm.js";
5
4
 
6
5
  // src/presets/index.ts
7
6
  var MARTIANS = ["127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/8"];
@@ -39,7 +38,7 @@ function antiAmplification(destinations, options = {}) {
39
38
  { min: port + 1, max: 1023 }
40
39
  ];
41
40
  return destinations.flatMap((dst) => [
42
- ...(options.keep_fragments ?? []).filter((prefix) => contains(dst, prefix)).map((prefix) => ({
41
+ ...(options.keep_fragments ?? []).filter((prefix) => contains2(dst, prefix)).map((prefix) => ({
43
42
  description: `UDP fragments to ${prefix} kept`,
44
43
  match: { protocol: "udp", dst: prefix, src_port: 0 },
45
44
  action: "accept"
@@ -174,10 +173,10 @@ var tunnelsOutsideOffered = (devices) => {
174
173
  continue;
175
174
  const subnets = (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
176
175
  for (const neighbor of neighborPolicies(device)) {
177
- if (!subnets.some((subnet) => contains(subnet, neighbor.address)))
176
+ if (!subnets.some((subnet) => contains2(subnet, neighbor.address)))
178
177
  continue;
179
178
  for (const prefix of offered(device, neighbor.export)) {
180
- if (contains(prefix, iface.local)) {
179
+ if (contains2(prefix, iface.local)) {
181
180
  findings.push({
182
181
  level: "error",
183
182
  device: device.name,
@@ -346,7 +345,7 @@ function internetExchange(exchange, members) {
346
345
  ]) {
347
346
  if (!address)
348
347
  continue;
349
- if (!lan || !contains(lan, address))
348
+ if (!lan || !contains2(lan, address))
350
349
  error(`${address} of ${member.name} is outside the peering LAN`);
351
350
  const holder = taken.get(address);
352
351
  if (holder)
@@ -367,19 +366,19 @@ function internetExchange(exchange, members) {
367
366
  return { ports, check };
368
367
  }
369
368
  export {
370
- vlansOf,
371
- tunnelsOutsideOffered,
372
- trustBoundary,
373
- offered,
374
- neighborPolicies,
375
- internetExchange,
376
- exportsEndInReject,
377
- communityScheme,
378
- bgpSanity,
379
- badTcpFlags,
380
- antiSpoofing,
381
- antiAmplification,
382
- PRIVATE_RANGES,
369
+ AMPLIFIERS,
383
370
  MARTIANS,
384
- AMPLIFIERS
371
+ PRIVATE_RANGES,
372
+ antiAmplification,
373
+ antiSpoofing,
374
+ badTcpFlags,
375
+ bgpSanity,
376
+ communityScheme,
377
+ exportsEndInReject,
378
+ internetExchange,
379
+ neighborPolicies,
380
+ offered,
381
+ trustBoundary,
382
+ tunnelsOutsideOffered,
383
+ vlansOf
385
384
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. Alpha.",
5
5
  "keywords": [
6
6
  "network",
@@ -86,14 +86,14 @@
86
86
  "ssh2": "^1.17.0"
87
87
  },
88
88
  "devDependencies": {
89
- "@commitlint/cli": "^19.5.0",
90
- "@commitlint/config-conventional": "^19.5.0",
89
+ "@commitlint/cli": "^19.8.1",
90
+ "@commitlint/config-conventional": "^19.8.1",
91
91
  "@types/bun": "^1.4.2",
92
- "@types/node": "^24",
93
- "husky": "^9.1.6",
94
- "prettier": "^3.9.9",
92
+ "@types/node": "^24.19.2",
93
+ "husky": "^9.1.7",
94
+ "prettier": "^3.9.10",
95
95
  "typescript": "^7.0.2",
96
96
  "vitepress": "^1.6.4"
97
97
  },
98
- "packageManager": "bun@1.3.5"
98
+ "packageManager": "bun@1.4.3"
99
99
  }
package/src/core/files.ts CHANGED
@@ -16,12 +16,14 @@ export function projectSources(root: string, directory = root): string[] {
16
16
  });
17
17
  }
18
18
 
19
- const escape = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ /** The first name written out after a call to defineDevice: the device's own, not that of another device a link, a peer or a tunnel names further on. */
20
+ const FIRST_NAME = /^[\s\S]*?\bname\s*:\s*(["'`])(.*?)\1/;
20
21
 
21
22
  /** Whether a file's text declares the device: it calls defineDevice and gives it the name, written out whole. */
22
23
  export function declaresDevice(text: string, name: string): boolean {
23
- const declares = new RegExp(`\\bname\\s*:\\s*(["'\`])${escape(name)}\\1`);
24
- return text.includes("defineDevice") && declares.test(text);
24
+ return [...text.matchAll(/\bdefineDevice\s*\(/g)].some(
25
+ (call) => FIRST_NAME.exec(text.slice(call.index + call[0].length))?.[2] === name
26
+ );
25
27
  }
26
28
 
27
29
  /** The project files that declare the device, relative to the root. */
@@ -2,7 +2,7 @@
2
2
  /// <reference lib="dom" />
3
3
  // Studio's page: loads what the server gathered from the config, shows the place the address names, and loads again whenever the server says the config changed.
4
4
 
5
- import { byId, go } from "./dom.js";
5
+ import { byId, enter, go } from "./dom.js";
6
6
  import { currentLayer, isLayer, setGraph, showLayer } from "./graph.js";
7
7
  import { problemCount, showTables } from "./tables.js";
8
8
  import { showTools } from "./tools.js";
@@ -42,12 +42,21 @@ function showSummary() {
42
42
  const count = problemCount(studio);
43
43
  problems.textContent = count ? String(count) : "";
44
44
 
45
- summary.textContent = [
46
- `${devices} ${devices === 1 ? "device" : "devices"}`,
47
- `${errors} ${errors === 1 ? "error" : "errors"}`,
48
- `${warnings} ${warnings === 1 ? "warning" : "warnings"}`,
49
- `${drifted} unlike the snapshot`,
50
- ].join(" · ");
45
+ const items = [
46
+ { value: devices, label: devices === 1 ? "device" : "devices", state: "neutral" },
47
+ { value: errors, label: errors === 1 ? "error" : "errors", state: errors ? "error" : "quiet" },
48
+ { value: warnings, label: warnings === 1 ? "warning" : "warnings", state: warnings ? "warning" : "quiet" },
49
+ { value: drifted, label: "unlike the snapshot", state: drifted ? "changed" : "quiet" },
50
+ ];
51
+
52
+ summary.replaceChildren(
53
+ ...items.map(({ value, label, state }) => {
54
+ const item = document.createElement("span");
55
+ item.className = `summary-item ${state}`;
56
+ item.append(String(value), ` ${label}`);
57
+ return item;
58
+ })
59
+ );
51
60
  }
52
61
 
53
62
  /** Show the place the address names, such as `#graph/bgp` or `#devices/edge-01/ports`. */
@@ -90,10 +99,22 @@ function route() {
90
99
  else if (view === "compare") showCompare(parts, tables, studio);
91
100
  else showTables(parts, tables, studio);
92
101
  tables.scrollTop = moved ? 0 : scrolled;
102
+
103
+ // A new place comes in; the same place drawn again, because the config changed, stays still.
104
+ if (moved) enter(tables);
93
105
  }
94
106
 
95
107
  window.addEventListener("hashchange", route);
96
108
 
109
+ // A menu closes once one of its items is chosen, or when the reader clicks anywhere else.
110
+ document.addEventListener("click", (event) => {
111
+ const target = event.target instanceof Element ? event.target : undefined;
112
+
113
+ for (const menu of document.querySelectorAll("details.menu[open]")) {
114
+ if (!target || !menu.contains(target) || target.closest(".menu-items button")) menu.removeAttribute("open");
115
+ }
116
+ });
117
+
97
118
  /** @type {ReturnType<typeof setTimeout> | undefined} */
98
119
  let searching;
99
120
 
@@ -141,7 +162,14 @@ events.addEventListener("error", () => {
141
162
  live.classList.remove("on");
142
163
  });
143
164
 
144
- // The config changed: load it again. The page itself changed, while working on Circuit: load the page again.
145
- events.addEventListener("message", (event) => (event.data === "page" ? location.reload() : void load()));
165
+ // The config changed: load it again, and the live mark beats once to say so. The page itself changed, while working on Circuit: load the page again.
166
+ events.addEventListener("message", (event) => {
167
+ if (event.data === "page") return location.reload();
168
+
169
+ live.classList.remove("beat");
170
+ void live.offsetWidth;
171
+ live.classList.add("beat");
172
+ void load();
173
+ });
146
174
 
147
175
  void load();
@@ -71,6 +71,26 @@ export function svgById(id) {
71
71
  return element;
72
72
  }
73
73
 
74
+ /** The reader's system asks for less motion: nothing moves for show, and changes happen at once. */
75
+ export const still = matchMedia("(prefers-reduced-motion: reduce)");
76
+
77
+ /**
78
+ * Bring a part of the page in as it changes: it fades in and rises a little.
79
+ *
80
+ * @param {Element} element
81
+ */
82
+ export function enter(element) {
83
+ if (still.matches) return;
84
+
85
+ element.animate(
86
+ [
87
+ { opacity: 0, transform: "translateY(6px)" },
88
+ { opacity: 1, transform: "none" },
89
+ ],
90
+ { duration: 240, easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
91
+ );
92
+ }
93
+
74
94
  /** Browser storage can be missing or refuse, in a private window for example. The page works without it. */
75
95
  export const store = {
76
96
  /**
@@ -64,15 +64,21 @@ export function svgOf(svg, viewport) {
64
64
  const originals = [svg, ...svg.querySelectorAll("*")];
65
65
  const copies = [copy, ...copy.querySelectorAll("*")];
66
66
 
67
+ // The dots travelling the cables are left out: a still image would show each one stopped at the drawing's corner.
68
+ const moving = [];
69
+
67
70
  for (const [index, original] of originals.entries()) {
68
71
  const target = copies[index];
69
72
  if (!(target instanceof SVGElement)) continue;
73
+ if (original.classList.contains("signal")) moving.push(target);
70
74
 
71
75
  const look = getComputedStyle(original);
72
76
  target.setAttribute("style", DRAWN.map((name) => `${name}:${look.getPropertyValue(name)}`).join(";"));
73
77
  target.removeAttribute("class");
74
78
  }
75
79
 
80
+ for (const dot of moving) dot.remove();
81
+
76
82
  // The drawing's own place on screen is left out: the file is framed around what is drawn.
77
83
  copy.querySelector("#viewport")?.removeAttribute("transform");
78
84
  copy.setAttribute("xmlns", "http://www.w3.org/2000/svg");
@@ -1,6 +1,12 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
- <rect width="32" height="32" rx="7" fill="#b4492f" />
3
- <path d="M22 9 H13 L9 13 V19 L13 23 H22" fill="none" stroke="#fdf8f4" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" />
4
- <circle cx="22" cy="9" r="2.6" fill="#b4492f" stroke="#fdf8f4" stroke-width="2.2" />
5
- <circle cx="22" cy="23" r="2.6" fill="#b4492f" stroke="#fdf8f4" stroke-width="2.2" />
2
+ <rect width="32" height="32" rx="6" fill="#f8e347" />
3
+ <rect x="4" y="8" width="24" height="16" rx="3" fill="#0a47b8" />
4
+ <g fill="#ffffff">
5
+ <rect x="7.5" y="11.5" width="4" height="3.5" rx="0.6" />
6
+ <rect x="14" y="11.5" width="4" height="3.5" rx="0.6" />
7
+ <rect x="20.5" y="11.5" width="4" height="3.5" rx="0.6" />
8
+ <rect x="7.5" y="17" width="4" height="3.5" rx="0.6" />
9
+ <rect x="14" y="17" width="4" height="3.5" rx="0.6" />
10
+ </g>
11
+ <rect x="20.5" y="17" width="4" height="3.5" rx="0.6" fill="#f8e347" />
6
12
  </svg>