From e53b2128de9793eb10b66c1cd660e3a239dded1f Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 09:47:16 +0900 Subject: [PATCH 01/16] Add devup-bridge-plugin for quota-free Figma reads devup-mcp reads Figma by shipping JS to the official MCP's use_figma tool, and that path is rate limited. The scripts only touch documented Plugin API - getNodeByIdAsync, getLocalPaintStylesAsync, variables - so a plugin we run ourselves can execute them against the open file with no API budget at all. Build-time codegen wraps the existing scripts as functions rather than copying them, so remote and bridge stay on one source. Placeholders become parameters; __DEVUP_SNAPSHOT_CURSOR__ stays literal because it is a marker node id shared with the Rust decoder, not a value. An unsubstituted placeholder fails the build. No eval: the plugin sandbox blocks dynamic code, and a bundler has to see the call sites. The UI bundle is inlined into ui.html because the iframe is injected via srcdoc and cannot fetch a relative script. Rust transport lands separately; this half builds and typechecks. --- plugin/.gitignore | 6 + plugin/manifest.json | 14 + plugin/package-lock.json | 4067 ++++++++++++++++++++++++++++++++ plugin/package.json | 20 + plugin/rspack.config.js | 68 + plugin/scripts/gen-scripts.mjs | 146 ++ plugin/scripts/inline-ui.mjs | 28 + plugin/src/code.ts | 114 + plugin/src/ui.html | 71 + plugin/src/ui.ts | 112 + plugin/tsconfig.json | 15 + 11 files changed, 4661 insertions(+) create mode 100644 plugin/.gitignore create mode 100644 plugin/manifest.json create mode 100644 plugin/package-lock.json create mode 100644 plugin/package.json create mode 100644 plugin/rspack.config.js create mode 100644 plugin/scripts/gen-scripts.mjs create mode 100644 plugin/scripts/inline-ui.mjs create mode 100644 plugin/src/code.ts create mode 100644 plugin/src/ui.html create mode 100644 plugin/src/ui.ts create mode 100644 plugin/tsconfig.json diff --git a/plugin/.gitignore b/plugin/.gitignore new file mode 100644 index 0000000..17a4278 --- /dev/null +++ b/plugin/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ + +# devup-mcp 의 스크립트에서 빌드 타임에 생성됩니다(`npm run gen`). +# 커밋하면 원본과 어긋난 사본이 돌아다니게 됩니다. +src/generated/ diff --git a/plugin/manifest.json b/plugin/manifest.json new file mode 100644 index 0000000..4a231eb --- /dev/null +++ b/plugin/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "Devup Bridge", + "id": "devup-bridge-plugin", + "api": "1.0.0", + "main": "dist/code.js", + "ui": "dist/ui.html", + "capabilities": ["inspect"], + "documentAccess": "dynamic-page", + "editorType": ["figma", "dev"], + "networkAccess": { + "allowedDomains": ["ws://localhost:1993", "ws://127.0.0.1:1993"], + "reasoning": "devup-mcp 가 같은 기기에서 여는 로컬 WebSocket 에 붙어, 열려 있는 파일의 노드를 읽어 돌려줍니다. 문서를 바꾸지 않고 외부로 보내지도 않습니다." + } +} diff --git a/plugin/package-lock.json b/plugin/package-lock.json new file mode 100644 index 0000000..57c584f --- /dev/null +++ b/plugin/package-lock.json @@ -0,0 +1,4067 @@ +{ + "name": "devup-bridge-plugin", + "version": "0.4.6", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "devup-bridge-plugin", + "version": "0.4.6", + "devDependencies": { + "@figma/plugin-typings": "^1.109.0", + "@rspack/cli": "^1.2.0", + "@rspack/core": "^1.2.0", + "html-webpack-plugin": "^5.6.3", + "typescript": "^5.8.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@figma/plugin-typings": { + "version": "1.138.0", + "resolved": "https://registry.npmjs.org/@figma/plugin-typings/-/plugin-typings-1.138.0.tgz", + "integrity": "sha512-bLP9wRf8c0e4z5GbXpDUXd7YNPXEDpgyeMmTe8Lm4EWMU8JEO1DFRfj1NaucIeRSurbde8y7xS1jas60LTTDaw==", + "dev": true, + "license": "MIT License" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.76.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.76.0.tgz", + "integrity": "sha512-pXo9gYLUxLQfUDZG40an+VV1DA5hvAsspkfpuTpDTNMR9ZmIUo2QGKRQB5kwWjh5pPIXZXeTvS2au6Vp7DurAg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.76.0", + "@jsonjoy.com/fs-node-utils": "4.76.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.76.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.76.0.tgz", + "integrity": "sha512-CTcvtizrAnr0zNkroYRpPoqAHWoNm8WNBVn1LJTdKka4G7Yho8cV1qdQpfSWOVcIcKF7VuOqfNvvMx4YUoeBDQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.76.0", + "@jsonjoy.com/fs-node-builtins": "4.76.0", + "@jsonjoy.com/fs-node-utils": "4.76.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.76.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.76.0.tgz", + "integrity": "sha512-ongwZm58vGpi1Ibmib4eMsbif6pRvARvdQnnJ0Y8BqnZGNmyoIADoZlAVG3bWz1kAG3cBnYZLiTE9qoXFvkCcg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.76.0", + "@jsonjoy.com/fs-node-builtins": "4.76.0", + "@jsonjoy.com/fs-node-utils": "4.76.0", + "@jsonjoy.com/fs-print": "4.76.0", + "@jsonjoy.com/fs-snapshot": "4.76.0", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.76.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.76.0.tgz", + "integrity": "sha512-yA+MnjUomZsXL3m4FXeDoS45s58k+3NrxEMYVZaBMnIIqhFs1hdz5+pTTPUPClqSgH87LIGq6nitDZq+T8ImJQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.76.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.76.0.tgz", + "integrity": "sha512-yZhyt2W1DKI+QKv1fN/YsJ36PU9nqfUstU3dRF2BM3jnc9abfT0ri6u+E/r2BdmZ3PyIq9H+b5FDxwST/H3OUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.76.0", + "@jsonjoy.com/fs-node-builtins": "4.76.0", + "@jsonjoy.com/fs-node-utils": "4.76.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.76.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.76.0.tgz", + "integrity": "sha512-xjxvUNDqklDrGh3bhkVQ/IQZd8+XUYXUU1emRBan+gYvePf+H2mWB33JxKgOQ59x2sJuokjUq7mA4b6PlYwatw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.76.0", + "glob-to-regex.js": "^1.0.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.76.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.76.0.tgz", + "integrity": "sha512-zZYQ8XHZs2EqAE4gzvykcxgR1r5DOPwc9bg3j0LKLFeUr7Ak8CJsZXC0zhnxIH14XYZh4WAu+GburaKppKO8fA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.76.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.76.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.76.0.tgz", + "integrity": "sha512-W0zFE+Cr1JRSIePN6pukCXqtzqu5sCZL4QXLb1FdYxUhqtpkPVXc12gbBlJg5k+bPTHihmCe3LDFu61siD9IBg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.76.0", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@module-federation/error-codes": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", + "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@module-federation/runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", + "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/runtime-core": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-core": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", + "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-tools": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", + "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/webpack-bundler-runtime": "0.22.0" + } + }, + "node_modules/@module-federation/sdk": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", + "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", + "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rspack/binding": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.12.tgz", + "integrity": "sha512-f4HHuLbvuld8Ba4iB/4ibse5XrKxFrgmM3S4P2AOKnPlekAFlBjmltCuaTL/W2ggYvILaVY+YcFXrEH1rrKeQA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "1.7.12", + "@rspack/binding-darwin-x64": "1.7.12", + "@rspack/binding-linux-arm64-gnu": "1.7.12", + "@rspack/binding-linux-arm64-musl": "1.7.12", + "@rspack/binding-linux-x64-gnu": "1.7.12", + "@rspack/binding-linux-x64-musl": "1.7.12", + "@rspack/binding-wasm32-wasi": "1.7.12", + "@rspack/binding-win32-arm64-msvc": "1.7.12", + "@rspack/binding-win32-ia32-msvc": "1.7.12", + "@rspack/binding-win32-x64-msvc": "1.7.12" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.12.tgz", + "integrity": "sha512-rbFprJaJiqrmfy8SHth8EsoRS0wg4bXcucwj9NiMzpGFq14Opw8c04iQ6H9BECYzgmN0PKZ9rh41LdVvhdZe4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.12.tgz", + "integrity": "sha512-jnOp+/UXOJa9xqUb8KXH03sysoO2e4Ij6tw6MqDdmdj8n/A8PQENRPUbW9AwXpPtVDJPus9r4fi7b3+6e4B8Hg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.12.tgz", + "integrity": "sha512-C8owWG+yvo7X0oVLIXetkoJhIFBP1LYNcAQqtgLmJnQLQDklGuP83dKC+zISGQWpjawHfZ1ER96vLgoTrxKZdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.12.tgz", + "integrity": "sha512-i51WWI64aRpsfSki6rN0aepPqXkVfS+vZM7+4bWDcmnhUmdMvhIPcYg0QRk3DtyJnu33jqNLM0WHY78k00NyfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.12.tgz", + "integrity": "sha512-MSos0FuPEefqo9V92ULd5hggKG29EkSNg1zDcypy0OkpsKh5pfjVxTLYFXgTcVyFoUQQbdG8zFBzYbwmJ8V4ew==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.12.tgz", + "integrity": "sha512-JcAMVKXOnjfpC3coWjCFPWD3Yl8RBw6a+IXQQ8mfRlHaHMIiOv8IfZqx15XRxMUn49CtP7Z0Na8iiAg2aKrcfw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.12.tgz", + "integrity": "sha512-n+ZqP6ZMc0nhOgvadg5VhEs9ojtbES80AcWeFnmGkbzIszvGSO63GKNiRkXtjJ9KFuRzytbbmsCqkUVH+Tywxg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "1.0.7" + } + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.12.tgz", + "integrity": "sha512-8+h5fYDXYdmugbdfZ+D1y8IQ3rv2EhSfyGP7vBe+bjNyaMa4jWrpucmZbtxojUL1AzaeuHbvMdj9UO/gelk/+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.12.tgz", + "integrity": "sha512-cDMGwTRSa2p9fNBVe1wTRkF2AEXZ9ARWW36QeC5CkLaI0Ezz8lvhF2+CSOPnhaQ1O1qtn0L0SF+lFnrY+I7xGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.12.tgz", + "integrity": "sha512-wIqFvlgFqrgUyj/6S/FJcvShnkZOmIeXTfqvheLY67MGq8qd8jb1YimQVKAIrmWB3yuJKUFACI3Ag1UBtEedEA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/cli": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/cli/-/cli-1.7.12.tgz", + "integrity": "sha512-aiVj5hm12/rhkH/KTvs9BugpRuLEutIw5o3KmQDvSMd6EqEIASuCCUI3/97uaNP58hqZSl7mAotsLcwn/OCbJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.7", + "@rspack/dev-server": "~1.1.5", + "exit-hook": "^4.0.0", + "webpack-bundle-analyzer": "4.10.2" + }, + "bin": { + "rspack": "bin/rspack.js" + }, + "peerDependencies": { + "@rspack/core": "^1.0.0-alpha || ^1.x" + } + }, + "node_modules/@rspack/core": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.12.tgz", + "integrity": "sha512-6CwFIHlhRmXfZoMj3v9MZ1SMTPBn+cHVXeMIeaGp5sufqinKsISbsqHu6ZMJu2wDSmZLdmQJX6zLxkhcAUlhkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@module-federation/runtime-tools": "0.22.0", + "@rspack/binding": "1.7.12", + "@rspack/lite-tapable": "1.1.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.1" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/dev-server": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rspack/dev-server/-/dev-server-1.1.5.tgz", + "integrity": "sha512-cwz0qc6iqqoJhyWqxP7ZqE2wyYNHkBMQUXxoQ0tNoZ4YNRkDyQ4HVJ/3oPSmMKbvJk/iJ16u7xZmwG6sK47q/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "http-proxy-middleware": "^2.0.9", + "p-retry": "^6.2.0", + "webpack-dev-server": "5.2.2", + "ws": "^8.18.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@rspack/core": "*" + } + }, + "node_modules/@rspack/lite-tapable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz", + "integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz", + "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bonjour-service": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.4.tgz", + "integrity": "sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", + "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "destroy": "1.2.0", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/exit-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-4.0.0.tgz", + "integrity": "sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.8", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.8.tgz", + "integrity": "sha512-MZmKQcTnhEh1SPSyMiEytIeDZDUoBZVorNHivQGXMASHf/BSGGOrKa2xQ5bGx3TCe1n109ecCt+cpww7wwWhKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x || 2.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", + "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/launch-editor": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.4" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.76.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.76.0.tgz", + "integrity": "sha512-g2rDa/iTAM+vuzCdFgYy4lRvTLGR3vrrWtyv2971iviMtdCplYaPlwdo/W0VcRjPk6iWxLvTUbM6zvUFDelAVg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.76.0", + "@jsonjoy.com/fs-fsa": "4.76.0", + "@jsonjoy.com/fs-node": "4.76.0", + "@jsonjoy.com/fs-node-builtins": "4.76.0", + "@jsonjoy.com/fs-node-to-fsa": "4.76.0", + "@jsonjoy.com/fs-node-utils": "4.76.0", + "@jsonjoy.com/fs-print": "4.76.0", + "@jsonjoy.com/fs-snapshot": "4.76.0", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.4.0.tgz", + "integrity": "sha512-ZzWFVzFgyRHi/T2Ecxm37lL6J9+hZO0P9L7LCuLxAbC6XWxkvxcaQBCXhQxMGn/Tdt9nD7zIBk0ELwhMQ3UEDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy-transport/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/spdy/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/thingies": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.1.tgz", + "integrity": "sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.6.tgz", + "integrity": "sha512-yBWCMvIfUmuhAE8vdqUKzH0vg9kuWN0KeG4vBnqRplUFHRU7lMQjkiJWxVQzvo2BTewqhPhDlMB41rAt2jVA9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/plugin/package.json b/plugin/package.json new file mode 100644 index 0000000..88ab0a4 --- /dev/null +++ b/plugin/package.json @@ -0,0 +1,20 @@ +{ + "name": "devup-bridge-plugin", + "version": "0.4.6", + "private": true, + "type": "module", + "description": "devup-mcp 가 Figma 를 읽을 때 공식 MCP 할당량을 쓰지 않도록, 열려 있는 파일을 로컬 WebSocket 으로 직접 읽어 주는 Figma 플러그인", + "scripts": { + "gen": "node scripts/gen-scripts.mjs", + "build": "npm run gen && rspack build && node scripts/inline-ui.mjs", + "watch": "npm run gen && rspack build --watch", + "typecheck": "npm run gen && tsc --noEmit" + }, + "devDependencies": { + "@figma/plugin-typings": "^1.109.0", + "@rspack/cli": "^1.2.0", + "@rspack/core": "^1.2.0", + "html-webpack-plugin": "^5.6.3", + "typescript": "^5.8.0" + } +} diff --git a/plugin/rspack.config.js b/plugin/rspack.config.js new file mode 100644 index 0000000..a5132bb --- /dev/null +++ b/plugin/rspack.config.js @@ -0,0 +1,68 @@ +// Figma 플러그인은 파일 두 개를 원한다. 메인 스레드용 `code.js` 하나와, UI +// iframe 에 그대로 인라인된 `ui.html` 하나다. iframe 은 외부 스크립트를 불러올 +// 수 없어서 UI 번들은 HTML 안에 박혀야 한다. + +import { fileURLToPath } from 'node:url' +import { dirname, resolve } from 'node:path' +import HtmlWebpackPlugin from 'html-webpack-plugin' + +const here = dirname(fileURLToPath(import.meta.url)) + +/** @type {import('@rspack/core').Configuration[]} */ +export default [ + { + name: 'code', + target: 'web', + mode: 'production', + entry: { code: resolve(here, 'src/code.ts') }, + output: { path: resolve(here, 'dist'), filename: '[name].js' }, + resolve: { extensions: ['.ts', '.js'] }, + module: { + rules: [ + { + test: /\.ts$/, + use: { + loader: 'builtin:swc-loader', + options: { + jsc: { parser: { syntax: 'typescript' }, target: 'es2020' }, + }, + }, + }, + ], + }, + // 플러그인 샌드박스에는 청크 로더가 없다. 한 파일로 나와야 한다. + optimization: { splitChunks: false }, + }, + { + name: 'ui', + target: 'web', + mode: 'production', + entry: { ui: resolve(here, 'src/ui.ts') }, + output: { path: resolve(here, 'dist'), filename: '[name].js' }, + resolve: { extensions: ['.ts', '.js'] }, + module: { + rules: [ + { + test: /\.ts$/, + use: { + loader: 'builtin:swc-loader', + options: { + jsc: { parser: { syntax: 'typescript' }, target: 'es2020' }, + }, + }, + }, + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + template: resolve(here, 'src/ui.html'), + filename: 'ui.html', + chunks: ['ui'], + inject: 'body', + // 번들 인라인은 빌드 뒤 scripts/inline-ui.mjs 가 처리한다. + scriptLoading: 'blocking', + }), + ], + optimization: { splitChunks: false }, + }, +] diff --git a/plugin/scripts/gen-scripts.mjs b/plugin/scripts/gen-scripts.mjs new file mode 100644 index 0000000..b14ea6c --- /dev/null +++ b/plugin/scripts/gen-scripts.mjs @@ -0,0 +1,146 @@ +// devup-mcp 의 Figma 읽기 스크립트를 플러그인이 부를 수 있는 함수로 바꾼다. +// +// 스크립트 원본은 `crates/devup-mcp-figma/src/scripts/*.js` 하나뿐이다. 원격 +// 경로(공식 Figma MCP `use_figma`)는 Rust 가 플레이스홀더를 값으로 치환해 코드 +// 문자열을 보내고, 브리지 경로는 이 파일이 같은 원본을 빌드 타임에 함수로 감싼다. +// 원본이 둘이 되면 두 경로의 봉투가 조용히 갈라지므로 복제하지 않는다. +// +// 스크립트는 최상위 `await` 와 최상위 `return` 을 쓰는 async 함수 "본문"이다. +// 그래서 감싸기만 하면 그대로 동작한다. eval 은 쓰지 않는다 — Figma 플러그인 +// 샌드박스에서 동적 코드 실행은 막혀 있고, 번들러가 정적으로 볼 수 있어야 한다. + +import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const here = dirname(fileURLToPath(import.meta.url)) +const pluginRoot = resolve(here, '..') +const figmaCrate = resolve(pluginRoot, '../crates/devup-mcp-figma/src') +const scriptDir = join(figmaCrate, 'scripts') +// 확장자는 붙이지 않는다 — `.js` 와 `.d.ts` 두 벌로 나간다. +const outFile = join(pluginRoot, 'src/generated/scripts') + +/** + * 런타임 파라미터 플레이스홀더 → 함수 인자 경로. + * + * Rust 의 `BuiltinScript::source` 가 채우는 값과 같은 자리다. 이름이 갈라지면 + * 브리지 경로만 조용히 빈 값으로 동작하므로 Rust 쪽과 함께 고쳐야 한다. + */ +const PARAMS = { + __DEVUP_NODE_ID__: 'p.nodeId', + __DEVUP_ROOT_IDS__: 'p.rootIds', + __DEVUP_SNAPSHOT__: 'p.snapshot', + __DEVUP_SEARCH__: 'p.search', + __DEVUP_EXPLORE__: 'p.explore', + __DEVUP_RESOURCE_BATCH__: 'p.resources', + __DEVUP_LARGE_VALUE__: 'p.largeValue', + __DEVUP_ASSET__: 'p.asset', + __DEVUP_THEME__: 'p.theme', +} + +/** + * 빌드 타임에 그대로 박히는 인클루드. 값이 아니라 소스/스키마라 파라미터가 아니다. + * + * `__DEVUP_SNAPSHOT_CURSOR__` 는 여기에도 위에도 없다. 그것은 치환 대상이 아니라 + * 스크립트와 Rust(`snapshot.rs` 의 `SNAPSHOT_CURSOR_ID`)가 공유하는 마커 노드 + * id 리터럴이다. 치환하면 페이지네이션이 조용히 깨진다. + */ +const INCLUDES = { + __DEVUP_LARGE_VALUE_HELPERS__: () => + readFileSync(join(scriptDir, 'large_value_helpers.js'), 'utf8'), + __DEVUP_PLUGIN_API_MANIFEST__: () => + readFileSync(join(figmaCrate, 'plugin_api_manifest.json'), 'utf8'), + __DEVUP_TEXT_SEGMENT_MANIFEST__: () => + readFileSync(join(figmaCrate, 'text_segment_manifest.json'), 'utf8'), +} + +/** `fast_snapshot.js` → `fastSnapshot`. Rust 의 `BuiltinScript` 이름과 맞춘다. */ +const camel = (file) => + file.replace(/\.js$/, '').replace(/_([a-z])/g, (_, c) => c.toUpperCase()) + +/** 본문에 남은 미치환 플레이스홀더가 있으면 빌드를 세운다. */ +function assertNoLeftovers(file, body) { + const left = [...body.matchAll(/"__DEVUP_[A-Z_]+__"/g)] + .map((m) => m[0]) + // 마커 노드 id 는 리터럴로 남는 것이 정상이다. + .filter((token) => token !== '"__DEVUP_SNAPSHOT_CURSOR__"') + if (left.length > 0) { + throw new Error( + `${file}: 치환되지 않은 플레이스홀더 ${[...new Set(left)].join(', ')} — ` + + 'gen-scripts.mjs 의 PARAMS/INCLUDES 에 추가해야 한다', + ) + } +} + +function transform(file) { + let body = readFileSync(join(scriptDir, file), 'utf8') + for (const [token, read] of Object.entries(INCLUDES)) { + body = body.split(`"${token}"`).join(read()) + } + for (const [token, expr] of Object.entries(PARAMS)) { + body = body.split(`"${token}"`).join(expr) + } + assertNoLeftovers(file, body) + return body +} + +const files = readdirSync(scriptDir) + .filter((f) => f.endsWith('.js')) + // 단독 실행되지 않고 다른 스크립트에 인클루드되기만 한다. + .filter((f) => f !== 'large_value_helpers.js') + .sort() + +// 본문은 손대지 않은 JavaScript다. `.ts` 로 내보내면 검증된 스크립트가 strict +// 검사에 걸려 수백 건의 암시적 any 를 낸다 — 고칠 대상은 devup-mcp 의 원본이지 +// 이 생성물이 아니므로, 실제 언어대로 `.js` 로 내보내고 타입은 경계에만 둔다. +const banner = `// 자동 생성 파일 — 직접 고치지 마십시오. +// 원본: crates/devup-mcp-figma/src/scripts/*.js +// 생성: plugin/scripts/gen-scripts.mjs (\`npm run gen\`) +// +// 원격 경로(공식 Figma MCP)와 브리지 경로가 같은 스크립트를 쓰도록 빌드 타임에 +// 감싼 결과입니다. 원본을 고치면 이 파일은 다시 생성됩니다.` + +const js = `${banner} + +${files + .map( + (file) => `/** 원본: ${file} */ +export async function ${camel(file)}(p) { +${transform(file)} +}`, + ) + .join('\n\n')} + +export const SCRIPTS = { +${files.map((f) => ` ${camel(f)},`).join('\n')} +} +` + +const dts = `${banner} + +/** 스크립트가 읽는 값. Rust 의 \`ScriptInputs\` 와 자리를 맞춘다. */ +export interface ScriptParams { + nodeId: string + rootIds: string[] + snapshot: unknown + search: unknown + explore: unknown + resources: unknown + largeValue: unknown + asset: unknown + theme: unknown +} + +${files.map((f) => `export declare function ${camel(f)}(p: ScriptParams): Promise`).join('\n')} + +/** 스크립트 이름 → 실행 함수. devup-mcp 가 보내는 \`script\` 값이 키다. */ +export declare const SCRIPTS: Record< + string, + (p: ScriptParams) => Promise +> +` + +mkdirSync(dirname(outFile), { recursive: true }) +writeFileSync(`${outFile}.js`, js, 'utf8') +writeFileSync(`${outFile}.d.ts`, dts, 'utf8') +console.log(`generated ${files.length} scripts -> ${outFile}.{js,d.ts}`) diff --git a/plugin/scripts/inline-ui.mjs b/plugin/scripts/inline-ui.mjs new file mode 100644 index 0000000..5f700aa --- /dev/null +++ b/plugin/scripts/inline-ui.mjs @@ -0,0 +1,28 @@ +// Figma 의 UI iframe 은 `srcdoc` 으로 주입되므로 상대 경로 스크립트를 가져올 수 +// 없다. 번들이 `` 가 번들 문자열 안에 있으면 HTML 파서가 거기서 블록을 닫는다. +const safe = js.split('').join('<\\/script>') + +writeFileSync(htmlPath, html.replace(tag, ``), 'utf8') +rmSync(jsPath, { force: true }) +rmSync(`${jsPath}.map`, { force: true }) + +console.log(`inlined ${js.length} bytes into ui.html`) diff --git a/plugin/src/code.ts b/plugin/src/code.ts new file mode 100644 index 0000000..fa44df1 --- /dev/null +++ b/plugin/src/code.ts @@ -0,0 +1,114 @@ +// 플러그인 메인 스레드 — Figma Plugin API 를 쓸 수 있는 유일한 쪽. +// +// 하는 일은 하나다. UI(iframe)가 WebSocket 으로 받아 넘겨준 작업을 이름으로 찾아 +// 실행하고 결과를 돌려준다. 네트워크는 UI 만, 문서는 메인만 만질 수 있어서 +// 둘을 postMessage 로 잇는 구조가 강제된다. +// +// 읽기 전용이다. SCRIPTS 는 devup-mcp 의 읽기 스크립트에서 생성되며 문서를 +// 바꾸는 호출을 포함하지 않는다. + +import { SCRIPTS, type ScriptParams } from './generated/scripts' + +/** UI → 메인. devup-mcp 가 보낸 작업 그대로다. */ +interface JobMessage { + kind: 'devup-job' + requestId: string + script: string + params: Partial +} + +/** 메인 → UI. 성공이면 data, 실패면 error 중 하나만 채운다. */ +interface ResultMessage { + kind: 'devup-result' + requestId: string + data?: unknown + error?: string +} + +interface StatusMessage { + kind: 'devup-status' + fileKey: string | null + fileName: string + port: number +} + +const PORT = 1993 + +/** + * 스크립트가 읽는 자리를 모두 채운 파라미터. + * + * 빠진 값을 `undefined` 로 두면 스크립트가 `undefined.length` 같은 데서 죽어 + * 원인을 찾기 어려워진다. Rust 쪽 `ScriptInputs::default()` 와 같은 빈 값으로 + * 맞춰 둔다. + */ +function fillParams(partial: Partial): ScriptParams { + return { + nodeId: partial.nodeId ?? '', + rootIds: partial.rootIds ?? [], + snapshot: partial.snapshot ?? {}, + search: partial.search ?? {}, + explore: partial.explore ?? {}, + resources: partial.resources ?? { variableIds: [], styles: [] }, + largeValue: partial.largeValue ?? {}, + asset: partial.asset ?? {}, + theme: partial.theme ?? { offset: 0 }, + } +} + +async function runJob(job: JobMessage): Promise { + const script = SCRIPTS[job.script] + if (!script) { + // devup-mcp 가 이 플러그인보다 새 스크립트를 아는 상태. 어느 쪽이 오래됐는지 + // 알 수 있도록 아는 목록을 함께 돌려준다. + return { + kind: 'devup-result', + requestId: job.requestId, + error: `DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${job.script} (아는 스크립트: ${Object.keys(SCRIPTS).join(', ')})`, + } + } + try { + const data = await script(fillParams(job.params)) + return { kind: 'devup-result', requestId: job.requestId, data } + } catch (error) { + // 스크립트는 DEVUP_* 코드를 던진다. 그 문자열을 그대로 올려야 Rust 가 + // 분기할 수 있으므로 감싸지 않는다. + return { + kind: 'devup-result', + requestId: job.requestId, + error: error instanceof Error ? error.message : String(error), + } + } +} + +figma.showUI(__html__, { width: 320, height: 220 }) + +figma.ui.onmessage = (message: unknown) => { + if (typeof message !== 'object' || message === null) return + const msg = message as { kind?: string } + + if (msg.kind === 'devup-ready') { + const status: StatusMessage = { + kind: 'devup-status', + fileKey: figma.fileKey ?? null, + fileName: figma.root.name, + port: PORT, + } + figma.ui.postMessage(status) + return + } + + if (msg.kind === 'devup-job') { + const job = message as JobMessage + // 비동기 결과는 콜백에서 보낸다. onmessage 를 async 로 만들면 Figma 가 + // 반환값을 기다리지 않아 예외가 조용히 사라진다. + runJob(job).then( + (result) => figma.ui.postMessage(result), + (error: unknown) => + figma.ui.postMessage({ + kind: 'devup-result', + requestId: job.requestId, + error: error instanceof Error ? error.message : String(error), + } satisfies ResultMessage), + ) + } +} diff --git a/plugin/src/ui.html b/plugin/src/ui.html new file mode 100644 index 0000000..98b07fa --- /dev/null +++ b/plugin/src/ui.html @@ -0,0 +1,71 @@ + + + + + + + +

