From f89d23880e3fb00213e97c0dda9be098ec3b94a7 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Sun, 11 Oct 2026 10:45:28 +0900 Subject: [PATCH] refactor(scripts): pin the draw.io release in one json file and share the zip reader --- packages/mcp-server/src/drawio-version.json | 5 ++ scripts/drawio-zip.mjs | 67 +++++++++++++++++++++ scripts/fetch-drawio.mjs | 59 +++--------------- 3 files changed, 81 insertions(+), 50 deletions(-) create mode 100644 packages/mcp-server/src/drawio-version.json create mode 100644 scripts/drawio-zip.mjs diff --git a/packages/mcp-server/src/drawio-version.json b/packages/mcp-server/src/drawio-version.json new file mode 100644 index 00000000..9126da33 --- /dev/null +++ b/packages/mcp-server/src/drawio-version.json @@ -0,0 +1,5 @@ +{ + "version": "v32.0.2", + "sha256": "3cb8abec8e9bfc7504760c9cdc9194ecf7e8de178aa2a1d668801c32ecf1a1a7", + "downloadUrl": "https://github.com/jgraph/drawio/releases/download/v32.0.2/draw.war" +} diff --git a/scripts/drawio-zip.mjs b/scripts/drawio-zip.mjs new file mode 100644 index 00000000..e458462b --- /dev/null +++ b/scripts/drawio-zip.mjs @@ -0,0 +1,67 @@ +/** + * Shared by the two draw.io fetch scripts (the web app's scripts/fetch-drawio.mjs + * and packages/mcp-server/scripts/fetch-drawio.mjs): the pinned release and a + * minimal reader for its draw.war zip file, so no extra dependency is needed. + */ +import crypto from "node:crypto" +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" +import zlib from "node:zlib" + +// Pinned because the UI relies on draw.io internals; bump deliberately. The +// web app and the MCP server embed the same release. +const VERSION_FILE = path.join( + path.dirname(fileURLToPath(import.meta.url)), + "../packages/mcp-server/src/drawio-version.json", +) + +/** { version, sha256, downloadUrl } of the pinned draw.war */ +export function readDrawioVersion() { + return JSON.parse(fs.readFileSync(VERSION_FILE, "utf8")) +} + +export function sha256(buf) { + return crypto.createHash("sha256").update(buf).digest("hex") +} + +// Minimal zip reader: finds the central directory and inflates each entry +export function* readZipEntries(buf) { + const EOCD_SIG = 0x06054b50 + let eocd = -1 + for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) { + if (buf.readUInt32LE(i) === EOCD_SIG) { + eocd = i + break + } + } + if (eocd < 0) throw new Error("Not a zip file (end record not found)") + + const count = buf.readUInt16LE(eocd + 10) + let offset = buf.readUInt32LE(eocd + 16) + + for (let n = 0; n < count; n++) { + if (buf.readUInt32LE(offset) !== 0x02014b50) { + throw new Error("Corrupt zip central directory") + } + const method = buf.readUInt16LE(offset + 10) + const compressedSize = buf.readUInt32LE(offset + 20) + const nameLength = buf.readUInt16LE(offset + 28) + const extraLength = buf.readUInt16LE(offset + 30) + const commentLength = buf.readUInt16LE(offset + 32) + const localOffset = buf.readUInt32LE(offset + 42) + const name = buf.toString("utf8", offset + 46, offset + 46 + nameLength) + offset += 46 + nameLength + extraLength + commentLength + + const localNameLength = buf.readUInt16LE(localOffset + 26) + const localExtraLength = buf.readUInt16LE(localOffset + 28) + const start = localOffset + 30 + localNameLength + localExtraLength + const raw = buf.subarray(start, start + compressedSize) + + if (name.endsWith("/")) continue + if (method !== 0 && method !== 8) { + throw new Error(`Unsupported zip compression ${method} (${name})`) + } + yield { name, data: method === 8 ? zlib.inflateRawSync(raw) : raw } + } +} diff --git a/scripts/fetch-drawio.mjs b/scripts/fetch-drawio.mjs index cf1af18e..2f77cb36 100644 --- a/scripts/fetch-drawio.mjs +++ b/scripts/fetch-drawio.mjs @@ -13,11 +13,10 @@ * The release asset (draw.war) is a zip file; it is unpacked with node:zlib * so no extra dependency is needed. */ -import crypto from "node:crypto" import fs from "node:fs" import path from "node:path" -import zlib from "node:zlib" import nextEnv from "@next/env" +import { readDrawioVersion, readZipEntries, sha256 } from "./drawio-zip.mjs" // npm runs this before Next reads the .env files, so read them here: an // external draw.io set in .env.local must skip the download too @@ -26,14 +25,15 @@ nextEnv.loadEnvConfig( process.env.npm_lifecycle_event === "predev", ) -// Pinned because the UI relies on draw.io internals; bump both deliberately -const DRAWIO_VERSION = "v32.0.2" -// SHA-256 of that release's draw.war, as GitHub lists it -const DRAWIO_SHA256 = - "3cb8abec8e9bfc7504760c9cdc9194ecf7e8de178aa2a1d668801c32ecf1a1a7" +// The pinned release and the SHA-256 of its draw.war, as GitHub lists it +// (packages/mcp-server/src/drawio-version.json, shared with the MCP server) +const { + version: DRAWIO_VERSION, + sha256: DRAWIO_SHA256, + downloadUrl: DOWNLOAD_URL, +} = readDrawioVersion() const DEST = path.join(process.cwd(), "public", "drawio") const STAMP = path.join(DEST, ".version") -const DOWNLOAD_URL = `https://github.com/jgraph/drawio/releases/download/${DRAWIO_VERSION}/draw.war` function log(message) { console.log(`[fetch-drawio] ${message}`) @@ -47,47 +47,6 @@ function readStamp() { } } -// Minimal zip reader: finds the central directory and inflates each entry -function* readZipEntries(buf) { - const EOCD_SIG = 0x06054b50 - let eocd = -1 - for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) { - if (buf.readUInt32LE(i) === EOCD_SIG) { - eocd = i - break - } - } - if (eocd < 0) throw new Error("Not a zip file (end record not found)") - - const count = buf.readUInt16LE(eocd + 10) - let offset = buf.readUInt32LE(eocd + 16) - - for (let n = 0; n < count; n++) { - if (buf.readUInt32LE(offset) !== 0x02014b50) { - throw new Error("Corrupt zip central directory") - } - const method = buf.readUInt16LE(offset + 10) - const compressedSize = buf.readUInt32LE(offset + 20) - const nameLength = buf.readUInt16LE(offset + 28) - const extraLength = buf.readUInt16LE(offset + 30) - const commentLength = buf.readUInt16LE(offset + 32) - const localOffset = buf.readUInt32LE(offset + 42) - const name = buf.toString("utf8", offset + 46, offset + 46 + nameLength) - offset += 46 + nameLength + extraLength + commentLength - - const localNameLength = buf.readUInt16LE(localOffset + 26) - const localExtraLength = buf.readUInt16LE(localOffset + 28) - const start = localOffset + 30 + localNameLength + localExtraLength - const raw = buf.subarray(start, start + compressedSize) - - if (name.endsWith("/")) continue - if (method !== 0 && method !== 8) { - throw new Error(`Unsupported zip compression ${method} (${name})`) - } - yield { name, data: method === 8 ? zlib.inflateRawSync(raw) : raw } - } -} - async function main() { if (process.env.NEXT_PUBLIC_DRAWIO_BASE_URL) { log("NEXT_PUBLIC_DRAWIO_BASE_URL is set, skipping the bundled copy") @@ -102,7 +61,7 @@ async function main() { if (!res.ok) throw new Error(`HTTP ${res.status} for ${DOWNLOAD_URL}`) buf = Buffer.from(await res.arrayBuffer()) // The copy runs on the app's own origin: it must be the real release - const actual = crypto.createHash("sha256").update(buf).digest("hex") + const actual = sha256(buf) if (actual !== DRAWIO_SHA256) { throw new Error(`draw.war checksum mismatch (got ${actual})`) }