drawio-mcp-server 2.1.1 → 2.3.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 +21 -68
- package/build/assets/auto-refresh.js +21 -0
- package/build/assets/auto-refresh.test.js +54 -0
- package/build/assets/downloader.js +4 -0
- package/build/assets/version.js +38 -0
- package/build/assets/version.test.js +26 -0
- package/build/documents-changed-broadcast.test.js +123 -0
- package/build/drawio-compat/log-report.js +21 -0
- package/build/drawio-compat/log-report.test.js +43 -0
- package/build/drawio-compat/matrix.js +14 -0
- package/build/index.js +152 -11
- package/build/install/config-io.js +21 -0
- package/build/install/config-io.test.js +37 -0
- package/build/install/hosts/claude-code.js +37 -0
- package/build/install/hosts/claude-code.test.js +47 -0
- package/build/install/hosts/claude-desktop.js +45 -0
- package/build/install/hosts/claude-desktop.test.js +57 -0
- package/build/install/hosts/codex.js +171 -0
- package/build/install/hosts/codex.test.js +124 -0
- package/build/install/hosts/index.js +15 -0
- package/build/install/hosts/opencode.js +48 -0
- package/build/install/hosts/opencode.test.js +60 -0
- package/build/install/hosts/zed.js +37 -0
- package/build/install/hosts/zed.test.js +47 -0
- package/build/install/index.js +170 -0
- package/build/install/index.test.js +115 -0
- package/build/install/install.integration.test.js +103 -0
- package/build/install/types.js +1 -0
- package/build/multi-transport.test.js +1 -0
- package/build/plugin/mcp-plugin.js +406 -89
- package/build/real-environment/import-export.test.js +107 -0
- package/build/stdio-shutdown.test.js +177 -0
- package/build/tool-registry.test.js +57 -0
- package/build/tools/index.js +4 -0
- package/build/tools/save-document.js +5 -0
- package/build/tools/set-document-title.js +12 -0
- package/build/vendored/compat/index.js +35 -0
- package/package.json +15 -10
package/README.md
CHANGED
|
@@ -9,12 +9,16 @@ Let's do some Vibe Diagramming with the most wide-spread diagramming tool called
|
|
|
9
9
|
|
|
10
10
|
## Key Highlights
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
12
|
+
- Claude Code plugin: `/plugin marketplace add lgazo/drawio-mcp-server` then `/plugin install drawio`, with `/drawio-open` and `/drawio-status` slash commands 
|
|
13
|
+
- `npx drawio-mcp-server install <host>` writes the MCP config for `claude-code`, `claude-desktop`, `codex`, `zed`, `opencode`, or `all` 
|
|
14
|
+
- Document persistence tools: `set-document-title` and `save-document` 
|
|
15
|
+
- Draw.io library API version compatibility mechanism 
|
|
16
|
+
- Enable Draw.io MCP in IFrames
|
|
17
|
+
- AWS, GCP, Azure, Cisco19, and CiscoSafe stencils auto-discovered at runtime from drawio's sidebar
|
|
18
|
+
- Multi-document targeting with `list-documents` and `target_document` selectors for multi-tab workflows
|
|
19
|
+
- Multi-page targeting with required `target_page` selectors for page-scoped tools
|
|
20
|
+
- Per-document FIFO serialization for live operations, so multiple agents can work on different files safely
|
|
21
|
+
- Page management tools: `list-pages`, `get-current-page`, `create-page`, `copy-page`, `rename-page`
|
|
18
22
|
- Import, embed, or expand [Mermaid](https://mermaid.js.org/) diagrams 
|
|
19
23
|
- Firefox support is back, TLS mode is necessary 
|
|
20
24
|
- Server supports TLS mode and optionally generates self-signed certificates 
|
|
@@ -58,10 +62,15 @@ No additional requirements - runs out of the box with `--editor` flag.
|
|
|
58
62
|
|
|
59
63
|
### 1. Configure your MCP host
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
**Fastest path:**
|
|
66
|
+
|
|
67
|
+
- **Claude Code:** `/plugin marketplace add lgazo/drawio-mcp-server` then `/plugin install drawio`.
|
|
68
|
+
- **Any other host:** `npx drawio-mcp-server install <host>` where `<host>` is `claude-code`, `claude-desktop`, `codex`, `zed`, `opencode`, or `all`.
|
|
69
|
+
|
|
70
|
+
See [docs/PLUGINS.md](./docs/PLUGINS.md) for the full reference, flags, and uninstall, and [docs/release.md](./docs/release.md) for how releases are cut.
|
|
62
71
|
|
|
63
72
|
<details>
|
|
64
|
-
<summary>Claude Desktop</summary>
|
|
73
|
+
<summary>Manual install (JSON snippet, Claude Desktop example)</summary>
|
|
65
74
|
|
|
66
75
|
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
67
76
|
|
|
@@ -75,63 +84,7 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
```
|
|
78
|
-
</details>
|
|
79
|
-
|
|
80
|
-
<details>
|
|
81
|
-
<summary>Claude Code</summary>
|
|
82
|
-
|
|
83
|
-
```sh
|
|
84
|
-
claude mcp add-json drawio '{"type":"stdio","command":"npx","args":["-y","drawio-mcp-server","--editor"]}'
|
|
85
|
-
```
|
|
86
|
-
</details>
|
|
87
|
-
|
|
88
|
-
<details>
|
|
89
|
-
<summary>Zed</summary>
|
|
90
|
-
|
|
91
|
-
Add to `~/.config/zed/settings.json`:
|
|
92
|
-
|
|
93
|
-
```json
|
|
94
|
-
{
|
|
95
|
-
"context_servers": {
|
|
96
|
-
"drawio": {
|
|
97
|
-
"command": "npx",
|
|
98
|
-
"args": ["-y", "drawio-mcp-server", "--editor"],
|
|
99
|
-
"env": {}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
```
|
|
104
|
-
</details>
|
|
105
|
-
|
|
106
|
-
<details>
|
|
107
|
-
<summary>Codex</summary>
|
|
108
|
-
|
|
109
|
-
Edit `~/.codex/config.toml`:
|
|
110
|
-
|
|
111
|
-
```toml
|
|
112
|
-
[mcp_servers.drawio]
|
|
113
|
-
command = "npx"
|
|
114
|
-
args = ["-y", "drawio-mcp-server", "--editor"]
|
|
115
|
-
```
|
|
116
|
-
</details>
|
|
117
87
|
|
|
118
|
-
<details>
|
|
119
|
-
<summary>OpenCode</summary>
|
|
120
|
-
|
|
121
|
-
Add to `opencode.json` in your project root or `~/.config/opencode/opencode.json`:
|
|
122
|
-
|
|
123
|
-
```jsonc
|
|
124
|
-
{
|
|
125
|
-
"$schema": "https://opencode.ai/config.json",
|
|
126
|
-
"mcp": {
|
|
127
|
-
"drawio": {
|
|
128
|
-
"type": "local",
|
|
129
|
-
"command": ["npx", "-y", "drawio-mcp-server", "--editor"],
|
|
130
|
-
"enabled": true
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
88
|
</details>
|
|
136
89
|
|
|
137
90
|
For other MCP clients and detailed configuration (including pnpm options), see [Configuration](./CONFIG.md).
|
|
@@ -223,11 +176,11 @@ See [DESKTOP.md](./DESKTOP.md) for the full setup steps and the current limitati
|
|
|
223
176
|
|
|
224
177
|
## Star History
|
|
225
178
|
|
|
226
|
-
<a href="https://star-history.
|
|
179
|
+
<a href="https://star-history.dera.page/#lgazo/drawio-mcp-server&type=Date">
|
|
227
180
|
<picture>
|
|
228
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://
|
|
229
|
-
<source media="(prefers-color-scheme: light)" srcset="https://
|
|
230
|
-
<img alt="Star History Chart" src="https://
|
|
181
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=lgazo/drawio-mcp-server&type=Date&theme=dark" />
|
|
182
|
+
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=lgazo/drawio-mcp-server&type=Date" />
|
|
183
|
+
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=lgazo/drawio-mcp-server&type=Date" />
|
|
231
184
|
</picture>
|
|
232
185
|
</a>
|
|
233
186
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { SERVER_COMPAT_MATRIX, versionInWindow, } from "../drawio-compat/matrix.js";
|
|
3
|
+
export { SERVER_COMPAT_MATRIX };
|
|
4
|
+
import { getAssetRoot, getCacheDir } from "./manager.js";
|
|
5
|
+
import { readCachedDrawioVersion } from "./version.js";
|
|
6
|
+
export async function ensureSupportedAssets(config, matrix, log, ports) {
|
|
7
|
+
const cacheDir = getCacheDir(config.assetPath);
|
|
8
|
+
const assetRoot = getAssetRoot(config);
|
|
9
|
+
const cached = await readCachedDrawioVersion(assetRoot);
|
|
10
|
+
if (cached && versionInWindow(cached, matrix)) {
|
|
11
|
+
return { version: cached, refetched: false };
|
|
12
|
+
}
|
|
13
|
+
log.log("warning", `cached drawio v${cached ?? "?"} is outside supported window (>= v${matrix.supportedFloor}); refetching latest`);
|
|
14
|
+
rmSync(assetRoot, { recursive: true, force: true });
|
|
15
|
+
await ports.downloadAndExtract(cacheDir, log);
|
|
16
|
+
const after = await readCachedDrawioVersion(assetRoot);
|
|
17
|
+
if (!after || !versionInWindow(after, matrix)) {
|
|
18
|
+
log.log("error", `drawio latest v${after ?? "?"} is still outside supported window; tools may misbehave`);
|
|
19
|
+
}
|
|
20
|
+
return { version: after, refetched: true };
|
|
21
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it, jest } from "@jest/globals";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ensureSupportedAssets } from "./auto-refresh.js";
|
|
6
|
+
import { SERVER_COMPAT_MATRIX } from "../drawio-compat/matrix.js";
|
|
7
|
+
function makeCachedVersion(version) {
|
|
8
|
+
const root = mkdtempSync(join(tmpdir(), "auto-refresh-"));
|
|
9
|
+
const webapp = join(root, "webapp");
|
|
10
|
+
const jsDir = join(webapp, "js");
|
|
11
|
+
mkdirSync(jsDir, { recursive: true });
|
|
12
|
+
writeFileSync(join(jsDir, "app.min.js"), `pre;EditorUi.VERSION="${version}";post`);
|
|
13
|
+
return root;
|
|
14
|
+
}
|
|
15
|
+
const logger = {
|
|
16
|
+
log: jest.fn(),
|
|
17
|
+
};
|
|
18
|
+
describe("ensureSupportedAssets", () => {
|
|
19
|
+
it("returns cached version without refetch when in range", async () => {
|
|
20
|
+
const cacheDir = makeCachedVersion("30.2.6");
|
|
21
|
+
const download = jest.fn(async () => { });
|
|
22
|
+
const result = await ensureSupportedAssets({ assetPath: cacheDir }, SERVER_COMPAT_MATRIX, logger, { downloadAndExtract: download });
|
|
23
|
+
expect(download).not.toHaveBeenCalled();
|
|
24
|
+
expect(result).toEqual({ version: "30.2.6", refetched: false });
|
|
25
|
+
});
|
|
26
|
+
it("wipes cache and re-downloads when cached is out of window", async () => {
|
|
27
|
+
const cacheDir = makeCachedVersion("28.0.0");
|
|
28
|
+
const download = jest.fn(async (target) => {
|
|
29
|
+
const jsDir = join(target, "webapp", "js");
|
|
30
|
+
mkdirSync(jsDir, { recursive: true });
|
|
31
|
+
writeFileSync(join(jsDir, "app.min.js"), `pre;EditorUi.VERSION="30.2.6";post`);
|
|
32
|
+
});
|
|
33
|
+
const result = await ensureSupportedAssets({ assetPath: cacheDir }, SERVER_COMPAT_MATRIX, logger, { downloadAndExtract: download });
|
|
34
|
+
expect(download).toHaveBeenCalled();
|
|
35
|
+
expect(result).toEqual({ version: "30.2.6", refetched: true });
|
|
36
|
+
});
|
|
37
|
+
it("logs error when refetched version is still out of window", async () => {
|
|
38
|
+
const cacheDir = makeCachedVersion("28.0.0");
|
|
39
|
+
const download = jest.fn(async (target) => {
|
|
40
|
+
const jsDir = join(target, "webapp", "js");
|
|
41
|
+
mkdirSync(jsDir, { recursive: true });
|
|
42
|
+
writeFileSync(join(jsDir, "app.min.js"), `pre;EditorUi.VERSION="28.5.0";post`);
|
|
43
|
+
});
|
|
44
|
+
const errorLogs = [];
|
|
45
|
+
const trackingLogger = {
|
|
46
|
+
log: (level, ...args) => {
|
|
47
|
+
if (level === "error")
|
|
48
|
+
errorLogs.push(args);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
await ensureSupportedAssets({ assetPath: cacheDir }, SERVER_COMPAT_MATRIX, trackingLogger, { downloadAndExtract: download });
|
|
52
|
+
expect(errorLogs.length).toBeGreaterThan(0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -85,11 +85,15 @@ export async function downloadAndExtractAssets(targetDir, log) {
|
|
|
85
85
|
}
|
|
86
86
|
export async function ensureAssets(config, log) {
|
|
87
87
|
const { getCacheDir, getAssetRoot, assetsExist } = await import("./manager.js");
|
|
88
|
+
const { ensureSupportedAssets, SERVER_COMPAT_MATRIX } = await import("./auto-refresh.js");
|
|
88
89
|
const cacheDir = getCacheDir(config.assetPath);
|
|
89
90
|
const assetRoot = getAssetRoot(config);
|
|
90
91
|
if (!assetsExist(config)) {
|
|
91
92
|
log.log("info", `Assets not found in ${assetRoot}. Downloading...`);
|
|
92
93
|
await downloadAndExtractAssets(cacheDir, log);
|
|
93
94
|
}
|
|
95
|
+
await ensureSupportedAssets(config, SERVER_COMPAT_MATRIX, log, {
|
|
96
|
+
downloadAndExtract: (targetDir) => downloadAndExtractAssets(targetDir, log),
|
|
97
|
+
});
|
|
94
98
|
return { assetRoot, isLocal: true };
|
|
95
99
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const CHUNK_BYTES = 1024 * 1024;
|
|
4
|
+
const OVERLAP_BYTES = 64;
|
|
5
|
+
const VERSION_RE = /EditorUi\.VERSION\s*=\s*"(\d+\.\d+\.\d+)"/;
|
|
6
|
+
export async function readCachedDrawioVersion(assetRoot) {
|
|
7
|
+
const path = join(assetRoot, "js", "app.min.js");
|
|
8
|
+
if (!existsSync(path))
|
|
9
|
+
return null;
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const stream = createReadStream(path, { highWaterMark: CHUNK_BYTES });
|
|
12
|
+
let tail = "";
|
|
13
|
+
let resolved = false;
|
|
14
|
+
const finish = (value) => {
|
|
15
|
+
if (resolved)
|
|
16
|
+
return;
|
|
17
|
+
resolved = true;
|
|
18
|
+
stream.destroy();
|
|
19
|
+
resolve(value);
|
|
20
|
+
};
|
|
21
|
+
stream.on("data", (chunk) => {
|
|
22
|
+
const text = tail + (typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
23
|
+
const match = VERSION_RE.exec(text);
|
|
24
|
+
if (match) {
|
|
25
|
+
finish(match[1] ?? null);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
tail = text.slice(-OVERLAP_BYTES);
|
|
29
|
+
});
|
|
30
|
+
stream.on("end", () => finish(null));
|
|
31
|
+
stream.on("error", (err) => {
|
|
32
|
+
if (resolved)
|
|
33
|
+
return;
|
|
34
|
+
resolved = true;
|
|
35
|
+
reject(err);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { readCachedDrawioVersion } from "./version.js";
|
|
6
|
+
function makeFixture(contents) {
|
|
7
|
+
const root = mkdtempSync(join(tmpdir(), "drawio-version-"));
|
|
8
|
+
const jsDir = join(root, "js");
|
|
9
|
+
mkdirSync(jsDir, { recursive: true });
|
|
10
|
+
writeFileSync(join(jsDir, "app.min.js"), contents);
|
|
11
|
+
return root;
|
|
12
|
+
}
|
|
13
|
+
describe("readCachedDrawioVersion", () => {
|
|
14
|
+
it("returns the version parsed from app.min.js", async () => {
|
|
15
|
+
const root = makeFixture(`prefix;EditorUi.VERSION="30.2.6";suffix`);
|
|
16
|
+
await expect(readCachedDrawioVersion(root)).resolves.toBe("30.2.6");
|
|
17
|
+
});
|
|
18
|
+
it("returns null when app.min.js is missing", async () => {
|
|
19
|
+
const root = mkdtempSync(join(tmpdir(), "drawio-version-"));
|
|
20
|
+
await expect(readCachedDrawioVersion(root)).resolves.toBeNull();
|
|
21
|
+
});
|
|
22
|
+
it("returns null when VERSION assignment is absent", async () => {
|
|
23
|
+
const root = makeFixture("no version marker here");
|
|
24
|
+
await expect(readCachedDrawioVersion(root)).resolves.toBeNull();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { createDrawioMcpApp } from "./index.js";
|
|
3
|
+
import { MemoryLogger } from "./real-environment/logger.js";
|
|
4
|
+
const HOST = "127.0.0.1";
|
|
5
|
+
async function waitForMessage(ws, predicate, timeoutMs = 2000) {
|
|
6
|
+
return await new Promise((resolve, reject) => {
|
|
7
|
+
const timer = setTimeout(() => {
|
|
8
|
+
ws.off("message", onMessage);
|
|
9
|
+
reject(new Error("timeout waiting for matching WebSocket message"));
|
|
10
|
+
}, timeoutMs);
|
|
11
|
+
function onMessage(data) {
|
|
12
|
+
const text = typeof data === "string" ? data : data.toString();
|
|
13
|
+
let json;
|
|
14
|
+
try {
|
|
15
|
+
json = JSON.parse(text);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (predicate(json)) {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
ws.off("message", onMessage);
|
|
23
|
+
resolve(json);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
ws.on("message", onMessage);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function openClient(port) {
|
|
30
|
+
const ws = new WebSocket(`ws://${HOST}:${port}`);
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
ws.once("open", () => resolve());
|
|
33
|
+
ws.once("error", reject);
|
|
34
|
+
});
|
|
35
|
+
// Server sends `sync-document-state` on connect; ignore it here.
|
|
36
|
+
return ws;
|
|
37
|
+
}
|
|
38
|
+
describe("documents-changed broadcast", () => {
|
|
39
|
+
let app;
|
|
40
|
+
let logger;
|
|
41
|
+
let port;
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
logger = new MemoryLogger();
|
|
44
|
+
app = createDrawioMcpApp({ log: logger });
|
|
45
|
+
const wsServer = await app.startWebSocketServer(0, HOST);
|
|
46
|
+
port = wsServer.address().port;
|
|
47
|
+
});
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
await app.close();
|
|
50
|
+
});
|
|
51
|
+
it("emits documents-changed after inbound document-state", async () => {
|
|
52
|
+
const ws = await openClient(port);
|
|
53
|
+
const received = waitForMessage(ws, (json) => json?.__control === "documents-changed");
|
|
54
|
+
ws.send(JSON.stringify({
|
|
55
|
+
__control: "document-state",
|
|
56
|
+
document: {
|
|
57
|
+
id: "doc-a",
|
|
58
|
+
title: "Alpha",
|
|
59
|
+
mode: "device",
|
|
60
|
+
hash: null,
|
|
61
|
+
file_url: null,
|
|
62
|
+
page_count: 1,
|
|
63
|
+
current_page: {
|
|
64
|
+
index: 0,
|
|
65
|
+
id: "p1",
|
|
66
|
+
name: "Page-1",
|
|
67
|
+
is_current: true,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}));
|
|
71
|
+
const payload = await received;
|
|
72
|
+
expect(payload.documents).toHaveLength(1);
|
|
73
|
+
expect(payload.documents[0].id).toBe("doc-a");
|
|
74
|
+
expect(payload.documents[0].title).toBe("Alpha");
|
|
75
|
+
ws.close();
|
|
76
|
+
});
|
|
77
|
+
it("emits documents-changed after inbound document-removed", async () => {
|
|
78
|
+
const ws = await openClient(port);
|
|
79
|
+
ws.send(JSON.stringify({
|
|
80
|
+
__control: "document-state",
|
|
81
|
+
document: {
|
|
82
|
+
id: "doc-b",
|
|
83
|
+
title: "Beta",
|
|
84
|
+
mode: "device",
|
|
85
|
+
hash: null,
|
|
86
|
+
file_url: null,
|
|
87
|
+
page_count: 1,
|
|
88
|
+
current_page: null,
|
|
89
|
+
},
|
|
90
|
+
}));
|
|
91
|
+
await waitForMessage(ws, (json) => json?.__control === "documents-changed" && json.documents.length === 1);
|
|
92
|
+
const removed = waitForMessage(ws, (json) => json?.__control === "documents-changed" && json.documents.length === 0);
|
|
93
|
+
ws.send(JSON.stringify({
|
|
94
|
+
__control: "document-removed",
|
|
95
|
+
document_id: "doc-b",
|
|
96
|
+
}));
|
|
97
|
+
const payload = await removed;
|
|
98
|
+
expect(payload.documents).toEqual([]);
|
|
99
|
+
ws.close();
|
|
100
|
+
});
|
|
101
|
+
it("emits documents-changed to remaining clients after peer disconnect", async () => {
|
|
102
|
+
const clientA = await openClient(port);
|
|
103
|
+
const clientB = await openClient(port);
|
|
104
|
+
clientA.send(JSON.stringify({
|
|
105
|
+
__control: "document-state",
|
|
106
|
+
document: {
|
|
107
|
+
id: "doc-c",
|
|
108
|
+
title: "Gamma",
|
|
109
|
+
mode: "device",
|
|
110
|
+
hash: null,
|
|
111
|
+
file_url: null,
|
|
112
|
+
page_count: 1,
|
|
113
|
+
current_page: null,
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
await waitForMessage(clientB, (json) => json?.__control === "documents-changed" && json.documents.length === 1);
|
|
117
|
+
const drained = waitForMessage(clientB, (json) => json?.__control === "documents-changed" && json.documents.length === 0);
|
|
118
|
+
clientA.close();
|
|
119
|
+
const payload = await drained;
|
|
120
|
+
expect(payload.documents).toEqual([]);
|
|
121
|
+
clientB.close();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function handleCompatReport(payload, log) {
|
|
2
|
+
const v = payload.drawioVersion ?? "unknown";
|
|
3
|
+
switch (payload.state) {
|
|
4
|
+
case "ok":
|
|
5
|
+
log.log("info", `drawio v${v} is within supported window`);
|
|
6
|
+
return;
|
|
7
|
+
case "below-floor":
|
|
8
|
+
log.log("error", `drawio v${v} predates supported floor v${payload.floor}; ` +
|
|
9
|
+
`version-gated tools will return errors`);
|
|
10
|
+
return;
|
|
11
|
+
case "above-window":
|
|
12
|
+
log.log("warning", `drawio v${v} is newer than the tested window ` +
|
|
13
|
+
`(last tested min: v${payload.detail ?? "?"}); running on newest impl`);
|
|
14
|
+
return;
|
|
15
|
+
case "no-version":
|
|
16
|
+
log.log("warning", `plugin could not detect drawio version (${payload.detail ?? "?"})`);
|
|
17
|
+
return;
|
|
18
|
+
default:
|
|
19
|
+
log.log("warning", `unknown compat state \`${payload.state}\` (drawio v${v})`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { handleCompatReport } from "./log-report.js";
|
|
3
|
+
function makeLogger() {
|
|
4
|
+
const calls = [];
|
|
5
|
+
return {
|
|
6
|
+
calls,
|
|
7
|
+
logger: {
|
|
8
|
+
log: (lvl, msg) => calls.push([lvl, msg]),
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
describe("handleCompatReport", () => {
|
|
13
|
+
it("emits info for ok state", () => {
|
|
14
|
+
const { calls, logger } = makeLogger();
|
|
15
|
+
handleCompatReport({ drawioVersion: "30.2.6", state: "ok", floor: "29.0.0" }, logger);
|
|
16
|
+
expect(calls.some(([lvl]) => lvl === "info")).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
it("emits error for below-floor state", () => {
|
|
19
|
+
const { calls, logger } = makeLogger();
|
|
20
|
+
handleCompatReport({ drawioVersion: "28.0.0", state: "below-floor", floor: "29.0.0" }, logger);
|
|
21
|
+
expect(calls.some(([lvl]) => lvl === "error")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it("emits warning for above-window state", () => {
|
|
24
|
+
const { calls, logger } = makeLogger();
|
|
25
|
+
handleCompatReport({
|
|
26
|
+
drawioVersion: "31.0.0",
|
|
27
|
+
state: "above-window",
|
|
28
|
+
floor: "29.0.0",
|
|
29
|
+
detail: "30.0.0",
|
|
30
|
+
}, logger);
|
|
31
|
+
expect(calls.some(([lvl]) => lvl === "warning")).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it("emits warning for no-version state", () => {
|
|
34
|
+
const { calls, logger } = makeLogger();
|
|
35
|
+
handleCompatReport({
|
|
36
|
+
drawioVersion: null,
|
|
37
|
+
state: "no-version",
|
|
38
|
+
floor: "29.0.0",
|
|
39
|
+
detail: "missing",
|
|
40
|
+
}, logger);
|
|
41
|
+
expect(calls.some(([lvl]) => lvl === "warning")).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isInRange, parseVersion, } from "../vendored/compat/index.js";
|
|
2
|
+
export const SERVER_COMPAT_MATRIX = {
|
|
3
|
+
supportedFloor: "29.0.0",
|
|
4
|
+
supportedRanges: [
|
|
5
|
+
{ min: "29.0.0", maxExclusive: "30.0.0" },
|
|
6
|
+
{ min: "30.0.0", maxExclusive: null },
|
|
7
|
+
],
|
|
8
|
+
};
|
|
9
|
+
export function versionInWindow(version, matrix) {
|
|
10
|
+
const parsed = parseVersion(version);
|
|
11
|
+
if (!parsed)
|
|
12
|
+
return false;
|
|
13
|
+
return matrix.supportedRanges.some((r) => isInRange(parsed, r));
|
|
14
|
+
}
|