Devup Bridge

+

이 창을 열어 두면 devup-mcp 가 이 파일을 직접 읽습니다.

+ +
+ + 시작하는 중… +
+
+ +

+ 읽기 전용입니다. 문서를 바꾸지 않고, 데이터는 같은 기기의 devup-mcp 로만 + 나갑니다. +

+ + diff --git a/plugin/src/ui.ts b/plugin/src/ui.ts new file mode 100644 index 0000000..b795fa4 --- /dev/null +++ b/plugin/src/ui.ts @@ -0,0 +1,112 @@ +// 플러그인 UI(iframe) — 네트워크를 쓸 수 있는 유일한 쪽. +// +// devup-mcp 가 연 로컬 WebSocket 에 붙어, 들어온 작업을 메인 스레드로 넘기고 +// 결과를 돌려보낸다. 문서는 만지지 않는다(만질 수도 없다). +// +// 연결은 끊어지는 것이 정상이다 — devup-mcp 는 MCP 클라이언트가 뜰 때마다 새로 +// 시작한다. 그래서 실패를 예외가 아니라 상태로 다루고 계속 재시도한다. + +interface StatusMessage { + kind: 'devup-status' + fileKey: string | null + fileName: string + port: number +} + +interface ResultMessage { + kind: 'devup-result' + requestId: string + data?: unknown + error?: string +} + +const RETRY_MS = 2000 + +let socket: WebSocket | null = null +// `status` 는 window 전역(문자열)과 겹친다. +let bridgeStatus: StatusMessage | null = null +let served = 0 + +const dot = document.getElementById('dot') as HTMLElement +const line = document.getElementById('line') as HTMLElement +const detail = document.getElementById('detail') as HTMLElement + +function render(state: 'on' | 'off', text: string) { + dot.className = `dot ${state}` + line.textContent = text + detail.textContent = bridgeStatus + ? `${bridgeStatus.fileName} · 처리 ${served}건` + : '파일 정보를 읽는 중…' +} + +function connect() { + if (!bridgeStatus) return + const url = `ws://127.0.0.1:${bridgeStatus.port}/plugin` + let ws: WebSocket + try { + ws = new WebSocket(url) + } catch { + render('off', 'devup-mcp 를 찾지 못했습니다') + setTimeout(connect, RETRY_MS) + return + } + socket = ws + + ws.onopen = () => { + render('on', 'devup-mcp 에 연결됨') + // 어느 파일인지 먼저 알려야 devup-mcp 가 요청을 이 소켓으로 보낼 수 있다. + ws.send( + JSON.stringify({ + kind: 'hello', + fileKey: bridgeStatus?.fileKey ?? null, + fileName: bridgeStatus?.fileName ?? '', + }), + ) + } + + ws.onmessage = (event) => { + let job: unknown + try { + job = JSON.parse(String(event.data)) + } catch { + return + } + // 작업은 그대로 메인 스레드로 넘긴다. UI 는 내용을 해석하지 않는다. + parent.postMessage({ pluginMessage: job }, '*') + } + + ws.onclose = () => { + socket = null + render('off', 'devup-mcp 연결 대기 중…') + setTimeout(connect, RETRY_MS) + } + + // onclose 가 뒤따르므로 여기서 재시도를 걸면 두 번 걸린다. + ws.onerror = () => {} +} + +window.onmessage = (event: MessageEvent) => { + const msg = event.data?.pluginMessage + if (!msg || typeof msg !== 'object') return + + if (msg.kind === 'devup-status') { + bridgeStatus = msg as StatusMessage + render('off', 'devup-mcp 연결 대기 중…') + connect() + return + } + + if (msg.kind === 'devup-result') { + const result = msg as ResultMessage + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(result)) + served += 1 + render('on', 'devup-mcp 에 연결됨') + } + // 소켓이 끊긴 사이의 결과는 버린다. devup-mcp 가 그 요청을 이미 타임아웃 + // 처리했고, 재연결 후 다시 보낼 것이다. + } +} + +// 메인 스레드에 파일 정보를 요청하는 것으로 시작한다. +parent.postMessage({ pluginMessage: { kind: 'devup-ready' } }, '*') diff --git a/plugin/tsconfig.json b/plugin/tsconfig.json new file mode 100644 index 0000000..44b551d --- /dev/null +++ b/plugin/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "types": ["@figma/plugin-typings"] + }, + "include": ["src/**/*.ts"] +} From bbc022aa7f54958d9801eaf26c2080865e30e1ab Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 10:07:24 +0900 Subject: [PATCH 02/16] feat: read Figma through the bridge plugin before the metered path The script reads that do the collecting all route to the official MCP's use_figma, which is what the rate limit applies to. They only call documented Plugin API, so the plugin we ship can run them against the open file for nothing. The plugin cannot accept connections, so devup-mcp listens and the plugin dials in. Binding is synchronous because the site that builds the upstream is; only the serving loop is spawned. A taken port or no runtime means no bridge, not an error - other MCP clients on the same machine are normal, and the remote path still works. Routing is decided before the call, not after a failure. Capability and transport failures fold to one ErrorCode, so an error alone cannot tell "the bridge does not do this" from "the bridge tried and failed", and retrying the second on the metered path would spend what was saved. Results are wrapped exactly as use_figma returns them. Decoders hunt for JSON inside content[].text, so a different shape would pass some and fail others, splitting the two paths screen by screen. --- Cargo.lock | 114 ++++++- crates/devup-mcp-figma/Cargo.toml | 3 +- crates/devup-mcp-figma/src/bridge.rs | 403 +++++++++++++++++++++++++ crates/devup-mcp-figma/src/lib.rs | 5 + crates/devup-mcp-figma/src/upstream.rs | 137 +++++++++ crates/devup-mcp/src/server/mod.rs | 29 +- 6 files changed, 679 insertions(+), 12 deletions(-) create mode 100644 crates/devup-mcp-figma/src/bridge.rs diff --git a/Cargo.lock b/Cargo.lock index 2fb1d9c..72b6f51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", + "base64 0.22.1", "bytes", "form_urlencoded", "futures-util", @@ -257,8 +258,10 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", + "sha1", "sync_wrapper", "tokio", + "tokio-tungstenite", "tower", "tower-layer", "tower-service", @@ -682,6 +685,12 @@ dependencies = [ "syn 3.0.4", ] +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "devup-mcp" version = "0.4.5" @@ -1081,6 +1090,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -1090,7 +1111,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", "wasm-bindgen", ] @@ -2346,6 +2367,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -2359,10 +2386,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.2" @@ -2384,6 +2421,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -2393,6 +2440,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.1" @@ -2883,6 +2939,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3221,6 +3288,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.19" @@ -3379,6 +3458,22 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "sha1", + "thiserror 2.0.20", +] + [[package]] name = "typenum" version = "1.20.1" @@ -3509,6 +3604,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.127" @@ -3887,6 +3991,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.4" diff --git a/crates/devup-mcp-figma/Cargo.toml b/crates/devup-mcp-figma/Cargo.toml index 84b95f9..4d24e6e 100644 --- a/crates/devup-mcp-figma/Cargo.toml +++ b/crates/devup-mcp-figma/Cargo.toml @@ -9,7 +9,8 @@ repository.workspace = true [dependencies] async-trait.workspace = true -axum.workspace = true +# `ws` 는 기본 기능이 아니다. 브리지가 플러그인을 받는 데 쓴다. +axum = { workspace = true, features = ["ws"] } base64.workspace = true image.workspace = true keyring.workspace = true diff --git a/crates/devup-mcp-figma/src/bridge.rs b/crates/devup-mcp-figma/src/bridge.rs new file mode 100644 index 0000000..35e0eb9 --- /dev/null +++ b/crates/devup-mcp-figma/src/bridge.rs @@ -0,0 +1,403 @@ +//! 열려 있는 Figma 파일을 우리 플러그인으로 직접 읽는 전송 계층. +//! +//! 공식 MCP 경로(`RemoteFigmaClient`)는 읽기 스크립트를 `use_figma` 로 보내는데, +//! 그 도구가 요금 한도를 쓴다. 스크립트가 건드리는 것은 문서화된 Plugin API 뿐이라 +//! 우리가 돌리는 플러그인이 같은 일을 할 수 있고, 그 경로에는 한도가 없다. +//! +//! 방향이 거꾸로인 점이 설계를 결정한다. Figma 플러그인은 들어오는 연결을 받지 +//! 못하고 나가는 WebSocket 만 열 수 있어서, **서버는 이쪽**이고 플러그인이 붙는다. +//! +//! 응답은 원격이 주는 모양 그대로 감싼다. 디코더들은 `content[].text` 안의 JSON +//! 을 찾도록 쓰여 있으므로, 여기서 모양을 바꾸면 두 경로가 조용히 갈라진다. + +use std::{ + collections::HashMap, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, + time::Duration, +}; + +use async_trait::async_trait; +use axum::{ + Router, + extract::{ + State, + ws::{Message, WebSocket, WebSocketUpgrade}, + }, + response::Response, + routing::any, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use tokio::{ + net::TcpListener, + sync::{Mutex, mpsc, oneshot}, + time::timeout, +}; + +use crate::{ + errors::{DevupError, ErrorCode}, + upstream::{FigmaUpstream, ReadToolCall, UpstreamResult}, +}; + +/// 플러그인 manifest 의 `allowedDomains` 와 같은 값이어야 한다. 바꾸려면 양쪽을 +/// 함께 고쳐야 하며, 플러그인은 재빌드·재설치가 필요하다. +pub const DEFAULT_BRIDGE_PORT: u16 = 1993; + +/// 한 번의 읽기에 허용하는 시간. 스크립트는 문서를 한 번 훑는 정도라 원격보다 +/// 훨씬 짧아도 되지만, 아주 큰 페이지의 첫 스냅샷은 몇 초가 걸린다. +const JOB_TIMEOUT: Duration = Duration::from_secs(90); + +/// 플러그인이 붙을 때 보내는 첫 메시지. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Hello { + #[serde(default)] + file_key: Option, +} + +/// 플러그인이 작업을 마치고 보내는 메시지. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PluginResult { + request_id: String, + #[serde(default)] + data: Option, + #[serde(default)] + error: Option, +} + +/// 서버가 플러그인에 보내는 작업. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct Job { + kind: &'static str, + request_id: String, + script: &'static str, + params: Value, +} + +/// 브리지가 맡을 수 있는 읽기 하나. `ReadToolCall::bridge_job` 이 만든다. +#[derive(Debug, Clone, PartialEq)] +pub struct BridgeJob { + /// 플러그인이 아는 이름. 코드젠이 파일명에서 만든 것과 같아야 한다. + pub script: &'static str, + /// 스크립트가 읽는 값. Rust 의 치환 자리와 1:1 이다. + pub params: Value, +} + +struct Connected { + outbox: mpsc::UnboundedSender, +} + +#[derive(Default)] +struct Inner { + /// fileKey → 그 파일을 열어 둔 플러그인. 같은 파일을 두 창에서 열면 나중에 + /// 붙은 쪽이 이긴다. 둘 다 같은 문서를 보므로 어느 쪽이든 답은 같다. + plugins: HashMap, + /// requestId → 결과를 기다리는 쪽. + pending: HashMap>, +} + +/// 플러그인 연결과 대기 중인 요청을 함께 들고 있는 공유 상태. +#[derive(Clone, Default)] +pub struct BridgeState { + inner: Arc>, + counter: Arc, +} + +fn unavailable(message: impl Into) -> DevupError { + DevupError::new(ErrorCode::DevupFigmaDirectUnavailable, message, false) +} + +impl BridgeState { + fn next_request_id(&self) -> String { + format!("job-{}", self.counter.fetch_add(1, Ordering::Relaxed)) + } + + /// 이 파일을 열어 둔 플러그인이 있는지. + pub async fn has_plugin(&self, file_key: &str) -> bool { + self.inner.lock().await.plugins.contains_key(file_key) + } + + /// 붙어 있는 파일 키 목록. 진단용. + pub async fn connected_files(&self) -> Vec { + let mut keys: Vec = self.inner.lock().await.plugins.keys().cloned().collect(); + keys.sort(); + keys + } + + async fn dispatch( + &self, + file_key: &str, + script: &'static str, + params: Value, + ) -> Result { + let request_id = self.next_request_id(); + let (tx, rx) = oneshot::channel(); + + { + let mut inner = self.inner.lock().await; + let Some(plugin) = inner.plugins.get(file_key) else { + return Err(unavailable(format!( + "no Devup Bridge plugin is open for file {file_key}" + ))); + }; + let job = Job { + kind: "devup-job", + request_id: request_id.clone(), + script, + params, + }; + let encoded = serde_json::to_string(&job) + .map_err(|error| unavailable(format!("bridge job encode failed: {error}")))?; + if plugin.outbox.send(encoded).is_err() { + // 소켓이 막 닫혔다. 등록을 지워 다음 호출이 곧장 폴백하도록 한다. + inner.plugins.remove(file_key); + return Err(unavailable(format!( + "the Devup Bridge plugin for file {file_key} disconnected" + ))); + } + inner.pending.insert(request_id.clone(), tx); + } + + let received = timeout(JOB_TIMEOUT, rx).await; + // 성공이든 실패든 대기표는 반드시 걷는다. 남겨 두면 연결이 오래 살아 있는 + // 동안 계속 쌓인다. + self.inner.lock().await.pending.remove(&request_id); + + match received { + Ok(Ok(result)) => match (result.data, result.error) { + // 스크립트가 던진 DEVUP_* 코드는 그대로 올린다. 원격 경로와 같은 + // 문자열이어야 위쪽 분기가 동일하게 동작한다. + (_, Some(message)) => Err(DevupError::new( + ErrorCode::DevupFigmaDirectUnavailable, + message, + false, + )), + (Some(data), None) => Ok(data), + (None, None) => Err(unavailable("bridge returned neither data nor error")), + }, + // 플러그인 창이 닫혔다. + Ok(Err(_)) => Err(unavailable("the Devup Bridge plugin disconnected mid-read")), + Err(_) => Err(unavailable( + "the Devup Bridge plugin did not answer in time", + )), + } + } +} + +/// 플러그인이 돌려준 값을 원격 `use_figma` 가 주는 모양으로 감싼다. +/// +/// 디코더들은 `content[].text` 안의 JSON 문자열을 찾도록 쓰여 있다. 값을 그대로 +/// 올리면 일부 디코더는 통과하고 일부는 실패해, 두 경로가 화면 단위로 갈라진다. +fn wrap_as_tool_result(data: &Value) -> Result { + let text = serde_json::to_string(data) + .map_err(|error| unavailable(format!("bridge result encode failed: {error}")))?; + Ok(json!({ "content": [{ "type": "text", "text": text }], "isError": false })) +} + +async fn plugin_socket(State(state): State, upgrade: WebSocketUpgrade) -> Response { + upgrade.on_upgrade(move |socket| handle_plugin(state, socket)) +} + +async fn handle_plugin(state: BridgeState, mut socket: WebSocket) { + let (outbox, mut outbox_rx) = mpsc::unbounded_channel::(); + let mut registered: Option = None; + + // 보내기와 받기를 한 루프에서 번갈아 본다. 소켓을 쪼개려면 Stream/Sink 트레이트 + // 의존성이 필요한데, 그것을 들이는 것보다 select 가 싸다. + loop { + tokio::select! { + outgoing = outbox_rx.recv() => { + let Some(text) = outgoing else { break }; + if socket.send(Message::Text(text.into())).await.is_err() { + break; + } + } + incoming = socket.recv() => { + let Some(Ok(message)) = incoming else { break }; + let Message::Text(text) = message else { continue }; + let Ok(value) = serde_json::from_str::(&text) else { continue }; + + match value.get("kind").and_then(Value::as_str) { + Some("hello") => { + let file_key = serde_json::from_value::(value) + .ok() + .and_then(|hello| hello.file_key) + .filter(|key| !key.is_empty()); + // fileKey 가 없는 파일(저장 전 초안)은 요청 대상이 될 수 없다. + if let Some(key) = file_key { + state.inner.lock().await.plugins.insert( + key.clone(), + Connected { outbox: outbox.clone() }, + ); + registered = Some(key); + } + } + Some("devup-result") => { + let Ok(result) = serde_json::from_value::(value) else { + continue; + }; + let waiting = state.inner.lock().await.pending.remove(&result.request_id); + // 받는 쪽이 이미 타임아웃했으면 버린다. + if let Some(waiting) = waiting { + let _ = waiting.send(result); + } + } + _ => {} + } + } + } + } + + if let Some(key) = registered { + state.inner.lock().await.plugins.remove(&key); + } +} + +/// 브리지 서버. 포트를 잡지 못하면 열지 않으며, 그 경우 호출자는 원격 경로만 쓴다. +pub struct BridgeServer { + state: BridgeState, +} + +impl BridgeServer { + /// 포트를 잡아 서버를 띄운다. + /// + /// 이미 다른 devup-mcp 가 같은 포트를 쓰고 있으면 `None` 을 준다. 한 대의 + /// 기기에서 MCP 클라이언트를 여러 개 띄우는 것은 정상이므로, 이는 오류가 + /// 아니라 "이 프로세스는 브리지를 쓰지 않는다"는 뜻이다. + /// + /// 서버가 만들어지는 자리가 동기 함수라 bind 도 동기로 한다 — 듣기 소켓을 + /// 여는 것은 어차피 기다리지 않는 일이고, 기다리는 부분만 따로 띄운다. + pub fn start(port: u16) -> Option { + let listener = std::net::TcpListener::bind(("127.0.0.1", port)).ok()?; + listener.set_nonblocking(true).ok()?; + // 런타임 밖에서 만들어졌다면 붙일 곳이 없다. 그때도 원격 경로는 멀쩡하다. + let runtime = tokio::runtime::Handle::try_current().ok()?; + + let state = BridgeState::default(); + let app = Router::new() + .route("/plugin", any(plugin_socket)) + .with_state(state.clone()); + runtime.spawn(async move { + let Ok(listener) = TcpListener::from_std(listener) else { + return; + }; + let _ = axum::serve(listener, app).await; + }); + Some(Self { state }) + } + + /// 환경 설정을 읽어 띄운다. + /// + /// `DEVUP_FIGMA_BRIDGE_PORT` 가 포트를 정하며, `0` 이나 `off` 는 끈다. 값이 + /// 없으면 기본 포트로 켠다 — 플러그인이 안 떠 있으면 어차피 원격으로 가므로 + /// 켜 두는 쪽이 손해가 없다. + pub fn from_env() -> Option { + let setting = std::env::var("DEVUP_FIGMA_BRIDGE_PORT").ok(); + let port = match setting.as_deref().map(str::trim) { + None | Some("") => DEFAULT_BRIDGE_PORT, + Some("off") | Some("0") => return None, + Some(value) => value.parse().ok()?, + }; + Self::start(port) + } + + pub fn state(&self) -> BridgeState { + self.state.clone() + } +} + +/// 이 요청을 맡을 수 있는지 **부르기 전에** 답하는 상류. +/// +/// 실패한 뒤에 폴백하면 안 되기 때문에 따로 둔다. `CapabilityUnavailable` 과 +/// `Transport` 는 같은 `ErrorCode` 로 접히므로, 에러만 보고는 "브리지가 못 하는 +/// 일"과 "브리지가 하다가 실패한 일"을 구분할 수 없다. 뒤엣것까지 원격으로 넘기면 +/// 아끼려던 요금을 그대로 쓰게 된다. +#[async_trait] +pub trait PreferredUpstream: FigmaUpstream { + async fn can_serve(&self, call: &ReadToolCall) -> bool; +} + +/// 플러그인을 통해 Figma 를 읽는 `FigmaUpstream`. +#[derive(Clone)] +pub struct BridgeFigmaClient { + state: BridgeState, +} + +impl BridgeFigmaClient { + pub fn new(state: BridgeState) -> Self { + Self { state } + } +} + +#[async_trait] +impl FigmaUpstream for BridgeFigmaClient { + async fn list_tools(&self) -> Result, DevupError> { + // 스크립트 경로만 대신하므로 광고하는 것도 그 하나다. + Ok(vec!["use_figma".to_owned()]) + } + + async fn call_read_tool(&self, call: ReadToolCall) -> Result { + let Some(job) = call.bridge_job() else { + // 공식 도구 이름으로 가는 읽기(get_metadata 등)는 응답 모양이 달라 + // 여기서 흉내 내지 않는다. + return Err(unavailable("the bridge serves script reads only")); + }; + let data = self + .state + .dispatch(call.file_key(), job.script, job.params) + .await?; + Ok(UpstreamResult { + raw: wrap_as_tool_result(&data)?, + }) + } +} + +#[async_trait] +impl PreferredUpstream for BridgeFigmaClient { + async fn can_serve(&self, call: &ReadToolCall) -> bool { + call.bridge_job().is_some() && self.state.has_plugin(call.file_key()).await + } +} + +/// 브리지가 맡을 수 있으면 브리지로, 아니면 원격으로 보낸다. +/// +/// 판정은 부르기 전에 끝난다. 브리지가 맡은 요청이 실패하면 그 실패를 그대로 +/// 올린다 — 노드가 없거나 봉투가 너무 큰 것은 원격에서도 같은 결과이고, 다시 +/// 물으면 아낀 요금을 도로 쓰게 된다. +pub struct FallbackUpstream { + preferred: P, + secondary: S, +} + +impl FallbackUpstream { + pub fn new(preferred: P, secondary: S) -> Self { + Self { + preferred, + secondary, + } + } +} + +#[async_trait] +impl FigmaUpstream for FallbackUpstream +where + P: PreferredUpstream, + S: FigmaUpstream, +{ + async fn list_tools(&self) -> Result, DevupError> { + // 능력 목록은 원격이 정본이다. 브리지는 그중 하나를 대신할 뿐이다. + self.secondary.list_tools().await + } + + async fn call_read_tool(&self, call: ReadToolCall) -> Result { + if self.preferred.can_serve(&call).await { + return self.preferred.call_read_tool(call).await; + } + self.secondary.call_read_tool(call).await + } +} diff --git a/crates/devup-mcp-figma/src/lib.rs b/crates/devup-mcp-figma/src/lib.rs index e48a3bd..757ae3d 100644 --- a/crates/devup-mcp-figma/src/lib.rs +++ b/crates/devup-mcp-figma/src/lib.rs @@ -1,4 +1,5 @@ mod assets; +mod bridge; mod collector; mod credentials; mod envelope; @@ -21,6 +22,10 @@ pub use collector::{ CollectorStep, PlannedCall, ReferencePng, ScreenFailure, SectionReadOptions, }; +pub use bridge::{ + BridgeFigmaClient, BridgeJob, BridgeServer, BridgeState, DEFAULT_BRIDGE_PORT, FallbackUpstream, + PreferredUpstream, +}; pub use credentials::{ ClientCredentialStore, ClientCredentials, CredentialStore, KeyringClientCredentialStore, KeyringCredentialStore, MemoryClientCredentialStore, MemoryCredentialStore, diff --git a/crates/devup-mcp-figma/src/upstream.rs b/crates/devup-mcp-figma/src/upstream.rs index 62a5a47..b1c2199 100644 --- a/crates/devup-mcp-figma/src/upstream.rs +++ b/crates/devup-mcp-figma/src/upstream.rs @@ -886,6 +886,143 @@ fn upstream_timeout_error(context: UpstreamFailureContext) -> DevupError { error } +impl BuiltinScript { + /// 플러그인이 아는 이름. + /// + /// 코드젠이 파일명에서 만든 것과 같아야 한다(`fast_snapshot.js` → + /// `fastSnapshot`). 어긋나면 플러그인이 `DEVUP_BRIDGE_UNKNOWN_SCRIPT` 를 + /// 돌려주며 아는 이름을 함께 알려 준다. + pub fn plugin_name(self) -> &'static str { + match self { + Self::NodeSnapshot => "snapshot", + // 두 변형이 같은 원본을 쓴다. 나뉘는 곳은 넘기는 rootIds 뿐이다. + Self::FastSnapshotEnvelope | Self::MultiRootSnapshotEnvelope => "fastSnapshot", + Self::FastThemeEnvelope => "fastTheme", + Self::PageCatalog => "pageCatalog", + Self::SearchSnapshot => "search", + Self::VariableCatalog => "variableCatalog", + Self::LocalVariables => "variables", + Self::UsedResources => "usedResources", + Self::ExploreSnapshot => "explore", + Self::SectionIndex => "sectionIndex", + Self::LargeValue => "largeValue", + Self::AssetExport => "assets", + } + } +} + +impl ReadToolCall { + /// 이 읽기가 향하는 파일. + pub fn file_key(&self) -> &str { + match self { + Self::Metadata { file_key, .. } + | Self::VariableDefs { file_key, .. } + | Self::DesignContext { file_key, .. } + | Self::CodeConnectMap { file_key, .. } + | Self::Screenshot { file_key, .. } + | Self::Snapshot { file_key, .. } + | Self::SearchSnapshot { file_key, .. } + | Self::PageCatalog { file_key } + | Self::ExploreSnapshot { file_key, .. } + | Self::FastTheme { file_key, .. } + | Self::LargeValue { file_key, .. } + | Self::AssetExport { file_key, .. } => file_key, + } + } + + /// 브리지가 대신할 수 있는 읽기면 스크립트 이름과 값을, 아니면 `None`. + /// + /// 값이 놓이는 자리는 [`BuiltinScript::source`] 의 치환 자리와 1:1 이다. 한쪽만 + /// 고치면 브리지 경로에서만 빈 값이 들어가 조용히 다른 결과가 나온다. + /// + /// 공식 도구 이름으로 가는 읽기(`get_metadata` 등)는 `None` 이다. 플러그인이 + /// 같은 정보를 만들 수는 있지만 응답 모양이 달라, 흉내 내면 디코더가 두 경로 + /// 에서 다르게 동작한다. + pub fn bridge_job(&self) -> Option { + use crate::bridge::BridgeJob; + + let job = match self { + Self::Metadata { .. } + | Self::VariableDefs { .. } + | Self::DesignContext { .. } + | Self::CodeConnectMap { .. } + | Self::Screenshot { .. } => return None, + + Self::Snapshot { + node_id, + script, + resources, + snapshot, + root_ids, + .. + } => BridgeJob { + script: script.plugin_name(), + params: json!({ + "nodeId": node_id, + // `source` 와 같은 기본값. 단일 루트 읽기는 대상 노드 하나다. + "rootIds": root_ids.clone() + .unwrap_or_else(|| vec![node_id.clone()]), + "snapshot": snapshot.clone().unwrap_or_default(), + // `source` 의 `empty_resources` 와 같은 빈 값. 스크립트는 두 + // 배열이 있다고 전제하므로 없는 채로 두면 거기서 죽는다. + "resources": resources.clone().unwrap_or(ResourceBatch { + variable_ids: Vec::new(), + styles: Vec::new(), + }), + }), + }, + + Self::SearchSnapshot { + node_id, options, .. + } => BridgeJob { + script: BuiltinScript::SearchSnapshot.plugin_name(), + params: json!({ "nodeId": node_id, "search": options }), + }, + + Self::PageCatalog { .. } => BridgeJob { + script: BuiltinScript::PageCatalog.plugin_name(), + params: json!({}), + }, + + Self::ExploreSnapshot { + node_id, options, .. + } => BridgeJob { + script: BuiltinScript::ExploreSnapshot.plugin_name(), + params: json!({ "nodeId": node_id, "explore": options }), + }, + + Self::FastTheme { offset, .. } => BridgeJob { + script: BuiltinScript::FastThemeEnvelope.plugin_name(), + params: json!({ "theme": { "offset": offset } }), + }, + + Self::LargeValue { options, .. } => BridgeJob { + script: BuiltinScript::LargeValue.plugin_name(), + params: json!({ "nodeId": options.node_id, "largeValue": options }), + }, + + Self::AssetExport { + version, request, .. + } => BridgeJob { + script: BuiltinScript::AssetExport.plugin_name(), + params: json!({ + "nodeId": request.node_id, + "asset": { + "assetId": request.asset_id, + "nodeId": request.node_id, + "field": request.field, + "imageHash": request.image_hash, + "format": request.format, + "scale": request.scale, + "version": version, + }, + }), + }, + }; + Some(job) + } +} + #[cfg(test)] mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; diff --git a/crates/devup-mcp/src/server/mod.rs b/crates/devup-mcp/src/server/mod.rs index 0902b7a..909a49e 100644 --- a/crates/devup-mcp/src/server/mod.rs +++ b/crates/devup-mcp/src/server/mod.rs @@ -40,13 +40,14 @@ use serde_json::{Value, json}; use devup_mcp_devup_ui::theme::ThemeScope; use devup_mcp_figma::{ - AuthStatus, ClientCredentialSource, ClientCredentials, CollectedParts, CollectedPayload, - CollectionRequest, CollectionScope, CollectorSession, CollectorStep, CredentialStore, - DEFAULT_CLIENT_NAME, DevupError, DirectPathSnapshot, ErrorCode, ExploreCandidate, ExploreKind, - ExploreNode, ExploreReadOptions, FigmaTarget, FigmaUpstream, KeyringClientCredentialStore, - KeyringCredentialStore, OAuthManager, ReadToolCall, RemoteFigmaClient, ResourceScope, - SearchReadOptions, SecretString, SectionCandidate, SectionIndex, SectionReadOptions, Snapshot, - SystemBrowser, TokenState, UpstreamResult, + AuthStatus, BridgeFigmaClient, BridgeServer, ClientCredentialSource, ClientCredentials, + CollectedParts, CollectedPayload, CollectionRequest, CollectionScope, CollectorSession, + CollectorStep, CredentialStore, DEFAULT_CLIENT_NAME, DevupError, DirectPathSnapshot, ErrorCode, + ExploreCandidate, ExploreKind, ExploreNode, ExploreReadOptions, FallbackUpstream, FigmaTarget, + FigmaUpstream, KeyringClientCredentialStore, KeyringCredentialStore, OAuthManager, + ReadToolCall, RemoteFigmaClient, ResourceScope, SearchReadOptions, SecretString, + SectionCandidate, SectionIndex, SectionReadOptions, Snapshot, SystemBrowser, TokenState, + UpstreamResult, }; use artifacts::{ArtifactKind, ArtifactRequestKey, ArtifactStore}; @@ -271,8 +272,18 @@ impl Services { figma_direct.credential_source, ); } - let upstream = RemoteFigmaClient::new(oauth.clone()); - Ok(Self::new(Arc::new(oauth), Arc::new(upstream))) + let remote = RemoteFigmaClient::new(oauth.clone()); + // A script read goes to the bridge when one is listening. With no + // plugin attached every call falls straight through to the remote + // path, so opening the door costs nothing when nobody walks through. + let upstream: Arc = match BridgeServer::from_env() { + Some(bridge) => Arc::new(FallbackUpstream::new( + BridgeFigmaClient::new(bridge.state()), + remote, + )), + None => Arc::new(remote), + }; + Ok(Self::new(Arc::new(oauth), upstream)) } } From c5acc81d6859de11140e6f6f666ab4ad6a8189eb Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 10:11:38 +0900 Subject: [PATCH 03/16] test: drive the bridge transport over a real socket Figma itself cannot be started here, so a WebSocket client stands in for the plugin. Everything on this side of it is covered: registration, the script name and parameter mapping, request correlation, the result envelope, and the routing decision. Binding port 0 lets the kernel pick, so the tests do not fight the default port or each other; BridgeServer now reports what it bound. The routing test is the one that matters. It pins that a read is not handed to the bridge before a plugin for that file is connected, that official-tool reads stay on the remote path even when one is, and that another file's plugin does not answer for this one. --- Cargo.lock | 41 +++- crates/devup-mcp-figma/Cargo.toml | 4 + crates/devup-mcp-figma/src/bridge.rs | 10 +- .../devup-mcp-figma/tests/bridge_transport.rs | 202 ++++++++++++++++++ 4 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 crates/devup-mcp-figma/tests/bridge_transport.rs diff --git a/Cargo.lock b/Cargo.lock index 72b6f51..23af185 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,7 +261,7 @@ dependencies = [ "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "tower", "tower-layer", "tower-service", @@ -745,6 +745,7 @@ dependencies = [ "async-trait", "axum", "base64 0.23.1", + "futures-util", "image", "keyring", "quick-xml", @@ -757,6 +758,7 @@ dependencies = [ "subtle", "tempfile", "tokio", + "tokio-tungstenite 0.28.0", "unicode-normalization", "url", "webbrowser", @@ -3288,6 +3290,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.28.0", +] + [[package]] name = "tokio-tungstenite" version = "0.29.0" @@ -3297,7 +3311,7 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.29.0", ] [[package]] @@ -3458,6 +3472,23 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "sha1", + "thiserror 2.0.20", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.29.0" @@ -3549,6 +3580,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" diff --git a/crates/devup-mcp-figma/Cargo.toml b/crates/devup-mcp-figma/Cargo.toml index 4d24e6e..55c2025 100644 --- a/crates/devup-mcp-figma/Cargo.toml +++ b/crates/devup-mcp-figma/Cargo.toml @@ -30,3 +30,7 @@ webbrowser.workspace = true [dev-dependencies] anyhow.workspace = true tempfile = "3" +# 브리지 서버에 붙는 가짜 플러그인 역할. axum 의 `ws` 가 이미 끌어오는 것과 같은 +# 구현이라 의존성 트리가 늘지 않는다. +tokio-tungstenite = "0.28" +futures-util = "0.3" diff --git a/crates/devup-mcp-figma/src/bridge.rs b/crates/devup-mcp-figma/src/bridge.rs index 35e0eb9..f5d2616 100644 --- a/crates/devup-mcp-figma/src/bridge.rs +++ b/crates/devup-mcp-figma/src/bridge.rs @@ -261,6 +261,7 @@ async fn handle_plugin(state: BridgeState, mut socket: WebSocket) { /// 브리지 서버. 포트를 잡지 못하면 열지 않으며, 그 경우 호출자는 원격 경로만 쓴다. pub struct BridgeServer { state: BridgeState, + port: u16, } impl BridgeServer { @@ -275,6 +276,8 @@ impl BridgeServer { pub fn start(port: u16) -> Option { let listener = std::net::TcpListener::bind(("127.0.0.1", port)).ok()?; listener.set_nonblocking(true).ok()?; + // 0 을 주면 커널이 빈 포트를 고른다. 실제로 잡힌 번호를 알아야 붙을 수 있다. + let bound = listener.local_addr().ok()?.port(); // 런타임 밖에서 만들어졌다면 붙일 곳이 없다. 그때도 원격 경로는 멀쩡하다. let runtime = tokio::runtime::Handle::try_current().ok()?; @@ -288,7 +291,7 @@ impl BridgeServer { }; let _ = axum::serve(listener, app).await; }); - Some(Self { state }) + Some(Self { state, port: bound }) } /// 환경 설정을 읽어 띄운다. @@ -309,6 +312,11 @@ impl BridgeServer { pub fn state(&self) -> BridgeState { self.state.clone() } + + /// 실제로 잡은 포트. `start(0)` 으로 띄웠을 때 어디에 붙어야 하는지 알려 준다. + pub fn port(&self) -> u16 { + self.port + } } /// 이 요청을 맡을 수 있는지 **부르기 전에** 답하는 상류. diff --git a/crates/devup-mcp-figma/tests/bridge_transport.rs b/crates/devup-mcp-figma/tests/bridge_transport.rs new file mode 100644 index 0000000..728fd42 --- /dev/null +++ b/crates/devup-mcp-figma/tests/bridge_transport.rs @@ -0,0 +1,202 @@ +//! 브리지 전송 계층을 진짜 소켓으로 통과시킨다. +//! +//! Figma 앱 자체는 여기서 띄울 수 없으므로 플러그인 자리에 WebSocket 클라이언트를 +//! 세운다. 그 너머(스크립트 실행)는 플러그인 빌드가 책임지고, 여기서는 그 앞의 +//! 모든 것 — 등록, 이름·값 매핑, 상관, 봉투 모양, 라우팅 판정 — 을 확인한다. + +use devup_mcp_figma::{ + BridgeFigmaClient, BridgeServer, FigmaUpstream, PreferredUpstream, ReadToolCall, +}; +use futures_util::{SinkExt, StreamExt}; +use serde_json::{Value, json}; +use tokio::net::TcpStream; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async, tungstenite::Message}; + +const FILE_KEY: &str = "FileKey123"; + +type Plugin = WebSocketStream>; + +/// 플러그인 흉내: 붙어서 자기 파일을 알리고, 등록될 때까지 기다린다. +async fn connect_plugin(server: &BridgeServer) -> Plugin { + let (mut socket, _) = connect_async(format!("ws://127.0.0.1:{}/plugin", server.port())) + .await + .expect("bridge accepts a plugin"); + socket + .send(Message::Text( + json!({ "kind": "hello", "fileKey": FILE_KEY }) + .to_string() + .into(), + )) + .await + .expect("hello is sent"); + + // 등록은 서버 쪽 태스크에서 일어난다. 보낸 직후에는 아직일 수 있다. + let state = server.state(); + for _ in 0..200 { + if state.has_plugin(FILE_KEY).await { + return socket; + } + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + panic!("plugin never registered"); +} + +/// 다음 작업 하나를 받아 요청 id 와 함께 돌려준다. +async fn next_job(socket: &mut Plugin) -> Value { + let message = socket + .next() + .await + .expect("a job arrives") + .expect("the socket stays open"); + let Message::Text(text) = message else { + panic!("jobs are sent as text"); + }; + serde_json::from_str(&text).expect("a job is JSON") +} + +#[tokio::test] +async fn a_script_read_reaches_the_plugin_and_comes_back_wrapped() { + let server = BridgeServer::start(0).expect("an ephemeral port is free"); + let mut plugin = connect_plugin(&server).await; + let client = BridgeFigmaClient::new(server.state()); + + let call = ReadToolCall::fast_snapshot(FILE_KEY, "1:2"); + let reading = tokio::spawn(async move { client.call_read_tool(call).await }); + + let job = next_job(&mut plugin).await; + assert_eq!(job["kind"], "devup-job"); + // 플러그인이 아는 이름이어야 한다. 코드젠이 파일명에서 만든 것과 같다. + assert_eq!(job["script"], "fastSnapshot"); + assert_eq!(job["params"]["nodeId"], "1:2"); + // 단일 루트 읽기는 대상 노드 하나를 루트로 넘긴다 — `source` 의 기본값과 같다. + assert_eq!(job["params"]["rootIds"], json!(["1:2"])); + + let payload = json!({ "fileKey": FILE_KEY, "nodes": [{ "id": "1:2" }] }); + plugin + .send(Message::Text( + json!({ + "kind": "devup-result", + "requestId": job["requestId"], + "data": payload, + }) + .to_string() + .into(), + )) + .await + .expect("the result is sent"); + + let result = reading + .await + .expect("the read task finishes") + .expect("the bridge answers"); + + // 원격 `use_figma` 와 같은 모양이어야 디코더들이 그대로 동작한다. + let text = result.raw["content"][0]["text"] + .as_str() + .expect("the payload rides in content[].text"); + assert_eq!( + serde_json::from_str::(text).expect("the carried text is JSON"), + payload, + ); +} + +#[tokio::test] +async fn each_script_read_maps_to_the_name_the_plugin_knows() { + let server = BridgeServer::start(0).expect("an ephemeral port is free"); + let mut plugin = connect_plugin(&server).await; + let client = BridgeFigmaClient::new(server.state()); + + for (call, expected) in [ + (ReadToolCall::section_index(FILE_KEY, "1:2"), "sectionIndex"), + (ReadToolCall::page_catalog(FILE_KEY), "pageCatalog"), + (ReadToolCall::fast_theme(FILE_KEY), "fastTheme"), + ] { + let client = client.clone(); + let reading = tokio::spawn(async move { client.call_read_tool(call).await }); + + let job = next_job(&mut plugin).await; + assert_eq!(job["script"], expected, "script name for {expected}"); + + plugin + .send(Message::Text( + json!({ + "kind": "devup-result", + "requestId": job["requestId"], + "data": { "ok": true }, + }) + .to_string() + .into(), + )) + .await + .expect("the result is sent"); + reading + .await + .expect("the read task finishes") + .expect("the bridge answers"); + } +} + +#[tokio::test] +async fn a_failing_script_surfaces_its_own_code() { + let server = BridgeServer::start(0).expect("an ephemeral port is free"); + let mut plugin = connect_plugin(&server).await; + let client = BridgeFigmaClient::new(server.state()); + + let call = ReadToolCall::fast_snapshot(FILE_KEY, "9:9"); + let reading = tokio::spawn(async move { client.call_read_tool(call).await }); + + let job = next_job(&mut plugin).await; + plugin + .send(Message::Text( + json!({ + "kind": "devup-result", + "requestId": job["requestId"], + "error": "DEVUP_NODE_NOT_FOUND", + }) + .to_string() + .into(), + )) + .await + .expect("the failure is sent"); + + let error = reading + .await + .expect("the read task finishes") + .expect_err("a script failure is an error"); + // 원격 경로와 같은 문자열이어야 위쪽 분기가 동일하게 동작한다. + assert_eq!(error.message, "DEVUP_NODE_NOT_FOUND"); +} + +#[tokio::test] +async fn routing_is_decided_before_the_call() { + let server = BridgeServer::start(0).expect("an ephemeral port is free"); + let client = BridgeFigmaClient::new(server.state()); + + // 플러그인이 붙기 전에는 스크립트 읽기도 맡지 않는다 — 원격이 받아야 한다. + assert!( + !client + .can_serve(&ReadToolCall::fast_snapshot(FILE_KEY, "1:2")) + .await + ); + + let _plugin = connect_plugin(&server).await; + assert!( + client + .can_serve(&ReadToolCall::fast_snapshot(FILE_KEY, "1:2")) + .await + ); + + // 공식 도구 이름으로 가는 읽기는 플러그인이 붙어 있어도 맡지 않는다. 응답 + // 모양이 달라 흉내 내면 두 경로가 갈라진다. + assert!( + !client + .can_serve(&ReadToolCall::metadata(FILE_KEY, Some("1:2"))) + .await + ); + // 다른 파일은 그 파일의 플러그인이 필요하다. + assert!( + !client + .can_serve(&ReadToolCall::fast_snapshot("OtherFile", "1:2")) + .await + ); +} From a2eb7ff8ee0c77672f541d8eae3a34999155187c Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 10:13:51 +0900 Subject: [PATCH 04/16] docs: say how to install the bridge plugin and where it silently fails The port is the trap. Figma fixes the addresses a plugin may dial in its manifest, and that list does not change at runtime, so moving DEVUP_FIGMA_BRIDGE_PORT alone leaves devup-mcp listening somewhere the plugin never knocks. Nothing errors - reads just go back to the metered path, spending the allowance this exists to save. Three places have to move together. --- plugin/README.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 plugin/README.md diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 0000000..979f3c9 --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,84 @@ +# Devup Bridge + +devup-mcp 가 Figma 를 읽을 때 공식 MCP 의 요금 한도를 쓰지 않게 해 주는 플러그인입니다. + +## 왜 필요한가 + +devup-mcp 의 수집은 읽기 스크립트를 공식 MCP 의 `use_figma` 로 보내는 방식이고, +한도가 걸리는 곳이 바로 그 도구입니다. 화면 하나를 받는 데 snapshot 만 십수 회가 +들어가므로 한도는 금방 바닥납니다. + +그 스크립트들이 건드리는 것은 문서화된 Plugin API 뿐입니다. + +``` +getNodeByIdAsync getStyleByIdAsync variables root fileKey +getLocalPaintStylesAsync / TextStylesAsync / EffectStylesAsync / GridStylesAsync +``` + +전부 평범한 플러그인이 쓸 수 있는 것들이라, 우리가 돌리는 플러그인이 같은 일을 +대신할 수 있습니다. 그 경로에는 한도가 없습니다. + +## 설치 + +```bash +cd plugin +npm install +npm run build +``` + +그다음 Figma 데스크톱 앱에서 **Plugins → Development → Import plugin from +manifest** 를 고르고 `plugin/manifest.json` 을 선택합니다. + +브라우저판에서는 개발 플러그인을 불러올 수 없으므로 데스크톱 앱이 필요합니다. + +## 쓰는 법 + +읽으려는 파일을 열고 플러그인을 실행한 뒤, 창을 **열어 둔 채로** devup-mcp 를 +쓰면 됩니다. 창을 닫으면 연결이 끊기고 그 파일의 읽기는 다시 공식 MCP 로 갑니다. + +창에 표시되는 점이 상태입니다. + +| 표시 | 뜻 | +|---|---| +| 초록 | devup-mcp 에 연결됨. 이 파일의 읽기는 한도를 쓰지 않습니다 | +| 빨강 | devup-mcp 를 찾지 못함. 2초마다 다시 시도합니다 | + +devup-mcp 쪽은 아무 설정도 필요 없습니다. 플러그인이 붙어 있으면 그 파일의 +스크립트 읽기를 브리지로 보내고, 안 붙어 있으면 호출마다 곧장 공식 MCP 로 +넘어갑니다. 켜 두어서 잃는 것은 없습니다. + +## 알아 둘 것 + +**포트를 바꾸려면 세 곳을 함께 고쳐야 합니다.** Figma 는 플러그인이 접속할 수 있는 +주소를 manifest 에 미리 적어 두게 하며, 이 목록은 실행 중에 바뀌지 않습니다. +그래서 `DEVUP_FIGMA_BRIDGE_PORT` 만 바꾸면 devup-mcp 는 새 포트에서 기다리는데 +플러그인은 여전히 1993 을 두드리게 되고, **아무 오류 없이 공식 MCP 로 폴백**합니다. +아끼려던 한도가 그대로 나가므로 눈치채기 어렵습니다. + +바꿔야 한다면 `manifest.json` 의 `allowedDomains`, `src/code.ts` 의 `PORT`, +그리고 환경 변수를 모두 같은 값으로 맞춘 뒤 플러그인을 다시 빌드·설치하십시오. + +끄려면 `DEVUP_FIGMA_BRIDGE_PORT=off` 를 주면 됩니다. + +**읽기 전용입니다.** 실행되는 스크립트는 devup-mcp 의 읽기 스크립트에서 생성되며 +문서를 바꾸는 호출을 포함하지 않습니다. 데이터는 같은 기기의 devup-mcp 로만 +나갑니다(`127.0.0.1`). + +**한 기기에서 devup-mcp 를 여러 개 띄우면** 먼저 뜬 쪽이 포트를 잡고, 나머지는 +브리지 없이 공식 MCP 로 동작합니다. 오류가 아니라 정상 동작입니다. + +## 스크립트는 복사본이 아닙니다 + +`src/generated/` 는 `npm run build` 가 devup-mcp 의 원본에서 만들어 냅니다. + +``` +crates/devup-mcp-figma/src/scripts/*.js ← 원본 (공식 MCP 경로도 이것을 씀) + ↓ plugin/scripts/gen-scripts.mjs +plugin/src/generated/scripts.js ← 빌드 산출물 (커밋하지 않음) +``` + +두 경로가 같은 소스를 쓰므로 봉투가 갈라질 수 없습니다. 원본의 플레이스홀더는 +함수 인자로 바뀌고, 치환되지 않은 것이 남으면 빌드가 실패합니다. + +`__DEVUP_SNAPSHOT_CURSOR__` 만 예외로 그대로 남습니다. 값이 아니라 Rust 디코더와 +공유하는 마커 노드 id 라서, 치환하면 페이지네이션이 조용히 깨집니다. From 1a6c93f446483b06dba5380019bcfa8b3dfa7b30 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 10:17:45 +0900 Subject: [PATCH 05/16] test: run the generated scripts instead of only bundling them Typechecking and bundling say nothing about whether the codegen wrapped the scripts correctly. A top-level return landing outside its function, or a placeholder resolving to something odd, bundles cleanly and only breaks inside Figma - where it is hard to tell whether the plugin or the script is at fault. Importing the module proves the wrapping parses, and pageCatalog is run against a stub because it touches the smallest figma surface, so the stub cannot stand in for what is being tested. The name list is checked against Rust's plugin_name() here too: a drift there is invisible until a read reaches a plugin that does not know the name. Wired into build, next to the placeholder check that already fails it. --- plugin/package.json | 3 +- plugin/scripts/verify-generated.mjs | 64 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 plugin/scripts/verify-generated.mjs diff --git a/plugin/package.json b/plugin/package.json index 88ab0a4..dd69675 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -6,7 +6,8 @@ "description": "devup-mcp 가 Figma 를 읽을 때 공식 MCP 할당량을 쓰지 않도록, 열려 있는 파일을 로컬 WebSocket 으로 직접 읽어 주는 Figma 플러그인", "scripts": { "gen": "node scripts/gen-scripts.mjs", - "build": "npm run gen && rspack build && node scripts/inline-ui.mjs", + "verify": "npm run gen && node scripts/verify-generated.mjs", + "build": "npm run gen && node scripts/verify-generated.mjs && rspack build && node scripts/inline-ui.mjs", "watch": "npm run gen && rspack build --watch", "typecheck": "npm run gen && tsc --noEmit" }, diff --git a/plugin/scripts/verify-generated.mjs b/plugin/scripts/verify-generated.mjs new file mode 100644 index 0000000..6c0a313 --- /dev/null +++ b/plugin/scripts/verify-generated.mjs @@ -0,0 +1,64 @@ +// 생성된 스크립트를 실제로 불러와 실행해 본다. +// +// 타입체크와 번들만으로는 코드젠이 옳았는지 알 수 없다. 최상위 `return` 이 함수 +// 밖에 놓이거나 플레이스홀더가 이상한 값으로 바뀌면 번들은 그대로 나오고 Figma +// 안에서야 터진다. 그때는 원인이 플러그인인지 스크립트인지 가리기 어렵다. +// +// 그래서 여기서 한 번 불러오고(구조가 깨졌으면 import 가 실패한다), 최소한의 +// figma 스텁으로 실행까지 해 본다. + +import assert from 'node:assert/strict' + +const { SCRIPTS, pageCatalog } = await import('../src/generated/scripts.js') + +/** Rust 의 `BuiltinScript::plugin_name` 이 내놓는 이름. 어긋나면 런타임에야 안다. */ +const EXPECTED = [ + 'assets', + 'explore', + 'fastSnapshot', + 'fastTheme', + 'largeValue', + 'pageCatalog', + 'search', + 'sectionIndex', + 'snapshot', + 'usedResources', + 'variableCatalog', + 'variables', +] + +const actual = Object.keys(SCRIPTS).sort() +assert.deepEqual( + actual, + [...EXPECTED].sort(), + 'SCRIPTS 의 이름이 Rust 의 plugin_name() 과 달라졌습니다. 두 곳을 함께 고쳐야 합니다.', +) + +for (const name of EXPECTED) { + assert.equal( + typeof SCRIPTS[name], + 'function', + `${name} 이 함수가 아닙니다 — 코드젠 래핑이 깨졌습니다`, + ) +} + +// 실행까지 확인한다. `pageCatalog` 는 figma 표면을 가장 적게 쓰므로 스텁이 +// 스크립트를 대신 검증해 버리는 일이 없다. +globalThis.figma = { + fileKey: 'FileKey123', + root: { + children: [ + { id: '0:1', type: 'PAGE', name: 'Page 1' }, + { id: '0:2', type: 'PAGE', name: 'Page 2' }, + ], + }, +} + +const catalog = await pageCatalog({}) +assert.equal(catalog.fileKey, 'FileKey123') +assert.deepEqual(catalog.rootIds, ['0:1', '0:2']) +assert.equal(catalog.nodes.length, 2) +assert.equal(catalog.nodes[0].fields.name, 'Page 1') +assert.equal(catalog.nodes[1].type, 'PAGE') + +console.log(`verified ${EXPECTED.length} generated scripts (pageCatalog executed)`) From 2e8572af4e30849ca9aef19e466c1e8e395ac90b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 10:46:19 +0900 Subject: [PATCH 06/16] feat: add a probe that reads a real Figma file through the bridge The socket tests put a WebSocket client where the plugin goes, so the part they cannot reach is the one that matters most: the generated scripts running against a real document. This binds the default port, waits for the plugin, and reads the open file's page list. page_catalog is the read to start with because it needs no node id, so a first check cannot fail merely for pointing at the wrong node. cargo run -p devup-mcp-figma --example bridge_probe --- .../devup-mcp-figma/examples/bridge_probe.rs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 crates/devup-mcp-figma/examples/bridge_probe.rs diff --git a/crates/devup-mcp-figma/examples/bridge_probe.rs b/crates/devup-mcp-figma/examples/bridge_probe.rs new file mode 100644 index 0000000..7603e71 --- /dev/null +++ b/crates/devup-mcp-figma/examples/bridge_probe.rs @@ -0,0 +1,70 @@ +//! 브리지를 실제 Figma 에 붙여 본다. +//! +//! 통합 테스트는 플러그인 자리에 WebSocket 클라이언트를 세우므로 그 너머 — +//! 생성된 스크립트가 진짜 문서를 읽는 부분 — 은 증명하지 못한다. 이 프로브는 +//! 기본 포트로 브리지를 띄우고, Figma 에서 플러그인이 붙기를 기다린 뒤, +//! 실제 파일의 페이지 목록을 읽어 낸다. +//! +//! ```text +//! cargo run -p devup-mcp-figma --example bridge_probe +//! ``` + +use std::time::Duration; + +use devup_mcp_figma::{ + BridgeFigmaClient, BridgeServer, DEFAULT_BRIDGE_PORT, FigmaUpstream, ReadToolCall, +}; +use serde_json::Value; + +#[tokio::main] +async fn main() { + let server = match BridgeServer::start(DEFAULT_BRIDGE_PORT) { + Some(server) => server, + None => { + eprintln!("PROBE_FAIL: port {DEFAULT_BRIDGE_PORT} is already taken"); + std::process::exit(1); + } + }; + println!("listening on ws://127.0.0.1:{}/plugin", server.port()); + println!("waiting for the Devup Bridge plugin in Figma..."); + + // 플러그인을 가져오고 파일을 여는 데 시간이 걸리므로 넉넉히 기다린다. + let state = server.state(); + let mut files: Vec = Vec::new(); + for _ in 0..3000 { + files = state.connected_files().await; + if !files.is_empty() { + break; + } + tokio::time::sleep(Duration::from_millis(200)).await; + } + let Some(file_key) = files.first().cloned() else { + eprintln!("PROBE_FAIL: no plugin connected within 10 minutes"); + std::process::exit(1); + }; + println!("plugin connected for file {file_key}"); + + // 페이지 목록은 노드 id 를 몰라도 읽을 수 있어 첫 확인에 맞다. + let client = BridgeFigmaClient::new(state); + let result = match client + .call_read_tool(ReadToolCall::page_catalog(&file_key)) + .await + { + Ok(result) => result, + Err(error) => { + eprintln!("PROBE_FAIL: {:?} / {}", error.code, error.message); + std::process::exit(1); + } + }; + + let text = result.raw["content"][0]["text"] + .as_str() + .expect("the payload rides in content[].text"); + let payload: Value = serde_json::from_str(text).expect("the carried text is JSON"); + + let pages = payload["nodes"].as_array().map_or(0, Vec::len); + println!("PROBE_OK fileKey={} pages={pages}", payload["fileKey"]); + for node in payload["nodes"].as_array().into_iter().flatten() { + println!(" {} {}", node["id"], node["fields"]["name"]); + } +} From 809dbcd7edccb7a8770a7f85357a51074cf4625b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 11:11:59 +0900 Subject: [PATCH 07/16] fix: make the bridge work against a real Figma install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified by importing the plugin into Figma and reading a live file. Two defects only that could find. Figma rejected the manifest outright: allowedDomains took ws://localhost but called ws://127.0.0.1 "not a valid URL", so the plugin never ran. The UI dialled 127.0.0.1 as well and had to move to localhost with it. Then the plugin connected and served nothing. figma.fileKey came back empty, and registration skipped anything without a key - the window said "connected" while no read ever arrived, which is the hardest version of this to diagnose. Keyless plugins now register, and a keyless one answers only when it is the only one connected; with several there is no telling which file is open, and falling back to the metered path beats reading the wrong document. Probe output against the open file: PROBE_OK fileKey="" pages=5 "0:1" "Devup UI 랜딩페이지" "530:2305" "Components" ... --- crates/devup-mcp-figma/src/bridge.rs | 50 ++++++++++++++++++++++------ plugin/manifest.json | 2 +- plugin/src/ui.ts | 5 ++- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/crates/devup-mcp-figma/src/bridge.rs b/crates/devup-mcp-figma/src/bridge.rs index f5d2616..24ac13c 100644 --- a/crates/devup-mcp-figma/src/bridge.rs +++ b/crates/devup-mcp-figma/src/bridge.rs @@ -112,6 +112,28 @@ fn unavailable(message: impl Into) -> DevupError { DevupError::new(ErrorCode::DevupFigmaDirectUnavailable, message, false) } +/// 어느 플러그인이 이 파일을 맡을지 고른다. +/// +/// 보통은 파일 키가 그대로 맞는다. 다만 `figma.fileKey` 는 늘 있는 값이 아니어서 +/// (Dev Mode 에서 비어 오는 것을 실제로 봤다) 키 없이 붙는 플러그인이 생긴다. +/// 그때 등록을 건너뛰면 창은 "연결됨"이라고 하는데 어떤 읽기도 오지 않는 — +/// 원인을 찾기 가장 어려운 — 상태가 된다. +/// +/// 그래서 키 없는 플러그인은 **혼자 붙어 있을 때만** 맡는다. 여럿이면 어느 파일을 +/// 보고 있는지 알 수 없고, 엉뚱한 파일을 읽어 주는 것보다 원격으로 넘기는 편이 낫다. +fn resolve_key(plugins: &HashMap, file_key: &str) -> Option { + if plugins.contains_key(file_key) { + return Some(file_key.to_owned()); + } + if plugins.len() == 1 + && let Some(key) = plugins.keys().next() + && key.is_empty() + { + return Some(key.clone()); + } + None +} + impl BridgeState { fn next_request_id(&self) -> String { format!("job-{}", self.counter.fetch_add(1, Ordering::Relaxed)) @@ -119,7 +141,7 @@ impl BridgeState { /// 이 파일을 열어 둔 플러그인이 있는지. pub async fn has_plugin(&self, file_key: &str) -> bool { - self.inner.lock().await.plugins.contains_key(file_key) + resolve_key(&self.inner.lock().await.plugins, file_key).is_some() } /// 붙어 있는 파일 키 목록. 진단용. @@ -140,6 +162,12 @@ impl BridgeState { { let mut inner = self.inner.lock().await; + let Some(resolved) = resolve_key(&inner.plugins, file_key) else { + return Err(unavailable(format!( + "no Devup Bridge plugin is open for file {file_key}" + ))); + }; + let file_key = resolved.as_str(); let Some(plugin) = inner.plugins.get(file_key) else { return Err(unavailable(format!( "no Devup Bridge plugin is open for file {file_key}" @@ -224,18 +252,18 @@ async fn handle_plugin(state: BridgeState, mut socket: WebSocket) { match value.get("kind").and_then(Value::as_str) { Some("hello") => { - let file_key = serde_json::from_value::(value) + // 키가 없어도 등록한다. 건너뛰면 창은 "연결됨"이라고 하는데 + // 어떤 읽기도 오지 않아 원인을 찾을 수 없다. 키 없는 연결을 + // 어디까지 믿을지는 resolve_key 가 정한다. + let key = serde_json::from_value::(value) .ok() .and_then(|hello| hello.file_key) - .filter(|key| !key.is_empty()); - // fileKey 가 없는 파일(저장 전 초안)은 요청 대상이 될 수 없다. - if let Some(key) = file_key { - state.inner.lock().await.plugins.insert( - key.clone(), - Connected { outbox: outbox.clone() }, - ); - registered = Some(key); - } + .unwrap_or_default(); + state.inner.lock().await.plugins.insert( + key.clone(), + Connected { outbox: outbox.clone() }, + ); + registered = Some(key); } Some("devup-result") => { let Ok(result) = serde_json::from_value::(value) else { diff --git a/plugin/manifest.json b/plugin/manifest.json index 4a231eb..53dd87b 100644 --- a/plugin/manifest.json +++ b/plugin/manifest.json @@ -8,7 +8,7 @@ "documentAccess": "dynamic-page", "editorType": ["figma", "dev"], "networkAccess": { - "allowedDomains": ["ws://localhost:1993", "ws://127.0.0.1:1993"], + "allowedDomains": ["ws://localhost:1993"], "reasoning": "devup-mcp 가 같은 기기에서 여는 로컬 WebSocket 에 붙어, 열려 있는 파일의 노드를 읽어 돌려줍니다. 문서를 바꾸지 않고 외부로 보내지도 않습니다." } } diff --git a/plugin/src/ui.ts b/plugin/src/ui.ts index b795fa4..a9e15e0 100644 --- a/plugin/src/ui.ts +++ b/plugin/src/ui.ts @@ -41,7 +41,10 @@ function render(state: 'on' | 'off', text: string) { function connect() { if (!bridgeStatus) return - const url = `ws://127.0.0.1:${bridgeStatus.port}/plugin` + // 반드시 `localhost` 여야 한다. Figma 는 manifest 의 allowedDomains 에 적힌 + // 주소로만 나가게 하는데, `ws://127.0.0.1:...` 은 "유효한 URL 이 아니다"라며 + // 매니페스트 자체를 거부한다 — 플러그인이 아예 실행되지 않는다. + const url = `ws://localhost:${bridgeStatus.port}/plugin` let ws: WebSocket try { ws = new WebSocket(url) From d838780aff2c28f587596bd2d95839764b4b73de Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 11:13:08 +0900 Subject: [PATCH 08/16] docs: record what the real install taught about addresses and file keys Both are silent failures. ws://127.0.0.1 in allowedDomains makes Figma reject the manifest, so the plugin never starts. An empty figma.fileKey leaves a connection that reports itself healthy while answering nothing, which is why a keyless plugin now only serves when it is alone. --- plugin/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugin/README.md b/plugin/README.md index 979f3c9..131d684 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -67,6 +67,17 @@ devup-mcp 쪽은 아무 설정도 필요 없습니다. 플러그인이 붙어 **한 기기에서 devup-mcp 를 여러 개 띄우면** 먼저 뜬 쪽이 포트를 잡고, 나머지는 브리지 없이 공식 MCP 로 동작합니다. 오류가 아니라 정상 동작입니다. +**`ws://127.0.0.1` 은 쓸 수 없습니다.** Figma 는 `allowedDomains` 에 그 주소를 적으면 +"유효한 URL 이 아니다"라며 **매니페스트 자체를 거부**해 플러그인이 실행되지 않습니다. +`localhost` 로만 적어야 하고, UI 도 같은 이름으로 접속합니다. 실제 설치에서 확인한 +동작입니다. + +**파일을 하나만 열어 두십시오.** 플러그인은 붙을 때 `figma.fileKey` 로 자기 파일을 +알리는데, 이 값이 **비어 오는 경우가 있습니다**(Dev Mode 에서 확인). 그때도 연결은 +등록되지만, 어느 파일인지 모르므로 **혼자 붙어 있을 때만** 읽기를 맡습니다. 키 없는 +플러그인이 둘 이상이면 어느 쪽이 대상 파일인지 가릴 수 없어 공식 MCP 로 넘어갑니다 — +엉뚱한 파일을 읽어 주는 것보다 낫기 때문입니다. + ## 스크립트는 복사본이 아닙니다 `src/generated/` 는 `npm run build` 가 devup-mcp 의 원본에서 만들어 냅니다. From af5f8d35e058f99b93c5490b4f58fd20095ff31a Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 11:20:09 +0900 Subject: [PATCH 09/16] test: pin the rule for plugins that report no file key The keyless path was found on a real install and fixed, but nothing held it in place: the next edit could drop back to skipping registration and the only symptom would be reads quietly going to the metered path again. Two cases, because the rule is a trade rather than a default. Alone, a keyless plugin answers for whatever file is asked - it is the only one open, so it is that file. With another plugin connected there is no telling which document is in front of it, and the remote path costs money where a wrong answer costs correctness. --- .../devup-mcp-figma/tests/bridge_transport.rs | 82 ++++++++++++++++++- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/crates/devup-mcp-figma/tests/bridge_transport.rs b/crates/devup-mcp-figma/tests/bridge_transport.rs index 728fd42..88a3588 100644 --- a/crates/devup-mcp-figma/tests/bridge_transport.rs +++ b/crates/devup-mcp-figma/tests/bridge_transport.rs @@ -17,13 +17,16 @@ const FILE_KEY: &str = "FileKey123"; type Plugin = WebSocketStream>; /// 플러그인 흉내: 붙어서 자기 파일을 알리고, 등록될 때까지 기다린다. -async fn connect_plugin(server: &BridgeServer) -> Plugin { +/// +/// `file_key` 가 `None` 이면 키 없이 붙는다. 지어낸 경우가 아니라 실제로 일어난다 — +/// 실기기에서 `figma.fileKey` 가 비어 오는 것을 확인했다. +async fn connect_plugin_as(server: &BridgeServer, file_key: Option<&str>) -> Plugin { let (mut socket, _) = connect_async(format!("ws://127.0.0.1:{}/plugin", server.port())) .await .expect("bridge accepts a plugin"); socket .send(Message::Text( - json!({ "kind": "hello", "fileKey": FILE_KEY }) + json!({ "kind": "hello", "fileKey": file_key }) .to_string() .into(), )) @@ -31,9 +34,10 @@ async fn connect_plugin(server: &BridgeServer) -> Plugin { .expect("hello is sent"); // 등록은 서버 쪽 태스크에서 일어난다. 보낸 직후에는 아직일 수 있다. + let expected = file_key.unwrap_or_default(); let state = server.state(); for _ in 0..200 { - if state.has_plugin(FILE_KEY).await { + if state.connected_files().await.iter().any(|k| k == expected) { return socket; } tokio::time::sleep(std::time::Duration::from_millis(10)).await; @@ -41,6 +45,10 @@ async fn connect_plugin(server: &BridgeServer) -> Plugin { panic!("plugin never registered"); } +async fn connect_plugin(server: &BridgeServer) -> Plugin { + connect_plugin_as(server, Some(FILE_KEY)).await +} + /// 다음 작업 하나를 받아 요청 id 와 함께 돌려준다. async fn next_job(socket: &mut Plugin) -> Value { let message = socket @@ -200,3 +208,71 @@ async fn routing_is_decided_before_the_call() { .await ); } + +/// 키 없이 붙은 플러그인도 혼자면 맡는다. +/// +/// `figma.fileKey` 는 늘 오는 값이 아니다. 실기기에서 비어 온 적이 있고, 그때 등록을 +/// 건너뛰었더니 플러그인 창은 "연결됨"이라고 하는데 읽기는 한 건도 도착하지 않았다. +/// 연결이 스스로를 멀쩡하다고 보고하는 실패라 원인을 찾기가 가장 어렵다. +#[tokio::test] +async fn a_plugin_without_a_file_key_serves_when_it_is_the_only_one() { + let server = BridgeServer::start(0).expect("an ephemeral port is free"); + let client = BridgeFigmaClient::new(server.state()); + let mut plugin = connect_plugin_as(&server, None).await; + + // 어느 파일을 물어도 이 하나가 답한다. 무엇을 열고 있는지 모르지만, 하나뿐이라 + // 그것이 열려 있는 파일이다. + assert!( + client + .can_serve(&ReadToolCall::fast_snapshot(FILE_KEY, "1:2")) + .await + ); + + let reading = { + let client = client.clone(); + tokio::spawn(async move { + client + .call_read_tool(ReadToolCall::page_catalog(FILE_KEY)) + .await + }) + }; + let job = next_job(&mut plugin).await; + assert_eq!(job["script"], "pageCatalog"); + plugin + .send(Message::Text( + json!({ + "kind": "devup-result", + "requestId": job["requestId"], + "data": { "ok": true }, + }) + .to_string() + .into(), + )) + .await + .expect("the result is sent"); + reading + .await + .expect("the read task finishes") + .expect("a keyless plugin still answers"); +} + +/// 키 없는 플러그인이 둘이면 아무도 맡지 않는다. +/// +/// 어느 쪽이 대상 파일을 열고 있는지 가릴 수 없다. 엉뚱한 문서를 읽어 주느니 요금을 +/// 쓰더라도 원격으로 넘기는 편이 낫다. +#[tokio::test] +async fn two_keyless_plugins_are_ambiguous_and_neither_serves() { + let server = BridgeServer::start(0).expect("an ephemeral port is free"); + let client = BridgeFigmaClient::new(server.state()); + + let _first = connect_plugin_as(&server, None).await; + // 둘 다 키가 없으면 같은 자리에 들어가므로, 서로 다른 파일의 플러그인을 하나 + // 더 붙여 "여럿"을 만든다. + let _second = connect_plugin_as(&server, Some("AnotherFile")).await; + + assert!( + !client + .can_serve(&ReadToolCall::fast_snapshot(FILE_KEY, "1:2")) + .await + ); +} From 896caa59ec117d97cc891cccd1d9b6d7b05d4917 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 12:26:11 +0900 Subject: [PATCH 10/16] fix: fill in the file key the plugin could not report The decoder wants a non-empty fileKey and the scripts copy it straight from figma.fileKey, which comes back empty on a real install. Every node arrived - eight of them - and the whole envelope was thrown away for the one field. Not invented: the caller named the file this read was for, and resolve_key already decided this plugin serves it. A manifest permission looked like the cleaner fix, but Figma rejects "permissions": ["fileKey"] outright and refuses to load the plugin, so the value has to be restored on this side. --- crates/devup-mcp-figma/src/bridge.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp-figma/src/bridge.rs b/crates/devup-mcp-figma/src/bridge.rs index 24ac13c..30a9625 100644 --- a/crates/devup-mcp-figma/src/bridge.rs +++ b/crates/devup-mcp-figma/src/bridge.rs @@ -217,6 +217,27 @@ impl BridgeState { } } +/// 봉투의 `fileKey` 가 비어 있으면 요청한 값으로 채운다. +/// +/// 스크립트는 `figma.fileKey` 를 그대로 싣는데, 그 값이 늘 오지는 않는다 — +/// 비어 오는 것을 실기기에서 확인했다. 디코더는 비어 있지 않은 `fileKey` 를 요구하므로 +/// 그대로 두면 노드를 다 받고도 "스냅샷을 찾지 못했다"며 버린다. +/// +/// 지어내는 값이 아니다. 이 읽기가 어느 파일을 향했는지는 호출자가 알고 있고, +/// 그 파일을 이 플러그인이 맡는다는 판단은 이미 `resolve_key` 가 내렸다. +fn stamp_file_key(data: &mut Value, file_key: &str) { + let Some(object) = data.as_object_mut() else { + return; + }; + let empty = object + .get("fileKey") + .and_then(Value::as_str) + .is_none_or(str::is_empty); + if empty { + object.insert("fileKey".to_owned(), Value::String(file_key.to_owned())); + } +} + /// 플러그인이 돌려준 값을 원격 `use_figma` 가 주는 모양으로 감싼다. /// /// 디코더들은 `content[].text` 안의 JSON 문자열을 찾도록 쓰여 있다. 값을 그대로 @@ -383,10 +404,11 @@ impl FigmaUpstream for BridgeFigmaClient { // 여기서 흉내 내지 않는다. return Err(unavailable("the bridge serves script reads only")); }; - let data = self + let mut data = self .state .dispatch(call.file_key(), job.script, job.params) .await?; + stamp_file_key(&mut data, call.file_key()); Ok(UpstreamResult { raw: wrap_as_tool_result(&data)?, }) From 399a51bea50557e0a8840e7f4e07f9c4dd0a12b5 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 13:28:44 +0900 Subject: [PATCH 11/16] perf: read a screen in one page when the transport does not cut The 15 KB page and 4 KB field budgets exist because the Figma MCP cuts a text result at 20,480 bytes. A local socket does not cut, so the same screen that took 32 round-trips now takes one or two. That matters more than it sounds. Chromium throttles a background window to one wake per minute, and a measured run spent 59.8s, 59.8s and 59.7s on three separate reads while the other twenty-nine took 9-72ms each. A silent AudioContext did not lift it. Fewer round-trips is the fix that does not depend on which window the user is looking at. The ceilings become defaults rather than constants: absent an override both scripts pack exactly as measured. The contract test now pins the 19 KiB bound and the 18000 packing ceiling instead of the literal that used to carry them. Also queues jobs in the plugin. Snapshot scripts move the current page with setCurrentPageAsync, so two running at once would read each other's screen and return plausible nodes from the wrong one. --- crates/devup-mcp-figma/src/bridge.rs | 29 ++++++++++- .../src/scripts/fast_snapshot.js | 15 +++++- .../devup-mcp-figma/src/scripts/snapshot.js | 8 ++- crates/devup-mcp-figma/src/upstream.rs | 9 ++++ .../tests/upstream_contract.rs | 11 +++- plugin/src/code.ts | 52 ++++++++++++++----- plugin/src/ui.ts | 38 ++++++++++++++ 7 files changed, 145 insertions(+), 17 deletions(-) diff --git a/crates/devup-mcp-figma/src/bridge.rs b/crates/devup-mcp-figma/src/bridge.rs index 30a9625..29a1665 100644 --- a/crates/devup-mcp-figma/src/bridge.rs +++ b/crates/devup-mcp-figma/src/bridge.rs @@ -217,6 +217,31 @@ impl BridgeState { } } +/// 한 번에 담을 수 있는 양. 로컬 소켓이라 공식 MCP 의 자름을 따를 이유가 없다. +/// +/// 기본값(봉투 19KB · 필드 4KB)은 공식 MCP 가 텍스트 결과를 20,480 바이트에서 +/// 자르기 때문에 생긴 것이다. 그 탓에 화면 하나가 30번 넘는 왕복으로 쪼개지고, +/// 창이 뒤로 가 있으면 왕복 하나가 60초까지 걸린다 — 실측한 값이다. +const BRIDGE_ENVELOPE_BYTES: usize = 8 * 1024 * 1024; +const BRIDGE_FIELD_BYTES: usize = 4 * 1024 * 1024; + +/// 스냅샷 읽기의 한도를 브리지 전송에 맞게 올린다. +/// +/// 값만 바꿀 뿐 무엇을 읽을지는 그대로다. 스크립트는 한 페이지에 다 담기면 +/// `complete` 로 표시하고, 큰 필드도 잘리지 않으므로 수집기가 그 필드를 따로 +/// 받으러 가지 않는다. 두 왕복이 한 왕복이 되는 것이 아니라 서른이 하나가 된다. +fn widen_budgets(params: &mut Value) { + let Some(object) = params.as_object_mut() else { + return; + }; + let Some(snapshot) = object.get_mut("snapshot").and_then(Value::as_object_mut) else { + return; + }; + snapshot.insert("maxEnvelopeBytes".to_owned(), BRIDGE_ENVELOPE_BYTES.into()); + snapshot.insert("maxPayloadBytes".to_owned(), BRIDGE_ENVELOPE_BYTES.into()); + snapshot.insert("maxFieldBytes".to_owned(), BRIDGE_FIELD_BYTES.into()); +} + /// 봉투의 `fileKey` 가 비어 있으면 요청한 값으로 채운다. /// /// 스크립트는 `figma.fileKey` 를 그대로 싣는데, 그 값이 늘 오지는 않는다 — @@ -404,9 +429,11 @@ impl FigmaUpstream for BridgeFigmaClient { // 여기서 흉내 내지 않는다. return Err(unavailable("the bridge serves script reads only")); }; + let mut params = job.params; + widen_budgets(&mut params); let mut data = self .state - .dispatch(call.file_key(), job.script, job.params) + .dispatch(call.file_key(), job.script, params) .await?; stamp_file_key(&mut data, call.file_key()); Ok(UpstreamResult { diff --git a/crates/devup-mcp-figma/src/scripts/fast_snapshot.js b/crates/devup-mcp-figma/src/scripts/fast_snapshot.js index 8947d03..8ec99c2 100644 --- a/crates/devup-mcp-figma/src/scripts/fast_snapshot.js +++ b/crates/devup-mcp-figma/src/scripts/fast_snapshot.js @@ -104,11 +104,22 @@ const offset = Math.max(0, Math.floor(Number(pageOptions.offset) || 0)); // `integrity.utf8Bytes` matched the text that arrived on every one of 488 // banked pages, so a 1 KiB margin under the cut is enough. There is no PNG // fallback any more; a page has to survive as text. +// 위 측정은 공식 MCP 가 텍스트 결과를 자르는 지점에 관한 것이다. 로컬 소켓으로 +// 읽을 때는 그 자름이 없어 한 페이지에 전부 담을 수 있고, 그러면 이 스크립트를 +// 수십 번 다시 부르지 않아도 된다. 천장을 옵션으로 열어 두되, 값이 없으면 위에서 +// 잰 그대로 동작한다. +const envelopeCeiling = Math.max( + 8 * 1024, + Math.floor(Number(pageOptions.maxEnvelopeBytes) || 19 * 1024), +); +const payloadCeiling = pageOptions.maxEnvelopeBytes + ? envelopeCeiling - 1024 + : 18000; const maxPayloadBytes = Math.min( - 18000, + payloadCeiling, Math.max(4096, Math.floor(Number(pageOptions.maxPayloadBytes) || 15000)), ); -const MAX_TEXT_ENVELOPE_BYTES = 19 * 1024; +const MAX_TEXT_ENVELOPE_BYTES = envelopeCeiling; // A field whose value equals its default carries no information the converter // can't recover from the key being absent, so it is dropped from the envelope. diff --git a/crates/devup-mcp-figma/src/scripts/snapshot.js b/crates/devup-mcp-figma/src/scripts/snapshot.js index 44caddc..9643a7b 100644 --- a/crates/devup-mcp-figma/src/scripts/snapshot.js +++ b/crates/devup-mcp-figma/src/scripts/snapshot.js @@ -6,8 +6,14 @@ const manifestSet = new Set(manifest); const textSegmentManifest = "__DEVUP_TEXT_SEGMENT_MANIFEST__"; const snapshotOptions = "__DEVUP_SNAPSHOT__"; const offset = Math.max(0, Math.floor(Number(snapshotOptions.offset) || 0)); +// 이 천장들은 공식 MCP 가 텍스트 결과를 자르는 지점에서 나왔다. 로컬 소켓으로 +// 읽을 때는 자름이 없으므로 한 번에 다 담을 수 있고, 그러면 큰 필드를 따로 +// 받아오는 chunk-read 왕복이 통째로 사라진다. 값이 없으면 기존 그대로다. +const payloadCeiling = snapshotOptions.maxEnvelopeBytes + ? Math.max(8 * 1024, Math.floor(Number(snapshotOptions.maxEnvelopeBytes))) + : 16000; const maxPayloadBytes = Math.min( - 16000, + payloadCeiling, Math.max(4096, Math.floor(Number(snapshotOptions.maxPayloadBytes) || 15000)), ); const maxFieldBytes = Math.min( diff --git a/crates/devup-mcp-figma/src/upstream.rs b/crates/devup-mcp-figma/src/upstream.rs index b1c2199..d4b3409 100644 --- a/crates/devup-mcp-figma/src/upstream.rs +++ b/crates/devup-mcp-figma/src/upstream.rs @@ -279,6 +279,12 @@ struct ScriptInputs<'a> { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SnapshotReadOptions { + /// 봉투 전체가 넘을 수 없는 바이트 수. `0` 이면 스크립트 기본 천장. + /// + /// 공식 MCP 는 텍스트 결과를 자르므로 그 아래로 쪼개 여러 번 읽어야 한다. + /// 로컬 소켓에는 그 자름이 없어 한 번에 담을 수 있고, 그만큼 왕복이 사라진다. + #[serde(default)] + pub max_envelope_bytes: usize, pub offset: usize, pub max_payload_bytes: usize, pub max_field_bytes: usize, @@ -294,6 +300,9 @@ impl Default for SnapshotReadOptions { // up in `scripts/fast_snapshot.js`. max_payload_bytes: 15_000, max_field_bytes: 4_096, + // 0 이면 스크립트가 위에서 잰 천장을 그대로 쓴다. 자름이 없는 전송 + // (로컬 브리지)만 이 값을 올려 한 페이지에 전부 담는다. + max_envelope_bytes: 0, } } } diff --git a/crates/devup-mcp-figma/tests/upstream_contract.rs b/crates/devup-mcp-figma/tests/upstream_contract.rs index 72a43ce..b5c5ce9 100644 --- a/crates/devup-mcp-figma/tests/upstream_contract.rs +++ b/crates/devup-mcp-figma/tests/upstream_contract.rs @@ -65,6 +65,7 @@ fn snapshot_is_byte_bounded_and_cursor_driven() { offset: 7, max_payload_bytes: 12_000, max_field_bytes: 4_096, + ..SnapshotReadOptions::default() }, ); let code = call.arguments()["code"].as_str().unwrap().to_owned(); @@ -395,7 +396,15 @@ fn fast_snapshot_is_paginated_manifest_scoped_and_read_only() { assert!(!code.contains("Math.floor(nodeBudget / 2)")); // The Figma MCP cuts a text result at 20,480 UTF-8 bytes; the page is // packed to 1 KiB under that, measured as the bytes that are cut. - assert!(code.contains("const MAX_TEXT_ENVELOPE_BYTES = 19 * 1024;")); + // + // What has to hold is the 19 KiB bound for this transport, not the literal + // that carries it. A transport that does not cut - the local bridge - hands + // its own ceiling in and reads a screen in one page instead of thirty, so + // the number is a default rather than a constant. + assert!(code.contains("Number(pageOptions.maxEnvelopeBytes) || 19 * 1024")); + assert!(code.contains("const MAX_TEXT_ENVELOPE_BYTES = envelopeCeiling;")); + // Absent an override the packing ceiling is what it was measured to be. + assert!(code.contains(": 18000;")); assert!(code.contains("utf8ByteLength(JSON.stringify(envelope))")); // Item B: PNG-chunked binary transport is gone entirely — text only, // dynamically byte-budgeted and cursor-paginated like the legacy path. diff --git a/plugin/src/code.ts b/plugin/src/code.ts index fa44df1..580b2b9 100644 --- a/plugin/src/code.ts +++ b/plugin/src/code.ts @@ -80,6 +80,42 @@ async function runJob(job: JobMessage): Promise { } } +/** + * 들어온 작업을 순서대로 하나씩 처리한다. + * + * 여러 건이 한꺼번에 와도 받아 두되 동시에 실행하지는 않는다. 스냅샷 스크립트는 + * `figma.setCurrentPageAsync` 로 현재 페이지를 옮기는데, 두 작업이 겹쳐 돌면 + * 서로의 페이지를 갈아치워 엉뚱한 화면을 읽은 결과가 섞여 나온다. 그 오염은 + * 오류로 드러나지 않고 그럴듯한 노드로 돌아오므로 가장 위험하다. + * + * 큐에 쌓아 두는 것만으로도 왕복 지연은 사라진다 — 다음 작업이 이미 도착해 있어 + * 앞 작업이 끝나는 즉시 시작한다. + */ +const queue: JobMessage[] = [] +let draining = false + +async function drain() { + if (draining) return + draining = true + try { + while (queue.length > 0) { + // biome-ignore lint/style/noNonNullAssertion: length 를 확인하고 꺼낸다 + const job = queue.shift()! + const result = await runJob(job).catch( + (error: unknown) => + ({ + kind: 'devup-result', + requestId: job.requestId, + error: error instanceof Error ? error.message : String(error), + }) satisfies ResultMessage, + ) + figma.ui.postMessage(result) + } + } finally { + draining = false + } +} + figma.showUI(__html__, { width: 320, height: 220 }) figma.ui.onmessage = (message: unknown) => { @@ -98,17 +134,9 @@ figma.ui.onmessage = (message: unknown) => { } if (msg.kind === 'devup-job') { - const job = message as JobMessage - // 비동기 결과는 콜백에서 보낸다. onmessage 를 async 로 만들면 Figma 가 - // 반환값을 기다리지 않아 예외가 조용히 사라진다. - runJob(job).then( - (result) => figma.ui.postMessage(result), - (error: unknown) => - figma.ui.postMessage({ - kind: 'devup-result', - requestId: job.requestId, - error: error instanceof Error ? error.message : String(error), - } satisfies ResultMessage), - ) + queue.push(message as JobMessage) + // onmessage 를 async 로 만들면 Figma 가 반환값을 기다리지 않아 예외가 조용히 + // 사라진다. 큐에 넣고 배수는 따로 돌린다. + void drain() } } diff --git a/plugin/src/ui.ts b/plugin/src/ui.ts index a9e15e0..ef03f0c 100644 --- a/plugin/src/ui.ts +++ b/plugin/src/ui.ts @@ -111,5 +111,43 @@ window.onmessage = (event: MessageEvent) => { } } +/** + * 창이 뒤로 가도 이 iframe 이 잠들지 않게 붙잡는다. + * + * Chromium 은 보이지 않는 페이지의 작업 큐를 분당 한 번 수준으로 묶는다. 그래서 + * 사용자가 다른 창을 누르는 순간 읽기 하나가 60초씩 걸렸고, 화면 하나를 받는 데 + * 필요한 수십 번의 왕복이 사실상 멈췄다. 실측한 값이다 — 앞의 8번은 206ms 에 + * 끝나고 9번째가 59초였다. + * + * 소리를 내고 있는 페이지는 그 대상에서 빠진다. 그래서 들리지 않는 소리를 낸다: + * 게인을 0 으로 둔 오실레이터라 스피커로는 아무것도 나가지 않지만, 브라우저에는 + * 재생 중인 페이지로 보인다. + * + * 실패해도 브리지는 그대로 동작한다. 느려질 뿐이므로 조용히 넘어간다. + */ +function keepAwake() { + try { + const Ctor = + window.AudioContext ?? + (window as unknown as { webkitAudioContext?: typeof AudioContext }) + .webkitAudioContext + if (!Ctor) return + const ctx = new Ctor() + const gain = ctx.createGain() + gain.gain.value = 0 + const osc = ctx.createOscillator() + osc.connect(gain) + gain.connect(ctx.destination) + osc.start() + // 사용자 제스처 없이 시작하면 suspended 로 태어난다. 플러그인 창을 연 것 + // 자체가 제스처로 잡히는 경우가 많아 대개 여기서 풀린다. + if (ctx.state === 'suspended') void ctx.resume() + } catch { + // 오디오를 못 쓰는 환경. 포그라운드에서는 여전히 제 속도가 난다. + } +} + +keepAwake() + // 메인 스레드에 파일 정보를 요청하는 것으로 시작한다. parent.postMessage({ pluginMessage: { kind: 'devup-ready' } }, '*') From 05c88c065afe10bf7e2a076b4de47992926cec1c Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 13:56:48 +0900 Subject: [PATCH 12/16] perf: let the transport decide how many resources one question carries Variables and styles were asked for eight at a time and used-resources twelve, because that is what fits in a result the Figma MCP will cut. Over a local socket nothing is cut, and a measured run spent 10.5s and 11.0s on two consecutive UsedResources reads that had no reason to be two reads. The collector holds no upstream - it plans calls and something else makes them - so the budget rides in on the request, set once where the session is built. Fallback reports the bridge's budget whenever a plugin is connected. Batches are cut long before the call, so which transport answers is not known yet; every read that may be batched is a script read, and those go to the bridge while a plugin is there. If one closes mid-collection the remaining batch is too large for the remote and is refused, which is the right outcome - the collection could not have continued anyway. --- crates/devup-mcp-figma/src/bridge.rs | 49 ++++++++++++++++++++++++- crates/devup-mcp-figma/src/collector.rs | 48 ++++++++++++++---------- crates/devup-mcp-figma/src/lib.rs | 2 +- crates/devup-mcp-figma/src/upstream.rs | 29 +++++++++++++++ crates/devup-mcp/src/server/mod.rs | 5 +++ 5 files changed, 111 insertions(+), 22 deletions(-) diff --git a/crates/devup-mcp-figma/src/bridge.rs b/crates/devup-mcp-figma/src/bridge.rs index 29a1665..2e73aaf 100644 --- a/crates/devup-mcp-figma/src/bridge.rs +++ b/crates/devup-mcp-figma/src/bridge.rs @@ -14,7 +14,7 @@ use std::{ collections::HashMap, sync::{ Arc, - atomic::{AtomicU64, Ordering}, + atomic::{AtomicU64, AtomicUsize, Ordering}, }, time::Duration, }; @@ -39,7 +39,7 @@ use tokio::{ use crate::{ errors::{DevupError, ErrorCode}, - upstream::{FigmaUpstream, ReadToolCall, UpstreamResult}, + upstream::{BatchBudget, FigmaUpstream, ReadToolCall, UpstreamResult}, }; /// 플러그인 manifest 의 `allowedDomains` 와 같은 값이어야 한다. 바꾸려면 양쪽을 @@ -106,6 +106,9 @@ struct Inner { pub struct BridgeState { inner: Arc>, counter: Arc, + /// 붙어 있는 플러그인 수. 배치 크기를 정할 때는 잠금을 기다릴 수 없어 + /// (그 자리가 async 가 아니다) 따로 센다. + connected: Arc, } fn unavailable(message: impl Into) -> DevupError { @@ -309,6 +312,10 @@ async fn handle_plugin(state: BridgeState, mut socket: WebSocket) { key.clone(), Connected { outbox: outbox.clone() }, ); + state.connected.store( + state.inner.lock().await.plugins.len(), + Ordering::Relaxed, + ); registered = Some(key); } Some("devup-result") => { @@ -330,6 +337,9 @@ async fn handle_plugin(state: BridgeState, mut socket: WebSocket) { if let Some(key) = registered { state.inner.lock().await.plugins.remove(&key); } + state + .connected + .store(state.inner.lock().await.plugins.len(), Ordering::Relaxed); } /// 브리지 서버. 포트를 잡지 못하면 열지 않으며, 그 경우 호출자는 원격 경로만 쓴다. @@ -402,6 +412,10 @@ impl BridgeServer { #[async_trait] pub trait PreferredUpstream: FigmaUpstream { async fn can_serve(&self, call: &ReadToolCall) -> bool; + + /// 지금 이 상류가 쓸 수 있는 상태인지. 배치 크기를 정하는 자리는 async 가 + /// 아니어서 `can_serve` 를 부를 수 없다. + fn is_live(&self) -> bool; } /// 플러그인을 통해 Figma 를 읽는 `FigmaUpstream`. @@ -440,6 +454,10 @@ impl FigmaUpstream for BridgeFigmaClient { raw: wrap_as_tool_result(&data)?, }) } + + fn batch_budget(&self) -> BatchBudget { + BRIDGE_BATCH + } } #[async_trait] @@ -447,8 +465,20 @@ impl PreferredUpstream for BridgeFigmaClient { async fn can_serve(&self, call: &ReadToolCall) -> bool { call.bridge_job().is_some() && self.state.has_plugin(call.file_key()).await } + + fn is_live(&self) -> bool { + self.state.connected.load(Ordering::Relaxed) > 0 + } } +/// 자르지 않는 전송이므로 쪼갤 이유가 없다. 변수와 스타일을 한 번에 다 묻는다. +const BRIDGE_BATCH: BatchBudget = BatchBudget { + variable_items: 4096, + style_items: 4096, + used_resource_items: 4096, + used_resource_bytes: BRIDGE_ENVELOPE_BYTES, +}; + /// 브리지가 맡을 수 있으면 브리지로, 아니면 원격으로 보낸다. /// /// 판정은 부르기 전에 끝난다. 브리지가 맡은 요청이 실패하면 그 실패를 그대로 @@ -485,4 +515,19 @@ where } self.secondary.call_read_tool(call).await } + + /// 붙어 있는 플러그인이 있으면 그쪽 크기로 묶는다. + /// + /// 배치는 부르기 한참 전에 나뉘므로 어느 전송이 받을지 그때는 알 수 없다. + /// 다만 크게 묶어도 되는 읽기는 전부 스크립트 경로이고, 플러그인이 붙어 있는 + /// 한 그것은 브리지가 받는다. 수집 도중 플러그인이 닫히면 남은 배치가 원격에 + /// 너무 커서 거절되는데, 그때는 수집 자체가 이어질 수 없으므로 실패가 드러나는 + /// 편이 맞다. + fn batch_budget(&self) -> BatchBudget { + if self.preferred.is_live() { + self.preferred.batch_budget() + } else { + self.secondary.batch_budget() + } + } } diff --git a/crates/devup-mcp-figma/src/collector.rs b/crates/devup-mcp-figma/src/collector.rs index 7e11281..3871921 100644 --- a/crates/devup-mcp-figma/src/collector.rs +++ b/crates/devup-mcp-figma/src/collector.rs @@ -14,12 +14,12 @@ use crate::large_values::{ LargeValueResult, descriptors_in_chunk, large_value_from_result, replace_descriptor, }; use crate::{ - AssetExportOutcome, AssetManifestEntry, AssetRequest, AssetSelection, AssetStatus, BatchLimits, - BuiltinScript, DevupError, ErrorCode, ExploreReadOptions, FigmaTarget, LargeValueAssembler, - LargeValueReadOptions, RawNode, ReadToolCall, ResourceBatch, ResourceScope, ResourceStyleRef, - SNAPSHOT_CURSOR_ID, SearchReadOptions, SectionIndex, SnapshotChunk, SnapshotCursor, - SnapshotReadOptions, UnresolvedResource, UpstreamResult, UsedResourceRefs, - asset_export_from_result, build_section_index, collect_used_resource_refs, + AssetExportOutcome, AssetManifestEntry, AssetRequest, AssetSelection, AssetStatus, BatchBudget, + BatchLimits, BuiltinScript, DevupError, ErrorCode, ExploreReadOptions, FigmaTarget, + LargeValueAssembler, LargeValueReadOptions, RawNode, ReadToolCall, ResourceBatch, + ResourceScope, ResourceStyleRef, SNAPSHOT_CURSOR_ID, SearchReadOptions, SectionIndex, + SnapshotChunk, SnapshotCursor, SnapshotReadOptions, UnresolvedResource, UpstreamResult, + UsedResourceRefs, asset_export_from_result, build_section_index, collect_used_resource_refs, decode_fast_multi_snapshot, decode_fast_snapshot, decode_fast_theme, exported_asset_from_bytes, merge_chunks, metadata::{MetadataResult, metadata_from_result_for_target}, @@ -33,8 +33,6 @@ use crate::{ const LARGE_SUBTREE_THRESHOLD: usize = 200; const MAX_PENDING_CALLS: usize = 4; -const VARIABLE_BATCH_SIZE: usize = 8; -const STYLE_BATCH_SIZE: usize = 8; // The byte budget below measures the *request* — the IDs — and the ceiling // that matters is on the *response*: the official MCP cuts a text result at // 20,480 UTF-8 bytes (measured; see `scripts/fast_snapshot.js`) and appends @@ -42,8 +40,6 @@ const STYLE_BATCH_SIZE: usize = 8; // codeSyntax is five hundred to a thousand bytes on the way back, so the // sixty-four this allowed could not fit; thirty-eight were measured filling // the cap exactly. Twelve leaves the response near a third of it. -const USED_RESOURCE_BATCH_ITEMS: usize = 12; -const USED_RESOURCE_BATCH_BYTES: usize = 12_000; // Consumer relations can be huge. Compact, bounded fragments are expanded // back to the exhaustive shape in Rust without dropping any relation. const STYLE_CONSUMER_BATCH_SIZE: usize = 320; @@ -63,6 +59,8 @@ pub enum CollectionScope { #[derive(Debug, Clone)] pub struct CollectionRequest { + /// 이 수집을 실어 나를 전송이 한 번에 받을 수 있는 양. + pub batch_budget: BatchBudget, pub target: FigmaTarget, pub scope: CollectionScope, pub resource_scope: ResourceScope, @@ -91,6 +89,9 @@ impl CollectionRequest { Self { target, scope, + // 자르는 전송을 전제로 한 값. 브리지로 수집할 때는 호출자가 + // `FigmaUpstream::batch_budget` 에서 받아 덮어쓴다. + batch_budget: BatchBudget::default(), resource_scope: ResourceScope::None, include_context: false, metadata_only: false, @@ -1940,7 +1941,10 @@ impl CollectorSession { let node_id = self.root_node_id.clone().ok_or_else(|| { invalid_call("No root node ID available for the Figma variable batch.") })?; - for variable_ids in catalog.variable_ids.chunks(VARIABLE_BATCH_SIZE) { + for variable_ids in catalog + .variable_ids + .chunks(self.request.batch_budget.variable_items.max(1)) + { self.enqueue( ReadToolCall::resource_batch( &self.request.target.file_key, @@ -1954,7 +1958,10 @@ impl CollectorSession { CallKind::VariableBatch, ); } - for styles in catalog.styles.chunks(STYLE_BATCH_SIZE) { + for styles in catalog + .styles + .chunks(self.request.batch_budget.style_items.max(1)) + { self.enqueue( ReadToolCall::resource_batch( &self.request.target.file_key, @@ -1991,7 +1998,7 @@ impl CollectorSession { let node_id = self.root_node_id.clone().ok_or_else(|| { invalid_call("No root node ID available for the used Figma resource batch.") })?; - for batch in used_resource_batches(&refs)? { + for batch in used_resource_batches(&refs, self.request.batch_budget)? { self.enqueue( ReadToolCall::used_resources(&self.request.target.file_key, &node_id, batch), Some(node_id.clone()), @@ -2411,7 +2418,10 @@ enum UsedResourceItem { Style(ResourceStyleRef), } -fn used_resource_batches(refs: &UsedResourceRefs) -> Result, DevupError> { +fn used_resource_batches( + refs: &UsedResourceRefs, + budget: BatchBudget, +) -> Result, DevupError> { let items = refs .variable_ids .iter() @@ -2427,7 +2437,7 @@ fn used_resource_batches(refs: &UsedResourceRefs) -> Result, for item in items { let mut candidate = current.clone(); add_used_resource(&mut candidate, item.clone()); - if used_resource_batch_fits(&candidate) { + if used_resource_batch_fits(&candidate, budget) { current = candidate; continue; } @@ -2444,7 +2454,7 @@ fn used_resource_batches(refs: &UsedResourceRefs) -> Result, styles: Vec::new(), }; add_used_resource(&mut current, item); - if !used_resource_batch_fits(¤t) { + if !used_resource_batch_fits(¤t, budget) { return Err(DevupError::new( ErrorCode::DevupFigmaResponseTooLarge, "A single Figma resource ID exceeds the safe batch size.", @@ -2465,9 +2475,9 @@ fn add_used_resource(batch: &mut ResourceBatch, item: UsedResourceItem) { } } -fn used_resource_batch_fits(batch: &ResourceBatch) -> bool { - batch.variable_ids.len() + batch.styles.len() <= USED_RESOURCE_BATCH_ITEMS - && serde_json::to_vec(batch).is_ok_and(|bytes| bytes.len() <= USED_RESOURCE_BATCH_BYTES) +fn used_resource_batch_fits(batch: &ResourceBatch, budget: BatchBudget) -> bool { + batch.variable_ids.len() + batch.styles.len() <= budget.used_resource_items + && serde_json::to_vec(batch).is_ok_and(|bytes| bytes.len() <= budget.used_resource_bytes) } #[cfg(test)] diff --git a/crates/devup-mcp-figma/src/lib.rs b/crates/devup-mcp-figma/src/lib.rs index 757ae3d..f18a845 100644 --- a/crates/devup-mcp-figma/src/lib.rs +++ b/crates/devup-mcp-figma/src/lib.rs @@ -72,7 +72,7 @@ pub use source::{ UpstreamFailureContext, UpstreamFailureKind, classify_upstream_failure, upstream_failure_error, }; pub use upstream::{ - BuiltinScript, ExploreReadOptions, FigmaUpstream, ReadToolCall, RemoteFigmaClient, + BatchBudget, BuiltinScript, ExploreReadOptions, FigmaUpstream, ReadToolCall, RemoteFigmaClient, SearchReadOptions, SnapshotReadOptions, UpstreamResult, }; pub use url::FigmaTarget; diff --git a/crates/devup-mcp-figma/src/upstream.rs b/crates/devup-mcp-figma/src/upstream.rs index d4b3409..1e35c75 100644 --- a/crates/devup-mcp-figma/src/upstream.rs +++ b/crates/devup-mcp-figma/src/upstream.rs @@ -796,10 +796,39 @@ pub struct UpstreamResult { pub raw: Value, } +/// 리소스를 몇 개씩 묶어 물어볼 수 있는지. +/// +/// 기본값은 공식 MCP 가 텍스트 결과를 자르는 지점에서 나왔다. 변수 8개, 스타일 +/// 8개씩 끊어 묻느라 화면 하나에 수십 번을 오가고, 그 왕복 하나하나가 창이 뒤에 +/// 있을 때 10초를 넘는다. 자르지 않는 전송은 한 번에 다 물어도 된다. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BatchBudget { + pub variable_items: usize, + pub style_items: usize, + pub used_resource_items: usize, + pub used_resource_bytes: usize, +} + +impl Default for BatchBudget { + fn default() -> Self { + Self { + variable_items: 8, + style_items: 8, + used_resource_items: 12, + used_resource_bytes: 12_000, + } + } +} + #[async_trait] pub trait FigmaUpstream: Send + Sync { async fn list_tools(&self) -> Result, DevupError>; async fn call_read_tool(&self, call: ReadToolCall) -> Result; + + /// 이 전송으로 한 번에 물어볼 수 있는 양. + fn batch_budget(&self) -> BatchBudget { + BatchBudget::default() + } } #[derive(Clone)] diff --git a/crates/devup-mcp/src/server/mod.rs b/crates/devup-mcp/src/server/mod.rs index 909a49e..9f3c5af 100644 --- a/crates/devup-mcp/src/server/mod.rs +++ b/crates/devup-mcp/src/server/mod.rs @@ -571,6 +571,11 @@ impl DevupServer { validation::validate_export_budget(&index.select(&[], true)?, outputs)?; } let target = request.target.clone(); + // 리소스를 몇 개씩 묶어 물을지는 이 수집을 실어 나를 전송이 정한다. + // 공식 MCP 는 응답을 자르므로 8개씩 끊지만, 자르지 않는 전송은 한 번에 + // 다 물어도 되고 그만큼 왕복이 사라진다. + let mut request = request; + request.batch_budget = self.services.upstream.batch_budget(); let mut collector = CollectorSession::new(request); loop { if let Some(job) = job { From 9eedb4bfd7536795540b03c2f033dcfab6af3479 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 14:26:20 +0900 Subject: [PATCH 13/16] test: add a handle to shrink the bridge's used-resource batch Every variable on a real screen came back unresolved - 33 of 33 - and the generated TSX named them after the tail of their id, so a bound colour read g="$842" from VariableID:495:842. used_resources.js is the resolver itself, not a discovery pass: it calls getVariableByIdAsync per id and reports the nulls as unresolved. So nothing was skipped; Figma returned nothing for all 33. Two things could do that, and they are fixed in different places. The batch is now 33 in one call where it used to be 12, so the plugin may not survive that many concurrent lookups. Or the file's variables live in a shared library the plugin has no permission to read - the ids include VariableID:<40-hex>/1140:6, which is a library variable. This env var lets the first one be ruled out by restarting rather than rebuilding. It is a diagnostic handle, not a setting anyone should need; the default stays one call. --- crates/devup-mcp-figma/src/bridge.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/devup-mcp-figma/src/bridge.rs b/crates/devup-mcp-figma/src/bridge.rs index 2e73aaf..e264cf5 100644 --- a/crates/devup-mcp-figma/src/bridge.rs +++ b/crates/devup-mcp-figma/src/bridge.rs @@ -456,7 +456,7 @@ impl FigmaUpstream for BridgeFigmaClient { } fn batch_budget(&self) -> BatchBudget { - BRIDGE_BATCH + bridge_batch_budget() } } @@ -479,6 +479,25 @@ const BRIDGE_BATCH: BatchBudget = BatchBudget { used_resource_bytes: BRIDGE_ENVELOPE_BYTES, }; +/// 한 번에 물을 리소스 수를 밖에서 낮춰 보기 위한 손잡이. +/// +/// 리소스가 하나도 해석되지 않는 것을 실기기에서 봤고, 원인이 이 크기인지 +/// (플러그인이 그만한 동시 조회를 감당하지 못하는지) 아니면 애초에 그 변수를 +/// 읽을 권한이 없는지 가려야 했다. 둘은 고치는 곳이 다르다. +const BRIDGE_USED_RESOURCE_ITEMS_ENV: &str = "DEVUP_FIGMA_BRIDGE_USED_RESOURCE_ITEMS"; + +fn bridge_batch_budget() -> BatchBudget { + let mut budget = BRIDGE_BATCH; + if let Some(items) = std::env::var(BRIDGE_USED_RESOURCE_ITEMS_ENV) + .ok() + .and_then(|raw| raw.trim().parse::().ok()) + .filter(|items| *items > 0) + { + budget.used_resource_items = items; + } + budget +} + /// 브리지가 맡을 수 있으면 브리지로, 아니면 원격으로 보낸다. /// /// 판정은 부르기 전에 끝난다. 브리지가 맡은 요청이 실패하면 그 실패를 그대로 From 3499b9edc68b85399b4fbc9812cdaa87ec7cc5a4 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 14:53:36 +0900 Subject: [PATCH 14/16] fix: resolve a file's own variables from its list, not one id at a time Every variable on two real screens came back unresolved, and the TSX named them after the tail of their id: a bound colour read g="$842" from VariableID:495:842. Ten such tokens, none of them in devup.json. The timings said what it was. UsedResources took 21421, 21471, 21919, 22014 and 21714 ms across five runs - 33 resources on one screen, 40 on another, and the same 21.5s either way. The lookups run under Promise.all, so the total is the slowest single one, not the sum: each getVariableByIdAsync was waiting out the same fixed timeout and then returning null. Meanwhile VariableCatalog, which lists the file's own collections and styles, answered in 12ms on the same file through the same transport. So ask the file for its variables once and index them by id. Only ids that are genuinely not in the file - library variables, whose ids carry a VariableID:<40-hex>/1140:6 shape - still need an individual lookup, and those now say which way they failed instead of sharing one reason with everything else. The contract test asserted this script never calls getLocalVariablesAsync. That ban existed to keep the script from hauling back everything in the file when it was asked for a few ids, and that still holds: the list is an index here, and the response is built from the requested ids alone. The test now checks that fact rather than the name of a call. --- .../src/scripts/used_resources.js | 28 +++++++++++++++++-- .../devup-mcp-figma/tests/used_resources.rs | 13 ++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/devup-mcp-figma/src/scripts/used_resources.js b/crates/devup-mcp-figma/src/scripts/used_resources.js index c3a786d..d86a6d1 100644 --- a/crates/devup-mcp-figma/src/scripts/used_resources.js +++ b/crates/devup-mcp-figma/src/scripts/used_resources.js @@ -34,14 +34,35 @@ function serialize(value, seen = new WeakSet(), depth = 0) { return result; } +// 파일에 있는 변수는 한 번에 다 받아 두고 id 로 찾는다. +// +// 하나씩 `getVariableByIdAsync` 로 묻는 것이 이 파일에서는 건당 21.5초를 쓰고 +// 끝내 null 을 냈다. 33개든 40개든 병렬이라 총 시간이 21.5초로 같았던 것이 +// 단서였다 ? 작업이 아니라 건당 대기였다. 같은 순간 지역 목록을 받는 호출은 +// 12ms 에 돌아온다. 그래서 그쪽을 먼저 쓴다. +const localVariableIndex = new Map(); +let localVariableListing = "ok"; +try { + for (const variable of await figma.variables.getLocalVariablesAsync()) { + localVariableIndex.set(variable.id, variable); + } +} catch (_error) { + localVariableListing = "unavailable"; +} + const variableResults = await Promise.all(resources.variableIds.map(async (id) => { + const local = localVariableIndex.get(id); + if (local) { + return { value: serialize(local), collectionId: local.variableCollectionId }; + } + // 목록에 없으면 이 파일 것이 아니다(라이브러리 변수). 그때만 개별로 묻는다. try { const value = await figma.variables.getVariableByIdAsync(id); return value ? { value: serialize(value), collectionId: value.variableCollectionId } - : { unresolved: { id, kind: "variable", reason: "notFoundOrUnavailable" } }; + : { unresolved: { id, kind: "variable", reason: "notInFileAndLookupEmpty" } }; } catch (_error) { - return { unresolved: { id, kind: "variable", reason: "notFoundOrUnavailable" } }; + return { unresolved: { id, kind: "variable", reason: "notInFileAndLookupThrew" } }; } })); @@ -85,6 +106,9 @@ return { styles: styleResults.flatMap((result) => result.value ? [result.value] : []), usedVariableIds: resources.variableIds, usedStyleIds: resources.styles.map((style) => style.id), + // 아무것도 해석되지 않았을 때 목록 자체를 못 받은 것인지 구분하기 위해 남긴다. + localVariableListing, + localVariableCount: localVariableIndex.size, unresolved: [...variableResults, ...styleResults] .flatMap((result) => result.unresolved ? [result.unresolved] : []) }; diff --git a/crates/devup-mcp-figma/tests/used_resources.rs b/crates/devup-mcp-figma/tests/used_resources.rs index a92aa73..33923fe 100644 --- a/crates/devup-mcp-figma/tests/used_resources.rs +++ b/crates/devup-mcp-figma/tests/used_resources.rs @@ -35,8 +35,19 @@ fn exact_id_script_fetches_used_resources_without_style_consumers() { assert!(code.contains("getVariableCollectionByIdAsync")); assert!(code.contains("getStyleByIdAsync")); assert!(code.contains("unresolved")); - assert!(!code.contains("getLocalVariablesAsync")); assert!(!code.contains("getStyleConsumersAsync")); + + // 이 스크립트가 지켜야 하는 것은 "요청한 것만 돌려준다"이고, 그것은 + // 출력이 요청한 id 로만 만들어지는지로 확인한다. + // + // 전에는 `getLocalVariablesAsync` 를 부르지 않는지를 봤다. 그 호출을 + // 금지한 이유는 파일에 있는 것을 통째로 실어 응답이 커지는 것을 막는 + // 데 있었는데, 실제로 이 파일에서 재보니 개별 조회가 건당 21.5초를 쓰고 + // 끝내 null 을 내는 동안 지역 목록 한 번은 12ms 에 돌아왔다. 지금은 그 + // 목록을 색인으로만 쓰고 응답에는 요청한 id 의 결과만 담으므로, 막으려던 + // 것은 여전히 막혀 있다. 호출 이름이 아니라 그 사실을 검사한다. + assert!(code.contains("usedVariableIds: resources.variableIds")); + assert!(code.contains("usedStyleIds: resources.styles.map")); } #[test] From 696dd376bd2f941d4fbabb9f186e262177214d5b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 22:33:16 +0900 Subject: [PATCH 15/16] docs: say how to install the bridge plugin in the main README chore: record the bridge transport as a minor changepack --- .changepacks/changepack_log_figma_bridge.json | 8 +++ README.md | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 .changepacks/changepack_log_figma_bridge.json diff --git a/.changepacks/changepack_log_figma_bridge.json b/.changepacks/changepack_log_figma_bridge.json new file mode 100644 index 0000000..9128a4c --- /dev/null +++ b/.changepacks/changepack_log_figma_bridge.json @@ -0,0 +1,8 @@ +{ + "changes": { + "crates/devup-mcp-figma/Cargo.toml": "Minor", + "crates/devup-mcp/Cargo.toml": "Minor" + }, + "note": "Read a design without spending the quota. Collection runs through the official MCP's use_figma, and that is the call Figma meters, so a handful of screens exhausts the allowance; every script devup-mcp sends uses only the standard Plugin API, so a plugin of our own can serve them. A plugin cannot accept connections, so devup-mcp listens on 127.0.0.1 and the plugin dials in, and a read is routed before the call by asking whether the bridge can serve it rather than by catching a failure, because CapabilityUnavailable and a transport fault arrive as the same error code and a failure cannot tell them apart. No code crosses the socket: the twelve scripts are wrapped into the plugin at build time from the same sources the remote path uses, the build fails on an unsubstituted placeholder, and devup-mcp sends only the name of the script to run. With no plugin attached every call falls through to the remote path unchanged, and DEVUP_FIGMA_BRIDGE_PORT=off closes the door entirely. Three things only a real install revealed are now handled: ws://127.0.0.1 invalidates the whole manifest so the address must be localhost, a plugin that reports no file key registers and serves any key while it is the only one attached rather than leaving the window claiming a connection it never uses, and a fileKey permission entry stops the plugin loading at all. Speed followed: pagination sized for a 20,480-byte truncation collapses to one page over a local socket, the transport now says how many resources one question carries instead of a constant, and a used-resource call that took 21.4 seconds regardless of load turned out to be waiting out a fixed timeout on every individual variable lookup and returning null, which named every bound token after the tail of its id; the file's own variables are read once and indexed, leaving individual lookups only for genuine library ids, and that call now answers in 24 milliseconds with nothing unresolved. Asset export still fails with the same fixed-delay signature and is not fixed here.", + "date": "2026-09-13T23:40:00+09:00" +} diff --git a/README.md b/README.md index 4a189a4..1d37ce3 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,61 @@ Figma MCP Catalog에 승인된 client(예: 직접 waitlist로 등록해 발급 devup-mcp가 Figma에 붙는 경로는 하나입니다 — **원격 OAuth (`direct`)**. `devup_figma_auth { action: "login" }`으로 브라우저 인증. Figma MCP Catalog에 승인된 client만 등록할 수 있습니다. 현재 사용 가능한지는 `devup_figma_auth { action: "doctor" }`로 확인하세요. +### 브리지 플러그인 — 한도를 쓰지 않고 읽기 + +원격 OAuth 경로는 Figma가 **사용량을 셉니다.** devup-mcp의 수집은 화면 하나에 snapshot을 여러 번 부르므로 한도가 금방 바닥납니다. + +그래서 **우리가 직접 만든 Figma 플러그인**을 통해 같은 읽기를 할 수 있습니다. 이 경로는 한도를 쓰지 않습니다. 플러그인이 붙어 있으면 스크립트 읽기가 그쪽으로 가고, **안 붙어 있으면 아무 일도 일어나지 않고 그대로 원격 경로로** 갑니다. 설치하지 않은 사람의 동작은 바뀌지 않습니다. + +#### 설치 — 앞은 자동, 마지막 한 단계는 수동입니다 + +**1. 플러그인을 빌드합니다** (명령 하나) + +```bash +cd plugin +npm install +npm run build +``` + +**2. Figma 데스크톱 앱에서 불러옵니다** ← **이 단계는 자동화할 수 없습니다** + +Figma의 플러그인 등록은 데스크톱 앱의 메뉴 동작이라 스크립트로 대신할 수 없습니다. 직접 하셔야 합니다. + +1. **Figma 데스크톱 앱**을 엽니다 (브라우저판은 로컬 플러그인을 못 불러옵니다) +2. 메뉴에서 **Plugins → Development → Import plugin from manifest...** +3. 이 저장소의 **`plugin/manifest.json`** 을 선택합니다 + +한 번만 하면 됩니다. 이후에는 플러그인 목록에 `Devup Bridge`로 남습니다. + +**3. 읽으려는 파일에서 플러그인을 실행합니다** + +Figma에서 대상 파일을 열고 `Devup Bridge`를 실행하면 창이 하나 뜹니다. **그 창이 열려 있는 동안만** 브리지가 동작합니다. 닫으면 다음 읽기부터 원격 경로로 돌아갑니다. + +#### 잘 되고 있는지 확인 + +플러그인 창의 표시등으로 판단합니다. + +| 표시 | 뜻 | +|---|---| +| 초록 | devup-mcp에 연결됨. 이 파일의 읽기는 한도를 쓰지 않습니다 | +| 회색 | devup-mcp를 찾지 못함. 2초마다 다시 시도합니다 | + +회색에서 안 바뀌면 devup-mcp가 실행 중인지, 그리고 `DEVUP_FIGMA_BRIDGE_PORT=off`로 꺼 두지 않았는지 확인하세요. + +#### 알아 두어야 할 것 + +- **기본으로 켜져 있습니다.** devup-mcp는 시작할 때 `127.0.0.1:1993`에 대기하고, 그 포트를 잡지 못하면 조용히 원격 경로만 씁니다. 끄려면 `DEVUP_FIGMA_BRIDGE_PORT=off`. +- **읽기 전용입니다.** 플러그인이 실행하는 스크립트는 **빌드 시점에 플러그인 안에 박혀 있고**, devup-mcp는 그중 어느 것을 실행할지 **이름만** 보냅니다. 소켓으로 코드가 오가지 않으며 Figma 문서를 바꾸는 호출은 존재하지 않습니다. +- **이 기기에서만 됩니다.** 대기 주소는 `127.0.0.1`이라 다른 기기에서는 붙을 수 없고, 원격 CI에서는 브리지가 없으니 자동으로 원격 경로를 씁니다. +- **파일은 한 번에 하나입니다.** 플러그인이 자기 파일 키를 보고하지 못하는 경우가 있어(Dev Mode 등), 키 없는 플러그인은 **혼자 붙어 있을 때만** 읽기를 받습니다. 두 개 이상이면 어느 파일인지 알 수 없으므로 원격 경로로 넘어갑니다. +- **포트를 바꾸려면 세 곳을 같이 고쳐야 합니다.** Figma는 플러그인이 접속할 수 있는 주소를 manifest에 미리 적어 두게 하고 실행 중에는 바꿀 수 없습니다. `manifest.json`의 `allowedDomains`, `src/code.ts`의 `PORT`, 그리고 환경 변수를 모두 고친 뒤 플러그인을 다시 빌드·설치해야 합니다. 하나만 바꾸면 **아무 말 없이 원격 경로로 돌아가** 한도를 그대로 씁니다. +- **`ws://127.0.0.1`은 쓸 수 없습니다.** Figma의 `allowedDomains`가 그 주소를 거부해 manifest 전체가 무효가 되고 플러그인이 아예 로드되지 않습니다. `localhost`여야 합니다. +- **Figma 창을 뒤로 보내면 느려집니다.** 브라우저가 백그라운드 플러그인을 분당 한 번꼴로 깨우기 때문에, 앞에서 160ms인 snapshot이 뒤에서는 11초가 됩니다. 수집 중에는 Figma를 앞에 두세요. + +자세한 내부 구조와 스크립트 생성 방식은 [`plugin/README.md`](plugin/README.md)에 있습니다. + +### 로컬 Dev Mode MCP를 쓰지 않는 이유 + Figma 데스크톱 앱의 로컬 Dev Mode MCP(`http://127.0.0.1:3845/mcp`)는 세 번째 경로로 안내했으나 제거했습니다. 읽기 도구 6개(`get_design_context`, `get_variable_defs`, `get_screenshot`, `get_motion_context`, `get_metadata`, `get_figjam`)만 제공하고 그중에 `use_figma`가 없습니다. devup-mcp의 수집은 snapshot·explore·section index·theme 모두 `use_figma`로 스크립트를 실행하므로 로컬에서는 실행할 도구 자체가 없습니다. 도구들이 `fileKey`를 받지 않고 데스크톱 앱에 열려 있는 파일만 가리키는 것도 같은 이유로 맞지 않습니다. "OAuth 없이 바로 쓸 수 있다"는 안내는 확신에 차서 틀린 안내였고, 믿은 쪽이 한 턴을 버린 뒤에야 알게 됩니다. ### 원격 OAuth 등록 제약 (실측) From e23906228a4a3c8b1a2e089643562b410a39a28c Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 13 Sep 2026 22:37:38 +0900 Subject: [PATCH 16/16] feat: ship the built bridge plugin so Figma can import it directly --- .github/workflows/ci.yml | 15 +++++++++++++ README.md | 18 ++++++--------- plugin/.gitignore | 5 ++++- plugin/README.md | 23 ++++++++++++++----- plugin/dist/code.js | 1 + plugin/dist/ui.html | 48 ++++++++++++++++++++++++++++++++++++++++ plugin/rspack.config.js | 4 ++++ 7 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 plugin/dist/code.js create mode 100644 plugin/dist/ui.html diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08bed55..24dcfc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,21 @@ jobs: - run: cargo install cargo-insta --version 1.48.0 --locked - run: cargo fmt --all -- --check - run: node --test crates/devup-mcp-figma/tests/explore_script_behavior.mjs + # `plugin/dist/` is committed so a user can import the plugin into Figma + # without running a build. That only stays true if the committed bundle + # is the one this source produces, so rebuild it and refuse a difference. + # The build is reproducible: same input, byte-identical output. + - name: The committed plugin bundle matches its source + if: matrix.os == 'ubuntu-latest' + shell: bash + working-directory: plugin + run: | + npm ci + npm run build + git diff --exit-code -- dist || { + echo "::error::plugin/dist is stale. Run 'npm run build' in plugin/ and commit the result." + exit 1 + } - run: cargo test --locked -p devup-mcp --test stdio_smoke - run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings - run: cargo insta test --workspace --all-features --check diff --git a/README.md b/README.md index 1d37ce3..c5418b4 100644 --- a/README.md +++ b/README.md @@ -193,19 +193,13 @@ devup-mcp가 Figma에 붙는 경로는 하나입니다 — **원격 OAuth (`dire 그래서 **우리가 직접 만든 Figma 플러그인**을 통해 같은 읽기를 할 수 있습니다. 이 경로는 한도를 쓰지 않습니다. 플러그인이 붙어 있으면 스크립트 읽기가 그쪽으로 가고, **안 붙어 있으면 아무 일도 일어나지 않고 그대로 원격 경로로** 갑니다. 설치하지 않은 사람의 동작은 바뀌지 않습니다. -#### 설치 — 앞은 자동, 마지막 한 단계는 수동입니다 +#### 설치 — 빌드할 필요 없습니다 -**1. 플러그인을 빌드합니다** (명령 하나) +빌드된 플러그인이 저장소에 들어 있습니다(`plugin/dist/`). `npm`도 Node도 필요 없고, 저장소를 받아 두었다면 바로 다음 단계로 갑니다. -```bash -cd plugin -npm install -npm run build -``` +**1. Figma 데스크톱 앱에서 불러옵니다** -**2. Figma 데스크톱 앱에서 불러옵니다** ← **이 단계는 자동화할 수 없습니다** - -Figma의 플러그인 등록은 데스크톱 앱의 메뉴 동작이라 스크립트로 대신할 수 없습니다. 직접 하셔야 합니다. +Figma의 플러그인 등록은 데스크톱 앱의 메뉴 동작이라 스크립트로 대신할 수 없습니다. 이 세 단계만 직접 하시면 됩니다. 1. **Figma 데스크톱 앱**을 엽니다 (브라우저판은 로컬 플러그인을 못 불러옵니다) 2. 메뉴에서 **Plugins → Development → Import plugin from manifest...** @@ -213,7 +207,9 @@ Figma의 플러그인 등록은 데스크톱 앱의 메뉴 동작이라 스크 한 번만 하면 됩니다. 이후에는 플러그인 목록에 `Devup Bridge`로 남습니다. -**3. 읽으려는 파일에서 플러그인을 실행합니다** +> 커밋된 번들이 소스와 어긋나지 않도록 CI가 매번 다시 빌드해 byte 단위로 대조합니다. 플러그인 소스를 고쳤다면 `cd plugin && npm install && npm run build` 후 `dist/`를 함께 커밋해야 합니다. + +**2. 읽으려는 파일에서 플러그인을 실행합니다** Figma에서 대상 파일을 열고 `Devup Bridge`를 실행하면 창이 하나 뜹니다. **그 창이 열려 있는 동안만** 브리지가 동작합니다. 닫으면 다음 읽기부터 원격 경로로 돌아갑니다. diff --git a/plugin/.gitignore b/plugin/.gitignore index 17a4278..9dae464 100644 --- a/plugin/.gitignore +++ b/plugin/.gitignore @@ -1,6 +1,9 @@ node_modules/ -dist/ # devup-mcp 의 스크립트에서 빌드 타임에 생성됩니다(`npm run gen`). # 커밋하면 원본과 어긋난 사본이 돌아다니게 됩니다. src/generated/ + +# dist/ 는 의도적으로 커밋합니다. Figma 는 manifest 가 가리키는 실제 번들 +# 파일을 읽으므로, 커밋해 두면 받는 사람이 npm 없이 곧장 import 할 수 있습니다. +# 원본과 어긋나지 않도록 CI(`plugin bundle`)가 다시 빌드해 byte 단위로 대조합니다. diff --git a/plugin/README.md b/plugin/README.md index 131d684..f6319d0 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -20,16 +20,25 @@ getLocalPaintStylesAsync / TextStylesAsync / EffectStylesAsync / GridStylesAsync ## 설치 +빌드할 필요 없습니다. `dist/` 가 저장소에 들어 있습니다. + +Figma 데스크톱 앱에서 **Plugins → Development → Import plugin from manifest** 를 +고르고 `plugin/manifest.json` 을 선택하면 끝입니다. + +브라우저판에서는 개발 플러그인을 불러올 수 없으므로 데스크톱 앱이 필요합니다. + +### 플러그인 소스를 고쳤다면 + ```bash cd plugin npm install -npm run build +npm run build # dist/ 를 다시 만든다 — 함께 커밋해야 한다 ``` -그다음 Figma 데스크톱 앱에서 **Plugins → Development → Import plugin from -manifest** 를 고르고 `plugin/manifest.json` 을 선택합니다. - -브라우저판에서는 개발 플러그인을 불러올 수 없으므로 데스크톱 앱이 필요합니다. +`dist/` 는 의도적으로 커밋합니다. 받는 사람이 Node 없이 곧장 import 할 수 있게 +하려는 것이고, 그 대가로 원본과 어긋날 위험이 생기므로 CI 가 매번 다시 빌드해 +`git diff --exit-code -- dist` 로 대조합니다. 빌드는 재현 가능합니다 — 같은 입력에서 +같은 바이트가 나옵니다. 어긋난 채로는 병합되지 않습니다. ## 쓰는 법 @@ -85,7 +94,9 @@ devup-mcp 쪽은 아무 설정도 필요 없습니다. 플러그인이 붙어 ``` crates/devup-mcp-figma/src/scripts/*.js ← 원본 (공식 MCP 경로도 이것을 씀) ↓ plugin/scripts/gen-scripts.mjs -plugin/src/generated/scripts.js ← 빌드 산출물 (커밋하지 않음) +plugin/src/generated/scripts.js ← 중간 산출물 (커밋하지 않음) + ↓ rspack + scripts/inline-ui.mjs +plugin/dist/{code.js,ui.html} ← 최종 번들 (커밋함, CI 가 대조) ``` 두 경로가 같은 소스를 쓰므로 봉투가 갈라질 수 없습니다. 원본의 플레이스홀더는 diff --git a/plugin/dist/code.js b/plugin/dist/code.js new file mode 100644 index 0000000..353b1a7 --- /dev/null +++ b/plugin/dist/code.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={},t={};function r(n){var i=t[n];if(void 0!==i)return i.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,r),a.exports}r.rv=()=>"1.7.12",r.ruid="bundler=rspack@1.7.12";let n={assets:async function(e){let t=e.asset;function r(e){return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:t.scale,status:"failed",byteLength:null,sha256:null,errorCode:e}}try{let e=await figma.getNodeByIdAsync(t.nodeId);if(!e||"function"!=typeof e.exportAsync)return r("DEVUP_ASSET_UNSUPPORTED_BY_UPSTREAM");if("string"==typeof t.field&&t.field.startsWith("fills/")){let n=Number(t.field.slice(6)),i="fills"in e&&Array.isArray(e.fills)?e.fills:[],a=Number.isInteger(n)?i[n]:null,l=a&&"IMAGE"===a.type?a.imageHash||a.imageRef:null;if(!a||l!==t.imageHash)return r("DEVUP_ASSET_SOURCE_CHANGED")}else if("node"!==t.field)return r("DEVUP_ASSET_FIELD_UNSUPPORTED");let n=String(t.format||"").toUpperCase();if(!["PNG","JPG","SVG","PDF"].includes(n))return r("DEVUP_ASSET_FORMAT_UNSUPPORTED");let i=Math.min(4,Math.max(1,Math.floor(Number(t.scale)||1))),a="SVG"===n,l={format:a?"SVG_STRING":n};("PNG"===n||"JPG"===n)&&(l.constraint={type:"SCALE",value:i});let o=await e.exportAsync(l),s=a&&"string"==typeof o?o:null,d=null===s?o instanceof Uint8Array?o:new Uint8Array(o):function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(s);if(0===d.length||d.length>8388608)return r("DEVUP_ASSET_RESPONSE_TOO_LARGE");let f=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,o,s,d,f,u,c,y]=a;for(let e=0;e<64;e+=1){let n=l(f,6)^l(f,11)^l(f,25),a=y+n+(f&u^~f&c)+t[e]+r[e]>>>0,h=(l(i,2)^l(i,13)^l(i,22))+(i&o^i&s^o&s)>>>0;y=c,c=u,u=f,f=d+a>>>0,d=s,s=o,o=i,i=a+h>>>0}for(let[e,t]of[i,o,s,d,f,u,c,y].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(d),u="PNG"===n&&d.length>786432;if(null!==s&&d.length>12288||u)return{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:i,status:"chunked",byteLength:d.length,sha256:f,cursor:{nextOffset:0,maxChunkBytes:12288},errorCode:null};return figma.io.write(`devup-asset-${t.assetId.replace(/[^A-Za-z0-9_-]/g,"_")}.${String(t.format).toLowerCase()}`,d),{kind:"devupAssetExport",fileKey:figma.fileKey||"",version:t.version,assetId:t.assetId,nodeId:t.nodeId,field:t.field,imageHash:t.imageHash,format:t.format,scale:i,status:"exported",byteLength:d.length,sha256:f,mimeType:null===s?null:"image/svg+xml",text:s,errorCode:null}}catch(e){return r("DEVUP_ASSET_EXPORT_FAILED")}},explore:async function(e){let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=e.explore,n=Math.max(20,Math.min(400,Number.isInteger(r.projectionLimit)?r.projectionLimit:200)),i=Math.max(0,Math.min(500,Number.isInteger(r.textPreviewLimit)?r.textPreviewLimit:160));function a(e){return"string"==typeof e?e.slice(0,240):""}let l=t,o="SECTION"===t.type?t:null;for(;l.parent&&"PAGE"!==l.parent.type;)l=l.parent,o||"SECTION"!==l.type||(o=l);let s="PAGE"===t.type?t:l.parent;if(!s||"PAGE"!==s.type)throw Error("DEVUP_PAGE_NOT_FOUND");function d(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function f(e){let t=d(e);return t&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&t.width/Math.max(1,t.height)>=.25&&t.width/Math.max(1,t.height)<=2.5}function u(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type&&t.length<12;){let e=a(r.name);e&&t.push(e),r=r.parent}return t.reverse()}await figma.setCurrentPageAsync(s);let c=!f(t)&&o?o:l,y=d(c)||d(t);if(!y)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let h="children"in s?s.children:[],p=h.map((e,t)=>({node:e,pageChildIndex:t,bounds:d(e)})).filter(e=>e.bounds).filter(e=>{var t;return e.node.id===c.id||e.bounds.y>=y.y-240&&e.bounds.y<=y.y+12e3&&(t=e.bounds,Math.min(t.x+t.width,y.x+y.width)-Math.max(t.x,y.x)>0)}).sort((e,t)=>e.bounds.y-t.bounds.y||e.bounds.x-t.bounds.x||e.node.id.localeCompare(t.node.id)),g="SECTION"===c.type?p.filter(e=>e.node.id===c.id):p.slice(0,n),m=new Map(g.map(e=>[e.node.id,e]));m.has(l.id)||m.set(l.id,{node:l,pageChildIndex:-1,bounds:d(l)}),m.has(c.id)||m.set(c.id,{node:c,pageChildIndex:-1,bounds:d(c)}),m.has(t.id)||m.set(t.id,{node:t,pageChildIndex:-1,bounds:d(t)});let b=t.parent;for(;b&&"PAGE"!==b.type&&(m.has(b.id)||m.set(b.id,{node:b,pageChildIndex:-1,bounds:d(b)}),b.id!==c.id);)b=b.parent;if("children"in c)for(let e of c.children.slice(0,n))m.has(e.id)||m.set(e.id,{node:e,pageChildIndex:-1,bounds:d(e)});let x="SECTION"===c.type&&"children"in c&&c.children.length>n;if("SECTION"===c.type&&"children"in c){let e=8*n,t=c.children.filter(e=>!f(e)).map(e=>({node:e,ancestors:[]})),r=0;for(;t.length&&r!m.has(e.id));if(m.size+t.length>n+2){x=!0;continue}for(let e of t)m.set(e.id,{node:e,pageChildIndex:-1,bounds:d(e)});continue}if("children"in e)for(let r of e.children)t.push({node:r,ancestors:[...i,e]})}x||=t.length>0}let E=[...m.values()].filter(e=>e.bounds).slice(0,n+2).map(({node:e,pageChildIndex:t,bounds:r})=>({id:e.id,type:e.type,fields:{name:a(e.name),parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:[],x:r.x,y:r.y,width:r.width,height:r.height,childCount:"children"in e?e.children.length:0,textPreview:"",pageChildIndex:t>=0?t:null,visible:!("visible"in e)||!1!==e.visible,breadcrumb:u(e)},extra:{},fieldErrors:{}})),S=E.reduce((e,t)=>{let r=t.fields;return e?{x:Math.min(e.x,r.x),y:Math.min(e.y,r.y),right:Math.max(e.right,r.x+r.width),bottom:Math.max(e.bottom,r.y+r.height)}:{x:r.x,y:r.y,right:r.x+r.width,bottom:r.y+r.height}},null)||{x:0,y:0,right:0,bottom:0},I="SECTION"!==c.type&&p.length>g.length||m.size>n+2||x,A={id:s.id,type:s.type,fields:{name:a(s.name),parentId:null,childrenIds:[],x:S.x,y:S.y,width:S.right-S.x,height:S.bottom-S.y,childCount:h.length,textPreview:"",projectionTruncated:I,visible:!0,breadcrumb:u(s),pageChildIndex:null},extra:{},fieldErrors:{}},N={fileKey:figma.fileKey||"",version:null,rootIds:[s.id],nodes:[A,...E.filter(e=>e.id!==s.id)],diagnostics:[]},w=new Set([s.id,l.id,c.id,t.id]);for(;JSON.stringify(N).length>14e3;){let e=N.nodes.length-1;for(;e>=0&&w.has(N.nodes[e].id);)e-=1;if(e<0)break;N.nodes.splice(e,1),A.fields.projectionTruncated=!0}if(JSON.stringify(N).length>14e3&&(N.nodes=N.nodes.filter(e=>w.has(e.id)).map(e=>({...e,fields:{...e.fields,name:a(e.fields.name).slice(0,80),textPreview:"",breadcrumb:e.fields.breadcrumb.slice(-4).map(e=>e.slice(0,80)),projectionTruncated:e.id===s.id||e.fields.projectionTruncated}}))),JSON.stringify(N).length>14e3)throw Error("DEVUP_EXPLORE_PROJECTION_TOO_LARGE");let O=14e3-JSON.stringify(N).length;if(i>0&&O>0)for(let e of N.nodes){let t=m.get(e.id);if(!t||e.id===s.id)continue;let r="";for(let e of function(e){if(0===i)return"";let t=[],r=[e],n=0;for(;r.length&&n<80&&t.join(" ").lengthO)break;r+=e,O-=t}if(e.fields.textPreview=r,0===O)break}return N},fastSnapshot:async function(e){let t=e.rootIds;if(!Array.isArray(t)||0===t.length)throw Error("DEVUP_ROOTS_INVALID");let r=await Promise.all(t.map(e=>figma.getNodeByIdAsync(e)));if(r.some(e=>!e))throw Error("DEVUP_NODE_NOT_FOUND");if(1===r.length&&"SECTION"===r[0].type)throw Error("DEVUP_TARGET_IS_SECTION");let n=["mobile","tablet","desktop"],i=e=>n.indexOf(String(e.name||"").trim().toLowerCase());if(1===r.length&&i(r[0])>=0){let e=r[0].parent;if(e&&"SECTION"===e.type&&"children"in e){let t=e.children.filter(e=>e.id===r[0].id||i(e)>=0).filter(e=>!1!==e.visible);t.length>1&&(r.length=0,r.push(...t))}}if(1===t.length){let e=[],t=new Set(r.map(e=>e.id)),n=[],i=e=>{if("reactions"in e&&Array.isArray(e.reactions)){for(let r of e.reactions)if(r&&r.trigger&&"AFTER_TIMEOUT"===r.trigger.type)for(let e of r.actions||[])e&&"NODE"===e.type&&e.transition&&"SMART_ANIMATE"===e.transition.type&&"string"==typeof e.destinationId&&!t.has(e.destinationId)&&(t.add(e.destinationId),n.push(e.destinationId));if("children"in e)for(let t of e.children)i(t)}};for(let e of r)i(e);for(;n.length>0;){let t=n.shift(),r=await figma.getNodeByIdAsync(t);r&&"DOCUMENT"!==r.type&&"PAGE"!==r.type&&(e.push(r),i(r))}r.push(...e)}let a=e.nodeId,l=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],o=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],s=e.snapshot,d=Math.max(0,Math.floor(Number(s.offset)||0)),f=Math.max(8192,Math.floor(Number(s.maxEnvelopeBytes)||19456)),u=Math.min(s.maxEnvelopeBytes?f-1024:18e3,Math.max(4096,Math.floor(Number(s.maxPayloadBytes)||15e3))),c=new Set(["backgroundStyleId","effectStyleId","fillStyleId","gridStyleId","strokeStyleId","textStyleId"]),y=new Set(["maxWidth","maxHeight","absoluteRenderBounds"]),h=new Map([["rotation",0],["cornerRadius",0],["isAsset",!1],["isMask",!1],["clipsContent",!1],["blendMode","PASS_THROUGH"],["strokeAlign","INSIDE"],["textCase","ORIGINAL"],["textDecoration","NONE"],["textAlignHorizontal","LEFT"],["textAlignVertical","TOP"],["counterAxisAlignItems","MIN"],["primaryAxisAlignItems","MIN"],["gridColumnCount",0],["gridRowCount",0],["gridColumnGap",0],["gridRowGap",0],["gridColumnAnchorIndex",-1],["gridRowAnchorIndex",-1],["gridColumnSpan",1],["gridRowSpan",1]]),p=new Set(["start","end","characters","fontWeight","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]),g=new Set(["parent","children","consumers"]);function m(e,t=!1,r=new WeakSet,n=0){let i;if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>m(e,t,r,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(r.has(e))return{$circular:!0};if(r.add(e),t){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}i=[...t].sort().filter(e=>!e.startsWith("_")&&!g.has(e))}else i=Object.keys(e).sort();let a={};for(let l of i)try{let i=m(e[l],t,r,n+1);i&&"function"===i.$unsupported||(a[l]=i)}catch(e){a[l]=t?{$error:"unavailable"}:{$error:String(e&&e.message?e.message:e)}}return r.delete(e),a}let b=[],x=[...r],E=new Set;for(let e=0;e=b.length&&b.length>0)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");function S(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1({id:e,styleType:t})).sort((e,t)=>e.id.localeCompare(t.id)),a=await Promise.all([...n.map(async e=>{try{let t=await figma.variables.getVariableByIdAsync(e);return t?{kind:"variable",value:m(t,!0),collectionId:t.variableCollectionId}:{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"variable",reason:"notFoundOrUnavailable"}}}}),...i.map(async({id:e,styleType:t})=>{try{let r=await figma.getStyleByIdAsync(e);if(!r)return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}};return{kind:"style",value:{...m(r,!0),styleType:t,value:m("PAINT"===t?r.paints:"EFFECT"===t?r.effects:"GRID"===t?r.layoutGrids:r,!0)}}}catch(t){return{kind:"unresolved",value:{id:e,kind:"style",reason:"notFoundOrUnavailable"}}}})]),l=[...new Set(a.filter(e=>"variable"===e.kind&&e.collectionId).map(e=>e.collectionId))].sort(),o=(await Promise.all(l.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?m(t,!0):null}catch(e){return null}}))).filter(e=>null!==e),s=a.filter(e=>"variable"===e.kind).map(e=>e.value),d=a.filter(e=>"style"===e.kind).map(e=>e.value),f=a.filter(e=>"unresolved"===e.kind).map(e=>e.value);return{collections:o,variables:s,styles:d,usedRemoteVariables:s.filter(e=>!0===e.remote),usedVariableIds:n,usedStyleIds:i.map(e=>e.id),localComplete:!1,usedRemoteComplete:0===f.length,unresolved:f,$variableRefCount:n.length,$styleRefCount:i.length}}let A=u-1024,N=null;for(let e=0;e<5;e+=1){let{pageNodes:e,packedBytes:t}=function(e){let t=[],r=2;for(let n=d;ne.id):[];for(let i of(n.length>0&&(t.childrenIds=n),l)){let n;try{if(!(i in e)){"overflowDirection"===i&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t[i]=null);continue}if(n=e[i],"function"==typeof n)continue;let r=m(n);if("overflowDirection"===i&&null==n){t[i]=null;continue}(null===r?!y.has(i):Array.isArray(r)?0===r.length:"object"==typeof r?0===Object.keys(r).length:!!(""===r&&c.has(i))||h.has(i)&&h.get(i)===r)||(t[i]=r)}catch(e){r[i]=String(e&&e.message?e.message:e)}}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{let r=m(e.getStyledTextSegments(o));if(1===r.length){let e=r[0];for(let t of Object.keys(e))p.has(t)||delete e[t]}r.length>0&&(t.styledTextSegments=r)}catch(e){r.styledTextSegments=String(e&&e.message?e.message:e)}let i={id:e.id,type:e.type,fields:t};return Object.keys(r).length>0&&(i.fieldErrors=r),i}(b[n]),a=S(JSON.stringify(i))+ +!!t.length;if(t.length&&r+a>e)break;t.push(i),r+=a}return{pageNodes:t,packedBytes:r}}(A);if(0===e.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let n=function(e,t){let n=Math.min(b.length,d+e.length),{$variableRefCount:i,$styleRefCount:l,...o}=t,s=[...e,{id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:d,nextOffset:n,complete:n>=b.length,totalNodes:b.length},extra:{},fieldErrors:{}}],f={kind:"devupFastSnapshotEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",rootId:a},snapshot:{fileKey:figma.fileKey||"",version:null,rootIds:r.map(e=>e.id),nodes:s,diagnostics:[]},resources:o,integrity:{nodeCount:s.length,variableRefCount:i,styleRefCount:l,utf8Bytes:0}},u=0;for(let e=0;e<8&&(u=S(JSON.stringify(f)),f.integrity.utf8Bytes!==u);e+=1)f.integrity.utf8Bytes=u;if(f.integrity.utf8Bytes!==S(JSON.stringify(f)))throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:f,bytes:u}}(e,await I(e));if(n.bytes<=f){N=n;break}if(1===e.length)throw Error("DEVUP_ENVELOPE_TOO_LARGE");A=Math.max(1,Math.min(A-1,t-(n.bytes-f)-256))}if(!N)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return N.envelope},fastTheme:async function(e){let t=Math.max(0,Math.floor(Number(e.theme.offset)||0));function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={};for(let a of function(e){let t=new Set(Object.keys(e)),r=e;for(;r&&r!==Object.prototype;){for(let e of Object.getOwnPropertyNames(r))t.add(e);r=Object.getPrototypeOf(r)}return[...t].sort()}(e))if(!(a.startsWith("_")||["parent","children","consumers"].includes(a)))try{let l=r(e[a],t,n+1);l&&"function"===l.$unsupported||(i[a]=l)}catch(e){i[a]={$error:"unavailable"}}return t.delete(e),i}let n=new Set,i=new Map;for(let e of[figma.root,...figma.root.findAll(()=>!0)])for(let t of["boundVariables","fills","strokes","effects","layoutGrids","textStyleId","fillStyleId","strokeStyleId","backgroundStyleId","effectStyleId","gridStyleId"])try{!function e(t,r="",a=new WeakSet,l=0){if(!(l>16)&&null!==t&&"object"==typeof t&&!a.has(t)){if(a.add(t),Array.isArray(t)){for(let n of t)e(n,r,a,l+1);return}for(let[o,s]of("VARIABLE_ALIAS"===t.type&&"string"==typeof t.id&&t.id&&"figma.mixed"!==t.id&&"MIXED"!==t.id&&n.add(t.id),Object.entries(t))){let t="textStyleId"===o?"TEXT":["fillStyleId","strokeStyleId","backgroundStyleId"].includes(o)?"PAINT":"effectStyleId"===o?"EFFECT":"gridStyleId"===o?"GRID":null;t&&"string"==typeof s&&s&&"figma.mixed"!==s&&"MIXED"!==s&&!i.has(s)&&i.set(s,t),e(s,o||r,a,l+1)}}}({[t]:e[t]})}catch(e){}let[a,l,o,s,d,f]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.variables.getLocalVariablesAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),u=new Set(l.map(e=>e.id)),c=new Set([...o,...s,...d,...f].map(e=>e.id)),y=[],h=[...n].filter(e=>!u.has(e)).sort().map(async e=>{try{return await figma.variables.getVariableByIdAsync(e)||null}catch(t){return y.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"}),null}}),p=[...i.entries()].filter(([e])=>!c.has(e)).sort(([e],[t])=>e.localeCompare(t)).map(async([e,t])=>{try{let r=await figma.getStyleByIdAsync(e);return r?{style:r,styleType:t}:null}catch(t){return y.push({id:e,kind:"style",reason:"notFoundOrUnavailable"}),null}}),g=(await Promise.all(h)).filter(Boolean),m=(await Promise.all(p)).filter(Boolean);for(let e of[...n].filter(e=>!u.has(e)))g.some(t=>t.id===e)||y.some(t=>t.id===e)||y.push({id:e,kind:"variable",reason:"notFoundOrUnavailable"});for(let[e]of[...i.entries()].filter(([e])=>!c.has(e)))m.some(t=>t.style.id===e)||y.some(t=>t.id===e)||y.push({id:e,kind:"style",reason:"notFoundOrUnavailable"});let b=[...new Set(g.map(e=>e.variableCollectionId))].filter(e=>!a.some(t=>t.id===e)).sort(),x=(await Promise.all(b.map(async e=>{try{return await figma.variables.getVariableCollectionByIdAsync(e)}catch(e){return null}}))).filter(Boolean);function E(e,t){return{...r(e),styleType:t,value:r("PAINT"===t?e.paints:"EFFECT"===t?e.effects:"GRID"===t?e.layoutGrids:e)}}let S=[...o.map(e=>E(e,"PAINT")),...s.map(e=>E(e,"TEXT")),...d.map(e=>E(e,"EFFECT")),...f.map(e=>E(e,"GRID")),...m.map(({style:e,styleType:t})=>E(e,t))].sort((e,t)=>e.id.localeCompare(t.id)),I=[...l,...g].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id)),A=[...a,...x].map(e=>r(e)).sort((e,t)=>e.id.localeCompare(t.id));function N(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}y.sort((e,t)=>e.kind.localeCompare(t.kind)||e.id.localeCompare(t.id));let w=[...A.map(e=>({kind:"collection",value:e})),...I.map(e=>({kind:"variable",value:e})),...S.map(e=>({kind:"style",value:e})),...[...n].sort().map(e=>({kind:"usedVariableId",value:e})),...[...i.keys()].sort().map(e=>({kind:"usedStyleId",value:e})),...y.map(e=>({kind:"unresolved",value:e}))];if(t>w.length)throw Error("DEVUP_SNAPSHOT_RANGE_INVALID");let O=w.map(e=>N(JSON.stringify(e.value)).length+1),T=18432,v=null;for(let e=0;e<6;e+=1){let{pageItems:e,packed:r}=function(e){let r=[],n=0;for(let i=t;i0)||!(n+O[i]>e));i+=1)r.push(w[i]),n+=O[i];return{pageItems:r,packed:n}}(T),n=function(e,r){let n=t=>e.filter(e=>e.kind===t).map(e=>e.value),i=n("collection"),a=n("variable"),l=n("style"),o=n("unresolved"),s={kind:"devupFastThemeEnvelope",schemaVersion:1,source:{fileKey:figma.fileKey||"",version:null},resources:{collections:i,variables:a,styles:l,usedRemoteVariables:[],usedVariableIds:n("usedVariableId"),usedStyleIds:n("usedStyleId"),localComplete:!0,usedRemoteComplete:0===y.length,unresolved:o},page:{offset:t,nextOffset:r,complete:r>=w.length,totalItems:w.length},integrity:{collectionCount:i.length,variableCount:a.length,styleCount:l.length,unresolvedCount:o.length,utf8Bytes:0}},d=new Uint8Array;for(let e=0;e<8&&(d=N(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length);e+=1)s.integrity.utf8Bytes=d.length;if(d=N(JSON.stringify(s)),s.integrity.utf8Bytes!==d.length)throw Error("DEVUP_ENVELOPE_LENGTH_UNSTABLE");return{envelope:s,bytes:d.length}}(e,t+e.length);if(n.bytes<=19456){v=n;break}if(e.length<=1)break;T=Math.max(1,Math.min(T-1,r-(n.bytes-19456)-256))}if(null===v||v.bytes>8388608)throw Error("DEVUP_ENVELOPE_TOO_LARGE");return v.envelope},largeValue:async function(e){let t,r=e.largeValue,n=await figma.getNodeByIdAsync(r.nodeId);if(!n)throw Error("DEVUP_NODE_NOT_FOUND");function i(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}let a=null;try{if("$export:svg"===r.field){if("function"!=typeof n.exportAsync)throw Error("unsupported");let e=await n.exportAsync({format:"SVG_STRING"});if("string"!=typeof e)throw Error("unsupported");a=i(e)}else if("string"==typeof r.field&&r.field.startsWith("$export:png")){if("function"!=typeof n.exportAsync)throw Error("unsupported");let e=Math.min(4,Math.max(1,Math.floor(Number(r.field.split("@")[1])||1))),t=await n.exportAsync({format:"PNG",constraint:{type:"SCALE",value:e}});a=t instanceof Uint8Array?t:new Uint8Array(t)}else if("styledTextSegments"===r.field&&"TEXT"===n.type&&"function"==typeof n.getStyledTextSegments)t=n.getStyledTextSegments(["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"]);else if(r.field in n)t=n[r.field];else throw Error("unsupported")}catch(e){return{kind:"devupLargeValueUnsupported",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,byteLength:r.byteLength,sha256:r.sha256,errorCode:"DEVUP_FIELD_UNSUPPORTED_BY_UPSTREAM"}}let l=null===a?i(JSON.stringify(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let l=e(t[a],r,n+1);l&&"function"===l.$unsupported||(i[a]=l)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(t))):a,o=function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=e.length,n=64*Math.ceil((r+9)/64),i=new Uint8Array(n);i.set(e),i[r]=128;let a=8*r;for(let e=0;e<8;e+=1)i[n-1-e]=255&Math.floor(a/2**(8*e));let l=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],o=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=o(r[e-2],17)^o(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[n,a,s,d,f,u,c,y]=l;for(let e=0;e<64;e+=1){let i=y+(o(f,6)^o(f,11)^o(f,25))+(f&u^~f&c)+t[e]+r[e]>>>0,l=(o(n,2)^o(n,13)^o(n,22))+(n&a^n&s^a&s)>>>0;y=c,c=u,u=f,f=d+i>>>0,d=s,s=a,a=n,n=i+l>>>0}for(let[e,t]of[n,a,s,d,f,u,c,y].entries())l[e]=l[e]+t>>>0}return l.map(e=>e.toString(16).padStart(8,"0")).join("")}(l);if(l.length!==r.byteLength||o!==r.sha256)throw Error("DEVUP_LARGE_VALUE_CHANGED");let s=Math.max(0,Math.floor(Number(r.offset)||0)),d=Math.min(65536,Math.max(1,Math.floor(Number(r.maxChunkBytes)||8192)));if(s>=l.length)throw Error("DEVUP_LARGE_VALUE_RANGE_INVALID");let f=Math.min(l.length,s+d);return{kind:"devupLargeValueFragment",fileKey:figma.fileKey||"",version:r.version,nodeId:r.nodeId,field:r.field,offset:s,nextOffset:f,byteLength:l.length,sha256:o,dataBase64:function(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="";for(let n=0;n>2],r+=t[(3&i)<<4|a>>4],r+=n+1>6]:"=",r+=n+2e.id),nodes:t.map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:null,childrenIds:[]},extra:{},fieldErrors:{}})),diagnostics:[]}},search:async function(e){let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=t;for(;r&&"PAGE"!==r.type;)r=r.parent;if(!r||"PAGE"!==r.type)throw Error("DEVUP_PAGE_NOT_FOUND");await figma.setCurrentPageAsync(r);let n=e.search,i=new Set((n.nodeTypes.length?n.nodeTypes:["PAGE","SECTION","FRAME","COMPONENT_SET","COMPONENT"]).map(e=>e.toUpperCase()));function a(e){return e.normalize("NFC").toLocaleLowerCase().replace(/\s/gu,"")}let l=t===r?r.findAll(()=>!0):"findAll"in t?t.findAll(()=>!0):[],o=[t,...l].filter(e=>i.has(e.type)&&"string"==typeof e.name).map(e=>({node:e,score:function(e){if(e===n.query)return 400;if("exact"===n.matchKind)return null;let t=a(e),r=a(n.query);if(t===r)return 300;if(t.startsWith(r))return 200;if(t.includes(r))return 100;if("fuzzy"!==n.matchKind)return null;let i=function(e,t){let r=Array.from({length:t.length+1},(e,t)=>t);for(let n=0;nnull!==e.score).sort((e,t)=>t.score-e.score||e.node.name.localeCompare(t.node.name)||e.node.id.localeCompare(t.node.id)).slice(0,n.limit),s=new Map([[r.id,r]]);for(let e of[t,...o.map(e=>e.node)]){let t=e;for(;t&&"DOCUMENT"!==t.type;)s.set(t.id,t),t=t.parent}return{fileKey:figma.fileKey||"",version:null,rootIds:[r.id],nodes:[...s.values()].map(e=>({id:e.id,type:e.type,fields:{name:e.name,parentId:e.parent&&"DOCUMENT"!==e.parent.type?e.parent.id:null,childrenIds:"children"in e?e.children.map(e=>e.id):[]},extra:{},fieldErrors:{}})),diagnostics:[]}},sectionIndex:async function(e){let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");if("SECTION"!==t.type){let e=t.parent,r=new Set;for(;e&&"SECTION"!==e.type&&!r.has(e.id);)r.add(e.id),e=e.parent;let n=e&&"SECTION"===e.type?e.id:null,i=n&&figma.fileKey?`https://www.figma.com/design/${figma.fileKey}?node-id=${n.replace(/:/g,"-")}`:null;throw Error("DEVUP_SECTION_REQUIRED "+JSON.stringify({pluginCode:"DEVUP_SECTION_REQUIRED",stage:"section-index",nodeId:t.id,nodeType:t.type,sectionId:n,nextAction:{tool:"devup_figma_export",how:n?`The url must point to a SECTION. This capture's SECTION is ${n}; select frames with frameIds.`:"The url must point to a SECTION. This node has no ancestor SECTION; choose the intended SECTION in Figma and copy its link.",arguments:i?"FRAME"===t.type&&t.parent===e?{url:i,frameIds:[t.id]}:{url:i}:null,requiredArguments:i?[]:["url"]}}))}let r=2048;function n(e){let t=e.absoluteBoundingBox||{x:"number"==typeof e.x?e.x:0,y:"number"==typeof e.y?e.y:0,width:"number"==typeof e.width?e.width:0,height:"number"==typeof e.height?e.height:0};return[t.x,t.y,t.width,t.height].every(Number.isFinite)?{x:t.x,y:t.y,width:t.width,height:t.height}:null}function i(e,t){if("FRAME"!==e.type||!1===e.visible||!t)return!1;let r=t.width/Math.max(1,t.height);return t.width>=240&&t.width<=1800&&t.height>=300&&t.height<=2e3&&r>=.25&&r<=2.5}function a(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+1[e.id,t.id])),s=[],d=0;for(let e=0;ee.id));for(let r of t.children){if(e.has(r.id))continue;let t=n(r);t&&!1!==r.visible&&s.push({node:r,box:t})}}s.sort((e,t)=>e.box.y-t.box.y||e.box.x-t.box.x||e.node.id.localeCompare(t.node.id));let f=l.length>d||s.length>100,u=s.slice(0,100),c=new Set(u.map(({node:e})=>e.id)),y=new Set(e.rootIds),h={},p=new Map([[t.id,null]]);for(let e=0;e{let r=e.parent;for(;r&&r.id!==t.id&&!c.has(r.id);)r=r.parent;return[e.id,r&&c.has(r.id)?r.id:t.id]})),m=e=>u.filter(({node:t})=>g.get(t.id)===e).map(({node:e})=>e.id),b=n(t);if(!b)throw Error("DEVUP_NODE_BOUNDS_UNAVAILABLE");let x={id:t.id,type:t.type,fields:{name:t.name,parentId:t.parent&&"DOCUMENT"!==t.parent.type?t.parent.id:null,childrenIds:m(t.id),absoluteBoundingBox:b,visible:!1!==t.visible,projectionTruncated:f,nodeScreenIds:h},extra:{},fieldErrors:{}},E=u.map(({node:e,box:t})=>{let n=function(e){let t=[e],r=0,n=0;for(let e=0;er}}(e),l=function(e){let t=[e],n="",i=0,l=0,o=!1;if(0===r)return{text:"",state:"budget-exhausted"};for(let e=0;e=120||l+e>r)return r-=l,{text:n.trimEnd(),state:e>r?"budget-exhausted":"truncated"};n+=t,i+=1,l+=e}}if("children"in s){let e=Math.max(0,64-t.length);s.children.length>e&&(o=!0),t.push(...s.children.slice(0,e))}}}return r-=l,{text:n,state:o?"truncated":n?"available":"no-text"}}(e);return{id:e.id,type:e.type,fields:{name:"string"==typeof e.name?e.name:"",parentId:g.get(e.id),childrenIds:m(e.id),absoluteBoundingBox:t,visible:!1!==e.visible,breadcrumb:function(e){let t=[],r=e;for(;r&&"DOCUMENT"!==r.type;)"string"==typeof r.name&&r.name&&t.push(r.name),r=r.parent;return t.reverse()}(e),directChildCount:"children"in e?e.children.length:0,textPreview:l.text,textPreviewState:l.state,subtreeNodeCount:n.subtreeNodeCount,estimatedSerializedBytes:n.estimatedSerializedBytes,selectionReasons:[i(e,t)?"screen-like":"explicit-selection-only","inside-section"],estimateTruncated:n.truncated},extra:{},fieldErrors:{}}}),S={fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:[x,...E],diagnostics:[]},I=a(JSON.stringify(S));for(let e=E.length-1;e>=0&&I>19456;e-=1)E[e].fields.textPreview="",E[e].fields.textPreviewState="budget-exhausted",I=a(JSON.stringify(S));if(I>19456)throw Error("DEVUP_SECTION_INDEX_TOO_LARGE "+JSON.stringify({pluginCode:"DEVUP_SECTION_INDEX_TOO_LARGE",stage:"section-index",responseBytes:I,maxResponseBytes:19456}));return S},snapshot:async function(e){let t=await figma.getNodeByIdAsync(e.nodeId);if(!t)throw Error("DEVUP_NODE_NOT_FOUND");let r=["absoluteBoundingBox","absoluteRenderBounds","arcData","backgroundStyleId","blendMode","bottomLeftRadius","bottomRightRadius","boundVariables","characters","clipsContent","componentProperties","componentPropertyDefinitions","componentPropertyReferences","constraints","cornerRadius","counterAxisAlignItems","dashPattern","defaultVariant","effectStyleId","effects","fillStyleId","fills","fontName","fontSize","gridColumnAnchorIndex","gridColumnCount","gridColumnGap","gridColumnSizes","gridColumnSpan","gridRowAnchorIndex","gridRowCount","gridRowGap","gridRowSizes","gridRowSpan","gridStyleId","height","inferredAutoLayout","isAsset","isMask","itemSpacing","layoutGrow","layoutMode","layoutWrap","layoutPositioning","layoutSizingHorizontal","layoutSizingVertical","letterSpacing","lineHeight","maxHeight","maxLines","maxWidth","minHeight","minWidth","name","opacity","overflowDirection","paddingBottom","paddingLeft","paddingRight","paddingTop","primaryAxisAlignItems","reactions","rotation","strokeAlign","strokeBottomWeight","strokeLeftWeight","strokeRightWeight","strokeStyleId","strokeTopWeight","strokeWeight","strokes","targetAspectRatio","textAlignHorizontal","textAlignVertical","textAutoResize","textCase","textDecoration","textStyleId","textTruncation","topLeftRadius","topRightRadius","variantProperties","visible","width","x","y"],n=new Set(r),i=["fontName","fontWeight","fontSize","textDecoration","textCase","lineHeight","letterSpacing","fills","textStyleId","fillStyleId","listOptions","indentation","hyperlink"],a=e.snapshot,l=Math.max(0,Math.floor(Number(a.offset)||0)),o=Math.min(a.maxEnvelopeBytes?Math.max(8192,Math.floor(Number(a.maxEnvelopeBytes))):16e3,Math.max(4096,Math.floor(Number(a.maxPayloadBytes)||15e3))),s=Math.min(o-1024,Math.max(512,Math.floor(Number(a.maxFieldBytes)||4096))),d=new Set(["id","type","parent","children"]),f=new Set(["parentId","childrenIds","name","characters","styledTextSegments","boundVariables"]);function u(e,t,r){let n=function(e){let t=[];for(let r=0;r=55296&&n<=56319){let t=r+1=56320&&t<=57343?(n=65536+(n-55296<<10)+(t-56320),r+=1):n=65533}else n>=56320&&n<=57343&&(n=65533);n<128?t.push(n):n<2048?t.push(192|n>>6,128|63&n):n<65536?t.push(224|n>>12,128|n>>6&63,128|63&n):t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n)}return new Uint8Array(t)}(JSON.stringify(r));return n.length>0x1000000?{$truncated:"max-large-value-bytes",byteLength:n.length}:{$largeValue:{nodeId:e,field:t,byteLength:n.length,sha256:function(e){let t=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0xfc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x6ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2],r=64*Math.ceil((e.length+9)/64),n=new Uint8Array(r);n.set(e),n[e.length]=128;let i=8*e.length;for(let e=0;e<8;e+=1)n[r-1-e]=255&Math.floor(i/2**(8*e));let a=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],l=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=l(r[e-2],17)^l(r[e-2],19)^r[e-2]>>>10;r[e]=r[e-16]+t+r[e-7]+n>>>0}let[i,o,s,d,f,u,c,y]=a;for(let e=0;e<64;e+=1){let n=y+(l(f,6)^l(f,11)^l(f,25))+(f&u^~f&c)+t[e]+r[e]>>>0,a=(l(i,2)^l(i,13)^l(i,22))+(i&o^i&s^o&s)>>>0;y=c,c=u,u=f,f=d+n>>>0,d=s,s=o,o=i,i=n+a>>>0}for(let[e,t]of[i,o,s,d,f,u,c,y].entries())a[e]=a[e]+t>>>0}return a.map(e=>e.toString(16).padStart(8,"0")).join("")}(n),cursor:{nextOffset:0,maxChunkBytes:12288}}}}function c(e){return function(e){let t=0;for(let r=0;r=55296&&n<=56319&&r+112)return{$truncated:"max-depth"};if("object"==typeof t&&"parent"in t&&"string"==typeof t.id&&"string"==typeof t.type)return{$nodeId:t.id,$nodeType:t.type};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(ArrayBuffer.isView(t))return{$binary:t.constructor.name,byteLength:t.byteLength};if(t instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:t.byteLength};if(r.has(t))return{$circular:!0};r.add(t);let i={};for(let a of Object.keys(t).sort())try{let l=e(t[a],r,n+1);l&&"function"===l.$unsupported||(i[a]=l)}catch(e){i[a]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i}(r),a=c(i);if(a<=s)return i;let l=u(e,t,i);return l.$truncated&&(n[t]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${a}>16777216`),l}let h=[],p=[t];for(;p.length;){let e=p.shift();h.push(e),"children"in e&&p.push(...e.children)}let g=[],m=o-1024,b=2;for(let e=l;eNumber(e.protected)-Number(t.protected)||t.byteLength-e.byteLength),n)){if(r<=t)break;let n=u(e.id,i.name,e[i.sectionName][i.name]);e[i.sectionName][i.name]=n,n.$truncated&&(e.fieldErrors[i.name]=`DEVUP_FIELD_VALUE_UNSUPPORTED:${i.byteLength}>16777216`),r=c(e)}return e}(function(e){let t={},a={},l={};for(let i of(t.parentId=e.parent?e.parent.id:null,e.parent&&("PAGE"===e.parent.type||"SECTION"===e.parent.type||"COMPONENT_SET"===e.parent.type)&&(t.parentType=e.parent.type,"SECTION"===e.parent.type&&(t.parentName=e.parent.name)),t.childrenIds="children"in e?e.children.map(e=>e.id):[],!("overflowDirection"in e)&&["FRAME","COMPONENT","INSTANCE","COMPONENT_SET"].includes(e.type)&&(t.overflowDirection=null),function(e){let t=new Set,n=e;for(;n&&n!==Object.prototype;){for(let e of Object.getOwnPropertyNames(n))t.add(e);n=Object.getPrototypeOf(n)}for(let n of r)try{n in e&&t.add(n)}catch(e){}return[...t].sort()}(e)))if(!(d.has(i)||i.startsWith("_")))try{let r=e[i];if("function"==typeof r)continue;let o=y(e.id,i,r,l);(n.has(i)?t:a)[i]=o}catch(e){l[i]=String(e&&e.message?e.message:e)}if("TEXT"===e.type&&"function"==typeof e.getStyledTextSegments)try{t.styledTextSegments=y(e.id,"styledTextSegments",e.getStyledTextSegments(i),l)}catch(e){l.styledTextSegments=String(e&&e.message?e.message:e)}return{id:e.id,type:e.type,fields:t,extra:a,fieldErrors:l}}(h[e]),m),a=c(t)+ +!!g.length;if(g.length&&b+a>m)break;g.push(t),b+=a}let x=Math.min(h.length,l+g.length);return g.push({id:"__DEVUP_SNAPSHOT_CURSOR__",type:"DEVUP_INTERNAL",fields:{offset:l,nextOffset:x,complete:x>=h.length,totalNodes:h.length},extra:{},fieldErrors:{}}),{fileKey:figma.fileKey||"",version:null,rootIds:[t.id],nodes:g,diagnostics:[]}},usedResources:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),l=e;for(;l&&l!==Object.prototype;){for(let e of Object.getOwnPropertyNames(l))a.add(e);l=Object.getPrototypeOf(l)}for(let l of[...a].sort())if(!(l.startsWith("_")||["parent","children","consumers"].includes(l)))try{let a=r(e[l],t,n+1);a&&"function"===a.$unsupported||(i[l]=a)}catch(e){i[l]={$error:"unavailable"}}return t.delete(e),i}let n=new Map,i="ok";try{for(let e of(await figma.variables.getLocalVariablesAsync()))n.set(e.id,e)}catch(e){i="unavailable"}let a=await Promise.all(t.variableIds.map(async e=>{let t=n.get(e);if(t)return{value:r(t),collectionId:t.variableCollectionId};try{let t=await figma.variables.getVariableByIdAsync(e);return t?{value:r(t),collectionId:t.variableCollectionId}:{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupEmpty"}}}catch(t){return{unresolved:{id:e,kind:"variable",reason:"notInFileAndLookupThrew"}}}})),l=[...new Set(a.flatMap(e=>e.collectionId?[e.collectionId]:[]))].sort(),o=await Promise.all(l.map(async e=>{try{let t=await figma.variables.getVariableCollectionByIdAsync(e);return t?[r(t)]:[]}catch(e){return[]}})),s=await Promise.all(t.styles.map(async e=>{try{let t=await figma.getStyleByIdAsync(e.id);if(!t)return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}};return{value:{...r(t),styleType:e.styleType,value:r("PAINT"===e.styleType?t.paints:"EFFECT"===e.styleType?t.effects:"GRID"===e.styleType?t.layoutGrids:t)}}}catch(t){return{unresolved:{id:e.id,kind:"style",reason:"notFoundOrUnavailable"}}}}));return{collections:o.flat(),variables:a.flatMap(e=>e.value?[e.value]:[]),styles:s.flatMap(e=>e.value?[e.value]:[]),usedVariableIds:t.variableIds,usedStyleIds:t.styles.map(e=>e.id),localVariableListing:i,localVariableCount:n.size,unresolved:[...a,...s].flatMap(e=>e.unresolved?[e.unresolved]:[])}},variableCatalog:async function(e){let[t,r,n,i,a]=await Promise.all([figma.variables.getLocalVariableCollectionsAsync(),figma.getLocalPaintStylesAsync(),figma.getLocalTextStylesAsync(),figma.getLocalEffectStylesAsync(),figma.getLocalGridStylesAsync()]),l=["PAINT","TEXT","EFFECT","GRID"];return{collections:t.map(e=>(function e(t,r=new WeakSet,n=0){if(null===t||["string","number","boolean"].includes(typeof t))return t;if(void 0===t)return{$undefined:!0};if("bigint"==typeof t)return{$bigint:t.toString()};if(["function","symbol"].includes(typeof t))return{$unsupported:typeof t};if(n>12)return{$truncated:"max-depth"};if(Array.isArray(t))return t.map(t=>e(t,r,n+1));if(r.has(t))return{$circular:!0};r.add(t);let i={},a=new Set(Object.keys(t)),l=t;for(;l&&l!==Object.prototype;){for(let e of Object.getOwnPropertyNames(l))a.add(e);l=Object.getPrototypeOf(l)}for(let l of[...a].sort())if(!l.startsWith("_"))try{let a=e(t[l],r,n+1);a&&"function"===a.$unsupported||(i[l]=a)}catch(e){i[l]={$error:String(e&&e.message?e.message:e)}}return r.delete(t),i})(e)),variableIds:[...new Set(t.flatMap(e=>e.variableIds))].sort(),styles:[r,n,i,a].flatMap((e,t)=>e.map(e=>({id:e.id,styleType:l[t]}))).sort((e,t)=>e.id.localeCompare(t.id)),localComplete:!0,usedRemoteComplete:!1}},variables:async function(e){let t=e.resources;function r(e,t=new WeakSet,n=0){if(null===e||["string","number","boolean"].includes(typeof e))return e;if(void 0===e)return{$undefined:!0};if("bigint"==typeof e)return{$bigint:e.toString()};if(["function","symbol"].includes(typeof e))return{$unsupported:typeof e};if(n>12)return{$truncated:"max-depth"};if("object"==typeof e&&"parent"in e&&"string"==typeof e.id&&"string"==typeof e.type)return{$nodeId:e.id,$nodeType:e.type};if(Array.isArray(e))return e.map(e=>r(e,t,n+1));if(ArrayBuffer.isView(e))return{$binary:e.constructor.name,byteLength:e.byteLength};if(e instanceof ArrayBuffer)return{$binary:"ArrayBuffer",byteLength:e.byteLength};if(t.has(e))return{$circular:!0};t.add(e);let i={},a=new Set(Object.keys(e)),l=e;for(;l&&l!==Object.prototype;){for(let e of Object.getOwnPropertyNames(l))a.add(e);l=Object.getPrototypeOf(l)}for(let l of[...a].sort())if(!(l.startsWith("_")||["parent","children","consumers"].includes(l)))try{let a=r(e[l],t,n+1);a&&"function"===a.$unsupported||(i[l]=a)}catch(e){i[l]={$error:String(e&&e.message?e.message:e)}}return t.delete(e),i}let[n,i]=await Promise.all([Promise.all(t.variableIds.map(e=>figma.variables.getVariableByIdAsync(e))),Promise.all(t.styles.map(e=>figma.getStyleByIdAsync(e.id)))]),a=new Map(t.styles.map(e=>[e.id,e])),l=await Promise.all(i.filter(Boolean).map(async e=>{let t=a.get(e.id),n=t.styleType;if(Number.isInteger(t.consumerStart)&&Number.isInteger(t.consumerEnd)){let i=await e.getStyleConsumersAsync();return{id:e.id,styleType:n,$consumerStart:t.consumerStart,$consumerEntries:i.slice(t.consumerStart,t.consumerEnd).map(e=>[e.node.id,e.node.type,r(e.fields)])}}let i=await e.getStyleConsumersAsync();return{...r(e),styleType:n,$consumerCount:i.length,value:r("PAINT"===n?e.paints:"EFFECT"===n?e.effects:"GRID"===n?e.layoutGrids:e)}}));return{variables:n.filter(Boolean).map(e=>r(e)),styles:l}}};async function i(e){let t=n[e.script];if(!t)return{kind:"devup-result",requestId:e.requestId,error:`DEVUP_BRIDGE_UNKNOWN_SCRIPT: ${e.script} (아는 스크립트: ${Object.keys(n).join(", ")})`};try{var r;let n=await t({nodeId:(r=e.params).nodeId??"",rootIds:r.rootIds??[],snapshot:r.snapshot??{},search:r.search??{},explore:r.explore??{},resources:r.resources??{variableIds:[],styles:[]},largeValue:r.largeValue??{},asset:r.asset??{},theme:r.theme??{offset:0}});return{kind:"devup-result",requestId:e.requestId,data:n}}catch(t){return{kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}}}let a=[],l=!1;async function o(){if(!l){l=!0;try{for(;a.length>0;){let e=a.shift(),t=await i(e).catch(t=>({kind:"devup-result",requestId:e.requestId,error:t instanceof Error?t.message:String(t)}));figma.ui.postMessage(t)}}finally{l=!1}}}figma.showUI(__html__,{width:320,height:220}),figma.ui.onmessage=e=>{if("object"==typeof e&&null!==e){if("devup-ready"===e.kind){let e={kind:"devup-status",fileKey:figma.fileKey??null,fileName:figma.root.name,port:1993};figma.ui.postMessage(e);return}"devup-job"===e.kind&&(a.push(e),o())}}})(); \ No newline at end of file diff --git a/plugin/dist/ui.html b/plugin/dist/ui.html new file mode 100644 index 0000000..21578a0 --- /dev/null +++ b/plugin/dist/ui.html @@ -0,0 +1,48 @@ +

Devup Bridge

이 창을 열어 두면 devup-mcp 가 이 파일을 직접 읽습니다.

시작하는 중…

읽기 전용입니다. 문서를 바꾸지 않고, 데이터는 같은 기기의 devup-mcp 로만 나갑니다.

\ No newline at end of file diff --git a/plugin/rspack.config.js b/plugin/rspack.config.js index a5132bb..1c79c88 100644 --- a/plugin/rspack.config.js +++ b/plugin/rspack.config.js @@ -14,6 +14,9 @@ export default [ name: 'code', target: 'web', mode: 'production', + // `dist/` 는 커밋된다. 소스맵은 Figma 가 읽지 않으면서 번들의 네 배가 넘고, + // 빌드마다 저장소에 들어갈 이유가 없다. + devtool: false, entry: { code: resolve(here, 'src/code.ts') }, output: { path: resolve(here, 'dist'), filename: '[name].js' }, resolve: { extensions: ['.ts', '.js'] }, @@ -37,6 +40,7 @@ export default [ name: 'ui', target: 'web', mode: 'production', + devtool: false, entry: { ui: resolve(here, 'src/ui.ts') }, output: { path: resolve(here, 'dist'), filename: '[name].js' }, resolve: { extensions: ['.ts', '.js'] },