Merge origin/main (#984, the canvas shell) into the cleanup branch

Conflicts resolved in 16 files. Where main's new shell, classic page and
tests depend on a wire format or a signature the cleanup had changed, main
wins; where main only extended code the cleanup had restructured, main's
change is carried over into the new structure:

- MCP HTTP server: the poll response keeps main's separate export fields
  (exportFormat, exportXml, exportOptions, exportId with a random id)
  instead of the cleanup's pendingExport object; the classic preview page
  is main's version
- MCP index: start_session keeps the cleanup's structure and gains main's
  BROWSER=none, previewUrl and same-origin note; get_selection is written
  in the registerTool style; create_new_diagram keeps the cleanup's
  helpers with main's comment
- history: addHistory takes main's source parameter and returns the index
  again (main's signature)
- new-diagram: the cleanup's prepareNewModel split plus main's page ids
  for diagrams without one
- diagram context: one ExportResult type (main's, with optional fields)
  for both the tagged exports and main's requestExport; the cleanup's
  single export resolver stays
- versions store and version-text test: main's VersionItem base type and
  describeChangeSummary rename, without the legacy history conversion the
  cleanup removed
- dictionaries: main's shell section added; openFileHint stays removed
  (unused on main too)
- tests: the server wiring test keeps the SDK client and gains main's
  session cases; the HTTP server test uses main's export fields; the DOM
  polyfill comes from tests/setup.ts
- package.json: main's postcss kept, the cleanup's removals kept; lockfile
  regenerated
This commit is contained in:
dayuan.jiang
2026-10-12 11:59:51 +09:00
88 changed files with 9744 additions and 605 deletions
+67
View File
@@ -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 }
}
}
+9 -50
View File
@@ -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})`)
}