From cbd8323773999d7373b8e8f2ac2836807f7c6f49 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 14 Mar 2026 19:52:07 +0900 Subject: [PATCH] Optimize export --- bun.lock | 62 ++--- package.json | 4 +- src/code-impl.ts | 1 + src/codegen/Codegen.ts | 24 ++ .../__tests__/codegen-viewport.test.ts | 24 +- .../exportPagesAndComponents.test.ts | 180 +++++++++++++- src/commands/exportPagesAndComponents.ts | 224 +++++++++++++++--- 7 files changed, 457 insertions(+), 62 deletions(-) diff --git a/bun.lock b/bun.lock index 5fb8ade..2fb675b 100644 --- a/bun.lock +++ b/bun.lock @@ -10,8 +10,8 @@ "devDependencies": { "@biomejs/biome": "^2.4", "@figma/plugin-typings": "^1.123", - "@rspack/cli": "^1.7.7", - "@rspack/core": "^1.7.7", + "@rspack/cli": "^1.7.8", + "@rspack/core": "^1.7.8", "@types/bun": "^1.3", "husky": "^9.1", "typescript": "^5.9", @@ -19,31 +19,31 @@ }, }, "packages": { - "@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], + "@biomejs/biome": ["@biomejs/biome@2.4.7", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.7", "@biomejs/cli-darwin-x64": "2.4.7", "@biomejs/cli-linux-arm64": "2.4.7", "@biomejs/cli-linux-arm64-musl": "2.4.7", "@biomejs/cli-linux-x64": "2.4.7", "@biomejs/cli-linux-x64-musl": "2.4.7", "@biomejs/cli-win32-arm64": "2.4.7", "@biomejs/cli-win32-x64": "2.4.7" }, "bin": { "biome": "bin/biome" } }, "sha512-vXrgcmNGZ4lpdwZSpMf1hWw1aWS6B+SyeSYKTLrNsiUsAdSRN0J4d/7mF3ogJFbIwFFSOL3wT92Zzxia/d5/ng=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Oo0cF5mHzmvDmTXw8XSjhCia8K6YrZnk7aCS54+/HxyMdZMruMO3nfpDsrlar/EQWe41r1qrwKiCa2QDYHDzWA=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-I+cOG3sd/7HdFtvDSnF9QQPrWguUH7zrkIMMykM3PtfWU9soTcS2yRb9Myq6MHmzbeCT08D1UmY+BaiMl5CcoQ=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-om6FugwmibzfP/6ALj5WRDVSND4H2G9X0nkI1HZpp2ySf9lW2j0X68oQSaHEnls6666oy4KDsc5RFjT4m0kV0w=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-I2NvM9KPb09jWml93O2/5WMfNR7Lee5Latag1JThDRMURVhPX74p9UDnyTw3Ae6cE1DgXfw7sqQgX7rkvpc0vw=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.7", "", { "os": "linux", "cpu": "x64" }, "sha512-bV8/uo2Tj+gumnk4sUdkerWyCPRabaZdv88IpbmDWARQQoA/Q0YaqPz1a+LSEDIL7OfrnPi9Hq1Llz4ZIGyIQQ=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.7", "", { "os": "linux", "cpu": "x64" }, "sha512-00kx4YrBMU8374zd2wHuRV5wseh0rom5HqRND+vDldJPrWwQw+mzd/d8byI9hPx926CG+vWzq6AeiT7Yi5y59g=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-hOUHBMlFCvDhu3WCq6vaBoG0dp0LkWxSEnEEsxxXvOa9TfT6ZBnbh72A/xBM7CBYB7WgwqboetzFEVDnMxelyw=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.7", "", { "os": "win32", "cpu": "x64" }, "sha512-qEpGjSkPC3qX4ycbMUthXvi9CkRq7kZpkqMY1OyhmYlYLnANnooDQ7hDerM8+0NJ+DZKVnsIc07h30XOpt7LtQ=="], "@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="], - "@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/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], - "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], "@figma/plugin-typings": ["@figma/plugin-typings@1.123.0", "", {}, "sha512-NLv2aQ8R9dP5psDplWpq+pJxRUGsJ1YEYYbBV2oTd03kS+aau7N9XWLjw52s1uVgi8jQ33N001EX3f7vSCztjQ=="], @@ -93,31 +93,31 @@ "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - "@rspack/binding": ["@rspack/binding@1.7.7", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.7", "@rspack/binding-darwin-x64": "1.7.7", "@rspack/binding-linux-arm64-gnu": "1.7.7", "@rspack/binding-linux-arm64-musl": "1.7.7", "@rspack/binding-linux-x64-gnu": "1.7.7", "@rspack/binding-linux-x64-musl": "1.7.7", "@rspack/binding-wasm32-wasi": "1.7.7", "@rspack/binding-win32-arm64-msvc": "1.7.7", "@rspack/binding-win32-ia32-msvc": "1.7.7", "@rspack/binding-win32-x64-msvc": "1.7.7" } }, "sha512-9FqHG2Bl70Bd4gUmwA+3xUx4pYphdLO9ToIm9iMWbBINyArME0XboZg4FoEdU13LqndkWqaamkE613BR0lRF3g=="], + "@rspack/binding": ["@rspack/binding@1.7.8", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.8", "@rspack/binding-darwin-x64": "1.7.8", "@rspack/binding-linux-arm64-gnu": "1.7.8", "@rspack/binding-linux-arm64-musl": "1.7.8", "@rspack/binding-linux-x64-gnu": "1.7.8", "@rspack/binding-linux-x64-musl": "1.7.8", "@rspack/binding-wasm32-wasi": "1.7.8", "@rspack/binding-win32-arm64-msvc": "1.7.8", "@rspack/binding-win32-ia32-msvc": "1.7.8", "@rspack/binding-win32-x64-msvc": "1.7.8" } }, "sha512-P4fbrQx5hRhAiC8TBTEMCTnNawrIzJLjWwAgrTwRxjgenpjNvimEkQBtSGrXOY+c+MV5Q74P+9wPvVWLKzRkQQ=="], - "@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.7.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eL14fHy2JqfQ0YA5YMN2hktXhbafDSZt5kthvlBCbpQZLnYB7RP7TjHManIW/xFpnzrabvxkrLUOHhuIbWixIw=="], + "@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.7.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-KS6SRc+4VYRdX1cKr1j1HEuMNyEzt7onBS0rkenaiCRRYF0z4WNZNyZqRiuxgM3qZ3TISF7gdmgJQyd4ZB43ig=="], - "@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.7.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Zt+whHag/cTw1pZfRwkv11tu5LaAHy2VkvRVCsHClwrfp81PRcNJ2oRMurOUmRt1YL0mRdpRbZTh7XjGSc6gGw=="], + "@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.7.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-uyXSDKLg2CtqIJrsJDlCqQH80YIPsCUiTToJ59cXAG3v4eke0Qbiv6d/+pV0h/mc0u4inAaSkr5dD18zkMIghw=="], - "@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.7.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-uSq4qkvmAzSDUTKE2v4yUgHIBdTily1k3BcK5wBCGFm9OPODj5lQZpAdOHHIwu+Jxyjoa7Mb64tghhj9hZcXcA=="], + "@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.7.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-dD6gSHA18Uj0eqc1FCwwQ5IO5mIckrpYN4H4kPk9Pjau+1mxWvC4y5Lryz1Z8P/Rh1lnQ/wwGE0XL9nd80+LqQ=="], - "@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.7.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-NhWCBfiu6plpmLRP6c6D5lBUaVrBr1nvjSEc7VyQF8TGh8URo2btH0wngEiX0nWvidsSlERt1l6Y5QPGuiCl1g=="], + "@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.7.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-m+uBi9mEVGkZ02PPOAYN2BSmmvc00XGa6v9CjV8qLpolpUXQIMzDNG+i1fD5SHp8LO+XWsZJOHypMsT0MzGTGw=="], - "@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.7.7", "", { "os": "linux", "cpu": "x64" }, "sha512-aRvf8gCI7jDeEN9i4u9fY5coa3ZAyHzGVA4ZhTJCgZ5wWA5A9SQewMSq7khS1WAAFE1USlk1tUuPujnrGoYrGg=="], + "@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.7.8", "", { "os": "linux", "cpu": "x64" }, "sha512-IAPp2L3yS33MAEkcGn/I1gO+a+WExJHXz2ZlRlL2oFCUGpYi2ZQHyAcJ3o2tJqkXmdqsTiN+OjEVMd/RcLa24g=="], - "@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.7.7", "", { "os": "linux", "cpu": "x64" }, "sha512-ALPto4OT7snzXbYDyqkLfh1BvwDTTH1hPYXGUXBzQ0wEV7sXeyvxCC4yjH6B5MhR7W3tFuF4IfDy5Z4BxmOoGQ=="], + "@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.7.8", "", { "os": "linux", "cpu": "x64" }, "sha512-do/QNzb4GWdXCsipblDcroqRDR3BFcbyzpZpAw/3j9ajvEqsOKpdHZpILT2NZX/VahhjqfqB3k0kJVt3uK7UYQ=="], - "@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.7.7", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-7DZvUp0v75n451qfZw1ppbPakL6NAc2gjb5e9AJcOb7KUMBHNyOxqpPo/jRYKxH7isPpLfpoId79WQGGNTTMAw=="], + "@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.7.8", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-mHtgYTpdhx01i0XNKFYBZyCjtv9YUe/sDfpD1QK4FytPFB+1VpYnmZiaJIMM77VpNsjxGAqWhmUYxi2P6jWifw=="], - "@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.7.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-oI08KqyVDKhq1Qi/YPMdrSLDOib0DQes9Cg67NJLZISe5UXwzvgBj7zyyKpaj8TLWnIlKSq4ITr3haRnd4lOfA=="], + "@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.7.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mkxg86F7kIT4pM9XvE/1LAGjK5NOQi/GJxKyyiKbUAeKM8XBUizVeNuvKR0avf2V5IDAIRXiH1SX8SpujMJteA=="], - "@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.7.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-nZ/t7XpO/+tRjK6m85an27j8FwJqpYXVSBGReZbB6dVHZiS7l6psjWkIf6A3E2umn/RjA7qvHaPH9czWkH+Fhw=="], + "@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.7.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-VmTOZ/X7M85lKFNwb2qJpCRzr4SgO42vucq/X7Uz1oSoTPAf8UUMNdi7BPnu+D4lgy6l8PwV804ZyHO3gGsvPA=="], - "@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.7.7", "", { "os": "win32", "cpu": "x64" }, "sha512-+XnPOC1MoeF5Qa24Z8+DCsytQP0Q9Ifdkh+XzTWgvjpFQmGAkDynHUVfscmJL/8k/nd1l/6TyXCL1EGoqa0huQ=="], + "@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.7.8", "", { "os": "win32", "cpu": "x64" }, "sha512-BK0I4HAwp/yQLnmdJpUtGHcht3x11e9fZwyaiMzznznFc+Oypbf+FS5h+aBgpb53QnNkPpdG7MfAPoKItOcU8A=="], - "@rspack/cli": ["@rspack/cli@1.7.7", "", { "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-NSE7kE0TGDgs1iWHQs6uI+H1KHXIKCFzb8zPJ/TgAbfFNQNRGuq3cPJXs2n+p+HmnN3xY4xFyTyMuS99BrprrA=="], + "@rspack/cli": ["@rspack/cli@1.7.8", "", { "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-CyOD12ecwkVsvzX1peVo+pZO6JoDzxD3nHC5yBcwrWrntsAcp+e5RNHjz3yJuLa/Mta1Mgr3EZzv7BMBQf14ew=="], - "@rspack/core": ["@rspack/core@1.7.7", "", { "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.7", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-efwVXxAA9eYgLtYX53zcuuex6Wr8DnOXeIw3JFoA8EuyN7TINGqnvkuGDuE+F9XQxQ3KBzVueiYdMK42sVTyUw=="], + "@rspack/core": ["@rspack/core@1.7.8", "", { "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.8", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-kT6yYo8xjKoDfM7iB8N9AmN9DJIlrs7UmQDbpTu1N4zaZocN1/t2fIAWOKjr5+3eJlZQR2twKZhDVHNLbLPjOw=="], "@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=="], @@ -147,7 +147,7 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - "@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], @@ -379,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=="], @@ -547,8 +547,6 @@ "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/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -582,5 +580,7 @@ "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 4bda83a..43af250 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "license": "", "devDependencies": { "@figma/plugin-typings": "^1.123", - "@rspack/cli": "^1.7.7", - "@rspack/core": "^1.7.7", + "@rspack/cli": "^1.7.8", + "@rspack/core": "^1.7.8", "husky": "^9.1", "typescript": "^5.9", diff --git a/src/code-impl.ts b/src/code-impl.ts index 9ee00d0..4441c14 100644 --- a/src/code-impl.ts +++ b/src/code-impl.ts @@ -23,6 +23,7 @@ import { extractCustomComponentImports, extractImports, } from './commands/exportPagesAndComponents' + export { extractCustomComponentImports, extractImports } import { getComponentName, resetTextStyleCache } from './utils' diff --git a/src/codegen/Codegen.ts b/src/codegen/Codegen.ts index a0b6581..3285d9b 100644 --- a/src/codegen/Codegen.ts +++ b/src/codegen/Codegen.ts @@ -55,6 +55,25 @@ export function resetGlobalBuildTreeCache(): void { globalBuildTreeCache.clear() } +// Global asset node registry populated during buildTree(). +// Tracks nodes classified as SVG/PNG assets so callers (e.g. export commands) +// can collect them without re-walking the Figma node tree via IPC. +const globalAssetNodes = new Map< + string, + { node: SceneNode; type: 'svg' | 'png' } +>() + +export function resetGlobalAssetNodes(): void { + globalAssetNodes.clear() +} + +export function getGlobalAssetNodes(): ReadonlyMap< + string, + { node: SceneNode; type: 'svg' | 'png' } +> { + return globalAssetNodes +} + /** * Get componentPropertyReferences from a node (if available). */ @@ -373,6 +392,11 @@ export class Codegen { // Handle asset nodes (images/SVGs) const assetNode = checkAssetNode(node) if (assetNode) { + // Register in global asset registry for export commands + const assetKey = `${assetNode}/${node.name}` + if (!globalAssetNodes.has(assetKey)) { + globalAssetNodes.set(assetKey, { node, type: assetNode }) + } const props = await getProps(node) props.src = `/${assetNode === 'svg' ? 'icons' : 'images'}/${node.name}.${assetNode}` if (assetNode === 'svg') { diff --git a/src/codegen/__tests__/codegen-viewport.test.ts b/src/codegen/__tests__/codegen-viewport.test.ts index 321a38e..cef6589 100644 --- a/src/codegen/__tests__/codegen-viewport.test.ts +++ b/src/codegen/__tests__/codegen-viewport.test.ts @@ -1,6 +1,11 @@ import { afterAll, beforeEach, describe, expect, test } from 'bun:test' import { resetTextStyleCache } from '../../utils' -import { Codegen, resetGlobalBuildTreeCache } from '../Codegen' +import { + Codegen, + getGlobalAssetNodes, + resetGlobalAssetNodes, + resetGlobalBuildTreeCache, +} from '../Codegen' import { resetGetPropsCache } from '../props' import { resetSelectorPropsCache } from '../props/selector' import { ResponsiveCodegen } from '../responsive/ResponsiveCodegen' @@ -47,6 +52,7 @@ import { ResponsiveCodegen } from '../responsive/ResponsiveCodegen' beforeEach(() => { resetGlobalBuildTreeCache() + resetGlobalAssetNodes() resetGetPropsCache() resetSelectorPropsCache() resetTextStyleCache() @@ -54,12 +60,28 @@ beforeEach(() => { afterAll(() => { resetGlobalBuildTreeCache() + resetGlobalAssetNodes() resetGetPropsCache() resetSelectorPropsCache() resetTextStyleCache() ;(globalThis as { figma?: unknown }).figma = undefined }) +describe('globalAssetNodes', () => { + test('resetGlobalAssetNodes clears and getGlobalAssetNodes returns empty map', () => { + resetGlobalAssetNodes() + const assets = getGlobalAssetNodes() + expect(assets.size).toBe(0) + }) + + test('getGlobalAssetNodes returns readonly map', () => { + const assets = getGlobalAssetNodes() + expect(typeof assets.get).toBe('function') + expect(typeof assets.has).toBe('function') + expect(typeof assets.size).toBe('number') + }) +}) + function createComponentNode( name: string, variantProperties: Record, diff --git a/src/commands/__tests__/exportPagesAndComponents.test.ts b/src/commands/__tests__/exportPagesAndComponents.test.ts index 638e837..69af737 100644 --- a/src/commands/__tests__/exportPagesAndComponents.test.ts +++ b/src/commands/__tests__/exportPagesAndComponents.test.ts @@ -1,11 +1,25 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, mock, test } from 'bun:test' import { + collectAssetNodes, DEVUP_COMPONENTS, extractCustomComponentImports, extractImports, generateImportStatements, } from '../exportPagesAndComponents' +mock.module('../../codegen/utils/check-asset-node', () => ({ + checkAssetNode: (node: { + type: string + isAsset?: boolean + }): string | null => { + if (node.type === 'VECTOR') return 'svg' + if (node.type === 'STAR') return 'svg' + if (node.type === 'POLYGON') return 'svg' + if (node.isAsset && node.type === 'RECTANGLE') return 'png' + return null + }, +})) + describe('DEVUP_COMPONENTS', () => { test('should contain expected devup-ui components', () => { expect(DEVUP_COMPONENTS).toContain('Box') @@ -128,6 +142,170 @@ describe('extractCustomComponentImports', () => { }) }) +describe('collectAssetNodes', () => { + let nodeIdCounter = 0 + function createNode( + type: string, + name: string, + { + visible = true, + isAsset = false, + children, + id, + }: { + visible?: boolean + isAsset?: boolean + children?: SceneNode[] + id?: string + } = {}, + ): SceneNode { + return { + type, + name, + visible, + isAsset, + id: id ?? `node-${nodeIdCounter++}`, + ...(children ? { children } : {}), + } as unknown as SceneNode + } + + test('should collect SVG asset node', () => { + const assets = new Map() + const node = createNode('VECTOR', 'arrow-icon') + collectAssetNodes(node, assets) + expect(assets.size).toBe(1) + expect(assets.get('svg/arrow-icon')?.type).toBe('svg') + }) + + test('should collect PNG asset node', () => { + const assets = new Map() + const node = createNode('RECTANGLE', 'photo', { isAsset: true }) + collectAssetNodes(node, assets) + expect(assets.size).toBe(1) + expect(assets.get('png/photo')?.type).toBe('png') + }) + + test('should skip invisible nodes', () => { + const assets = new Map() + const node = createNode('VECTOR', 'hidden-icon', { visible: false }) + collectAssetNodes(node, assets) + expect(assets.size).toBe(0) + }) + + test('should recursively collect from children', () => { + const assets = new Map() + const node = createNode('FRAME', 'container', { + children: [createNode('VECTOR', 'icon-a'), createNode('STAR', 'icon-b')], + }) + collectAssetNodes(node, assets) + expect(assets.size).toBe(2) + expect(assets.has('svg/icon-a')).toBe(true) + expect(assets.has('svg/icon-b')).toBe(true) + }) + + test('should not descend into asset node children', () => { + const assets = new Map() + // VECTOR is an asset — even if it somehow had children, we don't walk them + const node = createNode('VECTOR', 'icon-parent') + collectAssetNodes(node, assets) + expect(assets.size).toBe(1) + }) + + test('should deduplicate by type and name', () => { + const assets = new Map() + const node = createNode('FRAME', 'wrapper', { + children: [ + createNode('VECTOR', 'same-icon'), + createNode('VECTOR', 'same-icon'), + ], + }) + collectAssetNodes(node, assets) + expect(assets.size).toBe(1) + }) + + test('should collect from nested frames', () => { + const assets = new Map() + const node = createNode('FRAME', 'outer', { + children: [ + createNode('FRAME', 'inner', { + children: [createNode('POLYGON', 'deep-icon')], + }), + ], + }) + collectAssetNodes(node, assets) + expect(assets.size).toBe(1) + expect(assets.has('svg/deep-icon')).toBe(true) + }) + + test('should collect both SVG and PNG assets', () => { + const assets = new Map() + const node = createNode('FRAME', 'mixed', { + children: [ + createNode('VECTOR', 'icon'), + createNode('RECTANGLE', 'image', { isAsset: true }), + ], + }) + collectAssetNodes(node, assets) + expect(assets.size).toBe(2) + expect(assets.get('svg/icon')?.type).toBe('svg') + expect(assets.get('png/image')?.type).toBe('png') + }) + + test('should return empty map for non-asset leaf node', () => { + const assets = new Map() + const node = createNode('FRAME', 'empty-frame') + collectAssetNodes(node, assets) + expect(assets.size).toBe(0) + }) + + test('should skip already-visited nodes when visited set is provided', () => { + const assets = new Map() + const visited = new Set() + const sharedChild = createNode('VECTOR', 'shared-icon', { id: 'shared-1' }) + + // First call walks the child + const parent1 = createNode('FRAME', 'parent-a', { + children: [sharedChild], + }) + collectAssetNodes(parent1, assets, visited) + expect(assets.size).toBe(1) + + // Second call with overlapping subtree — shared-1 already visited + const parent2 = createNode('FRAME', 'parent-b', { + children: [sharedChild], + }) + collectAssetNodes(parent2, assets, visited) + // Still 1 — child was skipped via visited set + expect(assets.size).toBe(1) + expect(visited.has('shared-1')).toBe(true) + }) + + test('should skip visited parent node entirely', () => { + const assets = new Map() + const visited = new Set() + const node = createNode('FRAME', 'root', { + id: 'root-1', + children: [createNode('VECTOR', 'icon-inside')], + }) + + collectAssetNodes(node, assets, visited) + expect(assets.size).toBe(1) + + // Clear assets but keep visited — re-collecting same root yields nothing new + assets.clear() + collectAssetNodes(node, assets, visited) + expect(assets.size).toBe(0) + }) + + test('should work without visited set (backward compatible)', () => { + const assets = new Map() + const node = createNode('VECTOR', 'compat-icon') + // No visited parameter — should still work + collectAssetNodes(node, assets) + expect(assets.size).toBe(1) + }) +}) + describe('generateImportStatements', () => { test('should generate devup-ui import statement', () => { const result = generateImportStatements([['Test', '']]) diff --git a/src/commands/exportPagesAndComponents.ts b/src/commands/exportPagesAndComponents.ts index b3075eb..6841d4d 100644 --- a/src/commands/exportPagesAndComponents.ts +++ b/src/commands/exportPagesAndComponents.ts @@ -1,13 +1,28 @@ import JSZip from 'jszip' -import { Codegen } from '../codegen/Codegen' +import { + Codegen, + getGlobalAssetNodes, + resetGlobalAssetNodes, +} from '../codegen/Codegen' import { ResponsiveCodegen } from '../codegen/responsive/ResponsiveCodegen' +import { checkAssetNode } from '../codegen/utils/check-asset-node' +import { + perfEnd, + perfReport, + perfReset, + perfStart, +} from '../codegen/utils/perf' import { wrapComponent } from '../codegen/utils/wrap-component' import { getComponentName } from '../utils' import { downloadFile } from '../utils/download-file' import { toPascal } from '../utils/to-pascal' const NOTIFY_TIMEOUT = 3000 +// Figma throttles >4 concurrent exportAsync calls for large PNGs (screenshots). +// SVG/asset exports are lightweight and scale better with higher concurrency. +const SCREENSHOT_BATCH_SIZE = 4 +const ASSET_BATCH_SIZE = 8 export const DEVUP_COMPONENTS = [ 'Center', @@ -80,19 +95,72 @@ export function generateImportStatements( return statements.length > 0 ? `${statements.join('\n')}\n\n` : '' } +interface ScreenshotTarget { + node: SceneNode + folder: JSZip + fileName: string +} + +/** + * Recursively collect asset nodes (SVGs and PNGs) from a Figma node tree. + * Uses checkAssetNode to identify assets. Deduplicates by type + name. + * Does not descend into children of asset nodes (they are leaf nodes in codegen). + * + * Pass a `visited` set to skip already-walked subtrees when collecting + * from multiple overlapping roots (e.g. top-level nodes + external component sets). + */ +export function collectAssetNodes( + node: SceneNode, + assets: Map, + visited?: Set, +): void { + if (!node.visible) return + if (visited) { + const id = (node as SceneNode & { id?: string }).id + if (id) { + if (visited.has(id)) return + visited.add(id) + } + } + const assetType = checkAssetNode(node) + if (assetType) { + const key = `${assetType}/${node.name}` + if (!assets.has(key)) { + assets.set(key, { node, type: assetType }) + } + return + } + if ('children' in node) { + for (const child of (node as SceneNode & ChildrenMixin).children) { + collectAssetNodes(child, assets, visited) + } + } +} + export async function exportPagesAndComponents() { let notificationHandler = figma.notify('Preparing export...', { timeout: Infinity, }) try { + perfReset() + const tTotal = perfStart() + const zip = new JSZip() const componentsFolder = zip.folder('components') const pagesFolder = zip.folder('pages') + const iconsFolder = zip.folder('icons') + const imagesFolder = zip.folder('images') let componentCount = 0 let pageCount = 0 + // Deferred work collectors + const screenshotTargets: ScreenshotTarget[] = [] + + // Reset global asset registry so buildTree() populates it fresh + resetGlobalAssetNodes() + // Track processed COMPONENT_SETs to avoid duplicates const processedComponentSets = new Set() @@ -105,8 +173,12 @@ export async function exportPagesAndComponents() { const totalNodes = nodes.length let processedNodes = 0 - // Helper to update progress - function updateProgress(message: string) { + // Throttled notification — avoids cancel/recreate churn on rapid progress + let lastNotifyTime = 0 + function updateProgress(message: string, force = false) { + const now = Date.now() + if (!force && now - lastNotifyTime < 200) return + lastNotifyTime = now const percent = Math.round((processedNodes / totalNodes) * 100) notificationHandler.cancel() notificationHandler = figma.notify(`[${percent}%] ${message}`, { @@ -123,32 +195,35 @@ export async function exportPagesAndComponents() { updateProgress(`Processing component: ${componentName}`) + let t = perfStart() const responsiveCodes = await ResponsiveCodegen.generateVariantResponsiveComponents( componentSet, componentName, ) + perfEnd(`responsiveCodegen(${componentName})`, t) + t = perfStart() for (const [name, code] of responsiveCodes) { const importStatement = generateImportStatements([[name, code]]) const fullCode = importStatement + code componentsFolder?.file(`${name}.tsx`, fullCode) componentCount++ } - - // Capture screenshot of the component set - try { - const imageData = await componentSet.exportAsync({ - format: 'PNG', - constraint: { type: 'SCALE', value: 1 }, + perfEnd('writeComponentFiles', t) + + // Defer screenshot capture + if (componentsFolder) { + screenshotTargets.push({ + node: componentSet, + folder: componentsFolder, + fileName: `${componentName}.png`, }) - componentsFolder?.file(`${componentName}.png`, imageData) - } catch (e) { - console.error(`Failed to capture screenshot for ${componentName}:`, e) } } // Process each node + const tCodegen = perfStart() for (const node of nodes) { processedNodes++ @@ -167,9 +242,12 @@ export async function exportPagesAndComponents() { updateProgress(`Processing: ${node.name}`) // 3. Extract components using Codegen for other node types + let t = perfStart() const codegen = new Codegen(node) await codegen.run() + perfEnd(`codegen(${node.name})`, t) + t = perfStart() const componentsCodes = codegen.getComponentsCodes() const componentNodes = codegen.getComponentNodes() @@ -180,6 +258,7 @@ export async function exportPagesAndComponents() { componentsFolder?.file(`${name}.tsx`, fullCode) componentCount++ } + perfEnd('writeComponentFiles', t) // 4. Generate responsive codes for COMPONENT_SET components found inside for (const componentNode of componentNodes) { @@ -201,6 +280,7 @@ export async function exportPagesAndComponents() { updateProgress(`Generating page: ${sectionNode.name}`) + t = perfStart() const responsiveCodegen = new ResponsiveCodegen(sectionNode) const responsiveCode = await responsiveCodegen.generateResponsiveCode() const baseName = toPascal(sectionNode.name) @@ -216,22 +296,21 @@ export async function exportPagesAndComponents() { const fullCode = importStatement + wrappedCode pagesFolder?.file(`${pageName}.tsx`, fullCode) - - // Capture screenshot of the section - updateProgress(`Capturing screenshot: ${pageName}`) - try { - const imageData = await sectionNode.exportAsync({ - format: 'PNG', - constraint: { type: 'SCALE', value: 1 }, + perfEnd(`responsivePage(${pageName})`, t) + + // Defer screenshot capture + if (pagesFolder) { + screenshotTargets.push({ + node: sectionNode, + folder: pagesFolder, + fileName: `${pageName}.png`, }) - pagesFolder?.file(`${pageName}.png`, imageData) - } catch (e) { - console.error(`Failed to capture screenshot for ${pageName}:`, e) } pageCount++ } } + perfEnd('phase1.codegen', tCodegen) // Check if we have anything to export if (componentCount === 0 && pageCount === 0) { @@ -240,21 +319,112 @@ export async function exportPagesAndComponents() { return } + // Asset nodes were already collected by Codegen's buildTree() into the + // global registry — no need to re-walk the Figma node tree via IPC. + const assetNodes = getGlobalAssetNodes() + + // Phase 2: Batch export screenshots and assets in parallel + const tExport = perfStart() + const totalExports = screenshotTargets.length + assetNodes.size + let completedExports = 0 + + function updateExportProgress(label: string) { + const now = Date.now() + if (now - lastNotifyTime < 200) return + lastNotifyTime = now + const percent = Math.round((completedExports / totalExports) * 100) + notificationHandler.cancel() + notificationHandler = figma.notify(`Exporting [${percent}%] ${label}`, { + timeout: Infinity, + }) + } + + // Export screenshots in parallel batches + const tScreenshots = perfStart() + for (let i = 0; i < screenshotTargets.length; i += SCREENSHOT_BATCH_SIZE) { + const batch = screenshotTargets.slice(i, i + SCREENSHOT_BATCH_SIZE) + updateExportProgress(`screenshots (${i + 1}/${screenshotTargets.length})`) + await Promise.all( + batch.map(async ({ node, folder, fileName }) => { + try { + const t = perfStart() + const imageData = await node.exportAsync({ + format: 'PNG', + constraint: { type: 'SCALE', value: 1 }, + }) + folder.file(fileName, imageData) + perfEnd('exportAsync(screenshot)', t) + completedExports++ + } catch (e) { + console.error(`Failed to capture screenshot for ${fileName}:`, e) + completedExports++ + } + }), + ) + } + perfEnd('phase2.screenshots', tScreenshots) + + // Export asset files in parallel batches (SVGs → icons/, PNGs → images/) + const tAssets = perfStart() + const assetEntries = [...assetNodes.values()] + let assetCount = 0 + for (let i = 0; i < assetEntries.length; i += ASSET_BATCH_SIZE) { + const batch = assetEntries.slice(i, i + ASSET_BATCH_SIZE) + updateExportProgress(`assets (${i + 1}/${assetEntries.length})`) + await Promise.all( + batch.map(async ({ node, type }) => { + try { + const fileName = `${node.name}.${type}` + const t = perfStart() + if (type === 'svg') { + const svgData = await node.exportAsync({ format: 'SVG' }) + iconsFolder?.file(fileName, svgData) + } else { + const pngData = await node.exportAsync({ + format: 'PNG', + constraint: { type: 'SCALE', value: 2 }, + }) + imagesFolder?.file(fileName, pngData) + } + perfEnd(`exportAsync(${type})`, t) + assetCount++ + completedExports++ + } catch (e) { + console.error(`Failed to export asset ${node.name}:`, e) + completedExports++ + } + }), + ) + } + perfEnd('phase2.assets', tAssets) + perfEnd('phase2.export', tExport) + notificationHandler.cancel() notificationHandler = figma.notify('[100%] Creating zip file...', { timeout: Infinity, }) + const tZip = perfStart() await downloadFile( `${figma.currentPage.name}-export.zip`, - await zip.generateAsync({ type: 'uint8array' }), + await zip.generateAsync({ + type: 'uint8array', + compression: 'DEFLATE', + compressionOptions: { level: 1 }, + }), ) + perfEnd('phase3.zip', tZip) + + perfEnd('exportPagesAndComponents()', tTotal) + console.info(perfReport()) notificationHandler.cancel() - figma.notify( - `Exported ${componentCount} components and ${pageCount} pages`, - { timeout: NOTIFY_TIMEOUT }, - ) + + const parts = [`${componentCount} components`, `${pageCount} pages`] + if (assetCount > 0) { + parts.push(`${assetCount} assets`) + } + figma.notify(`Exported ${parts.join(', ')}`, { timeout: NOTIFY_TIMEOUT }) } catch (error) { console.error(error) notificationHandler.cancel()