diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml index bac068ca..e564f1f3 100644 --- a/.github/workflows/release-please.yaml +++ b/.github/workflows/release-please.yaml @@ -70,6 +70,10 @@ jobs: if: ${{ steps.release.outputs['packages/headers--release_created'] }} env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + - run: npm publish packages/images/ --provenance --access=public + if: ${{ steps.release.outputs['packages/images--release_created'] }} + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - run: npm publish packages/redirects/ --provenance --access=public if: ${{ steps.release.outputs['packages/redirects--release_created'] }} env: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7d163d62..ec51e6a5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -6,6 +6,7 @@ "packages/edge-functions": "2.14.3", "packages/functions": "4.1.3", "packages/headers": "2.0.1", + "packages/images": "0.0.0", "packages/otel": "3.0.2", "packages/redirects": "3.0.1", "packages/runtime": "4.0.2", diff --git a/README.md b/README.md index 6900504a..d4b9e34c 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ npm run build --workspaces=true | πŸ”§ [@netlify/dev-utils](packages/dev-utils) | TypeScript utilities for the local emulation of the Netlify environment | [![npm version](https://img.shields.io/npm/v/@netlify/dev-utils.svg)](https://www.npmjs.com/package/@netlify/dev-utils) | | ⚑ [@netlify/functions](packages/functions) | TypeScript utilities for interacting with Netlify Functions | [![npm version](https://img.shields.io/npm/v/@netlify/functions.svg)](https://www.npmjs.com/package/@netlify/functions) | | πŸ“‹ [@netlify/headers](packages/headers) | TypeScript implementation of Netlify's headers engine | [![npm version](https://img.shields.io/npm/v/@netlify/headers.svg)](https://www.npmjs.com/package/@netlify/headers) | +| πŸ–ΌοΈ [@netlify/images](packages/images) | TypeScript utilities for interacting with Netlify Image CDN | [![npm version](https://img.shields.io/npm/v/@netlify/images.svg)](https://www.npmjs.com/package/@netlify/images) | | πŸ” [@netlify/otel](packages/otel) | TypeScript utilities to interact with Netlify's OpenTelemetry | [![npm version](https://img.shields.io/npm/v/@netlify/otel.svg)](https://www.npmjs.com/package/@netlify/otel) | | πŸ”„ [@netlify/redirects](packages/redirects) | TypeScript implementation of Netlify's rewrites and redirects engine | [![npm version](https://img.shields.io/npm/v/@netlify/redirects.svg)](https://www.npmjs.com/package/@netlify/redirects) | | πŸ›οΈ [@netlify/runtime](packages/runtime) | Netlify compute runtime | [![npm version](https://img.shields.io/npm/v/@netlify/runtime.svg)](https://www.npmjs.com/package/@netlify/runtime) | diff --git a/eslint_temporary_suppressions.js b/eslint_temporary_suppressions.js index 6efd95c6..d3f459f4 100644 --- a/eslint_temporary_suppressions.js +++ b/eslint_temporary_suppressions.js @@ -539,6 +539,14 @@ export default [ '@typescript-eslint/no-unused-vars': 'off', }, }, + { + files: ['packages/images/src/main.test.ts'], + rules: { + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + }, + }, { files: ['packages/otel/src/bootstrap/main.ts'], rules: { diff --git a/package-lock.json b/package-lock.json index 7333ba29..8f4ddadb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "packages/edge-functions", "packages/functions", "packages/headers", + "packages/images", "packages/redirects", "packages/runtime", "packages/static", @@ -130,6 +131,16 @@ "node": ">=18" } }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@envelop/instrumentation": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@envelop/instrumentation/-/instrumentation-1.0.0.tgz", @@ -747,6 +758,22 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/@fastify/busboy": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", @@ -834,6 +861,367 @@ "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", "license": "ISC" }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@import-maps/resolve": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@import-maps/resolve/-/resolve-2.0.0.tgz", @@ -1439,6 +1827,10 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/@netlify/images": { + "resolved": "packages/images", + "link": true + }, "node_modules/@netlify/open-api": { "version": "2.37.0", "resolved": "https://registry.npmjs.org/@netlify/open-api/-/open-api-2.37.0.tgz", @@ -1720,87 +2112,420 @@ "@opentelemetry/core": "1.30.1" }, "engines": { - "node": ">=14" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.1.tgz", + "integrity": "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", + "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.1.tgz", + "integrity": "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "1.30.1", + "@opentelemetry/core": "1.30.1", + "@opentelemetry/propagator-b3": "1.30.1", + "@opentelemetry/propagator-jaeger": "1.30.1", + "@opentelemetry/sdk-trace-base": "1.30.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", + "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@opentelemetry/propagator-jaeger": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.1.tgz", - "integrity": "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "1.30.1" - }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" + "node": ">= 10.0.0" }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@opentelemetry/resources": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", - "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", - "license": "Apache-2.0", + "node_modules/@parcel/watcher-wasm": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-wasm/-/watcher-wasm-2.5.1.tgz", + "integrity": "sha512-RJxlQQLkaMMIuWRozy+z2vEqbaQlCuaCgVZIUCzQLYggY22LZbP5Y1+ia+FD724Ids9e+XIyOLXLrLgQSHIthw==", + "bundleDependencies": [ + "napi-wasm" + ], + "license": "MIT", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "napi-wasm": "^1.1.0" }, "engines": { - "node": ">=14" + "node": ">= 10.0.0" }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@opentelemetry/sdk-trace-node": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.1.tgz", - "integrity": "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/context-async-hooks": "1.30.1", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/propagator-b3": "1.30.1", - "@opentelemetry/propagator-jaeger": "1.30.1", - "@opentelemetry/sdk-trace-base": "1.30.1", - "semver": "^7.5.2" - }, + "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" + "node": ">= 10.0.0" }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", - "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" + "node": ">= 10.0.0" }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, "engines": { - "node": ">=14" + "node": ">=0.10" } }, "node_modules/@pkgjs/parseargs": { @@ -2218,6 +2943,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@tsd/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.8.3.tgz", @@ -3111,6 +3845,19 @@ "dev": true, "license": "MIT" }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/archiver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", @@ -3291,6 +4038,12 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -3550,6 +4303,32 @@ "node": ">=18" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/clipboardy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz", + "integrity": "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==", + "license": "MIT", + "dependencies": { + "execa": "^8.0.1", + "is-wsl": "^3.1.0", + "is64bit": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3669,7 +4448,6 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, "license": "MIT" }, "node_modules/consola": { @@ -3690,6 +4468,12 @@ "node": ">=18" } }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "license": "MIT" + }, "node_modules/copy-file": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/copy-file/-/copy-file-11.0.0.tgz", @@ -3763,6 +4547,95 @@ "node": ">= 8" } }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -3861,6 +4734,18 @@ "node": ">=0.10.0" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -4035,6 +4920,61 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-prop": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", @@ -4653,6 +5593,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -5118,6 +6067,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-port-please": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", + "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", + "license": "MIT" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -5253,6 +6208,23 @@ "dev": true, "license": "MIT" }, + "node_modules/h3": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.3.tgz", + "integrity": "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": "^0.3.4", + "defu": "^6.1.4", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.0", + "radix3": "^1.1.2", + "ufo": "^1.6.1", + "uncrypto": "^0.1.3" + } + }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -5308,6 +6280,16 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/http-shutdown": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", + "integrity": "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -5373,7 +6355,25 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": ">= 4" + } + }, + "node_modules/image-meta": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.1.tgz", + "integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==", + "license": "MIT" + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" } }, "node_modules/import-fresh": { @@ -5432,6 +6432,145 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ipx": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ipx/-/ipx-3.0.3.tgz", + "integrity": "sha512-c8ZWrM9Rzf8C/W1WoBb9KJ73C76+s3xyBL4iS5WdlPVIObE14tKKW79JIWbMkzhPZw71ZL/mLRMSvQOOhwbj0Q==", + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.1", + "citty": "^0.1.6", + "consola": "^3.4.2", + "defu": "^6.1.4", + "destr": "^2.0.3", + "etag": "^1.8.1", + "h3": "^1.15.1", + "image-meta": "^0.2.1", + "listhen": "^1.9.0", + "ofetch": "^1.4.1", + "pathe": "^2.0.3", + "sharp": "^0.33.5", + "svgo": "^3.3.2", + "ufo": "^1.5.4", + "unstorage": "^1.15.0", + "xss": "^1.0.15" + }, + "bin": { + "ipx": "bin/ipx.mjs" + } + }, + "node_modules/ipx/node_modules/@netlify/blobs": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-8.2.0.tgz", + "integrity": "sha512-9djLZHBKsoKk8XCgwWSEPK9QnT8qqxEQGuYh48gFIcNLvpBKkLnHbDZuyUxmNemCfDz7h0HnMXgSPnnUVgARhg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^14.16.0 || >=16.0.0" + } + }, + "node_modules/ipx/node_modules/unstorage": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.16.0.tgz", + "integrity": "sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.2", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.6", + "ofetch": "^1.4.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, "node_modules/irregular-plurals": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", @@ -5478,6 +6617,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5508,6 +6662,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-network-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", @@ -5595,6 +6767,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is64bit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz", + "integrity": "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==", + "license": "MIT", + "dependencies": { + "system-architecture": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -5648,6 +6850,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -5658,6 +6869,21 @@ "node": ">=10" } }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/js-image-generator": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/js-image-generator/-/js-image-generator-1.0.4.tgz", + "integrity": "sha512-ckb7kyVojGAnArouVR+5lBIuwU1fcrn7E/YYSd0FK7oIngAkMmRvHASLro9Zt5SQdWToaI66NybG+OGxPw/HlQ==", + "license": "ISC", + "dependencies": { + "jpeg-js": "^0.4.2" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5907,6 +7133,42 @@ "dev": true, "license": "MIT" }, + "node_modules/listhen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.9.0.tgz", + "integrity": "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==", + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.4.1", + "@parcel/watcher-wasm": "^2.4.1", + "citty": "^0.1.6", + "clipboardy": "^4.0.0", + "consola": "^3.2.3", + "crossws": ">=0.2.0 <0.4.0", + "defu": "^6.1.4", + "get-port-please": "^3.1.2", + "h3": "^1.12.0", + "http-shutdown": "^1.2.2", + "jiti": "^2.1.2", + "mlly": "^1.7.1", + "node-forge": "^1.3.1", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "ufo": "^1.5.4", + "untun": "^0.1.3", + "uqr": "^0.1.2" + }, + "bin": { + "listen": "bin/listhen.mjs", + "listhen": "bin/listhen.mjs" + } + }, + "node_modules/listhen/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "license": "MIT" + }, "node_modules/load-tsconfig": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", @@ -6112,6 +7374,12 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -6397,7 +7665,6 @@ "version": "1.7.4", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", - "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.14.0", @@ -6471,6 +7738,12 @@ "integrity": "sha512-4zdzIP+6muqPCuE8avnrgDJ6KW/2+UpHTRcTbMXCIRxiRmyrX+IZ4WSJGZdHPWF3WmQpXpy603XxecZ9iygN7w==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -6509,6 +7782,21 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-fetch-native": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz", + "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -6520,6 +7808,12 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-mock-http": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.0.tgz", + "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", + "license": "MIT" + }, "node_modules/node-source-walk": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-7.0.1.tgz", @@ -6686,6 +7980,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6708,6 +8014,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ofetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", + "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.3", + "node-fetch-native": "^1.6.4", + "ufo": "^1.5.4" + } + }, "node_modules/omit.js": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/omit.js/-/omit.js-2.0.2.tgz", @@ -6993,7 +8310,6 @@ "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/pathval": { @@ -7057,7 +8373,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, "license": "MIT", "dependencies": { "confbox": "^0.1.8", @@ -7402,6 +8717,12 @@ "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", "license": "MIT" }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7910,6 +9231,58 @@ "node": ">=10" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8140,7 +9513,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "dev": true, "license": "MIT" }, "node_modules/streamx": { @@ -8349,6 +9721,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/system-architecture": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", + "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tapable": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", @@ -8844,7 +10262,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, "license": "MIT" }, "node_modules/ulid": { @@ -8856,6 +10273,12 @@ "ulid": "dist/cli.js" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -8898,6 +10321,32 @@ "node": ">=0.10.0" } }, + "node_modules/untun": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/untun/-/untun-0.1.3.tgz", + "integrity": "sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.5", + "consola": "^3.2.3", + "pathe": "^1.1.1" + }, + "bin": { + "untun": "bin/untun.mjs" + } + }, + "node_modules/untun/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "license": "MIT" + }, + "node_modules/uqr": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", + "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==", + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -9351,6 +10800,28 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -9593,6 +11064,7 @@ "@netlify/edge-functions": "2.14.3", "@netlify/functions": "4.1.3", "@netlify/headers": "2.0.1", + "@netlify/images": "0.0.0", "@netlify/redirects": "3.0.1", "@netlify/runtime": "4.0.2", "@netlify/static": "3.0.1", @@ -9620,6 +11092,8 @@ "dot-prop": "9.0.0", "env-paths": "^3.0.0", "find-up": "7.0.0", + "image-size": "^2.0.2", + "js-image-generator": "^1.0.4", "lodash.debounce": "^4.0.8", "parse-gitignore": "^2.0.0", "uuid": "^11.1.0", @@ -9986,6 +11460,22 @@ "dev": true, "license": "MIT" }, + "packages/images": { + "name": "@netlify/images", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "ipx": "^3.0.3" + }, + "devDependencies": { + "@netlify/dev-utils": "^3.1.1", + "tsup": "^8.5.0", + "vitest": "^3.1.4" + }, + "engines": { + "node": ">=20.6.1" + } + }, "packages/otel": { "name": "@netlify/otel", "version": "3.0.2", diff --git a/package.json b/package.json index 0ab50eb6..f08bb09a 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "packages/edge-functions", "packages/functions", "packages/headers", + "packages/images", "packages/redirects", "packages/runtime", "packages/static", diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 2fcb2fdd..f6a5209a 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -58,6 +58,8 @@ "dot-prop": "9.0.0", "env-paths": "^3.0.0", "find-up": "7.0.0", + "js-image-generator": "^1.0.4", + "image-size": "^2.0.2", "lodash.debounce": "^4.0.8", "parse-gitignore": "^2.0.0", "uuid": "^11.1.0", diff --git a/packages/dev-utils/src/main.ts b/packages/dev-utils/src/main.ts index 62735563..9cdc8220 100644 --- a/packages/dev-utils/src/main.ts +++ b/packages/dev-utils/src/main.ts @@ -16,4 +16,5 @@ export { watchDebounced } from './lib/watch-debounced.js' export { EventInspector } from './test/event_inspector.js' export { MockFetch } from './test/fetch.js' export { Fixture } from './test/fixture.js' +export { createImageServerHandler, generateImage, getImageResponseSize } from './test/image.js' export { createMockLogger } from './test/logger.js' diff --git a/packages/dev-utils/src/test/fixture.ts b/packages/dev-utils/src/test/fixture.ts index 72c66cfd..25b310ef 100644 --- a/packages/dev-utils/src/test/fixture.ts +++ b/packages/dev-utils/src/test/fixture.ts @@ -9,7 +9,7 @@ import tmp from 'tmp-promise' const run = promisify(exec) export class Fixture { directory?: tmp.DirectoryResult - files: Record + files: Record npmDependencies: Record constructor() { @@ -87,7 +87,7 @@ export class Fixture { await fs.rm(this.directory!.path, { force: true, recursive: true }) } - withFile(path: string, contents: string) { + withFile(path: string, contents: string | Buffer) { this.files[path] = contents return this diff --git a/packages/dev-utils/src/test/image.ts b/packages/dev-utils/src/test/image.ts new file mode 100644 index 00000000..e5913d20 --- /dev/null +++ b/packages/dev-utils/src/test/image.ts @@ -0,0 +1,55 @@ +import { imageSize } from 'image-size' +import { generateImage as generateImageCallback } from 'js-image-generator' + +/** + * Returns Buffer of a generated random noise jpeg image with the specified width and height. + */ +export async function generateImage(width: number, height: number): Promise { + return new Promise((resolve, reject) => { + generateImageCallback(width, height, 80, (error, image) => { + if (error) { + reject(error) + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const imageBuffer = image.data as Buffer + + resolve(imageBuffer) + } + }) + }) +} + +/** + * Helper to create a server handler that responds with a random noise image. + */ +export function createImageServerHandler(imageConfigFromURL: (url: URL) => { width: number; height: number } | null) { + return async (request: Request): Promise => { + const url = new URL(request.url) + + const imageConfig = imageConfigFromURL(url) + + if (!imageConfig) { + return new Response('Not Found', { status: 404, headers: { 'Content-Type': 'text/plain' } }) + } + + try { + const imageBuffer = await generateImage(imageConfig.width, imageConfig.height) + return new Response(imageBuffer, { + headers: { + 'Content-Type': 'image/jpeg', + 'Content-Length': imageBuffer.length.toString(), + }, + }) + } catch (error) { + console.log('Error generating image', error) + return new Response('Error generating image', { status: 500 }) + } + } +} + +export async function getImageResponseSize(response: Response) { + if (!response.headers.get('content-type')?.startsWith('image/')) { + throw new Error('Response is not an image') + } + return imageSize(new Uint8Array(await response.arrayBuffer())) +} diff --git a/packages/dev/README.md b/packages/dev/README.md index cbb7fb49..a1c8e79a 100644 --- a/packages/dev/README.md +++ b/packages/dev/README.md @@ -8,7 +8,7 @@ users, it is primarily designed as a foundational library for higher-level tools [Netlify Vite Plugin](https://docs.netlify.com/integrations/vite/overview/). It provides a local request pipeline that mimics the Netlify platform’s request handling, including support for -Functions, Blobs, Static files, and Redirects. +Functions, Blobs, Static files, Redirects, and Image CDN. ## 🚧 Feature Support @@ -21,7 +21,7 @@ Functions, Blobs, Static files, and Redirects. | Redirects and Rewrites | βœ… Yes | | Headers | βœ… Yes | | Environment Variables | βœ… Yes | -| Image CDN | ❌ No | +| Image CDN | βœ… Yes | > Note: Missing features will be added incrementally. This module is **not** intended to be a full replacement for the > Netlify CLI. diff --git a/packages/dev/package.json b/packages/dev/package.json index 7b87f80d..8ef88792 100644 --- a/packages/dev/package.json +++ b/packages/dev/package.json @@ -58,6 +58,7 @@ "@netlify/edge-functions": "2.14.3", "@netlify/functions": "4.1.3", "@netlify/headers": "2.0.1", + "@netlify/images": "0.0.0", "@netlify/redirects": "3.0.1", "@netlify/runtime": "4.0.2", "@netlify/static": "3.0.1", diff --git a/packages/dev/src/main.test.ts b/packages/dev/src/main.test.ts index 7d17c921..e35a726a 100644 --- a/packages/dev/src/main.test.ts +++ b/packages/dev/src/main.test.ts @@ -1,7 +1,7 @@ import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' -import { Fixture } from '@netlify/dev-utils' +import { createImageServerHandler, Fixture, generateImage, getImageResponseSize, HTTPServer } from '@netlify/dev-utils' import { describe, expect, test } from 'vitest' import { isFile } from './lib/fs.js' @@ -447,6 +447,92 @@ describe('Handling requests', () => { await dev.stop() await fixture.destroy() }) + + test('Image CDN requests are supported', async () => { + const IMAGE_WIDTH = 800 + const IMAGE_HEIGHT = 400 + + const remoteServer = new HTTPServer( + createImageServerHandler(() => { + return { width: IMAGE_WIDTH, height: IMAGE_HEIGHT } + }), + ) + + const remoteServerAddress = await remoteServer.start() + + const fixture = new Fixture() + .withFile( + 'netlify.toml', + `[images] + remote_images = [ + "^${remoteServerAddress}/allowed/.*" + ] + + [[redirects]] + from = "/image-cdn-rewrite" + to = "/.netlify/images?url=:url&w=:width" + status = 200 + + [redirects.query] + url = ":url" + w = ":width"`, + ) + .withFile('local/image.jpg', await generateImage(IMAGE_WIDTH, IMAGE_HEIGHT)) + + const directory = await fixture.create() + + const dev = new NetlifyDev({ + projectRoot: directory, + edgeFunctions: { + // disable edge functions to avoid relying on edge functions handling spinning up internal server + // for local images + enabled: false, + }, + }) + + await dev.start() + + const localImageRequest = new Request( + `https://site.netlify/.netlify/images?url=${encodeURIComponent('local/image.jpg')}&w=100`, + ) + const localImageResponse = await dev.handle(localImageRequest) + expect(localImageResponse?.ok).toBe(true) + expect(localImageResponse?.headers.get('content-type')).toMatch(/^image\//) + expect(await getImageResponseSize(localImageResponse ?? new Response('No @netlify/dev response'))).toMatchObject({ + width: 100, + height: 50, + }) + + const allowedRemoteImageRequest = new Request( + `https://site.netlify/.netlify/images?url=${encodeURIComponent(`${remoteServerAddress}/allowed/image`)}&w=100`, + ) + const allowedRemoteImageResponse = await dev.handle(allowedRemoteImageRequest) + expect(allowedRemoteImageResponse?.ok).toBe(true) + expect(allowedRemoteImageResponse?.headers.get('content-type')).toMatch(/^image\//) + expect( + await getImageResponseSize(allowedRemoteImageResponse ?? new Response('No @netlify/dev response')), + ).toMatchObject({ width: 100, height: 50 }) + + const notAllowedRemoteImageRequest = new Request( + `https://site.netlify/.netlify/images?url=${encodeURIComponent(`${remoteServerAddress}/not-allowed/image`)}&w=100`, + ) + const notAllowedRemoteImageResponse = await dev.handle(notAllowedRemoteImageRequest) + expect(notAllowedRemoteImageResponse?.status).toBe(403) + + const rewriteImageRequest = new Request( + `https://site.netlify/image-cdn-rewrite?url=${encodeURIComponent('local/image.jpg')}&w=100`, + ) + const rewriteImageResponse = await dev.handle(rewriteImageRequest) + expect(rewriteImageResponse?.ok).toBe(true) + expect(rewriteImageResponse?.headers.get('content-type')).toMatch(/^image\//) + expect( + await getImageResponseSize(rewriteImageResponse ?? new Response('No @netlify/dev response')), + ).toMatchObject({ width: 100, height: 50 }) + + await remoteServer.stop() + await dev.stop() + await fixture.destroy() + }) }) describe('With linked site', () => { diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index c6649f52..b6d33ab7 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -4,18 +4,11 @@ import path from 'node:path' import process from 'node:process' import { resolveConfig } from '@netlify/config' -import { - ensureNetlifyIgnore, - getAPIToken, - mockLocation, - LocalState, - type Logger, - HTTPServer, - netlifyCommand, -} from '@netlify/dev-utils' +import { ensureNetlifyIgnore, getAPIToken, mockLocation, LocalState, type Logger, HTTPServer } from '@netlify/dev-utils' import { EdgeFunctionsHandler } from '@netlify/edge-functions/dev' import { FunctionsHandler } from '@netlify/functions/dev' import { HeadersHandler, type HeadersCollector } from '@netlify/headers' +import { ImageHandler } from '@netlify/images' import { RedirectsHandler } from '@netlify/redirects' import { StaticHandler } from '@netlify/static' @@ -71,6 +64,15 @@ export interface Features { enabled?: boolean } + /** + * Configuration options for Netlify Image CDN. + * + * {@link} https://docs.netlify.com/image-cdn/overview/ + */ + images?: { + enabled?: boolean + } + /** * Configuration options for Netlify redirects and rewrites. * @@ -121,7 +123,7 @@ interface HandleOptions { headersCollector?: HeadersCollector } -export type ResponseType = 'edge-function' | 'function' | 'redirect' | 'static' +export type ResponseType = 'edge-function' | 'function' | 'image' | 'redirect' | 'static' export class NetlifyDev { #apiHost?: string @@ -138,10 +140,12 @@ export class NetlifyDev { environmentVariables: boolean functions: boolean headers: boolean + images: boolean redirects: boolean static: boolean } #headersHandler?: HeadersHandler + #imageHandler?: ImageHandler #logger: Logger #projectRoot: string #redirectsHandler?: RedirectsHandler @@ -168,6 +172,7 @@ export class NetlifyDev { environmentVariables: options.environmentVariables?.enabled !== false, functions: options.functions?.enabled !== false, headers: options.headers?.enabled !== false, + images: options.images?.enabled !== false, redirects: options.redirects?.enabled !== false, static: options.staticFiles?.enabled !== false, } @@ -211,7 +216,14 @@ export class NetlifyDev { } } - // 2. Check if the request matches a function. + // 2. Check if the request matches an image. + const imageMatch = this.#imageHandler?.match(readRequest) + if (imageMatch) { + const response = await imageMatch.handle() + return { response, type: 'image' } + } + + // 3. Check if the request matches a function. const functionMatch = await this.#functionsHandler?.match(readRequest, destPath) if (functionMatch) { // If the function prefers static files, check if there is a static match @@ -232,13 +244,21 @@ export class NetlifyDev { return { response: await functionMatch.handle(getWriteRequest()), type: 'function' } } - // 3. Check if the request matches a redirect rule. + // 4. Check if the request matches a redirect rule. const redirectMatch = await this.#redirectsHandler?.match(readRequest) if (redirectMatch) { + const redirectRequest = new Request(redirectMatch.target) + // If the redirect rule matches Image CDN, we'll serve it. + const imageMatch = this.#imageHandler?.match(redirectRequest) + if (imageMatch) { + const response = await imageMatch.handle() + return { response, type: 'image' } + } + // If the redirect rule matches a function, we'll serve it. The exception // is if the function prefers static files, which in this case means that // we'll follow the redirect rule. - const functionMatch = await this.#functionsHandler?.match(new Request(redirectMatch.target), destPath) + const functionMatch = await this.#functionsHandler?.match(redirectRequest, destPath) if (functionMatch && !functionMatch.preferStatic) { return { response: await functionMatch.handle(getWriteRequest()), @@ -269,16 +289,7 @@ export class NetlifyDev { } } - const { pathname } = new URL(readRequest.url) - if (pathname.startsWith('/.netlify/images')) { - this.#logger.error( - `The Netlify Image CDN is currently only supported in the Netlify CLI. Run ${netlifyCommand('npx netlify dev')} to get started.`, - ) - - return - } - - // 4. Check if the request matches a static file. + // 5. Check if the request matches a static file. const staticMatch = await this.#staticHandler?.match(readRequest) if (staticMatch) { const response = await staticMatch.handle() @@ -390,10 +401,11 @@ export class NetlifyDev { let serverAddress: string | undefined // If a custom server has been provided, use it. If not, we must stand up - // a new one, since it's required for communication with edge functions. + // a new one, since it's required for communication with edge functions + // and local images support for Image CDN. if (typeof this.#server === 'string') { serverAddress = this.#server - } else if (this.#features.edgeFunctions) { + } else if (this.#features.edgeFunctions || this.#features.images) { const passthroughServer = new HTTPServer(async (req) => { const res = await this.handle(req) @@ -495,6 +507,14 @@ export class NetlifyDev { }) } + if (this.#features.images) { + this.#imageHandler = new ImageHandler({ + imagesConfig: this.#config?.config.images, + logger: this.#logger, + originServerAddress: serverAddress, + }) + } + return { serverAddress, } diff --git a/packages/images/.gitignore b/packages/images/.gitignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/packages/images/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/images/CHANGELOG.md b/packages/images/CHANGELOG.md new file mode 100644 index 00000000..825c32f0 --- /dev/null +++ b/packages/images/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/packages/images/package.json b/packages/images/package.json new file mode 100644 index 00000000..a9097e2e --- /dev/null +++ b/packages/images/package.json @@ -0,0 +1,42 @@ +{ + "name": "@netlify/images", + "version": "0.0.0", + "description": "TypeScript implementation of Netlify's Image CDN", + "type": "module", + "engines": { + "node": ">=20.6.1" + }, + "main": "./dist/main.js", + "exports": "./dist/main.js", + "types": "./dist/main.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "tsup-node", + "prepack": "npm run build", + "test": "vitest run", + "test:dev": "vitest", + "test:ci": "npm run build && vitest run", + "dev": "tsup-node --watch", + "publint": "npx -y publint --strict" + }, + "keywords": [], + "license": "MIT", + "repository": "netlify/primitives", + "bugs": { + "url": "https://github.com/netlify/primitives/issues" + }, + "author": "Netlify Inc.", + "directories": { + "test": "test" + }, + "devDependencies": { + "@netlify/dev-utils": "^3.1.1", + "tsup": "^8.5.0", + "vitest": "^3.1.4" + }, + "dependencies": { + "ipx": "^3.0.3" + } +} diff --git a/packages/images/src/main.test.ts b/packages/images/src/main.test.ts new file mode 100644 index 00000000..9e8270e4 --- /dev/null +++ b/packages/images/src/main.test.ts @@ -0,0 +1,344 @@ +import { createImageServerHandler, createMockLogger, getImageResponseSize, HTTPServer } from '@netlify/dev-utils' +import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest' +import { createIPXWebServer } from 'ipx' + +import { ImageHandler } from './main.js' + +const mockedIpxResponse = new Response('Mocked response from IPX') + +vi.mock('ipx', async () => { + return { + ...(await vi.importActual('ipx')), + createIPXWebServer: vi.fn(() => () => Promise.resolve(mockedIpxResponse.clone())), + } +}) + +const mockCreateIPXWebServer = vi.mocked(createIPXWebServer) + +beforeEach(() => { + mockCreateIPXWebServer.mockReset() +}) + +describe('`ImageHandler`', () => { + describe('constructor', () => { + test('warns about malformed remote image patterns', () => { + const logger = { ...createMockLogger(), warn: vi.fn() } + + new ImageHandler({ + logger, + imagesConfig: { + remote_images: ['https://example.com/images/.*', 'invalid-regex['], + }, + }) + + expect(logger.warn).toHaveBeenCalledWith( + 'Malformed remote image pattern: "invalid-regex[": Invalid regular expression: /invalid-regex[/: Unterminated character class. Skipping it.', + ) + }) + }) + + describe('match', () => { + describe('image endpoints', () => { + test('matches on `/.netlify/images', async () => { + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + originServerAddress: 'http://localhost:5173', + }) + + const url = new URL('/.netlify/images', 'https://netlify.com') + url.searchParams.set('url', 'image.png') + + const match = imageHandler.match(new Request(url)) + + expect(match).toBeDefined() + + const response = await match!.handle() + + expect(response.ok).toBe(true) + expect(response).toMatchObject(mockedIpxResponse) + }) + + test('matches on `/.netlify/images/', async () => { + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + originServerAddress: 'http://localhost:5173', + }) + + const url = new URL('/.netlify/images/', 'https://netlify.com') + url.searchParams.set('url', 'image.png') + + const match = imageHandler.match(new Request(url)) + + expect(match).toBeDefined() + + const response = await match!.handle() + + expect(response.ok).toBe(true) + expect(response).toMatchObject(mockedIpxResponse) + }) + + test('does not match on `/.netlify/foo', () => { + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + }) + + const url = new URL('/.netlify/foo', 'https://netlify.com') + url.searchParams.set('url', 'image.png') + + const match = imageHandler.match(new Request(url)) + + expect(match).not.toBeDefined() + }) + }) + + describe('request methods', () => { + test('allows GET requests', async () => { + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + originServerAddress: 'http://localhost:5173', + }) + + const url = new URL('/.netlify/images', 'https://netlify.com') + url.searchParams.set('url', 'image.png') + + const match = imageHandler.match(new Request(url)) + + expect(match).toBeDefined() + + const response = await match!.handle() + + expect(response.ok).toBe(true) + expect(response).toMatchObject(mockedIpxResponse) + }) + + test('does not allow POST requests', async () => { + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + }) + + const url = new URL('/.netlify/images', 'https://netlify.com') + url.searchParams.set('url', 'image.png') + + const match = imageHandler.match(new Request(url, { method: 'POST' })) + + expect(match).toBeDefined() + + const response = await match!.handle() + + expect(response.ok).toBe(false) + expect(await response.text()).toBe('Method Not Allowed') + }) + }) + + describe('local images', () => { + let originServerAddress: string + let originServer: HTTPServer + + const LOCAL_IMAGE_PATH = '/local/image.jpg' + const LOCAL_IMAGE_WIDTH = 800 + const LOCAL_IMAGE_HEIGHT = 400 + + beforeAll(async () => { + originServer = new HTTPServer( + createImageServerHandler((url: URL) => { + if (url.pathname === LOCAL_IMAGE_PATH) { + return { width: LOCAL_IMAGE_WIDTH, height: LOCAL_IMAGE_HEIGHT } + } + return null + }), + ) + + originServerAddress = await originServer.start() + }) + + afterAll(async () => { + await originServer.stop() + }) + + beforeEach(async () => { + mockCreateIPXWebServer.mockImplementation( + (await vi.importActual('ipx')).createIPXWebServer, + ) + }) + + test('preserves original width if width param is not used', async () => { + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + originServerAddress, + }) + + const url = new URL('/.netlify/images', originServerAddress) + url.searchParams.set('url', LOCAL_IMAGE_PATH) + + const match = imageHandler.match(new Request(url)) + + expect(match).toBeDefined() + + const response = await match!.handle() + + expect(response.ok).toBe(true) + + const { width } = await getImageResponseSize(response) + + expect(width).toBe(LOCAL_IMAGE_WIDTH) + }) + + test('resizes image to specified width preserving aspect ratio', async () => { + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + originServerAddress, + }) + + const requestedWidth = 200 + + const url = new URL('/.netlify/images', originServerAddress) + url.searchParams.set('url', LOCAL_IMAGE_PATH) + url.searchParams.set('w', requestedWidth.toString()) + + const match = imageHandler.match(new Request(url)) + + expect(match).toBeDefined() + + const response = await match!.handle() + + expect(response.ok).toBe(true) + + const { width, height } = await getImageResponseSize(response) + + expect(width).toBe(requestedWidth) + expect(width / height).toBe(LOCAL_IMAGE_WIDTH / LOCAL_IMAGE_HEIGHT) + }) + + test('resizes image to specified height preserving aspect ratio', async () => { + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + originServerAddress, + }) + + const requestedHeight = 200 + + const url = new URL('/.netlify/images', originServerAddress) + url.searchParams.set('url', LOCAL_IMAGE_PATH) + url.searchParams.set('h', requestedHeight.toString()) + + const match = imageHandler.match(new Request(url)) + + expect(match).toBeDefined() + + const response = await match!.handle() + + expect(response.ok).toBe(true) + + const { width, height } = await getImageResponseSize(response) + + expect(height).toBe(requestedHeight) + expect(width / height).toBe(LOCAL_IMAGE_WIDTH / LOCAL_IMAGE_HEIGHT) + }) + + test('resizes image to specified width and height ignoring original aspect ratio', async () => { + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + originServerAddress, + }) + + const requestedWidth = 200 + const requestedHeight = 200 + + const url = new URL('/.netlify/images', originServerAddress) + url.searchParams.set('url', LOCAL_IMAGE_PATH) + url.searchParams.set('w', requestedWidth.toString()) + url.searchParams.set('h', requestedHeight.toString()) + + const match = imageHandler.match(new Request(url)) + + expect(match).toBeDefined() + + const response = await match!.handle() + + expect(response.ok).toBe(true) + + const { width, height } = await getImageResponseSize(response) + + expect(width).toBe(requestedWidth) + expect(height).toBe(requestedHeight) + expect(width / height).not.toBe(LOCAL_IMAGE_WIDTH / LOCAL_IMAGE_HEIGHT) + }) + }) + + describe('remote images', () => { + let remoteServerAddress: string + let remoteServer: HTTPServer + + const IMAGE_WIDTH = 800 + const IMAGE_HEIGHT = 400 + + beforeAll(async () => { + remoteServer = new HTTPServer( + createImageServerHandler(() => { + return { width: IMAGE_WIDTH, height: IMAGE_HEIGHT } + }), + ) + + remoteServerAddress = await remoteServer.start() + }) + + afterAll(async () => { + await remoteServer.stop() + }) + + test('allow remote images matching configured patterns', async () => { + mockCreateIPXWebServer.mockImplementation( + (await vi.importActual('ipx')).createIPXWebServer, + ) + + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + imagesConfig: { + remote_images: [`^${remoteServerAddress}/.*`], + }, + }) + + const requestedWidth = 100 + + const url = new URL('/.netlify/images', 'https://netlify.com') + url.searchParams.set('url', remoteServerAddress) + url.searchParams.set('w', requestedWidth.toString()) + + const match = imageHandler.match(new Request(url)) + + expect(match).toBeDefined() + + const response = await match!.handle() + + expect(response.ok).toBe(true) + + const { width, height } = await getImageResponseSize(response) + + expect(width).toBe(requestedWidth) + expect(width / height).toBe(IMAGE_WIDTH / IMAGE_HEIGHT) + }, 30_000) + + test('does not allow remote images not matching configured patterns', async () => { + const imageHandler = new ImageHandler({ + logger: createMockLogger(), + imagesConfig: { + remote_images: [], + }, + }) + + const url = new URL('/.netlify/images', 'https://netlify.com') + url.searchParams.set('url', remoteServerAddress) + url.searchParams.set('w', '100') + + const match = imageHandler.match(new Request(url)) + + expect(match).toBeDefined() + + const response = await match!.handle() + + expect(response.status).toBe(403) + expect(await response.text()).toBe('Forbidden: Remote image URL not allowed') + }) + }) + }) +}) diff --git a/packages/images/src/main.ts b/packages/images/src/main.ts new file mode 100644 index 00000000..b10aacf1 --- /dev/null +++ b/packages/images/src/main.ts @@ -0,0 +1,133 @@ +import type { Logger } from '@netlify/dev-utils' +import { createIPX, createIPXWebServer, ipxFSStorage, ipxHttpStorage } from 'ipx' + +interface ImagesConfig { + remote_images: string[] +} + +interface ImageHandlerOptions { + imagesConfig?: ImagesConfig + logger: Logger + originServerAddress?: string +} + +export interface ImageMatch { + handle: () => Promise +} + +const IMAGE_CDN_ENDPOINTS = ['/.netlify/images', '/.netlify/images/'] + +export class ImageHandler { + #allowedRemoteUrlPatterns: RegExp[] + #logger: Logger + #originServerURL?: URL + + constructor({ logger, imagesConfig, originServerAddress }: ImageHandlerOptions) { + this.#logger = logger + this.#allowedRemoteUrlPatterns = (imagesConfig?.remote_images ?? []).reduce((acc, stringPattern) => { + try { + acc.push(new RegExp(stringPattern)) + } catch (maybeError) { + const error = maybeError instanceof Error ? maybeError : new Error(String(maybeError)) + this.#logger.warn(`Malformed remote image pattern: "${stringPattern}": ${error.message}. Skipping it.`) + } + return acc + }, []) + this.#originServerURL = originServerAddress ? new URL(originServerAddress) : undefined + } + + private generateIPXRequestURL(imageURL: URL, netlifyImageCdnParams: URLSearchParams): URL { + const ipxParams: string[] = [] + + const width = netlifyImageCdnParams.get('w') ?? netlifyImageCdnParams.get('width') + const height = netlifyImageCdnParams.get('h') ?? netlifyImageCdnParams.get('height') + + if (width && height) { + ipxParams.push(`resize_${width}x${height}`) + } else if (width) { + ipxParams.push(`width_${width}`) + } else if (height) { + ipxParams.push(`height_${height}`) + } + + const quality = netlifyImageCdnParams.get('q') ?? netlifyImageCdnParams.get('quality') + if (quality) { + ipxParams.push(`quality_${quality}`) + } + const format = netlifyImageCdnParams.get('fm') + if (format) { + ipxParams.push(`format_${format}`) + } + + const fit = netlifyImageCdnParams.get('fit') + if (fit) { + ipxParams.push(`fit_${fit === 'contain' ? 'inside' : fit}`) + } + + const position = netlifyImageCdnParams.get('position') + if (position) { + ipxParams.push(`position_${position}`) + } + + const ipxModifiers = ipxParams.join(',') + + return new URL(`/${ipxModifiers || `_`}/${encodeURIComponent(imageURL.href)}`, imageURL.origin) + } + + match(request: Request): ImageMatch | undefined { + const url = new URL(request.url) + + if (!IMAGE_CDN_ENDPOINTS.includes(url.pathname)) { + return + } + + return { + handle: async () => { + if (request.method !== 'GET') { + return new Response('Method Not Allowed', { status: 405 }) + } + + const sourceImageUrlParam = url.searchParams.get('url') + if (!sourceImageUrlParam) { + return new Response('Bad Request: Missing "url" query parameter', { status: 400 }) + } + + let sourceImageUrl: URL + try { + sourceImageUrl = new URL(sourceImageUrlParam, this.#originServerURL) + } catch (error) { + throw new Error( + `Failed to construct source image URL from "${sourceImageUrlParam}".` + + (!this.#originServerURL && !sourceImageUrlParam.startsWith('http') + ? '\nLooks like source image is local and `originServerAddress` was not provided.' + : ''), + { cause: error }, + ) + } + + // if it's not local image, check if it's allowed + if ( + sourceImageUrl.origin !== this.#originServerURL?.origin && + !this.#allowedRemoteUrlPatterns.some((allowedRemoteUrlPattern) => + allowedRemoteUrlPattern.test(sourceImageUrl.href), + ) + ) { + return new Response('Forbidden: Remote image URL not allowed', { status: 403 }) + } + + const ipx = createIPX({ + storage: ipxFSStorage(), + httpStorage: ipxHttpStorage({ + // checking if url is allowed is done above, so we disable IPX checking + allowAllDomains: true, + }), + }) + + const ipxHandler = createIPXWebServer(ipx) + + const ipxRequest = new Request(this.generateIPXRequestURL(sourceImageUrl, url.searchParams), request) + return ipxHandler(ipxRequest) + }, + } + } +} diff --git a/packages/images/tsconfig.json b/packages/images/tsconfig.json new file mode 100644 index 00000000..7b592eaa --- /dev/null +++ b/packages/images/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "rootDir": "./src", + "moduleResolution": "node", + "allowJs": true, + "declaration": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/packages/images/tsup.config.ts b/packages/images/tsup.config.ts new file mode 100644 index 00000000..9a088574 --- /dev/null +++ b/packages/images/tsup.config.ts @@ -0,0 +1,17 @@ +import { argv } from 'node:process' + +import { defineConfig } from 'tsup' + +export default defineConfig([ + { + clean: true, + format: ['esm'], + entry: ['src/main.ts'], + tsconfig: 'tsconfig.json', + splitting: false, + bundle: true, + dts: true, + outDir: './dist', + watch: argv.includes('--watch'), + }, +]) diff --git a/packages/vite-plugin/src/main.test.ts b/packages/vite-plugin/src/main.test.ts index bbead52d..52afe1f4 100644 --- a/packages/vite-plugin/src/main.test.ts +++ b/packages/vite-plugin/src/main.test.ts @@ -3,8 +3,8 @@ import path from 'node:path' import process from 'node:process' import { fileURLToPath, pathToFileURL } from 'node:url' -import { Fixture } from '@netlify/dev-utils' -import { type Browser, type ConsoleMessage, type Page, chromium } from 'playwright' +import { createImageServerHandler, Fixture, generateImage, HTTPServer } from '@netlify/dev-utils' +import { type Browser, type ConsoleMessage, type Locator, type Page, chromium } from 'playwright' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { createServer } from 'vite' @@ -221,7 +221,7 @@ defined on your team and site and much more. Run npx netlify init to get started expect(mockLogger.info).toHaveBeenNthCalledWith(1, 'Environment loaded', expect.objectContaining({})) expect(mockLogger.info).toHaveBeenNthCalledWith( 2, - 'Middleware loaded. Emulating features: blobs, environmentVariables, functions, headers, redirects, static.', + 'Middleware loaded. Emulating features: blobs, environmentVariables, functions, headers, images, redirects, static.', expect.objectContaining({}), ) expect(mockLogger.info).toHaveBeenNthCalledWith( @@ -429,6 +429,96 @@ defined on your team and site and much more. Run npx netlify init to get started await server.close() await fixture.destroy() }) + + test('Handles Image CDN requests', async () => { + const IMAGE_WIDTH = 800 + const IMAGE_HEIGHT = 400 + + const remoteServer = new HTTPServer( + createImageServerHandler(() => { + return { width: IMAGE_WIDTH, height: IMAGE_HEIGHT } + }), + ) + + const remoteServerAddress = await remoteServer.start() + + const fixture = new Fixture() + .withFile( + 'netlify.toml', + `[images] + remote_images = [ + "^${remoteServerAddress}/allowed/.*" + ]`, + ) + .withFile( + 'vite.config.js', + `import { defineConfig } from 'vite'; + import netlify from '@netlify/vite-plugin'; + + export default defineConfig({ + plugins: [ + netlify({ + middleware: true, + }) + ] + });`, + ) + .withFile( + 'index.html', + ` + + Hello World + +

Hello from the browser

+ + + + + `, + ) + .withFile('local/image.jpg', await generateImage(IMAGE_WIDTH, IMAGE_HEIGHT)) + + const directory = await fixture.create() + await fixture + .withPackages({ + vite: '6.0.0', + '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(), + }) + .create() + + const mockLogger = createMockViteLogger() + const { server, url } = await startTestServer({ + root: directory, + logLevel: 'info', + customLogger: mockLogger, + }) + + await page.goto(url) + + const getImageSize = (locator: Locator) => { + return locator.evaluate((img: HTMLImageElement) => { + if (img.naturalWidth === 0 || img.naturalHeight === 0) { + throw new Error(`Image was not loaded`) + } + + return { + width: img.naturalWidth, + height: img.naturalHeight, + } + }) + } + + expect(await getImageSize(page.locator('#local-image'))).toEqual({ width: 100, height: 50 }) + expect(await getImageSize(page.locator('#allowed-remote-image'))).toEqual({ width: 100, height: 50 }) + + await expect( + async () => await getImageSize(page.locator('#not-allowed-remote-image')), + 'Not allowed remote image should not load', + ).rejects.toThrowError(`Image was not loaded`) + + await server.close() + await fixture.destroy() + }) }) describe('With @vitejs/plugin-react', () => { diff --git a/release-please-config.json b/release-please-config.json index 0eedbaa3..5535e9e5 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -13,6 +13,7 @@ "packages/edge-functions": {}, "packages/functions": {}, "packages/headers": {}, + "packages/images": {}, "packages/otel": {}, "packages/redirects": {}, "packages/runtime": {},