Compare commits

..

3 Commits

Author SHA1 Message Date
ruinivist 14bef2e633 chore: bare minimum standalone repro for bun splitting bug
Removes all server-side code, tests, CI pipelines, Docker config, extra CSS, sidebars, and documentation files to provide a bare-minimum reproduction environment.

Modifies `App.tsx` to just eagerly load and render `<Excalidraw />`. Modifies `package.json` to only contain a basic build command to reproduce the bug where Bun injects a dynamic chunk rather than the entrypoint chunk.
2026-05-30 18:15:27 +00:00
ruinivist 7bfa1051c3 chore: strip repository to provide a minimal reproduction of bun code splitting bug
Removes all server-side code, tests, CI pipelines, Docker config, extra CSS, and sidebar logic. Modifies `App.tsx` to just eagerly load and render `<Excalidraw />`. Modifies `package.json` to only contain a basic build command to reproduce the bug where Bun injects a dynamic chunk rather than the entrypoint chunk.
2026-05-30 17:59:06 +00:00
ruinivist be361e3896 Add minimal reproduction script for bun code splitting bug
Adds `repro.ts`, a self-contained script that:
- Generates an HTML entrypoint, a main TS entrypoint, and thousands of dynamically imported chunks.
- Runs `bun build --splitting` against the generated structure.
- Parses the output `index.html` to confirm whether the injected script matches the main entrypoint or a random chunk.
- Updates `.gitignore` to safely ignore the temporary artifacts (`bun-repro-dir`).
2026-05-30 17:42:36 +00:00
26 changed files with 8 additions and 2451 deletions
-5
View File
@@ -1,5 +0,0 @@
node_modules
dist
data
.git
coverage
-80
View File
@@ -1,80 +0,0 @@
name: Publish GHCR Image
on:
push:
tags:
- 'release-*'
permissions:
contents: read
packages: write
concurrency:
group: publish-ghcr-${{ github.ref }}
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run typecheck
run: bun run typecheck
- name: Run tests
run: bun run test
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Derive image metadata
shell: bash
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#release-}"
version="${version#v}"
if [[ -z "$version" ]]; then
echo "Release version is empty after stripping prefix from tag: $GITHUB_REF_NAME" >&2
exit 1
fi
image_name="ghcr.io/${GITHUB_REPOSITORY,,}"
{
echo "VERSION=$version"
echo "IMAGE_NAME=$image_name"
} >> "$GITHUB_ENV"
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ env.VERSION }}
${{ env.IMAGE_NAME }}:latest
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ env.VERSION }}
-2
View File
@@ -35,5 +35,3 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.DS_Store
PLAN.md
excali-box-data/
-7
View File
@@ -1,7 +0,0 @@
# Commit Style
- Use Conventional Commits.
- Keep the type and subject lowercase.
- Keep commit messages terse.
Example: `feat: dark mode by default`
-36
View File
@@ -1,36 +0,0 @@
{
auto_https off
admin off
}
:80 {
encode zstd gzip
@mcp path /mcp
handle @mcp {
reverse_proxy 127.0.0.1:3001
}
@api path /api/*
handle @api {
reverse_proxy 127.0.0.1:3000
}
@root path /
handle @root {
reverse_proxy 127.0.0.1:3000
}
handle {
root * /srv/public
@html path /index.html /d/*
header @html Cache-Control "no-cache"
@assets path_regexp assets \.(?:css|js|mjs|svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|otf)$
header @assets Cache-Control "public, max-age=31536000, immutable"
try_files {path} /index.html
file_server
}
}
-35
View File
@@ -1,35 +0,0 @@
FROM oven/bun:1.3.11-alpine AS build
WORKDIR /app
COPY package.json bun.lock tsconfig.json ./
RUN bun install --frozen-lockfile
COPY src ./src
RUN bun run build
FROM caddy:2-alpine AS caddy
FROM oven/bun:1.3.11-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=127.0.0.1
ENV PORT=3000
ENV MCP_HOST=127.0.0.1
ENV MCP_PORT=3001
ENV DATABASE_PATH=/data/excalidraw.sqlite
COPY --from=caddy /usr/bin/caddy /usr/bin/caddy
COPY --from=build /app/dist/public /srv/public
COPY --from=build /app/dist/server /app/dist/server
COPY --from=build /app/dist/mcp /app/dist/mcp
COPY Caddyfile /etc/caddy/Caddyfile
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN mkdir -p /data && chmod +x /usr/local/bin/docker-entrypoint.sh
VOLUME ["/data"]
EXPOSE 80
CMD ["/usr/local/bin/docker-entrypoint.sh"]
-44
View File
@@ -1,44 +0,0 @@
# excali-box
Private self-hosted Excalidraw with Bun, Caddy, and SQLite.
This is bare wrapper around the excalidraw packages that adds autosave and disk-persistence.
No auth to keep things simple - you either run in a private network or put it behind Caddy auth or similar solutions.
> GPT 5.5 generated
## Pull from GHCR and run
Images are published to:
`ghcr.io/ruinivist/excalidraw-box:<version>`
```bash
docker pull ghcr.io/ruinivist/excalidraw-box:latest
docker run --rm -p 127.0.0.1:3000:80 -e PUBLIC_BASE_URL=http://localhost:3000 -v "$PWD/excali-box-data:/data" ghcr.io/ruinivist/excalidraw-box:latest
```
## Run locally
```bash
bun install
bun run dev
```
Open `http://localhost:3000`.
## Test
```bash
bun run test
bun run typecheck
```
## Build the image
```bash
docker build -t excali .
docker run --rm -p 127.0.0.1:3000:80 -e PUBLIC_BASE_URL=http://localhost:3000 -v "$PWD/excali-box-data:/data" excali
```
This will make the data persistent in the `excali-box-data` folder in the current directory, with Caddy serving the browser build on `localhost:3000`. `PUBLIC_BASE_URL` is required so MCP tools can return drawing URLs with the public browser origin. You would want it to be the same url you use to access the app in the browser - in case you reverse proxy or use a tailscale alias etc.
+2 -200
View File
@@ -6,12 +6,8 @@
"name": "excali",
"dependencies": {
"@excalidraw/excalidraw": "^0.18.1",
"@modelcontextprotocol/sdk": "^1.29.0",
"fast-json-patch": "^3.1.1",
"mcp-handler": "^1.1.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"zod": "^4.4.3",
},
"devDependencies": {
"@types/bun": "^1.3.14",
@@ -56,16 +52,12 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="],
"@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
@@ -116,18 +108,6 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="],
"@redis/bloom": ["@redis/bloom@1.2.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg=="],
"@redis/client": ["@redis/client@1.6.1", "", { "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", "yallist": "4.0.0" } }, "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw=="],
"@redis/graph": ["@redis/graph@1.1.1", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw=="],
"@redis/json": ["@redis/json@1.0.7", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ=="],
"@redis/search": ["@redis/search@1.2.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw=="],
"@redis/time-series": ["@redis/time-series@1.1.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
@@ -204,36 +184,20 @@
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browser-fs-access": ["browser-fs-access@0.29.1", "", {}, "sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"canvas-roundrect-polyfill": ["canvas-roundrect-polyfill@0.0.1", "", {}, "sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="],
"chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
@@ -242,19 +206,7 @@
"clsx": ["clsx@1.1.1", "", {}, "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
@@ -340,91 +292,33 @@
"dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"dompurify": ["dompurify@3.4.5", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
"es6-promise-pool": ["es6-promise-pool@2.5.0", "", {}, "sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-patch": ["fast-json-patch@3.1.1", "", {}, "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ=="],
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"fuzzy": ["fuzzy@0.1.3", "", {}, "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w=="],
"generic-pool": ["generic-pool@3.9.0", "", {}, "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"glur": ["glur@1.1.2", "", {}, "sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"image-blob-reduce": ["image-blob-reduce@3.0.1", "", { "dependencies": { "pica": "^7.1.0" } }, "sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q=="],
@@ -436,10 +330,6 @@
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
@@ -448,20 +338,12 @@
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
"jotai": ["jotai@2.11.0", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ=="],
"jotai-scope": ["jotai-scope@0.7.2", "", { "peerDependencies": { "jotai": ">=2.9.2", "react": ">=17.0.0" } }, "sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
@@ -478,60 +360,32 @@
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mcp-handler": ["mcp-handler@1.1.0", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^11.1.0", "redis": "^4.6.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "1.26.0", "next": ">=13.0.0" }, "optionalPeers": ["next"], "bin": { "mcp-adapter": "dist/cli/index.js", "mcp-handler": "dist/cli/index.js", "create-mcp-route": "dist/cli/index.js" } }, "sha512-MVCES7g18gcoZy+R/3v5nadkUMzMAWdos8jRl6DyljOKvd2/ZKDmwlCjL6zp4vo+7FeCXOYL1uWinHWlkKAAUg=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mermaid": ["mermaid@11.15.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"multimath": ["multimath@2.0.0", "", { "dependencies": { "glur": "^1.1.2", "object-assign": "^4.1.1" } }, "sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g=="],
"nanoid": ["nanoid@3.3.3", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"open-color": ["open-color@1.9.1", "", {}, "sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw=="],
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
"pako": ["pako@2.0.3", "", {}, "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"perfect-freehand": ["perfect-freehand@1.2.0", "", {}, "sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw=="],
"pica": ["pica@7.1.1", "", { "dependencies": { "glur": "^1.1.2", "inherits": "^2.0.3", "multimath": "^2.0.0", "object-assign": "^4.1.1", "webworkify": "^1.5.0" } }, "sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ=="],
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"png-chunk-text": ["png-chunk-text@1.0.0", "", {}, "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw=="],
"png-chunks-encode": ["png-chunks-encode@1.0.0", "", { "dependencies": { "crc-32": "^0.3.0", "sliced": "^1.0.1" } }, "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA=="],
@@ -542,16 +396,8 @@
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"pwacompat": ["pwacompat@2.0.17", "", {}, "sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w=="],
"qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
@@ -564,16 +410,10 @@
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"redis": ["redis@4.7.1", "", { "dependencies": { "@redis/bloom": "1.2.0", "@redis/client": "1.6.1", "@redis/graph": "1.1.1", "@redis/json": "1.0.7", "@redis/search": "1.2.0", "@redis/time-series": "1.1.0" } }, "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
"roughjs": ["roughjs@4.6.4", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
@@ -582,52 +422,30 @@
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"sliced": ["sliced@1.0.1", "", {}, "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="],
"tinyexec": ["tinyexec@1.2.2", "", {}, "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="],
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
@@ -636,8 +454,6 @@
"uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
@@ -654,14 +470,6 @@
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
"@chevrotain/cst-dts-gen/@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="],
@@ -716,14 +524,10 @@
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"mermaid/@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
"mermaid/@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="],
@@ -734,8 +538,6 @@
"roughjs/points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@radix-ui/react-roving-focus/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="],
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="],
-53
View File
@@ -1,53 +0,0 @@
#!/bin/sh
set -eu
app_pid=""
mcp_pid=""
caddy_pid=""
stop_children() {
for pid in "$app_pid" "$mcp_pid" "$caddy_pid"; do
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
fi
done
}
trap 'stop_children' INT TERM
bun /app/dist/server/server.js &
app_pid=$!
bun /app/dist/mcp/mcp-server.js &
mcp_pid=$!
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile &
caddy_pid=$!
exit_code=0
while :; do
if ! kill -0 "$app_pid" 2>/dev/null; then
wait "$app_pid" || exit_code=$?
break
fi
if ! kill -0 "$mcp_pid" 2>/dev/null; then
wait "$mcp_pid" || exit_code=$?
break
fi
if ! kill -0 "$caddy_pid" 2>/dev/null; then
wait "$caddy_pid" || exit_code=$?
break
fi
sleep 1
done
stop_children
wait "$app_pid" 2>/dev/null || true
wait "$mcp_pid" 2>/dev/null || true
wait "$caddy_pid" 2>/dev/null || true
exit "$exit_code"
+3 -14
View File
@@ -1,25 +1,14 @@
{
"name": "excali",
"name": "excali-repro",
"type": "module",
"private": true,
"scripts": {
"dev": "bun --hot src/dev-server.tsx",
"build": "rm -rf dist && bun run build:client && bun run build:server && bun run build:mcp",
"build:client": "bun build --target=browser --production --outdir ./dist/public ./src/index.html",
"build:server": "mkdir -p ./dist/server && bun build --target=bun --production --outfile ./dist/server/server.js ./src/server.tsx",
"build:mcp": "mkdir -p ./dist/mcp && bun build --target=bun --production --outfile ./dist/mcp/mcp-server.js ./src/mcp-server.ts",
"start": "DATABASE_PATH=./data/excalidraw.sqlite bun ./dist/server/server.js",
"test": "bun test",
"typecheck": "tsc --noEmit"
"build": "rm -rf dist && bun build --target=browser --splitting --outdir ./dist/public ./src/index.html"
},
"dependencies": {
"@excalidraw/excalidraw": "^0.18.1",
"@modelcontextprotocol/sdk": "^1.29.0",
"fast-json-patch": "^3.1.1",
"mcp-handler": "^1.1.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"zod": "^4.4.3"
"react-dom": "^19.2.6"
},
"devDependencies": {
"@types/bun": "^1.3.14",
+2 -559
View File
@@ -1,566 +1,9 @@
import "../node_modules/@excalidraw/excalidraw/dist/prod/index.css";
import { Excalidraw } from "@excalidraw/excalidraw";
import type {
AppState,
BinaryFiles,
ExcalidrawInitialDataState,
} from "@excalidraw/excalidraw/types";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { DEFAULT_TITLE, type DrawingMeta, type ScenePayload } from "./shared";
type DrawingResponse = DrawingMeta & ScenePayload;
const EXCALIDRAW_THEME_TOKEN_MAP = [
["--island-bg-color", "--app-island-bg"],
["--sidebar-bg-color", "--app-sidebar-bg"],
["--sidebar-border-color", "--app-sidebar-border"],
["--sidebar-shadow", "--app-sidebar-shadow"],
["--color-surface-lowest", "--app-surface-lowest"],
["--color-surface-low", "--app-surface-low"],
["--color-surface-primary-container", "--app-selected-bg"],
["--color-on-surface", "--app-text-color"],
["--color-on-primary-container", "--app-selected-text-color"],
["--color-disabled", "--app-disabled-color"],
["--input-bg-color", "--app-input-bg"],
["--input-border-color", "--app-input-border"],
["--input-label-color", "--app-input-color"],
["--default-border-color", "--app-button-border"],
["--button-hover-bg", "--app-button-hover-bg"],
["--button-active-bg", "--app-button-active-bg"],
["--button-active-border", "--app-button-active-border"],
["--overlay-bg-color", "--app-overlay-bg"],
] as const;
function pathToId(pathname = window.location.pathname): string | null {
const match = pathname.match(/^\/d\/([^/]+)$/);
return match?.[1] ?? null;
}
function sortDrawings(drawings: DrawingMeta[]): DrawingMeta[] {
return [...drawings].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
}
function replaceMeta(drawings: DrawingMeta[], drawing: DrawingMeta): DrawingMeta[] {
const filtered = drawings.filter((item) => item.id !== drawing.id);
return sortDrawings([drawing, ...filtered]);
}
function sceneToInitialData(scene: ScenePayload): ExcalidrawInitialDataState {
return scene as ExcalidrawInitialDataState;
}
function sceneFromEditor(
elements: readonly unknown[],
appState: AppState,
files: BinaryFiles,
): ScenePayload {
return {
elements: [...elements],
appState: appState as unknown as Record<string, unknown>,
files: files as unknown as Record<string, unknown>,
};
}
async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, {
...init,
headers: {
...(init?.body ? { "content-type": "application/json" } : {}),
...init?.headers,
},
});
const body = (await response.json()) as T & { error?: string };
if (!response.ok) {
throw new Error(body.error ?? `Request failed: ${response.status}`);
}
return body;
}
function DrawerIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="3.5" y="5" width="17" height="14" rx="2.5" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M9 5v14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path d="M5.75 9h1.5M5.75 12h1.5M5.75 15h1.5" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
export function App() {
const [drawings, setDrawings] = useState<DrawingMeta[]>([]);
const [activeId, setActiveId] = useState<string | null>(pathToId());
const [activeTitle, setActiveTitle] = useState(DEFAULT_TITLE);
const [scene, setScene] = useState<ScenePayload | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [toastMessage, setToastMessage] = useState<string | null>(null);
const currentIdRef = useRef<string | null>(activeId);
const currentTitleRef = useRef(activeTitle);
const latestSceneRef = useRef<ScenePayload | null>(null);
const ignoreChangeRef = useRef(false);
const appShellRef = useRef<HTMLDivElement | null>(null);
const timeoutRef = useRef<number | null>(null);
const inFlightSaveRef = useRef<Promise<void> | null>(null);
const loadVersionRef = useRef(0);
const themeSyncFrameRef = useRef<number | null>(null);
const toastTimeoutRef = useRef<number | null>(null);
const activeDrawing = useMemo(
() => drawings.find((drawing) => drawing.id === activeId) ?? null,
[activeId, drawings],
);
const refreshList = useCallback(async () => {
const list = await requestJson<DrawingMeta[]>("/api/drawings", { method: "GET" });
setDrawings(sortDrawings(list));
return list;
}, []);
const saveScene = useCallback(async (isManual = false) => {
const drawingId = currentIdRef.current;
const nextScene = latestSceneRef.current;
if (!drawingId || !nextScene) {
return;
}
setError(null);
if (isManual) {
setToastMessage("Saving...");
if (toastTimeoutRef.current !== null) {
clearTimeout(toastTimeoutRef.current);
toastTimeoutRef.current = null;
}
}
const promise = requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
method: "PUT",
body: JSON.stringify(nextScene),
})
.then((body) => {
setDrawings((current) => replaceMeta(current, body.drawing));
if (isManual) {
setToastMessage("Saved");
if (toastTimeoutRef.current !== null) {
clearTimeout(toastTimeoutRef.current);
}
toastTimeoutRef.current = window.setTimeout(() => setToastMessage(null), 2000);
}
})
.catch((saveError: unknown) => {
setError(saveError instanceof Error ? saveError.message : "Save failed");
if (isManual) {
setToastMessage("Save failed");
if (toastTimeoutRef.current !== null) {
clearTimeout(toastTimeoutRef.current);
}
toastTimeoutRef.current = window.setTimeout(() => setToastMessage(null), 2000);
}
throw saveError;
})
.finally(() => {
inFlightSaveRef.current = null;
});
inFlightSaveRef.current = promise;
await promise;
}, []);
const flushPendingSave = useCallback(async () => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
await saveScene();
return;
}
if (inFlightSaveRef.current) {
await inFlightSaveRef.current;
}
}, [saveScene]);
const triggerManualSave = useCallback(async () => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (inFlightSaveRef.current) {
await inFlightSaveRef.current;
}
await saveScene(true);
}, [saveScene]);
const scheduleSave = useCallback(() => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
timeoutRef.current = null;
void saveScene();
}, 30000);
}, [saveScene]);
const loadDrawing = useCallback(
async (drawingId: string) => {
const version = ++loadVersionRef.current;
setLoading(true);
setError(null);
try {
const [list, drawing] = await Promise.all([
requestJson<DrawingMeta[]>("/api/drawings", { method: "GET" }),
requestJson<DrawingResponse>(`/api/drawings/${drawingId}`, { method: "GET" }),
]);
if (version !== loadVersionRef.current) {
return;
}
const nextScene = {
elements: drawing.elements,
appState: drawing.appState,
files: drawing.files,
};
ignoreChangeRef.current = true;
currentIdRef.current = drawing.id;
currentTitleRef.current = drawing.title;
latestSceneRef.current = nextScene;
setDrawings(sortDrawings(list));
setActiveId(drawing.id);
setActiveTitle(drawing.title);
setScene(nextScene);
} catch (loadError) {
const list = await refreshList();
if (list.length === 0) {
const created = await requestJson<DrawingResponse>("/api/drawings", { method: "POST" });
window.history.replaceState(null, "", `/d/${created.id}`);
await loadDrawing(created.id);
return;
}
const fallback = list[0];
if (fallback && fallback.id !== drawingId) {
window.history.replaceState(null, "", `/d/${fallback.id}`);
await loadDrawing(fallback.id);
return;
}
setError(loadError instanceof Error ? loadError.message : "Failed to load drawing");
} finally {
if (version === loadVersionRef.current) {
setLoading(false);
}
}
},
[refreshList],
);
const navigateToDrawing = useCallback(
async (drawingId: string, options?: { replace?: boolean; flush?: boolean }) => {
const replace = options?.replace ?? false;
const flush = options?.flush ?? true;
if (drawingId === currentIdRef.current && latestSceneRef.current) {
if (replace) {
window.history.replaceState(null, "", `/d/${drawingId}`);
}
return;
}
if (flush) {
await flushPendingSave();
}
if (replace) {
window.history.replaceState(null, "", `/d/${drawingId}`);
} else {
window.history.pushState(null, "", `/d/${drawingId}`);
}
await loadDrawing(drawingId);
},
[flushPendingSave, loadDrawing],
);
const createDrawing = useCallback(async () => {
await flushPendingSave();
const created = await requestJson<DrawingResponse>("/api/drawings", { method: "POST" });
await navigateToDrawing(created.id, { flush: false });
}, [flushPendingSave, navigateToDrawing]);
const deleteDrawing = useCallback(
async (drawingId: string) => {
if (!window.confirm("Delete this drawing?")) {
return;
}
if (drawingId === currentIdRef.current) {
await flushPendingSave();
}
const response = await requestJson<{ ok: true; nextId: string | null }>(`/api/drawings/${drawingId}`, {
method: "DELETE",
});
if (drawingId === currentIdRef.current && response.nextId) {
await navigateToDrawing(response.nextId, { replace: true, flush: false });
} else {
await refreshList();
}
},
[flushPendingSave, navigateToDrawing, refreshList],
);
const handleTitleSubmit = useCallback(async () => {
const drawingId = currentIdRef.current;
if (!drawingId) {
return;
}
try {
const body = await requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
method: "PUT",
body: JSON.stringify({ title: currentTitleRef.current }),
});
currentTitleRef.current = body.drawing.title;
setActiveTitle(body.drawing.title);
setDrawings((current) => replaceMeta(current, body.drawing));
} catch (renameError) {
setError(renameError instanceof Error ? renameError.message : "Rename failed");
}
}, []);
const syncThemeTokens = useCallback(() => {
const appShell = appShellRef.current;
const excalidrawRoot = appShell?.querySelector<HTMLElement>(".excalidraw");
if (!appShell || !excalidrawRoot) {
return;
}
const computedStyles = window.getComputedStyle(excalidrawRoot);
for (const [sourceToken, targetToken] of EXCALIDRAW_THEME_TOKEN_MAP) {
const value = computedStyles.getPropertyValue(sourceToken).trim();
if (value) {
appShell.style.setProperty(targetToken, value);
}
}
}, []);
const scheduleThemeTokenSync = useCallback(() => {
if (themeSyncFrameRef.current !== null) {
window.cancelAnimationFrame(themeSyncFrameRef.current);
}
themeSyncFrameRef.current = window.requestAnimationFrame(() => {
themeSyncFrameRef.current = null;
syncThemeTokens();
});
}, [syncThemeTokens]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
e.preventDefault();
e.stopPropagation();
void triggerManualSave();
}
};
window.addEventListener("keydown", handleKeyDown, { capture: true });
return () => {
window.removeEventListener("keydown", handleKeyDown, { capture: true });
};
}, [triggerManualSave]);
useEffect(() => {
const currentPathId = pathToId();
if (currentPathId) {
void loadDrawing(currentPathId);
} else {
window.location.replace("/");
}
const onPopState = () => {
const nextId = pathToId();
if (nextId) {
void navigateToDrawing(nextId, { replace: true, flush: true });
}
};
window.addEventListener("popstate", onPopState);
return () => {
window.removeEventListener("popstate", onPopState);
};
}, [loadDrawing, navigateToDrawing]);
useEffect(() => {
return () => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
if (themeSyncFrameRef.current !== null) {
window.cancelAnimationFrame(themeSyncFrameRef.current);
}
};
}, []);
useEffect(() => {
if (!scene || loading) {
return;
}
scheduleThemeTokenSync();
}, [activeId, loading, scene, scheduleThemeTokenSync]);
useEffect(() => {
if (!isSidebarOpen) {
return;
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setIsSidebarOpen(false);
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [isSidebarOpen]);
const openSidebar = useCallback(() => {
setIsSidebarOpen(true);
}, []);
const closeSidebar = useCallback(() => {
setIsSidebarOpen(false);
}, []);
const handleSelectDrawing = useCallback(
async (drawingId: string) => {
setIsSidebarOpen(false);
await navigateToDrawing(drawingId);
},
[navigateToDrawing],
);
const handleCreateDrawing = useCallback(async () => {
setIsSidebarOpen(false);
await createDrawing();
}, [createDrawing]);
return (
<div className="app-shell" ref={appShellRef}>
{toastMessage && (
<div className="toast-overlay" aria-live="polite">
{toastMessage}
</div>
)}
<main className="editor-shell">
<div className="app-actions">
<button
type="button"
className="secondary-button app-actions-toggle"
onClick={openSidebar}
aria-label="Open drawings"
>
<DrawerIcon />
<span className="sr-only">Open drawings</span>
</button>
</div>
{loading || !scene ? (
<div className="editor-loading">{error ?? "Loading..."}</div>
) : (
<div className="editor-frame">
<Excalidraw
key={activeDrawing?.id ?? activeId}
initialData={sceneToInitialData(scene)}
onChange={(elements, appState, files) => {
const nextScene = sceneFromEditor(elements, appState, files);
latestSceneRef.current = nextScene;
scheduleThemeTokenSync();
if (ignoreChangeRef.current) {
ignoreChangeRef.current = false;
return;
}
scheduleSave();
}}
/>
</div>
)}
</main>
<div
className={isSidebarOpen ? "sidebar-backdrop sidebar-backdrop-open" : "sidebar-backdrop"}
onClick={closeSidebar}
aria-hidden={!isSidebarOpen}
/>
<aside className={isSidebarOpen ? "sidebar sidebar-open" : "sidebar"} aria-hidden={!isSidebarOpen}>
<div className="sidebar-header">
<h1>excali-box</h1>
<div className="sidebar-actions">
<button type="button" className="primary-button" onClick={() => void handleCreateDrawing()}>
New
</button>
<button type="button" className="icon-button" onClick={closeSidebar} aria-label="Close drawings">
×
</button>
</div>
</div>
<div className="drawing-list">
{drawings.map((drawing) => (
<div
key={drawing.id}
className={drawing.id === activeId ? "drawing-item drawing-item-active" : "drawing-item"}
>
{drawing.id === activeId ? (
<div className="drawing-link">
<input
className="title-input"
value={activeTitle}
onChange={(event) => {
currentTitleRef.current = event.target.value;
setActiveTitle(event.target.value);
}}
onBlur={() => void handleTitleSubmit()}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.currentTarget.blur();
}
}}
/>
</div>
) : (
<button
type="button"
className="drawing-link"
onClick={() => void handleSelectDrawing(drawing.id)}
>
<span className="drawing-title">{drawing.title}</span>
</button>
)}
<button
type="button"
className="icon-button"
onClick={() => void deleteDrawing(drawing.id)}
aria-label={`Delete ${drawing.title}`}
>
×
</button>
</div>
))}
</div>
</aside>
<div style={{ height: "100vh" }}>
<Excalidraw />
</div>
);
}
-82
View File
@@ -1,82 +0,0 @@
import { describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createApi } from "./api";
import { createDrawingStore } from "./db";
function withApi() {
const dir = mkdtempSync(join(tmpdir(), "excali-api-"));
const store = createDrawingStore(join(dir, "test.sqlite"));
const api = createApi(store);
return {
api,
cleanup() {
store.close();
rmSync(dir, { recursive: true, force: true });
},
};
}
describe("api", () => {
test("creates a drawing with dark theme by default", async () => {
const { api, cleanup } = withApi();
const created = await api.createDrawing().json();
expect(created.appState.theme).toBe("dark");
cleanup();
});
test("returns 400 for invalid JSON", async () => {
const { api, cleanup } = withApi();
const drawing = await api.createDrawing().json();
const response = await api.updateDrawing(
Object.assign(
new Request(`http://local/api/drawings/${drawing.id}`, {
method: "PUT",
body: "{",
}),
{ params: { id: drawing.id } },
),
);
expect(response.status).toBe(400);
cleanup();
});
test("returns 404 for missing drawing", () => {
const { api, cleanup } = withApi();
const response = api.getDrawing(
Object.assign(new Request("http://local/api/drawings/missing"), { params: { id: "missing" } }),
);
expect(response.status).toBe(404);
cleanup();
});
test("updates a drawing scene", async () => {
const { api, cleanup } = withApi();
const created = await api.createDrawing().json();
const response = await api.updateDrawing(
Object.assign(
new Request(`http://local/api/drawings/${created.id}`, {
method: "PUT",
body: JSON.stringify({
elements: [{ id: "shape" }],
appState: {},
files: {},
}),
}),
{ params: { id: created.id } },
),
);
expect(response.status).toBe(200);
cleanup();
});
});
-123
View File
@@ -1,123 +0,0 @@
import { type DrawingStore } from "./db";
import { HttpError, normalizeTitle, parseJsonBody, parseSceneText } from "./scene";
type RouteRequest = Request & {
params?: Record<string, string>;
};
function json(data: unknown, init?: ResponseInit): Response {
return Response.json(data, init);
}
function errorResponse(error: unknown): Response {
if (error instanceof HttpError) {
return json({ ok: false, error: error.message }, { status: error.status });
}
if (error instanceof Error && error.name === "AbortError") {
return json({ ok: false, error: "Request aborted" }, { status: 499 });
}
console.error(error);
return json({ ok: false, error: "Internal server error" }, { status: 500 });
}
export function createApi(store: DrawingStore) {
return {
health() {
return json({ ok: true });
},
listDrawings() {
return json(store.listDrawings());
},
createDrawing() {
const drawing = store.createDrawing();
return json(
{
id: drawing.id,
title: drawing.title,
createdAt: drawing.createdAt,
updatedAt: drawing.updatedAt,
...drawing.scene,
},
{ status: 201 },
);
},
getDrawing(request: RouteRequest) {
const id = request.params?.id;
const drawing = id ? store.getDrawing(id) : null;
if (!drawing) {
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
}
return json({
id: drawing.id,
title: drawing.title,
createdAt: drawing.createdAt,
updatedAt: drawing.updatedAt,
...drawing.scene,
});
},
async updateDrawing(request: RouteRequest) {
try {
const id = request.params?.id;
if (!id) {
throw new HttpError(400, "Missing drawing id");
}
const text = await request.text();
const raw = parseJsonBody<Record<string, unknown>>(text);
const hasTitle = Object.hasOwn(raw, "title");
const hasScene =
Object.hasOwn(raw, "elements") ||
Object.hasOwn(raw, "appState") ||
Object.hasOwn(raw, "files");
if (!hasTitle && !hasScene) {
throw new HttpError(400, "Nothing to update");
}
const scene = hasScene ? parseSceneText(text) : undefined;
const updated = store.updateDrawing(id, {
scene,
title: hasTitle ? normalizeTitle(raw.title) : undefined,
});
if (!updated) {
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
}
return json({
ok: true,
drawing: {
id: updated.id,
title: updated.title,
createdAt: updated.createdAt,
updatedAt: updated.updatedAt,
},
});
} catch (error) {
return errorResponse(error);
}
},
deleteDrawing(request: RouteRequest) {
const id = request.params?.id;
if (!id) {
return json({ ok: false, error: "Missing drawing id" }, { status: 400 });
}
const { deleted, next } = store.deleteDrawing(id);
if (!deleted) {
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
}
return json({ ok: true, nextId: next?.id ?? null });
},
};
}
-1
View File
@@ -1,7 +1,6 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "./styles.css";
const root = document.getElementById("root");
-50
View File
@@ -1,50 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createDrawingStore } from "./db";
const cleanup: string[] = [];
afterEach(() => {
while (cleanup.length > 0) {
const dir = cleanup.pop();
if (dir) {
rmSync(dir, { recursive: true, force: true });
}
}
});
function createTempStore() {
const dir = mkdtempSync(join(tmpdir(), "excali-"));
cleanup.push(dir);
return createDrawingStore(join(dir, "test.sqlite"));
}
describe("drawing store", () => {
test("creates, updates, lists, and deletes drawings", () => {
const store = createTempStore();
const created = store.createDrawing();
expect(created.title).toBe("Untitled");
expect(created.scene.appState.theme).toBe("dark");
expect(store.listDrawings()).toHaveLength(1);
const updated = store.updateDrawing(created.id, {
title: "Flow",
scene: {
elements: [{ id: "one" }],
appState: { gridSize: 20 },
files: {},
},
});
expect(updated?.title).toBe("Flow");
expect(updated?.scene.elements).toHaveLength(1);
const removed = store.deleteDrawing(created.id);
expect(removed.deleted).toBe(true);
expect(store.listDrawings()).toHaveLength(1);
expect(removed.next?.id).not.toBe(created.id);
});
});
-209
View File
@@ -1,209 +0,0 @@
import { Database } from "bun:sqlite";
import { randomUUID } from "node:crypto";
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { DEFAULT_TITLE, type DrawingMeta, type DrawingRecord, type ScenePayload } from "./shared";
import { coerceStoredScene, emptyScene, normalizeTitle } from "./scene";
type DrawingRow = {
id: string;
title: string;
data: string;
createdAt: string;
updatedAt: string;
};
type DrawingMetaRow = Omit<DrawingRow, "data">;
export type DrawingStore = ReturnType<typeof createDrawingStore>;
function createId(): string {
return randomUUID().replaceAll("-", "").slice(0, 12);
}
function resolveDatabasePath(): string {
if (process.env.DATABASE_PATH) {
return process.env.DATABASE_PATH;
}
try {
mkdirSync("/data", { recursive: true });
return "/data/excalidraw.sqlite";
} catch {
return `${process.cwd()}/data/excalidraw.sqlite`;
}
}
function readMeta(row: DrawingMetaRow | null | undefined): DrawingMeta | null {
if (!row) {
return null;
}
return {
id: row.id,
title: row.title,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
}
function readRow(row: DrawingRow | null | undefined): DrawingRecord | null {
if (!row) {
return null;
}
return {
...readMeta(row)!,
scene: coerceStoredScene(JSON.parse(row.data)),
};
}
export function createDrawingStore(databasePath = resolveDatabasePath()) {
mkdirSync(dirname(databasePath), { recursive: true });
const db = new Database(databasePath, { create: true, strict: true });
db.exec(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS drawings (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
data TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS drawings_updated_at_idx
ON drawings(updated_at DESC, created_at DESC);
`);
const listQuery = db.query(`
SELECT
id,
title,
created_at AS createdAt,
updated_at AS updatedAt
FROM drawings
ORDER BY updated_at DESC, created_at DESC
`);
const getQuery = db.query(`
SELECT
id,
title,
data,
created_at AS createdAt,
updated_at AS updatedAt
FROM drawings
WHERE id = ?1
`);
const createQuery = db.query(`
INSERT INTO drawings (id, title, data)
VALUES (?1, ?2, ?3)
`);
const updateSceneQuery = db.query(`
UPDATE drawings
SET data = ?2, updated_at = CURRENT_TIMESTAMP
WHERE id = ?1
`);
const updateTitleQuery = db.query(`
UPDATE drawings
SET title = ?2, updated_at = CURRENT_TIMESTAMP
WHERE id = ?1
`);
const updateBothQuery = db.query(`
UPDATE drawings
SET title = ?2, data = ?3, updated_at = CURRENT_TIMESTAMP
WHERE id = ?1
`);
const deleteQuery = db.query(`
DELETE FROM drawings
WHERE id = ?1
`);
function listDrawings(): DrawingMeta[] {
return (listQuery.all() as DrawingMetaRow[]).map((row) => readMeta(row)!);
}
function getDrawing(id: string): DrawingRecord | null {
return readRow(getQuery.get(id) as DrawingRow | null | undefined);
}
function createDrawing(title = DEFAULT_TITLE): DrawingRecord {
const id = createId();
createQuery.run(id, normalizeTitle(title), JSON.stringify(emptyScene()));
return getDrawing(id)!;
}
function getLatestDrawing(): DrawingMeta | null {
return listDrawings()[0] ?? null;
}
function ensureInitialDrawing(): DrawingMeta {
return getLatestDrawing() ?? createDrawing();
}
function updateDrawing(id: string, update: { scene?: ScenePayload; title?: string }): DrawingRecord | null {
const existing = getDrawing(id);
if (!existing) {
return null;
}
const hasScene = update.scene !== undefined;
const hasTitle = update.title !== undefined;
if (!hasScene && !hasTitle) {
return existing;
}
if (hasScene && hasTitle) {
updateBothQuery.run(id, normalizeTitle(update.title), JSON.stringify(update.scene));
} else if (hasScene) {
updateSceneQuery.run(id, JSON.stringify(update.scene));
} else {
updateTitleQuery.run(id, normalizeTitle(update.title));
}
return getDrawing(id);
}
function deleteDrawing(id: string): { deleted: boolean; next: DrawingMeta | null } {
const changes = Number(deleteQuery.run(id).changes);
if (changes === 0) {
return { deleted: false, next: null };
}
return {
deleted: true,
next: ensureInitialDrawing(),
};
}
function close() {
db.close(false);
}
return {
databasePath,
close,
listDrawings,
getDrawing,
createDrawing,
getLatestDrawing,
ensureInitialDrawing,
updateDrawing,
deleteDrawing,
};
}
let defaultStore: DrawingStore | null = null;
export function getDefaultDrawingStore(): DrawingStore {
defaultStore ??= createDrawingStore();
return defaultStore;
}
-19
View File
@@ -1,19 +0,0 @@
import index from "./index.html";
import { createServer } from "./server";
export function createDevServer() {
const server = createServer();
return {
...server,
routes: {
...server.routes,
"/d/:id": index,
},
} satisfies Parameters<typeof Bun.serve>[0];
}
if (import.meta.main) {
const server = Bun.serve(createDevServer());
console.log(`Listening on http://${server.hostname}:${server.port}`);
}
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- to resolve the hs and css from root -->
<base href="/" />
<title>excali-box</title>
<title>Repro</title>
<script type="module" src="./client.tsx"></script>
</head>
<body>
-119
View File
@@ -1,119 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createDrawingStore, type DrawingStore } from "./db";
import { createMcpToolHandlers, resolvePublicBaseUrl } from "./mcp-server";
import { type ScenePayload } from "./shared";
const cleanup: Array<{ dir: string; store: DrawingStore }> = [];
afterEach(() => {
while (cleanup.length > 0) {
const item = cleanup.pop();
if (item) {
item.store.close();
rmSync(item.dir, { recursive: true, force: true });
}
}
});
function createTempTools() {
const dir = mkdtempSync(join(tmpdir(), "excali-mcp-"));
const store = createDrawingStore(join(dir, "test.sqlite"));
cleanup.push({ dir, store });
return {
store,
tools: createMcpToolHandlers(store, "http://example.test"),
};
}
function testScene(): ScenePayload {
return {
elements: [
{ id: "one", type: "rectangle", x: 10, y: 20 },
{ id: "two", type: "text", text: "hello" },
],
appState: { theme: "dark" },
files: {},
};
}
describe("mcp tool handlers", () => {
test("PUBLIC_BASE_URL is required", () => {
expect(() => resolvePublicBaseUrl("")).toThrow("PUBLIC_BASE_URL is required");
});
test("create_drawing writes an explicit scene to a temp SQLite DB", () => {
const { store, tools } = createTempTools();
const scene = testScene();
const result = tools.create_drawing({ title: "explicit", scene });
const drawing = store.getDrawing(result.id);
expect(result).toEqual({
id: result.id,
title: "explicit",
url: `http://example.test/d/${result.id}`,
});
expect(drawing?.scene).toEqual(scene);
});
test("get_drawing returns raw scene plus URL", () => {
const { tools } = createTempTools();
const scene = testScene();
const result = tools.create_drawing({ title: "read", scene });
const drawing = tools.get_drawing({ id: result.id });
expect(drawing).toMatchObject({
id: result.id,
title: "read",
url: `http://example.test/d/${result.id}`,
scene,
});
});
test("replace_drawing updates the existing drawing", () => {
const { store, tools } = createTempTools();
const result = tools.create_drawing({ title: "replace", scene: testScene() });
const nextScene: ScenePayload = {
elements: [{ id: "manual", type: "rectangle" }],
appState: { theme: "dark" },
files: {},
};
tools.replace_drawing({ id: result.id, title: "replaced", scene: nextScene });
expect(store.listDrawings()).toHaveLength(1);
expect(store.getDrawing(result.id)?.title).toBe("replaced");
expect(store.getDrawing(result.id)?.scene).toEqual(nextScene);
});
test("patch_drawing replaces an element property and removes an element", () => {
const { store, tools } = createTempTools();
const result = tools.create_drawing({ title: "patch", scene: testScene() });
tools.patch_drawing({
id: result.id,
patch: [
{ op: "replace", path: "/elements/0/x", value: 42 },
{ op: "remove", path: "/elements/1" },
],
});
expect(store.getDrawing(result.id)?.scene.elements).toEqual([{ id: "one", type: "rectangle", x: 42, y: 20 }]);
});
test("missing drawing IDs return tool errors", () => {
const { tools } = createTempTools();
expect(() => tools.get_drawing({ id: "missing" })).toThrow("Drawing not found: missing");
expect(() =>
tools.replace_drawing({
id: "missing",
scene: testScene(),
}),
).toThrow("Drawing not found: missing");
});
});
-243
View File
@@ -1,243 +0,0 @@
import { applyPatch, type Operation } from "fast-json-patch";
import { createMcpHandler } from "mcp-handler";
import { z } from "zod";
import { createDrawingStore, type DrawingStore } from "./db";
import { normalizeScene, normalizeTitle } from "./scene";
import { type ScenePayload } from "./shared";
const sceneSchema = z.object({
elements: z.array(z.unknown()),
appState: z.record(z.string(), z.unknown()),
files: z.record(z.string(), z.unknown()),
});
const patchOperationSchema = z.object({
op: z.enum(["add", "remove", "replace", "move", "copy", "test"]),
path: z.string(),
from: z.string().optional(),
value: z.unknown().optional(),
});
const idInput = z.object({ id: z.string().min(1) });
const createDrawingInput = z.object({ title: z.string(), scene: sceneSchema });
const replaceDrawingInput = z.object({
id: z.string().min(1),
title: z.string().optional(),
scene: sceneSchema,
});
const patchDrawingInput = z.object({
id: z.string().min(1),
title: z.string().optional(),
patch: z.array(patchOperationSchema).min(1),
});
type ToolResult = {
content: Array<{ type: "text"; text: string }>;
};
function trimTrailingSlash(url: string): string {
return url.replace(/\/+$/, "");
}
export function resolvePublicBaseUrl(raw = process.env.PUBLIC_BASE_URL): string {
const value = raw?.trim();
if (!value) {
throw new Error("PUBLIC_BASE_URL is required for the MCP server");
}
try {
const url = new URL(value);
if (url.protocol === "http:" || url.protocol === "https:") {
return trimTrailingSlash(url.toString());
}
} catch {
}
throw new Error("PUBLIC_BASE_URL must be an absolute http(s) URL");
}
function drawingUrl(baseUrl: string, id: string): string {
return `${trimTrailingSlash(baseUrl)}/d/${id}`;
}
function jsonText(data: unknown): ToolResult {
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
};
}
function notFound(id: string): never {
throw new Error(`Drawing not found: ${id}`);
}
function mutationResult(publicBaseUrl: string, drawing: { id: string; title: string }) {
return {
id: drawing.id,
title: drawing.title,
url: drawingUrl(publicBaseUrl, drawing.id),
};
}
function updateExistingDrawing(
store: DrawingStore,
id: string,
update: { scene?: ScenePayload; title?: string },
) {
const updated = store.updateDrawing(id, update);
return updated ?? notFound(id);
}
function applyScenePatch(scene: ScenePayload, patch: Operation[]): ScenePayload {
try {
const result = applyPatch(structuredClone(scene), patch, true, true, true);
return normalizeScene(result.newDocument);
} catch (error) {
throw new Error(error instanceof Error ? `Invalid JSON Patch: ${error.message}` : "Invalid JSON Patch");
}
}
export function createMcpToolHandlers(store: DrawingStore, publicBaseUrl: string) {
return {
list_drawings() {
return store.listDrawings();
},
get_drawing(input: unknown) {
const { id } = idInput.parse(input);
const drawing = store.getDrawing(id) ?? notFound(id);
return {
id: drawing.id,
title: drawing.title,
createdAt: drawing.createdAt,
updatedAt: drawing.updatedAt,
url: drawingUrl(publicBaseUrl, drawing.id),
scene: drawing.scene,
};
},
create_drawing(input: unknown) {
const { title, scene } = createDrawingInput.parse(input);
const created = store.createDrawing(normalizeTitle(title));
const updated = updateExistingDrawing(store, created.id, {
title,
scene: normalizeScene(scene),
});
return mutationResult(publicBaseUrl, updated);
},
replace_drawing(input: unknown) {
const { id, title, scene } = replaceDrawingInput.parse(input);
const updated = updateExistingDrawing(store, id, {
title,
scene: normalizeScene(scene),
});
return mutationResult(publicBaseUrl, updated);
},
patch_drawing(input: unknown) {
const { id, title, patch } = patchDrawingInput.parse(input);
const drawing = store.getDrawing(id) ?? notFound(id);
const updated = updateExistingDrawing(store, id, {
title,
scene: applyScenePatch(drawing.scene, patch as Operation[]),
});
return mutationResult(publicBaseUrl, updated);
},
};
}
export function createExcaliMcpHandler(store: DrawingStore, publicBaseUrl: string) {
const tools = createMcpToolHandlers(store, publicBaseUrl);
return createMcpHandler(
(server) => {
server.registerTool(
"list_drawings",
{
title: "List Drawings",
description: "Returns drawing metadata ordered like the app list.",
},
async () => jsonText(tools.list_drawings()),
);
server.registerTool(
"get_drawing",
{
title: "Get Drawing",
description: "Returns drawing metadata, browser URL, and raw ScenePayload.",
inputSchema: idInput.shape,
},
async (input) => jsonText(tools.get_drawing(input)),
);
server.registerTool(
"create_drawing",
{
title: "Create Drawing",
description: "Creates a drawing from an explicit Excalidraw ScenePayload.",
inputSchema: createDrawingInput.shape,
},
async (input) => jsonText(tools.create_drawing(input)),
);
server.registerTool(
"replace_drawing",
{
title: "Replace Drawing",
description: "Replaces a drawing scene and optionally its title.",
inputSchema: replaceDrawingInput.shape,
},
async (input) => jsonText(tools.replace_drawing(input)),
);
server.registerTool(
"patch_drawing",
{
title: "Patch Drawing",
description: "Applies RFC 6902 JSON Patch operations to a drawing scene and optionally updates its title.",
inputSchema: patchDrawingInput.shape,
},
async (input) => jsonText(tools.patch_drawing(input)),
);
},
{
serverInfo: {
name: "excali",
version: "0.1.0",
},
},
{
basePath: "",
disableSse: true,
maxDuration: 60,
},
);
}
export function createMcpServer() {
const store = createDrawingStore(process.env.DATABASE_PATH);
const handler = createExcaliMcpHandler(store, resolvePublicBaseUrl());
return {
port: Number(process.env.MCP_PORT ?? "3001"),
hostname: process.env.MCP_HOST ?? "127.0.0.1",
fetch(request: Request) {
const url = new URL(request.url);
if (url.pathname !== "/mcp") {
return Response.json({ ok: false, error: "Not found" }, { status: 404 });
}
return handler(request);
},
} satisfies Parameters<typeof Bun.serve>[0];
}
if (import.meta.main) {
const server = Bun.serve(createMcpServer());
console.log(`MCP listening on http://${server.hostname}:${server.port}/mcp`);
}
-31
View File
@@ -1,31 +0,0 @@
import { describe, expect, test } from "bun:test";
import { HttpError, MAX_SCENE_BYTES, emptyScene, normalizeScene, parseJsonBody } from "./scene";
describe("scene helpers", () => {
test("creates empty scenes with dark theme by default", () => {
const scene = emptyScene();
expect(scene.appState.theme).toBe("dark");
});
test("normalizes volatile app state fields", () => {
const scene = normalizeScene({
elements: [],
appState: {
selectedElementIds: { one: true },
viewModeEnabled: false,
},
files: {},
});
expect(scene.appState.selectedElementIds).toBeUndefined();
expect(scene.appState.viewModeEnabled).toBe(false);
});
test("rejects oversized payloads", () => {
const text = "x".repeat(MAX_SCENE_BYTES + 1);
expect(() => parseJsonBody(text)).toThrow(HttpError);
expect(() => parseJsonBody(text)).toThrow("Payload too large");
});
});
-97
View File
@@ -1,97 +0,0 @@
import { DEFAULT_TITLE, type ScenePayload } from "./shared";
export const MAX_SCENE_BYTES = 25 * 1024 * 1024;
export class HttpError extends Error {
constructor(
readonly status: number,
message: string,
) {
super(message);
}
}
const VOLATILE_APP_STATE_KEYS = new Set([
"collaborators",
"selectedElementIds",
"selectedGroupIds",
"editingElement",
"editingLinearElement",
"openMenu",
"openSidebar",
"contextMenu",
"toast",
]);
export function emptyScene(): ScenePayload {
return {
elements: [],
appState: { theme: "dark" },
files: {},
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function normalizeTitle(input: unknown): string {
if (typeof input !== "string") {
return DEFAULT_TITLE;
}
const trimmed = input.trim();
return trimmed.length > 0 ? trimmed : DEFAULT_TITLE;
}
export function normalizeScene(input: unknown): ScenePayload {
if (!isRecord(input)) {
throw new HttpError(400, "Body must be an object");
}
if (!Array.isArray(input.elements)) {
throw new HttpError(400, "elements must be an array");
}
if (!isRecord(input.appState)) {
throw new HttpError(400, "appState must be an object");
}
if (!isRecord(input.files)) {
throw new HttpError(400, "files must be an object");
}
const appState = Object.fromEntries(
Object.entries(input.appState).filter(([key]) => !VOLATILE_APP_STATE_KEYS.has(key)),
);
return {
elements: input.elements,
appState,
files: input.files,
};
}
export function parseJsonBody<T = unknown>(text: string): T {
if (new TextEncoder().encode(text).byteLength > MAX_SCENE_BYTES) {
throw new HttpError(413, "Payload too large");
}
try {
return JSON.parse(text) as T;
} catch {
throw new HttpError(400, "Invalid JSON");
}
}
export function parseSceneText(text: string): ScenePayload {
return normalizeScene(parseJsonBody(text));
}
export function coerceStoredScene(input: unknown): ScenePayload {
try {
return normalizeScene(input);
} catch {
return emptyScene();
}
}
-70
View File
@@ -1,70 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createDrawingStore, type DrawingStore } from "./db";
import { createServer } from "./server";
const cleanup: string[] = [];
const stores: DrawingStore[] = [];
afterEach(() => {
while (stores.length > 0) {
stores.pop()?.close();
}
while (cleanup.length > 0) {
const dir = cleanup.pop();
if (dir) {
rmSync(dir, { recursive: true, force: true });
}
}
});
function createServerFixture() {
const dir = mkdtempSync(join(tmpdir(), "excali-server-"));
cleanup.push(dir);
const store = createDrawingStore(join(dir, "test.sqlite"));
stores.push(store);
return {
store,
server: createServer({ drawingStore: store }),
};
}
describe("createServer", () => {
test("redirects / to the latest drawing", () => {
const { server, store } = createServerFixture();
const response = server.routes["/"]?.(new Request("http://local/")) as Response;
const created = store.listDrawings();
expect(created).toHaveLength(1);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe(`http://local/d/${created[0]?.id}`);
});
test("keeps Bun responsible for API routes", async () => {
const { server } = createServerFixture();
const response = await server.routes["/api/health"]?.GET?.();
expect(response?.status).toBe(200);
expect(await response?.json()).toEqual({ ok: true });
});
test("does not expose a Bun static route for drawings in production", () => {
const { server } = createServerFixture();
expect(Object.hasOwn(server.routes, "/d/:id")).toBe(false);
});
test("returns JSON 404 for unmatched requests", async () => {
const { server } = createServerFixture();
const response = await server.fetch(new Request("http://local/index-abc123.js"));
expect(response.status).toBe(404);
expect(await response.json()).toEqual({ ok: false, error: "Not found" });
});
});
-40
View File
@@ -1,40 +0,0 @@
import { createApi } from "./api";
import { type DrawingStore, getDefaultDrawingStore } from "./db";
type CreateServerOptions = {
drawingStore?: DrawingStore;
};
export function createServer(options: CreateServerOptions = {}) {
const drawingStore = options.drawingStore ?? getDefaultDrawingStore();
const api = createApi(drawingStore);
return {
port: Number(process.env.PORT ?? "3000"),
hostname: process.env.HOST ?? "localhost",
routes: {
"/": (request: Request) => {
const drawing = drawingStore.ensureInitialDrawing();
return Response.redirect(new URL(`/d/${drawing.id}`, request.url), 302);
},
"/api/health": {
GET: () => api.health(),
},
"/api/drawings": {
GET: () => api.listDrawings(),
POST: () => api.createDrawing(),
},
"/api/drawings/:id": {
GET: (request: Request & { params: Record<string, string> }) => api.getDrawing(request),
PUT: (request: Request & { params: Record<string, string> }) => api.updateDrawing(request),
DELETE: (request: Request & { params: Record<string, string> }) => api.deleteDrawing(request),
},
},
fetch: (_request: Request) => Response.json({ ok: false, error: "Not found" }, { status: 404 }),
} satisfies Parameters<typeof Bun.serve>[0];
}
if (import.meta.main) {
const server = Bun.serve(createServer());
console.log(`Listening on http://${server.hostname}:${server.port}`);
}
-18
View File
@@ -1,18 +0,0 @@
export type ScenePayload = {
elements: unknown[];
appState: Record<string, unknown>;
files: Record<string, unknown>;
};
export type DrawingMeta = {
id: string;
title: string;
createdAt: string;
updatedAt: string;
};
export type DrawingRecord = DrawingMeta & {
scene: ScenePayload;
};
export const DEFAULT_TITLE = "Untitled";
-313
View File
@@ -1,313 +0,0 @@
:root {
color-scheme: light;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
}
* {
box-sizing: border-box;
}
html,
body,
#root {
margin: 0;
height: 100%;
}
body {
background: #f5f5f4;
color: #18181b;
}
button,
input {
font: inherit;
}
.app-shell {
height: 100%;
color: var(--app-text-color, #18181b);
--app-sidebar-font-size: 0.875rem;
--app-island-bg: #ffffff;
--app-sidebar-bg: #ffffff;
--app-sidebar-border: #e4e4e7;
--app-sidebar-shadow: 0 24px 80px rgba(24, 24, 27, 0.22);
--app-surface-lowest: #ffffff;
--app-surface-low: #f4f4f5;
--app-selected-bg: #f4f4f5;
--app-text-color: #18181b;
--app-selected-text-color: #18181b;
--app-disabled-color: #a1a1aa;
--app-input-bg: #ffffff;
--app-input-border: #d4d4d8;
--app-input-color: #18181b;
--app-button-border: #d4d4d8;
--app-button-hover-bg: #f4f4f5;
--app-button-active-bg: #e4e4e7;
--app-button-active-border: #5b8def;
--app-overlay-bg: rgba(255, 255, 255, 0.88);
}
.sidebar {
position: fixed;
top: 0;
left: 0;
z-index: 40;
display: flex;
height: 100%;
width: min(340px, calc(100vw - 32px));
flex-direction: column;
border-right: 1px solid var(--app-sidebar-border);
background: var(--app-sidebar-bg);
box-shadow: var(--app-sidebar-shadow);
transform: translateX(calc(-100% - 24px));
transition:
transform 180ms ease,
box-shadow 180ms ease;
}
.sidebar-open {
transform: translateX(0);
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid var(--app-sidebar-border);
padding: 16px;
}
.sidebar-header h1 {
margin: 0;
font-size: var(--app-sidebar-font-size);
font-weight: 600;
color: var(--app-text-color);
}
.sidebar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-actions .primary-button,
.sidebar-actions .icon-button {
height: 32px;
}
.drawing-list {
flex: 1;
overflow: auto;
padding: 8px;
}
.drawing-item {
display: flex;
align-items: center;
gap: 4px;
border-radius: 8px;
padding: 4px 12px;
}
.drawing-item-active {
background: var(--app-selected-bg);
color: var(--app-selected-text-color);
}
.drawing-link {
flex: 1 1 auto;
min-width: 0;
border: 0;
background: transparent;
color: inherit;
padding: 8px 0;
text-align: left;
font-size: var(--app-sidebar-font-size);
}
.drawing-title,
.title-input {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.title-input {
border: 1px solid var(--app-input-border);
border-radius: 6px;
background: var(--app-input-bg);
color: var(--app-input-color);
font-size: var(--app-sidebar-font-size);
padding: 6px 8px;
}
.editor-shell {
position: relative;
height: 100%;
}
.app-actions {
position: fixed;
right: max(16px, calc(env(safe-area-inset-right) + 16px));
bottom: calc(max(16px, env(safe-area-inset-bottom)) + 48px);
z-index: 20;
display: flex;
}
.app-actions-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--lg-button-size, 2.25rem);
height: var(--lg-button-size, 2.25rem);
padding: 0;
border: none;
border-radius: var(--border-radius-lg, 12px);
box-shadow: 0 0 0 1px var(--app-surface-lowest);
background: var(--app-surface-low);
color: var(--app-text-color);
}
.app-actions-toggle svg {
width: 1rem;
height: 1rem;
overflow: visible;
transform: translateY(0.5px) scale(1.08);
transform-origin: center;
}
.primary-button,
.secondary-button,
.icon-button {
border: 1px solid var(--app-button-border);
border-radius: 8px;
background: var(--app-island-bg);
color: var(--app-text-color);
transition:
background-color 120ms ease,
border-color 120ms ease,
color 120ms ease,
box-shadow 120ms ease;
}
.primary-button,
.secondary-button {
cursor: pointer;
font-size: var(--app-sidebar-font-size);
font-weight: 500;
padding: 8px 12px;
}
.primary-button:hover,
.secondary-button:hover,
.icon-button:hover {
background: var(--app-button-hover-bg);
}
.primary-button:active,
.secondary-button:active,
.icon-button:active {
background: var(--app-button-active-bg);
border-color: var(--app-button-active-border);
}
.secondary-button:disabled {
cursor: not-allowed;
opacity: 0.6;
color: var(--app-disabled-color);
}
.icon-button {
display: inline-flex;
flex: 0 0 32px;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
cursor: pointer;
}
.sidebar-backdrop {
position: fixed;
inset: 0;
z-index: 30;
background: color-mix(in srgb, var(--app-overlay-bg) 72%, #000000);
opacity: 0;
pointer-events: none;
transition: opacity 180ms ease;
}
.sidebar-backdrop-open {
opacity: 1;
pointer-events: auto;
}
.toast-overlay {
position: fixed;
top: 16px;
right: 16px;
z-index: 100;
background: var(--app-overlay-bg);
backdrop-filter: blur(4px);
color: var(--app-text-color);
padding: 8px 16px;
border-radius: 8px;
font-size: var(--app-sidebar-font-size);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 1px solid var(--app-sidebar-border);
pointer-events: none;
}
.editor-frame,
.editor-loading {
height: 100%;
}
.editor-loading {
display: grid;
place-items: center;
color: var(--app-text-color);
}
.app-actions-toggle:hover {
background: var(--app-button-hover-bg);
}
.app-actions-toggle:active {
box-shadow: 0 0 0 1px var(--app-button-active-border);
background: var(--app-button-active-bg);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 900px) {
.sidebar {
width: min(300px, calc(100vw - 20px));
}
.app-actions {
right: max(16px, calc(env(safe-area-inset-right) + 16px));
bottom: calc(max(16px, env(safe-area-inset-bottom)) + 56px);
}
}