diff --git a/.gitignore b/.gitignore index 79a729e..ed5a8f3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ dist/ code.js coverage -.claude \ No newline at end of file +.claude +.sisyphus diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..884bc01 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,107 @@ +# PROJECT KNOWLEDGE BASE + +**Generated:** 2026-02-10 +**Commit:** 0a8c481 +**Branch:** main + +## OVERVIEW + +Figma plugin that generates React/TypeScript components using `@devup-ui/react` from Figma designs, plus design system (Devup config) import/export. No UI iframe — codegen + menu commands only. + +## STRUCTURE + +``` +. +├── src/ +│ ├── code.ts # Entry point (thin wrapper → code-impl) +│ ├── code-impl.ts # Plugin bootstrap: codegen registration + command routing +│ ├── types.ts # Shared types: ComponentType, DevupElement, DevupNode +│ ├── utils.ts # Core helpers: getComponentName, space +│ ├── codegen/ # ** Code generation engine (see src/codegen/AGENTS.md) +│ ├── commands/ # Plugin menu commands (9 commands) +│ │ ├── devup/ # Design system export/import (JSON + Excel via SheetJS) +│ │ ├── exportAssets.ts # Asset export (stub — commented out implementation) +│ │ ├── exportComponents.ts +│ │ └── exportPagesAndComponents.ts +│ └── utils/ # Shared utilities (color, typography, file I/O) +├── manifest.json # Figma plugin manifest (id: 1412341601954480694) +├── rspack.config.js # Bundler: single entry → dist/code.js +├── ui.html # Legacy UI (not referenced in manifest) +└── dist/code.js # Compiled bundle (gitignored) +``` + +## WHERE TO LOOK + +| Task | Location | Notes | +|------|----------|-------| +| Add new CSS property support | `src/codegen/props/` | Create `your-prop.ts`, import in `props/index.ts` | +| Add new plugin command | `src/commands/` + `manifest.json` + `code-impl.ts` | Add to manifest menu, route in `runCommand()` | +| Fix code generation output | `src/codegen/Codegen.ts` | Tree-build phase: `buildTree()`, render phase: `renderTree()` | +| Fix responsive behavior | `src/codegen/responsive/` | Breakpoints in `index.ts`, merge logic in `ResponsiveCodegen.ts` | +| Fix component rendering | `src/codegen/render/index.ts` | `renderNode()` for JSX, `renderComponent()` for wrappers | +| Add shared utility | `src/utils/` | One function per file, pure functions | +| Fix design system export | `src/commands/devup/export-devup.ts` | JSON and Excel paths | +| Fix design system import | `src/commands/devup/import-devup.ts` | Applies typography via `apply-typography.ts` | +| Understand prop filtering | `src/codegen/props/index.ts` | `filterPropsWithComponent()` removes defaults per component | +| Debug node processing | `src/codegen/utils/node-proxy.ts` | `nodeProxyTracker` logs all property access when `debug=true` | + +## CONVENTIONS + +- **No semicolons**, single quotes, 2-space indent (Biome enforced) +- **Strict TypeScript** — `noImplicitAny` on, no DOM types (Figma sandbox only) +- **No `any` in production code** — `noExplicitAny: off` only in `__tests__/` files +- **Warnings = errors** — `biome check --error-on-warnings` +- **One function per utility file** — `src/utils/*.ts` and `src/codegen/utils/*.ts` +- **Prop getters return spread-compatible objects** — `getXxxProps(node) → Record` +- **Test co-location** — `__tests__/` directory adjacent to source, with `__snapshots__/` +- **No barrel exports in utils** — import each utility directly by path +- **Barrel exports via index.ts** — used in `codegen/props`, `codegen/render`, `codegen/responsive`, `commands/devup` + +## ANTI-PATTERNS (THIS PROJECT) + +- **Never treat responsive arrays as default values** — Arrays bypass `isDefaultProp` filtering (`is-default-prop.ts:27`) +- **Never pass `effect` or `viewport` as component props** — Reserved internal variant keys, handled via pseudo-selectors/responsive arrays +- **Never append rotation transforms** — Always replace entire value (`reaction.ts`) +- **Animation targets are not assets** — Nodes with `SMART_ANIMATE` reactions must not be exported as images (`check-asset-node.ts:35`) +- **Tile-mode fills are not images** — `PATTERN`/`TILE` fills are backgrounds, not exportable assets +- **Padding/margin zero-filtering is disabled** — Commented-out regexes in `is-default-prop.ts` were intentionally abandoned +- **`exportAssets` is a stub** — Entire implementation commented out, marked "in development" + +## COMMANDS + +```bash +bun run dev # Rspack dev server +bun run build # Production bundle → dist/code.js +bun run watch # Build in watch mode +bun run test # tsc --noEmit && bun test --coverage +bun run lint # biome check --error-on-warnings +bun run lint:fix # biome check --fix +``` + +## TOOLCHAIN + +| Tool | Version | Purpose | +|------|---------|---------| +| Bun | latest | Package manager + test runner | +| Rspack | 1.7.6 | Bundler (SWC transpilation) | +| TypeScript | 5.9 | Type checking (es2015 target) | +| Biome | 2.3 | Linting + formatting | +| Husky | 9 | Pre-commit: `bun run lint && bun run test` | + +## CI + +GitHub Actions (`.github/workflows/CI.yml`): `bun install` → `bun run lint` → `bun test --coverage` → Codecov upload (main only). Cancel-in-progress on same ref. + +## DEPENDENCIES + +Only **1 runtime dep**: `jszip` (ZIP creation for component/asset export). Network: only `https://cdn.sheetjs.com` allowed (Excel support via dynamic import). + +## NOTES + +- Plugin runs in **Figma sandbox** — no `window`, `document`, or DOM APIs +- `ui.html` exists at root but is NOT referenced in `manifest.json` — appears to be legacy +- `debug = true` in `code-impl.ts` enables `nodeProxyTracker` which logs all Figma node property access for test case generation +- Coverage threshold is `0.9999` (`bunfig.toml`) — near 100% coverage required +- `exportPagesAndComponents.ts` is excluded from coverage (`coveragePathIgnorePatterns`) +- Codegen test file is massive: `codegen.test.ts` = 59,673 lines (snapshot-heavy) +- Only 2 lint suppressions in entire codebase (both justified with comments) diff --git a/bun.lock b/bun.lock index 52d0e48..be2d2d7 100644 --- a/bun.lock +++ b/bun.lock @@ -9,9 +9,9 @@ }, "devDependencies": { "@biomejs/biome": "^2.3", - "@figma/plugin-typings": "^1.121", - "@rspack/cli": "^1.6.7", - "@rspack/core": "^1.6.7", + "@figma/plugin-typings": "^1.123", + "@rspack/cli": "^1.7.6", + "@rspack/core": "^1.7.6", "@types/bun": "^1.3", "husky": "^9.1", "typescript": "^5.9", @@ -19,40 +19,56 @@ }, }, "packages": { - "@biomejs/biome": ["@biomejs/biome@2.3.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.8", "@biomejs/cli-darwin-x64": "2.3.8", "@biomejs/cli-linux-arm64": "2.3.8", "@biomejs/cli-linux-arm64-musl": "2.3.8", "@biomejs/cli-linux-x64": "2.3.8", "@biomejs/cli-linux-x64-musl": "2.3.8", "@biomejs/cli-win32-arm64": "2.3.8", "@biomejs/cli-win32-x64": "2.3.8" }, "bin": { "biome": "bin/biome" } }, "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA=="], + "@biomejs/biome": ["@biomejs/biome@2.3.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.14", "@biomejs/cli-darwin-x64": "2.3.14", "@biomejs/cli-linux-arm64": "2.3.14", "@biomejs/cli-linux-arm64-musl": "2.3.14", "@biomejs/cli-linux-x64": "2.3.14", "@biomejs/cli-linux-x64-musl": "2.3.14", "@biomejs/cli-win32-arm64": "2.3.14", "@biomejs/cli-win32-x64": "2.3.14" }, "bin": { "biome": "bin/biome" } }, "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ=="], "@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="], - "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@figma/plugin-typings": ["@figma/plugin-typings@1.121.0", "", {}, "sha512-590qfxc5QtGs/AqdAEegNZUWzwEuaA9whl6T8LxscBaGjmqU3Z5jJHgHfYXissy1S5IBsXEZFmudKibpLcxGdQ=="], + "@figma/plugin-typings": ["@figma/plugin-typings@1.123.0", "", {}, "sha512-NLv2aQ8R9dP5psDplWpq+pJxRUGsJ1YEYYbBV2oTd03kS+aau7N9XWLjw52s1uVgi8jQ33N001EX3f7vSCztjQ=="], "@jsonjoy.com/base64": ["@jsonjoy.com/base64@1.1.2", "", { "peerDependencies": { "tslib": "2" } }, "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA=="], - "@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@1.2.1", "", { "peerDependencies": { "tslib": "2" } }, "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA=="], + "@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw=="], "@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@1.0.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g=="], + "@jsonjoy.com/fs-core": ["@jsonjoy.com/fs-core@4.56.10", "", { "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.56.10", "@jsonjoy.com/fs-node-utils": "4.56.10", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw=="], + + "@jsonjoy.com/fs-fsa": ["@jsonjoy.com/fs-fsa@4.56.10", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.56.10", "@jsonjoy.com/fs-node-builtins": "4.56.10", "@jsonjoy.com/fs-node-utils": "4.56.10", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q=="], + + "@jsonjoy.com/fs-node": ["@jsonjoy.com/fs-node@4.56.10", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.56.10", "@jsonjoy.com/fs-node-builtins": "4.56.10", "@jsonjoy.com/fs-node-utils": "4.56.10", "@jsonjoy.com/fs-print": "4.56.10", "@jsonjoy.com/fs-snapshot": "4.56.10", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q=="], + + "@jsonjoy.com/fs-node-builtins": ["@jsonjoy.com/fs-node-builtins@4.56.10", "", { "peerDependencies": { "tslib": "2" } }, "sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw=="], + + "@jsonjoy.com/fs-node-to-fsa": ["@jsonjoy.com/fs-node-to-fsa@4.56.10", "", { "dependencies": { "@jsonjoy.com/fs-fsa": "4.56.10", "@jsonjoy.com/fs-node-builtins": "4.56.10", "@jsonjoy.com/fs-node-utils": "4.56.10" }, "peerDependencies": { "tslib": "2" } }, "sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw=="], + + "@jsonjoy.com/fs-node-utils": ["@jsonjoy.com/fs-node-utils@4.56.10", "", { "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.56.10" }, "peerDependencies": { "tslib": "2" } }, "sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg=="], + + "@jsonjoy.com/fs-print": ["@jsonjoy.com/fs-print@4.56.10", "", { "dependencies": { "@jsonjoy.com/fs-node-utils": "4.56.10", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw=="], + + "@jsonjoy.com/fs-snapshot": ["@jsonjoy.com/fs-snapshot@4.56.10", "", { "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", "@jsonjoy.com/fs-node-utils": "4.56.10", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g=="], + "@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@1.21.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" }, "peerDependencies": { "tslib": "2" } }, "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg=="], "@jsonjoy.com/json-pointer": ["@jsonjoy.com/json-pointer@1.0.2", "", { "dependencies": { "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/util": "^1.9.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg=="], @@ -61,49 +77,49 @@ "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], - "@module-federation/error-codes": ["@module-federation/error-codes@0.21.6", "", {}, "sha512-MLJUCQ05KnoVl8xd6xs9a5g2/8U+eWmVxg7xiBMeR0+7OjdWUbHwcwgVFatRIwSZvFgKHfWEiI7wsU1q1XbTRQ=="], + "@module-federation/error-codes": ["@module-federation/error-codes@0.22.0", "", {}, "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug=="], - "@module-federation/runtime": ["@module-federation/runtime@0.21.6", "", { "dependencies": { "@module-federation/error-codes": "0.21.6", "@module-federation/runtime-core": "0.21.6", "@module-federation/sdk": "0.21.6" } }, "sha512-+caXwaQqwTNh+CQqyb4mZmXq7iEemRDrTZQGD+zyeH454JAYnJ3s/3oDFizdH6245pk+NiqDyOOkHzzFQorKhQ=="], + "@module-federation/runtime": ["@module-federation/runtime@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/runtime-core": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA=="], - "@module-federation/runtime-core": ["@module-federation/runtime-core@0.21.6", "", { "dependencies": { "@module-federation/error-codes": "0.21.6", "@module-federation/sdk": "0.21.6" } }, "sha512-5Hd1Y5qp5lU/aTiK66lidMlM/4ji2gr3EXAtJdreJzkY+bKcI5+21GRcliZ4RAkICmvdxQU5PHPL71XmNc7Lsw=="], + "@module-federation/runtime-core": ["@module-federation/runtime-core@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA=="], - "@module-federation/runtime-tools": ["@module-federation/runtime-tools@0.21.6", "", { "dependencies": { "@module-federation/runtime": "0.21.6", "@module-federation/webpack-bundler-runtime": "0.21.6" } }, "sha512-fnP+ZOZTFeBGiTAnxve+axGmiYn2D60h86nUISXjXClK3LUY1krUfPgf6MaD4YDJ4i51OGXZWPekeMe16pkd8Q=="], + "@module-federation/runtime-tools": ["@module-federation/runtime-tools@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/webpack-bundler-runtime": "0.22.0" } }, "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA=="], - "@module-federation/sdk": ["@module-federation/sdk@0.21.6", "", {}, "sha512-x6hARETb8iqHVhEsQBysuWpznNZViUh84qV2yE7AD+g7uIzHKiYdoWqj10posbo5XKf/147qgWDzKZoKoEP2dw=="], + "@module-federation/sdk": ["@module-federation/sdk@0.22.0", "", {}, "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g=="], - "@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.21.6", "", { "dependencies": { "@module-federation/runtime": "0.21.6", "@module-federation/sdk": "0.21.6" } }, "sha512-7zIp3LrcWbhGuFDTUMLJ2FJvcwjlddqhWGxi/MW3ur1a+HaO8v5tF2nl+vElKmbG1DFLU/52l3PElVcWf/YcsQ=="], + "@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - "@rspack/binding": ["@rspack/binding@1.6.7", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.6.7", "@rspack/binding-darwin-x64": "1.6.7", "@rspack/binding-linux-arm64-gnu": "1.6.7", "@rspack/binding-linux-arm64-musl": "1.6.7", "@rspack/binding-linux-x64-gnu": "1.6.7", "@rspack/binding-linux-x64-musl": "1.6.7", "@rspack/binding-wasm32-wasi": "1.6.7", "@rspack/binding-win32-arm64-msvc": "1.6.7", "@rspack/binding-win32-ia32-msvc": "1.6.7", "@rspack/binding-win32-x64-msvc": "1.6.7" } }, "sha512-7ICabuBN3gHc6PPN52+m1kruz3ogiJjg1C0gSWdLRk18m/4jlcM2aAy6wfXjgODJdB0Yh2ro/lIpBbj+AYWUGA=="], + "@rspack/binding": ["@rspack/binding@1.7.6", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.6", "@rspack/binding-darwin-x64": "1.7.6", "@rspack/binding-linux-arm64-gnu": "1.7.6", "@rspack/binding-linux-arm64-musl": "1.7.6", "@rspack/binding-linux-x64-gnu": "1.7.6", "@rspack/binding-linux-x64-musl": "1.7.6", "@rspack/binding-wasm32-wasi": "1.7.6", "@rspack/binding-win32-arm64-msvc": "1.7.6", "@rspack/binding-win32-ia32-msvc": "1.7.6", "@rspack/binding-win32-x64-msvc": "1.7.6" } }, "sha512-/NrEcfo8Gx22hLGysanrV6gHMuqZSxToSci/3M4kzEQtF5cPjfOv5pqeLK/+B6cr56ul/OmE96cCdWcXeVnFjQ=="], - "@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.6.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QiIAP8JTAtht0j8/xZZEQTJRB9e+KrOm9c7JJm73CewVg55rDWRrwopiVfBNlTu1coem1ztUHJYdQhg2uXfqww=="], + "@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.7.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NZ9AWtB1COLUX1tA9HQQvWpTy07NSFfKBU8A6ylWd5KH8AePZztpNgLLAVPTuNO4CZXYpwcoclf8jG/luJcQdQ=="], - "@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.6.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-DpQRxxTXkMMNPmBXeJBaAB8HmWKxH2IfvHv7vU+kBhJ3xdPtXU4/xBv1W3biluoNRG11gc1WLIgjzeGgaLCxmw=="], + "@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.7.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-J2g6xk8ZS7uc024dNTGTHxoFzFovAZIRixUG7PiciLKTMP78svbSSWrmW6N8oAsAkzYfJWwQpVgWfFNRHvYxSw=="], - "@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.6.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-211/XoBiooGGgUo/NxNpsrzGUXtH1d7g/4+UTtjYtfc8QHwu7ZMHcsqg0wss53fXzn/yyxd0DZ56vBHq52BiFw=="], + "@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.7.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-eQfcsaxhFrv5FmtaA7+O1F9/2yFDNIoPZzV/ZvqvFz5bBXVc4FAm/1fVpBg8Po/kX1h0chBc7Xkpry3cabFW8w=="], - "@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.6.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-0WnqAWz3WPDsXGvOOA++or7cHpoidVsH3FlqNaAfRu6ni6n7ig/s0/jKUB+C5FtXOgmGjAGkZHfFgNHsvZ0FWw=="], + "@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.7.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-DfQXKiyPIl7i1yECHy4eAkSmlUzzsSAbOjgMuKn7pudsWf483jg0UUYutNgXSlBjc/QSUp7906Cg8oty9OfwPA=="], - "@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.6.7", "", { "os": "linux", "cpu": "x64" }, "sha512-iMrE0Q4IuYpkE0MjpaOVaUDYbQFiCRI9D3EPoXzlXJj4kJSdNheODpHTBVRlWt8Xp7UAoWuIFXCvKFKcSMm3aQ=="], + "@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.7.6", "", { "os": "linux", "cpu": "x64" }, "sha512-NdA+2X3lk2GGrMMnTGyYTzM3pn+zNjaqXqlgKmFBXvjfZqzSsKq3pdD1KHZCd5QHN+Fwvoszj0JFsquEVhE1og=="], - "@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.6.7", "", { "os": "linux", "cpu": "x64" }, "sha512-e7gKFxpdEQwYGk7lTC/hukTgNtaoAstBXehnZNk4k3kuU6+86WDrkn18Cd949iNqfIPtIG/wIsFNGbkHsH69hQ=="], + "@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.7.6", "", { "os": "linux", "cpu": "x64" }, "sha512-rEy6MHKob02t/77YNgr6dREyJ0e0tv1X6Xsg8Z5E7rPXead06zefUbfazj4RELYySWnM38ovZyJAkPx/gOn3VA=="], - "@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.6.7", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-yx88EFdE9RP3hh7VhjjW6uc6wGU0KcpOcZp8T8E/a+X8L98fX0aVrtM1IDbndhmdluIMqGbfJNap2+QqOCY9Mw=="], + "@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.7.6", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-YupOrz0daSG+YBbCIgpDgzfMM38YpChv+afZpaxx5Ml7xPeAZIIdgWmLHnQ2rts73N2M1NspAiBwV00Xx0N4Vg=="], - "@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.6.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-vgxVYpFK8P5ulSXQQA+EbX78R/SUU+WIf0JIY+LoUoP89gZOsise/lKAJMAybzpeTJ1t0ndLchFznDYnzq+l4Q=="], + "@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.7.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-INj7aVXjBvlZ84kEhSK4kJ484ub0i+BzgnjDWOWM1K+eFYDZjLdAsQSS3fGGXwVc3qKbPIssFfnftATDMTEJHQ=="], - "@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.6.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-bV5RTW0Va0UQKJm9HWLt7fWNBPaBBBxCJOA2pJT3nGGm6CCXKnZSyEiVbFUk4jI/uiwBfqenlLkzaGoMRbeDhA=="], + "@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.7.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-lXGvC+z67UMcw58In12h8zCa9IyYRmuptUBMItQJzu+M278aMuD1nETyGLL7e4+OZ2lvrnnBIcjXN1hfw2yRzw=="], - "@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.6.7", "", { "os": "win32", "cpu": "x64" }, "sha512-8xlbuJQtYktlBjZupOHlO8FeZqSIhsV3ih7xBSiOYar6LI6uQzA7XiO3I5kaPSDirBMMMKv1Z4rKCxWx10a3TQ=="], + "@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.7.6", "", { "os": "win32", "cpu": "x64" }, "sha512-zeUxEc0ZaPpmaYlCeWcjSJUPuRRySiSHN23oJ2Xyw0jsQ01Qm4OScPdr0RhEOFuK/UE+ANyRtDo4zJsY52Hadw=="], - "@rspack/cli": ["@rspack/cli@1.6.7", "", { "dependencies": { "@discoveryjs/json-ext": "^0.5.7", "@rspack/dev-server": "~1.1.4", "exit-hook": "^4.0.0", "webpack-bundle-analyzer": "4.10.2" }, "peerDependencies": { "@rspack/core": "^1.0.0-alpha || ^1.x" }, "bin": { "rspack": "bin/rspack.js" } }, "sha512-lWU4Gfw3HIysXnBuN1Ta43BeQHewoHd3MAzRlxmEYsUPxau26h6oTpL5WnLsT/Eh4G7XqnW1NJ8COujnn9EGfg=="], + "@rspack/cli": ["@rspack/cli@1.7.6", "", { "dependencies": { "@discoveryjs/json-ext": "^0.5.7", "@rspack/dev-server": "~1.1.5", "exit-hook": "^4.0.0", "webpack-bundle-analyzer": "4.10.2" }, "peerDependencies": { "@rspack/core": "^1.0.0-alpha || ^1.x" }, "bin": { "rspack": "bin/rspack.js" } }, "sha512-oIC8F3es8EIZfme5jy/MCf9KJPhYlR9pBzKr8vzLal1H/aheGobNhiV0tYRl22I2gx9XAQItiea36g04byiEOw=="], - "@rspack/core": ["@rspack/core@1.6.7", "", { "dependencies": { "@module-federation/runtime-tools": "0.21.6", "@rspack/binding": "1.6.7", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-tkd4nSzTf+pDa9OAE4INi/JEa93HNszjWy5C9+trf4ZCXLLHsHxHQFbzoreuz4Vv2PlCWajgvAdiPMV1vGIkuw=="], + "@rspack/core": ["@rspack/core@1.7.6", "", { "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.6", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-Iax6UhrfZqJajA778c1d5DBFbSIqPOSrI34kpNIiNpWd8Jq7mFIa+Z60SQb5ZQDZuUxcCZikjz5BxinFjTkg7Q=="], - "@rspack/dev-server": ["@rspack/dev-server@1.1.4", "", { "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" }, "peerDependencies": { "@rspack/core": "*" } }, "sha512-kGHYX2jYf3ZiHwVl0aUEPBOBEIG1aWleCDCAi+Jg32KUu3qr/zDUpCEd0wPuHfLEgk0X0xAEYCS6JMO7nBStNQ=="], + "@rspack/dev-server": ["@rspack/dev-server@1.1.5", "", { "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" }, "peerDependencies": { "@rspack/core": "*" } }, "sha512-cwz0qc6iqqoJhyWqxP7ZqE2wyYNHkBMQUXxoQ0tNoZ4YNRkDyQ4HVJ/3oPSmMKbvJk/iJ16u7xZmwG6sK47q/A=="], "@rspack/lite-tapable": ["@rspack/lite-tapable@1.1.0", "", {}, "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw=="], @@ -113,7 +129,7 @@ "@types/bonjour": ["@types/bonjour@3.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ=="], - "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], @@ -121,7 +137,7 @@ "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="], - "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg=="], + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="], "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], @@ -131,7 +147,7 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - "@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="], + "@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="], "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], @@ -141,7 +157,7 @@ "@types/retry": ["@types/retry@0.12.2", "", {}, "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="], - "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], "@types/serve-index": ["@types/serve-index@1.9.4", "", { "dependencies": { "@types/express": "*" } }, "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug=="], @@ -179,7 +195,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -215,7 +231,7 @@ "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -353,7 +369,7 @@ "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - "memfs": ["memfs@4.51.1", "", { "dependencies": { "@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" } }, "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ=="], + "memfs": ["memfs@4.56.10", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.56.10", "@jsonjoy.com/fs-fsa": "4.56.10", "@jsonjoy.com/fs-node": "4.56.10", "@jsonjoy.com/fs-node-builtins": "4.56.10", "@jsonjoy.com/fs-node-to-fsa": "4.56.10", "@jsonjoy.com/fs-node-utils": "4.56.10", "@jsonjoy.com/fs-print": "4.56.10", "@jsonjoy.com/fs-snapshot": "4.56.10", "@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" } }, "sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w=="], "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], @@ -363,7 +379,7 @@ "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -409,7 +425,7 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -437,11 +453,11 @@ "selfsigned": ["selfsigned@2.4.1", "", { "dependencies": { "@types/node-forge": "^1.3.0", "node-forge": "^1" } }, "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q=="], - "send": ["send@0.19.1", "", { "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.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="], + "send": ["send@0.19.2", "", { "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" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - "serve-index": ["serve-index@1.9.1", "", { "dependencies": { "accepts": "~1.3.4", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", "http-errors": "~1.6.2", "mime-types": "~2.1.17", "parseurl": "~1.3.2" } }, "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw=="], + "serve-index": ["serve-index@1.9.2", "", { "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" } }, "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ=="], - "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], @@ -511,10 +527,20 @@ "websocket-extensions": ["websocket-extensions@0.1.4", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="], - "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@17.67.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" }, "peerDependencies": { "tslib": "2" } }, "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w=="], + + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/util": ["@jsonjoy.com/util@17.67.0", "", { "dependencies": { "@jsonjoy.com/buffers": "17.67.0", "@jsonjoy.com/codegen": "17.67.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew=="], + + "@jsonjoy.com/json-pack/@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@1.2.1", "", { "peerDependencies": { "tslib": "2" } }, "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA=="], + + "@jsonjoy.com/util/@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@1.2.1", "", { "peerDependencies": { "tslib": "2" } }, "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA=="], + + "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "compression/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -523,19 +549,11 @@ "express/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - "send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - - "serve-index/http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="], - - "serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + "serve-index/http-errors": ["http-errors@1.8.1", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.1" } }, "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g=="], "spdy/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -549,24 +567,22 @@ "websocket-driver/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "serve-index/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], - - "serve-index/http-errors/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="], - - "serve-index/http-errors/setprototypeof": ["setprototypeof@1.1.0", "", {}, "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="], + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/base64": ["@jsonjoy.com/base64@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw=="], - "serve-index/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q=="], - "serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/json-pointer": ["@jsonjoy.com/json-pointer@17.67.0", "", { "dependencies": { "@jsonjoy.com/util": "17.67.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA=="], - "serve-static/send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/util/@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q=="], - "serve-static/send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "serve-index/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], - "serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "serve-index/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], "spdy-transport/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "spdy/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "webpack-dev-middleware/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], } } diff --git a/package.json b/package.json index 2c74eff..4f34fbd 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "author": "", "license": "", "devDependencies": { - "@figma/plugin-typings": "^1.121", - "@rspack/cli": "^1.6.7", - "@rspack/core": "^1.6.7", + "@figma/plugin-typings": "^1.123", + "@rspack/cli": "^1.7.6", + "@rspack/core": "^1.7.6", "husky": "^9.1", "typescript": "^5.9", diff --git a/src/code-impl.ts b/src/code-impl.ts index 0d4112e..e550b86 100644 --- a/src/code-impl.ts +++ b/src/code-impl.ts @@ -1,12 +1,20 @@ -import { Codegen } from './codegen/Codegen' +import { + Codegen, + resetGlobalBuildTreeCache, + resetMainComponentCache, +} from './codegen/Codegen' +import { resetGetPropsCache } from './codegen/props' +import { resetSelectorPropsCache } from './codegen/props/selector' import { ResponsiveCodegen } from './codegen/responsive/ResponsiveCodegen' import { nodeProxyTracker } from './codegen/utils/node-proxy' +import { perfEnd, perfReport, perfReset, perfStart } from './codegen/utils/perf' +import { resetVariableCache } from './codegen/utils/variable-cache' import { wrapComponent } from './codegen/utils/wrap-component' import { exportDevup, importDevup } from './commands/devup' import { exportAssets } from './commands/exportAssets' import { exportComponents } from './commands/exportComponents' import { exportPagesAndComponents } from './commands/exportPagesAndComponents' -import { getComponentName } from './utils' +import { getComponentName, resetTextStyleCache } from './utils' import { toPascal } from './utils/to-pascal' const DEVUP_COMPONENTS = [ @@ -122,13 +130,28 @@ const debug = true export function registerCodegen(ctx: typeof figma) { if (ctx.editorType === 'dev' && ctx.mode === 'codegen') { ctx.codegen.on('generate', async ({ node: n, language }) => { - const node = debug ? nodeProxyTracker.wrap(n) : n + // Use the raw node for codegen (no Proxy overhead). + // Debug tracking happens AFTER codegen completes via separate walk. + const node = n switch (language) { case 'devup-ui': { const time = Date.now() + perfReset() + resetGetPropsCache() + resetSelectorPropsCache() + resetVariableCache() + resetTextStyleCache() + resetMainComponentCache() + resetGlobalBuildTreeCache() + + let t = perfStart() const codegen = new Codegen(node) await codegen.run() + perfEnd('Codegen.run()', t) + + t = perfStart() const componentsCodes = codegen.getComponentsCodes() + perfEnd('getComponentsCodes()', t) // Generate responsive component codes with variant support let responsiveComponentsCodes: ReadonlyArray< @@ -136,11 +159,13 @@ export function registerCodegen(ctx: typeof figma) { > = [] if (node.type === 'COMPONENT_SET') { const componentName = getComponentName(node) + t = perfStart() responsiveComponentsCodes = await ResponsiveCodegen.generateVariantResponsiveComponents( node, componentName, ) + perfEnd('generateVariantResponsiveComponents(COMPONENT_SET)', t) } // Generate responsive codes for components extracted from the page @@ -163,11 +188,16 @@ export function registerCodegen(ctx: typeof figma) { if (parentSet && !processedComponentSets.has(parentSet.id)) { processedComponentSets.add(parentSet.id) const componentName = getComponentName(parentSet) + t = perfStart() const responsiveCodes = await ResponsiveCodegen.generateVariantResponsiveComponents( parentSet, componentName, ) + perfEnd( + `generateVariantResponsiveComponents(${componentName})`, + t, + ) responsiveResults.push(...responsiveCodes) } } @@ -175,6 +205,7 @@ export function registerCodegen(ctx: typeof figma) { } console.info(`[benchmark] devup-ui end ${Date.now() - time}ms`) + console.info(perfReport()) // Check if node itself is SECTION or has a parent SECTION const isNodeSection = ResponsiveCodegen.canGenerateResponsive(node) @@ -232,6 +263,9 @@ export function registerCodegen(ctx: typeof figma) { } } if (debug) { + // Track AFTER codegen — collects all node properties for test case + // generation without Proxy overhead during the hot codegen path. + nodeProxyTracker.trackTree(node) console.log( await nodeProxyTracker.toTestCaseFormatWithVariables(node.id), ) diff --git a/src/codegen/AGENTS.md b/src/codegen/AGENTS.md new file mode 100644 index 0000000..e05841c --- /dev/null +++ b/src/codegen/AGENTS.md @@ -0,0 +1,99 @@ +# CODEGEN ENGINE + +Converts Figma `SceneNode` trees into React/TypeScript JSX using `@devup-ui/react` components. + +## PIPELINE + +``` +SceneNode → buildTree() → NodeTree (JSON) → renderTree() → JSX string + ↓ + ResponsiveCodegen merges trees across breakpoints +``` + +Two phases in `Codegen.ts`: +1. **Build** (`buildTree`): Walks Figma node hierarchy → intermediate `NodeTree` with resolved props, component types, children +2. **Render** (`renderTree`): Static method converts `NodeTree` → indented JSX string + +`INSTANCE` nodes trigger `addComponentTree()` to extract referenced components into separate output files. + +## STRUCTURE + +``` +codegen/ +├── Codegen.ts # Core engine (buildTree + renderTree) +├── types.ts # NodeTree, ComponentTree, Props interfaces +├── props/ # 22 prop getters — one per CSS concern +│ ├── index.ts # getProps() composes all; filterPropsWithComponent() +│ ├── auto-layout.ts # Flex direction, gap, wrap +│ ├── layout.ts # Width, height, min/max sizing +│ ├── position.ts # Absolute/relative positioning, z-index +│ ├── reaction.ts # Animations, transitions, keyframes (782 lines) +│ ├── selector.ts # Pseudo-selectors from component variants (251 lines) +│ └── ... # padding, border, background, blend, effect, etc. +├── render/ +│ ├── index.ts # renderNode() → JSX tag; renderComponent() → function wrapper +│ └── text.ts # renderText() → mixed text segments with styling +├── responsive/ +│ ├── index.ts # Breakpoint constants, mergePropsToResponsive/Variant +│ └── ResponsiveCodegen.ts # Section-based responsive code generation (1569 lines) +└── utils/ # 26 pure utility files (no barrel export) +``` + +## WHERE TO LOOK + +| Task | Start Here | Then | +|------|-----------|------| +| Add CSS property support | `props/your-prop.ts` | Import in `props/index.ts` | +| Fix JSX output formatting | `render/index.ts` | `propsToString` in `utils/props-to-str.ts` | +| Fix component detection | `utils/get-devup-component.ts` | Maps node + props → Flex/Box/Text/etc. | +| Fix asset detection | `utils/check-asset-node.ts` | Determines Image vs SVG vs skip | +| Fix default prop filtering | `utils/is-default-prop.ts` | Also `props/index.ts:filterPropsWithComponent` | +| Add responsive breakpoint | `responsive/index.ts` | Update `BREAKPOINTS` + `BREAKPOINT_ORDER` | +| Fix responsive merging | `responsive/ResponsiveCodegen.ts` | `mergePropsToResponsive` in `responsive/index.ts` | +| Fix variant props | `utils/extract-instance-variant-props.ts` | Reads `componentProperties` from instances | +| Debug with node logging | `utils/node-proxy.ts` | `nodeProxyTracker.wrap(node)` — logs all reads | + +## CONVENTIONS (CODEGEN-SPECIFIC) + +- **Prop getters**: `getXxxProps(node: SceneNode) → Record` — always return spread-compatible object +- **Async prop getters**: Background, border, effect, text-shadow, text-stroke, reaction use `await` (Figma API) +- **Component mapping**: `getDevupComponentByNode` for full nodes, `getDevupComponentByProps` for prop-only detection +- **Props flow**: `getProps()` → `filterProps()` (remove defaults) → `filterPropsWithComponent()` (remove component-implicit) → `propsToString()` +- **Text handling**: `renderText()` returns `{ children: string[], props }` — special path for mixed-style text segments +- **NodeTree `isComponent`**: Marks `INSTANCE` references — these render as `` not as nested JSX +- **Depth parameter**: `renderNode(component, props, depth, children)` — `depth=0` for children (parent handles indentation) + +## KEY TYPES (`types.ts`) + +```typescript +interface NodeTree { + component: string // 'Flex' | 'Box' | 'Text' | 'Image' | custom name + props: Props + children: NodeTree[] + nodeType: string // Figma type: 'FRAME', 'TEXT', 'INSTANCE', 'WRAPPER' + nodeName: string // Figma layer name + isComponent?: boolean // true → renders as reference + textChildren?: string[] // TEXT nodes only — inline content +} +``` + +## BREAKPOINTS (`responsive/index.ts`) + +| Key | Max Width | Array Index | +|-----|-----------|-------------| +| mobile | 480 | 0 | +| sm | 768 | 1 | +| tablet | 992 | 2 | +| lg | 1280 | 3 | +| pc | Infinity | 4 | + +Responsive values are 5-element arrays: `[mobile, sm, tablet, lg, pc]`. `optimizeResponsiveValue` trims trailing duplicates. + +## ANTI-PATTERNS (CODEGEN-SPECIFIC) + +- **Never skip responsive arrays in default-prop filtering** — `is-default-prop.ts:27` explicitly allows arrays through +- **`effect` and `viewport` are reserved variant keys** — `render/index.ts:47` and `ResponsiveCodegen.ts:112` filter them out of component props +- **Rotation transform: replace, never append** — `reaction.ts` always overwrites entire `transform` value +- **SMART_ANIMATE nodes are not assets** — `check-asset-node.ts` returns `false` for nodes with animation reactions +- **TILE/PATTERN fills are backgrounds** — `check-asset-node.ts` skips these as non-image content +- **`@todo` in text-stroke.ts** — Gradient stroke support is unimplemented diff --git a/src/codegen/Codegen.ts b/src/codegen/Codegen.ts index 548746d..4f1885d 100644 --- a/src/codegen/Codegen.ts +++ b/src/codegen/Codegen.ts @@ -1,6 +1,8 @@ import { getComponentName } from '../utils' import { getProps } from './props' +import { getPositionProps } from './props/position' import { getSelectorProps } from './props/selector' +import { getTransformProps } from './props/transform' import { renderComponent, renderNode } from './render' import { renderText } from './render/text' import type { ComponentTree, NodeTree } from './types' @@ -12,18 +14,77 @@ import { getDevupComponentByProps, } from './utils/get-devup-component' import { getPageNode } from './utils/get-page-node' +import { perfEnd, perfStart } from './utils/perf' import { buildCssUrl } from './utils/wrap-url' +// Global cache for node.getMainComponentAsync() results. +// Multiple Codegen instances (from ResponsiveCodegen) process the same INSTANCE nodes, +// each calling getMainComponentAsync which is an expensive Figma IPC call. +// Keyed by instance node.id; stores the Promise to deduplicate concurrent calls. +const mainComponentCache = new Map>() + +export function resetMainComponentCache(): void { + mainComponentCache.clear() +} + +function getMainComponentCached( + node: InstanceNode, +): Promise { + const cacheKey = node.id + if (cacheKey) { + const cached = mainComponentCache.get(cacheKey) + if (cached) return cached + } + const promise = node.getMainComponentAsync() + if (cacheKey) { + mainComponentCache.set(cacheKey, promise) + } + return promise +} + +// Global buildTree cache shared across all Codegen instances. +// ResponsiveCodegen creates multiple Codegen instances for the same component +// variants — without this, each instance rebuilds the entire subtree. +// Returns cloned trees (shallow-cloned props at every level) because +// downstream code mutates tree.props via Object.assign. +const globalBuildTreeCache = new Map>() + +export function resetGlobalBuildTreeCache(): void { + globalBuildTreeCache.clear() +} + +/** + * Clone a NodeTree — shallow-clone props at every level so mutations + * to one clone's props don't affect the cached original or other clones. + */ +function cloneTree(tree: NodeTree): NodeTree { + return { + component: tree.component, + props: { ...tree.props }, + children: tree.children.map(cloneTree), + nodeType: tree.nodeType, + nodeName: tree.nodeName, + isComponent: tree.isComponent, + textChildren: tree.textChildren ? [...tree.textChildren] : undefined, + } +} + export class Codegen { components: Map< - SceneNode, - { code: string; variants: Record } + string, + { node: SceneNode; code: string; variants: Record } > = new Map() code: string = '' // Tree representations private tree: NodeTree | null = null - private componentTrees: Map = new Map() + private componentTrees: Map = new Map() + // Cache buildTree results by node.id to avoid duplicate subtree builds + // (e.g., when addComponentTree and main tree walk process the same children) + private buildTreeCache: Map> = new Map() + // Collect fire-and-forget addComponentTree promises so we can await them + // before rendering component codes (decouples INSTANCE buildTree from addComponentTree) + private pendingComponentTrees: Promise[] = [] constructor(private node: SceneNode) { this.node = node @@ -44,8 +105,8 @@ export class Codegen { } getComponentsCodes() { - return Array.from(this.components.entries()).map( - ([node, { code, variants }]) => + return Array.from(this.components.values()).map( + ({ node, code, variants }) => [ getComponentName(node), renderComponent(getComponentName(node), code, variants), @@ -54,11 +115,11 @@ export class Codegen { } /** - * Get the component nodes (SceneNode keys from components Map). + * Get the component nodes (SceneNode values from components Map). * Useful for generating responsive codes for each component. */ getComponentNodes() { - return Array.from(this.components.keys()) + return Array.from(this.components.values()).map(({ node }) => node) } /** @@ -76,10 +137,17 @@ export class Codegen { this.tree = tree } + // Await all fire-and-forget addComponentTree calls before rendering + if (this.pendingComponentTrees.length > 0) { + await Promise.all(this.pendingComponentTrees) + this.pendingComponentTrees = [] + } + // Sync componentTrees to components - for (const [compNode, compTree] of this.componentTrees) { - if (!this.components.has(compNode)) { - this.components.set(compNode, { + for (const [compId, compTree] of this.componentTrees) { + if (!this.components.has(compId)) { + this.components.set(compId, { + node: compTree.node, code: Codegen.renderTree(compTree.tree, 0), variants: compTree.variants, }) @@ -92,8 +160,45 @@ export class Codegen { /** * Build a NodeTree representation of the node hierarchy. * This is the intermediate JSON representation that can be compared/merged. + * + * Uses a two-level cache: + * 1. Global cache (across instances) — returns cloned trees to prevent mutation leaks + * 2. Per-instance cache — returns the same promise within a single Codegen.run() */ async buildTree(node: SceneNode = this.node): Promise { + const cacheKey = node.id + if (cacheKey) { + // Per-instance cache (same tree object reused within one Codegen) + const instanceCached = this.buildTreeCache.get(cacheKey) + if (instanceCached) return instanceCached + + // Global cache (shared across Codegen instances from ResponsiveCodegen). + // Returns a CLONE because downstream code mutates tree.props. + const globalCached = globalBuildTreeCache.get(cacheKey) + if (globalCached) { + const cloned = globalCached.then(cloneTree) + this.buildTreeCache.set(cacheKey, cloned) + return cloned + } + } + const promise = this.doBuildTree(node) + if (cacheKey) { + this.buildTreeCache.set(cacheKey, promise) + globalBuildTreeCache.set(cacheKey, promise) + } + const result = await promise + // When called as the root-level buildTree (node === this.node), + // drain any fire-and-forget addComponentTree promises so that + // getComponentTrees() is populated before the caller inspects it. + if (node === this.node && this.pendingComponentTrees.length > 0) { + await Promise.all(this.pendingComponentTrees) + this.pendingComponentTrees = [] + } + return result + } + + private async doBuildTree(node: SceneNode): Promise { + const tBuild = perfStart() // Handle asset nodes (images/SVGs) const assetNode = checkAssetNode(node) if (assetNode) { @@ -109,6 +214,7 @@ export class Codegen { delete props.src } } + perfEnd('buildTree()', tBuild) return { component: 'src' in props ? 'Image' : 'Box', props, @@ -118,41 +224,33 @@ export class Codegen { } } - const props = await getProps(node) - - // Handle COMPONENT_SET or COMPONENT - add to componentTrees - if ( - (node.type === 'COMPONENT_SET' || node.type === 'COMPONENT') && - ((this.node.type === 'COMPONENT_SET' && - node === this.node.defaultVariant) || - this.node.type === 'COMPONENT') - ) { - await this.addComponentTree( - node.type === 'COMPONENT_SET' ? node.defaultVariant : node, - ) - } - - // Handle INSTANCE nodes - treat as component reference + // Handle INSTANCE nodes first — they only need position props (all sync), + // skipping the expensive full getProps() with 6 async Figma API calls. if (node.type === 'INSTANCE') { - const mainComponent = await node.getMainComponentAsync() - if (mainComponent) await this.addComponentTree(mainComponent) + const mainComponent = await getMainComponentCached(node) + // Fire addComponentTree without awaiting — it runs in the background. + // All pending promises are collected and awaited in run() before rendering. + if (mainComponent) { + this.pendingComponentTrees.push(this.addComponentTree(mainComponent)) + } const componentName = getComponentName(mainComponent || node) - - // Extract variant props from instance's componentProperties const variantProps = extractInstanceVariantProps(node) - // Check if needs position wrapper - if (props.pos) { + // Only compute position + transform (sync, no Figma API calls) + const posProps = getPositionProps(node) + if (posProps?.pos) { + const transformProps = getTransformProps(node) + perfEnd('buildTree()', tBuild) return { component: 'Box', props: { - pos: props.pos, - top: props.top, - left: props.left, - right: props.right, - bottom: props.bottom, - transform: props.transform, + pos: posProps.pos, + top: posProps.top, + left: posProps.left, + right: posProps.right, + bottom: posProps.bottom, + transform: posProps.transform || transformProps?.transform, w: (getPageNode(node as BaseNode & ChildrenMixin) as SceneNode) ?.width === node.width @@ -174,6 +272,7 @@ export class Codegen { } } + perfEnd('buildTree()', tBuild) return { component: componentName, props: variantProps, @@ -184,18 +283,36 @@ export class Codegen { } } - // Build children recursively + // Fire getProps early for non-INSTANCE nodes — it runs while we process children. + const propsPromise = getProps(node) + + // Handle COMPONENT_SET or COMPONENT - add to componentTrees (fire-and-forget) + if ( + (node.type === 'COMPONENT_SET' || node.type === 'COMPONENT') && + ((this.node.type === 'COMPONENT_SET' && + node === this.node.defaultVariant) || + this.node.type === 'COMPONENT') + ) { + this.pendingComponentTrees.push( + this.addComponentTree( + node.type === 'COMPONENT_SET' ? node.defaultVariant : node, + ), + ) + } + + // Build children sequentially — Figma's single-threaded IPC means + // concurrent subtree builds add overhead without improving throughput, + // and sequential order maximizes cache hits for shared nodes. const children: NodeTree[] = [] if ('children' in node) { for (const child of node.children) { - if (child.type === 'INSTANCE') { - const mainComponent = await child.getMainComponentAsync() - if (mainComponent) await this.addComponentTree(mainComponent) - } children.push(await this.buildTree(child)) } } + // Now await props (likely already resolved while children were processing) + const props = await propsPromise + // Handle TEXT nodes let textChildren: string[] | undefined if (node.type === 'TEXT') { @@ -206,6 +323,7 @@ export class Codegen { const component = getDevupComponentByNode(node, props) + perfEnd('buildTree()', tBuild) return { component, props, @@ -223,6 +341,11 @@ export class Codegen { async getTree(): Promise { if (!this.tree) { this.tree = await this.buildTree(this.node) + // Await any fire-and-forget addComponentTree calls launched during buildTree + if (this.pendingComponentTrees.length > 0) { + await Promise.all(this.pendingComponentTrees) + this.pendingComponentTrees = [] + } } return this.tree } @@ -230,29 +353,56 @@ export class Codegen { /** * Get component trees (for COMPONENT_SET/COMPONENT nodes). */ - getComponentTrees(): Map { + getComponentTrees(): Map { return this.componentTrees } /** * Add a component to componentTrees. */ + // Cache in-flight addComponentTree promises to prevent duplicate work + // when multiple INSTANCE nodes reference the same component + private addComponentTreePromises: Map> = new Map() + private async addComponentTree(node: ComponentNode): Promise { - if (this.componentTrees.has(node)) return + const nodeId = node.id || node.name + if (this.componentTrees.has(nodeId)) return + + // If already in-flight, await the same promise + const inflight = this.addComponentTreePromises.get(nodeId) + if (inflight) return inflight + const promise = this.doAddComponentTree(node, nodeId) + this.addComponentTreePromises.set(nodeId, promise) + return promise + } + + private async doAddComponentTree( + node: ComponentNode, + nodeId: string, + ): Promise { + const tAdd = perfStart() + + // Fire getProps + getSelectorProps early (2 independent API calls) + const propsPromise = getProps(node) + const t = perfStart() + const selectorPropsPromise = getSelectorProps(node) + + // Build children sequentially (same reasoning as doBuildTree). const childrenTrees: NodeTree[] = [] if ('children' in node) { for (const child of node.children) { - if (child.type === 'INSTANCE') { - const mainComponent = await child.getMainComponentAsync() - if (mainComponent) await this.addComponentTree(mainComponent) - } childrenTrees.push(await this.buildTree(child)) } } - const props = await getProps(node) - const selectorProps = await getSelectorProps(node) + // Await props + selectorProps (likely already resolved while children built) + const [props, selectorProps] = await Promise.all([ + propsPromise, + selectorPropsPromise, + ]) + perfEnd('getSelectorProps()', t) + const variants: Record = {} if (selectorProps) { @@ -260,8 +410,9 @@ export class Codegen { Object.assign(variants, selectorProps.variants) } - this.componentTrees.set(node, { + this.componentTrees.set(nodeId, { name: getComponentName(node), + node, tree: { component: getDevupComponentByProps(props), props, @@ -271,6 +422,7 @@ export class Codegen { }, variants, }) + perfEnd('addComponentTree()', tAdd) } /** diff --git a/src/codegen/__tests__/__snapshots__/codegen.test.ts.snap b/src/codegen/__tests__/__snapshots__/codegen.test.ts.snap index fb9abf3..ac4179b 100644 --- a/src/codegen/__tests__/__snapshots__/codegen.test.ts.snap +++ b/src/codegen/__tests__/__snapshots__/codegen.test.ts.snap @@ -3473,3 +3473,536 @@ exports[`render real world component real world $ 109`] = ` ) }" `; + +exports[`Codegen renders component set with variants: component: Default 1`] = ` +{ + "name": "Button", + "node": { + "children": [], + "name": "Default", + "parent": { + "children": [ + [Circular], + { + "children": [], + "name": "Hover", + "parent": [Circular], + "reactions": [ + { + "actions": [], + "trigger": { + "type": "ON_HOVER", + }, + }, + ], + "type": "COMPONENT", + "variantProperties": { + "state": "hover", + }, + "visible": true, + }, + ], + "componentPropertyDefinitions": { + "state": { + "type": "VARIANT", + "variantOptions": [ + "default", + "hover", + ], + }, + }, + "defaultVariant": [Circular], + "name": "Button", + "type": "COMPONENT_SET", + "visible": true, + }, + "reactions": [], + "type": "COMPONENT", + "variantProperties": { + "state": "default", + }, + "visible": true, + }, + "tree": { + "children": [], + "component": "Box", + "nodeName": "Default", + "nodeType": "COMPONENT", + "props": { + "aspectRatio": undefined, + "flex": undefined, + "h": undefined, + "maxH": undefined, + "maxW": undefined, + "minH": undefined, + "minW": undefined, + "w": undefined, + }, + }, + "variants": { + "state": "'default' | 'hover'", + }, +} +`; + +exports[`Codegen renders component set with effect property: component: Default 1`] = ` +{ + "name": "Button", + "node": { + "children": [], + "name": "Default", + "parent": { + "children": [ + [Circular], + { + "children": [], + "name": "Hover", + "parent": [Circular], + "reactions": [], + "type": "COMPONENT", + "variantProperties": { + "effect": "hover", + }, + "visible": true, + }, + ], + "componentPropertyDefinitions": { + "effect": { + "type": "VARIANT", + "variantOptions": [ + "default", + "hover", + ], + }, + }, + "defaultVariant": [Circular], + "name": "Button", + "type": "COMPONENT_SET", + "visible": true, + }, + "reactions": [], + "type": "COMPONENT", + "variantProperties": { + "effect": "default", + }, + "visible": true, + }, + "tree": { + "children": [], + "component": "Box", + "nodeName": "Default", + "nodeType": "COMPONENT", + "props": { + "aspectRatio": undefined, + "flex": undefined, + "h": undefined, + "maxH": undefined, + "maxW": undefined, + "minH": undefined, + "minW": undefined, + "w": undefined, + }, + }, + "variants": {}, +} +`; + +exports[`Codegen renders component set with transition: component: Default 1`] = ` +{ + "name": "Button", + "node": { + "children": [], + "name": "Default", + "parent": { + "children": [ + [Circular], + { + "children": [], + "name": "Hover", + "parent": [Circular], + "reactions": [ + { + "actions": [ + { + "transition": { + "duration": 0.3, + "easing": { + "type": "EASE_IN_OUT", + }, + "type": "SMART_ANIMATE", + }, + "type": "NODE", + }, + ], + "trigger": { + "type": "ON_HOVER", + }, + }, + ], + "type": "COMPONENT", + "variantProperties": {}, + "visible": true, + }, + ], + "componentPropertyDefinitions": {}, + "defaultVariant": [Circular], + "name": "Button", + "type": "COMPONENT_SET", + "visible": true, + }, + "reactions": [ + { + "actions": [ + { + "transition": { + "duration": 0.3, + "easing": { + "type": "EASE_IN_OUT", + }, + "type": "SMART_ANIMATE", + }, + "type": "NODE", + }, + ], + "trigger": { + "type": "ON_HOVER", + }, + }, + ], + "type": "COMPONENT", + "variantProperties": {}, + "visible": true, + }, + "tree": { + "children": [], + "component": "Box", + "nodeName": "Default", + "nodeType": "COMPONENT", + "props": { + "aspectRatio": undefined, + "flex": undefined, + "h": undefined, + "maxH": undefined, + "maxW": undefined, + "minH": undefined, + "minW": undefined, + "w": undefined, + }, + }, + "variants": {}, +} +`; + +exports[`Codegen renders component set with different props and transition: component: Default 1`] = ` +{ + "name": "Card", + "node": { + "children": [], + "name": "Default", + "parent": { + "children": [ + [Circular], + { + "children": [], + "name": "Hover", + "opacity": 0.8, + "parent": [Circular], + "reactions": [ + { + "actions": [ + { + "transition": { + "duration": 0.3, + "easing": { + "type": "EASE_IN_OUT", + }, + "type": "SMART_ANIMATE", + }, + "type": "NODE", + }, + ], + "trigger": { + "type": "ON_HOVER", + }, + }, + ], + "type": "COMPONENT", + "variantProperties": {}, + "visible": true, + }, + ], + "componentPropertyDefinitions": {}, + "defaultVariant": [Circular], + "name": "Card", + "type": "COMPONENT_SET", + "visible": true, + }, + "reactions": [ + { + "actions": [ + { + "transition": { + "duration": 0.3, + "easing": { + "type": "EASE_IN_OUT", + }, + "type": "SMART_ANIMATE", + }, + "type": "NODE", + }, + ], + "trigger": { + "type": "ON_HOVER", + }, + }, + ], + "type": "COMPONENT", + "variantProperties": {}, + "visible": true, + }, + "tree": { + "children": [], + "component": "Box", + "nodeName": "Default", + "nodeType": "COMPONENT", + "props": { + "_hover": { + "opacity": "0.8", + }, + "aspectRatio": undefined, + "flex": undefined, + "h": undefined, + "maxH": undefined, + "maxW": undefined, + "minH": undefined, + "minW": undefined, + "transition": "0.3ms ease-in-out", + "transitionProperty": "opacity", + "w": undefined, + }, + }, + "variants": {}, +} +`; + +exports[`Codegen renders component with parent component set: component: Hover 1`] = ` +{ + "name": "Button", + "node": { + "children": [], + "name": "Hover", + "parent": { + "children": [ + { + "children": [], + "name": "Default", + "reactions": [], + "type": "COMPONENT", + "variantProperties": { + "state": "default", + }, + "visible": true, + }, + { + "children": [], + "name": "Hover", + "reactions": [ + { + "actions": [], + "trigger": { + "type": "ON_HOVER", + }, + }, + ], + "type": "COMPONENT", + "variantProperties": { + "state": "hover", + }, + "visible": true, + }, + ], + "componentPropertyDefinitions": { + "state": { + "type": "VARIANT", + "variantOptions": [ + "default", + "hover", + ], + }, + }, + "defaultVariant": { + "children": [], + "name": "Default", + "reactions": [], + "type": "COMPONENT", + "variantProperties": { + "state": "default", + }, + "visible": true, + }, + "name": "Button", + "type": "COMPONENT_SET", + "visible": true, + }, + "reactions": [ + { + "actions": [], + "trigger": { + "type": "ON_HOVER", + }, + }, + ], + "type": "COMPONENT", + "variantProperties": { + "state": "hover", + }, + "visible": true, + }, + "tree": { + "children": [], + "component": "Box", + "nodeName": "Hover", + "nodeType": "COMPONENT", + "props": { + "aspectRatio": undefined, + "flex": undefined, + "h": undefined, + "maxH": undefined, + "maxW": undefined, + "minH": undefined, + "minW": undefined, + "w": undefined, + }, + }, + "variants": { + "state": "'default' | 'hover'", + }, +} +`; + +exports[`Codegen renders component set with press trigger: component: Default 1`] = ` +{ + "name": "Button", + "node": { + "children": [], + "name": "Default", + "parent": { + "children": [ + [Circular], + { + "children": [], + "name": "Active", + "parent": [Circular], + "reactions": [ + { + "actions": [], + "trigger": { + "type": "ON_PRESS", + }, + }, + ], + "type": "COMPONENT", + "variantProperties": {}, + "visible": true, + }, + ], + "componentPropertyDefinitions": {}, + "defaultVariant": [Circular], + "name": "Button", + "type": "COMPONENT_SET", + "visible": true, + }, + "reactions": [], + "type": "COMPONENT", + "variantProperties": {}, + "visible": true, + }, + "tree": { + "children": [], + "component": "Box", + "nodeName": "Default", + "nodeType": "COMPONENT", + "props": { + "aspectRatio": undefined, + "flex": undefined, + "h": undefined, + "maxH": undefined, + "maxW": undefined, + "minH": undefined, + "minW": undefined, + "w": undefined, + }, + }, + "variants": {}, +} +`; + +exports[`Codegen renders simple component without variants: component: Icon 1`] = ` +{ + "name": "Icon", + "node": { + "children": [], + "name": "Icon", + "type": "COMPONENT", + "visible": true, + }, + "tree": { + "children": [], + "component": "Box", + "nodeName": "Icon", + "nodeType": "COMPONENT", + "props": { + "aspectRatio": undefined, + "boxSize": "100%", + "flex": undefined, + "maxH": undefined, + "maxW": undefined, + "minH": undefined, + "minW": undefined, + }, + }, + "variants": {}, +} +`; + +exports[`Codegen renders component with parent component set name: component: Hover 1`] = ` +{ + "name": "Button", + "node": { + "children": [], + "name": "Hover", + "parent": { + "children": [], + "componentPropertyDefinitions": {}, + "defaultVariant": { + "children": [], + "name": "Default", + "type": "COMPONENT", + "visible": true, + }, + "name": "Button", + "type": "COMPONENT_SET", + "visible": true, + }, + "type": "COMPONENT", + "visible": true, + }, + "tree": { + "children": [], + "component": "Box", + "nodeName": "Hover", + "nodeType": "COMPONENT", + "props": { + "aspectRatio": undefined, + "flex": undefined, + "h": undefined, + "maxH": undefined, + "maxW": undefined, + "minH": undefined, + "minW": undefined, + "w": undefined, + }, + }, + "variants": {}, +} +`; diff --git a/src/codegen/props/__tests__/position.test.ts b/src/codegen/props/__tests__/position.test.ts index 75aa299..72787c1 100644 --- a/src/codegen/props/__tests__/position.test.ts +++ b/src/codegen/props/__tests__/position.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'bun:test' import { canBeAbsolute, getPositionProps } from '../position' vi.mock('../../utils/check-asset-node', () => ({ diff --git a/src/codegen/props/__tests__/reaction.test.ts b/src/codegen/props/__tests__/reaction.test.ts index 55fa12f..2ca56a2 100644 --- a/src/codegen/props/__tests__/reaction.test.ts +++ b/src/codegen/props/__tests__/reaction.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'bun:test' import { getReactionProps } from '../reaction' // Mock figma global diff --git a/src/codegen/props/index.ts b/src/codegen/props/index.ts index 5c056b3..334aaf6 100644 --- a/src/codegen/props/index.ts +++ b/src/codegen/props/index.ts @@ -1,3 +1,4 @@ +import { perfEnd, perfStart } from '../utils/perf' import { getAutoLayoutProps } from './auto-layout' import { getBackgroundProps } from './background' import { getBlendProps } from './blend' @@ -19,33 +20,146 @@ import { getTextStrokeProps } from './text-stroke' import { getTransformProps } from './transform' import { getVisibilityProps } from './visibility' +// Cache getProps() results keyed by node.id to avoid redundant computation. +// Figma returns new JS wrapper objects for the same node on each property access, +// so object-reference keys don't work — node.id is the stable identifier. +// For a COMPONENT_SET with N variants, getProps() is called O(N²) without caching +// because getSelectorProps/getSelectorPropsForGroup call it on overlapping node sets. +const getPropsCache = new Map>>() + +export function resetGetPropsCache(): void { + getPropsCache.clear() +} + export async function getProps( node: SceneNode, ): Promise> { - return { - ...getAutoLayoutProps(node), - ...getMinMaxProps(node), - ...getLayoutProps(node), - ...getBorderRadiusProps(node), - ...(await getBorderProps(node)), - ...(await getBackgroundProps(node)), - ...getBlendProps(node), - ...getPaddingProps(node), - ...getTextAlignProps(node), - ...getObjectFitProps(node), - ...getMaxLineProps(node), - ...getEllipsisProps(node), - ...(await getEffectProps(node)), - ...getPositionProps(node), - ...getGridChildProps(node), - ...getTransformProps(node), - ...getOverflowProps(node), - ...(await getTextStrokeProps(node)), - ...(await getTextShadowProps(node)), - ...(await getReactionProps(node)), - ...getCursorProps(node), - ...getVisibilityProps(node), + const cacheKey = node.id + if (cacheKey) { + const cached = getPropsCache.get(cacheKey) + if (cached) { + perfEnd('getProps(cached)', perfStart()) + // Return a shallow clone to prevent mutation of cached values + return { ...(await cached) } + } + } + + const t = perfStart() + const promise = (async () => { + const isText = node.type === 'TEXT' + + // PHASE 1: Fire all async prop getters — initiates Figma IPC calls immediately. + // These return Promises that resolve when IPC completes. + const tBorder = perfStart() + const borderP = getBorderProps(node) + const tBg = perfStart() + const bgP = getBackgroundProps(node) + const tEffect = perfStart() + const effectP = getEffectProps(node) + const tTextStroke = perfStart() + const textStrokeP = isText ? getTextStrokeProps(node) : undefined + const tTextShadow = perfStart() + const textShadowP = isText ? getTextShadowProps(node) : undefined + const tReaction = perfStart() + const reactionP = getReactionProps(node) + + // PHASE 2: Run sync prop getters while async IPC is pending in background. + // This overlaps ~129ms of sync work with ~17ms of async IPC wait. + // Compute sync results eagerly; they'll be interleaved in the original merge + // order below to preserve "last-key-wins" semantics. + const tSync = perfStart() + const autoLayoutProps = getAutoLayoutProps(node) + const minMaxProps = getMinMaxProps(node) + const layoutProps = getLayoutProps(node) + const borderRadiusProps = getBorderRadiusProps(node) + const blendProps = getBlendProps(node) + const paddingProps = getPaddingProps(node) + const textAlignProps = isText ? getTextAlignProps(node) : undefined + const objectFitProps = getObjectFitProps(node) + const maxLineProps = isText ? getMaxLineProps(node) : undefined + const ellipsisProps = isText ? getEllipsisProps(node) : undefined + const positionProps = getPositionProps(node) + const gridChildProps = getGridChildProps(node) + const transformProps = getTransformProps(node) + const overflowProps = getOverflowProps(node) + const cursorProps = getCursorProps(node) + const visibilityProps = getVisibilityProps(node) + perfEnd('getProps.sync', tSync) + + // PHASE 3: Await async results — likely already resolved during sync phase. + const [ + borderProps, + backgroundProps, + effectProps, + textStrokeProps, + textShadowProps, + reactionProps, + ] = await Promise.all([ + borderP.then((r) => { + perfEnd('getProps.border', tBorder) + return r + }), + bgP.then((r) => { + perfEnd('getProps.background', tBg) + return r + }), + effectP.then((r) => { + perfEnd('getProps.effect', tEffect) + return r + }), + textStrokeP + ? textStrokeP.then((r) => { + perfEnd('getProps.textStroke', tTextStroke) + return r + }) + : undefined, + textShadowP + ? textShadowP.then((r) => { + perfEnd('getProps.textShadow', tTextShadow) + return r + }) + : undefined, + reactionP.then((r) => { + perfEnd('getProps.reaction', tReaction) + return r + }), + ]) + + // PHASE 4: Merge in the ORIGINAL interleaved order to preserve last-key-wins. + // async results (border, background, effect, textStroke, textShadow, reaction) + // are placed at their original positions relative to sync getters. + return { + ...autoLayoutProps, + ...minMaxProps, + ...layoutProps, + ...borderRadiusProps, + ...borderProps, + ...backgroundProps, + ...blendProps, + ...paddingProps, + ...textAlignProps, + ...objectFitProps, + ...maxLineProps, + ...ellipsisProps, + ...effectProps, + ...positionProps, + ...gridChildProps, + ...transformProps, + ...overflowProps, + ...textStrokeProps, + ...textShadowProps, + ...reactionProps, + ...cursorProps, + ...visibilityProps, + } + })() + + if (cacheKey) { + getPropsCache.set(cacheKey, promise) } + const result = await promise + perfEnd('getProps()', t) + return result } export function filterPropsWithComponent( diff --git a/src/codegen/props/selector.ts b/src/codegen/props/selector.ts index f834cdb..a2bc00a 100644 --- a/src/codegen/props/selector.ts +++ b/src/codegen/props/selector.ts @@ -1,6 +1,28 @@ import { fmtPct } from '../utils/fmtPct' +import { perfEnd, perfStart } from '../utils/perf' import { getProps } from '.' +// Cache getSelectorProps() keyed by ComponentSetNode.id. +// Called from addComponentTree() for each component — but the result depends only on the parent set. +const selectorPropsCache = new Map< + string, + Promise<{ + props: Record + variants: Record + }> +>() + +// Cache getSelectorPropsForGroup() keyed by "setId::filter::viewport". +const selectorPropsForGroupCache = new Map< + string, + Promise> +>() + +export function resetSelectorPropsCache(): void { + selectorPropsCache.clear() + selectorPropsForGroupCache.clear() +} + // Shorthand prop names to CSS standard property names const shortToCssProperty: Record = { bg: 'background', @@ -77,7 +99,29 @@ export async function getSelectorProps( return getSelectorProps(node.parent) } if (node.type !== 'COMPONENT_SET') return + + const cacheKey = node.id + if (cacheKey) { + const cached = selectorPropsCache.get(cacheKey) + if (cached) return cached + } + + const promise = computeSelectorProps(node) + if (cacheKey) { + selectorPropsCache.set(cacheKey, promise) + } + return promise +} + +async function computeSelectorProps(node: ComponentSetNode): Promise<{ + props: Record + variants: Record +}> { const hasEffect = !!node.componentPropertyDefinitions.effect + const tSelector = perfStart() + console.info( + `[perf] getSelectorProps: processing ${node.children.length} children`, + ) const components = await Promise.all( node.children .filter((child) => { @@ -93,6 +137,7 @@ export async function getSelectorProps( ] as const, ), ) + perfEnd('getSelectorProps.getPropsAll()', tSelector) const defaultProps = await getProps(node.defaultVariant) @@ -159,6 +204,36 @@ export async function getSelectorPropsForGroup( const hasEffect = !!componentSet.componentPropertyDefinitions.effect if (!hasEffect) return {} + // Build cache key from componentSet.id + filter + viewport + const setId = componentSet.id + const filterKey = Object.entries(variantFilter) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([k, v]) => `${k}=${v}`) + .join('|') + const cacheKey = setId ? `${setId}::${filterKey}::${viewportValue ?? ''}` : '' + + if (cacheKey) { + const cached = selectorPropsForGroupCache.get(cacheKey) + if (cached) return cached + } + + const promise = computeSelectorPropsForGroup( + componentSet, + variantFilter, + viewportValue, + ) + + if (cacheKey) { + selectorPropsForGroupCache.set(cacheKey, promise) + } + return promise +} + +async function computeSelectorPropsForGroup( + componentSet: ComponentSetNode, + variantFilter: Record, + viewportValue?: string, +): Promise> { // Find viewport key if needed const viewportKey = Object.keys( componentSet.componentPropertyDefinitions, @@ -190,16 +265,27 @@ export async function getSelectorPropsForGroup( ) if (!defaultComponent) return {} + const tGroup = perfStart() + console.info( + `[perf] getSelectorPropsForGroup: processing ${matchingComponents.length} matching components`, + ) const defaultProps = await getProps(defaultComponent) const result: Record = {} const diffKeys = new Set() - // Calculate diffs for each effect state - for (const component of matchingComponents) { - const effect = component.variantProperties?.effect - if (!effect || effect === 'default') continue - - const props = await getProps(component) + // Calculate diffs for each effect state — fire all getProps() concurrently + const effectComponents = matchingComponents.filter((c) => { + const effect = c.variantProperties?.effect + return effect && effect !== 'default' + }) + const effectPropsResults = await Promise.all( + effectComponents.map(async (component) => { + const effect = component.variantProperties?.effect as string + const props = await getProps(component) + return { effect, props } + }), + ) + for (const { effect, props } of effectPropsResults) { const def = difference(props, defaultProps) if (Object.keys(def).length === 0) continue @@ -225,6 +311,7 @@ export async function getSelectorPropsForGroup( } } + perfEnd('getSelectorPropsForGroup.inner()', tGroup) return result } @@ -241,7 +328,7 @@ function triggerTypeToEffect(triggerType: Trigger['type'] | undefined) { function difference(a: Record, b: Record) { return Object.entries(a).reduce( (acc, [key, value]) => { - if (b[key] !== value) { + if (value !== undefined && b[key] !== value) { acc[key] = value } return acc diff --git a/src/codegen/render/text.ts b/src/codegen/render/text.ts index 1af4cf7..40b3907 100644 --- a/src/codegen/render/text.ts +++ b/src/codegen/render/text.ts @@ -2,6 +2,7 @@ import { propsToPropsWithTypography } from '../../utils' import { textSegmentToTypography } from '../../utils/text-segment-to-typography' import { fixTextChild } from '../utils/fix-text-child' import { paintToCSS } from '../utils/paint-to-css' +import { perfEnd, perfStart } from '../utils/perf' import { renderNode } from '.' /** @@ -34,6 +35,7 @@ export async function renderText(node: TextNode): Promise<{ children: string[] props: Record }> { + const tRender = perfStart() const segs = node.getStyledTextSegments(SEGMENT_TYPE) // select main color @@ -139,7 +141,8 @@ export async function renderText(node: TextNode): Promise<{ ) const resultChildren = children.flat() - if (resultChildren.length === 1) + if (resultChildren.length === 1) { + perfEnd('renderText()', tRender) return { children: resultChildren[0].children, props: { @@ -147,7 +150,9 @@ export async function renderText(node: TextNode): Promise<{ ...resultChildren[0].props, }, } + } + perfEnd('renderText()', tRender) return { children: resultChildren.map((child) => { if (Object.keys(child.props).length === 0) diff --git a/src/codegen/responsive/ResponsiveCodegen.ts b/src/codegen/responsive/ResponsiveCodegen.ts index 621a2e5..39aaee7 100644 --- a/src/codegen/responsive/ResponsiveCodegen.ts +++ b/src/codegen/responsive/ResponsiveCodegen.ts @@ -6,6 +6,7 @@ import { import { renderComponent, renderNode } from '../render' import type { NodeTree, Props } from '../types' import { paddingLeftMultiline } from '../utils/padding-left-multiline' +import { perfEnd, perfStart } from '../utils/perf' import { BREAKPOINT_ORDER, type BreakpointKey, @@ -63,13 +64,16 @@ export class ResponsiveCodegen { return Codegen.renderTree(tree, 0) } - // Extract trees per breakpoint using Codegen. - const breakpointTrees = new Map() - for (const [bp, node] of this.breakpointNodes) { - const codegen = new Codegen(node) - const tree = await codegen.getTree() - breakpointTrees.set(bp, tree) - } + // Extract trees per breakpoint using Codegen — all independent, run in parallel. + const breakpointEntries = [...this.breakpointNodes.entries()] + const treeResults = await Promise.all( + breakpointEntries.map(async ([bp, node]) => { + const codegen = new Codegen(node) + const tree = await codegen.getTree() + return [bp, tree] as const + }), + ) + const breakpointTrees = new Map(treeResults) // Merge trees and generate code. return this.generateMergedCode(breakpointTrees, 0) @@ -382,28 +386,37 @@ export class ResponsiveCodegen { } } - // Build trees for each viewport - const treesByBreakpoint = new Map() - for (const [bp, component] of viewportComponents) { - const codegen = new Codegen(component) - const tree = await codegen.getTree() - - // Get pseudo-selector props for this specific variant group AND viewport - // This ensures hover/active colors are correctly responsive per viewport - if (effectKey) { - const viewportValue = component.variantProperties?.[viewportKey] - const selectorProps = await getSelectorPropsForGroup( - componentSet, - variantFilter, - viewportValue, - ) - if (Object.keys(selectorProps).length > 0) { - Object.assign(tree.props, selectorProps) + // Build trees for each viewport — all independent, run in parallel. + const viewportEntries = [...viewportComponents.entries()] + const viewportTreeResults = await Promise.all( + viewportEntries.map(async ([bp, component]) => { + let t = perfStart() + const codegen = new Codegen(component) + const tree = await codegen.getTree() + perfEnd('Codegen.getTree(viewportVariant)', t) + + // Get pseudo-selector props for this specific variant group AND viewport + // This ensures hover/active colors are correctly responsive per viewport + if (effectKey) { + const viewportValue = component.variantProperties?.[viewportKey] + t = perfStart() + const selectorProps = await getSelectorPropsForGroup( + componentSet, + variantFilter, + viewportValue, + ) + perfEnd('getSelectorPropsForGroup(viewport)', t) + if (Object.keys(selectorProps).length > 0) { + Object.assign(tree.props, selectorProps) + } } - } - treesByBreakpoint.set(bp, tree) - } + return [bp, tree] as const + }), + ) + const treesByBreakpoint = new Map( + viewportTreeResults, + ) // Generate merged responsive code const mergedCode = responsiveCodegen.generateMergedCode( @@ -432,6 +445,11 @@ export class ResponsiveCodegen { componentSet: ComponentSetNode, componentName: string, ): Promise> { + console.info( + `[perf] generateVariantResponsiveComponents: ${componentName}, ${componentSet.children.length} children, ${Object.keys(componentSet.componentPropertyDefinitions).length} variant keys`, + ) + const tTotal = perfStart() + // Find viewport variant key const viewportKey = Object.keys( componentSet.componentPropertyDefinitions, @@ -468,28 +486,34 @@ export class ResponsiveCodegen { // If effect variant only, generate code from defaultVariant with pseudo-selectors if (effectKey && !viewportKey && otherVariantKeys.length === 0) { - return ResponsiveCodegen.generateEffectOnlyComponents( + const r = await ResponsiveCodegen.generateEffectOnlyComponents( componentSet, componentName, ) + perfEnd('generateVariantResponsiveComponents(total)', tTotal) + return r } // If no viewport variant, just handle other variants if (!viewportKey) { - return ResponsiveCodegen.generateNonViewportVariantComponents( + const r = await ResponsiveCodegen.generateNonViewportVariantComponents( componentSet, componentName, otherVariantKeys, variants, ) + perfEnd('generateVariantResponsiveComponents(total)', tTotal) + return r } // If no other variants, use existing viewport-only logic if (otherVariantKeys.length === 0) { - return ResponsiveCodegen.generateViewportResponsiveComponents( + const r = await ResponsiveCodegen.generateViewportResponsiveComponents( componentSet, componentName, ) + perfEnd('generateVariantResponsiveComponents(total)', tTotal) + return r } // Handle both viewport and other variants @@ -564,6 +588,7 @@ export class ResponsiveCodegen { } if (byCompositeVariant.size === 0) { + perfEnd('generateVariantResponsiveComponents(total)', tTotal) return [] } @@ -575,31 +600,48 @@ export class ResponsiveCodegen { Map >() - for (const [compositeKey, viewportComponents] of byCompositeVariant) { - // Use original names for Figma data access - const variantFilter = parseCompositeKeyToOriginal(compositeKey) - - const treesByBreakpoint = new Map() - for (const [bp, component] of viewportComponents) { - const codegen = new Codegen(component) - const tree = await codegen.getTree() + // Build trees for all composite variants in parallel — each is independent. + const compositeEntries = [...byCompositeVariant.entries()] + const compositeResults = await Promise.all( + compositeEntries.map(async ([compositeKey, viewportComponents]) => { + // Use original names for Figma data access + const variantFilter = parseCompositeKeyToOriginal(compositeKey) + + // Build trees for each viewport within this composite — also parallel. + const vpEntries = [...viewportComponents.entries()] + const vpResults = await Promise.all( + vpEntries.map(async ([bp, component]) => { + let t = perfStart() + const codegen = new Codegen(component) + const tree = await codegen.getTree() + perfEnd('Codegen.getTree(variant)', t) + + // Get pseudo-selector props for this specific variant group AND viewport + if (effectKey) { + const viewportValue = component.variantProperties?.[viewportKey] + t = perfStart() + const selectorProps = await getSelectorPropsForGroup( + componentSet, + variantFilter, + viewportValue, + ) + perfEnd('getSelectorPropsForGroup()', t) + if (Object.keys(selectorProps).length > 0) { + Object.assign(tree.props, selectorProps) + } + } - // Get pseudo-selector props for this specific variant group AND viewport - // This ensures hover/active colors are correctly responsive per viewport - if (effectKey) { - const viewportValue = component.variantProperties?.[viewportKey] - const selectorProps = await getSelectorPropsForGroup( - componentSet, - variantFilter, - viewportValue, - ) - if (Object.keys(selectorProps).length > 0) { - Object.assign(tree.props, selectorProps) - } - } + return [bp, tree] as const + }), + ) - treesByBreakpoint.set(bp, tree) - } + return [ + compositeKey, + new Map(vpResults), + ] as const + }), + ) + for (const [compositeKey, treesByBreakpoint] of compositeResults) { responsivePropsByComposite.set(compositeKey, treesByBreakpoint) } @@ -684,25 +726,32 @@ export class ResponsiveCodegen { componentSet.componentPropertyDefinitions, ).some((key) => key.toLowerCase() === 'effect') - // Build trees for each variant - const treesByVariant = new Map() - for (const [variantValue, component] of componentsByVariant) { - // Get pseudo-selector props for this specific variant group - const variantFilter: Record = { - [primaryVariantKey]: variantValue, - } - const selectorProps = hasEffect - ? await getSelectorPropsForGroup(componentSet, variantFilter) - : null + // Build trees for each variant — all independent, run in parallel. + const variantEntries = [...componentsByVariant.entries()] + const variantResults = await Promise.all( + variantEntries.map(async ([variantValue, component]) => { + // Get pseudo-selector props for this specific variant group + const variantFilter: Record = { + [primaryVariantKey]: variantValue, + } + let t = perfStart() + const selectorProps = hasEffect + ? await getSelectorPropsForGroup(componentSet, variantFilter) + : null + perfEnd('getSelectorPropsForGroup(nonViewport)', t) - const codegen = new Codegen(component) - const tree = await codegen.getTree() - // Add pseudo-selector props to tree - if (selectorProps && Object.keys(selectorProps).length > 0) { - Object.assign(tree.props, selectorProps) - } - treesByVariant.set(variantValue, tree) - } + t = perfStart() + const codegen = new Codegen(component) + const tree = await codegen.getTree() + perfEnd('Codegen.getTree(nonViewportVariant)', t) + // Add pseudo-selector props to tree + if (selectorProps && Object.keys(selectorProps).length > 0) { + Object.assign(tree.props, selectorProps) + } + return [variantValue, tree] as const + }), + ) + const treesByVariant = new Map(variantResults) // Generate merged code with variant conditionals const responsiveCodegen = new ResponsiveCodegen(null) diff --git a/src/codegen/types.ts b/src/codegen/types.ts index 4db4e3c..a99d27f 100644 --- a/src/codegen/types.ts +++ b/src/codegen/types.ts @@ -12,6 +12,7 @@ export interface NodeTree { export interface ComponentTree { name: string + node: SceneNode tree: NodeTree variants: Record } diff --git a/src/codegen/utils/__tests__/node-proxy.test.ts b/src/codegen/utils/__tests__/node-proxy.test.ts index ab030b6..1e08af1 100644 --- a/src/codegen/utils/__tests__/node-proxy.test.ts +++ b/src/codegen/utils/__tests__/node-proxy.test.ts @@ -749,6 +749,106 @@ describe('nodeProxyTracker', () => { expect(result.variables[0].id).toBe('VariableID:direct') }) + test('trackTree should track all nodes without Proxy overhead', () => { + const childNode = createMockNode({ + id: 'child-1', + name: 'ChildNode', + type: 'FRAME', + width: 50, + height: 50, + }) + + const parentNode = { + ...createMockNode({ id: 'parent-1', name: 'ParentNode' }), + children: [childNode], + } as unknown as SceneNode + + nodeProxyTracker.trackTree(parentNode) + + const logs = nodeProxyTracker.getAllAccessLogs() + expect(logs.length).toBe(2) + + const parentLog = nodeProxyTracker.getAccessLog('parent-1') + const childLog = nodeProxyTracker.getAccessLog('child-1') + + expect(parentLog).toBeDefined() + expect(childLog).toBeDefined() + + // Parent should have children serialized as node ID references + const childrenProp = parentLog?.properties.find((p) => p.key === 'children') + expect(childrenProp).toBeDefined() + expect(Array.isArray(childrenProp?.value)).toBe(true) + expect((childrenProp?.value as string[])[0]).toBe('[NodeId: child-1]') + + // Child should have properties tracked + expect(childLog?.properties.length).toBeGreaterThan(0) + expect(childLog?.properties.some((p) => p.key === 'width')).toBe(true) + }) + + test('trackTree should track TEXT nodes with styledTextSegments', () => { + const textNode = { + id: 'text-node-1', + name: 'TextNode', + type: 'TEXT', + characters: 'Hello World', + fontSize: 16, + fontName: { family: 'Inter', style: 'Regular' }, + fontWeight: 400, + visible: true, + fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 }, opacity: 1 }], + getStyledTextSegments: () => [ + { + start: 0, + end: 11, + characters: 'Hello World', + fontName: { family: 'Inter', style: 'Regular' }, + fontWeight: 400, + fontSize: 16, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { value: 24, unit: 'PIXELS' }, + letterSpacing: { value: 0, unit: 'PIXELS' }, + fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 }, opacity: 1 }], + textStyleId: '', + fillStyleId: '', + listOptions: { type: 'NONE' }, + indentation: 0, + hyperlink: null, + }, + ], + } as unknown as SceneNode + + const parentNode = { + ...createMockNode({ id: 'parent-1', name: 'ParentNode' }), + children: [textNode], + } as unknown as SceneNode + + nodeProxyTracker.trackTree(parentNode) + + const log = nodeProxyTracker.getAccessLog('text-node-1') + expect(log).toBeDefined() + expect(log?.nodeType).toBe('TEXT') + + // Check that styledTextSegments was tracked + const segmentsProp = log?.properties.find( + (p) => p.key === 'styledTextSegments', + ) + expect(segmentsProp).toBeDefined() + expect(Array.isArray(segmentsProp?.value)).toBe(true) + }) + + test('trackTree should skip already-tracked nodes', () => { + const node = createMockNode({ id: 'node-1', name: 'Node1' }) + + // Track twice + nodeProxyTracker.trackTree(node) + nodeProxyTracker.trackTree(node) + + // Should still only have one log entry + const logs = nodeProxyTracker.getAllAccessLogs() + expect(logs.length).toBe(1) + }) + test('re-exported assembleNodeTree works from node-proxy', () => { const nodes = [{ id: 'test-1', name: 'Test', type: 'FRAME' }] const result = assembleNodeTree(nodes) diff --git a/src/codegen/utils/__tests__/paint-to-css.test.ts b/src/codegen/utils/__tests__/paint-to-css.test.ts index 8e781b8..df9bbe7 100644 --- a/src/codegen/utils/__tests__/paint-to-css.test.ts +++ b/src/codegen/utils/__tests__/paint-to-css.test.ts @@ -1,5 +1,6 @@ -import { describe, expect, mock, test } from 'bun:test' +import { beforeEach, describe, expect, mock, test } from 'bun:test' import { paintToCSS } from '../paint-to-css' +import { resetVariableCache } from '../variable-cache' // mock asset checker to avoid real node handling mock.module('../check-asset-node', () => ({ @@ -7,6 +8,9 @@ mock.module('../check-asset-node', () => ({ })) describe('paintToCSS', () => { + beforeEach(() => { + resetVariableCache() + }) test('converts image paint with TILE scaleMode to repeat url', async () => { ;(globalThis as { figma?: unknown }).figma = { util: { rgba: (v: unknown) => v }, diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index e2c5977..04e5113 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -121,6 +121,194 @@ class NodeProxyTracker { return new Proxy(node, handler) } + /** + * Walk the entire node tree and record all properties without Proxy overhead. + * Call AFTER codegen completes to collect test case data. + */ + trackTree(node: SceneNode): void { + this.trackNodeDirect(node) + if ('children' in node && Array.isArray(node.children)) { + for (const child of node.children) { + if ( + child && + typeof child === 'object' && + 'id' in child && + 'type' in child + ) { + this.trackTree(child as unknown as SceneNode) + } + } + } + } + + /** + * Record all properties of a single node without Proxy. + * Direct property access — no interception overhead. + */ + private trackNodeDirect(node: SceneNode): void { + const nodeId = node.id + if (this.accessLogs.has(nodeId)) return + + const log: AccessLog = { + nodeId, + nodeName: node.name, + nodeType: node.type, + properties: [], + } + this.accessLogs.set(nodeId, log) + + const propsToTrack = [ + 'id', + 'name', + 'type', + 'visible', + 'parent', + 'children', + 'fills', + 'strokes', + 'effects', + 'opacity', + 'blendMode', + 'width', + 'height', + 'rotation', + 'cornerRadius', + 'topLeftRadius', + 'topRightRadius', + 'bottomLeftRadius', + 'bottomRightRadius', + 'layoutMode', + 'layoutAlign', + 'layoutGrow', + 'layoutSizingHorizontal', + 'layoutSizingVertical', + 'layoutPositioning', + 'primaryAxisAlignItems', + 'counterAxisAlignItems', + 'paddingLeft', + 'paddingRight', + 'paddingTop', + 'paddingBottom', + 'itemSpacing', + 'counterAxisSpacing', + 'clipsContent', + 'isAsset', + 'reactions', + 'minWidth', + 'maxWidth', + 'minHeight', + 'maxHeight', + 'targetAspectRatio', + 'inferredAutoLayout', + 'strokeWeight', + 'strokeTopWeight', + 'strokeBottomWeight', + 'strokeLeftWeight', + 'strokeRightWeight', + 'strokeAlign', + 'dashPattern', + 'characters', + 'fontName', + 'fontSize', + 'fontWeight', + 'lineHeight', + 'letterSpacing', + 'textAutoResize', + 'textAlignHorizontal', + 'textAlignVertical', + 'textTruncation', + 'maxLines', + 'gridColumnAnchorIndex', + 'gridRowAnchorIndex', + 'gridColumnCount', + ] + + const nodeObj = node as unknown as Record + for (const prop of propsToTrack) { + try { + const value = nodeObj[prop] + if (value === undefined) continue + + let serializedValue: unknown + if (value === null) { + serializedValue = value + } else if (typeof value === 'function') { + continue + } else if (typeof value === 'object') { + const valueObj = value as Record + if (Array.isArray(value)) { + if (prop === 'children') { + // Store children as node ID references + serializedValue = value.map((child) => { + if ( + child && + typeof child === 'object' && + 'id' in child && + 'type' in child + ) { + return `[NodeId: ${(child as Record).id}]` + } + return child + }) + } else { + serializedValue = this.serializeArray(value) + } + } else if ('id' in valueObj && 'type' in valueObj) { + serializedValue = `[NodeId: ${valueObj.id}]` + } else { + serializedValue = this.serializeObject(valueObj) + } + } else { + serializedValue = value + } + + log.properties.push({ key: prop, value: serializedValue }) + } catch { + // ignore - property may not exist on this node type + } + } + + // TEXT nodes: extract styled text segments + if (node.type === 'TEXT') { + try { + const textNode = node as TextNode + const SEGMENT_TYPE = [ + 'fontName', + 'fontWeight', + 'fontSize', + 'textDecoration', + 'textCase', + 'lineHeight', + 'letterSpacing', + 'fills', + 'textStyleId', + 'fillStyleId', + 'listOptions', + 'indentation', + 'hyperlink', + ] as const + const segments = textNode.getStyledTextSegments( + SEGMENT_TYPE as unknown as (keyof Omit< + StyledTextSegment, + 'characters' | 'start' | 'end' + >)[], + ) + const serializedSegments = segments.map((seg) => { + return { + ...seg, + fills: this.serializeArray(seg.fills as unknown[]), + } + }) + log.properties.push({ + key: 'styledTextSegments', + value: serializedSegments, + }) + } catch { + // ignore + } + } + } + private trackNodeRecursively(node: SceneNode): void { const wrappedNode = this.wrap(node) diff --git a/src/codegen/utils/paint-to-css.ts b/src/codegen/utils/paint-to-css.ts index 77e8fe3..b2f2fd0 100644 --- a/src/codegen/utils/paint-to-css.ts +++ b/src/codegen/utils/paint-to-css.ts @@ -4,6 +4,7 @@ import { toCamel } from '../../utils/to-camel' import { checkAssetNode } from './check-asset-node' import { fmtPct } from './fmtPct' import { solidToString } from './solid-to-string' +import { getVariableByIdCached } from './variable-cache' import { buildCssUrl } from './wrap-url' interface Point { @@ -19,7 +20,7 @@ async function processGradientStopColor( opacity: number, ): Promise { if (stop.boundVariables?.color) { - const variable = await figma.variables.getVariableByIdAsync( + const variable = await getVariableByIdCached( stop.boundVariables.color.id as string, ) if (variable?.name) { diff --git a/src/codegen/utils/perf.ts b/src/codegen/utils/perf.ts new file mode 100644 index 0000000..ddab222 --- /dev/null +++ b/src/codegen/utils/perf.ts @@ -0,0 +1,35 @@ +const perfEntries: Map = new Map() + +export function perfStart(): number { + return Date.now() +} + +export function perfEnd(label: string, start: number): void { + const elapsed = Date.now() - start + const entry = perfEntries.get(label) + if (entry) { + entry.total += elapsed + entry.count++ + } else { + perfEntries.set(label, { total: elapsed, count: 1 }) + } +} + +export function perfReset(): void { + perfEntries.clear() +} + +export function perfReport(): string { + const lines: string[] = ['[perf] --- Performance Report ---'] + const sorted = [...perfEntries.entries()].sort( + ([, a], [, b]) => b.total - a.total, + ) + for (const [label, { total, count }] of sorted) { + const avg = count > 0 ? (total / count).toFixed(1) : '0' + lines.push( + `[perf] ${label}: ${total}ms total, ${count} calls, ${avg}ms avg`, + ) + } + lines.push('[perf] --- End Report ---') + return lines.join('\n') +} diff --git a/src/codegen/utils/solid-to-string.ts b/src/codegen/utils/solid-to-string.ts index 912576a..5abb0f1 100644 --- a/src/codegen/utils/solid-to-string.ts +++ b/src/codegen/utils/solid-to-string.ts @@ -1,10 +1,11 @@ import { optimizeHex } from '../../utils/optimize-hex' import { rgbaToHex } from '../../utils/rgba-to-hex' import { toCamel } from '../../utils/to-camel' +import { getVariableByIdCached } from './variable-cache' export async function solidToString(solid: SolidPaint) { if (solid.boundVariables?.color) { - const variable = await figma.variables.getVariableByIdAsync( + const variable = await getVariableByIdCached( solid.boundVariables.color.id as string, ) if (variable?.name) return `$${toCamel(variable.name)}` diff --git a/src/codegen/utils/variable-cache.ts b/src/codegen/utils/variable-cache.ts new file mode 100644 index 0000000..04c6a61 --- /dev/null +++ b/src/codegen/utils/variable-cache.ts @@ -0,0 +1,22 @@ +// Global cache for figma.variables.getVariableByIdAsync() results. +// This is the single hottest Figma API call — used by solidToString, +// processGradientStopColor, and transitively by every prop getter that +// resolves colors (border, background, text-stroke, reaction, renderText). +// Keyed by variable ID; stores the Promise to deduplicate concurrent calls. +const variableByIdCache = new Map>() + +export function getVariableByIdCached( + variableId: string, +): Promise { + const cached = variableByIdCache.get(variableId) + if (cached) return cached + const promise = Promise.resolve( + figma.variables.getVariableByIdAsync(variableId), + ) + variableByIdCache.set(variableId, promise) + return promise +} + +export function resetVariableCache(): void { + variableByIdCache.clear() +} diff --git a/src/utils.ts b/src/utils.ts index fa4a640..9e9f0e1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,33 @@ import { toCamel } from './utils/to-camel' import { toPascal } from './utils/to-pascal' +// Cache for figma.getLocalTextStylesAsync() — called once per codegen run +let localTextStyleIdsCache: Promise> | null = null + +function getLocalTextStyleIds(): Promise> { + if (localTextStyleIdsCache) return localTextStyleIdsCache + localTextStyleIdsCache = Promise.resolve( + figma.getLocalTextStylesAsync(), + ).then((styles) => new Set(styles.map((s) => s.id))) + return localTextStyleIdsCache +} + +// Cache for figma.getStyleByIdAsync() — keyed by style ID +const styleByIdCache = new Map>() + +function getStyleByIdCached(styleId: string): Promise { + const cached = styleByIdCache.get(styleId) + if (cached) return cached + const promise = Promise.resolve(figma.getStyleByIdAsync(styleId)) + styleByIdCache.set(styleId, promise) + return promise +} + +export function resetTextStyleCache(): void { + localTextStyleIdsCache = null + styleByIdCache.clear() +} + export async function propsToPropsWithTypography( props: Record, textStyleId: string, @@ -8,9 +35,9 @@ export async function propsToPropsWithTypography( const ret: Record = { ...props } delete ret.w delete ret.h - const styles = await figma.getLocalTextStylesAsync() - if (textStyleId && styles.find((style) => style.id === textStyleId)) { - const style = await figma.getStyleByIdAsync(textStyleId) + const localStyleIds = await getLocalTextStyleIds() + if (textStyleId && localStyleIds.has(textStyleId)) { + const style = await getStyleByIdCached(textStyleId) if (style) { const split = style.name.split('/') ret.typography = toCamel(split[split.length - 1])