refactor(mcp-server): make the diagram modules usable from the web app

The web app will reuse the MCP server's XML engine instead of its own
copy in lib/utils.ts, so these modules now run in the browser too.

- Relative imports end in .ts, rewritten to .js by tsc
  (rewriteRelativeImportExtensions); Next.js resolves them directly
- Every module uses the global DOMParser/XMLSerializer: native in the
  browser, linkedom in Node via installDomPolyfill. pages.ts parsed with
  linkedom but serialized with the global serializer, which throws in
  the browser
- The saxes syntax check moves to xml-syntax.ts, so the browser does not
  pull in linkedom; it now also rejects undeclared prefixes such as
  xlink:, as the browser does
- Page decompression uses pako and atob instead of node:zlib and Buffer
- hasCells moves to pages.ts, away from the file system code
- The duplicate cell id check counts UserObject/object ids
- wrapCellsInModel drops comments and text before the first cell, which
  the web app accepts today
- validateAndFixXml takes { strict: false } for diagrams with user content
- Web tests run these modules with a browser DOM (jsdom)
- saxes becomes a direct dependency of the web app
This commit is contained in:
dayuan.jiang
2026-10-04 12:25:53 +09:00
parent 289e6bc25f
commit b7c543ca70
29 changed files with 287 additions and 98 deletions
+24 -4
View File
@@ -16,7 +16,7 @@
*/
import { beforeAll, describe, expect, it } from "vitest"
import { installDomPolyfill } from "../src/dom.js"
import { installDomPolyfill } from "../src/dom.ts"
// Install the DOM polyfill exactly as index.ts does at runtime — the
// helpers under test rely on it.
@@ -24,7 +24,7 @@ beforeAll(() => {
installDomPolyfill()
})
import { applyDiagramOperations } from "../src/diagram-operations.js"
import { applyDiagramOperations } from "../src/diagram-operations.ts"
import {
addPageToDoc,
deletePageFromDoc,
@@ -39,8 +39,8 @@ import {
projectPage,
renamePageInDoc,
serializeMxfile,
} from "../src/pages.js"
import { validateAndFixXml } from "../src/xml-validation.js"
} from "../src/pages.ts"
import { validateAndFixXml } from "../src/xml-validation.ts"
const BARE_MODEL_ONE_CELL = `<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="2" vertex="1" parent="1" value="Hello"><mxGeometry x="40" y="40" width="100" height="40" as="geometry"/></mxCell></root></mxGraphModel>`
@@ -296,6 +296,26 @@ describe("xml-validation.ts — multi-page support", () => {
expect(result.error).toMatch(/duplicate cell ID/i)
})
it("counts the id of a UserObject/object, not of the mxCell it wraps", () => {
const page = (cells: string) =>
`<mxfile><diagram id="p1" name="P1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/>${cells}</root></mxGraphModel></diagram></mxfile>`
const wrapped = (id: string) =>
`<UserObject id="${id}" link="x"><mxCell id="${id}" vertex="1" parent="1"/></UserObject>`
expect(validateAndFixXml(page(wrapped("u"))).valid).toBe(true)
const clash = validateAndFixXml(
page(`${wrapped("2")}<mxCell id="2" vertex="1" parent="1"/>`),
)
expect(clash.valid).toBe(false)
expect(clash.error).toMatch(/duplicate cell ID/i)
})
it("can skip the strict checks for diagrams with user content", () => {
const custom = `<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><MyObj id="5" label="x"><mxCell vertex="1" parent="1"/></MyObj></root></mxGraphModel>`
expect(validateAndFixXml(custom).valid).toBe(true) // fixed by removal
const lenient = validateAndFixXml(custom, { strict: false })
expect(lenient).toMatchObject({ valid: true, fixed: null })
})
it("rejects duplicate <diagram> ids across the file", () => {
const bad = `<mxfile host="app.diagrams.net"><diagram id="p1" name="A"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram><diagram id="p1" name="B"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
const result = validateAndFixXml(bad)