├── .github └── workflows │ ├── publish.yml │ └── pull-request.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.ts └── tests │ ├── core.test.ts │ ├── full.test.ts │ ├── integration.test.ts │ ├── json.test.ts │ ├── memoryStorage.ts │ ├── safety.test.ts │ ├── scope.test.ts │ └── types.ts └── tsconfig.json /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | newversion: 6 | description: "Version bump" 7 | type: choice 8 | options: 9 | - major 10 | - minor 11 | - patch 12 | required: true 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | token: ${{ secrets.NPM_TOKEN }} 21 | registry-url: "https://registry.npmjs.org" 22 | node-version: 16 23 | cache: npm 24 | - run: npm ci 25 | - run: npm run build 26 | - run: git config --global user.email "v.klepov@gmail.com" 27 | - run: git config --global user.name "thoughtspile" 28 | - run: npm version ${{ inputs.newversion }} 29 | - run: git push --follow-tags 30 | - run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Test PR 2 | on: pull_request 3 | jobs: 4 | full: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: actions/setup-node@v4 9 | with: 10 | node-version: 16 11 | cache: npm 12 | - run: npm ci 13 | - run: npm run lint 14 | - run: npm run test:coverage 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/nano-staged 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .husky 2 | node_modules 3 | dist/tests 4 | tsconfig.json 5 | coverage 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | .husky 4 | .*ignore 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Vladimir Klepov 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 | # banditstash 2 | 3 | TypeScript-first, extensible local and sessionStorage wrapper: 4 | 5 | - **Type-safe:** no sneaky bugs if storage is corrupted. 6 | - **Sane defaults:** JSON serialization, runtime validation and catching errors out of the box. 7 | - **Key scoping:** prevent collisions and access values with ease. 8 | - **Tiny:** 400 bytes full build, or a 187-byte core with modular features. 9 | - **Extensible:** replace JSON with any serializer or use your favorite validation library. 10 | - **Familiar API:** no trickery, just good old getItem / setItem with stricter types. 11 | - **Custom storage:** not limited to local / sessionStorage, works in SSR. 12 | 13 | **Beware!** This is an early version of the package. API might change, bugs might exist. 14 | 15 | Banditstash has a companion 400-byte type-checking library, [banditypes,](https://github.com/thoughtspile/banditypes) to make validation much more convenient without inflating your bundle. 16 | 17 | ## Install 18 | 19 | ```sh 20 | npm install --save banditstash 21 | ``` 22 | 23 | ## Basic usage 24 | 25 | Default `banditStash` factory gives you: 26 | 27 | - JSON serialization for convenience 28 | - Type-safe access 29 | - Runtime validation to prevent malformed objects from exploding at runtime 30 | - Catching getItem / setItem errors 31 | - Optional scoping to prevent key collisions 32 | - Fallback for missing storage (e.g. in SSR) 33 | 34 | ```ts 35 | import { banditStash, fail } from "banditstash"; 36 | 37 | // Passing explicit type parameter for outer type is recommended 38 | const setStash = banditStash>({ 39 | storage: window.sessionStorage, 40 | parse: (raw) => { 41 | // parse must convert arbitrary JSON to Set... 42 | if (Array.isArray(raw)) { 43 | return new Set(raw.filter((x): x is string => typeof x === "string")); 44 | } 45 | // or throw error via fail() 46 | fail(); 47 | }, 48 | // prepare must convert Set to a JSON-serializable format 49 | prepare: (data) => [...data], 50 | // If getItem can't return Set 51 | fallback: () => new Set(), 52 | // (optional) prefix all storage keys with "app:" 53 | scope: "app", 54 | }); 55 | 56 | // getItem always returns Set — either from storage or fallback: 57 | const readMessages: Set = setStash.getItem("read-ids"); 58 | const isMessageRead = readMessages.has("id"); 59 | 60 | // setItem accepts Set and serializes it for you: 61 | setStash.setItem("read-ids", new Set(["123", "234"])); 62 | 63 | // removeItem is same as in raw storage 64 | setStash.removeItem("read-ids"); 65 | 66 | // Bind key with .singleton() for easy access to a sinlge item: 67 | const readStash = setStash.singleton("read-messages"); 68 | const ids = readStash.getItem(); 69 | readStash.setItem(ids.add("123")); 70 | readStash.removeItem(); 71 | ``` 72 | 73 | This setup catches errors from both `getItem` (validation fails, invalid JSON in storage, missing storage) and `setItem` (full or missing storage, failed serialization). This can be disabled with explicit `fallback: false` and `safeSet: false`, respectively — useful for debugging, or to show an explicit error message to the user. 74 | 75 | Manual object validation is quite tedious, so I suggest the companion validator — [banditypes.](https://github.com/thoughtspile/banditypes) If you want something more established, every other validation library — superstruct, zod, io-ts — also integrates easily. 76 | 77 | ## Custom banditStashes 78 | 79 | `banditstash` is designed to be modular and extensible via plugins. In fact, default `banditStash` is just a combination of 3 plugins — `safeGet`, `safeSet`, and `scope` — and two formatters, `json` and your custom formatter defined via `prepare` and `parse`. **Plugins** modify getItem / setItem / removeItem behavior (like wrapping in try / catch, changing keys, or whatever.) **Formatters** are a special case of plugins that validate and transform the stored value during getItem and setItem. 80 | 81 | Using the base `makeBanditStash` factory with `use` and `format` methods, you can further reduce bundle size (down to 187 bytes without plugins) or modify the behavior of your stores. Plugins and formatters are chainable and _always_ return a new object. 82 | 83 | ```ts 84 | import { makeBanditStash, fail } from "banditstash"; 85 | 86 | const stringStore = makeBanditStash(localStorage).format({ 87 | parse: (data) => (data == null ? fail() : data), 88 | }); 89 | const readonlyStringStore = stringStore.use((stash) => ({ 90 | getItem: stash.getItem, 91 | setItem: () => { 92 | throw new Error("setItem on readonly store"); 93 | }, 94 | removeItem: () => { 95 | throw new Error("removeItem on readonly store"); 96 | }, 97 | })); 98 | ``` 99 | 100 | Stashes built using the default factory can be further enhanced with more plugins or formatters. 101 | 102 | ### Custom storage 103 | 104 | Banditstash is not limited to browser Storage APIs — you can provide any object with `getItem`, `setItem` and `removeItem` methods that accept string key. The values needn't be strings, and makeBanditStash will infer storage value type. 105 | 106 | ```ts 107 | import { makeBanditStash, fail } from "banditstash"; 108 | 109 | const map = new Map(); 110 | const memoryStorage = makeBanditStash({ 111 | getItem: (key) => map.get(key) ?? fail(), 112 | setItem: (key, value) => map.set(key, value), 113 | removeItem: (key) => map.delete(key), 114 | }); 115 | 116 | const universalStorage = makeBanditStash( 117 | typeof window === "undefined" 118 | ? { 119 | getItem: () => null, 120 | setItem: () => {}, 121 | removeItem: () => {}, 122 | } 123 | : window.localStorage, 124 | ); 125 | ``` 126 | 127 | banditstash provides one built-in custom storage — `noStorage`. It throws error on any access, but lets you construct a banditstash instance when no storage is available (e.g. in SSR). 128 | 129 | ### Custom serializer 130 | 131 | If JSON does not satisfy you as a storage format, you can easily use your own serializer. Here's an example of manually serializing a number: 132 | 133 | ```ts 134 | import { makeBanditStash, fail } from "banditstash"; 135 | 136 | const numberStash = makeBanditStash(localStorage).format({ 137 | parse: (raw) => { 138 | const num = Number(raw); 139 | return Number.isNaN(num) ? fail() : num; 140 | }, 141 | prepare: String, 142 | }); 143 | ``` 144 | 145 | Any serialization library, like [arson](https://github.com/benjamn/arson) or [devalue,](https://github.com/Rich-Harris/devalue) will work: 146 | 147 | ```ts 148 | import { makeBanditStash, fail } from "banditstash"; 149 | import arson from "arson"; 150 | 151 | const dateStash = makeBanditStash(localStorage).format({ 152 | parse: arson.parse, 153 | prepare: arson.stringify, 154 | }); 155 | dateStash.setItem("registered", new Date(2022, 3, 16)); 156 | const registeredAt: Date = dateStash.getItem("registered"); 157 | ``` 158 | 159 | Default JSON serialization is implemented via `json` formatter: 160 | 161 | ```ts 162 | import { makeBanditStash, json } from "banditstash"; 163 | 164 | makeBanditStash(localStorage).format(json()); 165 | ``` 166 | 167 | ### Using a validation library 168 | 169 | Manual type-checking can get tedious. Banditstash plays nicely with any validation library, as long as you `throw` (or `fail()`) on invalid values. I recommend either the 400-byte companion library [banditypes](https://github.com/thoughtspile/banditypes) or [superstruct](https://docs.superstructjs.org/) — it's small and modular, just like banditstash: 170 | 171 | ```ts 172 | import { makeBanditStash, fail } from "banditstash"; 173 | import { object, string, number, min, type Infer } from "superstruct"; 174 | 175 | const userSchema = object({ 176 | name: string(), 177 | age: min(number(), 0), 178 | }); 179 | 180 | const userStore = makeBanditStash(localStorage).format< 181 | Infer 182 | >({ 183 | parse: (raw) => userSchema.create(raw), 184 | }); 185 | 186 | userStore.setItem("me", { name: "vladimir", age: 28 }); 187 | localStorage.set("broken", JSON.stringify({ name: "evil" })); 188 | try { 189 | userStore.getItem("broken"); 190 | } catch (err) { 191 | console.log("validation failed"); 192 | } 193 | ``` 194 | 195 | Any other validation library — [zod,](https://zod.dev/) [io-ts,](https://gcanti.github.io/io-ts/) [yup,](https://github.com/jquense/yup) etc — is similarly easy to add. 196 | 197 | ### Scoping 198 | 199 | `scope` plugin adds prefix to all keys to avoid key collisions. It's still useful even without TypeScript: 200 | 201 | ```ts 202 | import { makeBanditStash, scope } from "banditstash"; 203 | 204 | const appStorage = makeBanditStash(localStorage).use(scope("app")); 205 | 206 | const userStorage = appStorage.use(scope("user")); 207 | const cacheStorage = appStorage.use(scope("cache")); 208 | 209 | userStorage.getItem("avatar"); 210 | // equivalent to 211 | localStorage.getItem("app:user:avatar"); 212 | ``` 213 | 214 | ### Runtime safety 215 | 216 | Banditstash provides two helpers for catching runtime errors: `safeGet` to handle `getItem` errors, and `safeSet` for `setItem`: 217 | 218 | ```ts 219 | import { makeBanditStash, safeGet, safeSet, json } from "banditstash"; 220 | const safeStorage = makeBanditStash(window.localStorage) 221 | .format(json()) 222 | .use(safeGet(() => ({}))) 223 | .use(safeSet()); 224 | ``` 225 | 226 | Note that, due to chaining, `safeGet` and `safeSet` only handle errors from plugins applied _above_ them, so it's best to use these in the tail of the chain. 227 | 228 | ## API reference 229 | 230 | ### `banditStash(options)` 231 | 232 | Creates a default stash with JSON serialization, validation and error handling. Specifying data type explicitly is recommended. 233 | 234 | Options: 235 | 236 | - `storage`: `localStorage`, `sessionStorage`, or an object with compatible `getItem`, `setItem`, and `removeItem` methods. If `undefined` is passed, `noStorage` is used to construct the instance. 237 | - `parse`: a function that either converts a free-form JSON to the `Data` type, or throws an error, during `getItem`. Usually required. 238 | - `prepare`: a function that converts `Data` to a JSON-serializable object during `setItem`. Required for non-serializable types like `Date`, `Map`, `Set`, etc. 239 | - `fallback: (() => Data) | false` : value to return when `getItem` can't retrieve data from storage. If set to false, error will be thrown. 240 | - `safeSet?: false` (optional): if false, setItem might throw. Defaults to true. 241 | - `scope: string`: (optional) a prefix for all the keys in the storage. 242 | 243 | ### `fail()` 244 | 245 | A helper to conveniently throw errors in `parse`: 246 | 247 | ```ts 248 | { 249 | parse: raw => raw.length === 10 ? raw : fail(), 250 | // equivalent to 251 | parse: raw => { 252 | if (raw.length === 10) return raw; 253 | throw new TypeError(); 254 | } 255 | } 256 | ``` 257 | 258 | ### `noStorage()` 259 | 260 | A custom storage that throws on every access. Can be used when `Storage` is not available to safely construct `banditstash`: 261 | 262 | ```ts 263 | import { makeBanditStash, noStorage } from "banditstash"; 264 | 265 | makeBanditStash( 266 | typeof window === "undefined" ? noStorage() : window.localStorage, 267 | ); 268 | ``` 269 | 270 | Full `banditStash` falls back to `noStorage` if `storage` option is falsy. 271 | 272 | ### `makeBanditStash(storage)` 273 | 274 | Creates a custom banditStash instance without formatters or plugins. `getItem`, `setItem` and `removeItem` are always bound to storage, `format`, `use` and `singleton` methods are added. 275 | 276 | ### `#BanditStash.getItem(key)` 277 | 278 | Reads value from storage, passing it through `parse` pipeline. Returns a parsed `Data` type. Throws if parse fails and `safeGet` plugin is not used. 279 | 280 | ### `#BanditStash.setItem(key, value)` 281 | 282 | Writes value to storage, passing it through `prepare` pipeline. Throws if prepare fails or `storage.setItem` throws, and `safeSet` is not used. 283 | 284 | ### `#BanditStash.removeItem(key)` 285 | 286 | Removes value from storage. 287 | 288 | ### `#BanditStash.singleton(key)` 289 | 290 | Returns a singleton store whose `getItem`, `setItem` and `removeItem` can be called without key. Singleton stores don't support formatters and plugins, so make sure it's called last. 291 | 292 | ### `#BanditStash.format(formatter)` 293 | 294 | Returns a new stash that exposes data of `Outer` type. Formatter object contains 2 functions: 295 | 296 | - `parse` maps data from Inner (storage) type to Outer or throws (use `fail()` helper). 297 | - `prepare` maps data from Outer to Inner type. 298 | 299 | If Outer is assignable to Inner (e.g. `string -> Json`), `prepare` is optional. If Inner is assignable to Outer (e.g. `string -> string`), `parse` is optional. 300 | 301 | There is a built-in `json()` formatter that converts between JSON objects and strings. 302 | 303 | ### `#BanditStash.use(plugin)` 304 | 305 | Return a new stash with `getItem`, `setItem` or `removeItem` behavior modified by the plugin. Plugin is a function called with the original stash. At this point plugin API is unstable, so prefer built-in plugins: 306 | 307 | - `safeGet(() => fallback)` — return `fallback` instead of throwing error in `getItem` 308 | - `safeSet()` — ignore errors in `setItem` 309 | - `scope(prefix: string)` — prefix all keys with prefix, `'key' -> 'prefix:key'` 310 | 311 | ## License 312 | 313 | [MIT License](./LICENSE) 314 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "banditstash", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "banditstash", 9 | "version": "0.1.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "arson": "^0.2.6", 13 | "banditypes": "^0.2.5", 14 | "c8": "^10.1.3", 15 | "husky": "^9.1.7", 16 | "nano-staged": "^0.8.0", 17 | "prettier": "^3.4.2", 18 | "superstruct": "^2.0.2", 19 | "terser": "^5.37.0", 20 | "typescript": "^5.7.2", 21 | "uvu": "^0.5.6", 22 | "zod": "^3.24.1" 23 | }, 24 | "engines": { 25 | "node": "^14 || ^16 || ^18 || >=20" 26 | } 27 | }, 28 | "node_modules/@bcoe/v8-coverage": { 29 | "version": "1.0.1", 30 | "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.1.tgz", 31 | "integrity": "sha512-W+a0/JpU28AqH4IKtwUPcEUnUyXMDLALcn5/JLczGGT9fHE2sIby/xP/oQnx3nxkForzgzPy201RAKcB4xPAFQ==", 32 | "dev": true, 33 | "engines": { 34 | "node": ">=18" 35 | } 36 | }, 37 | "node_modules/@isaacs/cliui": { 38 | "version": "8.0.2", 39 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 40 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 41 | "dev": true, 42 | "dependencies": { 43 | "string-width": "^5.1.2", 44 | "string-width-cjs": "npm:string-width@^4.2.0", 45 | "strip-ansi": "^7.0.1", 46 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 47 | "wrap-ansi": "^8.1.0", 48 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 49 | }, 50 | "engines": { 51 | "node": ">=12" 52 | } 53 | }, 54 | "node_modules/@istanbuljs/schema": { 55 | "version": "0.1.3", 56 | "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", 57 | "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", 58 | "dev": true, 59 | "engines": { 60 | "node": ">=8" 61 | } 62 | }, 63 | "node_modules/@jridgewell/gen-mapping": { 64 | "version": "0.3.8", 65 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", 66 | "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", 67 | "dev": true, 68 | "dependencies": { 69 | "@jridgewell/set-array": "^1.2.1", 70 | "@jridgewell/sourcemap-codec": "^1.4.10", 71 | "@jridgewell/trace-mapping": "^0.3.24" 72 | }, 73 | "engines": { 74 | "node": ">=6.0.0" 75 | } 76 | }, 77 | "node_modules/@jridgewell/resolve-uri": { 78 | "version": "3.1.2", 79 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 80 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 81 | "dev": true, 82 | "engines": { 83 | "node": ">=6.0.0" 84 | } 85 | }, 86 | "node_modules/@jridgewell/set-array": { 87 | "version": "1.2.1", 88 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", 89 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", 90 | "dev": true, 91 | "engines": { 92 | "node": ">=6.0.0" 93 | } 94 | }, 95 | "node_modules/@jridgewell/source-map": { 96 | "version": "0.3.6", 97 | "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", 98 | "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", 99 | "dev": true, 100 | "dependencies": { 101 | "@jridgewell/gen-mapping": "^0.3.5", 102 | "@jridgewell/trace-mapping": "^0.3.25" 103 | } 104 | }, 105 | "node_modules/@jridgewell/sourcemap-codec": { 106 | "version": "1.5.0", 107 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 108 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 109 | "dev": true 110 | }, 111 | "node_modules/@jridgewell/trace-mapping": { 112 | "version": "0.3.25", 113 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", 114 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", 115 | "dev": true, 116 | "dependencies": { 117 | "@jridgewell/resolve-uri": "^3.1.0", 118 | "@jridgewell/sourcemap-codec": "^1.4.14" 119 | } 120 | }, 121 | "node_modules/@pkgjs/parseargs": { 122 | "version": "0.11.0", 123 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 124 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 125 | "dev": true, 126 | "optional": true, 127 | "engines": { 128 | "node": ">=14" 129 | } 130 | }, 131 | "node_modules/@types/istanbul-lib-coverage": { 132 | "version": "2.0.6", 133 | "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", 134 | "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", 135 | "dev": true 136 | }, 137 | "node_modules/acorn": { 138 | "version": "8.14.0", 139 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 140 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 141 | "dev": true, 142 | "bin": { 143 | "acorn": "bin/acorn" 144 | }, 145 | "engines": { 146 | "node": ">=0.4.0" 147 | } 148 | }, 149 | "node_modules/ansi-regex": { 150 | "version": "6.1.0", 151 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", 152 | "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", 153 | "dev": true, 154 | "engines": { 155 | "node": ">=12" 156 | }, 157 | "funding": { 158 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 159 | } 160 | }, 161 | "node_modules/ansi-styles": { 162 | "version": "6.2.1", 163 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 164 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 165 | "dev": true, 166 | "engines": { 167 | "node": ">=12" 168 | }, 169 | "funding": { 170 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 171 | } 172 | }, 173 | "node_modules/arson": { 174 | "version": "0.2.6", 175 | "resolved": "https://registry.npmjs.org/arson/-/arson-0.2.6.tgz", 176 | "integrity": "sha512-wVRnIfjOaCWu3jrf3j1CU/eotDf7tuM34cBswo32EwyLPaMiaWgETfROdYVv47VWEbWSOaZaDnkypGQtQduLbw==", 177 | "dev": true 178 | }, 179 | "node_modules/balanced-match": { 180 | "version": "1.0.2", 181 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 182 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 183 | "dev": true 184 | }, 185 | "node_modules/banditypes": { 186 | "version": "0.2.5", 187 | "resolved": "https://registry.npmjs.org/banditypes/-/banditypes-0.2.5.tgz", 188 | "integrity": "sha512-yyfPhzlDEtH2CtqxkUKYO1R4eQ6HGGUsnztSpFgASTD82gZKN50YQQV3wmh+u26YEWAEKVnd3HmCXs7AaD8RgQ==", 189 | "dev": true, 190 | "engines": { 191 | "node": "^14 || ^16 || >=18" 192 | } 193 | }, 194 | "node_modules/brace-expansion": { 195 | "version": "2.0.1", 196 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 197 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 198 | "dev": true, 199 | "dependencies": { 200 | "balanced-match": "^1.0.0" 201 | } 202 | }, 203 | "node_modules/buffer-from": { 204 | "version": "1.1.2", 205 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 206 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 207 | "dev": true 208 | }, 209 | "node_modules/c8": { 210 | "version": "10.1.3", 211 | "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", 212 | "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", 213 | "dev": true, 214 | "dependencies": { 215 | "@bcoe/v8-coverage": "^1.0.1", 216 | "@istanbuljs/schema": "^0.1.3", 217 | "find-up": "^5.0.0", 218 | "foreground-child": "^3.1.1", 219 | "istanbul-lib-coverage": "^3.2.0", 220 | "istanbul-lib-report": "^3.0.1", 221 | "istanbul-reports": "^3.1.6", 222 | "test-exclude": "^7.0.1", 223 | "v8-to-istanbul": "^9.0.0", 224 | "yargs": "^17.7.2", 225 | "yargs-parser": "^21.1.1" 226 | }, 227 | "bin": { 228 | "c8": "bin/c8.js" 229 | }, 230 | "engines": { 231 | "node": ">=18" 232 | }, 233 | "peerDependencies": { 234 | "monocart-coverage-reports": "^2" 235 | }, 236 | "peerDependenciesMeta": { 237 | "monocart-coverage-reports": { 238 | "optional": true 239 | } 240 | } 241 | }, 242 | "node_modules/cliui": { 243 | "version": "8.0.1", 244 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 245 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 246 | "dev": true, 247 | "dependencies": { 248 | "string-width": "^4.2.0", 249 | "strip-ansi": "^6.0.1", 250 | "wrap-ansi": "^7.0.0" 251 | }, 252 | "engines": { 253 | "node": ">=12" 254 | } 255 | }, 256 | "node_modules/cliui/node_modules/ansi-regex": { 257 | "version": "5.0.1", 258 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 259 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 260 | "dev": true, 261 | "engines": { 262 | "node": ">=8" 263 | } 264 | }, 265 | "node_modules/cliui/node_modules/ansi-styles": { 266 | "version": "4.3.0", 267 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 268 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 269 | "dev": true, 270 | "dependencies": { 271 | "color-convert": "^2.0.1" 272 | }, 273 | "engines": { 274 | "node": ">=8" 275 | }, 276 | "funding": { 277 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 278 | } 279 | }, 280 | "node_modules/cliui/node_modules/emoji-regex": { 281 | "version": "8.0.0", 282 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 283 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 284 | "dev": true 285 | }, 286 | "node_modules/cliui/node_modules/string-width": { 287 | "version": "4.2.3", 288 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 289 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 290 | "dev": true, 291 | "dependencies": { 292 | "emoji-regex": "^8.0.0", 293 | "is-fullwidth-code-point": "^3.0.0", 294 | "strip-ansi": "^6.0.1" 295 | }, 296 | "engines": { 297 | "node": ">=8" 298 | } 299 | }, 300 | "node_modules/cliui/node_modules/strip-ansi": { 301 | "version": "6.0.1", 302 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 303 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 304 | "dev": true, 305 | "dependencies": { 306 | "ansi-regex": "^5.0.1" 307 | }, 308 | "engines": { 309 | "node": ">=8" 310 | } 311 | }, 312 | "node_modules/cliui/node_modules/wrap-ansi": { 313 | "version": "7.0.0", 314 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 315 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 316 | "dev": true, 317 | "dependencies": { 318 | "ansi-styles": "^4.0.0", 319 | "string-width": "^4.1.0", 320 | "strip-ansi": "^6.0.0" 321 | }, 322 | "engines": { 323 | "node": ">=10" 324 | }, 325 | "funding": { 326 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 327 | } 328 | }, 329 | "node_modules/color-convert": { 330 | "version": "2.0.1", 331 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 332 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 333 | "dev": true, 334 | "dependencies": { 335 | "color-name": "~1.1.4" 336 | }, 337 | "engines": { 338 | "node": ">=7.0.0" 339 | } 340 | }, 341 | "node_modules/color-name": { 342 | "version": "1.1.4", 343 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 344 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 345 | "dev": true 346 | }, 347 | "node_modules/commander": { 348 | "version": "2.20.3", 349 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 350 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 351 | "dev": true 352 | }, 353 | "node_modules/convert-source-map": { 354 | "version": "2.0.0", 355 | "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", 356 | "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", 357 | "dev": true 358 | }, 359 | "node_modules/cross-spawn": { 360 | "version": "7.0.6", 361 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 362 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 363 | "dev": true, 364 | "dependencies": { 365 | "path-key": "^3.1.0", 366 | "shebang-command": "^2.0.0", 367 | "which": "^2.0.1" 368 | }, 369 | "engines": { 370 | "node": ">= 8" 371 | } 372 | }, 373 | "node_modules/dequal": { 374 | "version": "2.0.3", 375 | "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", 376 | "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", 377 | "dev": true, 378 | "engines": { 379 | "node": ">=6" 380 | } 381 | }, 382 | "node_modules/diff": { 383 | "version": "5.2.0", 384 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", 385 | "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", 386 | "dev": true, 387 | "engines": { 388 | "node": ">=0.3.1" 389 | } 390 | }, 391 | "node_modules/eastasianwidth": { 392 | "version": "0.2.0", 393 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 394 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 395 | "dev": true 396 | }, 397 | "node_modules/emoji-regex": { 398 | "version": "9.2.2", 399 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 400 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", 401 | "dev": true 402 | }, 403 | "node_modules/escalade": { 404 | "version": "3.2.0", 405 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 406 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 407 | "dev": true, 408 | "engines": { 409 | "node": ">=6" 410 | } 411 | }, 412 | "node_modules/find-up": { 413 | "version": "5.0.0", 414 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 415 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 416 | "dev": true, 417 | "dependencies": { 418 | "locate-path": "^6.0.0", 419 | "path-exists": "^4.0.0" 420 | }, 421 | "engines": { 422 | "node": ">=10" 423 | }, 424 | "funding": { 425 | "url": "https://github.com/sponsors/sindresorhus" 426 | } 427 | }, 428 | "node_modules/foreground-child": { 429 | "version": "3.3.0", 430 | "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", 431 | "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", 432 | "dev": true, 433 | "dependencies": { 434 | "cross-spawn": "^7.0.0", 435 | "signal-exit": "^4.0.1" 436 | }, 437 | "engines": { 438 | "node": ">=14" 439 | }, 440 | "funding": { 441 | "url": "https://github.com/sponsors/isaacs" 442 | } 443 | }, 444 | "node_modules/get-caller-file": { 445 | "version": "2.0.5", 446 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 447 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 448 | "dev": true, 449 | "engines": { 450 | "node": "6.* || 8.* || >= 10.*" 451 | } 452 | }, 453 | "node_modules/glob": { 454 | "version": "10.4.5", 455 | "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", 456 | "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", 457 | "dev": true, 458 | "dependencies": { 459 | "foreground-child": "^3.1.0", 460 | "jackspeak": "^3.1.2", 461 | "minimatch": "^9.0.4", 462 | "minipass": "^7.1.2", 463 | "package-json-from-dist": "^1.0.0", 464 | "path-scurry": "^1.11.1" 465 | }, 466 | "bin": { 467 | "glob": "dist/esm/bin.mjs" 468 | }, 469 | "funding": { 470 | "url": "https://github.com/sponsors/isaacs" 471 | } 472 | }, 473 | "node_modules/has-flag": { 474 | "version": "4.0.0", 475 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 476 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 477 | "dev": true, 478 | "engines": { 479 | "node": ">=8" 480 | } 481 | }, 482 | "node_modules/html-escaper": { 483 | "version": "2.0.2", 484 | "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", 485 | "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", 486 | "dev": true 487 | }, 488 | "node_modules/husky": { 489 | "version": "9.1.7", 490 | "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", 491 | "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", 492 | "dev": true, 493 | "bin": { 494 | "husky": "bin.js" 495 | }, 496 | "engines": { 497 | "node": ">=18" 498 | }, 499 | "funding": { 500 | "url": "https://github.com/sponsors/typicode" 501 | } 502 | }, 503 | "node_modules/is-fullwidth-code-point": { 504 | "version": "3.0.0", 505 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 506 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 507 | "dev": true, 508 | "engines": { 509 | "node": ">=8" 510 | } 511 | }, 512 | "node_modules/isexe": { 513 | "version": "2.0.0", 514 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 515 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 516 | "dev": true 517 | }, 518 | "node_modules/istanbul-lib-coverage": { 519 | "version": "3.2.2", 520 | "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", 521 | "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", 522 | "dev": true, 523 | "engines": { 524 | "node": ">=8" 525 | } 526 | }, 527 | "node_modules/istanbul-lib-report": { 528 | "version": "3.0.1", 529 | "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", 530 | "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", 531 | "dev": true, 532 | "dependencies": { 533 | "istanbul-lib-coverage": "^3.0.0", 534 | "make-dir": "^4.0.0", 535 | "supports-color": "^7.1.0" 536 | }, 537 | "engines": { 538 | "node": ">=10" 539 | } 540 | }, 541 | "node_modules/istanbul-reports": { 542 | "version": "3.1.7", 543 | "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", 544 | "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", 545 | "dev": true, 546 | "dependencies": { 547 | "html-escaper": "^2.0.0", 548 | "istanbul-lib-report": "^3.0.0" 549 | }, 550 | "engines": { 551 | "node": ">=8" 552 | } 553 | }, 554 | "node_modules/jackspeak": { 555 | "version": "3.4.3", 556 | "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", 557 | "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", 558 | "dev": true, 559 | "dependencies": { 560 | "@isaacs/cliui": "^8.0.2" 561 | }, 562 | "funding": { 563 | "url": "https://github.com/sponsors/isaacs" 564 | }, 565 | "optionalDependencies": { 566 | "@pkgjs/parseargs": "^0.11.0" 567 | } 568 | }, 569 | "node_modules/kleur": { 570 | "version": "4.1.5", 571 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", 572 | "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", 573 | "dev": true, 574 | "engines": { 575 | "node": ">=6" 576 | } 577 | }, 578 | "node_modules/locate-path": { 579 | "version": "6.0.0", 580 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 581 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 582 | "dev": true, 583 | "dependencies": { 584 | "p-locate": "^5.0.0" 585 | }, 586 | "engines": { 587 | "node": ">=10" 588 | }, 589 | "funding": { 590 | "url": "https://github.com/sponsors/sindresorhus" 591 | } 592 | }, 593 | "node_modules/lru-cache": { 594 | "version": "10.4.3", 595 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 596 | "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 597 | "dev": true 598 | }, 599 | "node_modules/make-dir": { 600 | "version": "4.0.0", 601 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", 602 | "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", 603 | "dev": true, 604 | "dependencies": { 605 | "semver": "^7.5.3" 606 | }, 607 | "engines": { 608 | "node": ">=10" 609 | }, 610 | "funding": { 611 | "url": "https://github.com/sponsors/sindresorhus" 612 | } 613 | }, 614 | "node_modules/minimatch": { 615 | "version": "9.0.5", 616 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 617 | "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 618 | "dev": true, 619 | "dependencies": { 620 | "brace-expansion": "^2.0.1" 621 | }, 622 | "engines": { 623 | "node": ">=16 || 14 >=14.17" 624 | }, 625 | "funding": { 626 | "url": "https://github.com/sponsors/isaacs" 627 | } 628 | }, 629 | "node_modules/minipass": { 630 | "version": "7.1.2", 631 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", 632 | "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", 633 | "dev": true, 634 | "engines": { 635 | "node": ">=16 || 14 >=14.17" 636 | } 637 | }, 638 | "node_modules/mri": { 639 | "version": "1.2.0", 640 | "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", 641 | "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", 642 | "dev": true, 643 | "engines": { 644 | "node": ">=4" 645 | } 646 | }, 647 | "node_modules/nano-staged": { 648 | "version": "0.8.0", 649 | "resolved": "https://registry.npmjs.org/nano-staged/-/nano-staged-0.8.0.tgz", 650 | "integrity": "sha512-QSEqPGTCJbkHU2yLvfY6huqYPjdBrOaTMKatO1F8nCSrkQGXeKwtCiCnsdxnuMhbg3DTVywKaeWLGCE5oJpq0g==", 651 | "dev": true, 652 | "dependencies": { 653 | "picocolors": "^1.0.0" 654 | }, 655 | "bin": { 656 | "nano-staged": "lib/bin.js" 657 | }, 658 | "engines": { 659 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 660 | } 661 | }, 662 | "node_modules/p-limit": { 663 | "version": "3.1.0", 664 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 665 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 666 | "dev": true, 667 | "dependencies": { 668 | "yocto-queue": "^0.1.0" 669 | }, 670 | "engines": { 671 | "node": ">=10" 672 | }, 673 | "funding": { 674 | "url": "https://github.com/sponsors/sindresorhus" 675 | } 676 | }, 677 | "node_modules/p-locate": { 678 | "version": "5.0.0", 679 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 680 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 681 | "dev": true, 682 | "dependencies": { 683 | "p-limit": "^3.0.2" 684 | }, 685 | "engines": { 686 | "node": ">=10" 687 | }, 688 | "funding": { 689 | "url": "https://github.com/sponsors/sindresorhus" 690 | } 691 | }, 692 | "node_modules/package-json-from-dist": { 693 | "version": "1.0.1", 694 | "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", 695 | "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", 696 | "dev": true 697 | }, 698 | "node_modules/path-exists": { 699 | "version": "4.0.0", 700 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 701 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 702 | "dev": true, 703 | "engines": { 704 | "node": ">=8" 705 | } 706 | }, 707 | "node_modules/path-key": { 708 | "version": "3.1.1", 709 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 710 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 711 | "dev": true, 712 | "engines": { 713 | "node": ">=8" 714 | } 715 | }, 716 | "node_modules/path-scurry": { 717 | "version": "1.11.1", 718 | "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", 719 | "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", 720 | "dev": true, 721 | "dependencies": { 722 | "lru-cache": "^10.2.0", 723 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 724 | }, 725 | "engines": { 726 | "node": ">=16 || 14 >=14.18" 727 | }, 728 | "funding": { 729 | "url": "https://github.com/sponsors/isaacs" 730 | } 731 | }, 732 | "node_modules/picocolors": { 733 | "version": "1.1.1", 734 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 735 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 736 | "dev": true 737 | }, 738 | "node_modules/prettier": { 739 | "version": "3.4.2", 740 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", 741 | "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", 742 | "dev": true, 743 | "bin": { 744 | "prettier": "bin/prettier.cjs" 745 | }, 746 | "engines": { 747 | "node": ">=14" 748 | }, 749 | "funding": { 750 | "url": "https://github.com/prettier/prettier?sponsor=1" 751 | } 752 | }, 753 | "node_modules/require-directory": { 754 | "version": "2.1.1", 755 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 756 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 757 | "dev": true, 758 | "engines": { 759 | "node": ">=0.10.0" 760 | } 761 | }, 762 | "node_modules/sade": { 763 | "version": "1.8.1", 764 | "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", 765 | "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", 766 | "dev": true, 767 | "dependencies": { 768 | "mri": "^1.1.0" 769 | }, 770 | "engines": { 771 | "node": ">=6" 772 | } 773 | }, 774 | "node_modules/semver": { 775 | "version": "7.6.3", 776 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", 777 | "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", 778 | "dev": true, 779 | "bin": { 780 | "semver": "bin/semver.js" 781 | }, 782 | "engines": { 783 | "node": ">=10" 784 | } 785 | }, 786 | "node_modules/shebang-command": { 787 | "version": "2.0.0", 788 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 789 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 790 | "dev": true, 791 | "dependencies": { 792 | "shebang-regex": "^3.0.0" 793 | }, 794 | "engines": { 795 | "node": ">=8" 796 | } 797 | }, 798 | "node_modules/shebang-regex": { 799 | "version": "3.0.0", 800 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 801 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 802 | "dev": true, 803 | "engines": { 804 | "node": ">=8" 805 | } 806 | }, 807 | "node_modules/signal-exit": { 808 | "version": "4.1.0", 809 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 810 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 811 | "dev": true, 812 | "engines": { 813 | "node": ">=14" 814 | }, 815 | "funding": { 816 | "url": "https://github.com/sponsors/isaacs" 817 | } 818 | }, 819 | "node_modules/source-map": { 820 | "version": "0.6.1", 821 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 822 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 823 | "dev": true, 824 | "engines": { 825 | "node": ">=0.10.0" 826 | } 827 | }, 828 | "node_modules/source-map-support": { 829 | "version": "0.5.21", 830 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 831 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 832 | "dev": true, 833 | "dependencies": { 834 | "buffer-from": "^1.0.0", 835 | "source-map": "^0.6.0" 836 | } 837 | }, 838 | "node_modules/string-width": { 839 | "version": "5.1.2", 840 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 841 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 842 | "dev": true, 843 | "dependencies": { 844 | "eastasianwidth": "^0.2.0", 845 | "emoji-regex": "^9.2.2", 846 | "strip-ansi": "^7.0.1" 847 | }, 848 | "engines": { 849 | "node": ">=12" 850 | }, 851 | "funding": { 852 | "url": "https://github.com/sponsors/sindresorhus" 853 | } 854 | }, 855 | "node_modules/string-width-cjs": { 856 | "name": "string-width", 857 | "version": "4.2.3", 858 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 859 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 860 | "dev": true, 861 | "dependencies": { 862 | "emoji-regex": "^8.0.0", 863 | "is-fullwidth-code-point": "^3.0.0", 864 | "strip-ansi": "^6.0.1" 865 | }, 866 | "engines": { 867 | "node": ">=8" 868 | } 869 | }, 870 | "node_modules/string-width-cjs/node_modules/ansi-regex": { 871 | "version": "5.0.1", 872 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 873 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 874 | "dev": true, 875 | "engines": { 876 | "node": ">=8" 877 | } 878 | }, 879 | "node_modules/string-width-cjs/node_modules/emoji-regex": { 880 | "version": "8.0.0", 881 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 882 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 883 | "dev": true 884 | }, 885 | "node_modules/string-width-cjs/node_modules/strip-ansi": { 886 | "version": "6.0.1", 887 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 888 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 889 | "dev": true, 890 | "dependencies": { 891 | "ansi-regex": "^5.0.1" 892 | }, 893 | "engines": { 894 | "node": ">=8" 895 | } 896 | }, 897 | "node_modules/strip-ansi": { 898 | "version": "7.1.0", 899 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 900 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 901 | "dev": true, 902 | "dependencies": { 903 | "ansi-regex": "^6.0.1" 904 | }, 905 | "engines": { 906 | "node": ">=12" 907 | }, 908 | "funding": { 909 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 910 | } 911 | }, 912 | "node_modules/strip-ansi-cjs": { 913 | "name": "strip-ansi", 914 | "version": "6.0.1", 915 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 916 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 917 | "dev": true, 918 | "dependencies": { 919 | "ansi-regex": "^5.0.1" 920 | }, 921 | "engines": { 922 | "node": ">=8" 923 | } 924 | }, 925 | "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { 926 | "version": "5.0.1", 927 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 928 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 929 | "dev": true, 930 | "engines": { 931 | "node": ">=8" 932 | } 933 | }, 934 | "node_modules/superstruct": { 935 | "version": "2.0.2", 936 | "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", 937 | "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", 938 | "dev": true, 939 | "engines": { 940 | "node": ">=14.0.0" 941 | } 942 | }, 943 | "node_modules/supports-color": { 944 | "version": "7.2.0", 945 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 946 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 947 | "dev": true, 948 | "dependencies": { 949 | "has-flag": "^4.0.0" 950 | }, 951 | "engines": { 952 | "node": ">=8" 953 | } 954 | }, 955 | "node_modules/terser": { 956 | "version": "5.37.0", 957 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", 958 | "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", 959 | "dev": true, 960 | "dependencies": { 961 | "@jridgewell/source-map": "^0.3.3", 962 | "acorn": "^8.8.2", 963 | "commander": "^2.20.0", 964 | "source-map-support": "~0.5.20" 965 | }, 966 | "bin": { 967 | "terser": "bin/terser" 968 | }, 969 | "engines": { 970 | "node": ">=10" 971 | } 972 | }, 973 | "node_modules/test-exclude": { 974 | "version": "7.0.1", 975 | "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", 976 | "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", 977 | "dev": true, 978 | "dependencies": { 979 | "@istanbuljs/schema": "^0.1.2", 980 | "glob": "^10.4.1", 981 | "minimatch": "^9.0.4" 982 | }, 983 | "engines": { 984 | "node": ">=18" 985 | } 986 | }, 987 | "node_modules/typescript": { 988 | "version": "5.7.2", 989 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", 990 | "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", 991 | "dev": true, 992 | "bin": { 993 | "tsc": "bin/tsc", 994 | "tsserver": "bin/tsserver" 995 | }, 996 | "engines": { 997 | "node": ">=14.17" 998 | } 999 | }, 1000 | "node_modules/uvu": { 1001 | "version": "0.5.6", 1002 | "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", 1003 | "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", 1004 | "dev": true, 1005 | "dependencies": { 1006 | "dequal": "^2.0.0", 1007 | "diff": "^5.0.0", 1008 | "kleur": "^4.0.3", 1009 | "sade": "^1.7.3" 1010 | }, 1011 | "bin": { 1012 | "uvu": "bin.js" 1013 | }, 1014 | "engines": { 1015 | "node": ">=8" 1016 | } 1017 | }, 1018 | "node_modules/v8-to-istanbul": { 1019 | "version": "9.3.0", 1020 | "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", 1021 | "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", 1022 | "dev": true, 1023 | "dependencies": { 1024 | "@jridgewell/trace-mapping": "^0.3.12", 1025 | "@types/istanbul-lib-coverage": "^2.0.1", 1026 | "convert-source-map": "^2.0.0" 1027 | }, 1028 | "engines": { 1029 | "node": ">=10.12.0" 1030 | } 1031 | }, 1032 | "node_modules/which": { 1033 | "version": "2.0.2", 1034 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1035 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1036 | "dev": true, 1037 | "dependencies": { 1038 | "isexe": "^2.0.0" 1039 | }, 1040 | "bin": { 1041 | "node-which": "bin/node-which" 1042 | }, 1043 | "engines": { 1044 | "node": ">= 8" 1045 | } 1046 | }, 1047 | "node_modules/wrap-ansi": { 1048 | "version": "8.1.0", 1049 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 1050 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 1051 | "dev": true, 1052 | "dependencies": { 1053 | "ansi-styles": "^6.1.0", 1054 | "string-width": "^5.0.1", 1055 | "strip-ansi": "^7.0.1" 1056 | }, 1057 | "engines": { 1058 | "node": ">=12" 1059 | }, 1060 | "funding": { 1061 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1062 | } 1063 | }, 1064 | "node_modules/wrap-ansi-cjs": { 1065 | "name": "wrap-ansi", 1066 | "version": "7.0.0", 1067 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1068 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1069 | "dev": true, 1070 | "dependencies": { 1071 | "ansi-styles": "^4.0.0", 1072 | "string-width": "^4.1.0", 1073 | "strip-ansi": "^6.0.0" 1074 | }, 1075 | "engines": { 1076 | "node": ">=10" 1077 | }, 1078 | "funding": { 1079 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1080 | } 1081 | }, 1082 | "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { 1083 | "version": "5.0.1", 1084 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1085 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1086 | "dev": true, 1087 | "engines": { 1088 | "node": ">=8" 1089 | } 1090 | }, 1091 | "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { 1092 | "version": "4.3.0", 1093 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1094 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1095 | "dev": true, 1096 | "dependencies": { 1097 | "color-convert": "^2.0.1" 1098 | }, 1099 | "engines": { 1100 | "node": ">=8" 1101 | }, 1102 | "funding": { 1103 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1104 | } 1105 | }, 1106 | "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 1107 | "version": "8.0.0", 1108 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1109 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1110 | "dev": true 1111 | }, 1112 | "node_modules/wrap-ansi-cjs/node_modules/string-width": { 1113 | "version": "4.2.3", 1114 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1115 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1116 | "dev": true, 1117 | "dependencies": { 1118 | "emoji-regex": "^8.0.0", 1119 | "is-fullwidth-code-point": "^3.0.0", 1120 | "strip-ansi": "^6.0.1" 1121 | }, 1122 | "engines": { 1123 | "node": ">=8" 1124 | } 1125 | }, 1126 | "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 1127 | "version": "6.0.1", 1128 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1129 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1130 | "dev": true, 1131 | "dependencies": { 1132 | "ansi-regex": "^5.0.1" 1133 | }, 1134 | "engines": { 1135 | "node": ">=8" 1136 | } 1137 | }, 1138 | "node_modules/y18n": { 1139 | "version": "5.0.8", 1140 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1141 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1142 | "dev": true, 1143 | "engines": { 1144 | "node": ">=10" 1145 | } 1146 | }, 1147 | "node_modules/yargs": { 1148 | "version": "17.7.2", 1149 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 1150 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 1151 | "dev": true, 1152 | "dependencies": { 1153 | "cliui": "^8.0.1", 1154 | "escalade": "^3.1.1", 1155 | "get-caller-file": "^2.0.5", 1156 | "require-directory": "^2.1.1", 1157 | "string-width": "^4.2.3", 1158 | "y18n": "^5.0.5", 1159 | "yargs-parser": "^21.1.1" 1160 | }, 1161 | "engines": { 1162 | "node": ">=12" 1163 | } 1164 | }, 1165 | "node_modules/yargs-parser": { 1166 | "version": "21.1.1", 1167 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 1168 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 1169 | "dev": true, 1170 | "engines": { 1171 | "node": ">=12" 1172 | } 1173 | }, 1174 | "node_modules/yargs/node_modules/ansi-regex": { 1175 | "version": "5.0.1", 1176 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1177 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1178 | "dev": true, 1179 | "engines": { 1180 | "node": ">=8" 1181 | } 1182 | }, 1183 | "node_modules/yargs/node_modules/emoji-regex": { 1184 | "version": "8.0.0", 1185 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1186 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1187 | "dev": true 1188 | }, 1189 | "node_modules/yargs/node_modules/string-width": { 1190 | "version": "4.2.3", 1191 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1192 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1193 | "dev": true, 1194 | "dependencies": { 1195 | "emoji-regex": "^8.0.0", 1196 | "is-fullwidth-code-point": "^3.0.0", 1197 | "strip-ansi": "^6.0.1" 1198 | }, 1199 | "engines": { 1200 | "node": ">=8" 1201 | } 1202 | }, 1203 | "node_modules/yargs/node_modules/strip-ansi": { 1204 | "version": "6.0.1", 1205 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1206 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1207 | "dev": true, 1208 | "dependencies": { 1209 | "ansi-regex": "^5.0.1" 1210 | }, 1211 | "engines": { 1212 | "node": ">=8" 1213 | } 1214 | }, 1215 | "node_modules/yocto-queue": { 1216 | "version": "0.1.0", 1217 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1218 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1219 | "dev": true, 1220 | "engines": { 1221 | "node": ">=10" 1222 | }, 1223 | "funding": { 1224 | "url": "https://github.com/sponsors/sindresorhus" 1225 | } 1226 | }, 1227 | "node_modules/zod": { 1228 | "version": "3.24.1", 1229 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", 1230 | "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", 1231 | "dev": true, 1232 | "funding": { 1233 | "url": "https://github.com/sponsors/colinhacks" 1234 | } 1235 | } 1236 | } 1237 | } 1238 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "banditstash", 3 | "version": "0.1.0", 4 | "description": "Type-safe wrapper for localStorage / sessionStorage", 5 | "author": "Vladimir Klepov v.klepov@gmail.com", 6 | "license": "MIT", 7 | "type": "module", 8 | "sideEffects": false, 9 | "exports": { 10 | ".": "./dist/index.js" 11 | }, 12 | "types": "./dist/index.d.ts", 13 | "engines": { 14 | "node": "^14 || ^16 || ^18 || >=20" 15 | }, 16 | "scripts": { 17 | "test": "npm run build && uvu dist/tests '.test.js$'", 18 | "test:coverage": "c8 --100 npm test", 19 | "lint": "prettier --check --log-level warn .", 20 | "format": "prettier --write --log-level warn .", 21 | "verify": "npm run lint && npm run test:coverage", 22 | "terser": "npx npx terser dist/index.js -m -c --toplevel > dist/index.min.js", 23 | "size:raw": "cat dist/index.min.js | wc -c", 24 | "size:gz": "cat dist/index.min.js | gzip -c | wc -c", 25 | "size": "npm run terser && npm run size:raw && npm run size:gz", 26 | "build": "tsc" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/thoughtspile/banditstash.git" 31 | }, 32 | "keywords": [ 33 | "localstorage", 34 | "sessionstorage", 35 | "typescript", 36 | "zod" 37 | ], 38 | "bugs": { 39 | "url": "https://github.com/thoughtspile/banditstash/issues" 40 | }, 41 | "homepage": "https://github.com/thoughtspile/banditstash#readme", 42 | "devDependencies": { 43 | "arson": "^0.2.6", 44 | "banditypes": "^0.2.5", 45 | "c8": "^10.1.3", 46 | "husky": "^9.1.7", 47 | "nano-staged": "^0.8.0", 48 | "prettier": "^3.4.2", 49 | "superstruct": "^2.0.2", 50 | "terser": "^5.37.0", 51 | "typescript": "^5.7.2", 52 | "uvu": "^0.5.6", 53 | "zod": "^3.24.1" 54 | }, 55 | "nano-staged": { 56 | "*": "prettier --log-level warn --write" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | var id = (v: T) => v; 2 | 3 | export function makeBanditStash(storage: TypedStorage) { 4 | var store: BanditStash = { 5 | getItem: storage.getItem.bind(storage), 6 | setItem: storage.setItem.bind(storage), 7 | removeItem: storage.removeItem.bind(storage), 8 | 9 | format: (options) => 10 | makeBanditStash({ 11 | getItem: (key) => (options.parse || id)(storage.getItem(key)), 12 | setItem: (key, value) => 13 | storage.setItem(key, (options.prepare || id)(value)), 14 | removeItem: store.removeItem, 15 | }), 16 | use: (options) => makeBanditStash(options(store)), 17 | 18 | singleton: (key) => ({ 19 | getItem: storage.getItem.bind(storage, key), 20 | setItem: storage.setItem.bind(storage, key), 21 | removeItem: storage.removeItem.bind(storage, key), 22 | }), 23 | }; 24 | return store; 25 | } 26 | 27 | export function json() { 28 | return { 29 | parse: JSON.parse as (raw: string | null) => Json, 30 | prepare: JSON.stringify as (data: Json) => string, 31 | }; 32 | } 33 | 34 | export function scope(prefix: string): BanditPlugin { 35 | return (storage) => ({ 36 | getItem: (key) => storage.getItem(prefix + ":" + key), 37 | setItem: (key, value) => storage.setItem(prefix + ":" + key, value), 38 | removeItem: (key) => storage.removeItem(prefix + ":" + key), 39 | }); 40 | } 41 | 42 | export function safeGet(fallback: () => T): BanditPlugin { 43 | return (store) => ({ 44 | getItem: (key) => { 45 | try { 46 | return store.getItem(key); 47 | } catch (err) { 48 | return fallback(); 49 | } 50 | }, 51 | setItem: store.setItem, 52 | removeItem: store.removeItem, 53 | }); 54 | } 55 | 56 | export function safeSet(): BanditPlugin { 57 | return (store) => ({ 58 | setItem: (key, value) => { 59 | try { 60 | store.setItem(key, value); 61 | } catch (err) {} 62 | }, 63 | getItem: store.getItem, 64 | removeItem: store.removeItem, 65 | }); 66 | } 67 | 68 | export function fail(message?: string): never { 69 | throw new TypeError(message || "BanditStash error"); 70 | } 71 | 72 | export function noStorage(): TypedStorage { 73 | return { getItem: fail, setItem: fail, removeItem: fail }; 74 | } 75 | 76 | /** types */ 77 | type Primitive = string | number | boolean | null | undefined; 78 | export type Json = Primitive | Json[] | { [key: string]: Json }; 79 | 80 | export interface BanditStash extends TypedStorage { 81 | singleton: (key: Key) => SingletonStore; 82 | use: (plugin: BanditPlugin) => BanditStash; 83 | format: (formatter: BanditFormatter) => BanditStash; 84 | } 85 | 86 | export interface TypedStorage { 87 | getItem: (key: string) => ItemType; 88 | setItem: (key: string, value: ItemType) => void; 89 | removeItem: (key: string) => void; 90 | } 91 | 92 | export type BanditFormatter = ([Inner] extends [Outer] 93 | ? { parse?: (rawValue: Inner) => Outer } 94 | : { parse: (rawValue: Inner) => Outer }) & 95 | ([Outer] extends [Inner] 96 | ? { prepare?: (rawValue: Outer) => Inner } 97 | : { prepare: (rawValue: Outer) => Inner }); 98 | 99 | export type BanditPlugin = ( 100 | store: BanditStash, 101 | ) => TypedStorage; 102 | 103 | export interface SingletonStore { 104 | getItem: () => T; 105 | setItem: (value: T) => void; 106 | removeItem: () => void; 107 | } 108 | 109 | export function banditStash(options: BanditStashOptions) { 110 | let store = makeBanditStash(options.storage || noStorage()) 111 | .format(json()) 112 | .format(options); 113 | options.scope && (store = store.use(scope(options.scope))); 114 | options.fallback && (store = store.use(safeGet(options.fallback))); 115 | options.safeSet !== false && (store = store.use(safeSet())); 116 | return store; 117 | } 118 | 119 | export type BanditStashOptions = BanditFormatter & { 120 | storage: 121 | | Pick 122 | | null 123 | | undefined; 124 | scope?: string; 125 | fallback: false | (() => T); 126 | safeSet?: boolean; 127 | }; 128 | -------------------------------------------------------------------------------- /src/tests/core.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from "uvu"; 2 | import { is, throws } from "uvu/assert"; 3 | import { fail, makeBanditStash } from "../index.js"; 4 | import { memoryStorage } from "./memoryStorage.js"; 5 | 6 | const { data, storage } = memoryStorage(); 7 | const core = suite("core"); 8 | core.before.each(() => data.clear()); 9 | 10 | core("getItem", () => { 11 | data.set("breakfast", "beer"); 12 | is(makeBanditStash(storage).getItem("breakfast"), "beer"); 13 | }); 14 | core("getItem miss", () => { 15 | is(makeBanditStash(storage).getItem("miss"), null); 16 | }); 17 | core("getItem binding", () => { 18 | data.set("breakfast", "beer"); 19 | const { getItem } = makeBanditStash(storage); 20 | is(getItem("breakfast"), "beer"); 21 | }); 22 | 23 | core("setItem", () => { 24 | makeBanditStash(storage).setItem("breakfast", "beer"); 25 | is(data.get("breakfast"), "beer"); 26 | }); 27 | core("setItem binding", () => { 28 | const { setItem } = makeBanditStash(storage); 29 | setItem("breakfast", "beer"); 30 | is(data.get("breakfast"), "beer"); 31 | }); 32 | 33 | core("removeItem", () => { 34 | data.set("breakfast", "beer"); 35 | makeBanditStash(storage).removeItem("breakfast"); 36 | is(data.has("breakfast"), false); 37 | }); 38 | core("removeItem binding", () => { 39 | data.set("breakfast", "beer"); 40 | const { removeItem } = makeBanditStash(storage); 41 | removeItem("breakfast"); 42 | is(data.has("breakfast"), false); 43 | }); 44 | 45 | core("format parses item", () => { 46 | data.set("breakfast", "beer"); 47 | const res = makeBanditStash(storage) 48 | .format({ parse: (data) => `parsed:${data}` }) 49 | .getItem("breakfast"); 50 | is(res, "parsed:beer"); 51 | }); 52 | core("format parse fail throws error", () => { 53 | const res = makeBanditStash(storage).format({ parse: () => fail() }); 54 | throws(() => res.getItem("key"), TypeError, "BanditStore"); 55 | }); 56 | core("format prepares item", () => { 57 | makeBanditStash(storage) 58 | .format({ prepare: (data) => `prepared:${data}` }) 59 | .setItem("breakfast", "beer"); 60 | is(data.get("breakfast"), "prepared:beer"); 61 | }); 62 | core("format: parse & prepare are optional", () => { 63 | const noopFmt = makeBanditStash(storage).format({}); 64 | noopFmt.setItem("hello", "beer"); 65 | is(data.get("hello"), "beer"); 66 | is(noopFmt.getItem("hello"), "beer"); 67 | }); 68 | 69 | core("use maps methods", () => { 70 | const stash = makeBanditStash(storage).use((store) => ({ 71 | getItem: () => "get-trick", 72 | setItem: () => "set-trick", 73 | removeItem: () => "remove-trick", 74 | })); 75 | is(stash.getItem("any"), "get-trick"); 76 | is(stash.setItem("any", "any"), "set-trick"); 77 | is(stash.removeItem("any"), "remove-trick"); 78 | }); 79 | 80 | core("singleton", () => { 81 | const stash = makeBanditStash(storage).singleton("secret"); 82 | stash.setItem("secret:value"); 83 | is(data.get("secret"), "secret:value"); 84 | is(stash.getItem(), "secret:value"); 85 | stash.removeItem(); 86 | is(data.has("secret"), false); 87 | }); 88 | 89 | core.run(); 90 | -------------------------------------------------------------------------------- /src/tests/full.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from "uvu"; 2 | import { equal, is, throws } from "uvu/assert"; 3 | import { fail, banditStash } from "../index.js"; 4 | import { memoryStorage } from "./memoryStorage.js"; 5 | 6 | const { data, storage } = memoryStorage(); 7 | const full = suite("default factory"); 8 | full.before.each(() => data.clear()); 9 | 10 | const fallback = () => new Set([0]); 11 | const setStash = banditStash>({ 12 | storage, 13 | prepare: (set) => Array.from(set), 14 | parse: (raw) => 15 | Array.isArray(raw) 16 | ? new Set(raw.filter((x): x is number => typeof x === "number")) 17 | : fail(), 18 | fallback, 19 | }); 20 | 21 | full("setItem", () => { 22 | const set = new Set([10, 11]); 23 | setStash.setItem("secret", set); 24 | is(data.has("secret"), true); 25 | }); 26 | full("getItem", () => { 27 | const set = new Set([10, 11]); 28 | setStash.setItem("secret", set); 29 | equal(setStash.getItem("secret"), set); 30 | }); 31 | full("removeItem", () => { 32 | setStash.setItem("secret", new Set([10, 11])); 33 | setStash.removeItem("secret"); 34 | is(data.has("secret"), false); 35 | }); 36 | full.run(); 37 | 38 | const fallbackSuite = suite("fallback"); 39 | fallbackSuite.before.each(() => data.clear()); 40 | fallbackSuite("fallback on missing", () => { 41 | equal(setStash.getItem("miss"), fallback()); 42 | }); 43 | fallbackSuite("fallback on bad JSON", () => { 44 | data.set("bad", "[[{"); 45 | equal(setStash.getItem("bad"), fallback()); 46 | }); 47 | fallbackSuite("fallback on parse error", () => { 48 | data.set("bad", JSON.stringify({ hello: "world" })); 49 | equal(setStash.getItem("bad"), fallback()); 50 | }); 51 | 52 | const unsafeGet = banditStash>({ 53 | storage, 54 | prepare: (set) => Array.from(set), 55 | parse: (raw) => 56 | Array.isArray(raw) 57 | ? new Set(raw.filter((x): x is number => typeof x === "number")) 58 | : fail(), 59 | fallback: false, 60 | }); 61 | fallbackSuite("throw on missing w/ fallback: false", () => { 62 | throws(() => unsafeGet.getItem("bad")); 63 | }); 64 | fallbackSuite("throw on bad JSON w/ fallback: false", () => { 65 | data.set("bad", "[[{"); 66 | throws(() => unsafeGet.getItem("bad")); 67 | }); 68 | fallbackSuite("throw on parse error w/ fallback: false", () => { 69 | data.set("bad", JSON.stringify({ hello: "world" })); 70 | throws(() => unsafeGet.getItem("bad")); 71 | }); 72 | fallbackSuite.run(); 73 | 74 | const safeSetSuite = suite("safeSet"); 75 | safeSetSuite.before.each(() => data.clear()); 76 | safeSetSuite("ignores error", () => { 77 | const stash = banditStash({ 78 | storage, 79 | prepare: () => { 80 | throw new Error(); 81 | }, 82 | fallback: false, 83 | }); 84 | stash.setItem("bad", ""); 85 | }); 86 | 87 | safeSetSuite("throw on prepare error w/ safeSet: false", () => { 88 | const stash = banditStash({ 89 | storage, 90 | prepare: () => { 91 | throw new Error(); 92 | }, 93 | fallback: false, 94 | safeSet: false, 95 | }); 96 | throws(() => stash.setItem("bad", "")); 97 | }); 98 | safeSetSuite.run(); 99 | 100 | const scopeSuite = suite("scope"); 101 | scopeSuite("allows scope", () => { 102 | const stash = banditStash({ 103 | storage, 104 | parse: String, 105 | fallback: false, 106 | scope: "prefix", 107 | }); 108 | stash.setItem("key", "sub-value"); 109 | is(data.get("prefix:key"), '"sub-value"'); 110 | is(stash.getItem("key"), "sub-value"); 111 | stash.removeItem("key"); 112 | is(data.has("prefix:key"), false); 113 | }); 114 | scopeSuite.run(); 115 | 116 | const noStorageSuite = suite("missing storage"); 117 | noStorageSuite("can be constructed", () => { 118 | banditStash({ 119 | storage: undefined, 120 | parse: String, 121 | fallback: false, 122 | }); 123 | }); 124 | noStorageSuite("all methods throw", () => { 125 | const noStorage = banditStash({ 126 | storage: undefined, 127 | parse: String, 128 | fallback: false, 129 | safeSet: false, 130 | }); 131 | throws(() => noStorage.getItem("key")); 132 | throws(() => noStorage.setItem("key", "value")); 133 | throws(() => noStorage.removeItem("key")); 134 | }); 135 | noStorageSuite.run(); 136 | -------------------------------------------------------------------------------- /src/tests/integration.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "uvu"; 2 | import { z } from "zod"; 3 | import { object, string, number, min, type Infer } from "superstruct"; 4 | import * as banditypes from "banditypes"; 5 | import arson from "arson"; 6 | import { equal, is, throws } from "uvu/assert"; 7 | import { banditStash, fail, makeBanditStash } from "../index.js"; 8 | import { memoryStorage } from "./memoryStorage.js"; 9 | 10 | const { data, storage } = memoryStorage(); 11 | test.before.each(() => data.clear()); 12 | test("can make set store", () => { 13 | const store = banditStash>({ 14 | storage, 15 | parse: (raw) => { 16 | // parse must convert arbitrary JSON to Set... 17 | if (Array.isArray(raw)) { 18 | return new Set(raw.filter((x): x is number => typeof x === "number")); 19 | } 20 | // or throw error via fail() 21 | fail(); 22 | }, 23 | // prepare must convert Set to a JSON-serializable format 24 | prepare: (data) => Array.from(data), 25 | // If getItem can't return Set 26 | fallback: () => new Set(), 27 | }); 28 | store.setItem("key", new Set([1, 2, 3])); 29 | is(data.get("key"), "[1,2,3]"); 30 | equal(store.getItem("key"), new Set([1, 2, 3])); 31 | store.removeItem("key"); 32 | is(data.get("key"), undefined); 33 | equal(store.getItem("miss"), new Set()); 34 | }); 35 | 36 | test("works with zod validator", () => { 37 | const rentSchema = z.object({ 38 | price: z.number(), 39 | location: z.object({ 40 | country: z.string(), 41 | city: z.string(), 42 | }), 43 | }); 44 | const store = banditStash>({ 45 | storage, 46 | parse: rentSchema.parse, 47 | fallback: false, 48 | }); 49 | const sample = { 50 | price: 100, 51 | location: { country: "israel", city: "Tel Aviv" }, 52 | }; 53 | store.setItem("key", sample); 54 | is(data.get("key"), JSON.stringify(sample)); 55 | equal(store.getItem("key"), sample); 56 | store.removeItem("key"); 57 | is(data.get("key"), undefined); 58 | data.set("invalid", JSON.stringify({ location: sample.location })); 59 | throws(() => store.getItem("invalid")); 60 | }); 61 | 62 | test("works with superstruct validator", () => { 63 | const rentSchema = object({ 64 | price: min(number(), 0), 65 | location: object({ 66 | country: string(), 67 | city: string(), 68 | }), 69 | }); 70 | const store = banditStash>({ 71 | storage, 72 | parse: (raw) => rentSchema.create(raw), 73 | fallback: false, 74 | }); 75 | const sample = { 76 | price: 100, 77 | location: { country: "israel", city: "Tel Aviv" }, 78 | }; 79 | store.setItem("key", sample); 80 | is(data.get("key"), JSON.stringify(sample)); 81 | equal(store.getItem("key"), sample); 82 | store.removeItem("key"); 83 | is(data.get("key"), undefined); 84 | data.set("invalid", JSON.stringify({ location: sample.location })); 85 | throws(() => store.getItem("invalid")); 86 | }); 87 | 88 | test("works with banditypes validator", () => { 89 | const rentSchema = banditypes.object({ 90 | price: banditypes.number().map((n) => (n >= 0 ? n : banditypes.fail())), 91 | location: banditypes.object({ 92 | country: banditypes.string(), 93 | city: banditypes.string(), 94 | }), 95 | }); 96 | const store = banditStash>({ 97 | storage, 98 | parse: (raw) => rentSchema(raw), 99 | fallback: false, 100 | }); 101 | const sample = { 102 | price: 100, 103 | location: { country: "israel", city: "Tel Aviv" }, 104 | }; 105 | store.setItem("key", sample); 106 | is(data.get("key"), JSON.stringify(sample)); 107 | equal(store.getItem("key"), sample); 108 | store.removeItem("key"); 109 | is(data.get("key"), undefined); 110 | data.set("invalid", JSON.stringify({ location: sample.location })); 111 | throws(() => store.getItem("invalid")); 112 | }); 113 | 114 | test("works with arson serializer", () => { 115 | const store = makeBanditStash(storage).format>({ 116 | parse: arson.parse, 117 | prepare: arson.stringify, 118 | }); 119 | store.setItem("key", new Set([1, 2, 3])); 120 | is(data.get("key"), arson.stringify(new Set([1, 2, 3]))); 121 | equal(store.getItem("key"), new Set([1, 2, 3])); 122 | store.removeItem("key"); 123 | is(data.get("key"), undefined); 124 | throws(() => store.getItem("miss")); 125 | }); 126 | 127 | test.run(); 128 | -------------------------------------------------------------------------------- /src/tests/json.test.ts: -------------------------------------------------------------------------------- 1 | import { suite as makeSuite } from "uvu"; 2 | import { equal, is, throws } from "uvu/assert"; 3 | import { json, makeBanditStash } from "../index.js"; 4 | import { memoryStorage } from "./memoryStorage.js"; 5 | 6 | const { data, storage } = memoryStorage(); 7 | const suite = makeSuite("json"); 8 | suite.before.each(() => data.clear()); 9 | const stash = makeBanditStash(storage).format(json()); 10 | 11 | suite("getItem", () => { 12 | data.set("breakfast", JSON.stringify({ beer: true })); 13 | equal(stash.getItem("breakfast"), { beer: true }); 14 | }); 15 | suite("getItem miss", () => { 16 | is(stash.getItem("miss"), null); 17 | }); 18 | suite("getItem bad JSON", () => { 19 | data.set("bad", "{__]"); 20 | throws(() => stash.getItem("bad")); 21 | }); 22 | 23 | suite("setItem", () => { 24 | stash.setItem("breakfast", { beer: true }); 25 | is(data.get("breakfast"), JSON.stringify({ beer: true })); 26 | }); 27 | suite("setItem error", () => { 28 | const obj = {}; 29 | obj["loop"] = obj; 30 | throws(() => stash.setItem("circle", obj)); 31 | is(data.has("circle"), false); 32 | }); 33 | 34 | suite.run(); 35 | -------------------------------------------------------------------------------- /src/tests/memoryStorage.ts: -------------------------------------------------------------------------------- 1 | import type { TypedStorage } from "../index.js"; 2 | 3 | export function memoryStorage() { 4 | const data = new Map(); 5 | const storage: TypedStorage = { 6 | getItem: (key) => data.get(key) ?? null, 7 | setItem: (key, value) => data.set(key, value), 8 | removeItem: (key) => data.delete(key), 9 | }; 10 | return { data, storage }; 11 | } 12 | -------------------------------------------------------------------------------- /src/tests/safety.test.ts: -------------------------------------------------------------------------------- 1 | import { suite as makeSuite } from "uvu"; 2 | import { equal, is, throws } from "uvu/assert"; 3 | import { 4 | fail, 5 | makeBanditStash, 6 | noStorage, 7 | safeGet, 8 | safeSet, 9 | } from "../index.js"; 10 | import { memoryStorage } from "./memoryStorage.js"; 11 | 12 | const { data, storage } = memoryStorage(); 13 | const safeGetSuite = makeSuite("safeGet"); 14 | safeGetSuite.before.each(() => data.clear()); 15 | 16 | safeGetSuite("fallback on parse error", () => { 17 | const stash = makeBanditStash(storage) 18 | .format({ parse: () => fail() }) 19 | .use(safeGet(() => "__fallback__")); 20 | is(stash.getItem("key"), "__fallback__"); 21 | }); 22 | safeGetSuite("fallback on noStorage", () => { 23 | const stash = makeBanditStash(noStorage()).use(safeGet(() => "__fallback__")); 24 | is(stash.getItem("key"), "__fallback__"); 25 | }); 26 | 27 | safeGetSuite.run(); 28 | 29 | const safeSetSuite = makeSuite("safeGet"); 30 | safeSetSuite.before.each(() => data.clear()); 31 | 32 | const error = () => { 33 | throw new Error(); 34 | }; 35 | safeSetSuite("ignores setItem throw", () => { 36 | const stash = makeBanditStash({ ...storage, setItem: error }).use(safeSet()); 37 | stash.setItem("key", "value"); 38 | }); 39 | safeSetSuite("ignores prepare throw", () => { 40 | const stash = makeBanditStash(storage) 41 | .format({ parse: () => "", prepare: error }) 42 | .use(safeSet()); 43 | stash.setItem("key", "value"); 44 | }); 45 | 46 | safeSetSuite.run(); 47 | -------------------------------------------------------------------------------- /src/tests/scope.test.ts: -------------------------------------------------------------------------------- 1 | import { suite as makeSuite } from "uvu"; 2 | import { equal, is, throws } from "uvu/assert"; 3 | import { scope, makeBanditStash } from "../index.js"; 4 | import { memoryStorage } from "./memoryStorage.js"; 5 | 6 | const { data, storage } = memoryStorage(); 7 | const suite = makeSuite("scope"); 8 | suite.before.each(() => data.clear()); 9 | const stash = makeBanditStash(storage).use(scope("prefix")); 10 | 11 | suite("getItem", () => { 12 | data.set("prefix:breakfast", "beer"); 13 | equal(stash.getItem("breakfast"), "beer"); 14 | }); 15 | suite("setItem", () => { 16 | stash.setItem("breakfast", "beer"); 17 | is(data.get("prefix:breakfast"), "beer"); 18 | }); 19 | suite("removeItem", () => { 20 | data.set("prefix:breakfast", "beer"); 21 | stash.removeItem("breakfast"); 22 | is(data.has("prefix:breakfast"), false); 23 | }); 24 | 25 | suite("chaining", () => { 26 | const sub = stash.use(scope("sub")); 27 | sub.setItem("key", "sub-value"); 28 | is(data.get("prefix:sub:key"), "sub-value"); 29 | is(sub.getItem("key"), "sub-value"); 30 | sub.removeItem("key"); 31 | is(data.has("prefix:sub:key"), false); 32 | }); 33 | 34 | suite("chaining singleton", () => { 35 | const secret = stash.singleton("secret"); 36 | secret.setItem("secret-value"); 37 | is(data.get("prefix:secret"), "secret-value"); 38 | is(secret.getItem(), "secret-value"); 39 | secret.removeItem(); 40 | is(data.has("prefix:secret"), false); 41 | }); 42 | 43 | suite.run(); 44 | -------------------------------------------------------------------------------- /src/tests/types.ts: -------------------------------------------------------------------------------- 1 | import { makeBanditStash, type BanditFormatter } from "../index.js"; 2 | 3 | // happy path 4 | const ok: BanditFormatter = { 5 | parse: (x: string) => Number(x), 6 | prepare: (x: number) => String(x), 7 | }; 8 | const returnTypesError: BanditFormatter = { 9 | // @ts-expect-error 10 | parse: () => 9, 11 | // @ts-expect-error 12 | prepare: () => 9, 13 | }; 14 | 15 | // @ts-expect-error 16 | const parsePrepareRequired: BanditFormatter = {}; 17 | // @ts-expect-error 18 | const prepareRequired: BanditFormatter = { 19 | parse: Number, 20 | }; 21 | // @ts-expect-error 22 | const parseRequired: BanditFormatter = { 23 | prepare: String, 24 | }; 25 | 26 | const parseOnlyOk: BanditFormatter = { 27 | parse: String, 28 | }; 29 | 30 | const prepareOnlyOk: BanditFormatter = { 31 | prepare: String, 32 | }; 33 | 34 | const eqAllowsEmpty: BanditFormatter = {}; 35 | const eqAllowsParams: BanditFormatter = { 36 | parse: () => "", 37 | prepare: () => "", 38 | }; 39 | 40 | const setStash = makeBanditStash>(localStorage as any); 41 | const x: Set = setStash.getItem("key"); 42 | setStash.removeItem("key"); 43 | setStash.setItem("key", new Set(["10"])); 44 | // @ts-expect-error 45 | setStash.setItem("key", new Set([10])); 46 | // @ts-expect-error 47 | setStash.setItem("key", 10); 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2020", 4 | "lib": ["ES6", "DOM"], 5 | "moduleResolution": "node", 6 | "target": "ES6", 7 | "verbatimModuleSyntax": true, 8 | "isolatedModules": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strictNullChecks": true, 11 | "noUncheckedIndexedAccess": true, 12 | "esModuleInterop": true, 13 | "declaration": true, 14 | "rootDirs": ["./src"], 15 | "outDir": "dist" 16 | }, 17 | "include": ["src"] 18 | } 19 | --------------------------------------------------------------------------------