diff --git a/.github/ISSUE_TEMPLATE/---bug-report.yml b/.github/ISSUE_TEMPLATE/---bug-report.yml index 408f66e..4f43e12 100644 --- a/.github/ISSUE_TEMPLATE/---bug-report.yml +++ b/.github/ISSUE_TEMPLATE/---bug-report.yml @@ -13,7 +13,7 @@ body: required: true attributes: label: 🛠️ To reproduce - description: A reproduction of the bug via https://stackblitz.com/github/danielroe/package-name/tree/main/playground + description: A reproduction of the bug via https://stackblitz.com/github/danielroe/fnv1a-64/tree/main/playground placeholder: https://stackblitz.com/[...] - type: textarea validations: diff --git a/README.md b/README.md index 0e717e5..b2e346d 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,79 @@ -# package-name +# fnv1a-64 [![npm version][npm-version-src]][npm-version-href] [![npm downloads][npm-downloads-src]][npm-downloads-href] [![Github Actions][github-actions-src]][github-actions-href] [![Codecov][codecov-src]][codecov-href] -> Package description +> Tiny, fast, dependency-free 64-bit [FNV-1a](http://www.isthe.com/chongo/tech/comp/fnv/) string hash for Node and the browser. -## Usage +The core computes a true 64-bit hash in two 32-bit lanes with plain JS numbers. It's ~320B gzipped (minified) and hashes a short key in ~36ns. + +## Why + +I created this for [Nuxt](https://github.com/nuxt/nuxt) due to the following issues with other FNV packages. These are all good packages and you should consider them if you don't have the same constraints I did: -Install package: +| Package | Width | Gzip | Notes | +| --- | --- | --- | --- | +| `fnv1a`, `object-code` | 32-bit | tiny | collides at ~50k distinct keys | +| `@sindresorhus/fnv1a` | 64-bit | ~1KB | BigInt, ~20x slower | +| `fnv-lite` | 128-bit | ~1.4KB | byte arrays, ~40x slower | +| `fnv-plus` | multi | ~9KB | _excellent_ package, but contains lots of utilities | +| `fnv-hash` | 64-bit | native | can't run in the browser | + +## Usage ```sh -# npm -npm install package-name +npm install fnv1a-64 +``` + +```js +import { fnv1a64, fnv1a64Base36, fnv1a64BigInt, fnv1a64Hex } from 'fnv1a-64' -# pnpm -pnpm install package-name +fnv1a64Hex('hello world') // => '779a65e7023cd2e7' (16-char zero-padded hex) +fnv1a64Base36('hello world') // => '1th7cxzlyc0dj' (shortest, good for cache keys) +fnv1a64BigInt('hello world') // => 8618312879776256743n +fnv1a64('hello world') // => { high: 2006607335, low: 37540583 } (fast core, no BigInt) ``` +- **`fnv1a64Hex`** and **`fnv1a64Base36`** are what you usually want as a map or cache key. +- **`fnv1a64`** returns the raw 32-bit lanes and is the fastest path if you never need a string. +- **`fnv1a64BigInt`** composes the lanes into a `bigint` when you want a single comparable number. + +All outputs are deterministic: the same input always produces the same result. Hex is always exactly 16 characters; base36 length varies with the value but is stable per value. + +## Non-ASCII: UTF-16 code units, not UTF-8 bytes + +The hash iterates `str.charCodeAt(i)`, so it hashes UTF-16 code units rather than UTF-8 bytes. For **ASCII input this is bit-for-bit identical to a canonical FNV-1a-64**. For non-ASCII (accents, CJK, emoji) the output is stable and collision-resistant but will **not** match an FNV-1a-64 computed over the UTF-8 encoding of the same string. + +If you need to interoperate with a hash produced elsewhere over UTF-8 bytes, encode first: + ```js -import {} from 'package-name' +const bytes = new TextEncoder().encode(str) +// then hash the bytes with a UTF-8-aware FNV-1a-64 implementation ``` +> [!IMPORTANT] + +> FNV-1a is a fast, **non-cryptographic** hash. It's useful for hash tables, cache keys, checksums, and bucketing, but do not use it for anything security-sensitive: it is not collision-resistant against an adversary and offers no preimage resistance. + +## Benchmark + +Run `pnpm bench` (uses [mitata](https://github.com/evanwashere/mitata)). Indicative numbers on Node 22 (Apple silicon), lower is better: + +| | width | short key | 1 KB string | +| --- | --- | --- | --- | +| `fnv1a64` (lanes) | 64 | **~42 ns** | ~2.7 µs | +| `fnv1a64Base36` | 64 | ~98 ns | ~2.8 µs | +| `fnv1a64Hex` | 64 | ~339 ns | ~3.0 µs | +| `fnv-plus` `fast1a64` | 64 | ~63 ns | **~1.7 µs** | +| `murmurhash` v3 | 32 | ~257 ns | ~1.7 µs | +| `@sindresorhus/fnv1a` | 64 | ~714 ns | ~29 µs | +| `fnv-lite` `hex` | 128 | ~5.6 µs | ~297 µs | +| `xxhashjs` `h64` | 64 | ~34 µs | ~59 µs | + +The `fnv1a64` core wins short keys outright. `fnv-plus` is competitive (and faster on long strings) but ships ~9 KB gzipped for a whole multi-width toolkit rather than one function. `murmurhash` is faster than 64-bit alternatives but is 32-bit, so it collides. `fnv-lite` and `xxhashjs` pay a large constant cost for their byte-array / `cuint` internals. + ## 💻 Development - Clone this repository @@ -38,11 +89,11 @@ Published under [MIT License](./LICENCE). -[npm-version-src]: https://npmx.dev/api/registry/badge/version/package-name -[npm-version-href]: https://npmx.dev/package/package-name -[npm-downloads-src]: https://npmx.dev/api/registry/badge/downloads/package-name -[npm-downloads-href]: https://npm.chart.dev/package-name -[github-actions-src]: https://img.shields.io/github/actions/workflow/status/danielroe/package-name/ci.yml?branch=main&style=flat-square -[github-actions-href]: https://github.com/danielroe/package-name/actions?query=workflow%3Aci -[codecov-src]: https://img.shields.io/codecov/c/gh/danielroe/package-name/main?style=flat-square -[codecov-href]: https://codecov.io/gh/danielroe/package-name +[npm-version-src]: https://npmx.dev/api/registry/badge/version/fnv1a-64 +[npm-version-href]: https://npmx.dev/package/fnv1a-64 +[npm-downloads-src]: https://npmx.dev/api/registry/badge/downloads/fnv1a-64 +[npm-downloads-href]: https://npm.chart.dev/fnv1a-64 +[github-actions-src]: https://img.shields.io/github/actions/workflow/status/danielroe/fnv1a-64/ci.yml?branch=main&style=flat-square +[github-actions-href]: https://github.com/danielroe/fnv1a-64/actions?query=workflow%3Aci +[codecov-src]: https://img.shields.io/codecov/c/gh/danielroe/fnv1a-64/main?style=flat-square +[codecov-href]: https://codecov.io/gh/danielroe/fnv1a-64 diff --git a/bench.mjs b/bench.mjs new file mode 100644 index 0000000..c5d249c --- /dev/null +++ b/bench.mjs @@ -0,0 +1,25 @@ +import sindresorhusFnv1a from '@sindresorhus/fnv1a' +import fnvLite from 'fnv-lite' +import fnvPlus from 'fnv-plus' +import { bench, group, run } from 'mitata' +import murmurhash from 'murmurhash' +import xxhash from 'xxhashjs' +import { fnv1a64, fnv1a64Base36, fnv1a64Hex } from './src/index.ts' + +const short = 'user:12345:profile' +const long = 'a'.repeat(1024) + +for (const [label, input] of [['short key', short], ['1KB string', long]]) { + group(label, () => { + bench('fnv1a-64 (lanes)', () => fnv1a64(input)) + bench('fnv1a-64 (hex)', () => fnv1a64Hex(input)) + bench('fnv1a-64 (base36)', () => fnv1a64Base36(input)) + bench('@sindresorhus/fnv1a size:64', () => sindresorhusFnv1a(input, { size: 64 })) + bench('fnv-plus fast1a64 (64-bit)', () => fnvPlus.fast1a64(input)) + bench('fnv-lite hex (128-bit)', () => fnvLite.hex(input)) + bench('xxhashjs h64', () => xxhash.h64(input, 0).toString(16)) + bench('murmurhash v3 (32-bit)', () => murmurhash.v3(input)) + }) +} + +run() diff --git a/collisions.mjs b/collisions.mjs new file mode 100644 index 0000000..5247a96 --- /dev/null +++ b/collisions.mjs @@ -0,0 +1,43 @@ +/* eslint-disable no-console */ +import process from 'node:process' +import sindresorhusFnv1a from '@sindresorhus/fnv1a' +import fnvLite from 'fnv-lite' +import fnvPlus from 'fnv-plus' +import murmurhash from 'murmurhash' +import xxhash from 'xxhashjs' +import { fnv1a64Hex } from './src/index.ts' + +const N = Number(process.argv[2] ?? 1_000_000) + +const prefixes = ['user', 'session', 'cache', 'route', 'asset', 'chunk', 'node', 'query'] +function key(i) { + return `${prefixes[i % prefixes.length]}:${i}:${((i * 2654435761) >>> 0).toString(36)}` +} + +const hashers = { + 'fnv1a-64': i => fnv1a64Hex(key(i)), + '@sindresorhus/fnv1a size:64': i => sindresorhusFnv1a(key(i), { size: 64 }).toString(), + 'fnv-plus fast1a64 (64-bit)': i => fnvPlus.fast1a64(key(i)), + 'fnv-lite hex (128-bit)': i => fnvLite.hex(key(i)), + 'xxhashjs h64': i => xxhash.h64(key(i), 0).toString(16), + 'murmurhash v3 (32-bit)': i => String(murmurhash.v3(key(i))), +} + +console.log(`Hashing ${N.toLocaleString()} distinct realistic keys\n`) +console.log('hasher'.padEnd(32), 'collisions') +console.log('-'.repeat(46)) + +for (const [name, fn] of Object.entries(hashers)) { + const seen = new Set() + let collisions = 0 + for (let i = 0; i < N; i++) { + const h = fn(i) + if (seen.has(h)) { + collisions++ + } + else { + seen.add(h) + } + } + console.log(name.padEnd(32), collisions.toLocaleString()) +} diff --git a/package.json b/package.json index 4e00343..8879a7d 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,19 @@ { - "name": "package-name", + "name": "fnv1a-64", "type": "module", "version": "0.0.0", "packageManager": "pnpm@11.10.0", - "description": "", + "description": "Tiny, fast, dependency-free 64-bit FNV-1a string hash for Node and the browser", "license": "MIT", - "repository": "danielroe/package-name", + "repository": "danielroe/fnv1a-64", + "keywords": [ + "fnv", + "fnv1a", + "fnv-1a", + "hash", + "64-bit", + "non-cryptographic" + ], "sideEffects": false, "exports": { ".": "./src/index.ts", @@ -38,19 +46,27 @@ "test:unit": "vitest", "test:knip": "knip", "test:versions": "installed-check -d --no-workspaces", + "bench": "node bench.mjs", + "collisions": "node collisions.mjs", "test:types": "tsc --noEmit" }, "devDependencies": { "@antfu/eslint-config": "latest", + "@sindresorhus/fnv1a": "^3.1.0", "@vitest/coverage-v8": "latest", "eslint": "latest", + "fnv-lite": "^1.2.0", + "fnv-plus": "^1.3.1", "installed-check": "latest", "knip": "latest", + "mitata": "^1.0.34", + "murmurhash": "^2.0.1", "nano-staged": "1.0.2", "simple-git-hooks": "latest", "tsdown": "0.22.3", - "typescript": "latest", - "vitest": "latest" + "typescript": "6.0.3", + "vitest": "latest", + "xxhashjs": "^0.2.2" }, "simple-git-hooks": { "pre-commit": "npx nano-staged" diff --git a/playground/index.js b/playground/index.js index c8e3d2b..e30c034 100644 --- a/playground/index.js +++ b/playground/index.js @@ -1,7 +1,8 @@ import assert from 'node:assert' -import * as pkg from 'package-name' +import { fnv1a64, fnv1a64Base36, fnv1a64Hex } from 'fnv1a-64' // eslint-disable-next-line no-console -console.log(pkg.welcome()) +console.log(fnv1a64Hex('hello world'), fnv1a64Base36('hello world')) -assert.strictEqual(pkg.welcome(), 'hello world') +assert.strictEqual(fnv1a64Hex('hello world'), '779a65e7023cd2e7') +assert.deepStrictEqual(fnv1a64(''), { high: 0xCBF29CE4, low: 0x84222325 }) diff --git a/playground/package.json b/playground/package.json index 663c069..1983fbb 100644 --- a/playground/package.json +++ b/playground/package.json @@ -5,6 +5,6 @@ "dev": "node index.js" }, "dependencies": { - "package-name": "latest" + "fnv1a-64": "latest" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 194ac7f..666c6ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - package-name: link:. + fnv1a-64: link:. importers: @@ -13,19 +13,34 @@ importers: devDependencies: '@antfu/eslint-config': specifier: latest - version: 9.1.0(@typescript-eslint/rule-tester@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(@typescript-eslint/typescript-estree@8.62.0(typescript@7.0.2))(@typescript-eslint/utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(@vue/compiler-sfc@3.5.35)(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)(vitest@4.1.10) + version: 9.1.0(@typescript-eslint/rule-tester@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(@typescript-eslint/typescript-estree@8.62.0(typescript@6.0.3))(@typescript-eslint/utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.35)(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.10) + '@sindresorhus/fnv1a': + specifier: ^3.1.0 + version: 3.1.0 '@vitest/coverage-v8': specifier: latest version: 4.1.10(vitest@4.1.10) eslint: specifier: latest version: 10.7.0(jiti@2.7.0) + fnv-lite: + specifier: ^1.2.0 + version: 1.2.0 + fnv-plus: + specifier: ^1.3.1 + version: 1.3.1 installed-check: specifier: latest version: 10.0.1 knip: specifier: latest version: 6.26.0 + mitata: + specifier: ^1.0.34 + version: 1.0.34 + murmurhash: + specifier: ^2.0.1 + version: 2.0.1 nano-staged: specifier: 1.0.2 version: 1.0.2 @@ -34,17 +49,20 @@ importers: version: 2.13.1 tsdown: specifier: 0.22.3 - version: 0.22.3(oxc-resolver@11.21.3)(typescript@7.0.2) + version: 0.22.3(oxc-resolver@11.21.3)(typescript@6.0.3) typescript: - specifier: latest - version: 7.0.2 + specifier: 6.0.3 + version: 6.0.3 vitest: specifier: latest version: 4.1.10(@vitest/coverage-v8@4.1.10)(vite@8.0.16(jiti@2.7.0)(yaml@2.9.0)) + xxhashjs: + specifier: ^0.2.2 + version: 0.2.2 playground: dependencies: - package-name: + fnv1a-64: specifier: link:.. version: link:.. @@ -779,6 +797,10 @@ packages: resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} engines: {node: '>=18'} + '@sindresorhus/fnv1a@3.1.0': + resolution: {integrity: sha512-KV321z5m/0nuAg83W1dPLy85HpHDk7Sdi4fJbwvacWsEhAh+rZUW4ZfGcXmUIvjZg4ss2bcwNlRhJ7GBEUG08w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -940,126 +962,6 @@ packages: resolution: {integrity: sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/typescript-aix-ppc64@7.0.2': - resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} - engines: {node: '>=16.20.0'} - cpu: [ppc64] - os: [aix] - - '@typescript/typescript-darwin-arm64@7.0.2': - resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [darwin] - - '@typescript/typescript-darwin-x64@7.0.2': - resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [darwin] - - '@typescript/typescript-freebsd-arm64@7.0.2': - resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [freebsd] - - '@typescript/typescript-freebsd-x64@7.0.2': - resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [freebsd] - - '@typescript/typescript-linux-arm64@7.0.2': - resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [linux] - - '@typescript/typescript-linux-arm@7.0.2': - resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} - engines: {node: '>=16.20.0'} - cpu: [arm] - os: [linux] - - '@typescript/typescript-linux-loong64@7.0.2': - resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} - engines: {node: '>=16.20.0'} - cpu: [loong64] - os: [linux] - - '@typescript/typescript-linux-mips64el@7.0.2': - resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} - engines: {node: '>=16.20.0'} - cpu: [mips64el] - os: [linux] - - '@typescript/typescript-linux-ppc64@7.0.2': - resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} - engines: {node: '>=16.20.0'} - cpu: [ppc64] - os: [linux] - - '@typescript/typescript-linux-riscv64@7.0.2': - resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} - engines: {node: '>=16.20.0'} - cpu: [riscv64] - os: [linux] - - '@typescript/typescript-linux-s390x@7.0.2': - resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} - engines: {node: '>=16.20.0'} - cpu: [s390x] - os: [linux] - - '@typescript/typescript-linux-x64@7.0.2': - resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [linux] - - '@typescript/typescript-netbsd-arm64@7.0.2': - resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [netbsd] - - '@typescript/typescript-netbsd-x64@7.0.2': - resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [netbsd] - - '@typescript/typescript-openbsd-arm64@7.0.2': - resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [openbsd] - - '@typescript/typescript-openbsd-x64@7.0.2': - resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [openbsd] - - '@typescript/typescript-sunos-x64@7.0.2': - resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [sunos] - - '@typescript/typescript-win32-arm64@7.0.2': - resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [win32] - - '@typescript/typescript-win32-x64@7.0.2': - resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [win32] - '@vitest/coverage-v8@4.1.10': resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} peerDependencies: @@ -1298,6 +1200,9 @@ packages: engines: {node: '>=4'} hasBin: true + cuint@0.2.2: + resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1646,6 +1551,12 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + fnv-lite@1.2.0: + resolution: {integrity: sha512-bWOFDuEm1ffmavTjpE8ep+3i1f3X+zy5DcO8Um2rPyWNZn+vV6Pdo1tAnYhTISHxgtF6M8tG1QwWXh+llMUOjQ==} + + fnv-plus@1.3.1: + resolution: {integrity: sha512-Gz1EvfOneuFfk4yG458dJ3TLJ7gV19q3OM/vVvvHf7eT02Hm1DleB4edsia6ahbKgAYxO9gvyQ1ioWZR+a00Yw==} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -2090,6 +2001,9 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mitata@1.0.34: + resolution: {integrity: sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA==} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -2099,6 +2013,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + murmurhash@2.0.1: + resolution: {integrity: sha512-5vQEh3y+DG/lMPM0mCGPDnyV8chYg/g7rl6v3Gd8WMF9S429ox3Xk8qrk174kWhG767KQMqqxLD1WnGd77hiew==} + nano-staged@1.0.2: resolution: {integrity: sha512-Fytar3zHLY99nlMfqPPbraxZodqQAHPpdPRyYaplL+lB9DCR6pUrafxbG+Btz4+7fO5Rm/+DO4ZeDO/nLSUMhw==} engines: {node: ^22 || >= 24} @@ -2487,9 +2404,9 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - typescript@7.0.2: - resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} - engines: {node: '>=16.20.0'} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} hasBin: true ufo@1.6.4: @@ -2660,6 +2577,9 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xxhashjs@0.2.2: + resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==} + yaml-eslint-parser@2.0.0: resolution: {integrity: sha512-h0uDm97wvT2bokfwwTmY6kJ1hp6YDFL0nRHwNKz8s/VD1FH/vvZjAKoMUE+un0eaYBSG7/c6h+lJTP+31tjgTw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2681,7 +2601,7 @@ packages: snapshots: - '@antfu/eslint-config@9.1.0(@typescript-eslint/rule-tester@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(@typescript-eslint/typescript-estree@8.62.0(typescript@7.0.2))(@typescript-eslint/utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(@vue/compiler-sfc@3.5.35)(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)(vitest@4.1.10)': + '@antfu/eslint-config@9.1.0(@typescript-eslint/rule-tester@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(@typescript-eslint/typescript-estree@8.62.0(typescript@6.0.3))(@typescript-eslint/utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.35)(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.10)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 1.6.0 @@ -2689,9 +2609,9 @@ snapshots: '@eslint-community/eslint-plugin-eslint-comments': 4.7.2(eslint@10.7.0(jiti@2.7.0)) '@eslint/markdown': 8.0.2 '@stylistic/eslint-plugin': 5.10.0(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) - '@typescript-eslint/parser': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) - '@vitest/eslint-plugin': 1.6.20(@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)(vitest@4.1.10) + '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@vitest/eslint-plugin': 1.6.20(@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.10) ansis: 4.3.1 cac: 7.0.0 eslint: 10.7.0(jiti@2.7.0) @@ -2699,19 +2619,19 @@ snapshots: eslint-flat-config-utils: 3.2.0 eslint-merge-processors: 2.0.0(eslint@10.7.0(jiti@2.7.0)) eslint-plugin-antfu: 3.2.3(eslint@10.7.0(jiti@2.7.0)) - eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(@typescript-eslint/typescript-estree@8.62.0(typescript@7.0.2))(@typescript-eslint/utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0)) + eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(@typescript-eslint/typescript-estree@8.62.0(typescript@6.0.3))(@typescript-eslint/utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)) eslint-plugin-import-lite: 0.6.0(eslint@10.7.0(jiti@2.7.0)) eslint-plugin-jsdoc: 63.0.10(eslint@10.7.0(jiti@2.7.0)) eslint-plugin-jsonc: 3.2.0(eslint@10.7.0(jiti@2.7.0)) - eslint-plugin-n: 18.2.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + eslint-plugin-n: 18.2.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) eslint-plugin-no-only-tests: 3.4.0 - eslint-plugin-perfectionist: 5.9.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + eslint-plugin-perfectionist: 5.9.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) eslint-plugin-pnpm: 1.6.1(eslint@10.7.0(jiti@2.7.0)) eslint-plugin-regexp: 3.1.0(eslint@10.7.0(jiti@2.7.0)) eslint-plugin-toml: 1.4.0(eslint@10.7.0(jiti@2.7.0)) eslint-plugin-unicorn: 68.0.0(eslint@10.7.0(jiti@2.7.0)) - eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0)) - eslint-plugin-vue: 10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.7.0(jiti@2.7.0)))(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.7.0(jiti@2.7.0))) + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)) + eslint-plugin-vue: 10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.7.0(jiti@2.7.0)))(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.7.0(jiti@2.7.0))) eslint-plugin-yml: 3.5.0(eslint@10.7.0(jiti@2.7.0)) eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.35)(eslint@10.7.0(jiti@2.7.0)) globals: 17.7.0 @@ -3246,6 +3166,8 @@ snapshots: '@sindresorhus/base62@1.0.0': {} + '@sindresorhus/fnv1a@3.1.0': {} + '@standard-schema/spec@1.1.0': {} '@stylistic/eslint-plugin@5.10.0(eslint@10.7.0(jiti@2.7.0))': @@ -3298,75 +3220,75 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)': + '@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + '@typescript-eslint/parser': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.62.0 - '@typescript-eslint/type-utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) - '@typescript-eslint/utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + '@typescript-eslint/type-utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.62.0 eslint: 10.7.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@7.0.2) - typescript: 7.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)': + '@typescript-eslint/parser@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.60.1 '@typescript-eslint/types': 8.60.1 - '@typescript-eslint/typescript-estree': 8.60.1(typescript@7.0.2) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.60.1 debug: 4.4.3 eslint: 10.7.0(jiti@2.7.0) - typescript: 7.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)': + '@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.62.0 '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/typescript-estree': 8.62.0(typescript@7.0.2) + '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.62.0 debug: 4.4.3 eslint: 10.7.0(jiti@2.7.0) - typescript: 7.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.60.1(typescript@7.0.2)': + '@typescript-eslint/project-service@8.60.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@7.0.2) + '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 debug: 4.4.3 - typescript: 7.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.62.0(typescript@7.0.2)': + '@typescript-eslint/project-service@8.62.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@7.0.2) + '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 debug: 4.4.3 - typescript: 7.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/rule-tester@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)': + '@typescript-eslint/rule-tester@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/parser': 8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) - '@typescript-eslint/typescript-estree': 8.60.1(typescript@7.0.2) - '@typescript-eslint/utils': 8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + '@typescript-eslint/parser': 8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) ajv: 6.15.0 eslint: 10.7.0(jiti@2.7.0) json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 semver: 7.8.5 - typescript: 7.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3380,23 +3302,23 @@ snapshots: '@typescript-eslint/types': 8.62.0 '@typescript-eslint/visitor-keys': 8.62.0 - '@typescript-eslint/tsconfig-utils@8.60.1(typescript@7.0.2)': + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@6.0.3)': dependencies: - typescript: 7.0.2 + typescript: 6.0.3 - '@typescript-eslint/tsconfig-utils@8.62.0(typescript@7.0.2)': + '@typescript-eslint/tsconfig-utils@8.62.0(typescript@6.0.3)': dependencies: - typescript: 7.0.2 + typescript: 6.0.3 - '@typescript-eslint/type-utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)': + '@typescript-eslint/type-utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/typescript-estree': 8.62.0(typescript@7.0.2) - '@typescript-eslint/utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 eslint: 10.7.0(jiti@2.7.0) - ts-api-utils: 2.5.0(typescript@7.0.2) - typescript: 7.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3404,55 +3326,55 @@ snapshots: '@typescript-eslint/types@8.62.0': {} - '@typescript-eslint/typescript-estree@8.60.1(typescript@7.0.2)': + '@typescript-eslint/typescript-estree@8.60.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.60.1(typescript@7.0.2) - '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@7.0.2) + '@typescript-eslint/project-service': 8.60.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) '@typescript-eslint/types': 8.60.1 '@typescript-eslint/visitor-keys': 8.60.1 debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@7.0.2) - typescript: 7.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.62.0(typescript@7.0.2)': + '@typescript-eslint/typescript-estree@8.62.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.62.0(typescript@7.0.2) - '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@7.0.2) + '@typescript-eslint/project-service': 8.62.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 '@typescript-eslint/visitor-keys': 8.62.0 debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@7.0.2) - typescript: 7.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)': + '@typescript-eslint/utils@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.60.1 '@typescript-eslint/types': 8.60.1 - '@typescript-eslint/typescript-estree': 8.60.1(typescript@7.0.2) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) eslint: 10.7.0(jiti@2.7.0) - typescript: 7.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)': + '@typescript-eslint/utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.62.0 '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/typescript-estree': 8.62.0(typescript@7.0.2) + '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) eslint: 10.7.0(jiti@2.7.0) - typescript: 7.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3466,66 +3388,6 @@ snapshots: '@typescript-eslint/types': 8.62.0 eslint-visitor-keys: 5.0.1 - '@typescript/typescript-aix-ppc64@7.0.2': - optional: true - - '@typescript/typescript-darwin-arm64@7.0.2': - optional: true - - '@typescript/typescript-darwin-x64@7.0.2': - optional: true - - '@typescript/typescript-freebsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-freebsd-x64@7.0.2': - optional: true - - '@typescript/typescript-linux-arm64@7.0.2': - optional: true - - '@typescript/typescript-linux-arm@7.0.2': - optional: true - - '@typescript/typescript-linux-loong64@7.0.2': - optional: true - - '@typescript/typescript-linux-mips64el@7.0.2': - optional: true - - '@typescript/typescript-linux-ppc64@7.0.2': - optional: true - - '@typescript/typescript-linux-riscv64@7.0.2': - optional: true - - '@typescript/typescript-linux-s390x@7.0.2': - optional: true - - '@typescript/typescript-linux-x64@7.0.2': - optional: true - - '@typescript/typescript-netbsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-netbsd-x64@7.0.2': - optional: true - - '@typescript/typescript-openbsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-openbsd-x64@7.0.2': - optional: true - - '@typescript/typescript-sunos-x64@7.0.2': - optional: true - - '@typescript/typescript-win32-arm64@7.0.2': - optional: true - - '@typescript/typescript-win32-x64@7.0.2': - optional: true - '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -3540,14 +3402,14 @@ snapshots: tinyrainbow: 3.1.0 vitest: 4.1.10(@vitest/coverage-v8@4.1.10)(vite@8.0.16(jiti@2.7.0)(yaml@2.9.0)) - '@vitest/eslint-plugin@1.6.20(@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2)(vitest@4.1.10)': + '@vitest/eslint-plugin@1.6.20(@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.10)': dependencies: '@typescript-eslint/scope-manager': 8.62.0 - '@typescript-eslint/utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + '@typescript-eslint/utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.7.0(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) - typescript: 7.0.2 + '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + typescript: 6.0.3 vitest: 4.1.10(@vitest/coverage-v8@4.1.10)(vite@8.0.16(jiti@2.7.0)(yaml@2.9.0)) transitivePeerDependencies: - supports-color @@ -3754,6 +3616,8 @@ snapshots: cssesc@3.0.0: {} + cuint@0.2.2: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -3836,12 +3700,12 @@ snapshots: dependencies: eslint: 10.7.0(jiti@2.7.0) - eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(@typescript-eslint/typescript-estree@8.62.0(typescript@7.0.2))(@typescript-eslint/utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(@typescript-eslint/typescript-estree@8.62.0(typescript@6.0.3))(@typescript-eslint/utils@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)): dependencies: '@es-joy/jsdoccomment': 0.84.0 - '@typescript-eslint/rule-tester': 8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) - '@typescript-eslint/typescript-estree': 8.62.0(typescript@7.0.2) - '@typescript-eslint/utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + '@typescript-eslint/rule-tester': 8.60.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.7.0(jiti@2.7.0) eslint-plugin-es-x@7.8.0(eslint@10.7.0(jiti@2.7.0)): @@ -3890,7 +3754,7 @@ snapshots: transitivePeerDependencies: - '@eslint/json' - eslint-plugin-n@18.2.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2): + eslint-plugin-n@18.2.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) enhanced-resolve: 5.23.0 @@ -3902,13 +3766,13 @@ snapshots: ignore: 5.3.2 semver: 7.8.5 optionalDependencies: - typescript: 7.0.2 + typescript: 6.0.3 eslint-plugin-no-only-tests@3.4.0: {} - eslint-plugin-perfectionist@5.9.1(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2): + eslint-plugin-perfectionist@5.9.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + '@typescript-eslint/utils': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.7.0(jiti@2.7.0) natural-orderby: 5.0.0 transitivePeerDependencies: @@ -3968,13 +3832,13 @@ snapshots: semver: 7.8.5 strip-indent: 4.1.1 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)): dependencies: eslint: 10.7.0(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-vue@10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.7.0(jiti@2.7.0)))(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2))(eslint@10.7.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.7.0(jiti@2.7.0))): + eslint-plugin-vue@10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.7.0(jiti@2.7.0)))(@typescript-eslint/parser@8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.7.0(jiti@2.7.0))): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) eslint: 10.7.0(jiti@2.7.0) @@ -3986,7 +3850,7 @@ snapshots: xml-name-validator: 4.0.0 optionalDependencies: '@stylistic/eslint-plugin': 5.10.0(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/parser': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@7.0.2) + '@typescript-eslint/parser': 8.62.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) eslint-plugin-yml@3.5.0(eslint@10.7.0(jiti@2.7.0)): dependencies: @@ -4134,6 +3998,10 @@ snapshots: flatted@3.4.2: {} + fnv-lite@1.2.0: {} + + fnv-plus@1.3.1: {} + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -4744,6 +4612,8 @@ snapshots: minipass@7.1.3: {} + mitata@1.0.34: {} + mlly@1.8.2: dependencies: acorn: 8.16.0 @@ -4755,6 +4625,8 @@ snapshots: ms@2.1.3: {} + murmurhash@2.0.1: {} + nano-staged@1.0.2: {} nanoid@3.3.12: {} @@ -4962,7 +4834,7 @@ snapshots: resolve-workspace-root@2.0.1: {} - rolldown-plugin-dts@0.26.0(oxc-resolver@11.21.3)(rolldown@1.1.2)(typescript@7.0.2): + rolldown-plugin-dts@0.26.0(oxc-resolver@11.21.3)(rolldown@1.1.2)(typescript@6.0.3): dependencies: '@babel/generator': 8.0.0 '@babel/helper-validator-identifier': 8.0.2 @@ -4974,7 +4846,7 @@ snapshots: obug: 2.1.3 rolldown: 1.1.2 optionalDependencies: - typescript: 7.0.2 + typescript: 6.0.3 transitivePeerDependencies: - oxc-resolver @@ -5127,11 +4999,11 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@2.5.0(typescript@7.0.2): + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: - typescript: 7.0.2 + typescript: 6.0.3 - tsdown@0.22.3(oxc-resolver@11.21.3)(typescript@7.0.2): + tsdown@0.22.3(oxc-resolver@11.21.3)(typescript@6.0.3): dependencies: ansis: 4.3.1 cac: 7.0.0 @@ -5142,14 +5014,14 @@ snapshots: obug: 2.1.3 picomatch: 4.0.4 rolldown: 1.1.2 - rolldown-plugin-dts: 0.26.0(oxc-resolver@11.21.3)(rolldown@1.1.2)(typescript@7.0.2) + rolldown-plugin-dts: 0.26.0(oxc-resolver@11.21.3)(rolldown@1.1.2)(typescript@6.0.3) semver: 7.8.5 tinyexec: 1.2.4 tinyglobby: 0.2.17 tree-kill: 1.2.2 unconfig-core: 7.5.0 optionalDependencies: - typescript: 7.0.2 + typescript: 6.0.3 transitivePeerDependencies: - '@ts-macro/tsc' - '@typescript/native-preview' @@ -5165,28 +5037,7 @@ snapshots: type-fest@4.41.0: {} - typescript@7.0.2: - optionalDependencies: - '@typescript/typescript-aix-ppc64': 7.0.2 - '@typescript/typescript-darwin-arm64': 7.0.2 - '@typescript/typescript-darwin-x64': 7.0.2 - '@typescript/typescript-freebsd-arm64': 7.0.2 - '@typescript/typescript-freebsd-x64': 7.0.2 - '@typescript/typescript-linux-arm': 7.0.2 - '@typescript/typescript-linux-arm64': 7.0.2 - '@typescript/typescript-linux-loong64': 7.0.2 - '@typescript/typescript-linux-mips64el': 7.0.2 - '@typescript/typescript-linux-ppc64': 7.0.2 - '@typescript/typescript-linux-riscv64': 7.0.2 - '@typescript/typescript-linux-s390x': 7.0.2 - '@typescript/typescript-linux-x64': 7.0.2 - '@typescript/typescript-netbsd-arm64': 7.0.2 - '@typescript/typescript-netbsd-x64': 7.0.2 - '@typescript/typescript-openbsd-arm64': 7.0.2 - '@typescript/typescript-openbsd-x64': 7.0.2 - '@typescript/typescript-sunos-x64': 7.0.2 - '@typescript/typescript-win32-arm64': 7.0.2 - '@typescript/typescript-win32-x64': 7.0.2 + typescript@6.0.3: {} ufo@1.6.4: {} @@ -5320,6 +5171,10 @@ snapshots: xml-name-validator@4.0.0: {} + xxhashjs@0.2.2: + dependencies: + cuint: 0.2.2 + yaml-eslint-parser@2.0.0: dependencies: eslint-visitor-keys: 5.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5a8ce56..5f10408 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,7 @@ +shellEmulator: true + +trustPolicy: no-downgrade + packages: - playground @@ -6,4 +10,4 @@ allowBuilds: simple-git-hooks: true overrides: - package-name: "link:." + fnv1a-64: 'link:.' diff --git a/src/index.ts b/src/index.ts index 663a54a..2606a86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,83 @@ -export const welcome = () => 'hello world' +/** + * The two 32-bit lanes of a 64-bit FNV-1a hash. + * + * `high` is the most-significant 32 bits, `low` the least-significant. Both are + * unsigned integers in the range `0` to `2^32 - 1`. + */ +export interface Fnv1a64Lanes { + high: number + low: number +} + +/** + * Compute the 64-bit FNV-1a hash of a string as two 32-bit lanes. + * + * This is the fast core: no BigInt, no allocations, plain `Math.imul`-free + * 32-bit arithmetic. Prefer {@link fnv1a64Hex} or {@link fnv1a64Base36} for a + * usable key; use this directly only when you want to avoid string formatting. + * + * The hash is computed over UTF-16 code units (`str.charCodeAt(i)`), not UTF-8 + * bytes. For ASCII input this matches a canonical FNV-1a-64; for non-ASCII it + * does not. See the README for details. + * + * @param str - The string to hash. + * @returns The `{ high, low }` 32-bit lanes of the 64-bit hash. + */ +export function fnv1a64(str: string): Fnv1a64Lanes { + let low = 0x84222325 + let high = 0xCBF29CE4 + for (let i = 0; i < str.length; i++) { + low ^= str.charCodeAt(i) + const lowByLow = (low & 0xFFFF) * 0x1B3 + const highOfLow = (low >>> 16) * 0x1B3 + const highByHigh = (high & 0xFFFF) * 0x1B3 + ((high >>> 16) * 0x1B3 << 16) + const carry = (lowByLow >>> 16) + highOfLow + // The 64-bit prime 0x100000001B3's 2^40 bit folds `low * 2^8` into the high lane. + high = (highByHigh + (carry >>> 16) + low * 0x100) >>> 0 + low = ((lowByLow & 0xFFFF) | ((carry & 0xFFFF) << 16)) >>> 0 + } + return { high: high >>> 0, low: low >>> 0 } +} + +/** + * Compute the 64-bit FNV-1a hash of a string as a `bigint`. + * + * Ergonomic and comparable, at the cost of composing the two lanes into a + * `bigint`. For a compact string key, prefer {@link fnv1a64Base36}. + * + * @param str - The string to hash. + * @returns The 64-bit hash as an unsigned `bigint`. + */ +export function fnv1a64BigInt(str: string): bigint { + const { high, low } = fnv1a64(str) + return (BigInt(high) << 32n) | BigInt(low) +} + +/** + * Compute the 64-bit FNV-1a hash of a string as a 16-character zero-padded + * lowercase hex string. + * + * The output is always exactly 16 characters, so equal-length comparison and + * fixed-width storage are safe. + * + * @param str - The string to hash. + * @returns A 16-character hex string. + */ +export function fnv1a64Hex(str: string): string { + const { high, low } = fnv1a64(str) + return high.toString(16).padStart(8, '0') + low.toString(16).padStart(8, '0') +} + +/** + * Compute the 64-bit FNV-1a hash of a string as a base36 string. + * + * This is the shortest textual form (up to 13 characters) and is ideal for + * cache keys. The length varies with the value; it is not zero-padded. Equal + * inputs always produce identical strings. + * + * @param str - The string to hash. + * @returns A base36 string of the 64-bit hash. + */ +export function fnv1a64Base36(str: string): string { + return fnv1a64BigInt(str).toString(36) +} diff --git a/test/index.test.ts b/test/index.test.ts index c50d9f5..d873b2a 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,8 +1,115 @@ import { describe, expect, it } from 'vitest' -import { welcome } from '../src' +import { fnv1a64, fnv1a64Base36, fnv1a64BigInt, fnv1a64Hex } from '../src' -describe('package-name', () => { - it('works', () => { - expect(welcome()).toMatchInlineSnapshot('"hello world"') +function reference(str: string): { high: number, low: number, value: bigint } { + let h = 0xCBF29CE484222325n + const mask = (1n << 64n) - 1n + for (let i = 0; i < str.length; i++) { + h ^= BigInt(str.charCodeAt(i)) + h = (h * 0x100000001B3n) & mask + } + return { high: Number(h >> 32n), low: Number(h & 0xFFFFFFFFn), value: h } +} + +const cases = ['', 'a', 'abc', 'hello world', 'x'.repeat(200)] + +describe('fnv1a64', () => { + it('matches a bigint reference bit-for-bit', () => { + for (const input of cases) { + const ref = reference(input) + const { high, low } = fnv1a64(input) + expect({ high, low }, `lanes for ${JSON.stringify(input)}`).toEqual({ + high: ref.high, + low: ref.low, + }) + } + }) + + it('returns the offset basis for the empty string', () => { + expect(fnv1a64('')).toEqual({ high: 0xCBF29CE4, low: 0x84222325 }) + }) + + it('returns unsigned lanes', () => { + for (const input of cases) { + const { high, low } = fnv1a64(input) + expect(high).toBeGreaterThanOrEqual(0) + expect(low).toBeGreaterThanOrEqual(0) + expect(high).toBeLessThanOrEqual(0xFFFFFFFF) + expect(low).toBeLessThanOrEqual(0xFFFFFFFF) + } + }) + + it('is stable across calls', () => { + for (const input of cases) { + expect(fnv1a64(input)).toEqual(fnv1a64(input)) + } + }) +}) + +describe('fnv1a64BigInt', () => { + it('matches a bigint reference', () => { + for (const input of cases) { + expect(fnv1a64BigInt(input)).toBe(reference(input).value) + } + }) +}) + +describe('fnv1a64Hex', () => { + it('is 16 zero-padded characters', () => { + for (const input of cases) { + const hex = fnv1a64Hex(input) + expect(hex).toHaveLength(16) + expect(hex).toMatch(/^[0-9a-f]{16}$/) + expect(BigInt(`0x${hex}`)).toBe(reference(input).value) + } + }) + + it('matches known snapshots', () => { + expect(fnv1a64Hex('')).toBe('cbf29ce484222325') + expect(fnv1a64Hex('a')).toBe('af63dc4c8601ec8c') + expect(fnv1a64Hex('hello world')).toBe('779a65e7023cd2e7') + }) +}) + +describe('fnv1a64Base36', () => { + it('round-trips to the bigint value', () => { + for (const input of cases) { + expect(parseBase36(fnv1a64Base36(input))).toBe(reference(input).value) + } + }) + + it('is stable and deterministic', () => { + expect(fnv1a64Base36('abc')).toBe(fnv1a64Base36('abc')) }) }) + +describe('non-ascii behaviour', () => { + it('hashes utf-16 code units, not utf-8 bytes', () => { + for (const input of ['café', '日本語', '\u{1F600}emoji']) { + const ref = reference(input) + expect(fnv1a64(input)).toEqual({ high: ref.high, low: ref.low }) + } + }) +}) + +describe('collision smoke test', () => { + it('has no collisions across 50k realistic keys', () => { + const seen = new Set() + const prefixes = ['user', 'session', 'cache', 'route', 'asset', 'chunk', 'node', 'query'] + let count = 0 + for (let i = 0; i < 50_000; i++) { + const key = `${prefixes[i % prefixes.length]}:${i}:${(i * 2654435761 >>> 0).toString(36)}` + seen.add(fnv1a64Hex(key)) + count++ + } + expect(seen.size).toBe(count) + }) +}) + +function parseBase36(value: string): bigint { + let result = 0n + for (const char of value) { + result = result * 36n + BigInt(Number.parseInt(char, 36)) + } + return result +} diff --git a/tsconfig.json b/tsconfig.json index c8cef40..74f0ab4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,9 +18,7 @@ "verbatimModuleSyntax": true, "skipLibCheck": true }, - "include": [ - "src", - "test", - "playground" + "exclude": [ + "dist" ] } diff --git a/vitest.config.ts b/vitest.config.ts index e20afee..f4bfbb7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ resolve: { alias: { - 'package-name': fileURLToPath( + 'fnv1a-64': fileURLToPath( new URL('./src/index.ts', import.meta.url).href, ), },