mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 12:39:52 +08:00
feat(mcp-server): canvas shell bundled from the web app's canvas with esbuild
The shell (packages/mcp-server/shell) renders DrawioFrame inside the shared providers without the chat: its config comes from window.__MCP_CONFIG__, the four dictionaries ship in the bundle, the theme is kept under an mcp: localStorage key, and system fonts stand in for the web fonts. scripts/build-shell.mjs bundles it into dist/shell with the root's esbuild and Tailwind; check-package caps shell.js at 1.5 MB and shell.css at 300 KB.
This commit is contained in:
@@ -21,6 +21,8 @@ const SCANNED = [
|
||||
"lib/diagram-diff.ts",
|
||||
"stores/canvas-store.ts",
|
||||
"stores/versions-store.ts",
|
||||
// The MCP's browser shell, built from these modules by esbuild
|
||||
"packages/mcp-server/shell",
|
||||
]
|
||||
|
||||
const FORBIDDEN = [
|
||||
@@ -92,6 +94,7 @@ describe("canvas import boundary", () => {
|
||||
it("scans the canvas modules", () => {
|
||||
expect(files).toContain("components/canvas/canvas-stage.tsx")
|
||||
expect(files).toContain("lib/drawio/editor-bridge.ts")
|
||||
expect(files).toContain("packages/mcp-server/shell/main.tsx")
|
||||
})
|
||||
|
||||
it.each(files)("%s imports no chat or Next-only module", (file) => {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* The MCP's canvas shell takes its settings from the config the server
|
||||
* writes into the page, and picks its language and theme in the browser.
|
||||
*/
|
||||
import { readFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { i18n } from "@/lib/i18n/config"
|
||||
import {
|
||||
getShellDictionary,
|
||||
matchLocale,
|
||||
pickLocale,
|
||||
} from "@/packages/mcp-server/shell/dictionary"
|
||||
import { readShellConfig } from "@/packages/mcp-server/shell/runtime-config"
|
||||
import {
|
||||
isDarkPreferred,
|
||||
THEME_STORAGE_KEY,
|
||||
} from "@/packages/mcp-server/shell/theme"
|
||||
|
||||
const shellDir = join(process.cwd(), "packages/mcp-server/shell")
|
||||
|
||||
describe("shell runtime config", () => {
|
||||
it("takes the server's values", () => {
|
||||
const config = readShellConfig({
|
||||
sessionId: "mcp-abc",
|
||||
token: "0123",
|
||||
apiBase: "/api",
|
||||
drawioBaseUrl: "https://embed.diagrams.net/",
|
||||
drawioUi: "sketch",
|
||||
drawioLang: "ja",
|
||||
drawioDark: "dark",
|
||||
lang: "ja",
|
||||
})
|
||||
expect(config.sessionId).toBe("mcp-abc")
|
||||
expect(config.drawioBaseUrl).toBe("https://embed.diagrams.net/")
|
||||
expect(config.drawioUi).toBe("sketch")
|
||||
expect(config.drawioDark).toBe("dark")
|
||||
})
|
||||
|
||||
it("fills in defaults for what is missing or malformed", () => {
|
||||
expect(readShellConfig(undefined)).toEqual({
|
||||
sessionId: "",
|
||||
token: "",
|
||||
apiBase: "/api",
|
||||
drawioBaseUrl: "/drawio/index.html",
|
||||
drawioUi: "",
|
||||
drawioLang: "",
|
||||
drawioDark: "auto",
|
||||
lang: "",
|
||||
})
|
||||
const odd = readShellConfig({
|
||||
sessionId: 42,
|
||||
drawioDark: "sometimes",
|
||||
token: "t",
|
||||
})
|
||||
expect(odd.sessionId).toBe("")
|
||||
expect(odd.drawioDark).toBe("auto")
|
||||
expect(odd.token).toBe("t")
|
||||
})
|
||||
})
|
||||
|
||||
describe("shell language", () => {
|
||||
it("maps language tags to the dictionaries we have", () => {
|
||||
expect(matchLocale("en-US")).toBe("en")
|
||||
expect(matchLocale("zh")).toBe("zh")
|
||||
expect(matchLocale("zh-CN")).toBe("zh")
|
||||
expect(matchLocale("zh-Hans-CN")).toBe("zh")
|
||||
expect(matchLocale("zh-TW")).toBe("zh-Hant")
|
||||
expect(matchLocale("zh-Hant")).toBe("zh-Hant")
|
||||
expect(matchLocale("ZH-HK")).toBe("zh-Hant")
|
||||
expect(matchLocale("ja-JP")).toBe("ja")
|
||||
expect(matchLocale("de")).toBeNull()
|
||||
expect(matchLocale("")).toBeNull()
|
||||
})
|
||||
|
||||
it("prefers the server's language, then the browser's, then English", () => {
|
||||
expect(pickLocale("zh-tw", ["en-US"])).toBe("zh-Hant")
|
||||
expect(pickLocale("", ["de-DE", "ja-JP", "en"])).toBe("ja")
|
||||
expect(pickLocale("de", ["fr"])).toBe("en")
|
||||
expect(pickLocale("", [])).toBe("en")
|
||||
})
|
||||
|
||||
it("has every dictionary, with the shell's own texts", () => {
|
||||
for (const locale of i18n.locales) {
|
||||
const dict = getShellDictionary(locale)
|
||||
expect(Object.keys(dict.shell).sort(), locale).toEqual([
|
||||
"toggleTheme",
|
||||
"waiting",
|
||||
])
|
||||
expect(dict.shell.waiting, locale).toBeTruthy()
|
||||
expect(dict.workspace.canvasLoading, locale).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("shell theme", () => {
|
||||
it("follows the stored choice, then the server, then the system", () => {
|
||||
expect(isDarkPreferred("true", "light", false)).toBe(true)
|
||||
expect(isDarkPreferred("false", "dark", true)).toBe(false)
|
||||
expect(isDarkPreferred(null, "dark", false)).toBe(true)
|
||||
expect(isDarkPreferred(null, "light", true)).toBe(false)
|
||||
expect(isDarkPreferred(null, "auto", true)).toBe(true)
|
||||
expect(isDarkPreferred(null, "auto", false)).toBe(false)
|
||||
expect(isDarkPreferred("maybe", "auto", true)).toBe(true)
|
||||
})
|
||||
|
||||
it("is applied before the first paint by the page's own script", () => {
|
||||
const html = readFileSync(join(shellDir, "index.html"), "utf8")
|
||||
expect(THEME_STORAGE_KEY.startsWith("mcp:")).toBe(true)
|
||||
expect(html).toContain(`localStorage.getItem("${THEME_STORAGE_KEY}")`)
|
||||
expect(html).toContain("window.__MCP_CONFIG__ = {{CONFIG_JSON}};")
|
||||
expect(html).toContain('src="/shell/shell.js"')
|
||||
expect(html).toContain('href="/shell/shell.css"')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user