From d5d2e30289c4d88938807e871f5663f6b4f73169 Mon Sep 17 00:00:00 2001 From: Mahir Senturk Date: Fri, 18 Sep 2026 22:44:17 +0200 Subject: [PATCH 1/2] Prepare HEIC decoder for public release --- .changeset/config.json | 2 +- .changeset/initial-release.md | 21 - .github/ISSUE_TEMPLATE/bug_report.yml | 35 + .github/ISSUE_TEMPLATE/feature_request.yml | 15 + .github/PULL_REQUEST_TEMPLATE.md | 12 + .github/workflows/ci.yml | 20 +- .gitignore | 3 + CHANGELOG.md | 12 + CONTRIBUTING.md | 83 + README.md | 244 +-- SECURITY.md | 33 +- docs/api.md | 110 ++ docs/compatibility.md | 53 + docs/releasing.md | 42 + package.json | 30 +- pnpm-lock.yaml | 2070 ++++++++++++++++++++ test/browser/color.spec.ts | 35 +- test/browser/cross-strategy.spec.ts | 10 +- test/browser/decode-worker.js | 8 +- test/browser/decode.spec.ts | 31 +- test/browser/framesize.spec.ts | 8 +- test/browser/wasm.spec.ts | 44 + tools/check-package.mjs | 40 + tsup.config.ts | 2 +- wasm/index.ts | 4 +- wasm/libheif-js.d.ts | 2 +- 26 files changed, 2736 insertions(+), 233 deletions(-) delete mode 100644 .changeset/initial-release.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/api.md create mode 100644 docs/compatibility.md create mode 100644 docs/releasing.md create mode 100644 pnpm-lock.yaml create mode 100644 tools/check-package.mjs diff --git a/.changeset/config.json b/.changeset/config.json index fce1c26..dcd3d8b 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -5,7 +5,7 @@ "fixed": [], "linked": [], "access": "public", - "baseBranch": "main", + "baseBranch": "master", "updateInternalDependencies": "patch", "ignore": [] } diff --git a/.changeset/initial-release.md b/.changeset/initial-release.md deleted file mode 100644 index 9c89a1b..0000000 --- a/.changeset/initial-release.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@su-engineering/heic': minor ---- - -Initial release. - -Browser-first HEIC/HEIF decoder. Parses the ISOBMFF container in JavaScript and -hands the HEVC bitstream to WebCodecs `VideoDecoder`, so HEIC decodes with -hardware acceleration and no WebAssembly on most devices. The libheif wasm -fallback is a separate entry point that is never fetched unless the caller opts -in. - -- Three-strategy cascade: `createImageBitmap`, WebCodecs, wasm adapter -- Grid images (the common iPhone case) composited inside the decoder output - callback, which is what avoids the hardware frame-pool deadlock -- `irot` / `imir` / `clap` applied in `ipma` association order -- 8-bit and 10-bit, sRGB and Display P3 output -- `isHeic()` reads bytes rather than filenames, and distinguishes AVIF -- `probeSupport()` reports capability before anything is downloaded -- Bounds-checked, fuzzed parser with typed errors -- Runs in a Web Worker diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..bc3922b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,35 @@ +name: Bug report +description: Report a reproducible parser or decoder problem. +body: + - type: markdown + attributes: + value: "For vulnerabilities, follow SECURITY.md instead. Do not upload personal photos or sensitive metadata." + - type: textarea + id: behavior + attributes: + label: Expected and actual behavior + description: Include steps to reproduce and a minimal code example. + validations: + required: true + - type: input + id: version + attributes: + label: Package version + validations: + required: true + - type: input + id: environment + attributes: + label: Browser and operating system versions + validations: + required: true + - type: textarea + id: diagnostics + attributes: + label: Strategy and diagnostics + description: Include probeSupport(), error context/attempts, warnings, and relevant logs. + - type: textarea + id: fixture + attributes: + label: Reproducing input + description: Attach only a non-sensitive file you have permission to redistribute, or a generation script. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..a4162e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,15 @@ +name: Feature request +description: Propose an improvement with a concrete use case. +body: + - type: textarea + id: problem + attributes: + label: What problem would this solve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed behavior and alternatives + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8de1871 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +## Problem and behavior + +Describe the concrete trigger and the resulting behavior. + +## Validation + +List checks run and any skipped tests or device coverage gaps. + +- [ ] Typecheck, build, package validation, and unit tests pass. +- [ ] Relevant browser tests run for decode changes. +- [ ] Documentation and changeset updated when applicable. +- [ ] New fixtures can be redistributed and contain no sensitive information. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 837d404..a3b14e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,12 +2,20 @@ name: CI on: push: - branches: [main] + branches: [master] pull_request: +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: check: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -18,6 +26,7 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm typecheck - run: pnpm build + - run: pnpm test:package # The parser and the fuzzer need no browser at all, and they are where the # security-relevant behaviour lives. @@ -25,6 +34,7 @@ jobs: browser: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -34,6 +44,7 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm build + - run: pnpm test:package - run: pnpm exec playwright install --with-deps chromium firefox webkit - run: pnpm test:browser @@ -49,3 +60,10 @@ jobs: - name: Note on coverage run: | echo "::notice::WebCodecs HEVC does not run in headless CI. This job covers the parser and the wasm path only." + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: browser-test-results + path: test-results/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index 5da6860..f510361 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ playwright-report/ # Personal photo corpus used for local development. The committed fixtures under # test/fixtures/ are the ones CI runs against; see test/fixtures/README.md. test/fixtures/local/ + +# Bundled public-API consumer used by browser integration tests. +test/.consumer/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bda4784 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## 0.1.0 + +Initial public release. + +- Browser-first HEIC decoding with native, WebCodecs HEVC, and optional libheif WASM strategies. +- Single-image and tiled-grid planning with container rotation, mirroring, and clean-aperture cropping. +- Typed errors, source metadata, capability probes, and worker-compatible rendering. +- Reproducible build/test setup, public API documentation, and package-entry validation. + +See [compatibility limitations](docs/compatibility.md) before deploying. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a3130f7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,83 @@ +# Contributing + +Thanks for helping improve HEIC decoding on the web. We welcome bug fixes, documentation improvements, and regression tests for files the parser or decoder mishandles. + +## Local development + +Use Node.js 22.12+ and pnpm 9.15.9 (declared in `package.json`). If your Node installation does not include Corepack, install pnpm 9.15.9 using your preferred package manager. + +```sh +git clone https://github.com/su-engineering/heic-web.git +cd heic-web +corepack enable +pnpm install --frozen-lockfile +pnpm typecheck +pnpm build +pnpm test:package +pnpm test:unit +``` + +The package can be used before registry publication by building it and importing `dist/index.js`, or by installing a tarball created with `pnpm pack --pack-destination /tmp/heic-pack`. The WASM entry point is `dist/wasm.js`; its optional peer dependency must be available in the consuming project. + +## Repository map + +| Directory | Responsibility | +| --- | --- | +| `src/parser/` | Bounds-checked ISOBMFF/HEIF parsing, item extraction, grid descriptors, and HEVC configuration. | +| `src/plan.ts` | Primary-image planning, dimensions, tile groups, transforms, and feature warnings. | +| `src/decoders/` | Native image decoding and WebCodecs HEVC decoding. | +| `src/render/` | Canvas compositing helpers and container transforms. | +| `wasm/` | Optional libheif adapter and its module declarations. | +| `test/unit/` | Parser tests and deterministic mutation fuzzing. | +| `test/browser/` | Public-API browser tests and worker harnesses. | +| `test/fixtures/` | Committed fixtures; private local corpus is ignored by Git. | +| `tools/` | Test server, package validation, fixture generation, and inspection tools. | + +## Browser tests + +Build and run `pnpm test:package` first; the harness imports `dist/` and the bundled consumer created by that check. + +```sh +pnpm exec playwright install chromium firefox webkit +pnpm test:browser +``` + +On a supported Linux distribution, add `--with-deps` to install required system libraries. If a browser fails to launch on an unsupported distribution, run the suite in the same Ubuntu environment as CI rather than changing host libraries blindly. + +The default projects are `chromium`, `firefox`, and `webkit`. To test a real Chromium installation with platform HEVC: + +```sh +HEIC_CHROME="/path/to/chrome" pnpm test:browser --project=chrome-hevc +``` + +The environment variable adds that project to the matrix. Setting it does not create HEVC support; capability-dependent tests still skip if the installed browser cannot decode HEVC. Playwright WebKit is not a substitute for testing released Safari on an Apple device. + +Run `pnpm test:all` after installing browsers for the combined checks. Record skipped tests as well as failures when reporting coverage. + +## Reproducing a bug + +Include the input's provenance, browser and OS versions, package version, strategy, and error name/message/context. For `HeicUnsupportedError`, include `attempts`. Explain the expected orientation, dimensions, or colors. A minimal redistributable fixture is preferable to a personal photograph. + +For container inspection (Node.js 22.6+ with type stripping): + +```sh +pnpm dump path/to/photo.heic --boxes +pnpm validate-corpus path/to/corpus +``` + +See [fixture documentation](https://github.com/su-engineering/heic-web/blob/master/test/fixtures/README.md) for reference-image generation and manifest provenance. Fixture regeneration requires libheif command-line tools, ImageMagick, and ExifTool; normal builds and tests use committed fixtures and do not need those tools. + +## Submitting a change + +1. Create a focused branch from `master`. +2. Explain the observable problem and add a regression test for behavior changes. +3. Preserve typed parser errors and bounds/count checks. Avoid DOM dependencies in decode paths. +4. Run typecheck, build, package validation, and unit tests; run relevant browser tests for decoding changes. +5. Update documentation when API behavior or requirements change. For package changes, run `pnpm exec changeset` and add a concise release note. +6. Open a pull request explaining the change, validation, and any coverage gaps. + +Do not commit `dist/`, browser reports, `node_modules/`, or the private photo corpus. Only contribute fixtures you have permission to redistribute, without sensitive metadata. Contributions are licensed under the repository's MIT license. + +## Community expectations + +Be respectful, explain disagreements with evidence, and focus reviews on the work. Do not harass contributors or disclose private information. Report conduct concerns privately to hello@su.engineering. Use the [security policy](SECURITY.md) for vulnerabilities. diff --git a/README.md b/README.md index f0ad987..b7c1ead 100644 --- a/README.md +++ b/README.md @@ -1,191 +1,149 @@ # @su-engineering/heic -Decode iPhone HEIC photos in the browser, without shipping a WebAssembly codec to -everyone. +**Decode HEIC photos in the browser with native decoding, WebCodecs, and an optional WebAssembly fallback.** -Chrome and Firefox cannot display HEIC. Google never licensed HEVC, so there is -no native image decode path and no plan to add one. Every iPhone still produces -HEIC by default, so any site that accepts profile picture uploads silently fails -for a large share of its users. +[![CI](https://github.com/su-engineering/heic-web/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/su-engineering/heic-web/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -The usual answer is to ship libheif compiled to WebAssembly, roughly 1.2 MB, to -every visitor. That is the only reason most sites do not bother. +HEIC uploads need not force every visitor to download a software codec. This TypeScript library parses the HEIF container, tries the browser's image decoder, then uses WebCodecs HEVC decoding when available. You choose whether to load the libheif fallback if both paths fail. -This package takes a different route. HEIC is an HEVC-coded image inside an -ISOBMFF container, and Chrome's WebCodecs `VideoDecoder` has supported HEVC since -Chrome 107, backed by platform hardware decode. Parse the container in JavaScript, -hand the raw bitstream to `VideoDecoder`, and you get hardware-accelerated HEIC -decode with no wasm at all, on the large majority of devices. +- **On-demand fallback:** the core entry point does not import libheif or fetch a codec. +- **Primary-image decoding:** single HEVC items and tiled grids, with container rotation, mirroring, and clean-aperture cropping. +- **Useful diagnostics:** selected strategy, source dimensions, color metadata, and warnings accompany each bitmap. +- **Worker support:** no DOM dependency; compositing uses `OffscreenCanvas`. +- **Typed API:** ESM, TypeScript declarations, and a standalone browser bundle. -**10.5 KB gzipped.** wasm becomes a last-resort fallback that you opt into, not -the default. +The project is at **0.1.0**. Test it with representative files and target devices before production use. The repository is named `heic-web`; the npm package name is `@su-engineering/heic`. -## Usage +[API reference](docs/api.md) · [Compatibility and limitations](docs/compatibility.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) -```ts -import { decodeHeic, isHeic } from '@su-engineering/heic'; +## Installation -const file = input.files[0]; -if ((await isHeic(file)).isHeic) { - const { image, width, height } = await decodeHeic(file); - canvas.getContext('2d').drawImage(image, 0, 0); -} -``` +When the package has been published to your registry: -## Support matrix +```sh +npm install @su-engineering/heic +``` -Measured, not assumed — these are the numbers `probeSupport()` returns in each -browser, on macOS: +For a checkout before publication, see [local development](CONTRIBUTING.md#local-development). Decoding runs in a browser or browser worker. Node.js can run the container parser, but this package does not provide a Node.js pixel decoder. -| Browser | Native | WebCodecs HEVC | Strategy used | Cost | -|---|---|---|---|---| -| Safari / WebKit | yes | yes | `native` | free | -| Chrome, Edge, Brave (macOS, most Android, Windows with the HEVC extension) | no | yes | `webcodecs` | ~10 KB | -| Chrome on Linux, Windows without the HEVC extension | no | no | `wasm` | ~1.2 MB, opt-in | -| Firefox | no | no | `wasm` | ~1.2 MB, opt-in | +## Quick start -Ask before you download anything: +This example assumes an `` and `` on the page. It uses only browser-provided decoders; unsupported environments receive a typed error. ```ts -import { probeSupport } from '@su-engineering/heic'; - -const report = await probeSupport(); -// { native: false, webcodecs: true, -// hevcCodecStrings: ['hvc1.3.e.L93.B0', ...], recommended: 'webcodecs' } - -if (report.recommended === 'wasm') { - // Only now is it worth preloading the fallback. -} +import { decodeHeic, isHeic, HeicUnsupportedError } from '@su-engineering/heic'; + +const input = document.querySelector('#photo')!; +const canvas = document.querySelector('#preview')!; + +input.addEventListener('change', async () => { + const file = input.files?.[0]; + if (!file || !(await isHeic(file)).isHeic) return; + + try { + const decoded = await decodeHeic(file, { maxDimension: 1024 }); + try { + canvas.width = decoded.width; + canvas.height = decoded.height; + canvas.getContext('2d')!.drawImage(decoded.image, 0, 0); + console.log(decoded.strategy, decoded.warnings); + } finally { + decoded.image.close(); // Release pixels when you no longer need them. + } + } catch (error) { + if (error instanceof HeicUnsupportedError) { + console.error('No decoder succeeded:', error.attempts); + } else { + console.error('Could not decode this photo:', error); + } + } +}); ``` -## The wasm fallback +`isHeic()` inspects file contents, rather than trusting the filename or MIME type. It is a routing hint, not a complete validation step. AVIF is not decoded by this package. -Nothing is fetched behind your back. If strategies 1 and 2 both fail and you have -not supplied an adapter, `decodeHeic` throws `HeicUnsupportedError` naming what -was missing. +## Add the optional WASM fallback + +```sh +npm install libheif-js +``` ```ts import { decodeHeic } from '@su-engineering/heic'; -import { wasmDecoder } from '@su-engineering/heic/wasm'; -await decodeHeic(file, { wasmLoader: async () => wasmDecoder }); +const decoded = await decodeHeic(file, { + wasmLoader: async () => { + const { wasmDecoder } = await import('@su-engineering/heic/wasm'); + return wasmDecoder; + }, +}); +// Draw or transfer decoded.image, then close it when finished. ``` -`@su-engineering/heic/wasm` is a separate entry point, so bundlers never pull the -codec into your main chunk. It needs `libheif-js`, an optional peer dependency — -the core package builds and passes its tests without it installed. - -## API +The separate entry point loads `libheif-js/wasm-bundle.js` on its first decode. A bundler supporting dynamic imports can keep the adapter and codec out of the initial chunk. Check your bundler's output: asset splitting and download sizes depend on your toolchain and the libheif version. -### `decodeHeic(input, options?): Promise` +For self-hosted assets, custom builds, or direct browser imports, use [`createWasmAdapter`](docs/api.md#wasm-adapters). The core library is MIT licensed; optional libheif distributions have [their own licenses](docs/compatibility.md#third-party-code). -`input` is a `Blob`, `File`, `ArrayBuffer` or `Uint8Array`. +## Choose by capability -| Option | Default | Meaning | -|---|---|---| -| `strategy` | `'auto'` | `'native'`, `'webcodecs'`, `'wasm'`, or `'auto'` for the cascade | -| `colorSpace` | `'srgb'` | Canvas colour space. `'display-p3'` preserves wide-gamut source colour | -| `maxDimension` | — | Downscale the result so its longest side is at most this. A memory control, see below | -| `signal` | — | `AbortSignal`, honoured at every await including mid-way through a multi-tile decode | -| `wasmLoader` | — | Supplies the fallback adapter on demand | - -The result reports what happened, not just pixels: +In `auto` mode the cascade is **native → WebCodecs → supplied fallback**. Without a fallback, a file that needs one fails with `HeicUnsupportedError`. ```ts -{ - image: ImageBitmap, - width, height, // of the returned bitmap - sourceWidth, sourceHeight, // intrinsic size, before any maxDimension scaling - strategy: 'webcodecs', - bitDepth: 8, - isGrid: true, - tileCount: 48, - sourceColor: { type: 'icc', profile: Uint8Array } | { type: 'nclx', ... } | null, - transformsApplied: { rotation: 270, mirrored: 'none', cropped: false }, - warnings: [{ code: 'gain-map-ignored', message: '...' }], -} -``` - -### `isHeic(input): Promise<{ isHeic, brand?, primaryItemType?, coding? }>` - -Reads the `ftyp` brands and the primary item type — never the filename or the -MIME type the browser guessed, both of which are routinely wrong for photos -copied off a phone. Given a `Blob`, it reads only the first 64 KB, because you -will want to call it on every file a user drops. - -AVIF shares the `mif1` brand with HEIC, so the result is discriminated rather -than boolean. A `{ isHeic: false, coding: 'av1' }` tells you to route the file to -an AVIF decoder instead of treating it as unreadable. - -### `probeSupport(): Promise` - -Cheap capability report. `isConfigSupported` is a query, not a decoder, and the -native probe decodes a 471-byte inline image. - -### `parseHeif(buffer): HeifFile` - -The container parser on its own, for inspection tooling and tests. There is also -a box-tree dumper: +import { probeSupport } from '@su-engineering/heic'; -``` -node --experimental-strip-types tools/dump.ts photo.heic --boxes +const support = await probeSupport(); +console.log(support.native, support.webcodecs, support.recommended); ``` -### `maxDimension` is a memory control +HEVC support depends on the browser, OS, installed codecs, hardware, and file profile. A capability probe is advisory; decoding a particular file can still fail. Use HTTPS or localhost for WebCodecs. See the [compatibility guide](docs/compatibility.md) for requirements and test coverage. -Not an image processing feature. A 48 MP iPhone photo composited at full -resolution is a ~190 MB canvas, and if you are building a 512 px avatar you -should not have to hold that. When set, the bitmap is produced with -`resizeQuality: 'high'` and the full-size canvas is released immediately. Only -the decoder can free it that early, which is the whole reason this one flag lives -here rather than in a wrapper. +## API at a glance -## Works in a Web Worker +| Export | Purpose | +| --- | --- | +| `decodeHeic(input, options?)` | Decode a `Blob`, `File`, `ArrayBuffer`, or `Uint8Array` to an `ImageBitmap` plus metadata. | +| `isHeic(input)` | Inspect up to the first 64 KiB for HEIC identification and coding hints. | +| `probeSupport()` | Probe native HEIC decoding and accepted WebCodecs HEVC configurations. | +| `parseHeif(buffer)` | Inspect container items, properties, references, and locations without decoding pixels. | +| `planDecode(buffer)` | Resolve the primary image, tile layout, transforms, and source metadata. | +| `registerDecoderAdapter(adapter)` | Register a fallback for subsequent calls in the current JavaScript realm. | -The core never touches `document` or `window` — `OffscreenCanvas` only, and -`VideoDecoder` is available in workers. This is a tested requirement, not an -aspiration: the suite runs a full decode inside a worker on every browser. +Common decode options are `strategy`, `maxDimension`, `colorSpace`, `signal`, and `wasmLoader`. The [API reference](docs/api.md) documents their behavior and the complete exported surface. -## What this does not do +## Scope and limits -Deliberately narrow. It decodes. It does not resize, re-encode, strip EXIF, or -provide UI. +This library returns pixels for the primary HEVC image. It does not encode HEIC, supply an upload UI, preserve EXIF in the output, or decode AVIF, animation, or Live Photo video. Recognized alpha, depth, and HDR gain-map auxiliary items produce warnings; warning coverage is not exhaustive. -Not supported in 0.1, and detected rather than ignored — when one of these is -present the primary image still decodes and a structured warning says what was -skipped: +`maxDimension` reduces the returned bitmap size. **It does not cap peak decode memory:** decoding and compositing may still allocate the full-resolution image. Apply file-size limits, bound concurrent decodes, and use workers for large or untrusted uploads. -- Alpha aux images -- Depth maps -- HDR gain maps (iOS 17+) -- Live Photo motion tracks -- Multi-image bursts and image sequences -- Animated HEIF -- HEIC **encoding** -- `VideoFrame` output — for grid images there is no single decoder frame, and - offering it would work for single-item files and be a lie for the common case -- Any re-encoding, format conversion or metadata stripping +`display-p3` requests a compositing canvas color space; it is not a guarantee of color fidelity across strategies. See [color and HDR limitations](docs/compatibility.md#color-and-hdr). -## Security +## Development -This parses hostile input in your users' browsers. Every read is bounds-checked, -nesting is capped, and no allocation is made on a declared size without first -validating it against the actual buffer. The parser is fuzzed with mutated real -files: every crash is a bug, every hang is a worse one. +```sh +git clone https://github.com/su-engineering/heic-web.git +cd heic-web +corepack enable +pnpm install --frozen-lockfile +pnpm typecheck +pnpm build +pnpm test:package +pnpm test:unit +pnpm exec playwright install --with-deps chromium firefox webkit +pnpm test:browser +``` -**This is a client-side decoder. Your server must still validate uploads -independently.** Nothing here is a substitute for that. +Use Node.js 22.12 or newer for development. The locked pnpm version is declared in `package.json`. Browser installation with `--with-deps` is intended for supported Linux distributions; on other platforms, see [test setup](CONTRIBUTING.md#browser-tests). -See [SECURITY.md](./SECURITY.md). +Headless Linux tests exercise the parser and fallback. They cannot establish platform HEVC coverage. Committed fixtures also do not cover the full range of Apple camera files. See [test fixtures](https://github.com/su-engineering/heic-web/blob/master/test/fixtures/README.md) and the [release checklist](docs/releasing.md). -## Errors +## Help and contributions -A typed hierarchy, never a bare `Error`. Each carries enough context to file a -useful bug report — brand, item type, strategy attempted, codec string. +Report reproducible bugs through [GitHub issues](https://github.com/su-engineering/heic-web/issues). Include browser/OS versions, the selected strategy or error context, and a redistributable sample when possible. Keep personal photographs and exploitable inputs out of public reports. -`HeicParseError`, `HeicUnsupportedError`, `HeicDecodeError`, `HeicAbortError`, -all extending `HeicError`. +Small fixes, regression tests, and consented fixture contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md). Report vulnerabilities privately using [SECURITY.md](SECURITY.md). -## Licence +## License -MIT +[MIT](LICENSE), copyright SU Engineering. See [third-party code](docs/compatibility.md#third-party-code) for optional codec licensing. diff --git a/SECURITY.md b/SECURITY.md index e3123b9..97233b7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,17 +16,18 @@ for anything that looks exploitable until a fix is out. ## Threat model -The input is hostile. The attacker controls every byte of the file, and the -attack surface is the container parser, not the codec — the actual HEVC decoding -is done by the browser or by libheif. +The input is hostile. The attacker controls every byte of the file. The attack +surface includes container parsing, decode planning, rendering, and the selected +browser or optional libheif codec. Upstream codecs are outside this repository, +but reports that help isolate a problem are welcome. -What the parser guarantees: +The parser and planner enforce these checks: -- **Every read is bounds-checked.** All byte access goes through one `Reader` - class, which validates against its window before touching the buffer. Nothing - else in the package indexes raw bytes, so this is enforceable by review. +- **Binary reads are bounds-checked.** The `Reader` class validates its window + before accessing the buffer. Specialized payload and codec parsers also use + explicit bounds checks. - **Box nesting is capped** at 32, and the number of sibling boxes at one level - at 65,536. A file cannot make the walker recurse or spin. + at 65,536. These bound the box walker's recursion and sibling iteration. - **No allocation on a declared size** without validating it against the real buffer first. A malformed `extent_length` cannot trigger a 4 GB allocation; a 64-bit box size above `Number.MAX_SAFE_INTEGER` is rejected rather than @@ -43,8 +44,9 @@ What the parser guarantees: What it does not defend against: - Bugs in the browser's own HEVC decoder or in libheif. Those are upstream. -- Resource exhaustion from a legitimately enormous but valid image, beyond the - caps above. Use `maxDimension` if you are decoding files you did not choose. +- Resource exhaustion from a large valid image within the caps above. + `maxDimension` scales the final result and does not cap peak decode memory. + Enforce application input/dimension limits and bound concurrent decodes. ## This is a client-side decoder @@ -57,5 +59,12 @@ cleanly in a browser can still be crafted to attack whatever handles it next. The parser is fuzzed against mutated real-world files on every run: byte flips, truncation, hostile 32-bit size and count fields, zeroed runs, and spliced regions. Every case must produce a typed `HeicError` rather than a crash, and -every case is time-boxed, because a hang is a denial of service on the user's own -tab and is treated as a worse bug than a crash. +completed cases are checked against an elapsed-time budget. This synchronous +fuzzer does not interrupt an infinite loop; process-level CI timeouts remain +necessary. + +## Supported versions + +Security fixes target the latest released version. There is currently no +long-term support commitment for older releases. Use current browser and +optional codec versions, and update when a security fix is released. diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..c6139a9 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,110 @@ +# API reference + +Import core exports from `@su-engineering/heic`. Import the optional adapter from `@su-engineering/heic/wasm`. The package is ESM; generated declarations are the authoritative TypeScript contract. + +## `decodeHeic(input, options?)` + +Returns `Promise`. Input is a `Blob` (including `File`), `ArrayBuffer`, or `Uint8Array`. The complete input is read and parsed before a decoding strategy is attempted. Do not modify an input buffer during decoding. + +| Option | Default | Behavior | +| --- | --- | --- | +| `strategy` | `'auto'` | Try native, then WebCodecs, then a supplied adapter. Set `'native'`, `'webcodecs'`, or `'wasm'` to restrict decoding to that path. | +| `colorSpace` | `'srgb'` | Request `'srgb'` or `'display-p3'` for compositing canvases. Native decoding uses browser defaults. | +| `maxDimension` | Unset | Downscale the final bitmap's longest side to this many pixels; never upscale. Use a positive finite number. Does not constrain peak memory. | +| `signal` | Unset | Cooperatively cancel via `AbortSignal`. Synchronous parsing/codec work and browser operations cannot always be interrupted immediately. | +| `wasmLoader` | Unset | Async function returning a `DecoderAdapter`, called only when the cascade reaches the fallback. A globally registered adapter takes precedence. | + +Cancellation example: + +```ts +const controller = new AbortController(); +const pending = decodeHeic(file, { signal: controller.signal }); +// When the user cancels: +controller.abort(); +await pending; // Rejects with HeicAbortError at a cancellation checkpoint. +``` + +### Result and ownership + +| Field | Meaning | +| --- | --- | +| `image` | Caller-owned `ImageBitmap`; call `close()` after rendering or otherwise consuming it. | +| `width`, `height` | Returned bitmap size, after optional downscaling. | +| `sourceWidth`, `sourceHeight` | Intrinsic display size after container transforms, before downscaling. | +| `strategy` | Successful path: `'native'`, `'webcodecs'`, or `'wasm'`. | +| `bitDepth` | Source bit depth from container/codec metadata; not a promise of output precision. | +| `isGrid`, `tileCount` | Whether the primary image is tiled, and its planned tile count. | +| `sourceColor` | `null`, ICC bytes (`{ type: 'icc', profile }`), or nclx primaries/transfer/matrix/full-range metadata. | +| `transformsApplied` | Counter-clockwise rotation, mirror direction, and whether clean-aperture cropping was applied. | +| `warnings` | Structured `{ code, message }` diagnostics for recognized unsupported features and unusual layouts. | + +The decoder returns pixels, not a new HEIC file. EXIF and other source metadata are not serialized into a new output file. Warnings are advisory and do not enumerate every unsupported feature. + +## `isHeic(input)` + +Returns `Promise` with `isHeic: boolean` and optional `brand`, `primaryItemType`, and `coding` (`'hevc'`, `'av1'`, or `'unknown'`). Inspects at most the first 64 KiB, including for a `Blob`. + +A malformed, ambiguous, or incomplete prefix can yield limited information. A positive identification does not guarantee a valid file or successful decode. AVIF shares HEIF brands; an AV1 result should be routed elsewhere. + +## `probeSupport()` + +Returns `Promise`: + +```ts +{ + native: boolean, + webcodecs: boolean, + hevcCodecStrings: string[], + recommended: 'native' | 'webcodecs' | 'wasm' | 'none', +} +``` + +The current implementation recommends `'wasm'` when both browser paths are unavailable, even when no adapter is installed. It does not load libheif. It tests a small inline native image and queries accepted WebCodecs configurations; it does not certify every file, profile, or output color space. + +## Errors + +`HeicError` extends `Error` and provides `context` (available brand, item ID/type, strategy, codec, offset, or box). Subclasses: + +| Error | Meaning | +| --- | --- | +| `HeicParseError` | Malformed/truncated input or a container structure the parser refuses. | +| `HeicUnsupportedError` | Unsupported image features, resource limits, or no successful strategy; includes `attempts`. | +| `HeicDecodeError` | A decoder or rendering operation failed. | +| `HeicAbortError` | Cancellation detected. | + +Auto mode collects strategy failures and can end with `HeicUnsupportedError` even when a decoder failed on corrupt data. Forced paths may expose a more direct decode error. Custom adapter/loader and platform errors can also propagate; always handle an unknown error in application code. + +## WASM adapters + +`wasmDecoder` is a reusable default adapter. `createWasmAdapter(options?)` creates an independent instance, loading and normalizing a libheif module once on first use. + +```ts +import { createWasmAdapter } from '@su-engineering/heic/wasm'; + +const adapter = createWasmAdapter({ + loadLibheif: () => import('/vendor/libheif-bundle.mjs'), +}); +await decodeHeic(file, { wasmLoader: async () => adapter }); +``` + +The URL above is an application-provided, self-hosted ESM WASM bundle, not an asset shipped by this package. For direct browser use, serve compatible adapter assets and their relative imports too. The default loader uses `libheif-js/wasm-bundle.js`; bare specifiers require a bundler or suitable import mapping. + +A custom `DecoderAdapter` has `name`, `appliesTransforms`, and an async `decode(request)` method. The request contains the complete `Uint8Array` data, requested color space, and optional signal. Return `{ image, width, height }`, where `image` is an `ImageBitmap` or `OffscreenCanvas`. Set `appliesTransforms: true` only if the pixels already include container `irot`, `imir`, and `clap`; otherwise the core applies them. Returned pixel resources are handed to the core; do not reuse them after returning. + +`registerDecoderAdapter(adapter)` sets a realm-wide fallback. Pass `undefined` to clear it. `getRegisteredAdapter()` retrieves it. Registration takes precedence over per-call loaders; prefer per-call loading for independent consumers. + +## Parser and inspection exports + +These functions do not decode pixels and accept `ArrayBuffer`/`Uint8Array` where a buffer is required. Their structures are exposed for diagnostics and advanced integrations; consult generated declarations before depending on them. + +| Export | Purpose | +| --- | --- | +| `parseHeif` | Parse the file into `HeifFile`: brands, primary item, items, locations, properties, and references. | +| `propertiesForItem`, `findProperty` | Resolve associated properties and find a typed property. | +| `readItemData` | Extract an item's payload from its extents. | +| `readGrid`, `parseGridPayload` | Read grid references and grid descriptors. | +| `planDecode` | Resolve an `ImagePlan` with tiles, groups, display size, transforms, and warnings. | +| `parseHvcC`, `hvccToCodecString` | Parse HEVC configuration and produce a WebCodecs codec string. | +| `hvccToAnnexBPrologue`, `lengthPrefixedToAnnexB` | Prepare HEVC NAL data for Annex B decoding. | + +Exported types include `BinaryInput`, `IsHeicResult`, `DecodeOptions`, `DecodedImage`, `Strategy`, `OutputColorSpace`, `SourceColor`, `TransformsApplied`, `HeicWarning`, `SupportReport`, `HeicErrorContext`, `DecoderAdapter`, `AdapterRequest`, `AdapterResult`, `HeifFile`, `ItemInfo`, `ItemLocation`, `ItemProperty`, `ItemProperties`, `ItemReferences`, `GridDescriptor`, `HvcC`, `ImagePlan`, `PlannedTile`, `TileGroup`, and `TransformOp`. The WASM entry point additionally exports `WasmAdapterOptions`. diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 0000000..78a470b --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,53 @@ +# Compatibility and limitations + +## Runtime requirements + +Pixel decoding targets modern browsers and dedicated browser workers. It uses `ImageBitmap` and `createImageBitmap`; software and WebCodecs compositing also require `OffscreenCanvas` with a 2D context. WebCodecs requires `VideoDecoder` and `EncodedVideoChunk`, normally in a secure context (HTTPS or localhost). + +The Node.js engine field describes package tooling compatibility, not a server pixel-decoding implementation. Parser/inspection functions can be used without browser rendering APIs. Development uses Node.js 22.12+. + +## Strategy selection + +| Strategy | Requirement | Fallback behavior | +| --- | --- | --- | +| `native` | Browser image decoder accepts the HEIC file and returns plausible primary dimensions. | Auto mode continues if unavailable or rejected. | +| `webcodecs` | Browser exposes WebCodecs and accepts the file's HEVC configuration. | Auto mode continues after decode failure. | +| `wasm` | Caller supplies/registers an adapter; default adapter needs `libheif-js`. | No implicit codec download; absence/failure ends the cascade. | + +Do not infer support from a browser name or version alone. Platform codecs, hardware, browser builds, and the input profile can change the outcome. `probeSupport()` is an advisory probe; the successful `decoded.strategy` reports what actually happened. + +## File support + +Supported primary items are HEVC `hvc1`/`hev1` images and supported HEVC tiled grids. Container `irot`, `imir`, and valid `clap` transforms are planned; native/libheif paths are expected to apply those themselves. Unsupported essential properties cause an error. + +The library decodes one primary image. Recognized alpha, depth/disparity, and HDR gain-map auxiliary items can generate warnings but are not composited. It does not decode AVIF/AV1, arbitrary derived-image types, animation, image sequences, Live Photo motion, or all HEIF extensions. Sequence and motion features are not exhaustively detected, so no warning does not establish their absence. + +## Color and HDR + +`sourceColor` describes parsed ICC or nclx metadata. Reporting metadata does not mean applying the ICC profile. `colorSpace: 'display-p3'` requests a compositing canvas space; native decoding follows browser defaults, and the libheif adapter writes decoded RGBA bytes into the requested canvas. Cross-strategy color equivalence and full wide-gamut/HDR preservation are not guaranteed. + +Source bit depth is metadata, not output precision. The default libheif path uses 8-bit RGBA output. Gain-map reconstruction and alpha compositing are outside current scope. Test color-critical workflows against independently rendered references on your target devices. + +## Memory and cancellation + +The planner caps the primary coded image at 256 million pixels and grid references at 4,096 tiles. These are rejection thresholds, not a memory budget or a guarantee that an accepted file will fit on a device. Tile buffers, decoder state, transforms, and canvases add overhead. + +`maxDimension` downsizes the output after decoding/compositing; it does not prevent full-resolution allocations. Enforce input-size and application dimension limits, limit concurrent jobs, and use a worker when blocking the UI is unacceptable. + +Cancellation is cooperative. An `AbortSignal` is checked around parts of the pipeline; synchronous work and an in-flight platform/codec operation may finish before cancellation is observed. Terminating an application-owned worker can provide a stronger boundary for expensive tasks. + +## What the tests establish + +- Unit tests cover parser behavior, bounds checks, HEVC configuration, and deterministic fixture mutations. +- Browser tests exercise the public built package, software decoding, transforms, dimensions, and workers across Playwright engines. +- Native and WebCodecs tests skip when capability probes fail. Headless Linux generally cannot validate platform HEVC decoding. +- The optional `chrome-hevc` project can exercise an installed browser with platform decoding. Released Safari still needs Apple-device testing. +- Committed fixtures include a tiny single-image file and generated asymmetric transform variants. They do not provide a broad real-world Apple grid/device corpus. Private local fixtures improve local coverage but are absent from CI. + +A green CI run establishes only the tests that executed. Review skips and the input corpus before making support claims. See [fixture documentation](https://github.com/su-engineering/heic-web/blob/master/test/fixtures/README.md). + +## Third-party code + +The core library and repository contributions are [MIT licensed](../LICENSE). `libheif-js` is an optional, separately distributed dependency; its package declares LGPL-3.0 and includes upstream codec code with separate notices. The browser's platform decoder is supplied by the browser/OS. + +Keep upstream license notices with any redistributed codec assets and review the licenses of the exact build you ship. This repository's MIT license does not relicense libheif or its bundled codecs. diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..321b73e --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,42 @@ +# Release checklist + +Releases are maintainer-operated. The repository does not automatically publish to npm on push. Changesets is configured for public package publishing with `master` as the base branch. + +## Before making the repository public + +- Review the complete Git history and tracked files for credentials, private photos, sensitive metadata, and code/assets you cannot redistribute. Ignoring a file now does not remove historical copies. +- Confirm the MIT license and ownership; review optional codec license notices separately. +- Set the GitHub description, documentation homepage, and relevant topics. Keep visibility private until the owner chooses to open-source it. +- Enable issues and configure private vulnerability reporting if desired; ensure the security contact is monitored. +- Configure branch protection or a ruleset requiring the CI `check` and `browser` jobs, with appropriate maintainer access. +- Verify CI on the release commit and manually test platform HEVC and released Safari using representative, consented Apple photos, including grids. + +## Prepare a version + +```sh +pnpm install --frozen-lockfile +pnpm test:all +pnpm exec changeset +pnpm exec changeset version +``` + +Review version/changelog changes and commit them. For an initial release already at the intended version, avoid an accidental extra version bump. Inspect the package before publishing: + +```sh +mkdir -p /tmp/heic-pack +pnpm pack --pack-destination /tmp/heic-pack +``` + +Install the tarball into a clean consumer project. Verify the ESM and declaration entry points, the `heic.global.js` browser bundle, and lazy fallback integration with the supported libheif build. The package should contain built assets, docs, README, security policy, and license, without test photographs or local dependencies. + +`pnpm test:package` checks declared build targets and executes the standalone bundle in a sandbox. `prepublishOnly` checks type safety, build targets, and unit tests; it does not run the browser suite. Browser checks and device testing remain release requirements. + +## Publish + +Confirm npm organization access and your authenticated publishing identity. When explicitly authorized to publish: + +```sh +pnpm release +``` + +This invokes `changeset publish`. Review the published version and registry access, then push any generated tags and create release notes summarizing behavior changes and known compatibility limits. Add provenance/trusted publishing through a separately reviewed release workflow if needed; never commit registry tokens. diff --git a/package.json b/package.json index d56351e..0c36ac7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "sideEffects": false, "files": [ "dist", + "docs", "README.md", + "CHANGELOG.md", + "CONTRIBUTING.md", "SECURITY.md", "LICENSE" ], @@ -32,12 +35,13 @@ "test": "vitest run", "test:unit": "vitest run", "test:browser": "playwright test", - "test:all": "pnpm typecheck && pnpm build && pnpm test:unit && pnpm test:browser", + "test:all": "pnpm typecheck && pnpm build && pnpm test:package && pnpm test:unit && pnpm test:browser", "fixtures": "sh tools/make-fixtures.sh", "dump": "node --experimental-strip-types tools/dump.ts", "validate-corpus": "node --experimental-strip-types tools/validate-corpus.ts", - "prepublishOnly": "pnpm typecheck && pnpm build && pnpm test:unit", - "release": "changeset publish" + "prepublishOnly": "pnpm typecheck && pnpm build && pnpm test:package && pnpm test:unit", + "release": "changeset publish", + "test:package": "node tools/check-package.mjs" }, "keywords": [ "heic", @@ -52,10 +56,12 @@ "@changesets/cli": "^2.27.10", "@playwright/test": "^1.62.1", "@types/node": "^22.20.1", + "esbuild": "^0.28.1", "libheif-js": "^1.19.8", "tsup": "^8.5.1", "typescript": "^5.9.3", - "vitest": "^2.1.9" + "vite": "^7.3.1", + "vitest": "^4.1.11" }, "peerDependencies": { "libheif-js": "^1.18.0" @@ -67,9 +73,23 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/su-engineering/heic.git" + "url": "git+https://github.com/su-engineering/heic-web.git" }, "engines": { "node": ">=18" + }, + "packageManager": "pnpm@9.15.9", + "homepage": "https://github.com/su-engineering/heic-web#readme", + "bugs": { + "url": "https://github.com/su-engineering/heic-web/issues" + }, + "pnpm": { + "overrides": { + "esbuild": "^0.28.1" + } + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..afe8206 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2070 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + esbuild: ^0.28.1 + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: ^2.27.10 + version: 2.31.1(@types/node@22.20.3) + '@playwright/test': + specifier: ^1.62.1 + version: 1.63.0 + '@types/node': + specifier: ^22.20.1 + version: 22.20.3 + esbuild: + specifier: ^0.28.1 + version: 0.28.2 + libheif-js: + specifier: ^1.19.8 + version: 1.23.2 + tsup: + specifier: ^8.5.1 + version: 8.5.1(postcss@8.5.28)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.6(@types/node@22.20.3) + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@22.20.3)(vite@7.3.6(@types/node@22.20.3)) + +packages: + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.31.1': + resolution: {integrity: sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==} + hasBin: true + + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@playwright/test@1.63.0': + resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} + engines: {node: '>=20'} + hasBin: true + + '@rollup/rollup-android-arm-eabi@4.63.3': + resolution: {integrity: sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.3': + resolution: {integrity: sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.3': + resolution: {integrity: sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.3': + resolution: {integrity: sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.3': + resolution: {integrity: sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.3': + resolution: {integrity: sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.3': + resolution: {integrity: sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.63.3': + resolution: {integrity: sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.63.3': + resolution: {integrity: sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.63.3': + resolution: {integrity: sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.63.3': + resolution: {integrity: sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.63.3': + resolution: {integrity: sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.63.3': + resolution: {integrity: sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.63.3': + resolution: {integrity: sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.63.3': + resolution: {integrity: sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.63.3': + resolution: {integrity: sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.63.3': + resolution: {integrity: sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.63.3': + resolution: {integrity: sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.63.3': + resolution: {integrity: sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.63.3': + resolution: {integrity: sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.3': + resolution: {integrity: sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.3': + resolution: {integrity: sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.3': + resolution: {integrity: sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.3': + resolution: {integrity: sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.3': + resolution: {integrity: sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@22.20.3': + resolution: {integrity: sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==} + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: ^0.28.1 + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + human-id@4.2.1: + resolution: {integrity: sha512-zPGsiS+dWoTZtZ4AtpA9Y+BdSFSNWvnouNlWNoUFyAM6xHOHmdCvqO3k8AIbdamCOv4gUFUVNPf6rJFfc4UiJw==} + hasBin: true + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-yaml@3.15.2: + resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} + hasBin: true + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + libheif-js@1.23.2: + resolution: {integrity: sha512-qvHIXtggEsw1lCNCWBYKloL2Z36DJBm0R9ThGiH2JnhKYdeZFLPFkP30Lw4yMskxxhx0bKg1gLrBHX1D2w2pSw==} + engines: {node: '>=8.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} + engines: {node: '>=20'} + hasBin: true + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.63.3: + resolution: {integrity: sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@babel/runtime@7.29.7': {} + + '@changesets/apply-release-plan@7.1.1': + dependencies: + '@changesets/config': 3.1.4 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.8.5 + + '@changesets/assemble-release-plan@6.0.10': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.8.5 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.31.1(@types/node@22.20.3)': + dependencies: + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.4 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@22.20.3) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.8.5 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.4': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.4': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.8.5 + + '@changesets/get-release-plan@4.0.16': + dependencies: + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.3.2 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.2.1 + prettier: 2.8.8 + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@inquirer/external-editor@1.0.3(@types/node@22.20.3)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 22.20.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.7 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.7 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + + '@playwright/test@1.63.0': + dependencies: + playwright: 1.63.0 + + '@rollup/rollup-android-arm-eabi@4.63.3': + optional: true + + '@rollup/rollup-android-arm64@4.63.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.3': + optional: true + + '@rollup/rollup-darwin-x64@4.63.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.3': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.3': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.3': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.3': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@12.20.55': {} + + '@types/node@22.20.3': + dependencies: + undici-types: 6.21.0 + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@7.3.6(@types/node@22.20.3))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.20.3) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + acorn@8.18.0: {} + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + any-promise@1.3.0: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + assertion-error@2.0.1: {} + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bundle-require@5.1.0(esbuild@0.28.2): + dependencies: + esbuild: 0.28.2 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + chai@6.2.2: {} + + chardet@2.2.0: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-indent@6.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + es-module-lexer@2.3.2: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + esprima@4.0.1: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + extendable-error@0.1.7: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.63.3 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fsevents@2.3.3: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + human-id@4.2.1: {} + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + joycon@3.1.1: {} + + js-yaml@3.15.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + libheif-js@1.23.2: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mlly@1.8.2: + dependencies: + acorn: 8.18.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + mri@1.2.0: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.19: {} + + object-assign@4.1.1: {} + + obug@2.2.1: {} + + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + pify@4.0.1: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + playwright-core@1.63.0: {} + + playwright@1.63.0: + dependencies: + playwright-core: 1.63.0 + + postcss-load-config@6.0.1(postcss@8.5.28): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.28 + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@2.8.8: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.15.2 + pify: 4.0.1 + strip-bom: 3.0.0 + + readdirp@4.1.2: {} + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + rollup@4.63.3: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.3 + '@rollup/rollup-android-arm64': 4.63.3 + '@rollup/rollup-darwin-arm64': 4.63.3 + '@rollup/rollup-darwin-x64': 4.63.3 + '@rollup/rollup-freebsd-arm64': 4.63.3 + '@rollup/rollup-freebsd-x64': 4.63.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.3 + '@rollup/rollup-linux-arm-musleabihf': 4.63.3 + '@rollup/rollup-linux-arm64-gnu': 4.63.3 + '@rollup/rollup-linux-arm64-musl': 4.63.3 + '@rollup/rollup-linux-loong64-gnu': 4.63.3 + '@rollup/rollup-linux-loong64-musl': 4.63.3 + '@rollup/rollup-linux-ppc64-gnu': 4.63.3 + '@rollup/rollup-linux-ppc64-musl': 4.63.3 + '@rollup/rollup-linux-riscv64-gnu': 4.63.3 + '@rollup/rollup-linux-riscv64-musl': 4.63.3 + '@rollup/rollup-linux-s390x-gnu': 4.63.3 + '@rollup/rollup-linux-x64-gnu': 4.63.3 + '@rollup/rollup-linux-x64-musl': 4.63.3 + '@rollup/rollup-openbsd-x64': 4.63.3 + '@rollup/rollup-openharmony-arm64': 4.63.3 + '@rollup/rollup-win32-arm64-msvc': 4.63.3 + '@rollup/rollup-win32-ia32-msvc': 4.63.3 + '@rollup/rollup-win32-x64-gnu': 4.63.3 + '@rollup/rollup-win32-x64-msvc': 4.63.3 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.17 + ts-interface-checker: 0.1.13 + + term-size@2.2.1: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(postcss@8.5.28)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.28.2) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.28.2 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.28) + resolve-from: 5.0.0 + rollup: 4.63.3 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.28 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + undici-types@6.21.0: {} + + universalify@0.1.2: {} + + vite@7.3.6(@types/node@22.20.3): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.28 + rollup: 4.63.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.3 + fsevents: 2.3.3 + + vitest@4.1.11(@types/node@22.20.3)(vite@7.3.6(@types/node@22.20.3)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@7.3.6(@types/node@22.20.3)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 7.3.6(@types/node@22.20.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.3 + transitivePeerDependencies: + - msw + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/test/browser/color.spec.ts b/test/browser/color.spec.ts index 302c185..f20d6db 100644 --- a/test/browser/color.spec.ts +++ b/test/browser/color.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { hasNative, hasWebCodecsHevc, openHarness } from './support.ts'; +import { hasNative, hasWebCodecsHevc, loadFixtures, openHarness } from './support.ts'; test.beforeEach(async ({ page }) => { await openHarness(page); @@ -16,7 +16,8 @@ test.beforeEach(async ({ page }) => { test('reports the source colour characteristics', async ({ page }) => { test.skip(!(await hasWebCodecsHevc(page)) && !(await hasNative(page)), 'no decode path'); - const results = await page.evaluate(async () => { + const fixtureUrls = loadFixtures().map((fixture) => fixture.url); + const results = await page.evaluate(async (urls) => { const read = async (url: string) => { const blob = await (await fetch(url)).blob(); const decoded = await window.heic.decodeHeic(blob); @@ -24,29 +25,25 @@ test('reports the source colour characteristics', async ({ page }) => { decoded.image.close(); return summary; }; - return { - // An Apple photo carrying a full ICC profile. - apple: await read('/test/fixtures/local/IMG_0679.HEIC'), - // A libheif-encoded file carrying CICP values instead. - libheif: await read('/test/fixtures/generated/asym-base.heic'), - }; - }); + return Promise.all(urls.map(read)); + }, fixtureUrls); - expect(results.apple.color?.type).toBe('icc'); - if (results.apple.color?.type === 'icc') { - expect(results.apple.color.profile.byteLength).toBeGreaterThan(100); + for (const result of results) { + if (result.color?.type === 'icc') { + expect(result.color.profile.byteLength).toBeGreaterThan(100); + } } - - expect(results.libheif.color?.type).toBe('nclx'); - if (results.libheif.color?.type === 'nclx') { + const libheif = results[fixtureUrls.indexOf('/test/fixtures/generated/asym-base.heic')]!; + expect(libheif.color?.type).toBe('nclx'); + if (libheif.color?.type === 'nclx') { // CICP: primaries, transfer and matrix are reported raw so a caller can // decide what they mean rather than being handed our interpretation. - expect(typeof results.libheif.color.primaries).toBe('number'); - expect(typeof results.libheif.color.transfer).toBe('number'); - expect(typeof results.libheif.color.fullRange).toBe('boolean'); + expect(typeof libheif.color.primaries).toBe('number'); + expect(typeof libheif.color.transfer).toBe('number'); + expect(typeof libheif.color.fullRange).toBe('boolean'); } // The generated fixtures are 10-bit, which is what pixi reports. - expect(results.libheif.bitDepth).toBe(10); + expect(libheif.bitDepth).toBe(10); }); test('composites in display-p3 when asked', async ({ page }) => { diff --git a/test/browser/cross-strategy.spec.ts b/test/browser/cross-strategy.spec.ts index 75145ea..0024dbb 100644 --- a/test/browser/cross-strategy.spec.ts +++ b/test/browser/cross-strategy.spec.ts @@ -115,7 +115,7 @@ test.describe('cross-strategy consistency', () => { test('the wasm strategy works on its own, with no WebCodecs involved', async ({ page }) => { // The path a Firefox or Linux-desktop user actually takes. const result = await page.evaluate(async () => { - const blob = await (await fetch('/test/fixtures/local/IMG_0679.HEIC')).blob(); + const blob = await (await fetch('/test/fixtures/generated/asym-irot-270.heic')).blob(); const decoded = await window.heic.decodeHeic(blob, { strategy: 'wasm', wasmLoader: async () => window.wasmAdapter(), @@ -132,10 +132,10 @@ test('the wasm strategy works on its own, with no WebCodecs involved', async ({ }); expect(result.strategy).toBe('wasm'); - // Rotated once, not twice: 4032x3024 coded, 3024x4032 displayed. - expect(result.width).toBe(3024); - expect(result.height).toBe(4032); + // Rotated once, not twice: 480x320 coded, 320x480 displayed. + expect(result.width).toBe(320); + expect(result.height).toBe(480); expect(result.rotation).toBe(270); // Reported from our parser even though libheif did the decoding. - expect(result.tileCount).toBe(48); + expect(result.tileCount).toBe(1); }); diff --git a/test/browser/decode-worker.js b/test/browser/decode-worker.js index b3842b4..549f906 100644 --- a/test/browser/decode-worker.js +++ b/test/browser/decode-worker.js @@ -8,6 +8,7 @@ * creeps in, this file is where it surfaces. */ import { decodeHeic, isHeic, probeSupport } from '/dist/index.js'; +import { createWasmAdapter } from '/dist/wasm.js'; self.onmessage = async (event) => { try { @@ -16,7 +17,12 @@ self.onmessage = async (event) => { const detection = await isHeic(blob); const report = await probeSupport(); - const decoded = await decodeHeic(blob, { strategy: event.data.strategy }); + const decoded = await decodeHeic(blob, { + strategy: event.data.strategy, + wasmLoader: async () => createWasmAdapter({ + loadLibheif: () => import('/node_modules/libheif-js/libheif-wasm/libheif-bundle.mjs'), + }), + }); self.postMessage({ ok: true, diff --git a/test/browser/decode.spec.ts b/test/browser/decode.spec.ts index 72a0a73..3691156 100644 --- a/test/browser/decode.spec.ts +++ b/test/browser/decode.spec.ts @@ -64,11 +64,12 @@ test.describe('isHeic', () => { test('reads only the first 64 KB of a Blob', async ({ page }) => { const sliced = await page.evaluate(async () => { - const blob = await (await fetch('/test/fixtures/local/IMG_3031.heic')).blob(); + const blob = await (await fetch('/test/fixtures/generated/asym-base.heic')).blob(); + const largeBlob = new Blob([blob, new Uint8Array(2_000_000)]); const ranges: { start: number; end: number }[] = []; // Wrap slice() to observe what the detector actually asks for. - const original = blob.slice.bind(blob); - const spy = new Proxy(blob, { + const original = largeBlob.slice.bind(largeBlob); + const spy = new Proxy(largeBlob, { get(target, property) { if (property === 'slice') { return (start: number, end: number) => { @@ -81,7 +82,7 @@ test.describe('isHeic', () => { }, }); await window.heic.isHeic(spy as Blob); - return { ranges, size: blob.size }; + return { ranges, size: largeBlob.size }; }); expect(sliced.size).toBeGreaterThan(1_000_000); expect(sliced.ranges).toEqual([{ start: 0, end: 65_536 }]); @@ -142,8 +143,8 @@ test.describe('decode', () => { test('maxDimension scales the result and leaves sourceWidth intact', async ({ page }) => { test.skip(!(await hasWebCodecsHevc(page)) && !(await hasNative(page)), 'no decode path'); const result = await page.evaluate(async () => { - const blob = await (await fetch('/test/fixtures/local/IMG_3031.heic')).blob(); - const decoded = await window.heic.decodeHeic(blob, { maxDimension: 512 }); + const blob = await (await fetch('/test/fixtures/generated/asym-base.heic')).blob(); + const decoded = await window.heic.decodeHeic(blob, { maxDimension: 240 }); const summary = { width: decoded.width, height: decoded.height, @@ -153,17 +154,17 @@ test.describe('decode', () => { decoded.image.close(); return summary; }); - expect(Math.max(result.width, result.height)).toBe(512); + expect(Math.max(result.width, result.height)).toBe(240); // Aspect ratio preserved, and the intrinsic size still reported. - expect(result.sourceWidth).toBe(3888); - expect(result.sourceHeight).toBe(6912); - expect(result.width / result.height).toBeCloseTo(3888 / 6912, 2); + expect(result.sourceWidth).toBe(480); + expect(result.sourceHeight).toBe(320); + expect(result.width / result.height).toBeCloseTo(480 / 320, 2); }); test('honours an AbortSignal mid-decode', async ({ page }) => { test.skip(!(await hasWebCodecsHevc(page)), 'WebCodecs path required to abort mid-tile'); const outcome = await page.evaluate(async () => { - const blob = await (await fetch('/test/fixtures/local/IMG_3031.heic')).blob(); + const blob = await (await fetch('/test/fixtures/generated/asym-base.heic')).blob(); const controller = new AbortController(); const promise = window.heic.decodeHeic(blob, { strategy: 'webcodecs', @@ -242,7 +243,7 @@ test.describe('resource leaks', () => { const outcome = await new Promise>((resolve) => { worker.onmessage = (event) => resolve(event.data); worker.onerror = (event) => resolve({ ok: false, error: `worker error: ${event.message}` }); - worker.postMessage({ url: '/test/fixtures/local/IMG_0679.HEIC', strategy: 'webcodecs' }); + worker.postMessage({ url: '/test/fixtures/generated/asym-irot-270.heic', strategy: 'webcodecs' }); }); worker.terminate(); return outcome; @@ -251,9 +252,9 @@ test.describe('resource leaks', () => { expect(result.ok, String(result.error)).toBe(true); expect(result.isHeic).toBe(true); expect(result.strategy).toBe('webcodecs'); - expect(result.width).toBe(3024); - expect(result.height).toBe(4032); - expect(result.tileCount).toBe(48); + expect(result.width).toBe(320); + expect(result.height).toBe(480); + expect(result.tileCount).toBe(1); // The rotation is applied inside the worker too, via OffscreenCanvas only. expect(result.rotation).toBe(270); }); diff --git a/test/browser/framesize.spec.ts b/test/browser/framesize.spec.ts index 2d8e024..05fbf92 100644 --- a/test/browser/framesize.spec.ts +++ b/test/browser/framesize.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { hasWebCodecsHevc, openHarness } from './support.ts'; +import { hasWebCodecsHevc, loadFixtures, openHarness } from './support.ts'; /** * The decoder's output frame size must match the item's `ispe`. @@ -16,11 +16,7 @@ test.beforeEach(async ({ page }) => { await openHarness(page); }); -for (const url of [ - '/test/fixtures/libheif-2x2-single.heic', - '/test/fixtures/generated/asym-base.heic', - '/test/fixtures/local/IMG_0679.HEIC', -]) { +for (const { url } of loadFixtures()) { test(`decoder frame size matches ispe for ${url.split('/').pop()}`, async ({ page }) => { test.skip(!(await hasWebCodecsHevc(page)), 'WebCodecs path required'); diff --git a/test/browser/wasm.spec.ts b/test/browser/wasm.spec.ts index 316caa5..7ff48fc 100644 --- a/test/browser/wasm.spec.ts +++ b/test/browser/wasm.spec.ts @@ -117,3 +117,47 @@ test.describe('wasm strategy', () => { expect(outcome.loaderCalled).toBe(false); }); }); + + +test('default WASM adapter decodes through a bundled consumer', async ({ page }) => { + const result = await page.evaluate(async () => { + // Built by test:package; this uses the documented default import, rather + // than the harness's custom ESM loader. + const url = '/test/.consumer/stdin.js'; + const consumer = await import(/* @vite-ignore */ url); + const file = await (await fetch('/test/fixtures/generated/asym-irot-90.heic')).blob(); + const decoded = await consumer.decodeHeic(file, { + strategy: 'wasm', + wasmLoader: async () => consumer.wasmDecoder, + maxDimension: 160, + }); + const summary = { + width: decoded.width, height: decoded.height, + sourceWidth: decoded.sourceWidth, sourceHeight: decoded.sourceHeight, + strategy: decoded.strategy, + }; + decoded.image.close(); + return summary; + }); + expect(result).toEqual({ width: 107, height: 160, sourceWidth: 320, sourceHeight: 480, strategy: 'wasm' }); +}); + +test('WASM decoding works in a dedicated worker', async ({ page }) => { + const result = await page.evaluate(async () => { + const worker = new Worker('/test/browser/decode-worker.js', { type: 'module' }); + try { + return await new Promise>((resolve, reject) => { + worker.onmessage = (event) => resolve(event.data); + worker.onerror = (event) => reject(new Error(event.message)); + worker.postMessage({ url: '/test/fixtures/generated/asym-irot-270.heic', strategy: 'wasm' }); + }); + } finally { + worker.terminate(); + } + }); + expect(result.ok, String(result.error)).toBe(true); + expect(result.strategy).toBe('wasm'); + expect(result.width).toBe(320); + expect(result.height).toBe(480); + expect(result.rotation).toBe(270); +}); diff --git a/tools/check-package.mjs b/tools/check-package.mjs new file mode 100644 index 0000000..cb30742 --- /dev/null +++ b/tools/check-package.mjs @@ -0,0 +1,40 @@ +/** Validate every declared package target and the standalone browser build. */ +import assert from 'node:assert/strict'; +import { existsSync, readFileSync } from 'node:fs'; +import vm from 'node:vm'; +import { build } from 'esbuild'; + +const pkg = JSON.parse(readFileSync('package.json', 'utf8')); +const targets = new Set([pkg.main, pkg.types, pkg.unpkg, pkg.jsdelivr]); +function collect(value) { + if (typeof value === 'string') targets.add(value); + else for (const child of Object.values(value)) collect(child); +} +collect(pkg.exports); +for (const target of targets) { + assert.ok(existsSync(target), `Missing declared package target: ${target}`); +} +const context = vm.createContext({}); +vm.runInContext(readFileSync(pkg.unpkg, 'utf8'), context); +assert.equal(typeof context.HeicDecoder.decodeHeic, 'function'); +assert.equal(typeof context.HeicDecoder.probeSupport, 'function'); +const core = await import('../dist/index.js'); +const fallback = await import('../dist/wasm.js'); +assert.equal(typeof core.decodeHeic, 'function'); +assert.equal(typeof fallback.createWasmAdapter, 'function'); +console.log(`Verified ${targets.size} package targets, ESM entry points, and browser global.`); + +// Exercise package self-reference and the default optional codec import as a +// real browser bundler sees them, without shipping test assets in dist/. +await build({ + stdin: { + contents: "export { decodeHeic } from '@su-engineering/heic'; export { wasmDecoder } from '@su-engineering/heic/wasm';", + resolveDir: process.cwd(), + }, + bundle: true, + format: 'esm', + platform: 'browser', + splitting: true, + outdir: 'test/.consumer', +}); +console.log('Built browser consumer with the default lazy WASM adapter.'); diff --git a/tsup.config.ts b/tsup.config.ts index 8fa4d36..2b328b1 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -14,7 +14,7 @@ export default defineConfig([ }, { // CDN build: - entry: { 'heic.global': 'src/index.ts' }, + entry: { heic: 'src/index.ts' }, format: ['iife'], globalName: 'HeicDecoder', dts: false, diff --git a/wasm/index.ts b/wasm/index.ts index 33ddd02..6e461ac 100644 --- a/wasm/index.ts +++ b/wasm/index.ts @@ -28,7 +28,7 @@ interface LibheifModule { export interface WasmAdapterOptions { /** - * Supplies the libheif module. Defaults to `import('libheif-js')`. + * Supplies the libheif module. Defaults to `import('libheif-js/wasm-bundle.js')`. * * Override it to pin a specific build, to serve the wasm from your own origin, * or to reuse an instance you already loaded. In a browser without a bundler, @@ -49,7 +49,7 @@ export interface WasmAdapterOptions { * The module is loaded once, on first decode, and reused. */ export function createWasmAdapter(options: WasmAdapterOptions = {}): DecoderAdapter { - const load = options.loadLibheif ?? ((): Promise => import('libheif-js')); + const load = options.loadLibheif ?? ((): Promise => import('libheif-js/wasm-bundle.js')); let modulePromise: Promise | undefined; diff --git a/wasm/libheif-js.d.ts b/wasm/libheif-js.d.ts index 662ac2e..f33e28f 100644 --- a/wasm/libheif-js.d.ts +++ b/wasm/libheif-js.d.ts @@ -4,7 +4,7 @@ * either way; the structural interfaces the adapter actually relies on are * declared in index.ts next to the code that uses them. */ -declare module 'libheif-js' { +declare module 'libheif-js/wasm-bundle.js' { const libheif: unknown; export default libheif; } From a56d67845a34687ce401ad5e7d9377c9143237de Mon Sep 17 00:00:00 2001 From: Mahir Senturk Date: Fri, 18 Sep 2026 22:48:10 +0200 Subject: [PATCH 2/2] Check fixture-specific HEVC capabilities in browser tests --- .github/workflows/ci.yml | 9 +++--- docs/compatibility.md | 2 +- test/browser/annexb.spec.ts | 3 +- test/browser/color.spec.ts | 5 ++- test/browser/cross-strategy.spec.ts | 2 +- test/browser/decode.spec.ts | 2 +- test/browser/framesize.spec.ts | 2 +- test/browser/support.ts | 50 ++++++++++++++++++++++------- 8 files changed, 51 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3b14e5..2b5ab35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,10 +48,9 @@ jobs: - run: pnpm exec playwright install --with-deps chromium firefox webkit - run: pnpm test:browser - # Headless Linux browsers have no platform HEVC decoder, so the WebCodecs - # strategy does not run here and its tests skip themselves. What this job - # actually covers is the container parser and the wasm fallback, on all - # three engines. + # Headless Linux generally lacks platform HEVC decoding. Some WebKit + # builds accept limited HEVC profiles. Tests check each fixture config; + # software fallback tests run unconditionally on all three engines. # # A green badge on this workflow does NOT mean the primary decode path is # tested. That needs a real device matrix — see the chrome-hevc project in @@ -59,7 +58,7 @@ jobs: # HEIC_CHROME="/path/to/chrome" pnpm test:browser --project=chrome-hevc - name: Note on coverage run: | - echo "::notice::WebCodecs HEVC does not run in headless CI. This job covers the parser and the wasm path only." + echo "::notice::Platform HEVC needs device testing. CI covers parser/WASM paths and any fixture-specific WebCodecs configurations accepted by its browsers." - uses: actions/upload-artifact@v4 if: failure() diff --git a/docs/compatibility.md b/docs/compatibility.md index 78a470b..28fcd08 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -40,7 +40,7 @@ Cancellation is cooperative. An `AbortSignal` is checked around parts of the pip - Unit tests cover parser behavior, bounds checks, HEVC configuration, and deterministic fixture mutations. - Browser tests exercise the public built package, software decoding, transforms, dimensions, and workers across Playwright engines. -- Native and WebCodecs tests skip when capability probes fail. Headless Linux generally cannot validate platform HEVC decoding. +- Native and WebCodecs tests skip when capability probes fail. Headless Linux generally cannot validate platform HEVC decoding; some WebKit builds accept limited profiles through software decoders. Tests query each fixture configuration. - The optional `chrome-hevc` project can exercise an installed browser with platform decoding. Released Safari still needs Apple-device testing. - Committed fixtures include a tiny single-image file and generated asymmetric transform variants. They do not provide a broad real-world Apple grid/device corpus. Private local fixtures improve local coverage but are absent from CI. diff --git a/test/browser/annexb.spec.ts b/test/browser/annexb.spec.ts index a6b7b32..0d20fa2 100644 --- a/test/browser/annexb.spec.ts +++ b/test/browser/annexb.spec.ts @@ -71,7 +71,8 @@ test('an Annex B stream built from our helpers decodes in a real VideoDecoder', return { codec, supported: true, frames, size, failure }; }); - expect(result.supported, `this browser rejected ${result.codec}`).toBe(true); + test.skip(!result.supported, `this browser rejected the fixture's Annex B codec ${result.codec}`); + expect(result.supported).toBe(true); expect(result.failure).toBeUndefined(); expect(result.frames).toBe(1); expect(result.size).toBe('480x320'); diff --git a/test/browser/color.spec.ts b/test/browser/color.spec.ts index f20d6db..f696408 100644 --- a/test/browser/color.spec.ts +++ b/test/browser/color.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { hasNative, hasWebCodecsHevc, loadFixtures, openHarness } from './support.ts'; +import { hasWebCodecsHevc, loadFixtures, openHarness } from './support.ts'; test.beforeEach(async ({ page }) => { await openHarness(page); @@ -14,13 +14,12 @@ test.beforeEach(async ({ page }) => { * So the source characteristics are reported and the output space is an option. */ test('reports the source colour characteristics', async ({ page }) => { - test.skip(!(await hasWebCodecsHevc(page)) && !(await hasNative(page)), 'no decode path'); const fixtureUrls = loadFixtures().map((fixture) => fixture.url); const results = await page.evaluate(async (urls) => { const read = async (url: string) => { const blob = await (await fetch(url)).blob(); - const decoded = await window.heic.decodeHeic(blob); + const decoded = await window.heic.decodeHeic(blob, { strategy: 'wasm', wasmLoader: async () => window.wasmAdapter() }); const summary = { color: decoded.sourceColor, bitDepth: decoded.bitDepth }; decoded.image.close(); return summary; diff --git a/test/browser/cross-strategy.spec.ts b/test/browser/cross-strategy.spec.ts index 0024dbb..d2f3c4d 100644 --- a/test/browser/cross-strategy.spec.ts +++ b/test/browser/cross-strategy.spec.ts @@ -39,7 +39,7 @@ test.describe('cross-strategy consistency', () => { for (const fixture of loadFixtures()) { test(`${fixture.name}: WebCodecs and wasm agree`, async ({ page }) => { test.skip( - !(await hasWebCodecsHevc(page)), + !(await hasWebCodecsHevc(page, fixture.url)), 'needs the WebCodecs path to compare against', ); diff --git a/test/browser/decode.spec.ts b/test/browser/decode.spec.ts index 3691156..9b3d7fb 100644 --- a/test/browser/decode.spec.ts +++ b/test/browser/decode.spec.ts @@ -92,7 +92,7 @@ test.describe('isHeic', () => { test.describe('decode', () => { for (const fixture of loadFixtures()) { test(`decodes ${fixture.name} (${fixture.device})`, async ({ page }) => { - const supported = (await hasWebCodecsHevc(page)) || (await hasNative(page)); + const supported = (await hasWebCodecsHevc(page, fixture.url)) || (await hasNative(page, fixture.url)); test.skip( !supported, 'no native or WebCodecs HEVC decode in this browser; the wasm path is covered separately', diff --git a/test/browser/framesize.spec.ts b/test/browser/framesize.spec.ts index 05fbf92..357ad3a 100644 --- a/test/browser/framesize.spec.ts +++ b/test/browser/framesize.spec.ts @@ -18,7 +18,7 @@ test.beforeEach(async ({ page }) => { for (const { url } of loadFixtures()) { test(`decoder frame size matches ispe for ${url.split('/').pop()}`, async ({ page }) => { - test.skip(!(await hasWebCodecsHevc(page)), 'WebCodecs path required'); + test.skip(!(await hasWebCodecsHevc(page, url)), 'WebCodecs path required'); const result = await page.evaluate(async (fixtureUrl) => { const bytes = new Uint8Array(await (await fetch(fixtureUrl)).arrayBuffer()); diff --git a/test/browser/support.ts b/test/browser/support.ts index 0b9c410..a1fe4f8 100644 --- a/test/browser/support.ts +++ b/test/browser/support.ts @@ -73,17 +73,45 @@ export async function openHarness(page: Page): Promise { if (errors.length > 0) throw new Error(`harness failed to load: ${errors.join('; ')}`); } -/** Whether this browser can run the WebCodecs strategy at all. */ -export async function hasWebCodecsHevc(page: Page): Promise { - return page.evaluate(async () => { - const report = await (window as never as { heic: { probeSupport(): Promise<{ webcodecs: boolean }> } }).heic.probeSupport(); - return report.webcodecs; - }); +/** Check the actual fixture profile, not only a generic HEVC probe. */ +export async function hasWebCodecsHevc( + page: Page, + url = '/test/fixtures/generated/asym-base.heic', +): Promise { + return page.evaluate(async (fixtureUrl) => { + if (typeof VideoDecoder === 'undefined') return false; + const response = await fetch(fixtureUrl); + if (!response.ok) throw new Error(`Fixture fetch failed: ${fixtureUrl}`); + const plan = window.heic.planDecode(new Uint8Array(await response.arrayBuffer())); + for (const group of plan.tileGroups) { + const tile = plan.tiles[group.tileIndices[0]!]!; + const configs: VideoDecoderConfig[] = [ + { codec: group.codec, description: new Uint8Array(group.hvcc.raw), codedWidth: tile.width, codedHeight: tile.height, optimizeForLatency: true }, + { codec: window.heic.hvccToCodecString(group.hvcc, 'hev1'), codedWidth: tile.width, codedHeight: tile.height, optimizeForLatency: true }, + ]; + let supported = false; + for (const config of configs) { + try { + if ((await VideoDecoder.isConfigSupported(config)).supported) supported = true; + } catch { /* An unsupported configuration can reject instead of returning false. */ } + } + if (!supported) return false; + } + return true; + }, url); } -export async function hasNative(page: Page): Promise { - return page.evaluate(async () => { - const report = await (window as never as { heic: { probeSupport(): Promise<{ native: boolean }> } }).heic.probeSupport(); - return report.native; - }); +export async function hasNative( + page: Page, + url = '/test/fixtures/generated/asym-base.heic', +): Promise { + return page.evaluate(async (fixtureUrl) => { + const response = await fetch(fixtureUrl); + if (!response.ok) throw new Error(`Fixture fetch failed: ${fixtureUrl}`); + try { + const image = await createImageBitmap(await response.blob()); + image.close(); + return true; + } catch { return false; } + }, url); }