├── .github ├── FUNDING.yml ├── dependabot.yml ├── release.yml └── workflows │ ├── bump.yml │ ├── ci.yml │ ├── dependabot.yml │ ├── docs.yml │ └── publish.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── biome.json ├── bun.lock ├── package.json ├── scripts └── exports.ts ├── src ├── index.ts └── lib │ ├── r2-file-storage.test.ts │ └── r2-file-storage.ts ├── tsconfig.json └── typedoc.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sergiodxa 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "monthly" 9 | reviewers: 10 | - "sergiodxa" 11 | assignees: 12 | - "sergiodxa" 13 | 14 | - package-ecosystem: bun 15 | directory: / 16 | schedule: 17 | interval: "weekly" 18 | reviewers: 19 | - "sergiodxa" 20 | assignees: 21 | - "sergiodxa" 22 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: New Features 4 | labels: 5 | - enhancement 6 | - title: Documentation Changes 7 | labels: 8 | - documentation 9 | - title: Bug Fixes 10 | labels: 11 | - bug 12 | - title: Example 13 | labels: 14 | - example 15 | - title: Deprecations 16 | labels: 17 | - deprecated 18 | - title: Other Changes 19 | labels: 20 | - "*" 21 | -------------------------------------------------------------------------------- /.github/workflows/bump.yml: -------------------------------------------------------------------------------- 1 | name: Bump version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Type of version to bump" 8 | required: true 9 | type: choice 10 | options: 11 | - major 12 | - minor 13 | - patch 14 | 15 | jobs: 16 | bump-version: 17 | name: Bump version 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | ssh-key: ${{ secrets.DEPLOY_KEY }} 23 | 24 | - uses: oven-sh/setup-bun@v2 25 | - run: bun install --frozen-lockfile 26 | 27 | - uses: actions/setup-node@v4 28 | with: 29 | node-version: "lts/*" 30 | 31 | - run: | 32 | git config user.name 'Sergio Xalambrí' 33 | git config user.email 'hello@sergiodxa.com' 34 | 35 | - run: npm version ${{ github.event.inputs.version }} 36 | - run: bun run quality:fix 37 | - run: git push origin main --follow-tags 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: oven-sh/setup-bun@v2 12 | - run: bun install --frozen-lockfile 13 | - run: bun run build 14 | 15 | typecheck: 16 | name: Typechecker 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: oven-sh/setup-bun@v2 21 | - run: bun install --frozen-lockfile 22 | - run: bun run typecheck 23 | 24 | quality: 25 | name: Code Quality 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: oven-sh/setup-bun@v2 30 | - run: bun install --frozen-lockfile 31 | - run: bun run quality 32 | 33 | test: 34 | name: Tests 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: oven-sh/setup-bun@v2 39 | - run: bun install --frozen-lockfile 40 | - run: bun test 41 | 42 | exports: 43 | name: Verify Exports 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: oven-sh/setup-bun@v2 48 | - run: bun install --frozen-lockfile 49 | - run: bun run exports 50 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Enable auto-merge for Dependabot PRs 2 | 3 | on: 4 | pull_request: 5 | types: opened 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | dependabot: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.actor == 'dependabot[bot]' }} 15 | steps: 16 | - id: metadata 17 | uses: dependabot/fetch-metadata@v2 18 | with: 19 | github-token: "${{ secrets.GITHUB_TOKEN }}" 20 | 21 | - run: gh pr merge --auto --squash "$PR_URL" 22 | env: 23 | PR_URL: ${{github.event.pull_request.html_url}} 24 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 25 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Documentation 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "docs" 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | deploy: 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: oven-sh/setup-bun@v2 26 | - run: bun install --frozen-lockfile 27 | - run: bunx typedoc 28 | - uses: actions/configure-pages@v5 29 | - uses: actions/upload-pages-artifact@v3 30 | with: 31 | path: "./docs" 32 | - uses: actions/deploy-pages@v4 33 | id: deployment 34 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-gpr: 9 | name: "Publish to GitHub Package Registry" 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: oven-sh/setup-bun@v2 17 | - run: bun install --frozen-lockfile 18 | - run: bun run build 19 | - run: bun run exports 20 | 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: "lts/*" 24 | registry-url: https://npm.pkg.github.com/ 25 | 26 | - run: npm publish --access public 27 | env: 28 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 29 | 30 | publish-npm: 31 | name: "Publish to npm" 32 | runs-on: ubuntu-latest 33 | permissions: 34 | contents: read 35 | id-token: write 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: oven-sh/setup-bun@v2 39 | - run: bun install --frozen-lockfile 40 | - run: bun run build 41 | - run: bun run exports 42 | 43 | - uses: actions/setup-node@v4 44 | with: 45 | node-version: "lts/*" 46 | registry-url: "https://registry.npmjs.org" 47 | 48 | - run: npm publish --provenance --access public 49 | env: 50 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /coverage 3 | /docs 4 | /node_modules -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "biomejs.biome" 4 | }, 5 | "[javascriptreact]": { 6 | "editor.defaultFormatter": "biomejs.biome" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "biomejs.biome" 10 | }, 11 | "[jsonc]": { 12 | "editor.defaultFormatter": "biomejs.biome" 13 | }, 14 | "[typescript]": { 15 | "editor.defaultFormatter": "biomejs.biome" 16 | }, 17 | "[typescriptreact]": { 18 | "editor.defaultFormatter": "biomejs.biome" 19 | }, 20 | "editor.codeActionsOnSave": { 21 | "source.organizeImports.biome": "explicit", 22 | "quickfix.biome": "explicit" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution 2 | 3 | ## Setup 4 | 5 | Run `bun install` to install the dependencies. 6 | 7 | Run the tests with `bun test`. 8 | 9 | Run the code quality checker with `bun run quality`. 10 | 11 | Run the typechecker with `bun run typecheck`. 12 | 13 | Run the exports checker with `bun run exports`. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sergio Xalambrí 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @edgefirst-dev/r2-file-storage 2 | 3 | An implementation of [@mjackson/file-storage](https://github.com/mjackson/remix-the-web/tree/main/packages/file-storage) that uses R2 as the storage backend. 4 | 5 | ## Features 6 | 7 | - Simple, intuitive key/value API (like [Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API), but for `File`s instead of strings) 8 | - A generic `FileStorage` class that works for various Cloudflare R2. 9 | - Preserves all `File` metadata including `file.name`, `file.type`, and `file.lastModified` 10 | 11 | ## Installation 12 | 13 | Install from npm or GitHub Package Registry with; 14 | 15 | ```sh 16 | bun add @edgefirst-dev/r2-file-storage 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```ts 22 | import { R2FileStorage } from "@edgefirst-dev/r2-file-storage"; 23 | 24 | let storage = new R2FileStorage(r2); // Get r2 from your Cloudflare bindings 25 | 26 | let file = new File(["hello world"], "hello.txt", { type: "text/plain" }); 27 | let key = "hello-key"; 28 | 29 | // Set the file in storage. 30 | await storage.set(key, file); 31 | 32 | let file = await storage.put(key, file); 33 | 34 | // Then, sometime later... 35 | let fileFromStorage = await storage.get(key); 36 | // All of the original file's metadata is intact 37 | fileFromStorage.name; // 'hello.txt' 38 | fileFromStorage.type; // 'text/plain' 39 | 40 | // To remove from storage 41 | await storage.remove(key); 42 | 43 | await storage.list({ prefix: "hello" }); // List all files with keys starting with "hello" 44 | ``` 45 | 46 | ## License 47 | 48 | See [LICENSE](./LICENSE) 49 | 50 | ## Author 51 | 52 | - [Sergio Xalambrí](https://sergiodxa.com) 53 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.7.1/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "correctness": { 11 | "useHookAtTopLevel": "error" 12 | }, 13 | "performance": { 14 | "noBarrelFile": "error", 15 | "noReExportAll": "error" 16 | }, 17 | "style": { 18 | "noDefaultExport": "error", 19 | "noNegationElse": "error", 20 | "useConst": "off", 21 | "useExportType": "off", 22 | "useImportType": "off" 23 | }, 24 | "suspicious": { 25 | "noConsoleLog": "warn", 26 | "noEmptyBlockStatements": "warn", 27 | "noSkippedTests": "error" 28 | } 29 | } 30 | }, 31 | "formatter": { "enabled": true }, 32 | "vcs": { 33 | "enabled": true, 34 | "clientKind": "git", 35 | "defaultBranch": "main", 36 | "useIgnoreFile": true 37 | }, 38 | "overrides": [ 39 | { 40 | "include": ["**/*.md"], 41 | "formatter": { "indentStyle": "tab" } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "@edgefirst-dev/r2-file-storage", 6 | "devDependencies": { 7 | "@arethetypeswrong/cli": "^0.18.1", 8 | "@biomejs/biome": "^1.9.4", 9 | "@total-typescript/tsconfig": "^1.0.4", 10 | "@types/bun": "^1.2.14", 11 | "consola": "^3.2.3", 12 | "typedoc": "^0.28.0", 13 | "typedoc-plugin-mdn-links": "^5.0.1", 14 | "typescript": "^5.7.2", 15 | }, 16 | "peerDependencies": { 17 | "@cloudflare/workers-types": "^4.20250523.0", 18 | "@mjackson/file-storage": "^0.6.1", 19 | }, 20 | }, 21 | }, 22 | "packages": { 23 | "@andrewbranch/untar.js": ["@andrewbranch/untar.js@1.0.3", "", {}, "sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw=="], 24 | 25 | "@arethetypeswrong/cli": ["@arethetypeswrong/cli@0.18.1", "", { "dependencies": { "@arethetypeswrong/core": "0.18.1", "chalk": "^4.1.2", "cli-table3": "^0.6.3", "commander": "^10.0.1", "marked": "^9.1.2", "marked-terminal": "^7.1.0", "semver": "^7.5.4" }, "bin": { "attw": "dist/index.js" } }, "sha512-SS1Z5gRSvbP4tl98KlNygSUp3Yfenktt782MQKEbYm6GFPowztnnvdEUhQGm2uVDIH4YkU6av+n8Lm6OEOigqA=="], 26 | 27 | "@arethetypeswrong/core": ["@arethetypeswrong/core@0.18.1", "", { "dependencies": { "@andrewbranch/untar.js": "^1.0.3", "@loaderkit/resolve": "^1.0.2", "cjs-module-lexer": "^1.2.3", "fflate": "^0.8.2", "lru-cache": "^11.0.1", "semver": "^7.5.4", "typescript": "5.6.1-rc", "validate-npm-package-name": "^5.0.0" } }, "sha512-uUw47cLgB6zYOpAxFp94NG/J9ev0wcOC+UOmTCFEWtbDEn4vpR0ScoPxD7LCGcPczOd7bDJSJL/gMSz3BknYcw=="], 28 | 29 | "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], 30 | 31 | "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], 32 | 33 | "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], 34 | 35 | "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], 36 | 37 | "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], 38 | 39 | "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], 40 | 41 | "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], 42 | 43 | "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], 44 | 45 | "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], 46 | 47 | "@braidai/lang": ["@braidai/lang@1.1.0", "", {}, "sha512-xyJYkiyNQtTyCLeHxZmOs7rnB94D+N1IjKNArQIh8+8lTBOY7TFgwEV+Ow5a1uaBi5j2w9fLbWcJFTWLDItl5g=="], 48 | 49 | "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250523.0", "", {}, "sha512-0ghPbRDqjORo4qX8YkayNvJc7SSfK8cn5YhSkx1316vd+32cSlhqJTz8sRiavZiJw79glPq0rO1TNTJwvuuZOg=="], 50 | 51 | "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], 52 | 53 | "@gerrit0/mini-shiki": ["@gerrit0/mini-shiki@3.2.2", "", { "dependencies": { "@shikijs/engine-oniguruma": "^3.2.1", "@shikijs/langs": "^3.2.1", "@shikijs/themes": "^3.2.1", "@shikijs/types": "^3.2.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-vaZNGhGLKMY14HbF53xxHNgFO9Wz+t5lTlGNpl2N9xFiKQ0I5oIe0vKjU9dh7Nb3Dw6lZ7wqUE0ri+zcdpnK+Q=="], 54 | 55 | "@loaderkit/resolve": ["@loaderkit/resolve@1.0.4", "", { "dependencies": { "@braidai/lang": "^1.0.0" } }, "sha512-rJzYKVcV4dxJv+vW6jlvagF8zvGxHJ2+HTr1e2qOejfmGhAApgJHl8Aog4mMszxceTRiKTTbnpgmTO1bEZHV/A=="], 56 | 57 | "@mjackson/file-storage": ["@mjackson/file-storage@0.6.1", "", { "dependencies": { "@mjackson/lazy-file": "^3.3.1" } }, "sha512-H3GEVpmfmNryNoYloddIOba5OAwckfVGMvutPeI94Shbv/R+NVh89gIYa8SK3Vfa+ky9PitclP+5XQ6/zlSdQQ=="], 58 | 59 | "@mjackson/lazy-file": ["@mjackson/lazy-file@3.3.1", "", { "dependencies": { "mrmime": "^2.0.0" } }, "sha512-BxpNT1KmLx0OLYfgQESx/AKGD2czwfZXh9c0SaDUQY2DRAaVYtAvSQE5EkpATFdQQKqfL+iXVoaQ/SN+w7/CDA=="], 60 | 61 | "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.2.1", "", { "dependencies": { "@shikijs/types": "3.2.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-wZZAkayEn6qu2+YjenEoFqj0OyQI64EWsNR6/71d1EkG4sxEOFooowKivsWPpaWNBu3sxAG+zPz5kzBL/SsreQ=="], 62 | 63 | "@shikijs/langs": ["@shikijs/langs@3.2.1", "", { "dependencies": { "@shikijs/types": "3.2.1" } }, "sha512-If0iDHYRSGbihiA8+7uRsgb1er1Yj11pwpX1c6HLYnizDsKAw5iaT3JXj5ZpaimXSWky/IhxTm7C6nkiYVym+A=="], 64 | 65 | "@shikijs/themes": ["@shikijs/themes@3.2.1", "", { "dependencies": { "@shikijs/types": "3.2.1" } }, "sha512-k5DKJUT8IldBvAm8WcrDT5+7GA7se6lLksR+2E3SvyqGTyFMzU2F9Gb7rmD+t+Pga1MKrYFxDIeyWjMZWM6uBQ=="], 66 | 67 | "@shikijs/types": ["@shikijs/types@3.2.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA=="], 68 | 69 | "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], 70 | 71 | "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], 72 | 73 | "@total-typescript/tsconfig": ["@total-typescript/tsconfig@1.0.4", "", {}, "sha512-fO4ctMPGz1kOFOQ4RCPBRBfMy3gDn+pegUfrGyUFRMv/Rd0ZM3/SHH3hFCYG4u6bPLG8OlmOGcBLDexvyr3A5w=="], 74 | 75 | "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], 76 | 77 | "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], 78 | 79 | "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], 80 | 81 | "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], 82 | 83 | "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], 84 | 85 | "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], 86 | 87 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 88 | 89 | "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], 90 | 91 | "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 92 | 93 | "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 94 | 95 | "brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], 96 | 97 | "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], 98 | 99 | "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 100 | 101 | "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], 102 | 103 | "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], 104 | 105 | "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], 106 | 107 | "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], 108 | 109 | "cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], 110 | 111 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 112 | 113 | "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 114 | 115 | "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], 116 | 117 | "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], 118 | 119 | "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 120 | 121 | "emojilib": ["emojilib@2.4.0", "", {}, "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw=="], 122 | 123 | "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 124 | 125 | "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], 126 | 127 | "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 128 | 129 | "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], 130 | 131 | "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], 132 | 133 | "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 134 | 135 | "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], 136 | 137 | "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], 138 | 139 | "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], 140 | 141 | "lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="], 142 | 143 | "lunr": ["lunr@2.3.9", "", {}, "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="], 144 | 145 | "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="], 146 | 147 | "marked": ["marked@9.1.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q=="], 148 | 149 | "marked-terminal": ["marked-terminal@7.3.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "ansi-regex": "^6.1.0", "chalk": "^5.4.1", "cli-highlight": "^2.1.11", "cli-table3": "^0.6.5", "node-emoji": "^2.2.0", "supports-hyperlinks": "^3.1.0" }, "peerDependencies": { "marked": ">=1 <16" } }, "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw=="], 150 | 151 | "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], 152 | 153 | "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 154 | 155 | "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], 156 | 157 | "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], 158 | 159 | "node-emoji": ["node-emoji@2.2.0", "", { "dependencies": { "@sindresorhus/is": "^4.6.0", "char-regex": "^1.0.2", "emojilib": "^2.4.0", "skin-tone": "^2.0.0" } }, "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw=="], 160 | 161 | "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], 162 | 163 | "parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], 164 | 165 | "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="], 166 | 167 | "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], 168 | 169 | "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], 170 | 171 | "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], 172 | 173 | "skin-tone": ["skin-tone@2.0.0", "", { "dependencies": { "unicode-emoji-modifier-base": "^1.0.0" } }, "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA=="], 174 | 175 | "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 176 | 177 | "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 178 | 179 | "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 180 | 181 | "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="], 182 | 183 | "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], 184 | 185 | "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], 186 | 187 | "typedoc": ["typedoc@0.28.5", "", { "dependencies": { "@gerrit0/mini-shiki": "^3.2.2", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", "yaml": "^2.7.1" }, "peerDependencies": { "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" }, "bin": { "typedoc": "bin/typedoc" } }, "sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA=="], 188 | 189 | "typedoc-plugin-mdn-links": ["typedoc-plugin-mdn-links@5.0.2", "", { "peerDependencies": { "typedoc": "0.27.x || 0.28.x" } }, "sha512-Bd3lsVWPSpDkn6NGZyPHpcK088PUvH4SRq4RD97OjA6l8PQA3yOnJhGACtjmIDdcenRTgWUosH+55ANZhx/wkw=="], 190 | 191 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 192 | 193 | "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], 194 | 195 | "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], 196 | 197 | "unicode-emoji-modifier-base": ["unicode-emoji-modifier-base@1.0.0", "", {}, "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g=="], 198 | 199 | "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], 200 | 201 | "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], 202 | 203 | "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], 204 | 205 | "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], 206 | 207 | "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], 208 | 209 | "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], 210 | 211 | "@arethetypeswrong/core/typescript": ["typescript@5.6.1-rc", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ=="], 212 | 213 | "marked-terminal/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], 214 | 215 | "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], 216 | 217 | "strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@edgefirst-dev/r2-file-storage", 3 | "version": "1.3.0", 4 | "description": "An implementation of @mjackson/file-storage that uses R2 as the storage backend.", 5 | "license": "MIT", 6 | "funding": [ 7 | "https://github.com/sponsors/sergiodxa" 8 | ], 9 | "author": { 10 | "name": "Sergio Xalambrí", 11 | "email": "hello+oss@sergiodxa.com", 12 | "url": "https://sergiodxa.com" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/edgefirst-dev/r2-file-storage" 17 | }, 18 | "homepage": "https://edgefirst-dev.github.io/r2-file-storage", 19 | "bugs": { 20 | "url": "https://github.com/edgefirst-dev/r2-file-storage/issues" 21 | }, 22 | "scripts": { 23 | "build": "tsc", 24 | "typecheck": "tsc --noEmit", 25 | "quality": "biome check .", 26 | "quality:fix": "biome check . --write --unsafe", 27 | "exports": "bun run ./scripts/exports.ts" 28 | }, 29 | "sideEffects": false, 30 | "type": "module", 31 | "engines": { 32 | "node": ">=20.0.0" 33 | }, 34 | "files": [ 35 | "build", 36 | "package.json", 37 | "README.md" 38 | ], 39 | "exports": { 40 | ".": "./build/index.js", 41 | "./package.json": "./package.json" 42 | }, 43 | "peerDependencies": { 44 | "@cloudflare/workers-types": "^4.20250523.0", 45 | "@mjackson/file-storage": "^0.6.1" 46 | }, 47 | "devDependencies": { 48 | "@arethetypeswrong/cli": "^0.18.1", 49 | "@biomejs/biome": "^1.9.4", 50 | "@total-typescript/tsconfig": "^1.0.4", 51 | "@types/bun": "^1.2.14", 52 | "consola": "^3.2.3", 53 | "typedoc": "^0.28.0", 54 | "typedoc-plugin-mdn-links": "^5.0.1", 55 | "typescript": "^5.7.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /scripts/exports.ts: -------------------------------------------------------------------------------- 1 | async function main() { 2 | let proc = Bun.spawn([ 3 | "bunx", 4 | "attw", 5 | "-f", 6 | "table-flipped", 7 | "--no-emoji", 8 | "--no-color", 9 | "--pack", 10 | ]); 11 | 12 | let text = await new Response(proc.stdout).text(); 13 | 14 | let entrypointLines = text 15 | .slice(text.indexOf('"remix-i18next/')) 16 | .split("\n") 17 | .filter(Boolean) 18 | .filter((line) => !line.includes("─")) 19 | .map((line) => 20 | line 21 | .replaceAll(/[^\d "()/A-Za-z│-]/g, "") 22 | .replaceAll("90m│39m", "│") 23 | .replaceAll(/^│/g, "") 24 | .replaceAll(/│$/g, ""), 25 | ); 26 | 27 | let pkg = await Bun.file("package.json").json(); 28 | let entrypoints = entrypointLines.map((entrypointLine) => { 29 | let [entrypoint, ...resolutionColumns] = entrypointLine.split("│"); 30 | if (!entrypoint) throw new Error("Entrypoint not found"); 31 | if (!resolutionColumns[2]) throw new Error("ESM resolution not found"); 32 | if (!resolutionColumns[3]) throw new Error("Bundler resolution not found"); 33 | return { 34 | entrypoint: entrypoint.replace(pkg.name, ".").trim(), 35 | esm: resolutionColumns[2].trim(), 36 | bundler: resolutionColumns[3].trim(), 37 | }; 38 | }); 39 | 40 | let entrypointsWithProblems = entrypoints.filter( 41 | (item) => item.esm.includes("fail") || item.bundler.includes("fail"), 42 | ); 43 | 44 | if (entrypointsWithProblems.length > 0) { 45 | console.error("Entrypoints with problems:"); 46 | process.exit(1); 47 | } 48 | } 49 | 50 | await main().catch((error) => { 51 | console.error(error); 52 | process.exit(1); 53 | }); 54 | 55 | export {}; 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { R2FileStorage } from "./lib/r2-file-storage.js"; 2 | export { R2FileStorage }; 3 | -------------------------------------------------------------------------------- /src/lib/r2-file-storage.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, mock, test } from "bun:test"; 2 | import { R2Bucket } from "@cloudflare/workers-types"; 3 | import { R2FileStorage } from "./r2-file-storage"; 4 | 5 | let get = mock(); 6 | let put = mock(); 7 | let del = mock(); 8 | 9 | const r2 = { get, put, delete: del } as unknown as R2Bucket; 10 | 11 | describe("R2FileStorage", () => { 12 | test("stores and retrieves files", async () => { 13 | let storage = new R2FileStorage(r2); 14 | 15 | let file = new File(["Hello, world!"], "hello.txt", { type: "text/plain" }); 16 | 17 | put.mockImplementationOnce(async (key: string, buffer: ArrayBufferLike) => { 18 | expect(key).toBe("hello"); 19 | expect(new Uint8Array(buffer)).toEqual( 20 | new TextEncoder().encode("Hello, world!"), 21 | ); 22 | }); 23 | 24 | storage.set("hello", file); 25 | 26 | get.mockImplementationOnce(async (key: string) => { 27 | expect(key).toBe("hello"); 28 | return { 29 | arrayBuffer: async () => 30 | new TextEncoder().encode("Hello, world!").buffer, 31 | httpMetadata: { contentType: "text/plain" }, 32 | uploaded: new Date(), 33 | }; 34 | }); 35 | 36 | expect(storage.has("hello")).resolves.toBeTrue(); 37 | 38 | get.mockImplementationOnce(async (key: string) => { 39 | expect(key).toBe("hello"); 40 | return { 41 | arrayBuffer: async () => 42 | new TextEncoder().encode("Hello, world!").buffer, 43 | httpMetadata: { contentType: "text/plain" }, 44 | customMetadata: { name: "hello.txt" }, 45 | uploaded: new Date(), 46 | }; 47 | }); 48 | 49 | let retrieved = await storage.get("hello"); 50 | 51 | expect(retrieved).toBeTruthy(); 52 | expect(retrieved?.name).toBe("hello.txt"); 53 | expect(retrieved?.type).toBe("text/plain;charset=utf-8"); 54 | expect(retrieved?.size).toBe(13); 55 | 56 | let text = await retrieved?.text(); 57 | 58 | expect(text).toBe("Hello, world!"); 59 | 60 | del.mockImplementationOnce(async (key) => { 61 | expect(key).toBe("hello"); 62 | }); 63 | 64 | await storage.remove("hello"); 65 | 66 | get.mockImplementation(async (key: string) => { 67 | expect(key).toBe("hello"); 68 | return null; 69 | }); 70 | 71 | expect(storage.has("hello")).resolves.toBeFalse(); 72 | expect(storage.get("hello")).resolves.toBeNull(); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/lib/r2-file-storage.ts: -------------------------------------------------------------------------------- 1 | import type { R2Bucket } from "@cloudflare/workers-types"; 2 | import type { 3 | FileKey, 4 | FileMetadata, 5 | FileStorage, 6 | ListOptions, 7 | ListResult, 8 | } from "@mjackson/file-storage"; 9 | 10 | export namespace R2FileStorage { 11 | export interface CustomMetadata extends Record { 12 | name: string; 13 | type: string; 14 | } 15 | } 16 | 17 | /** 18 | * A `FileStorage` that is backed by an R2 bucket. 19 | */ 20 | export class R2FileStorage implements FileStorage { 21 | /** 22 | * @param r2 The R2Bucket bindings to use 23 | */ 24 | constructor(protected r2: R2Bucket) {} 25 | 26 | async has(key: string) { 27 | let object = await this.r2.get(key); 28 | return object !== null; 29 | } 30 | 31 | async set(key: string, file: File) { 32 | await this.put(key, file); 33 | } 34 | 35 | async put(key: string, file: File) { 36 | let customMetadata = { 37 | name: file.name, 38 | type: file.type, 39 | } satisfies R2FileStorage.CustomMetadata; 40 | 41 | let body = await file.arrayBuffer(); 42 | 43 | await this.r2.put(key, body, { 44 | httpMetadata: { contentType: file.type }, 45 | customMetadata, 46 | }); 47 | 48 | return file; 49 | } 50 | 51 | async get(key: string) { 52 | let object = await this.r2.get(key); 53 | if (!object) return null; 54 | 55 | let buffer = await object.arrayBuffer(); 56 | 57 | let metadata = 58 | object.customMetadata as unknown as R2FileStorage.CustomMetadata; 59 | 60 | return new File([buffer], metadata?.name ?? key, { 61 | type: object.httpMetadata?.contentType ?? metadata?.type, 62 | lastModified: object.uploaded.getTime(), 63 | }); 64 | } 65 | 66 | async remove(key: string) { 67 | await this.r2.delete(key); 68 | } 69 | 70 | async list(options?: T): Promise> { 71 | let result = await this.r2.list({ 72 | cursor: options?.cursor, 73 | limit: options?.limit, 74 | prefix: options?.prefix, 75 | }); 76 | 77 | return { 78 | files: result.objects.map((object) => { 79 | let metadata = 80 | object.customMetadata as unknown as R2FileStorage.CustomMetadata; 81 | 82 | if (options?.includeMetadata === true) { 83 | return { 84 | key: object.key, 85 | lastModified: object.uploaded.getTime(), 86 | size: object.size, 87 | name: metadata?.name ?? object.key, 88 | type: object.httpMetadata?.contentType ?? metadata?.type, 89 | } satisfies FileMetadata; 90 | } 91 | 92 | return { key: object.key } satisfies FileKey; 93 | }) as ListResult["files"], 94 | cursor: result.truncated ? result.cursor : undefined, 95 | }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Change to `@total-typescript/tsconfig/tsc/dom/library` for DOM usage */ 3 | "extends": "@total-typescript/tsconfig/tsc/no-dom/library", 4 | "include": ["src/**/*"], 5 | "exclude": ["src/**/*.test.*"], 6 | "compilerOptions": { 7 | "outDir": "./build", 8 | "types": ["@cloudflare/workers-types"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "includeVersion": true, 4 | "entryPoints": ["./src/index.ts"], 5 | "out": "docs", 6 | "json": "docs/index.json", 7 | "cleanOutputDir": true, 8 | "plugin": ["typedoc-plugin-mdn-links"], 9 | "categorizeByGroup": false 10 | } 11 | --------------------------------------------------------------------------------