diff --git a/package.json b/package.json index 23c128d..c75190c 100644 --- a/package.json +++ b/package.json @@ -47,5 +47,11 @@ "typescript": "^5.3.3", "ultracite": "7.7.0", "vitest": "^3.2.0" + }, + "pnpm": { + "overrides": { + "vite": "^7.3.5", + "esbuild": "^0.28.1" + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e0cb2a..0146088 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,10 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + vite: ^7.3.5 + esbuild: ^0.28.1 + importers: .: @@ -105,158 +109,158 @@ packages: resolution: {integrity: sha512-V/SdHS0DV9L8gBJc5ljQbTmbgV7C8Cn7V91qn3JfnHKgvSmuUq2kLH27UPgCbAa0T2ZVXOkBzbIog5ZnOzAUMg==} engines: {node: '>=18.0.0'} - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -549,7 +553,7 @@ packages: resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + vite: ^7.3.5 peerDependenciesMeta: msw: optional: true @@ -626,8 +630,8 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -824,8 +828,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@7.3.3: - resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -963,82 +967,82 @@ snapshots: dependencies: zod: 4.4.3 - '@esbuild/aix-ppc64@0.27.7': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.7': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.7': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.7': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.7': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.7': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.7': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.7': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.7': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.7': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.7': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.7': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.7': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.7': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.7': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.7': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.7': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.7': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.7': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.7': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.7': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.7': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.7': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.7': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.7': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.7': + '@esbuild/win32-x64@0.28.1': optional: true '@jridgewell/sourcemap-codec@1.5.5': {} @@ -1200,13 +1204,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.6(vite@7.3.3(@types/node@25.9.1)(yaml@2.9.0))': + '@vitest/mocker@3.2.6(vite@7.3.6(@types/node@25.9.1)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.3(@types/node@25.9.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@25.9.1)(yaml@2.9.0) '@vitest/pretty-format@3.2.6': dependencies: @@ -1274,34 +1278,34 @@ snapshots: es-module-lexer@1.7.0: {} - esbuild@0.27.7: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 estree-walker@3.0.3: dependencies: @@ -1498,7 +1502,7 @@ snapshots: debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.3(@types/node@25.9.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@25.9.1)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -1513,9 +1517,9 @@ snapshots: - tsx - yaml - vite@7.3.3(@types/node@25.9.1)(yaml@2.9.0): + vite@7.3.6(@types/node@25.9.1)(yaml@2.9.0): dependencies: - esbuild: 0.27.7 + esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.15 @@ -1530,7 +1534,7 @@ snapshots: dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.6 - '@vitest/mocker': 3.2.6(vite@7.3.3(@types/node@25.9.1)(yaml@2.9.0)) + '@vitest/mocker': 3.2.6(vite@7.3.6(@types/node@25.9.1)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.6 '@vitest/runner': 3.2.6 '@vitest/snapshot': 3.2.6 @@ -1548,7 +1552,7 @@ snapshots: tinyglobby: 0.2.16 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.3(@types/node@25.9.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@25.9.1)(yaml@2.9.0) vite-node: 3.2.4(@types/node@25.9.1)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: diff --git a/src/output/errors/scrape-failed-error.ts b/src/output/errors/scrape-failed-error.ts new file mode 100644 index 0000000..5b4b81b --- /dev/null +++ b/src/output/errors/scrape-failed-error.ts @@ -0,0 +1,9 @@ +export class ScrapeFailedError extends Error { + readonly statusCode: number | undefined; + + constructor(message: string, statusCode?: number) { + super(message); + this.name = "ScrapeFailedError"; + this.statusCode = statusCode; + } +} diff --git a/src/output/services/detect-scrape-failure.ts b/src/output/services/detect-scrape-failure.ts new file mode 100644 index 0000000..46d598f --- /dev/null +++ b/src/output/services/detect-scrape-failure.ts @@ -0,0 +1,122 @@ +import type { ResultEntry, SyncResponse } from "@decodo/sdk-ts"; +import { ScrapeFailedError } from "../errors/scrape-failed-error.js"; + +const HTTP_ERROR_THRESHOLD = 400; + +function readString( + source: Record, + key: string +): string | undefined { + const value = source[key]; + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +function readNumber( + source: Record, + key: string +): number | undefined { + const value = source[key]; + return typeof value === "number" ? value : undefined; +} + +function failureFromEnvelope(value: unknown): ScrapeFailedError | undefined { + if (typeof value !== "object" || value === null) { + return; + } + + const envelope = value as Record; + if (envelope.status !== "failed") { + return; + } + + const statusCode = readNumber(envelope, "status_code"); + const message = + readString(envelope, "message") ?? + (statusCode === undefined + ? "Scrape failed." + : `Scrape failed with status ${statusCode}.`); + + return new ScrapeFailedError(message, statusCode); +} + +function hasUsableContent(content: unknown): boolean { + if (content === undefined || content === null) { + return false; + } + + if (typeof content === "string") { + return content.trim().length > 0; + } + + if (Array.isArray(content)) { + return content.length > 0; + } + + if (typeof content === "object") { + const envelope = content as Record; + if (Array.isArray(envelope.results)) { + return envelope.results.length > 0; + } + return Object.keys(envelope).length > 0; + } + + return true; +} + +function failureFromStatus(entry: ResultEntry): ScrapeFailedError | undefined { + const { status_code: statusCode } = entry; + if (typeof statusCode !== "number" || statusCode < HTTP_ERROR_THRESHOLD) { + return; + } + + if (hasUsableContent(entry.content)) { + return; + } + + const message = + (typeof entry.content === "object" && entry.content !== null + ? readString(entry.content as Record, "message") + : undefined) ?? `Scrape request returned status ${statusCode}.`; + + return new ScrapeFailedError(message, statusCode); +} + +function readResults(response: SyncResponse): ResultEntry[] { + return Array.isArray(response.results) ? response.results : []; +} + +export function detectScrapeFailure( + response: SyncResponse +): ScrapeFailedError | undefined { + const topLevelFailure = failureFromEnvelope(response); + if (topLevelFailure) { + return topLevelFailure; + } + + for (const entry of readResults(response)) { + const failure = + failureFromEnvelope(entry.content) ?? failureFromStatus(entry); + if (failure) { + return failure; + } + } + + return; +} + +export function detectDegradedStatus( + response: SyncResponse +): number | undefined { + for (const entry of readResults(response)) { + const { status_code: statusCode } = entry; + if ( + typeof statusCode === "number" && + statusCode >= HTTP_ERROR_THRESHOLD && + hasUsableContent(entry.content) + ) { + return statusCode; + } + } + + return; +} diff --git a/src/output/services/write-scrape-response.ts b/src/output/services/write-scrape-response.ts index 7f682c4..2be4d04 100644 --- a/src/output/services/write-scrape-response.ts +++ b/src/output/services/write-scrape-response.ts @@ -3,6 +3,10 @@ import { writeBinaryOutput } from "../../platform/services/write-binary.js"; import { extractPngFromResponse } from "../../scrape/services/extract-png.js"; import { defaultScreenshotFilename } from "../../scrape/services/screenshot-output-filename.js"; import type { WriteScrapeResponseContext } from "../types/write-scrape-response.js"; +import { + detectDegradedStatus, + detectScrapeFailure, +} from "./detect-scrape-failure.js"; import { extractPayload } from "./extract-payload.js"; import { renderPayload } from "./render-output.js"; import { resolvePrettyIndent } from "./resolve-pretty.js"; @@ -15,6 +19,18 @@ export function writeScrapeResponse( ): void { const { options } = context; + const failure = detectScrapeFailure(response); + if (failure) { + throw failure; + } + + const degradedStatus = detectDegradedStatus(response); + if (degradedStatus !== undefined) { + console.error( + `Warning: target returned HTTP ${degradedStatus}; emitting the content it returned` + ); + } + if (context.binary?.kind === "png") { writeBinaryOutput(extractPngFromResponse(response), { output: options.output, diff --git a/src/platform/services/handle-cli-error.ts b/src/platform/services/handle-cli-error.ts index a5789dd..f5c45bd 100644 --- a/src/platform/services/handle-cli-error.ts +++ b/src/platform/services/handle-cli-error.ts @@ -7,6 +7,7 @@ import { } from "@decodo/sdk-ts"; import { PLAYGROUND_URL } from "../../auth/constants.js"; import { AuthRequiredError } from "../../auth/errors/auth-required-error.js"; +import { ScrapeFailedError } from "../../output/errors/scrape-failed-error.js"; import { EXIT } from "../constants.js"; import { CliUsageError } from "../errors/cli-usage-error.js"; @@ -63,7 +64,14 @@ export function resolveCliExitCode(err: unknown): number { return EXIT.TIMEOUT; } + if (err instanceof ScrapeFailedError) { + return EXIT.NETWORK; + } + if (err instanceof DecodoError) { + if (err.statusCode === 400 || err.statusCode === 422) { + return EXIT.VALIDATION; + } return EXIT.NETWORK; } diff --git a/tests/output/errors/scrape-failed-error.test.ts b/tests/output/errors/scrape-failed-error.test.ts new file mode 100644 index 0000000..3d0ce4e --- /dev/null +++ b/tests/output/errors/scrape-failed-error.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { ScrapeFailedError } from "../../../src/output/errors/scrape-failed-error.js"; + +describe("ScrapeFailedError", () => { + it("sets name, message, and status code", () => { + const err = new ScrapeFailedError("scrape failed", 613); + + expect(err).toBeInstanceOf(Error); + expect(err.name).toBe("ScrapeFailedError"); + expect(err.message).toBe("scrape failed"); + expect(err.statusCode).toBe(613); + }); + + it("leaves status code undefined when omitted", () => { + const err = new ScrapeFailedError("scrape failed"); + + expect(err.statusCode).toBeUndefined(); + }); +}); diff --git a/tests/output/services/detect-scrape-failure.test.ts b/tests/output/services/detect-scrape-failure.test.ts new file mode 100644 index 0000000..100f5c8 --- /dev/null +++ b/tests/output/services/detect-scrape-failure.test.ts @@ -0,0 +1,147 @@ +import type { SyncResponse } from "@decodo/sdk-ts"; +import { describe, expect, it } from "vitest"; +import { + detectDegradedStatus, + detectScrapeFailure, +} from "../../../src/output/services/detect-scrape-failure.js"; + +describe("detectScrapeFailure", () => { + it("returns undefined for a successful response", () => { + const response = { + results: [{ content: { items: [1] }, status_code: 200 }], + } as SyncResponse; + + expect(detectScrapeFailure(response)).toBeUndefined(); + }); + + it("flags a failed content envelope and surfaces its message", () => { + const response = { + results: [ + { + content: { + status: "failed", + status_code: 613, + message: "Trends request was rejected", + }, + }, + ], + } as unknown as SyncResponse; + + const failure = detectScrapeFailure(response); + + expect(failure?.message).toBe("Trends request was rejected"); + expect(failure?.statusCode).toBe(613); + }); + + it("flags a failed envelope without a message using its status code", () => { + const response = { + results: [{ content: { status: "failed", status_code: 12_002 } }], + } as unknown as SyncResponse; + + const failure = detectScrapeFailure(response); + + expect(failure?.message).toContain("12002"); + expect(failure?.statusCode).toBe(12_002); + }); + + it("flags an entry whose status code is an HTTP error", () => { + const response = { + results: [ + { + content: { results: [], _warnings: ["not found"] }, + status_code: 404, + }, + ], + } as unknown as SyncResponse; + + const failure = detectScrapeFailure(response); + + expect(failure?.message).toContain("404"); + expect(failure?.statusCode).toBe(404); + }); + + it("ignores entries that omit a status code", () => { + const response = { + results: [{ content: { ok: true } }], + } as SyncResponse; + + expect(detectScrapeFailure(response)).toBeUndefined(); + }); + + it("does not flag an HTTP error that still returned a body", () => { + const response = { + results: [{ content: "404 Not Found", status_code: 404 }], + } as unknown as SyncResponse; + + expect(detectScrapeFailure(response)).toBeUndefined(); + }); + + it("returns the first failure across multiple results", () => { + const response = { + results: [ + { content: { ok: true }, status_code: 200 }, + { + content: { + status: "failed", + status_code: 613, + message: "second failed", + }, + }, + ], + } as unknown as SyncResponse; + + expect(detectScrapeFailure(response)?.message).toBe("second failed"); + }); + + it("flags a top-level failure envelope that omits results", () => { + const response = { + status: "failed", + status_code: 613, + message: "We were not able to scrape the target.", + task_id: "7476155848721478657", + } as unknown as SyncResponse; + + const failure = detectScrapeFailure(response); + + expect(failure?.message).toBe("We were not able to scrape the target."); + expect(failure?.statusCode).toBe(613); + }); + + it("does not throw when results is missing", () => { + const response = {} as unknown as SyncResponse; + + expect(detectScrapeFailure(response)).toBeUndefined(); + }); +}); + +describe("detectDegradedStatus", () => { + it("returns undefined for a successful response", () => { + const response = { + results: [{ content: "ok", status_code: 200 }], + } as unknown as SyncResponse; + + expect(detectDegradedStatus(response)).toBeUndefined(); + }); + + it("returns the status code when an HTTP error still returned a body", () => { + const response = { + results: [{ content: "404 Not Found", status_code: 404 }], + } as unknown as SyncResponse; + + expect(detectDegradedStatus(response)).toBe(404); + }); + + it("returns undefined when an HTTP error returned no usable content", () => { + const response = { + results: [{ content: { results: [] }, status_code: 404 }], + } as unknown as SyncResponse; + + expect(detectDegradedStatus(response)).toBeUndefined(); + }); + + it("does not throw when results is missing", () => { + const response = {} as unknown as SyncResponse; + + expect(detectDegradedStatus(response)).toBeUndefined(); + }); +}); diff --git a/tests/output/services/write-scrape-response.test.ts b/tests/output/services/write-scrape-response.test.ts index ad686b6..879bd86 100644 --- a/tests/output/services/write-scrape-response.test.ts +++ b/tests/output/services/write-scrape-response.test.ts @@ -133,6 +133,47 @@ describe("writeScrapeResponse", () => { expect(stderr).toEqual([]); }); + it("throws on a failed content envelope without writing to stdout", () => { + const response = { + results: [ + { + content: { + status: "failed", + status_code: 12_002, + message: "blocked", + }, + }, + ], + } as unknown as SyncResponse; + + expect(() => writeScrapeResponse(response, { options: {} })).toThrow( + "blocked" + ); + expect(written).toBeUndefined(); + }); + + it("throws on an HTTP error status even with --full", () => { + const response = { + results: [{ content: { results: [] }, status_code: 404 }], + } as unknown as SyncResponse; + + expect(() => + writeScrapeResponse(response, { options: { full: true } }) + ).toThrow("404"); + expect(written).toBeUndefined(); + }); + + it("emits content and warns when an HTTP error still returned a body", () => { + const response = { + results: [{ content: "404 Not Found", status_code: 404 }], + } as unknown as SyncResponse; + + writeScrapeResponse(response, { options: {} }); + + expect(written).toBe("404 Not Found\n"); + expect(stderr.join("\n")).toContain("HTTP 404"); + }); + it("refuses TTY stdout for binary png without -o", () => { Object.defineProperty(process.stdout, "isTTY", { value: true, diff --git a/tests/platform/services/handle-cli-error.test.ts b/tests/platform/services/handle-cli-error.test.ts index f83f7e3..b0ca8b1 100644 --- a/tests/platform/services/handle-cli-error.test.ts +++ b/tests/platform/services/handle-cli-error.test.ts @@ -7,6 +7,7 @@ import { } from "@decodo/sdk-ts"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AuthRequiredError } from "../../../src/auth/errors/auth-required-error.js"; +import { ScrapeFailedError } from "../../../src/output/errors/scrape-failed-error.js"; import { CliUsageError } from "../../../src/platform/errors/cli-usage-error.js"; import { handleCliError } from "../../../src/platform/services/handle-cli-error.js"; @@ -88,6 +89,28 @@ describe("handleCliError", () => { expect(exitCode).toBe(7); }); + it("maps 400 Decodo errors to exit code 4 (validation)", () => { + const err = new DecodoError("Validation failed, see documentation", 400); + + expect(() => handleCliError(err)).toThrow("process.exit:4"); + expect(exitCode).toBe(4); + }); + + it("maps 422 Decodo errors to exit code 4 (validation)", () => { + const err = new DecodoError("Unprocessable entity", 422); + + expect(() => handleCliError(err)).toThrow("process.exit:4"); + expect(exitCode).toBe(4); + }); + + it("maps scrape failures to exit code 7 with the API message", () => { + const err = new ScrapeFailedError("Trends request was rejected", 613); + + expect(() => handleCliError(err)).toThrow("process.exit:7"); + expect(exitCode).toBe(7); + expect(stderr.join("\n")).toContain("Trends request was rejected"); + }); + it("maps syscall-coded network failures in the cause chain to exit code 7", () => { const cause = Object.assign( new Error("getaddrinfo ENOTFOUND api.decodo.com"),