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/.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/Cargo.lock b/Cargo.lock index 2fb1d9c..23af185 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 0.29.0", "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" @@ -736,6 +745,7 @@ dependencies = [ "async-trait", "axum", "base64 0.23.1", + "futures-util", "image", "keyring", "quick-xml", @@ -748,6 +758,7 @@ dependencies = [ "subtle", "tempfile", "tokio", + "tokio-tungstenite 0.28.0", "unicode-normalization", "url", "webbrowser", @@ -1081,6 +1092,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 +1113,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", "wasm-bindgen", ] @@ -2346,6 +2369,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 +2388,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 +2423,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 +2442,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 +2941,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 +3290,30 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.29.0", +] + [[package]] name = "tokio-util" version = "0.7.19" @@ -3379,6 +3472,39 @@ 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" +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" @@ -3454,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" @@ -3509,6 +3641,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 +4028,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/README.md b/README.md index 4a189a4..c5418b4 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,57 @@ 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 플러그인**을 통해 같은 읽기를 할 수 있습니다. 이 경로는 한도를 쓰지 않습니다. 플러그인이 붙어 있으면 스크립트 읽기가 그쪽으로 가고, **안 붙어 있으면 아무 일도 일어나지 않고 그대로 원격 경로로** 갑니다. 설치하지 않은 사람의 동작은 바뀌지 않습니다. + +#### 설치 — 빌드할 필요 없습니다 + +빌드된 플러그인이 저장소에 들어 있습니다(`plugin/dist/`). `npm`도 Node도 필요 없고, 저장소를 받아 두었다면 바로 다음 단계로 갑니다. + +**1. Figma 데스크톱 앱에서 불러옵니다** + +Figma의 플러그인 등록은 데스크톱 앱의 메뉴 동작이라 스크립트로 대신할 수 없습니다. 이 세 단계만 직접 하시면 됩니다. + +1. **Figma 데스크톱 앱**을 엽니다 (브라우저판은 로컬 플러그인을 못 불러옵니다) +2. 메뉴에서 **Plugins → Development → Import plugin from manifest...** +3. 이 저장소의 **`plugin/manifest.json`** 을 선택합니다 + +한 번만 하면 됩니다. 이후에는 플러그인 목록에 `Devup Bridge`로 남습니다. + +> 커밋된 번들이 소스와 어긋나지 않도록 CI가 매번 다시 빌드해 byte 단위로 대조합니다. 플러그인 소스를 고쳤다면 `cd plugin && npm install && npm run build` 후 `dist/`를 함께 커밋해야 합니다. + +**2. 읽으려는 파일에서 플러그인을 실행합니다** + +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 등록 제약 (실측) diff --git a/crates/devup-mcp-figma/Cargo.toml b/crates/devup-mcp-figma/Cargo.toml index 84b95f9..55c2025 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 @@ -29,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/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"]); + } +} diff --git a/crates/devup-mcp-figma/src/bridge.rs b/crates/devup-mcp-figma/src/bridge.rs new file mode 100644 index 0000000..e264cf5 --- /dev/null +++ b/crates/devup-mcp-figma/src/bridge.rs @@ -0,0 +1,552 @@ +//! 열려 있는 Figma 파일을 우리 플러그인으로 직접 읽는 전송 계층. +//! +//! 공식 MCP 경로(`RemoteFigmaClient`)는 읽기 스크립트를 `use_figma` 로 보내는데, +//! 그 도구가 요금 한도를 쓴다. 스크립트가 건드리는 것은 문서화된 Plugin API 뿐이라 +//! 우리가 돌리는 플러그인이 같은 일을 할 수 있고, 그 경로에는 한도가 없다. +//! +//! 방향이 거꾸로인 점이 설계를 결정한다. Figma 플러그인은 들어오는 연결을 받지 +//! 못하고 나가는 WebSocket 만 열 수 있어서, **서버는 이쪽**이고 플러그인이 붙는다. +//! +//! 응답은 원격이 주는 모양 그대로 감싼다. 디코더들은 `content[].text` 안의 JSON +//! 을 찾도록 쓰여 있으므로, 여기서 모양을 바꾸면 두 경로가 조용히 갈라진다. + +use std::{ + collections::HashMap, + sync::{ + Arc, + atomic::{AtomicU64, AtomicUsize, 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::{BatchBudget, 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, + /// 붙어 있는 플러그인 수. 배치 크기를 정할 때는 잠금을 기다릴 수 없어 + /// (그 자리가 async 가 아니다) 따로 센다. + connected: Arc, +} + +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)) + } + + /// 이 파일을 열어 둔 플러그인이 있는지. + pub async fn has_plugin(&self, file_key: &str) -> bool { + resolve_key(&self.inner.lock().await.plugins, file_key).is_some() + } + + /// 붙어 있는 파일 키 목록. 진단용. + 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(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}" + ))); + }; + 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", + )), + } + } +} + +/// 한 번에 담을 수 있는 양. 로컬 소켓이라 공식 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` 를 그대로 싣는데, 그 값이 늘 오지는 않는다 — +/// 비어 오는 것을 실기기에서 확인했다. 디코더는 비어 있지 않은 `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 문자열을 찾도록 쓰여 있다. 값을 그대로 +/// 올리면 일부 디코더는 통과하고 일부는 실패해, 두 경로가 화면 단위로 갈라진다. +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") => { + // 키가 없어도 등록한다. 건너뛰면 창은 "연결됨"이라고 하는데 + // 어떤 읽기도 오지 않아 원인을 찾을 수 없다. 키 없는 연결을 + // 어디까지 믿을지는 resolve_key 가 정한다. + let key = serde_json::from_value::(value) + .ok() + .and_then(|hello| hello.file_key) + .unwrap_or_default(); + state.inner.lock().await.plugins.insert( + key.clone(), + Connected { outbox: outbox.clone() }, + ); + state.connected.store( + state.inner.lock().await.plugins.len(), + Ordering::Relaxed, + ); + 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); + } + state + .connected + .store(state.inner.lock().await.plugins.len(), Ordering::Relaxed); +} + +/// 브리지 서버. 포트를 잡지 못하면 열지 않으며, 그 경우 호출자는 원격 경로만 쓴다. +pub struct BridgeServer { + state: BridgeState, + port: u16, +} + +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()?; + // 0 을 주면 커널이 빈 포트를 고른다. 실제로 잡힌 번호를 알아야 붙을 수 있다. + let bound = listener.local_addr().ok()?.port(); + // 런타임 밖에서 만들어졌다면 붙일 곳이 없다. 그때도 원격 경로는 멀쩡하다. + 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, port: bound }) + } + + /// 환경 설정을 읽어 띄운다. + /// + /// `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() + } + + /// 실제로 잡은 포트. `start(0)` 으로 띄웠을 때 어디에 붙어야 하는지 알려 준다. + pub fn port(&self) -> u16 { + self.port + } +} + +/// 이 요청을 맡을 수 있는지 **부르기 전에** 답하는 상류. +/// +/// 실패한 뒤에 폴백하면 안 되기 때문에 따로 둔다. `CapabilityUnavailable` 과 +/// `Transport` 는 같은 `ErrorCode` 로 접히므로, 에러만 보고는 "브리지가 못 하는 +/// 일"과 "브리지가 하다가 실패한 일"을 구분할 수 없다. 뒤엣것까지 원격으로 넘기면 +/// 아끼려던 요금을 그대로 쓰게 된다. +#[async_trait] +pub trait PreferredUpstream: FigmaUpstream { + async fn can_serve(&self, call: &ReadToolCall) -> bool; + + /// 지금 이 상류가 쓸 수 있는 상태인지. 배치 크기를 정하는 자리는 async 가 + /// 아니어서 `can_serve` 를 부를 수 없다. + fn is_live(&self) -> 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 mut params = job.params; + widen_budgets(&mut params); + let mut data = self + .state + .dispatch(call.file_key(), job.script, params) + .await?; + stamp_file_key(&mut data, call.file_key()); + Ok(UpstreamResult { + raw: wrap_as_tool_result(&data)?, + }) + } + + fn batch_budget(&self) -> BatchBudget { + bridge_batch_budget() + } +} + +#[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 + } + + 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, +}; + +/// 한 번에 물을 리소스 수를 밖에서 낮춰 보기 위한 손잡이. +/// +/// 리소스가 하나도 해석되지 않는 것을 실기기에서 봤고, 원인이 이 크기인지 +/// (플러그인이 그만한 동시 조회를 감당하지 못하는지) 아니면 애초에 그 변수를 +/// 읽을 권한이 없는지 가려야 했다. 둘은 고치는 곳이 다르다. +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 +} + +/// 브리지가 맡을 수 있으면 브리지로, 아니면 원격으로 보낸다. +/// +/// 판정은 부르기 전에 끝난다. 브리지가 맡은 요청이 실패하면 그 실패를 그대로 +/// 올린다 — 노드가 없거나 봉투가 너무 큰 것은 원격에서도 같은 결과이고, 다시 +/// 물으면 아낀 요금을 도로 쓰게 된다. +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 + } + + /// 붙어 있는 플러그인이 있으면 그쪽 크기로 묶는다. + /// + /// 배치는 부르기 한참 전에 나뉘므로 어느 전송이 받을지 그때는 알 수 없다. + /// 다만 크게 묶어도 되는 읽기는 전부 스크립트 경로이고, 플러그인이 붙어 있는 + /// 한 그것은 브리지가 받는다. 수집 도중 플러그인이 닫히면 남은 배치가 원격에 + /// 너무 커서 거절되는데, 그때는 수집 자체가 이어질 수 없으므로 실패가 드러나는 + /// 편이 맞다. + 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 e48a3bd..f18a845 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, @@ -67,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/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/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/src/upstream.rs b/crates/devup-mcp-figma/src/upstream.rs index 62a5a47..1e35c75 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, } } } @@ -787,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)] @@ -886,6 +924,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-figma/tests/bridge_transport.rs b/crates/devup-mcp-figma/tests/bridge_transport.rs new file mode 100644 index 0000000..88a3588 --- /dev/null +++ b/crates/devup-mcp-figma/tests/bridge_transport.rs @@ -0,0 +1,278 @@ +//! 브리지 전송 계층을 진짜 소켓으로 통과시킨다. +//! +//! 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>; + +/// 플러그인 흉내: 붙어서 자기 파일을 알리고, 등록될 때까지 기다린다. +/// +/// `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 }) + .to_string() + .into(), + )) + .await + .expect("hello is sent"); + + // 등록은 서버 쪽 태스크에서 일어난다. 보낸 직후에는 아직일 수 있다. + let expected = file_key.unwrap_or_default(); + let state = server.state(); + for _ in 0..200 { + if state.connected_files().await.iter().any(|k| k == expected) { + return socket; + } + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + 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 + .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 + ); +} + +/// 키 없이 붙은 플러그인도 혼자면 맡는다. +/// +/// `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 + ); +} 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/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] diff --git a/crates/devup-mcp/src/server/mod.rs b/crates/devup-mcp/src/server/mod.rs index 0902b7a..9f3c5af 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)) } } @@ -560,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 { diff --git a/plugin/.gitignore b/plugin/.gitignore new file mode 100644 index 0000000..9dae464 --- /dev/null +++ b/plugin/.gitignore @@ -0,0 +1,9 @@ +node_modules/ + +# 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 new file mode 100644 index 0000000..f6319d0 --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,106 @@ +# Devup Bridge + +devup-mcp 가 Figma 를 읽을 때 공식 MCP 의 요금 한도를 쓰지 않게 해 주는 플러그인입니다. + +## 왜 필요한가 + +devup-mcp 의 수집은 읽기 스크립트를 공식 MCP 의 `use_figma` 로 보내는 방식이고, +한도가 걸리는 곳이 바로 그 도구입니다. 화면 하나를 받는 데 snapshot 만 십수 회가 +들어가므로 한도는 금방 바닥납니다. + +그 스크립트들이 건드리는 것은 문서화된 Plugin API 뿐입니다. + +``` +getNodeByIdAsync getStyleByIdAsync variables root fileKey +getLocalPaintStylesAsync / TextStylesAsync / EffectStylesAsync / GridStylesAsync +``` + +전부 평범한 플러그인이 쓸 수 있는 것들이라, 우리가 돌리는 플러그인이 같은 일을 +대신할 수 있습니다. 그 경로에는 한도가 없습니다. + +## 설치 + +빌드할 필요 없습니다. `dist/` 가 저장소에 들어 있습니다. + +Figma 데스크톱 앱에서 **Plugins → Development → Import plugin from manifest** 를 +고르고 `plugin/manifest.json` 을 선택하면 끝입니다. + +브라우저판에서는 개발 플러그인을 불러올 수 없으므로 데스크톱 앱이 필요합니다. + +### 플러그인 소스를 고쳤다면 + +```bash +cd plugin +npm install +npm run build # dist/ 를 다시 만든다 — 함께 커밋해야 한다 +``` + +`dist/` 는 의도적으로 커밋합니다. 받는 사람이 Node 없이 곧장 import 할 수 있게 +하려는 것이고, 그 대가로 원본과 어긋날 위험이 생기므로 CI 가 매번 다시 빌드해 +`git diff --exit-code -- dist` 로 대조합니다. 빌드는 재현 가능합니다 — 같은 입력에서 +같은 바이트가 나옵니다. 어긋난 채로는 병합되지 않습니다. + +## 쓰는 법 + +읽으려는 파일을 열고 플러그인을 실행한 뒤, 창을 **열어 둔 채로** 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 로 동작합니다. 오류가 아니라 정상 동작입니다. + +**`ws://127.0.0.1` 은 쓸 수 없습니다.** Figma 는 `allowedDomains` 에 그 주소를 적으면 +"유효한 URL 이 아니다"라며 **매니페스트 자체를 거부**해 플러그인이 실행되지 않습니다. +`localhost` 로만 적어야 하고, UI 도 같은 이름으로 접속합니다. 실제 설치에서 확인한 +동작입니다. + +**파일을 하나만 열어 두십시오.** 플러그인은 붙을 때 `figma.fileKey` 로 자기 파일을 +알리는데, 이 값이 **비어 오는 경우가 있습니다**(Dev Mode 에서 확인). 그때도 연결은 +등록되지만, 어느 파일인지 모르므로 **혼자 붙어 있을 때만** 읽기를 맡습니다. 키 없는 +플러그인이 둘 이상이면 어느 쪽이 대상 파일인지 가릴 수 없어 공식 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 ← 중간 산출물 (커밋하지 않음) + ↓ rspack + scripts/inline-ui.mjs +plugin/dist/{code.js,ui.html} ← 최종 번들 (커밋함, CI 가 대조) +``` + +두 경로가 같은 소스를 쓰므로 봉투가 갈라질 수 없습니다. 원본의 플레이스홀더는 +함수 인자로 바뀌고, 치환되지 않은 것이 남으면 빌드가 실패합니다. + +`__DEVUP_SNAPSHOT_CURSOR__` 만 예외로 그대로 남습니다. 값이 아니라 Rust 디코더와 +공유하는 마커 노드 id 라서, 치환하면 페이지네이션이 조용히 깨집니다. 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/manifest.json b/plugin/manifest.json new file mode 100644 index 0000000..53dd87b --- /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"], + "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..dd69675 --- /dev/null +++ b/plugin/package.json @@ -0,0 +1,21 @@ +{ + "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", + "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" + }, + "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..1c79c88 --- /dev/null +++ b/plugin/rspack.config.js @@ -0,0 +1,72 @@ +// 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', + // `dist/` 는 커밋된다. 소스맵은 Figma 가 읽지 않으면서 번들의 네 배가 넘고, + // 빌드마다 저장소에 들어갈 이유가 없다. + devtool: false, + 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', + devtool: false, + 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/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)`) diff --git a/plugin/src/code.ts b/plugin/src/code.ts new file mode 100644 index 0000000..580b2b9 --- /dev/null +++ b/plugin/src/code.ts @@ -0,0 +1,142 @@ +// 플러그인 메인 스레드 — 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.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) => { + 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') { + queue.push(message as JobMessage) + // onmessage 를 async 로 만들면 Figma 가 반환값을 기다리지 않아 예외가 조용히 + // 사라진다. 큐에 넣고 배수는 따로 돌린다. + void drain() + } +} 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..ef03f0c --- /dev/null +++ b/plugin/src/ui.ts @@ -0,0 +1,153 @@ +// 플러그인 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 + // 반드시 `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) + } 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 가 그 요청을 이미 타임아웃 + // 처리했고, 재연결 후 다시 보낼 것이다. + } +} + +/** + * 창이 뒤로 가도 이 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' } }, '*') 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"] +}