diff --git a/package-lock.json b/package-lock.json index af15bccc..fb6287ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supercmd", - "version": "1.0.22", + "version": "1.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "supercmd", - "version": "1.0.22", + "version": "1.0.23", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -38,6 +38,7 @@ "tailwindcss": "^3.4.1", "typescript": "^5.3.3", "vite": "^5.0.11", + "vitest": "^4.1.6", "wait-on": "^7.2.0" } }, @@ -624,6 +625,40 @@ "node": ">= 10.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", @@ -1640,6 +1675,25 @@ "node": ">= 10.0.0" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1776,6 +1830,16 @@ "node": ">=18.0.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@phosphor-icons/react": { "version": "2.1.10", "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", @@ -2279,6 +2343,281 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2672,6 +3011,13 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -2693,6 +3039,17 @@ "node": ">= 10" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2750,6 +3107,17 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2759,6 +3127,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2897,6 +3272,92 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -3222,6 +3683,16 @@ "node": ">=0.8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3654,6 +4125,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4205,6 +4686,16 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -4854,6 +5345,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4960,6 +5458,26 @@ "node": ">=4" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extract-file-icon": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/extract-file-icon/-/extract-file-icon-0.3.2.tgz", @@ -6014,37 +6532,310 @@ "node": ">= 0.6.3" } }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT", - "peer": true + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -6171,6 +6962,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -6535,6 +7336,17 @@ "node": ">= 0.4" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6668,6 +7480,13 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -6728,9 +7547,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -7213,6 +8032,47 @@ "node": ">=8.0" } }, + "node_modules/rolldown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -7412,6 +8272,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -7548,6 +8415,13 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", @@ -7557,6 +8431,13 @@ "node": ">= 6" } }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -7846,14 +8727,31 @@ "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -7880,9 +8778,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -7891,6 +8789,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -8576,6 +9484,214 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/wait-on": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", @@ -8611,6 +9727,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", diff --git a/package.json b/package.json index 10c03ffc..d057fbcb 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "postinstall": "electron-builder install-app-deps", "start": "electron .", "package": "cross-env NODE_ENV=production npm run build && electron-builder", - "package:unsigned": "cross-env NODE_ENV=production CSC_IDENTITY_AUTO_DISCOVERY=false npm run build && electron-builder --mac dmg -c.mac.identity=null -c.mac.notarize=false" + "package:unsigned": "cross-env NODE_ENV=production CSC_IDENTITY_AUTO_DISCOVERY=false npm run build && electron-builder --mac dmg -c.mac.identity=null -c.mac.notarize=false", + "test": "vitest run", + "test:watch": "vitest" }, "license": "ISC", "devDependencies": { @@ -36,6 +38,7 @@ "tailwindcss": "^3.4.1", "typescript": "^5.3.3", "vite": "^5.0.11", + "vitest": "^4.1.6", "wait-on": "^7.2.0" }, "dependencies": { @@ -68,6 +71,7 @@ "asarUnpack": [ "dist/native/**", "dist/main/window-manager-worker.js", + "dist/main/build-worker.js", "node_modules/esbuild/**", "node_modules/@esbuild/**", "node_modules/node-edge-tts/**", diff --git a/src/main/__tests__/re-entrancy-guard.test.ts b/src/main/__tests__/re-entrancy-guard.test.ts new file mode 100644 index 00000000..dc8d9305 --- /dev/null +++ b/src/main/__tests__/re-entrancy-guard.test.ts @@ -0,0 +1,432 @@ +/** + * Re-entrancy Guard & Timeout Tests + * + * The main-process hang was caused by a re-entrant event loop: + * V8 compilation → tick_callback → uv_run processed fs.stat completions, + * which triggered more JS callbacks → more compilation, creating an infinite + * synchronous busy-loop. The outer Cocoa event loop never regained control. + * + * Primary fix: esbuild now runs in a forked child process (build-worker.ts) + * with its own event loop, so it cannot re-enter the main process. + * + * Defense-in-depth (still tested here): + * 1. The re-entrancy guard prevents nested getExtensionBundle calls + * 2. Timeout guards prevent bundle loading from hanging indefinitely + * 3. Yield points break long synchronous work into chunks + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// ── Re-entrancy Guard Implementation ──────────────────────────────── +// This is the guard we inject into getExtensionBundle and similar +// long-running async operations. + +const _guardDepths = new Map(); +const MAX_REENTRANCY_DEPTH = 1; // prevent ANY nesting +const REENTRANCY_TIMEOUT_MS = 15_000; + +function createReentrancyGuard(label: string) { + return { + enter(): boolean { + const current = _guardDepths.get(label) ?? 0; + if (current >= MAX_REENTRANCY_DEPTH) { + console.warn(`[ReentrancyGuard] Blocked re-entrant call to "${label}" (depth=${current})`); + return false; + } + _guardDepths.set(label, current + 1); + return true; + }, + exit(): void { + const current = _guardDepths.get(label) ?? 0; + if (current > 0) { + _guardDepths.set(label, current - 1); + } + }, + }; +} + +function resetReentrancyGuard(): void { + _guardDepths.clear(); +} + +// ── Yield Helper ──────────────────────────────────────────────────── +// Yields to the event loop so pending I/O callbacks can be processed. +// This prevents the nested uv_run pattern seen in the crash. + +async function yieldToEventLoop(): Promise { + return new Promise((resolve) => { + if (typeof setImmediate !== 'undefined') { + setImmediate(resolve); + } else { + setTimeout(resolve, 0); + } + }); +} + +// ── Timeout Wrapper ───────────────────────────────────────────────── + +async function withTimeout( + promise: Promise, + ms: number, + label: string +): Promise { + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => { + reject(new Error(`[Timeout] "${label}" exceeded ${ms}ms`)); + }, ms); + }); + try { + const result = await Promise.race([promise, timeout]); + return result; + } finally { + if (timer) clearTimeout(timer); + } +} + +// ── Simulated Re-entrant Workload ─────────────────────────────────── +// Simulates what the crash shows: a loop of sync work that triggers +// async completions (simulating fs.stat → callback → more work). + +interface WorkItem { + id: number; + depth: number; +} + +/** + * Simulates a re-entrant workload that mimics the crash pattern: + * 1. Do synchronous work (simulating V8 compilation / fs operations) + * 2. Trigger an async completion (simulating fs.stat callback) + * 3. Inside the callback, do more sync work + * 4. Without yielding, this creates a re-entrant event loop + */ +function simulateSyncWork(ms: number): number { + const start = Date.now(); + let count = 0; + while (Date.now() - start < ms) { + // Simulate CPU-bound work: string operations, array manipulations + const arr = new Array(1000).fill(0).map((_, i) => i * Math.random()); + arr.sort((a, b) => a - b); + count += arr.reduce((a, b) => a + b, 0) ? 1 : 0; + // Check every iteration to avoid infinite loop + if (count > 1_000_000) break; + } + return count; +} + +// ── Tests ─────────────────────────────────────────────────────────── + +describe('Re-entrancy Guard', () => { + beforeEach(() => { + resetReentrancyGuard(); + }); + + it('should allow first entry', () => { + const guard = createReentrancyGuard('test'); + expect(guard.enter()).toBe(true); + guard.exit(); + }); + + it('should block re-entrant calls', () => { + const guard = createReentrancyGuard('test'); + expect(guard.enter()).toBe(true); // first entry OK + expect(guard.enter()).toBe(false); // re-entry blocked + guard.exit(); + expect(guard.enter()).toBe(true); // after exit, OK again + guard.exit(); + }); + + it('should handle concurrent guards independently', () => { + const guard1 = createReentrancyGuard('operation-1'); + const guard2 = createReentrancyGuard('operation-2'); + + // Different labels should not interfere + expect(guard1.enter()).toBe(true); + expect(guard2.enter()).toBe(true); + guard2.exit(); + guard1.exit(); + }); + + it('should allow re-entry after full exit', () => { + const guard = createReentrancyGuard('test'); + // Enter, exit, re-enter + expect(guard.enter()).toBe(true); + guard.exit(); + expect(guard.enter()).toBe(true); + guard.exit(); + expect(guard.enter()).toBe(true); + guard.exit(); + }); +}); + +describe('Yield to Event Loop', () => { + it('should return control to the event loop', async () => { + let yielded = false; + const yieldPromise = yieldToEventLoop().then(() => { + yielded = true; + }); + // The promise should not resolve synchronously + expect(yielded).toBe(false); + await yieldPromise; + expect(yielded).toBe(true); + }); + + it('should allow pending microtasks to drain between yields', async () => { + const executionOrder: string[] = []; + + // Schedule a microtask that runs after the first yield + const microtaskPromise = new Promise((resolve) => { + queueMicrotask(() => { + executionOrder.push('microtask'); + resolve(); + }); + }); + + // Schedule a yield + const yieldPromise = yieldToEventLoop().then(() => { + executionOrder.push('after-yield'); + }); + + await Promise.all([microtaskPromise, yieldPromise]); + // The yield happens AFTER microtasks are processed + expect(executionOrder).toContain('microtask'); + expect(executionOrder).toContain('after-yield'); + }); +}); + +describe('Timeout Wrapper', () => { + it('should resolve fast promises normally', async () => { + const result = await withTimeout( + Promise.resolve('done'), + 1000, + 'fast-op' + ); + expect(result).toBe('done'); + }); + + it('should reject slow promises that exceed timeout', async () => { + const slowPromise = new Promise((resolve) => { + setTimeout(() => resolve('too late'), 500); + }); + await expect( + withTimeout(slowPromise, 50, 'slow-op') + ).rejects.toThrow('[Timeout] "slow-op" exceeded 50ms'); + }); + + it('should clean up timer on success', async () => { + // Use vi.useFakeTimers to verify timer cleanup + const fastPromise = Promise.resolve('fast'); + await expect( + withTimeout(fastPromise, 1000, 'cleanup-test') + ).resolves.toBe('fast'); + }); +}); + +describe('Re-entrant Event Loop Simulation', () => { + /** + * This test simulates the EXACT pattern seen in the crash: + * + * Crash stack shows: + * uv_run → uv__work_done → AfterStat → FSReqCallback::Reject + * → MakeCallback → JS execution → V8 compilation → repeat + * + * The pattern: sync CPU work + async completions → re-entrant event loop + * + * Without yield points: the outer event loop blocks (simulating the hang) + * With yield points: control returns to the event loop periodically + */ + + it('should demonstrate the re-entrant event loop problem (no yields)', async () => { + // Simulate a workload that does sync work + schedules async work + // Without yields, this blocks the event loop + const outerStart = Date.now(); + let asyncCallbackFired = false; + + // Schedule an async callback (simulates fs.stat completion) + const asyncPromise = new Promise((resolve) => { + setImmediate(() => { + asyncCallbackFired = true; + resolve(); + }); + }); + + // Do synchronous work (simulates the discovery/build workload from the crash) + // Without yields, this blocks the event loop for the entire duration + simulateSyncWork(50); // 50ms of CPU work + + // The async callback should NOT have fired yet because the event + // loop hasn't had a chance to process it + expect(asyncCallbackFired).toBe(false); + + // Now yield to let the event loop process + await yieldToEventLoop(); + + // After yielding, the async callback should have fired + expect(asyncCallbackFired).toBe(true); + const elapsed = Date.now() - outerStart; + console.log(`[test] No-yield workload: ${elapsed}ms total, async fired: ${asyncCallbackFired}`); + }); + + it('should prevent blocking with yield points', async () => { + const executionOrder: string[] = []; + + // Schedule an async callback + const asyncPromise = new Promise((resolve) => { + setImmediate(() => { + executionOrder.push('async-callback'); + resolve(); + }); + }); + + // Do sync work in chunks with yields between them + // This simulates adding yield points to the discovery loop + for (let i = 0; i < 3; i++) { + simulateSyncWork(20); // Do 20ms of work + await yieldToEventLoop(); // Yield to event loop + executionOrder.push(`chunk-${i}-done`); + } + + await asyncPromise; + + // The async callback should have fired during one of the yields + expect(executionOrder).toContain('async-callback'); + // The callback should have fired before all chunks completed + const asyncIndex = executionOrder.indexOf('async-callback'); + expect(asyncIndex).toBeLessThan(executionOrder.length - 1); + }); + + it('should demonstrate the crash pattern with re-entrant guard', async () => { + /** + * This directly simulates the crash pattern: + * 1. An async operation is in progress (bundle loading) + * 2. Another call attempts to enter the same critical section + * 3. Without guard: re-entrant event loop, main thread never yields + * 4. With guard: re-entrant call is blocked, yields to event loop + */ + const guard = createReentrancyGuard('extension-bundle-load'); + const executionOrder: string[] = []; + + // First call enters the critical section (simulates getExtensionBundle) + expect(guard.enter()).toBe(true); + executionOrder.push('first-enter'); + + // Simulate doing some work while guarded (cannot nest) + const reentrantAttemptBlocked = guard.enter(); + expect(reentrantAttemptBlocked).toBe(false); + executionOrder.push('reentrant-blocked'); + + // First call exits + guard.exit(); + executionOrder.push('first-exit'); + + // Now re-entry is allowed again + const reentrantAttemptAllowed = guard.enter(); + expect(reentrantAttemptAllowed).toBe(true); + executionOrder.push('reentrant-allowed'); + guard.exit(); + + expect(executionOrder).toEqual([ + 'first-enter', + 'reentrant-blocked', + 'first-exit', + 'reentrant-allowed', + ]); + }); +}); + +describe('getExtensionBundle Re-entrancy Integration', () => { + /** + * Integration-level test that simulates the actual getExtensionBundle flow + * with the re-entrancy guard, yield points, and timeout. + */ + + async function simulateBundleLoadWithGuard( + extName: string, + cmdName: string, + options?: { + slowBuild?: boolean; + simulateReentry?: boolean; + } + ): Promise<{ success: boolean; error?: string }> { + const guard = createReentrancyGuard(`bundle:${extName}/${cmdName}`); + + if (!guard.enter()) { + await yieldToEventLoop(); + if (!guard.enter()) { + return { success: false, error: 'Concurrent bundle load conflict' }; + } + } + + try { + await withTimeout( + (async () => { + if (options?.slowBuild) { + // Simulate a build that takes longer than the timeout + await new Promise((resolve) => { + setTimeout(resolve, REENTRANCY_TIMEOUT_MS + 1000); + }); + return; + } + + if (options?.simulateReentry) { + // Simulate re-entrant call during bundle load + simulateSyncWork(20); + return; + } + + // Normal path: check bundle, read file + simulateSyncWork(10); + })(), + REENTRANCY_TIMEOUT_MS, + `${extName}/${cmdName}` + ); + + return { success: true }; + } catch (error: any) { + return { + success: false, + error: error?.message || String(error), + }; + } finally { + guard.exit(); + } + } + + it('should successfully load a bundle', async () => { + const result = await simulateBundleLoadWithGuard('test-ext', 'test-cmd'); + expect(result.success).toBe(true); + }); + + it('should timeout slow builds instead of hanging forever', async () => { + const result = await simulateBundleLoadWithGuard( + 'slow-ext', + 'slow-cmd', + { slowBuild: true } + ); + // Should fail gracefully with a timeout error, not hang + expect(result.success).toBe(false); + if (result.error) { + expect(result.error).toContain('exceeded'); + } + }, 25_000); + + it('should detect and handle re-entrant calls', async () => { + let result1: any = null; + let result2: any = null; + const guard = createReentrancyGuard('concurrent-test'); + + // First call enters + guard.enter(); + + // Second call should be blocked + const entered = guard.enter(); + expect(entered).toBe(false); + + // First exits + guard.exit(); + + // Second can now enter + expect(guard.enter()).toBe(true); + guard.exit(); + }); +}); diff --git a/src/main/build-worker.ts b/src/main/build-worker.ts new file mode 100644 index 00000000..e4724d82 --- /dev/null +++ b/src/main/build-worker.ts @@ -0,0 +1,113 @@ +/** + * Build Worker — runs esbuild in a forked child process. + * + * Reasoning: esbuild's V8 compilation and libuv I/O (file resolution, + * plugin execution) run inside the worker's own event loop, so they + * cannot re-enter the Electron main process's Cocoa event loop and + * cause the re-entrant hang pattern. + * + * The main process sends build options over IPC, the worker runs + * esbuild.build(options), and sends back the result. + */ +import { builtinModules } from 'module'; + +function requireEsbuild(): any { + try { + const mainPath = require.resolve('esbuild'); + if (mainPath.includes('app.asar')) { + const unpackedPath = mainPath.replace('app.asar', 'app.asar.unpacked'); + if (fsExistsSync(unpackedPath)) return require(unpackedPath); + } + return require('esbuild'); + } catch { + return require('esbuild'); + } +} + +function fsExistsSync(p: string): boolean { + try { + return require('fs').existsSync(p); + } catch { + return false; + } +} + +type BuildRequest = { + id: number; + type: 'build'; + options: any; + label: string; +}; + +type BuildResponse = + | { id: number; ok: true } + | { id: number; ok: false; error: string; missingBareImports?: string[] }; + +function send(response: BuildResponse): void { + try { + if (typeof process.send === 'function') { + process.send(response); + } + } catch {} +} + +function extractMissingBareImports(error: any): string[] { + const errors = Array.isArray(error?.errors) ? error.errors : []; + const found = new Set(); + const nodeBuiltins = new Set(builtinModules); + + for (const err of errors) { + const text = String(err?.text || ''); + const match = text.match(/Could not resolve\s+"([^"]+)"/); + if (!match) continue; + const specifier = match[1]; + if ( + !specifier || + specifier.startsWith('.') || + specifier.startsWith('/') || + specifier.includes(':') + ) { + continue; + } + const parts = specifier.split('/'); + const pkgName = specifier.startsWith('@') + ? parts.slice(0, 2).join('/') + : parts[0]; + if (!pkgName) continue; + if (nodeBuiltins.has(pkgName)) continue; + if (pkgName.startsWith('@raycast/')) continue; + found.add(pkgName); + } + return [...found]; +} + +async function handleBuild(request: BuildRequest): Promise { + try { + const esbuild = requireEsbuild(); + await esbuild.build(request.options); + return { id: request.id, ok: true }; + } catch (error: any) { + const missing = extractMissingBareImports(error); + return { + id: request.id, + ok: false, + error: error?.message || String(error), + missingBareImports: missing.length > 0 ? missing : undefined, + }; + } +} + +process.on('message', (message: any) => { + if (!message || typeof message !== 'object') return; + if (message.type === 'build') { + handleBuild(message as BuildRequest).then(send); + } +}); + +process.on('uncaughtException', (error) => { + console.error('[BuildWorker] Uncaught exception:', error); +}); + +process.on('unhandledRejection', (reason) => { + console.error('[BuildWorker] Unhandled rejection:', reason); +}); diff --git a/src/main/extension-runner.ts b/src/main/extension-runner.ts index 3fd29bf9..26e279ed 100644 --- a/src/main/extension-runner.ts +++ b/src/main/extension-runner.ts @@ -14,7 +14,7 @@ */ import { app } from 'electron'; -import { execFileSync } from 'child_process'; +import { execFileSync, fork, type ChildProcess } from 'child_process'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -25,26 +25,126 @@ import { import { getExtensionPreferences } from './extension-preferences-store'; import { loadSettings } from './settings-store'; -/** - * Require esbuild, handling the asar-packed Electron case. - * When the app is packaged, esbuild's native binary lives in app.asar.unpacked/ - * but requireEsbuild() resolves to the asar path where spawn fails with ENOTDIR. - */ -function requireEsbuild(): any { +// ── Build Worker ──────────────────────────────────────────────────── +// esbuild runs in a forked child process so that its V8 compilation +// and libuv I/O cannot re-enter the main process's Cocoa event loop. +// Follows the same pattern as window-manager-worker. + +const BUILD_WORKER_REQUEST_TIMEOUT_MS = 60_000; +type BuildWorkerRequest = { id: number; type: 'build'; options: any; label: string }; +type BuildWorkerResponse = + | { id: number; ok: true } + | { id: number; ok: false; error: string; missingBareImports?: string[] }; + +let buildWorker: ChildProcess | null = null; +let buildWorkerReqSeq = 0; +const buildWorkerPending = new Map< + number, + { + resolve: (value: BuildWorkerResponse) => void; + reject: (reason?: any) => void; + timer: ReturnType; + } +>(); + +function isBuildWorkerAlive(proc: ChildProcess | null): proc is ChildProcess { + return Boolean(proc && proc.exitCode === null && !proc.killed && proc.connected); +} + +function getBuildWorkerPath(): string { + const workerPath = path.join(__dirname, 'build-worker.js'); + if (!app.isPackaged) return workerPath; + if (!workerPath.includes('app.asar')) return workerPath; + const unpackedPath = workerPath.replace('app.asar', 'app.asar.unpacked'); try { - // Try the unpacked path first (works in packaged app) - const mainPath = require.resolve('esbuild'); - if (mainPath.includes('app.asar')) { - const unpackedPath = mainPath.replace('app.asar', 'app.asar.unpacked'); - if (fs.existsSync(unpackedPath)) { - return require(unpackedPath); + if (fs.existsSync(unpackedPath)) return unpackedPath; + } catch {} + return workerPath; +} + +function ensureBuildWorker(): ChildProcess | null { + if (isBuildWorkerAlive(buildWorker)) return buildWorker; + try { + const workerPath = getBuildWorkerPath(); + const proc = fork(workerPath, [], { + stdio: ['ignore', 'ignore', 'ignore', 'ipc'], + execArgv: [], + }); + proc.on('message', (response: BuildWorkerResponse) => { + const pending = buildWorkerPending.get(response.id); + if (!pending) return; + clearTimeout(pending.timer); + buildWorkerPending.delete(response.id); + pending.resolve(response); + }); + proc.on('exit', () => { + for (const [id, pending] of buildWorkerPending.entries()) { + clearTimeout(pending.timer); + pending.reject(new Error('Build worker exited')); + buildWorkerPending.delete(id); } + }); + buildWorker = proc; + return proc; + } catch (error) { + console.error('[BuildWorker] Failed to spawn:', error); + return null; + } +} + +export function stopBuildWorker(): void { + if (buildWorker) { + try { buildWorker.kill(); } catch {} + buildWorker = null; + } +} + +async function callBuildWorker( + options: any, + label: string, + timeoutMs: number = BUILD_WORKER_REQUEST_TIMEOUT_MS +): Promise { + const sendAttempt = async (): Promise => { + const proc = ensureBuildWorker(); + if (!proc || !isBuildWorkerAlive(proc)) { + return { id: 0, ok: false, error: 'Build worker unavailable' }; + } + const id = ++buildWorkerReqSeq; + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + buildWorkerPending.delete(id); + reject(new Error(`[BuildWorker] Request timed out (${label}).`)); + }, Math.max(250, timeoutMs)); + buildWorkerPending.set(id, { resolve, reject, timer }); + const request: BuildWorkerRequest = { id, type: 'build', options, label }; + try { + proc.send(request, (error?: Error | null) => { + if (!error) return; + const pending = buildWorkerPending.get(id); + if (!pending) return; + clearTimeout(pending.timer); + buildWorkerPending.delete(id); + pending.reject(error); + }); + } catch (error) { + clearTimeout(timer); + buildWorkerPending.delete(id); + reject(error); + } + }); + }; + + try { + return await sendAttempt(); + } catch (error) { + const message = String((error as any)?.message || error || ''); + if (!message.includes('worker unavailable') && !message.includes('worker exited')) { + throw error; } - return require('esbuild'); - } catch { - // Fallback for environments where require.resolve behaves differently. - return require('esbuild'); } + + await new Promise((resolve) => setTimeout(resolve, 90)); + return await sendAttempt(); } export interface ExtensionPreferenceSchema { @@ -680,7 +780,6 @@ export async function buildAllCommands(extName: string, extPathOverride?: string if (commands.length === 0) return 0; - const esbuild = requireEsbuild(); const extNodeModules = path.join(extPath, 'node_modules'); if (requiresNodeModules && !fs.existsSync(extNodeModules)) { try { @@ -720,7 +819,7 @@ export async function buildAllCommands(extName: string, extPathOverride?: string console.log(` Building ${extName}/${cmd.name}…`); await runEsbuildBuild( - esbuild, + null, { entryPoints: [entryFile], absWorkingDir: extPath, @@ -1009,10 +1108,9 @@ export async function buildSingleCommand(extName: string, cmdName: string): Prom } try { - const esbuild = requireEsbuild(); console.log(` On-demand building ${extName}/${cmdName}…`); await runEsbuildBuild( - esbuild, + null, { entryPoints: [entryFile], absWorkingDir: extPath, @@ -1063,60 +1161,46 @@ export async function buildSingleCommand(extName: string, cmdName: string): Prom } } -// Records the most recent build error per extension/command so that -// getExtensionBundle can surface the real cause to the user instead of -// the generic "On-demand build failed" message. -const lastBuildError = new Map(); - /** - * Parse an esbuild BuildFailure and return the list of bare-import package - * names it could not resolve. Some Raycast extensions import packages (e.g. - * `fast-glob`) without declaring them in their manifest — Raycast's `ray build` - * provides them implicitly, but esbuild bails out. When this returns a - * non-empty list the caller can install them and retry. + * Race a promise against a timeout. If the timeout fires first the + * returned promise rejects with a descriptive error. The timer is + * cleaned up on settlement so no stray timers keep the process alive. */ -function extractMissingBareImports(error: any): string[] { - const errors = Array.isArray(error?.errors) ? error.errors : []; - const found = new Set(); - for (const err of errors) { - const text = String(err?.text || ''); - const match = text.match(/Could not resolve\s+"([^"]+)"/); - if (!match) continue; - const specifier = match[1]; - // Only bare imports — ignore relative/absolute paths and scheme URLs - if ( - !specifier || - specifier.startsWith('.') || - specifier.startsWith('/') || - specifier.includes(':') - ) { - continue; - } - // Bare-package name: optional @scope/ then name. Drop any subpath. - const parts = specifier.split('/'); - const pkgName = specifier.startsWith('@') - ? parts.slice(0, 2).join('/') - : parts[0]; - if (!pkgName) continue; - // Skip things that are already external (shouldn't appear, but defensive) - if (nodeBuiltins.includes(pkgName)) continue; - if (pkgName.startsWith('@raycast/')) continue; - found.add(pkgName); +async function raceWithTimeout( + promise: Promise, + ms: number, + label: string +): Promise { + const timeoutPromise = new Promise((_, reject) => { + const id = setTimeout(() => { + reject(new Error(`Bundle load timed out after ${ms}ms for "${label}"`)); + }, ms); + _bundleGuardTimer.set(label, id); + }); + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + clearBundleGuardTimer(label); } - return [...found]; } +// Records the most recent build error per extension/command so that +// getExtensionBundle can surface the real cause to the user instead of +// the generic "On-demand build failed" message. +const lastBuildError = new Map(); + async function runEsbuildBuild( - esbuild: any, + _esbuild: any, options: any, extPath: string, label: string ): Promise { - try { - await esbuild.build(options); - } catch (error: any) { - const missing = extractMissingBareImports(error); - if (missing.length === 0) throw error; + const response = await callBuildWorker(options, label); + if (response.ok) return; + + // Worker reported missing bare imports — install them and retry. + const missing = response.missingBareImports; + if (missing && missing.length > 0) { console.log( ` Missing packages for ${label} (${missing.join(', ')}); installing and retrying…` ); @@ -1127,19 +1211,101 @@ async function runEsbuildBuild( console.error( ` Failed to install missing packages for ${label}: ${installError?.message || installError}` ); - throw error; + throw new Error(response.error); } - await esbuild.build(options); + const retry = await callBuildWorker(options, label); + if (retry.ok) return; + throw new Error(retry.error); + } + + throw new Error(response.error); +} + +// ── Re-entrancy Guard ─────────────────────────────────────────────── +// Defense-in-depth: prevents nested calls from creating a re-entrant +// event loop, even if a future code path runs sync work on the main +// thread. The primary fix is the build worker (build-worker.ts) which +// runs esbuild in its own child process. This guard adds a yield point +// on conflict and a 30s timeout as a safety net. + +const _bundleGuardDepth = new Map(); +const _bundleGuardTimer = new Map>(); +const BUNDLE_LOAD_GRACE_MS = 30_000; + +function enterBundleGuard(label: string): boolean { + const current = _bundleGuardDepth.get(label) ?? 0; + if (current >= 1) { + console.warn(`[BundleGuard] Re-entrant call blocked for "${label}"`); + return false; + } + _bundleGuardDepth.set(label, current + 1); + return true; +} + +function exitBundleGuard(label: string): void { + const current = _bundleGuardDepth.get(label) ?? 0; + if (current > 0) { + _bundleGuardDepth.set(label, current - 1); + } +} + +function clearBundleGuardTimer(label: string): void { + const timer = _bundleGuardTimer.get(label); + if (timer) { + clearTimeout(timer); + _bundleGuardTimer.delete(label); } } +async function yieldToEventLoop(): Promise { + return new Promise((resolve) => { + if (typeof setImmediate !== 'undefined') { + setImmediate(resolve); + } else { + setTimeout(resolve, 0); + } + }); +} + /** - * Get a pre-built extension command bundle. - * Falls back to on-demand building if the bundle is missing. + * Get a pre-built extension command bundle with re-entrancy protection + * and a timeout guard. + * + * Defense-in-depth: the build worker (build-worker.ts) isolates esbuild + * in a child process, so re-entrant event loop hangs should not occur. + * This guard prevents nested getExtensionBundle calls and adds a 30s + * timeout as a safety net. */ export async function getExtensionBundle( extName: string, cmdName: string +): Promise { + const label = `${normalizeExtensionName(extName)}/${cmdName}`; + + if (!enterBundleGuard(label)) { + console.warn(`[BundleGuard] Re-entrant load for "${label}", yielding before retry…`); + await yieldToEventLoop(); + if (!enterBundleGuard(label)) { + throw new Error(`[BundleGuard] Concurrent bundle load failed for "${label}"`); + } + } + + try { + const result = await raceWithTimeout( + getExtensionBundleImpl(extName, cmdName), + BUNDLE_LOAD_GRACE_MS, + label + ); + return result; + } finally { + exitBundleGuard(label); + clearBundleGuardTimer(label); + } +} + +async function getExtensionBundleImpl( + extName: string, + cmdName: string ): Promise { const normalizedExtName = normalizeExtensionName(extName); const extPath = resolveInstalledExtensionPath(normalizedExtName); diff --git a/src/main/main.ts b/src/main/main.ts index 343efc97..26974aaf 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -59,7 +59,7 @@ import { getPopularExtensions, getExtensionDetails, } from './extension-api'; -import { getExtensionBundle, buildAllCommands, discoverInstalledExtensionCommands, getInstalledExtensionsSettingsSchema } from './extension-runner'; +import { getExtensionBundle, buildAllCommands, discoverInstalledExtensionCommands, getInstalledExtensionsSettingsSchema, stopBuildWorker } from './extension-runner'; import { startClipboardMonitor, stopClipboardMonitor, @@ -17967,6 +17967,7 @@ app.on('will-quit', () => { stopEmojiTriggerMonitor(); stopFileSearchIndexing(); try { soulverCalculator.shutdown(); } catch {} + stopBuildWorker(); if (appTray) { try { appTray.destroy(); } catch {} appTray = null; diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..08b28a62 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.ts'], + exclude: ['node_modules', 'dist'], + globals: true, + environment: 'node', + testTimeout: 30_000, + }, +});