diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c7187e9..ec37616 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,9 @@ jobs: - run: pnpm i + - name: Lint + run: pnpm lint + - name: Typecheck run: pnpm typecheck diff --git a/.oxfmtrc.json b/.oxfmtrc.json deleted file mode 100644 index 450fa02..0000000 --- a/.oxfmtrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://unpkg.com/oxfmt/configuration_schema.json", - "ignorePatterns": [] -} diff --git a/.oxlintrc.json b/.oxlintrc.json deleted file mode 100644 index e1c16f5..0000000 --- a/.oxlintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://unpkg.com/oxlint/configuration_schema.json", - "plugins": ["unicorn", "typescript", "oxc"], - "rules": {}, - "ignorePatterns": [] -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1252877..a06a896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,11 @@ ### ๐Ÿ’… Refactors -- โš ๏ธ Replace registry factories with classes ([#59](https://github.com/agntn/registries/pull/59)) +- โš ๏ธ Replace registry factories with classes ([#59](https://github.com/agntn/registries/pull/59)) ### ๐Ÿก Chore -- โš ๏ธ Rebrand as @agntn/registries ([#60](https://github.com/agntn/registries/pull/60)) +- โš ๏ธ Rebrand as @agntn/registries ([#60](https://github.com/agntn/registries/pull/60)) - Update packagerManager version ([8719073](https://github.com/agntn/registries/commit/8719073)) - Update version ([43b02af](https://github.com/agntn/registries/commit/43b02af)) @@ -30,8 +30,8 @@ #### โš ๏ธ Breaking Changes -- โš ๏ธ Replace registry factories with classes ([#59](https://github.com/agntn/registries/pull/59)) -- โš ๏ธ Rebrand as @agntn/registries ([#60](https://github.com/agntn/registries/pull/60)) +- โš ๏ธ Replace registry factories with classes ([#59](https://github.com/agntn/registries/pull/59)) +- โš ๏ธ Rebrand as @agntn/registries ([#60](https://github.com/agntn/registries/pull/60)) ### โค๏ธ Contributors diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 0000000..9ee93b3 --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,7 @@ +import oxfmt from "@agntn/ox/oxfmt"; +import { defineConfig } from "oxfmt"; + +export default defineConfig({ + ...oxfmt, + ignorePatterns: ["dist"], +}); diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 0000000..8716cb5 --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,49 @@ +import oxlint from "@agntn/ox/oxlint"; +import { defineConfig } from "oxlint"; + +export default defineConfig({ + ...oxlint, + rules: { + ...oxlint.rules, + "typescript/prefer-readonly-parameter-types": [ + "error", + { + allow: [ + { from: "file", name: "ToolResult" }, + { + from: "package", + name: "ExtensionAPI", + package: "@earendil-works/pi-coding-agent", + }, + { + from: "package", + name: ["ExtensionAPI", "ToolDefinition"], + package: "@oh-my-pi/pi-coding-agent", + }, + { from: "lib", name: ["AbortSignal", "ErrorOptions"] }, + { from: "file", name: "Client", path: "./src/core/client.ts" }, + { + from: "file", + name: [ + "ClientOptions", + "CreateCachedOptions", + "Dependency", + "Lockfile", + "LockfileEntry", + "Maintainer", + "Package", + "ParsedPURL", + "RateLimiter", + "Registry", + "URLBuilder", + "Version", + ], + }, + { from: "package", name: "Storage", package: "unstorage" }, + ], + ignoreInferredTypes: true, + }, + ], + }, + ignorePatterns: ["dist"], +}); diff --git a/package.json b/package.json index 63ec1c5..a4d2e30 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "dev:prepare": "obuild --stub", "fmt": "oxlint . --fix && oxfmt .", "lint": "oxlint . && oxfmt --check .", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && tsc -p test/tsconfig.json --noEmit", "test": "vitest", "test:run": "vitest run", "prepack": "pnpm run build", @@ -75,12 +75,14 @@ "unstorage": "^1.17.4" }, "devDependencies": { + "@agntn/ox": "^0.1.0", "@types/node": "^25.3.0", "ai": "^6.0.116", "changelogen": "^0.6.2", "obuild": "^0.4.31", - "oxfmt": "^0.36.0", - "oxlint": "^1.51.0", + "oxfmt": "^0.65.0", + "oxlint": "^1.80.0", + "oxlint-tsgolint": "^7.0.2001", "typescript": "^5.8.3", "vitest": "^4.0.0", "zod": "^4.3.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b512f8d..fe9262b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: specifier: ^1.17.4 version: 1.17.4 devDependencies: + '@agntn/ox': + specifier: ^0.1.0 + version: 0.1.0(oxfmt@0.65.0)(oxlint-tsgolint@7.0.2001)(oxlint@1.80.0(oxlint-tsgolint@7.0.2001)) '@types/node': specifier: ^25.3.0 version: 25.3.0 @@ -34,11 +37,14 @@ importers: specifier: ^0.4.31 version: 0.4.31(chokidar@5.0.0)(dotenv@17.3.1)(giget@2.0.0)(jiti@2.6.1)(picomatch@4.0.3)(rollup@4.59.0)(typescript@5.9.3) oxfmt: - specifier: ^0.36.0 - version: 0.36.0 + specifier: ^0.65.0 + version: 0.65.0 oxlint: - specifier: ^1.51.0 - version: 1.51.0 + specifier: ^1.80.0 + version: 1.80.0(oxlint-tsgolint@7.0.2001) + oxlint-tsgolint: + specifier: ^7.0.2001 + version: 7.0.2001 typescript: specifier: ^5.8.3 version: 5.9.3 @@ -51,6 +57,14 @@ importers: packages: + '@agntn/ox@0.1.0': + resolution: {integrity: sha512-bYyCMczypvi/bxDgK1gOFhOnGFAqTNnTTWl+yIocaP68wsfGtwZwxN9Rld1bMcLXRVlO9PJCl7EASqToz8dOVw==} + engines: {node: '>=24'} + peerDependencies: + oxfmt: '>=0.63.0' + oxlint: '>=1.78.0' + oxlint-tsgolint: '>=7.0.2001' + '@ai-sdk/gateway@3.0.66': resolution: {integrity: sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A==} engines: {node: '>=18'} @@ -276,246 +290,276 @@ packages: '@oxc-project/types@0.114.0': resolution: {integrity: sha512-//nBfbzHQHvJs8oFIjv6coZ6uxQ4alLfiPe6D5vit6c4pmxATHHlVwgB1k+Hv4yoAMyncdxgRBF5K4BYWUCzvA==} - '@oxfmt/binding-android-arm-eabi@0.36.0': - resolution: {integrity: sha512-Z4yVHJWx/swHHjtr0dXrBZb6LxS+qNz1qdza222mWwPTUK4L790+5i3LTgjx3KYGBzcYpjaiZBw4vOx94dH7MQ==} + '@oxfmt/binding-android-arm-eabi@0.65.0': + resolution: {integrity: sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.36.0': - resolution: {integrity: sha512-3ElCJRFNPQl7jexf2CAa9XmAm8eC5JPrIDSjc9jSchkVSFTEqyL0NtZinBB2h1a4i4JgP1oGl/5G5n8YR4FN8Q==} + '@oxfmt/binding-android-arm64@0.65.0': + resolution: {integrity: sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.36.0': - resolution: {integrity: sha512-nak4znWCqIExKhYSY/mz/lWsqWIpdsS7o0+SRzXR1Q0m7GrMcG1UrF1pS7TLGZhhkf7nTfEF7q6oZzJiodRDuw==} + '@oxfmt/binding-darwin-arm64@0.65.0': + resolution: {integrity: sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.36.0': - resolution: {integrity: sha512-V4GP96thDnpKx6ADnMDnhIXNdtV+Ql9D4HUU+a37VTeVbs5qQSF/s6hhUP1b3xUqU7iRcwh72jUU2Y12rtGHAw==} + '@oxfmt/binding-darwin-x64@0.65.0': + resolution: {integrity: sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.36.0': - resolution: {integrity: sha512-/xapWCADfI5wrhxpEUjhI9fnw7MV5BUZizVa8e24n3VSK6A3Y1TB/ClOP1tfxNspykFKXp4NBWl6NtDJP3osqQ==} + '@oxfmt/binding-freebsd-x64@0.65.0': + resolution: {integrity: sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.36.0': - resolution: {integrity: sha512-1lOmv61XMFIH5uNm27620kRRzWt/RK6tdn250BRDoG9W7OXGOQ5UyI1HVT+SFkoOoKztBiinWgi68+NA1MjBVQ==} + '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': + resolution: {integrity: sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.36.0': - resolution: {integrity: sha512-vMH23AskdR1ujUS9sPck2Df9rBVoZUnCVY86jisILzIQ/QQ/yKUTi7tgnIvydPx7TyB/48wsQ5QMr5Knq5p/aw==} + '@oxfmt/binding-linux-arm-musleabihf@0.65.0': + resolution: {integrity: sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.36.0': - resolution: {integrity: sha512-Hy1V+zOBHpBiENRx77qrUTt5aPDHeCASRc8K5KwwAHkX2AKP0nV89eL17hsZrE9GmnXFjsNmd80lyf7aRTXsbw==} + '@oxfmt/binding-linux-arm64-gnu@0.65.0': + resolution: {integrity: sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.36.0': - resolution: {integrity: sha512-SPGLJkOIHSIC6ABUQ5V8NqJpvYhMJueJv26NYqfCnwi/Mn6A61amkpJJ9Suy0Nmvs+OWESJpcebrBUbXPGZyQQ==} + '@oxfmt/binding-linux-arm64-musl@0.65.0': + resolution: {integrity: sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.36.0': - resolution: {integrity: sha512-3EuoyB8x9x8ysYJjbEO/M9fkSk72zQKnXCvpZMDHXlnY36/1qMp55Nm0PrCwjGO/1pen5hdOVkz9WmP3nAp2IQ==} + '@oxfmt/binding-linux-ppc64-gnu@0.65.0': + resolution: {integrity: sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.36.0': - resolution: {integrity: sha512-MpY3itLwpGh8dnywtrZtaZ604T1m715SydCKy0+qTxetv+IHzuA+aO/AGzrlzUNYZZmtWtmDBrChZGibvZxbRQ==} + '@oxfmt/binding-linux-riscv64-gnu@0.65.0': + resolution: {integrity: sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.36.0': - resolution: {integrity: sha512-mmDhe4Vtx+XwQPRPn/V25+APnkApYgZ23q+6GVsNYY98pf3aU0aI3Me96pbRs/AfJ1jIiGC+/6q71FEu8dHcHw==} + '@oxfmt/binding-linux-riscv64-musl@0.65.0': + resolution: {integrity: sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.36.0': - resolution: {integrity: sha512-AYXhU+DmNWLSnvVwkHM92fuYhogtVHab7UQrPNaDf1sxadugg9gWVmcgJDlIwxJdpk5CVW/TFvwUKwI432zhhA==} + '@oxfmt/binding-linux-s390x-gnu@0.65.0': + resolution: {integrity: sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.36.0': - resolution: {integrity: sha512-H16QhhQ3usoakMleiAAQ2mg0NsBDAdyE9agUgfC8IHHh3jZEbr0rIKwjEqwbOHK5M0EmfhJmr+aGO/MgZPsneA==} + '@oxfmt/binding-linux-x64-gnu@0.65.0': + resolution: {integrity: sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.36.0': - resolution: {integrity: sha512-EFFGkixA39BcmHiCe2ECdrq02D6FCve5ka6ObbvrheXl4V+R0U/E+/uLyVx1X65LW8TA8QQHdnbdDallRekohw==} + '@oxfmt/binding-linux-x64-musl@0.65.0': + resolution: {integrity: sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.36.0': - resolution: {integrity: sha512-zr/t369wZWFOj1qf06Z5gGNjFymfUNDrxKMmr7FKiDRVI1sNsdKRCuRL4XVjtcptKQ+ao3FfxLN1vrynivmCYg==} + '@oxfmt/binding-openharmony-arm64@0.65.0': + resolution: {integrity: sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.36.0': - resolution: {integrity: sha512-FxO7UksTv8h4olzACgrqAXNF6BP329+H322323iDrMB5V/+a1kcAw07fsOsUmqNrb9iJBsCQgH/zqcqp5903ag==} + '@oxfmt/binding-win32-arm64-msvc@0.65.0': + resolution: {integrity: sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.36.0': - resolution: {integrity: sha512-OjoMQ89H01M0oLMfr/CPNH1zi48ZIwxAKObUl57oh7ssUBNDp/2Vjf7E1TQ8M4oj4VFQ/byxl2SmcPNaI2YNDg==} + '@oxfmt/binding-win32-ia32-msvc@0.65.0': + resolution: {integrity: sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.36.0': - resolution: {integrity: sha512-MoyeQ9S36ZTz/4bDhOKJgOBIDROd4dQ5AkT9iezhEaUBxAPdNX9Oq0jD8OSnCj3G4wam/XNxVWKMA52kmzmPtQ==} + '@oxfmt/binding-win32-x64-msvc@0.65.0': + resolution: {integrity: sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.51.0': - resolution: {integrity: sha512-jJYIqbx4sX+suIxWstc4P7SzhEwb4ArWA2KVrmEuu9vH2i0qM6QIHz/ehmbGE4/2fZbpuMuBzTl7UkfNoqiSgw==} + '@oxlint-tsgolint/darwin-arm64@7.0.2001': + resolution: {integrity: sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@7.0.2001': + resolution: {integrity: sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@7.0.2001': + resolution: {integrity: sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@7.0.2001': + resolution: {integrity: sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@7.0.2001': + resolution: {integrity: sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@7.0.2001': + resolution: {integrity: sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.80.0': + resolution: {integrity: sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.51.0': - resolution: {integrity: sha512-GtXyBCcH4ti98YdiMNCrpBNGitx87EjEWxevnyhcBK12k/Vu4EzSB45rzSC4fGFUD6sQgeaxItRCEEWeVwPafw==} + '@oxlint/binding-android-arm64@1.80.0': + resolution: {integrity: sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.51.0': - resolution: {integrity: sha512-3QJbeYaMHn6Bh2XeBXuITSsbnIctyTjvHf5nRjKYrT9pPeErNIpp5VDEeAXC0CZSwSVTsc8WOSDwgrAI24JolQ==} + '@oxlint/binding-darwin-arm64@1.80.0': + resolution: {integrity: sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.51.0': - resolution: {integrity: sha512-NzErhMaTEN1cY0E8C5APy74lw5VwsNfJfVPBMWPVQLqAbO0k4FFLjvHURvkUL+Y18Wu+8Vs1kbqPh2hjXYA4pg==} + '@oxlint/binding-darwin-x64@1.80.0': + resolution: {integrity: sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.51.0': - resolution: {integrity: sha512-msAIh3vPAoKoHlOE/oe6Q5C/n9umypv/k81lED82ibrJotn+3YG2Qp1kiR8o/Dg5iOEU97c6tl0utxcyFenpFw==} + '@oxlint/binding-freebsd-x64@1.80.0': + resolution: {integrity: sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.51.0': - resolution: {integrity: sha512-CqQPcvqYyMe9ZBot2stjGogEzk1z8gGAngIX7srSzrzexmXixwVxBdFZyxTVM0CjGfDeV+Ru0w25/WNjlMM2Hw==} + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + resolution: {integrity: sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.51.0': - resolution: {integrity: sha512-dstrlYQgZMnyOssxSbolGCge/sDbko12N/35RBNuqLpoPbft2aeBidBAb0dvQlyBd9RJ6u8D4o4Eh8Un6iTgyQ==} + '@oxlint/binding-linux-arm-musleabihf@1.80.0': + resolution: {integrity: sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.51.0': - resolution: {integrity: sha512-QEjUpXO7d35rP1/raLGGbAsBLLGZIzV3ZbeSjqWlD3oRnxpRIZ6iL4o51XQHkconn3uKssc+1VKdtHJ81BBhDA==} + '@oxlint/binding-linux-arm64-gnu@1.80.0': + resolution: {integrity: sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.51.0': - resolution: {integrity: sha512-YSJua5irtG4DoMAjUapDTPhkQLHhBIY0G9JqlZS6/SZPzqDkPku/1GdWs0D6h/wyx0Iz31lNCfIaWKBQhzP0wQ==} + '@oxlint/binding-linux-arm64-musl@1.80.0': + resolution: {integrity: sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.51.0': - resolution: {integrity: sha512-7L4Wj2IEUNDETKssB9IDYt16T6WlF+X2jgC/hBq3diGHda9vJLpAgb09+D3quFq7TdkFtI7hwz/jmuQmQFPc1Q==} + '@oxlint/binding-linux-ppc64-gnu@1.80.0': + resolution: {integrity: sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.51.0': - resolution: {integrity: sha512-cBUHqtOXy76G41lOB401qpFoKx1xq17qYkhWrLSM7eEjiHM9sOtYqpr6ZdqCnN9s6ZpzudX4EkeHOFH2E9q0vA==} + '@oxlint/binding-linux-riscv64-gnu@1.80.0': + resolution: {integrity: sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.51.0': - resolution: {integrity: sha512-WKbg8CysgZcHfZX0ixQFBRSBvFZUHa3SBnEjHY2FVYt2nbNJEjzTxA3ZR5wMU0NOCNKIAFUFvAh5/XJKPRJuJg==} + '@oxlint/binding-linux-riscv64-musl@1.80.0': + resolution: {integrity: sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.51.0': - resolution: {integrity: sha512-N1QRUvJTxqXNSu35YOufdjsAVmKVx5bkrggOWAhTWBc3J4qjcBwr1IfyLh/6YCg8sYRSR1GraldS9jUgJL/U4A==} + '@oxlint/binding-linux-s390x-gnu@1.80.0': + resolution: {integrity: sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.51.0': - resolution: {integrity: sha512-e0Mz0DizsCoqNIjeOg6OUKe8JKJWZ5zZlwsd05Bmr51Jo3AOL4UJnPvwKumr4BBtBrDZkCmOLhCvDGm95nJM2g==} + '@oxlint/binding-linux-x64-gnu@1.80.0': + resolution: {integrity: sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.51.0': - resolution: {integrity: sha512-wD8HGTWhYBKXvRDvoBVB1y+fEYV01samhWQSy1Zkxq2vpezvMnjaFKRuiP6tBNITLGuffbNDEXOwcAhJ3gI5Ug==} + '@oxlint/binding-linux-x64-musl@1.80.0': + resolution: {integrity: sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.51.0': - resolution: {integrity: sha512-5NSwQ2hDEJ0GPXqikjWtwzgAQCsS7P9aLMNenjjKa+gknN3lTCwwwERsT6lKXSirfU3jLjexA2XQvQALh5h27w==} + '@oxlint/binding-openharmony-arm64@1.80.0': + resolution: {integrity: sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.51.0': - resolution: {integrity: sha512-JEZyah1M0RHMw8d+jjSSJmSmO8sABA1J1RtrHYujGPeCkYg1NeH0TGuClpe2h5QtioRTaF57y/TZfn/2IFV6fA==} + '@oxlint/binding-win32-arm64-msvc@1.80.0': + resolution: {integrity: sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.51.0': - resolution: {integrity: sha512-q3cEoKH6kwjz/WRyHwSf0nlD2F5Qw536kCXvmlSu+kaShzgrA0ojmh45CA81qL+7udfCaZL2SdKCZlLiGBVFlg==} + '@oxlint/binding-win32-ia32-msvc@1.80.0': + resolution: {integrity: sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.51.0': - resolution: {integrity: sha512-Q14+fOGb9T28nWF/0EUsYqERiRA7cl1oy4TJrGmLaqhm+aO2cV+JttboHI3CbdeMCAyDI1+NoSlrM7Melhp/cw==} + '@oxlint/binding-win32-x64-msvc@1.80.0': + resolution: {integrity: sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1050,20 +1094,35 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} - oxfmt@0.36.0: - resolution: {integrity: sha512-/ejJ+KoSW6J9bcNT9a9UtJSJNWhJ3yOLSBLbkoFHJs/8CZjmaZVZAJe4YgO1KMJlKpNQasrn/G9JQUEZI3p0EQ==} + oxfmt@0.65.0: + resolution: {integrity: sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true - oxlint@1.51.0: - resolution: {integrity: sha512-g6DNPaV9/WI9MoX2XllafxQuxwY1TV++j7hP8fTJByVBuCoVtm3dy9f/2vtH/HU40JztcgWF4G7ua+gkainklQ==} + oxlint-tsgolint@7.0.2001: + resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==} + hasBin: true + + oxlint@1.80.0: + resolution: {integrity: sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.15.0' + oxlint-tsgolint: '>=7.0.2001' + vite-plus: '*' peerDependenciesMeta: oxlint-tsgolint: optional: true + vite-plus: + optional: true package-name-regex@2.0.6: resolution: {integrity: sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA==} @@ -1380,6 +1439,12 @@ packages: snapshots: + '@agntn/ox@0.1.0(oxfmt@0.65.0)(oxlint-tsgolint@7.0.2001)(oxlint@1.80.0(oxlint-tsgolint@7.0.2001))': + dependencies: + oxfmt: 0.65.0 + oxlint: 1.80.0(oxlint-tsgolint@7.0.2001) + oxlint-tsgolint: 7.0.2001 + '@ai-sdk/gateway@3.0.66(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -1539,118 +1604,136 @@ snapshots: '@oxc-project/types@0.114.0': {} - '@oxfmt/binding-android-arm-eabi@0.36.0': + '@oxfmt/binding-android-arm-eabi@0.65.0': optional: true - '@oxfmt/binding-android-arm64@0.36.0': + '@oxfmt/binding-android-arm64@0.65.0': optional: true - '@oxfmt/binding-darwin-arm64@0.36.0': + '@oxfmt/binding-darwin-arm64@0.65.0': optional: true - '@oxfmt/binding-darwin-x64@0.36.0': + '@oxfmt/binding-darwin-x64@0.65.0': optional: true - '@oxfmt/binding-freebsd-x64@0.36.0': + '@oxfmt/binding-freebsd-x64@0.65.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.36.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.36.0': + '@oxfmt/binding-linux-arm-musleabihf@0.65.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.36.0': + '@oxfmt/binding-linux-arm64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.36.0': + '@oxfmt/binding-linux-arm64-musl@0.65.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.36.0': + '@oxfmt/binding-linux-ppc64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.36.0': + '@oxfmt/binding-linux-riscv64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.36.0': + '@oxfmt/binding-linux-riscv64-musl@0.65.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.36.0': + '@oxfmt/binding-linux-s390x-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.36.0': + '@oxfmt/binding-linux-x64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.36.0': + '@oxfmt/binding-linux-x64-musl@0.65.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.36.0': + '@oxfmt/binding-openharmony-arm64@0.65.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.36.0': + '@oxfmt/binding-win32-arm64-msvc@0.65.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.36.0': + '@oxfmt/binding-win32-ia32-msvc@0.65.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.36.0': + '@oxfmt/binding-win32-x64-msvc@0.65.0': optional: true - '@oxlint/binding-android-arm-eabi@1.51.0': + '@oxlint-tsgolint/darwin-arm64@7.0.2001': optional: true - '@oxlint/binding-android-arm64@1.51.0': + '@oxlint-tsgolint/darwin-x64@7.0.2001': optional: true - '@oxlint/binding-darwin-arm64@1.51.0': + '@oxlint-tsgolint/linux-arm64@7.0.2001': optional: true - '@oxlint/binding-darwin-x64@1.51.0': + '@oxlint-tsgolint/linux-x64@7.0.2001': optional: true - '@oxlint/binding-freebsd-x64@1.51.0': + '@oxlint-tsgolint/win32-arm64@7.0.2001': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.51.0': + '@oxlint-tsgolint/win32-x64@7.0.2001': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.51.0': + '@oxlint/binding-android-arm-eabi@1.80.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.51.0': + '@oxlint/binding-android-arm64@1.80.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.51.0': + '@oxlint/binding-darwin-arm64@1.80.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.51.0': + '@oxlint/binding-darwin-x64@1.80.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.51.0': + '@oxlint/binding-freebsd-x64@1.80.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.51.0': + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.51.0': + '@oxlint/binding-linux-arm-musleabihf@1.80.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.51.0': + '@oxlint/binding-linux-arm64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-x64-musl@1.51.0': + '@oxlint/binding-linux-arm64-musl@1.80.0': optional: true - '@oxlint/binding-openharmony-arm64@1.51.0': + '@oxlint/binding-linux-ppc64-gnu@1.80.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.51.0': + '@oxlint/binding-linux-riscv64-gnu@1.80.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.51.0': + '@oxlint/binding-linux-riscv64-musl@1.80.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.51.0': + '@oxlint/binding-linux-s390x-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.80.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.80.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.80.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.80.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.80.0': optional: true '@rolldown/binding-android-arm64@1.0.0-rc.5': @@ -2114,51 +2197,61 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 - oxfmt@0.36.0: + oxfmt@0.65.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.36.0 - '@oxfmt/binding-android-arm64': 0.36.0 - '@oxfmt/binding-darwin-arm64': 0.36.0 - '@oxfmt/binding-darwin-x64': 0.36.0 - '@oxfmt/binding-freebsd-x64': 0.36.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.36.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.36.0 - '@oxfmt/binding-linux-arm64-gnu': 0.36.0 - '@oxfmt/binding-linux-arm64-musl': 0.36.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.36.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.36.0 - '@oxfmt/binding-linux-riscv64-musl': 0.36.0 - '@oxfmt/binding-linux-s390x-gnu': 0.36.0 - '@oxfmt/binding-linux-x64-gnu': 0.36.0 - '@oxfmt/binding-linux-x64-musl': 0.36.0 - '@oxfmt/binding-openharmony-arm64': 0.36.0 - '@oxfmt/binding-win32-arm64-msvc': 0.36.0 - '@oxfmt/binding-win32-ia32-msvc': 0.36.0 - '@oxfmt/binding-win32-x64-msvc': 0.36.0 - - oxlint@1.51.0: + '@oxfmt/binding-android-arm-eabi': 0.65.0 + '@oxfmt/binding-android-arm64': 0.65.0 + '@oxfmt/binding-darwin-arm64': 0.65.0 + '@oxfmt/binding-darwin-x64': 0.65.0 + '@oxfmt/binding-freebsd-x64': 0.65.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.65.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.65.0 + '@oxfmt/binding-linux-arm64-gnu': 0.65.0 + '@oxfmt/binding-linux-arm64-musl': 0.65.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.65.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.65.0 + '@oxfmt/binding-linux-riscv64-musl': 0.65.0 + '@oxfmt/binding-linux-s390x-gnu': 0.65.0 + '@oxfmt/binding-linux-x64-gnu': 0.65.0 + '@oxfmt/binding-linux-x64-musl': 0.65.0 + '@oxfmt/binding-openharmony-arm64': 0.65.0 + '@oxfmt/binding-win32-arm64-msvc': 0.65.0 + '@oxfmt/binding-win32-ia32-msvc': 0.65.0 + '@oxfmt/binding-win32-x64-msvc': 0.65.0 + + oxlint-tsgolint@7.0.2001: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 7.0.2001 + '@oxlint-tsgolint/darwin-x64': 7.0.2001 + '@oxlint-tsgolint/linux-arm64': 7.0.2001 + '@oxlint-tsgolint/linux-x64': 7.0.2001 + '@oxlint-tsgolint/win32-arm64': 7.0.2001 + '@oxlint-tsgolint/win32-x64': 7.0.2001 + + oxlint@1.80.0(oxlint-tsgolint@7.0.2001): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.51.0 - '@oxlint/binding-android-arm64': 1.51.0 - '@oxlint/binding-darwin-arm64': 1.51.0 - '@oxlint/binding-darwin-x64': 1.51.0 - '@oxlint/binding-freebsd-x64': 1.51.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.51.0 - '@oxlint/binding-linux-arm-musleabihf': 1.51.0 - '@oxlint/binding-linux-arm64-gnu': 1.51.0 - '@oxlint/binding-linux-arm64-musl': 1.51.0 - '@oxlint/binding-linux-ppc64-gnu': 1.51.0 - '@oxlint/binding-linux-riscv64-gnu': 1.51.0 - '@oxlint/binding-linux-riscv64-musl': 1.51.0 - '@oxlint/binding-linux-s390x-gnu': 1.51.0 - '@oxlint/binding-linux-x64-gnu': 1.51.0 - '@oxlint/binding-linux-x64-musl': 1.51.0 - '@oxlint/binding-openharmony-arm64': 1.51.0 - '@oxlint/binding-win32-arm64-msvc': 1.51.0 - '@oxlint/binding-win32-ia32-msvc': 1.51.0 - '@oxlint/binding-win32-x64-msvc': 1.51.0 + '@oxlint/binding-android-arm-eabi': 1.80.0 + '@oxlint/binding-android-arm64': 1.80.0 + '@oxlint/binding-darwin-arm64': 1.80.0 + '@oxlint/binding-darwin-x64': 1.80.0 + '@oxlint/binding-freebsd-x64': 1.80.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.80.0 + '@oxlint/binding-linux-arm-musleabihf': 1.80.0 + '@oxlint/binding-linux-arm64-gnu': 1.80.0 + '@oxlint/binding-linux-arm64-musl': 1.80.0 + '@oxlint/binding-linux-ppc64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-musl': 1.80.0 + '@oxlint/binding-linux-s390x-gnu': 1.80.0 + '@oxlint/binding-linux-x64-gnu': 1.80.0 + '@oxlint/binding-linux-x64-musl': 1.80.0 + '@oxlint/binding-openharmony-arm64': 1.80.0 + '@oxlint/binding-win32-arm64-msvc': 1.80.0 + '@oxlint/binding-win32-ia32-msvc': 1.80.0 + '@oxlint/binding-win32-x64-msvc': 1.80.0 + oxlint-tsgolint: 7.0.2001 package-name-regex@2.0.6: {} diff --git a/src/cache/cached-registry.ts b/src/cache/cached-registry.ts index 913314c..53761ea 100644 --- a/src/cache/cached-registry.ts +++ b/src/cache/cached-registry.ts @@ -92,10 +92,15 @@ export class CachedRegistry extends Registry { } /** - * Generic cache-through method: - * 1. Check lockfile for fresh entry - * 2. If fresh -> read from storage - * 3. If stale/missing -> fetch, store, update lockfile + * Read a fresh cached value or fetch and persist its replacement. + * + * @param name - Package name. + * @param type - Cached data type. + * @param version - Package version when the cache entry is versioned. + * @param fetcher - Upstream operation used after a cache miss. + * @param signal - Optional cancellation signal. + * @param extraMeta - Optional lockfile metadata derived from the fetched value. + * @returns {Promise} The cached or freshly fetched value. */ private async cached( name: string, diff --git a/src/cache/index.ts b/src/cache/index.ts index 10d322f..3d00942 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -38,7 +38,13 @@ export interface CreateCachedOptions { storage?: Storage; } -/** Create a registry instance with caching + lockfile tracking. */ +/** + * Create a registry with cache and lockfile tracking. + * + * @param ecosystem - Registry ecosystem key. + * @param options - Optional registry, client, and storage overrides. + * @returns {Registry} A cached registry instance. + */ export function createCached(ecosystem: string, options?: CreateCachedOptions): Registry { const inner = create(ecosystem, options?.baseURL, options?.client); return new CachedRegistry(inner, options?.storage); diff --git a/src/cache/lockfile.ts b/src/cache/lockfile.ts index 901dedd..03225bf 100644 --- a/src/cache/lockfile.ts +++ b/src/cache/lockfile.ts @@ -40,7 +40,12 @@ function emptyLockfile(): Lockfile { return { version: LOCKFILE_VERSION, entries: {} }; } -/** Read the lockfile from storage. Returns empty lockfile if not found or invalid. */ +/** + * Read the lockfile, falling back to an empty valid structure. + * + * @param storage - Optional storage backend. + * @returns {Promise} The stored or empty lockfile. + */ export async function readLockfile(storage?: Storage): Promise { const s = storage ?? getStorage(); try { @@ -52,13 +57,26 @@ export async function readLockfile(storage?: Storage): Promise { } } -/** Write the lockfile to storage. */ +/** + * Persist the lockfile. + * + * @param lockfile - Lockfile to persist. + * @param storage - Optional storage backend. + */ export async function writeLockfile(lockfile: Lockfile, storage?: Storage): Promise { const s = storage ?? getStorage(); await s.setItem(LOCKFILE_KEY, lockfile); } -/** Build a cache key from ecosystem + name + type + optional version. */ +/** + * Build a stable cache key. + * + * @param ecosystem - Registry ecosystem. + * @param name - Package name. + * @param type - Cached data type. + * @param version - Optional package version. + * @returns {string} The cache key. + */ export function cacheKey( ecosystem: string, name: string, @@ -69,13 +87,24 @@ export function cacheKey( return version ? `${base}:${version}` : base; } -/** Check if a lockfile entry is still fresh. */ +/** + * Check whether a lockfile entry is fresh. + * + * @param entry - Entry to inspect. + * @returns {boolean} Whether the entry is still valid. + */ export function isFresh(entry: LockfileEntry): boolean { const expiresAt = entry.fetchedAt + entry.ttl * 1000; return Date.now() < expiresAt; } -/** Get a lockfile entry if it exists and is fresh. Returns null otherwise. */ +/** + * Read a fresh lockfile entry. + * + * @param lockfile - Lockfile to inspect. + * @param key - Cache key. + * @returns {LockfileEntry | null} The fresh entry, or null. + */ export function getFreshEntry(lockfile: Lockfile, key: string): LockfileEntry | null { const entry = lockfile.entries[key]; if (!entry) return null; @@ -83,17 +112,32 @@ export function getFreshEntry(lockfile: Lockfile, key: string): LockfileEntry | return entry; } -/** Upsert an entry in the lockfile. Does NOT persist โ€” call writeLockfile() after. */ +/** + * Upsert an in-memory lockfile entry without persisting it. + * + * @param lockfile - Lockfile to mutate. + * @param entry - Entry to store. + */ export function setEntry(lockfile: Lockfile, entry: LockfileEntry): void { lockfile.entries[entry.key] = entry; } -/** Remove an entry from the lockfile. */ +/** + * Remove an in-memory lockfile entry. + * + * @param lockfile - Lockfile to mutate. + * @param key - Cache key to remove. + */ export function removeEntry(lockfile: Lockfile, key: string): void { delete lockfile.entries[key]; } -/** Remove all stale entries from the lockfile. Returns count of removed entries. */ +/** + * Remove every stale in-memory entry. + * + * @param lockfile - Lockfile to prune. + * @returns {number} The number of removed entries. + */ export function pruneStale(lockfile: Lockfile): number { let removed = 0; for (const key of Object.keys(lockfile.entries)) { diff --git a/src/cache/paths.ts b/src/cache/paths.ts index 939bcc9..bd9a185 100644 --- a/src/cache/paths.ts +++ b/src/cache/paths.ts @@ -4,10 +4,9 @@ import { join } from "node:path"; const APP_NAME = "registries"; /** - * Resolve the cache directory following platform conventions: - * - Linux: $XDG_CACHE_HOME/registries or ~/.cache/registries - * - macOS: ~/Library/Caches/registries - * - Windows: %LOCALAPPDATA%/registries/cache + * Resolve the cache directory using the current platform convention. + * + * @returns {string} The absolute cache directory path. */ export function getCacheDir(): string { // Explicit override always wins diff --git a/src/cache/storage.ts b/src/cache/storage.ts index 7758d9d..664d9d9 100644 --- a/src/cache/storage.ts +++ b/src/cache/storage.ts @@ -7,30 +7,42 @@ import { hash } from "node:crypto"; let _storage: Storage | undefined; /** - * Configure a custom unstorage instance for @agntn/registries. - * Call this before any cache operations to use a non-default driver - * (e.g. Cloudflare KV, Redis, memory). + * Configure the storage backend before the first cache operation. + * + * @param storage - Storage backend to use. */ export function configureStorage(storage: Storage): void { _storage = storage; } -/** Get or create the shared storage instance. Uses configured storage or falls back to fs driver. */ +/** + * Get or create the shared storage backend. + * + * @returns {Storage} The configured or filesystem-backed storage. + */ export function getStorage(): Storage { - if (!_storage) { - _storage = createStorage({ - driver: fsDriver({ base: getCacheDir() }), - }); - } + _storage ??= createStorage({ + driver: fsDriver({ base: getCacheDir() }), + }); return _storage; } -/** Get a namespaced storage for a specific ecosystem (e.g. 'npm', 'cargo'). */ +/** + * Get storage scoped to one registry ecosystem. + * + * @param ecosystem - Registry ecosystem key. + * @returns {Storage} Namespaced storage for the ecosystem. + */ export function getEcosystemStorage(ecosystem: string): Storage { return prefixStorage(getStorage(), ecosystem); } -/** Compute a sha256 integrity hash of a JSON-serializable value. */ +/** + * Compute a SHA-256 integrity value for JSON-serializable data. + * + * @param value - Value to hash. + * @returns {string} The prefixed hexadecimal digest. + */ export function computeIntegrity(value: unknown): string { return `sha256-${hash("sha256", JSON.stringify(value), "hex")}`; } diff --git a/src/cli.ts b/src/cli.ts index 5769bac..fb38c8d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,4 +17,4 @@ const main = defineCommand({ }, }); -runMain(main); +await runMain(main); diff --git a/src/commands/cache.ts b/src/commands/cache.ts index 2848da4..8299de8 100644 --- a/src/commands/cache.ts +++ b/src/commands/cache.ts @@ -45,10 +45,10 @@ export default defineCommand({ const stale = entries.length - fresh.length; consola.log(""); - consola.log(` \x1b[1mCache status\x1b[0m`); - consola.log(` \x1b[90mDirectory:\x1b[0m ${getCacheDir()}`); + consola.log(` \x1B[1mCache status\x1B[0m`); + consola.log(` \x1B[90mDirectory:\x1B[0m ${getCacheDir()}`); consola.log( - ` \x1b[90mEntries:\x1b[0m ${entries.length} total, ${fresh.length} fresh, ${stale} stale`, + ` \x1B[90mEntries:\x1B[0m ${entries.length} total, ${fresh.length} fresh, ${stale} stale`, ); consola.log(""); @@ -60,7 +60,7 @@ export default defineCommand({ byEco.set(eco, (byEco.get(eco) ?? 0) + 1); } for (const [eco, count] of byEco) { - consola.log(` \x1b[36m${eco}\x1b[0m: ${count} cached`); + consola.log(` \x1B[36m${eco}\x1B[0m: ${count} cached`); } consola.log(""); } diff --git a/src/commands/deps.ts b/src/commands/deps.ts index d5c66b4..da65b06 100644 --- a/src/commands/deps.ts +++ b/src/commands/deps.ts @@ -45,7 +45,12 @@ export default defineCommand({ import type { Dependency } from "../core/types.ts"; -function outputDeps(name: string, version: string, deps: Dependency[], json: boolean): void { +function outputDeps( + name: string, + version: string, + deps: readonly Dependency[], + json: boolean, +): void { if (json) { console.log(JSON.stringify(deps, null, 2)); return; @@ -53,32 +58,40 @@ function outputDeps(name: string, version: string, deps: Dependency[], json: boo consola.log(""); consola.log( - ` \x1b[1m${name}@${version}\x1b[0m โ€” ${deps.length} dependenc${deps.length === 1 ? "y" : "ies"}`, + ` \x1B[1m${name}@${version}\x1B[0m โ€” ${deps.length} dependenc${deps.length === 1 ? "y" : "ies"}`, ); consola.log(""); - const byScope = new Map(); - for (const dep of deps) { - const scope = dep.scope || "runtime"; - if (!byScope.has(scope)) byScope.set(scope, []); - byScope.get(scope)!.push(dep); - } + const byScope = groupByScope(deps); const scopeColors: Record = { - runtime: "\x1b[32m", - development: "\x1b[33m", - build: "\x1b[35m", - test: "\x1b[36m", - optional: "\x1b[90m", + runtime: "\x1B[32m", + development: "\x1B[33m", + build: "\x1B[35m", + test: "\x1B[36m", + optional: "\x1B[90m", }; for (const [scope, scopeDeps] of byScope) { - const color = scopeColors[scope] ?? "\x1b[0m"; - consola.log(` ${color}${scope}\x1b[0m (${scopeDeps.length})`); + const color = scopeColors[scope] ?? "\x1B[0m"; + consola.log(` ${color}${scope}\x1B[0m (${scopeDeps.length})`); for (const dep of scopeDeps) { - const opt = dep.optional ? " \x1b[90m(optional)\x1b[0m" : ""; - consola.log(` ${dep.name} \x1b[90m${dep.requirements}\x1b[0m${opt}`); + const opt = dep.optional ? " \x1B[90m(optional)\x1B[0m" : ""; + consola.log(` ${dep.name} \x1B[90m${dep.requirements}\x1B[0m${opt}`); } consola.log(""); } } + +function groupByScope(deps: readonly Dependency[]): Map { + const byScope = new Map(); + + for (const dep of deps) { + const scope = dep.scope || "runtime"; + const scopeDeps = byScope.get(scope) ?? []; + scopeDeps.push(dep); + byScope.set(scope, scopeDeps); + } + + return byScope; +} diff --git a/src/commands/info.ts b/src/commands/info.ts index 4bf9de9..2b3940e 100644 --- a/src/commands/info.ts +++ b/src/commands/info.ts @@ -9,34 +9,42 @@ export function outputPackageInfo(pkg: Package, name: string, reg: Registry): vo const urls = reg.urls(); const registryUrl = urls.registry(name); const docsUrl = resolveDocsUrl(pkg, urls, pkg.latestVersion || undefined); - const showDocs = - Boolean(docsUrl) && - docsUrl !== pkg.homepage && - docsUrl !== pkg.repository && - docsUrl !== registryUrl; + const showDocs = shouldShowDocs(docsUrl, pkg, registryUrl); consola.log(""); consola.log( - ` \x1b[1m\x1b[36m${pkg.name}\x1b[0m${pkg.latestVersion ? `\x1b[90m@${pkg.latestVersion}\x1b[0m` : ""}`, + ` \x1B[1m\x1B[36m${pkg.name}\x1B[0m${pkg.latestVersion ? `\x1B[90m@${pkg.latestVersion}\x1B[0m` : ""}`, ); if (pkg.description) { consola.log(` ${pkg.description}`); } consola.log(""); - if (pkg.licenses) consola.log(` \x1b[90mLicense:\x1b[0m ${pkg.licenses}`); - if (pkg.repository) consola.log(` \x1b[90mRepository:\x1b[0m ${pkg.repository}`); - if (pkg.homepage) consola.log(` \x1b[90mHomepage:\x1b[0m ${pkg.homepage}`); - if (showDocs) consola.log(` \x1b[90mDocs:\x1b[0m ${docsUrl}`); - consola.log(` \x1b[90mRegistry:\x1b[0m ${registryUrl}`); - if (pkg.keywords.length > 0) { - consola.log(` \x1b[90mKeywords:\x1b[0m ${pkg.keywords.join(", ")}`); + const rows = [ + ["License:", " ", pkg.licenses, Boolean(pkg.licenses)], + ["Repository:", " ", pkg.repository, Boolean(pkg.repository)], + ["Homepage:", " ", pkg.homepage, Boolean(pkg.homepage)], + ["Docs:", " ", docsUrl, showDocs], + ["Registry:", " ", registryUrl, true], + ["Keywords:", " ", pkg.keywords.join(", "), pkg.keywords.length > 0], + ["Namespace:", " ", pkg.namespace, Boolean(pkg.namespace)], + ["Ecosystem:", " ", reg.ecosystem(), true], + ] as const; + + for (const [label, spacing, value, visible] of rows) { + if (visible) consola.log(` \x1B[90m${label}\x1B[0m${spacing}${value}`); } - if (pkg.namespace) consola.log(` \x1b[90mNamespace:\x1b[0m ${pkg.namespace}`); - consola.log(` \x1b[90mEcosystem:\x1b[0m ${reg.ecosystem()}`); consola.log(""); } +function shouldShowDocs( + docsUrl: string, + pkg: Readonly>, + registryUrl: string, +): boolean { + return Boolean(docsUrl) && ![pkg.homepage, pkg.repository, registryUrl].includes(docsUrl); +} + export default defineCommand({ meta: { name: "info", diff --git a/src/commands/maintainers.ts b/src/commands/maintainers.ts index ac66e1f..b303f18 100644 --- a/src/commands/maintainers.ts +++ b/src/commands/maintainers.ts @@ -1,3 +1,4 @@ +import type { Maintainer } from "../core/types.ts"; import { defineCommand } from "citty"; import consola from "consola"; import { sharedArgs, resolvePURL, withErrorHandling } from "./shared.ts"; @@ -25,22 +26,34 @@ export default defineCommand({ return; } - consola.log(""); - consola.log( - ` \x1b[1m${name}\x1b[0m โ€” ${maintainers.length} maintainer${maintainers.length === 1 ? "" : "s"}`, - ); - consola.log(""); - - for (const m of maintainers) { - const nameStr = m.name || m.login || "unknown"; - const parts: string[] = [nameStr]; - if (m.login && m.login !== nameStr) parts.push(`\x1b[90m(${m.login})\x1b[0m`); - if (m.email) parts.push(`\x1b[90m<${m.email}>\x1b[0m`); - if (m.role) parts.push(`\x1b[36m[${m.role}]\x1b[0m`); - consola.log(` ${parts.join(" ")}`); - if (m.url) consola.log(` \x1b[90m${m.url}\x1b[0m`); - } - consola.log(""); + outputMaintainers(name, maintainers); }); }, }); + +function outputMaintainers(name: string, maintainers: readonly Maintainer[]): void { + consola.log(""); + consola.log( + ` \x1B[1m${name}\x1B[0m โ€” ${maintainers.length} maintainer${maintainers.length === 1 ? "" : "s"}`, + ); + consola.log(""); + + for (const maintainer of maintainers) { + consola.log(` ${formatMaintainer(maintainer).join(" ")}`); + if (maintainer.url) consola.log(` \x1B[90m${maintainer.url}\x1B[0m`); + } + consola.log(""); +} + +function formatMaintainer(maintainer: Readonly): string[] { + const displayName = maintainer.name || maintainer.login || "unknown"; + const parts = [displayName]; + + if (maintainer.login && maintainer.login !== displayName) { + parts.push(`\x1B[90m(${maintainer.login})\x1B[0m`); + } + if (maintainer.email) parts.push(`\x1B[90m<${maintainer.email}>\x1B[0m`); + if (maintainer.role) parts.push(`\x1B[36m[${maintainer.role}]\x1B[0m`); + + return parts; +} diff --git a/src/commands/shared.ts b/src/commands/shared.ts index 4c5d10e..45127d8 100644 --- a/src/commands/shared.ts +++ b/src/commands/shared.ts @@ -25,8 +25,11 @@ export const sharedArgs = { } as const; /** - * Resolve a PURL string or shorthand into [registry, name, version]. - * Uses CachedRegistry unless --no-cache is set. + * Resolve a PURL or shorthand, using cache unless disabled. + * + * @param input - PURL or shorthand package identifier. + * @param useCache - Whether to wrap the registry with caching. + * @returns {[Registry, string, string]} The registry, package name, and version. */ export function resolvePURL(input: string, useCache = true): [Registry, string, string] { let purl = input; @@ -45,7 +48,12 @@ export function resolvePURL(input: string, useCache = true): [Registry, string, return createFromPURL(purl); } -/** Run a command with standard error handling. */ +/** + * Run a command with standard CLI error handling. + * + * @param fn - Command operation to run. + * @returns {Promise} Completion after the command or handled exit. + */ export async function withErrorHandling(fn: () => Promise): Promise { try { await fn(); diff --git a/src/commands/versions.ts b/src/commands/versions.ts index 8c67a79..3d76bf4 100644 --- a/src/commands/versions.ts +++ b/src/commands/versions.ts @@ -3,7 +3,7 @@ import consola from "consola"; import type { Version } from "../core/types.ts"; import { sharedArgs, resolvePURL, withErrorHandling } from "./shared.ts"; -export function selectRecentVersions(versions: Version[], limit: number): Version[] { +export function selectRecentVersions(versions: readonly Version[], limit: number): Version[] { return versions .toSorted((a, b) => { if (a.publishedAt === null) return b.publishedAt === null ? 0 : 1; @@ -46,21 +46,21 @@ export default defineCommand({ consola.log(""); consola.log( - ` \x1b[1m${name}\x1b[0m โ€” ${versions.length} version${versions.length === 1 ? "" : "s"}`, + ` \x1B[1m${name}\x1B[0m โ€” ${versions.length} version${versions.length === 1 ? "" : "s"}`, ); consola.log(""); for (const v of shown) { - const status = v.status ? ` \x1b[33m[${v.status}]\x1b[0m` : ""; + const status = v.status ? ` \x1B[33m[${v.status}]\x1B[0m` : ""; const date = v.publishedAt - ? ` \x1b[90m${v.publishedAt.toISOString().slice(0, 10)}\x1b[0m` + ? ` \x1B[90m${v.publishedAt.toISOString().slice(0, 10)}\x1B[0m` : ""; consola.log(` ${v.number}${status}${date}`); } if (versions.length > limit) { consola.log( - `\n \x1b[90m... and ${versions.length - limit} more (use --limit to show more)\x1b[0m`, + `\n \x1B[90m... and ${versions.length - limit} more (use --limit to show more)\x1B[0m`, ); } consola.log(""); diff --git a/src/core/client.ts b/src/core/client.ts index 565cd3c..05efb21 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -9,6 +9,10 @@ const DEFAULT_TIMEOUT = 30_000; const DEFAULT_USER_AGENT = "registries/0.1.0"; const MAX_TIMER_DELAY = 2_147_483_647; +function retryCount(value: unknown): number { + return typeof value === "number" ? value : 0; +} + function parseRetryAfterValue(header: string | null | undefined): number | undefined { if (!header) return undefined; const trimmed = header.trim(); @@ -27,19 +31,22 @@ function parseRetryAfterValue(header: string | null | undefined): number | undef } /** - * Parse a `Retry-After` header into seconds. - * - * Handles two formats (RFC 7231 ยง7.1.3): - * - **Numeric**: `"120"` โ†’ 120 - * - **HTTP-date**: `"Wed, 21 Oct 2025 07:28:00 GMT"` โ†’ seconds until that time + * Parse a Retry-After header into seconds, defaulting to 60. * - * Returns 60 when the header is absent, empty, or unparseable. + * @param header - Numeric seconds or an HTTP date. + * @returns {number} The retry delay in seconds. */ export function parseRetryAfter(header: string | null | undefined): number { return parseRetryAfterValue(header) ?? 60; } -/** Apply a valid, timer-safe `Retry-After` value or reject an unschedulable delay. */ +/** + * Apply a valid Retry-After value or reject an unschedulable delay. + * + * @param header - Numeric seconds or an HTTP date. + * @param fallbackDelay - Delay in milliseconds used without a valid header. + * @returns {number} A timer-safe delay in milliseconds. + */ export function retryDelayFor(header: string | null | undefined, fallbackDelay: number): number { const retryAfter = parseRetryAfterValue(header); if (retryAfter === undefined) return fallbackDelay; @@ -73,7 +80,7 @@ export class Client { this.fetch = ofetch.create({ retry: this.maxRetries, retryDelay(context) { - const remaining = typeof context.options.retry === "number" ? context.options.retry : 0; + const remaining = retryCount(context.options.retry); const attempt = maxRetries - remaining; const delay = baseDelay * Math.pow(2, attempt - 1); const jitteredDelay = delay + delay * Math.random() * 0.1; @@ -82,7 +89,9 @@ export class Client { return retryDelayFor(retryAfter, jitteredDelay); } catch (error) { if (error instanceof RateLimitError && context.response?.status !== 429) { - throw new HTTPError(context.response?.status ?? 0, String(context.request), ""); + const requestURL = + typeof context.request === "string" ? context.request : context.request.url; + throw new HTTPError(context.response?.status ?? 0, requestURL, ""); } throw error; } @@ -96,11 +105,18 @@ export class Client { }); } - /** Fetch JSON from a URL with retry and rate limiting. */ + /** + * Fetch JSON with retry and rate limiting. + * + * @param url - Request URL. + * @param signal - Optional cancellation signal. + * @param headers - Optional request headers. + * @returns {Promise} The decoded response body. + */ async getJSON( url: string, signal?: AbortSignal, - headers?: Record, + headers?: Readonly>, ): Promise { if (this.rateLimiter) { await this.rateLimiter.wait(signal); @@ -125,10 +141,12 @@ export class Client { let _defaultClient: Client | undefined; -/** Get or create the shared default client. */ +/** + * Get or create the shared HTTP client. + * + * @returns {Client} The shared client. + */ export function defaultClient(): Client { - if (!_defaultClient) { - _defaultClient = new Client(); - } + _defaultClient ??= new Client(); return _defaultClient; } diff --git a/src/core/license.ts b/src/core/license.ts index 1615f4f..1af23ae 100644 --- a/src/core/license.ts +++ b/src/core/license.ts @@ -1,8 +1,8 @@ /** - * Normalize a license string to SPDX expression. + * Normalize common license names to SPDX expressions. * - * Handles common variations: "MIT License" โ†’ "MIT", "Apache 2.0" โ†’ "Apache-2.0". - * Passes through already-valid SPDX identifiers unchanged. + * @param raw - License name or SPDX expression. + * @returns {string} The normalized SPDX expression, or an empty string. */ export function normalizeLicense(raw: string | null | undefined): string { if (!raw) return ""; @@ -56,13 +56,14 @@ const LICENSE_MAP = new Map([ ]); /** - * Combine multiple license strings into a single SPDX expression. + * Combine license strings into one SPDX expression. * - * Default operator is OR (disjunctive, user picks one). - * Pass "AND" for ecosystems where multiple licenses mean all apply (e.g. Arch Linux). + * @param licenses - License names or expressions to normalize. + * @param operator - SPDX operator joining multiple licenses. + * @returns {string} The combined SPDX expression, or an empty string. */ export function combineLicenses( - licenses: (string | null | undefined)[], + licenses: readonly (string | null | undefined)[], operator: "OR" | "AND" = "OR", ): string { const normalized = licenses.map((l) => normalizeLicense(l)).filter(Boolean); diff --git a/src/core/purl.ts b/src/core/purl.ts index f485117..7eb4fc6 100644 --- a/src/core/purl.ts +++ b/src/core/purl.ts @@ -5,7 +5,6 @@ import { InvalidPURLError } from "./errors.ts"; const QUALIFIER_KEY_PATTERN = /^[a-z][a-z0-9._-]*$/; -/** Decode a percent-encoded PURL component, throwing InvalidPURLError on malformed sequences. */ function decodePURLComponent(purlStr: string, value: string): string { try { return decodeURIComponent(value); @@ -14,11 +13,54 @@ function decodePURLComponent(purlStr: string, value: string): string { } } +function extractSubpath(purlStr: string, value: string): { remainder: string; subpath: string } { + const hashIdx = value.indexOf("#"); + if (hashIdx === -1) return { remainder: value, subpath: "" }; + + const subpath = value + .slice(hashIdx + 1) + .split("/") + .map((segment) => decodePURLComponent(purlStr, segment)) + .join("/"); + return { remainder: value.slice(0, hashIdx), subpath }; +} + +function extractQualifiers( + purlStr: string, + value: string, +): { qualifiers: Record; remainder: string } { + const queryIdx = value.indexOf("?"); + if (queryIdx === -1) return { qualifiers: {}, remainder: value }; + + const qualifiers: Record = {}; + for (const pair of value.slice(queryIdx + 1).split("&")) { + const eqIdx = pair.indexOf("="); + if (eqIdx !== -1) { + qualifiers[decodePURLComponent(purlStr, pair.slice(0, eqIdx))] = decodePURLComponent( + purlStr, + pair.slice(eqIdx + 1), + ); + } + } + return { qualifiers, remainder: value.slice(0, queryIdx) }; +} + +function extractVersion(purlStr: string, value: string): { remainder: string; version: string } { + const lastSlashIdx = value.lastIndexOf("/"); + const lastAtIdx = value.lastIndexOf("@"); + if (lastAtIdx === -1 || lastAtIdx <= lastSlashIdx) return { remainder: value, version: "" }; + + return { + remainder: value.slice(0, lastAtIdx), + version: decodePURLComponent(purlStr, value.slice(lastAtIdx + 1)), + }; +} + /** - * Parse a PURL string into its components. - * - * Format: `pkg://@?#` + * Parse a package URL into normalized components. * + * @param purlStr - Package URL to parse. + * @returns {ParsedPURL} The normalized components. * @see https://github.com/package-url/purl-spec (ECMA-427) */ export function parsePURL(purlStr: string): ParsedPURL { @@ -26,47 +68,10 @@ export function parsePURL(purlStr: string): ParsedPURL { throw new InvalidPURLError(purlStr, 'must start with "pkg:"'); } - let remainder = purlStr.slice(4); // strip 'pkg:' - - // Extract subpath - let subpath = ""; - const hashIdx = remainder.indexOf("#"); - if (hashIdx !== -1) { - subpath = remainder - .slice(hashIdx + 1) - .split("/") - .map((s) => decodePURLComponent(purlStr, s)) - .join("/"); - remainder = remainder.slice(0, hashIdx); - } - - // Extract qualifiers - const qualifiers: Record = {}; - const queryIdx = remainder.indexOf("?"); - if (queryIdx !== -1) { - const queryStr = remainder.slice(queryIdx + 1); - remainder = remainder.slice(0, queryIdx); - for (const pair of queryStr.split("&")) { - const eqIdx = pair.indexOf("="); - if (eqIdx !== -1) { - qualifiers[decodePURLComponent(purlStr, pair.slice(0, eqIdx))] = decodePURLComponent( - purlStr, - pair.slice(eqIdx + 1), - ); - } - } - } - - // Extract version โ€” the version separator is the last '@' that appears - // after the last '/', so scoped names like 'npm/@vue/core' are not mistaken - // for having a version. - let version = ""; - const lastSlashBeforeVersion = remainder.lastIndexOf("/"); - const lastAtIdx = remainder.lastIndexOf("@"); - if (lastAtIdx !== -1 && lastAtIdx > lastSlashBeforeVersion) { - version = decodePURLComponent(purlStr, remainder.slice(lastAtIdx + 1)); - remainder = remainder.slice(0, lastAtIdx); - } + const subpathParts = extractSubpath(purlStr, purlStr.slice(4)); + const qualifierParts = extractQualifiers(purlStr, subpathParts.remainder); + const versionParts = extractVersion(purlStr, qualifierParts.remainder); + const remainder = versionParts.remainder; // Extract type const slashIdx = remainder.indexOf("/"); @@ -102,17 +107,29 @@ export function parsePURL(purlStr: string): ParsedPURL { // Ecosystem-specific normalization per PURL spec if (type === "pypi") { - name = name.toLowerCase().replace(/[-_.]+/g, "-"); + name = name.toLowerCase().replaceAll(/[-_.]+/g, "-"); } if (type === "alpm") { name = name.toLowerCase(); } - return { type, namespace, name, version, qualifiers, subpath }; + return { + type, + namespace, + name, + version: versionParts.version, + qualifiers: qualifierParts.qualifiers, + subpath: subpathParts.subpath, + }; } -/** Build the full name from namespace + name (e.g., "@scope/pkg" for npm). */ +/** + * Build a registry package name from PURL components. + * + * @param parsed - Parsed package URL components. + * @returns {string} The namespace-qualified package name. + */ export function fullName(parsed: ParsedPURL): string { if (parsed.namespace) { return `${parsed.namespace}/${parsed.name}`; @@ -120,7 +137,13 @@ export function fullName(parsed: ParsedPURL): string { return parsed.name; } -/** Create a registry instance from a PURL, returning [registry, name, version]. */ +/** + * Create a registry from a package URL. + * + * @param purlStr - Package URL to resolve. + * @param client - Optional HTTP client. + * @returns {[Registry, string, string]} The registry, package name, and version. + */ export function createFromPURL(purlStr: string, client?: Client): [Registry, string, string] { const parsed = parsePURL(purlStr); const baseURL = parsed.qualifiers["repository_url"] ?? ""; @@ -128,15 +151,22 @@ export function createFromPURL(purlStr: string, client?: Client): [Registry, str return [reg, fullName(parsed), parsed.version]; } -/** Build a PURL string from components. Inverse of `parsePURL`. */ -export function buildPURL(parts: { - type: string; - name: string; - version?: string; - namespace?: string; - qualifiers?: Record; - subpath?: string; -}): string { +/** + * Build a package URL from normalized components. + * + * @param parts - Package URL components. + * @returns {string} The canonical package URL. + */ +export function buildPURL( + parts: Readonly<{ + type: string; + name: string; + version?: string; + namespace?: string; + qualifiers?: Readonly>; + subpath?: string; + }>, +): string { let purl = `pkg:${parts.type}/`; if (parts.namespace) { purl += `${parts.namespace diff --git a/src/core/registry.ts b/src/core/registry.ts index 059c1a6..b42df5d 100644 --- a/src/core/registry.ts +++ b/src/core/registry.ts @@ -20,7 +20,13 @@ export abstract class Registry { const constructors = new Map Registry>(); const defaults = new Map(); -/** Register an ecosystem class. Called by each registry module on import. */ +/** + * Register an ecosystem adapter. + * + * @param ecosystem - Ecosystem key. + * @param defaultURL - Default registry API URL. + * @param RegistryClass - Adapter constructor. + */ export function register( ecosystem: string, defaultURL: string, @@ -30,7 +36,14 @@ export function register( defaults.set(ecosystem, defaultURL); } -/** Create a registry instance for the given ecosystem. */ +/** + * Create an adapter for a registered ecosystem. + * + * @param ecosystem - Ecosystem key. + * @param baseURL - Optional registry API URL override. + * @param client - Optional HTTP client. + * @returns {Registry} The registry adapter. + */ export function create(ecosystem: string, baseURL?: string, client?: Client): Registry { const RegistryClass = constructors.get(ecosystem); if (!RegistryClass) { @@ -39,12 +52,21 @@ export function create(ecosystem: string, baseURL?: string, client?: Client): Re return new RegistryClass(baseURL || defaults.get(ecosystem)!, client ?? defaultClient()); } -/** List all registered ecosystem names. */ +/** + * List registered ecosystems. + * + * @returns {string[]} The ecosystem keys. + */ export function ecosystems(): string[] { return [...constructors.keys()]; } -/** Check if an ecosystem is registered. */ +/** + * Check whether an ecosystem is registered. + * + * @param ecosystem - Ecosystem key. + * @returns {boolean} Whether an adapter is registered. + */ export function has(ecosystem: string): boolean { return constructors.has(ecosystem); } diff --git a/src/core/repository.ts b/src/core/repository.ts index 7e97f7f..24c5b50 100644 --- a/src/core/repository.ts +++ b/src/core/repository.ts @@ -1,79 +1,47 @@ -/** - * Normalize a repository URL to a clean HTTPS git URL. - * - * Handles various formats: - * - "git+https://github.com/foo/bar.git" โ†’ "https://github.com/foo/bar" - * - "git://github.com/foo/bar" โ†’ "https://github.com/foo/bar" - * - "ssh://git@github.com/foo/bar.git" โ†’ "https://github.com/foo/bar" - * - "github:foo/bar" โ†’ "https://github.com/foo/bar" - * - { url: "..." } โ†’ extracted and normalized - */ -export function normalizeRepositoryURL(raw: unknown): string { - if (!raw) return ""; +type PrefixReplacement = readonly [prefix: string, replacement: string]; - let url: string; +const REPOSITORY_SHORTHANDS: readonly PrefixReplacement[] = [ + ["github:", "https://github.com/"], + ["gitlab:", "https://gitlab.com/"], + ["bitbucket:", "https://bitbucket.org/"], +]; - if (typeof raw === "string") { - url = raw; - } else if (typeof raw === "object" && raw !== null) { - const obj = raw as Record; - if (typeof obj["url"] === "string") { - url = obj["url"]; - } else { - return ""; - } - } else { - return ""; - } +const GIT_TRANSPORTS: readonly PrefixReplacement[] = [ + ["git://", "https://"], + ["ssh://git@", "https://"], +]; - url = url.trim(); - if (!url) return ""; +function extractRepositoryURL(raw: unknown): string { + if (typeof raw === "string") return raw; + if (typeof raw !== "object" || raw === null) return ""; - // GitHub shorthand: "github:user/repo" - if (url.startsWith("github:")) { - url = `https://github.com/${url.slice(7)}`; - } - - // GitLab shorthand - if (url.startsWith("gitlab:")) { - url = `https://gitlab.com/${url.slice(7)}`; - } - - // Bitbucket shorthand - if (url.startsWith("bitbucket:")) { - url = `https://bitbucket.org/${url.slice(10)}`; - } + const url = (raw as Record)["url"]; + return typeof url === "string" ? url : ""; +} - // Strip "git+" prefix - if (url.startsWith("git+")) { - url = url.slice(4); +function replacePrefix(value: string, replacements: readonly PrefixReplacement[]): string { + for (const [prefix, replacement] of replacements) { + if (value.startsWith(prefix)) return replacement + value.slice(prefix.length); } + return value; +} - // Convert git:// to https:// - if (url.startsWith("git://")) { - url = "https://" + url.slice(6); - } +/** + * Normalize repository shorthands and Git transports to HTTPS. + * + * @param raw - Repository URL string or object with a URL field. + * @returns {string} The normalized URL, or an empty string. + */ +export function normalizeRepositoryURL(raw: unknown): string { + let url = extractRepositoryURL(raw).trim(); + if (!url) return ""; - // Convert ssh://git@host/... to https://host/... - if (url.startsWith("ssh://git@")) { - url = "https://" + url.slice(10); - } + url = replacePrefix(url, REPOSITORY_SHORTHANDS); + if (url.startsWith("git+")) url = url.slice(4); + url = replacePrefix(url, GIT_TRANSPORTS); - // Convert git@host:user/repo to https://host/user/repo const sshMatch = url.match(/^git@([^:]+):(.+)$/); - if (sshMatch) { - url = `https://${sshMatch[1]}/${sshMatch[2]}`; - } - - // Strip trailing slash first so ".git/" doesn't survive - if (url.endsWith("/")) { - url = url.slice(0, -1); - } - - // Strip .git suffix - if (url.endsWith(".git")) { - url = url.slice(0, -4); - } + if (sshMatch) url = `https://${sshMatch[1]}/${sshMatch[2]}`; - return url; + return url.replace(/\/$/, "").replace(/\.git$/, ""); } diff --git a/src/helpers.ts b/src/helpers.ts index 9246491..9a9e883 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -3,7 +3,14 @@ import type { Client } from "./core/client.ts"; import { createFromPURL } from "./core/purl.ts"; import { InvalidPURLError } from "./core/errors.ts"; -/** Fetch normalized package metadata from a PURL. */ +/** + * Fetch normalized package metadata from a package URL. + * + * @param purl - Package URL. + * @param signal - Optional cancellation signal. + * @param client - Optional HTTP client. + * @returns {Promise} Normalized package metadata. + */ export async function fetchPackageFromPURL( purl: string, signal?: AbortSignal, @@ -13,7 +20,14 @@ export async function fetchPackageFromPURL( return reg.fetchPackage(name, signal); } -/** Fetch all versions from a PURL. */ +/** + * Fetch all versions from a package URL. + * + * @param purl - Package URL. + * @param signal - Optional cancellation signal. + * @param client - Optional HTTP client. + * @returns {Promise} Normalized versions. + */ export async function fetchVersionsFromPURL( purl: string, signal?: AbortSignal, @@ -23,7 +37,14 @@ export async function fetchVersionsFromPURL( return reg.fetchVersions(name, signal); } -/** Fetch dependencies for a specific version from a PURL. */ +/** + * Fetch dependencies for a versioned package URL. + * + * @param purl - Versioned package URL. + * @param signal - Optional cancellation signal. + * @param client - Optional HTTP client. + * @returns {Promise} Normalized dependencies. + */ export async function fetchDependenciesFromPURL( purl: string, signal?: AbortSignal, @@ -36,7 +57,14 @@ export async function fetchDependenciesFromPURL( return reg.fetchDependencies(name, version, signal); } -/** Fetch maintainers from a PURL. */ +/** + * Fetch maintainers from a package URL. + * + * @param purl - Package URL. + * @param signal - Optional cancellation signal. + * @param client - Optional HTTP client. + * @returns {Promise} Normalized maintainers. + */ export async function fetchMaintainersFromPURL( purl: string, signal?: AbortSignal, @@ -48,10 +76,16 @@ export async function fetchMaintainersFromPURL( const DEFAULT_CONCURRENCY = 15; -/** Bulk fetch packages from multiple PURLs, with concurrency limit. */ +/** + * Fetch packages concurrently, skipping failed lookups. + * + * @param purls - Package URLs to fetch. + * @param options - Optional concurrency, cancellation, and client settings. + * @returns {Promise>} Results keyed by package URL. + */ export async function bulkFetchPackages( - purls: string[], - options?: { concurrency?: number; signal?: AbortSignal; client?: Client }, + purls: readonly string[], + options?: Readonly<{ concurrency?: number; signal?: AbortSignal; client?: Client }>, ): Promise> { const concurrency = options?.concurrency ?? DEFAULT_CONCURRENCY; const results = new Map(); @@ -79,21 +113,18 @@ export async function bulkFetchPackages( } /** - * Select the best matching version from a list. - * - * Resolution order: - * 1. Exact match for `requested` (non-yanked/deprecated/retracted) - * 2. Exact match for `latest` (non-yanked/deprecated/retracted) - * 3. Newest available version with no negative status (by publishedAt) + * Select an exact requested or latest version, then the newest usable one. * - * Returns `null` when no usable version exists. + * @param versions - Candidate versions. + * @param options - Optional requested and latest version numbers. + * @returns {Version | null} The selected version, or null. */ export function selectVersion( - versions: Version[], - options?: { + versions: readonly Version[], + options?: Readonly<{ requested?: string; latest?: string; - }, + }>, ): Version | null { const { requested, latest } = options ?? {}; @@ -122,19 +153,22 @@ export function selectVersion( /** * Resolve the best documentation URL for a package. * - * Fallback chain: - * 1. `package.documentation` (explicit docs URL from registry) - * 2. `package.homepage` (project homepage) - * 3. `urls.documentation()` (ecosystem default, e.g. docs.rs, rubydoc.info) + * @param pkg - Package metadata. + * @param urls - Ecosystem URL builder. + * @param version - Optional package version. + * @returns {string} The explicit, homepage, or ecosystem documentation URL. */ export function resolveDocsUrl(pkg: Package, urls: URLBuilder, version?: string): string { return pkg.documentation || pkg.homepage || urls.documentation(pkg.name, version); } /** - * Resolve the best README URL for a package. + * Resolve the ecosystem README URL for a package. * - * Returns the ecosystem-specific URL where the package README can be fetched. + * @param pkg - Package metadata. + * @param urls - Ecosystem URL builder. + * @param version - Optional package version. + * @returns {string} The README URL. */ export function resolveReadmeUrl(pkg: Package, urls: URLBuilder, version?: string): string { return urls.readme(pkg.name, version); diff --git a/src/registries/alpm.ts b/src/registries/alpm.ts index 70985ca..977d69a 100644 --- a/src/registries/alpm.ts +++ b/src/registries/alpm.ts @@ -1,9 +1,10 @@ import type { Client } from "../core/client.ts"; import type { Dependency, Maintainer, Package, URLBuilder, Version } from "../core/types.ts"; import { Registry, register } from "../core/registry.ts"; -import { HTTPError, NotFoundError } from "../core/errors.ts"; +import { NotFoundError } from "../core/errors.ts"; import { combineLicenses } from "../core/license.ts"; import { buildPURL } from "../core/purl.ts"; +import { rethrowFetchError } from "./error.ts"; const AUR_BASE_URL = "https://aur.archlinux.org"; @@ -254,7 +255,6 @@ export class AlpmRegistry extends Registry { })); } - /** Search official repos by exact package name. Prefers x86_64 results. */ private async searchOfficial(name: string, signal?: AbortSignal): Promise { const url = `${this.baseURL}/packages/search/json/?name=${encodeURIComponent(name)}`; @@ -266,13 +266,9 @@ export class AlpmRegistry extends Registry { } // Prefer x86_64 arch, fall back to first result - return data.results.find((r) => r.arch === "x86_64") || data.results[0]!; + return data.results.find((r) => r.arch === "x86_64") ?? data.results[0]!; } catch (error) { - if (error instanceof NotFoundError) throw error; - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("alpm", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -287,8 +283,8 @@ export class AlpmRegistry extends Registry { homepage: result.URL || "", documentation: "", repository: "", - licenses: combineLicenses(result.License || []), - keywords: result.Keywords || [], + licenses: combineLicenses(result.License ?? []), + keywords: result.Keywords ?? [], namespace: "aur", latestVersion: result.Version, metadata: { @@ -309,7 +305,7 @@ export class AlpmRegistry extends Registry { { number: result.Version, publishedAt: result.LastModified ? new Date(result.LastModified * 1000) : null, - licenses: combineLicenses(result.License || []), + licenses: combineLicenses(result.License ?? []), integrity: "", status: result.OutOfDate ? "deprecated" : "", metadata: { @@ -329,10 +325,10 @@ export class AlpmRegistry extends Registry { this.assertVersionMatches(`aur/${name}`, version, result.Version); return this.buildDependencies( - result.Depends || [], - result.MakeDepends || [], - result.OptDepends || [], - result.CheckDepends || [], + result.Depends ?? [], + result.MakeDepends ?? [], + result.OptDepends ?? [], + result.CheckDepends ?? [], ); } @@ -353,7 +349,6 @@ export class AlpmRegistry extends Registry { ]; } - /** Fetch package info from AUR RPC v5. */ private async fetchAurInfo(name: string, signal?: AbortSignal): Promise { const url = `${AUR_BASE_URL}/rpc/v5/info/${encodeURIComponent(name)}`; @@ -366,17 +361,12 @@ export class AlpmRegistry extends Registry { return data.results[0]!; } catch (error) { - if (error instanceof NotFoundError) throw error; - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("alpm", `aur/${name}`); - } - throw error; + rethrowFetchError(error, this.ecosystem(), `aur/${name}`); } } // --- Shared helpers --- - /** Parse "namespace/name" into components. Defaults to "arch" namespace. */ private parseName(fullName: string): { namespace: string; pkgName: string } { const normalized = fullName.trim().toLowerCase(); const slashIdx = normalized.indexOf("/"); @@ -401,18 +391,18 @@ export class AlpmRegistry extends Registry { } } - /** Format ALPM version: epoch:pkgver-pkgrel (epoch omitted when 0). */ - private formatVersion(result: ArchPackageResult): string { + private formatVersion( + result: Readonly>, + ): string { const epoch = result.epoch && result.epoch > 0 ? `${result.epoch}:` : ""; return `${epoch}${result.pkgver}-${result.pkgrel}`; } - /** Build normalized dependency list from ALPM dependency arrays. */ private buildDependencies( - depends: string[], - makedepends: string[], - optdepends: string[], - checkdepends: string[], + depends: readonly string[], + makedepends: readonly string[], + optdepends: readonly string[], + checkdepends: readonly string[], ): Dependency[] { const dependencies: Dependency[] = []; @@ -459,7 +449,6 @@ export class AlpmRegistry extends Registry { return dependencies; } - /** Parse a dependency string like "glibc>=2.33" or "bash". */ private parseDep(dep: string): { name: string; requirements: string } { const match = dep.match(/^([a-zA-Z0-9@._+-]+)(.*)$/); if (!match) return { name: dep, requirements: "" }; @@ -469,7 +458,6 @@ export class AlpmRegistry extends Registry { }; } - /** Parse optional dependency like "perl-locale-gettext: translation support". */ private parseOptDep(dep: string): { name: string; requirements: string; description: string } { const colonIdx = dep.indexOf(":"); if (colonIdx === -1) { diff --git a/src/registries/cargo.ts b/src/registries/cargo.ts index 978d16e..04f76db 100644 --- a/src/registries/cargo.ts +++ b/src/registries/cargo.ts @@ -1,10 +1,10 @@ import type { Client } from "../core/client.ts"; import type { Dependency, Maintainer, Package, URLBuilder, Version } from "../core/types.ts"; import { Registry, register } from "../core/registry.ts"; -import { HTTPError, NotFoundError } from "../core/errors.ts"; import { normalizeLicense } from "../core/license.ts"; import { normalizeRepositoryURL } from "../core/repository.ts"; import { buildPURL } from "../core/purl.ts"; +import { rethrowFetchError } from "./error.ts"; /** Crates.io API response for a single crate. */ interface CratesPackageResponse { @@ -94,6 +94,10 @@ interface CratesMaintainer { url: string; } +function preferNonEmpty(value: string, fallback: string): string { + return value || fallback; +} + /** Crates.io registry client. */ export class CargoRegistry extends Registry { constructor(baseURL: string, client: Client) { @@ -115,7 +119,7 @@ export class CargoRegistry extends Registry { try { const data = await this.client.getJSON(url, signal); const crateData = data.crate; - const latestVersion = crateData.max_stable_version || crateData.max_version; + const latestVersion = preferNonEmpty(crateData.max_stable_version, crateData.max_version); const latestVersionData = data.versions.find((v) => v.num === latestVersion); return { @@ -141,10 +145,7 @@ export class CargoRegistry extends Registry { }, }; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("cargo", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -175,10 +176,7 @@ export class CargoRegistry extends Registry { return versions; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("cargo", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -214,10 +212,7 @@ export class CargoRegistry extends Registry { return dependencies; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("cargo", name, version); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name, version); } } @@ -241,10 +236,7 @@ export class CargoRegistry extends Registry { return maintainers; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("cargo", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } diff --git a/src/registries/error.ts b/src/registries/error.ts new file mode 100644 index 0000000..fe2cec3 --- /dev/null +++ b/src/registries/error.ts @@ -0,0 +1,13 @@ +import { HTTPError, NotFoundError } from "../core/errors.ts"; + +export function rethrowFetchError( + error: unknown, + ecosystem: string, + packageName: string, + version = "", +): never { + if (error instanceof HTTPError && error.isNotFound()) { + throw new NotFoundError(ecosystem, packageName, version); + } + throw error; +} diff --git a/src/registries/npm.ts b/src/registries/npm.ts index b7bb376..4f7bd8c 100644 --- a/src/registries/npm.ts +++ b/src/registries/npm.ts @@ -1,10 +1,16 @@ import type { Client } from "../core/client.ts"; import type { Dependency, Maintainer, Package, URLBuilder, Version } from "../core/types.ts"; import { Registry, register } from "../core/registry.ts"; -import { HTTPError, NotFoundError } from "../core/errors.ts"; import { normalizeLicense } from "../core/license.ts"; import { normalizeRepositoryURL } from "../core/repository.ts"; import { buildPURL } from "../core/purl.ts"; +import { rethrowFetchError } from "./error.ts"; + +interface NpmPerson { + readonly name?: string; + readonly email?: string; + readonly url?: string; +} /** npm registry API response for a single package. */ interface NpmPackageResponse { @@ -27,11 +33,7 @@ interface NpmPackageResponse { latest: string; }; versions: Record; - maintainers?: Array<{ - name?: string; - email?: string; - url?: string; - }>; + maintainers?: readonly NpmPerson[]; time?: Record; } @@ -46,21 +48,9 @@ interface NpmVersion { type?: string; }; keywords?: string[]; - author?: { - name?: string; - email?: string; - url?: string; - }; - contributors?: Array<{ - name?: string; - email?: string; - url?: string; - }>; - maintainers?: Array<{ - name?: string; - email?: string; - url?: string; - }>; + author?: NpmPerson; + contributors?: readonly NpmPerson[]; + maintainers?: readonly NpmPerson[]; dependencies?: Record; devDependencies?: Record; optionalDependencies?: Record; @@ -98,7 +88,9 @@ export class NpmRegistry extends Registry { const latestVersion = data["dist-tags"].latest; const latestVersionData = data.versions[latestVersion]; - const licenses = this.extractLicense(data.license || latestVersionData?.license); + const licenses = data.license + ? this.extractLicense(data.license) + : this.extractLicense(latestVersionData?.license); const namespace = this.extractNamespace(name); return { @@ -108,16 +100,13 @@ export class NpmRegistry extends Registry { documentation: "", repository: normalizeRepositoryURL(data.repository || ""), licenses, - keywords: data.keywords || [], + keywords: data.keywords ?? [], namespace, latestVersion, metadata: {}, }; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("npm", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -127,36 +116,28 @@ export class NpmRegistry extends Registry { try { const data = await this.client.getJSON(url, signal); - const versions: Version[] = []; - for (const [versionStr, versionData] of Object.entries(data.versions)) { + return Object.entries(data.versions).map(([versionStr, versionData]) => { const licenses = this.extractLicense(versionData.license); const publishedAt = data.time?.[versionStr] ? new Date(data.time[versionStr]) : null; - const status = versionData.deprecated ? "deprecated" : ""; - const integrity = versionData.dist?.integrity ? versionData.dist.integrity : versionData.dist?.shasum ? `sha1-${versionData.dist.shasum}` : ""; - versions.push({ + return { number: versionStr, publishedAt, licenses, integrity, status, metadata: {}, - }); - } - - return versions; + }; + }); } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("npm", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -170,69 +151,16 @@ export class NpmRegistry extends Registry { try { const versionData = await this.client.getJSON(url, signal); - - const dependencies: Dependency[] = []; - - // Runtime dependencies - if (versionData.dependencies) { - for (const [depName, requirements] of Object.entries(versionData.dependencies)) { - if ( - versionData.optionalDependencies && - Object.hasOwn(versionData.optionalDependencies, depName) - ) { - continue; - } - dependencies.push({ - name: depName, - requirements, - scope: "runtime", - optional: false, - }); - } - } - - // Development dependencies - if (versionData.devDependencies) { - for (const [depName, requirements] of Object.entries(versionData.devDependencies)) { - dependencies.push({ - name: depName, - requirements, - scope: "development", - optional: false, - }); - } - } - - // Optional dependencies - if (versionData.optionalDependencies) { - for (const [depName, requirements] of Object.entries(versionData.optionalDependencies)) { - dependencies.push({ - name: depName, - requirements, - scope: "runtime", - optional: true, - }); - } - } - - // Peer dependencies - if (versionData.peerDependencies) { - for (const [depName, requirements] of Object.entries(versionData.peerDependencies)) { - dependencies.push({ - name: depName, - requirements, - scope: "runtime", - optional: false, - }); - } - } - - return dependencies; + const optionalNames = Object.keys(versionData.optionalDependencies ?? {}); + + return [ + ...this.mapDependencies(versionData.dependencies, "runtime", false, optionalNames), + ...this.mapDependencies(versionData.devDependencies, "development", false), + ...this.mapDependencies(versionData.optionalDependencies, "runtime", true), + ...this.mapDependencies(versionData.peerDependencies, "runtime", false), + ]; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("npm", name, version); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name, version); } } @@ -242,68 +170,15 @@ export class NpmRegistry extends Registry { try { const data = await this.client.getJSON(url, signal); - const maintainers: Maintainer[] = []; - const seen = new Set(); - - // Use top-level maintainers (people with publish access) - if (data.maintainers) { - for (const maintainer of data.maintainers) { - const key = this.maintainerKey(maintainer.name, maintainer.email); - if (!key || seen.has(key)) continue; - seen.add(key); - maintainers.push({ - uuid: "", - login: maintainer.email ? maintainer.email.split("@")[0] : "", - name: maintainer.name || "", - email: maintainer.email || "", - url: maintainer.url || "", - role: "", - }); - } - } - - // Supplement with author and contributors from the latest version only - const latestTag = data["dist-tags"].latest; - const latestVersion = data.versions[latestTag]; - - if (latestVersion?.author) { - const key = this.maintainerKey(latestVersion.author.name, latestVersion.author.email); - if (key && !seen.has(key)) { - seen.add(key); - maintainers.push({ - uuid: "", - login: latestVersion.author.email ? latestVersion.author.email.split("@")[0] : "", - name: latestVersion.author.name || "", - email: latestVersion.author.email || "", - url: latestVersion.author.url || "", - role: "author", - }); - } - } - - if (latestVersion?.contributors) { - for (const contributor of latestVersion.contributors) { - const key = this.maintainerKey(contributor.name, contributor.email); - if (key && !seen.has(key)) { - seen.add(key); - maintainers.push({ - uuid: "", - login: contributor.email ? contributor.email.split("@")[0] : "", - name: contributor.name || "", - email: contributor.email || "", - url: contributor.url || "", - role: "contributor", - }); - } - } - } + const latestVersion = data.versions[data["dist-tags"].latest]; - return maintainers; + return this.collectMaintainers( + data.maintainers, + latestVersion?.author, + latestVersion?.contributors, + ); } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("npm", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -333,14 +208,62 @@ export class NpmRegistry extends Registry { }; } - /** Build a stable dedup key. Prefers email (unique per account), falls back to name. */ + private mapDependencies( + entries: Readonly> | undefined, + scope: Dependency["scope"], + optional: boolean, + excluded: readonly string[] = [], + ): Dependency[] { + const dependencies: Dependency[] = []; + + for (const [name, requirements] of Object.entries(entries ?? {})) { + if (excluded.includes(name)) continue; + dependencies.push({ name, requirements, scope, optional }); + } + + return dependencies; + } + + private collectMaintainers( + maintainers: readonly NpmPerson[] | undefined, + author: Readonly | undefined, + contributors: readonly NpmPerson[] | undefined, + ): Maintainer[] { + const candidates: Array = [ + ...(maintainers ?? []).map((maintainer) => [maintainer, ""] as const), + ...(author ? [[author, "author"] as const] : []), + ...(contributors ?? []).map((contributor) => [contributor, "contributor"] as const), + ]; + const seen = new Set(); + const result: Maintainer[] = []; + + for (const [maintainer, role] of candidates) { + const key = this.maintainerKey(maintainer.name, maintainer.email); + if (!key || seen.has(key)) continue; + seen.add(key); + result.push(this.toMaintainer(maintainer, role)); + } + + return result; + } + + private toMaintainer(maintainer: Readonly, role: string): Maintainer { + return { + uuid: "", + login: maintainer.email ? maintainer.email.split("@")[0] : "", + name: maintainer.name || "", + email: maintainer.email || "", + url: maintainer.url || "", + role, + }; + } + private maintainerKey(name: string | undefined, email: string | undefined): string { if (email) return email; if (name) return name; return ""; } - /** Encode package name for URL (handle scoped packages). */ private encodeName(name: string): string { if (name.startsWith("@")) { return name.replace("/", "%2F"); @@ -348,7 +271,6 @@ export class NpmRegistry extends Registry { return name; } - /** Extract namespace from scoped package name. */ private extractNamespace(name: string): string { if (name.startsWith("@")) { const parts = name.split("/"); @@ -357,8 +279,7 @@ export class NpmRegistry extends Registry { return ""; } - /** Extract and normalize license. */ - private extractLicense(raw: string | { type?: string } | undefined): string { + private extractLicense(raw: string | Readonly<{ type?: string }> | undefined): string { if (!raw) return ""; if (typeof raw === "string") { diff --git a/src/registries/packagist.ts b/src/registries/packagist.ts index badaafa..1de219b 100644 --- a/src/registries/packagist.ts +++ b/src/registries/packagist.ts @@ -1,10 +1,11 @@ import type { Client } from "../core/client.ts"; import type { Dependency, Maintainer, Package, URLBuilder, Version } from "../core/types.ts"; import { Registry, register } from "../core/registry.ts"; -import { HTTPError, NotFoundError, InvalidPURLError } from "../core/errors.ts"; +import { NotFoundError, InvalidPURLError } from "../core/errors.ts"; import { combineLicenses } from "../core/license.ts"; import { normalizeRepositoryURL } from "../core/repository.ts"; import { buildPURL } from "../core/purl.ts"; +import { rethrowFetchError } from "./error.ts"; /** Packagist API response for a single package. */ interface PackagistPackageResponse { @@ -17,18 +18,20 @@ interface PackagistPackageResponse { }; } +interface PackagistAuthor { + readonly name?: string; + readonly email?: string; + readonly homepage?: string; + readonly role?: string; +} + /** Packagist version data. */ interface PackagistVersion { name: string; version: string; license?: string | string[]; keywords?: string[]; - authors?: Array<{ - name?: string; - email?: string; - homepage?: string; - role?: string; - }>; + authors?: readonly PackagistAuthor[]; require?: Record; "require-dev"?: Record; source?: { @@ -82,19 +85,16 @@ export class PackagistRegistry extends Registry { homepage: "", documentation: "", repository: normalizeRepositoryURL( - packageData.repository || latestVersionData.source?.url || "", + packageData.repository ? packageData.repository : (latestVersionData.source?.url ?? ""), ), licenses, - keywords: packageData.keywords || [], + keywords: packageData.keywords ?? [], namespace: vendor, latestVersion, metadata: {}, }; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("composer", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -122,10 +122,7 @@ export class PackagistRegistry extends Registry { return versions; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("composer", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -177,10 +174,7 @@ export class PackagistRegistry extends Registry { return dependencies; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("composer", name, version); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name, version); } } @@ -190,35 +184,9 @@ export class PackagistRegistry extends Registry { try { const data = await this.client.getJSON(url, signal); - const maintainers: Maintainer[] = []; - const seen = new Set(); - - // Collect authors from all versions - for (const versionData of Object.values(data.package.versions)) { - if (versionData.authors) { - for (const author of versionData.authors) { - const key = `${author.name}:${author.email}`; - if (!seen.has(key)) { - seen.add(key); - maintainers.push({ - uuid: "", - login: author.email ? author.email.split("@")[0] : "", - name: author.name || "", - email: author.email || "", - url: author.homepage || "", - role: author.role || "", - }); - } - } - } - } - - return maintainers; + return this.collectMaintainers(Object.values(data.package.versions)); } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("composer", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -248,7 +216,35 @@ export class PackagistRegistry extends Registry { }; } - /** Parse "vendor/package" format. */ + private collectMaintainers( + versions: readonly Readonly>[], + ): Maintainer[] { + const maintainers: Maintainer[] = []; + const seen = new Set(); + + for (const version of versions) { + for (const author of version.authors ?? []) { + const key = `${author.name}:${author.email}`; + if (seen.has(key)) continue; + seen.add(key); + maintainers.push(this.toMaintainer(author)); + } + } + + return maintainers; + } + + private toMaintainer(author: Readonly): Maintainer { + return { + uuid: "", + login: author.email ? author.email.split("@")[0] : "", + name: author.name || "", + email: author.email || "", + url: author.homepage || "", + role: author.role || "", + }; + } + private parseName(name: string): [string, string] { const parts = name.split("/"); if (parts.length !== 2 || !parts[0] || !parts[1]) { @@ -260,8 +256,7 @@ export class PackagistRegistry extends Registry { return [parts[0]!, parts[1]!]; } - /** Find the highest non-dev version. */ - private findLatestVersion(versions: string[]): string { + private findLatestVersion(versions: readonly string[]): string { // Filter out dev versions const stable = versions.filter((v) => !v.startsWith("dev-") && !v.endsWith("-dev")); if (stable.length === 0) { @@ -294,15 +289,13 @@ export class PackagistRegistry extends Registry { ); } - /** Extract and normalize licenses. */ - private extractLicenses(raw: string | string[] | undefined): string { + private extractLicenses(raw: string | readonly string[] | undefined): string { if (!raw) return ""; const licenses = Array.isArray(raw) ? raw : [raw]; return combineLicenses(licenses); } - /** Skip PHP and extension dependencies. */ private shouldSkipDependency(name: string): boolean { return name === "php" || name.startsWith("ext-"); } diff --git a/src/registries/pypi.ts b/src/registries/pypi.ts index 3f5bb05..54c4946 100644 --- a/src/registries/pypi.ts +++ b/src/registries/pypi.ts @@ -1,10 +1,10 @@ import type { Client } from "../core/client.ts"; import type { Dependency, Maintainer, Package, URLBuilder, Version } from "../core/types.ts"; import { Registry, register } from "../core/registry.ts"; -import { HTTPError, NotFoundError } from "../core/errors.ts"; import { normalizeLicense } from "../core/license.ts"; import { normalizeRepositoryURL } from "../core/repository.ts"; import { buildPURL } from "../core/purl.ts"; +import { rethrowFetchError } from "./error.ts"; const PYPI_FILENAME_SEPARATORS = ["_", "-", "."] as const; @@ -46,12 +46,12 @@ interface PyPISimpleResponse { /** A single file entry in the Simple API response. */ interface PyPISimpleFile { - filename: string; - url: string; - hashes: { sha256?: string }; - "requires-python"?: string; - yanked?: string | false; - "upload-time"?: string; + readonly filename: string; + readonly url: string; + readonly hashes: Readonly<{ sha256?: string }>; + readonly "requires-python"?: string; + readonly yanked?: string | false; + readonly "upload-time"?: string; } /** PyPI registry client. */ @@ -96,10 +96,7 @@ export class PyPIRegistry extends Registry { metadata: {}, }; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("pypi", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -112,56 +109,12 @@ export class PyPIRegistry extends Registry { Accept: "application/vnd.pypi.simple.v1+json", }); - const filesByVersion = new Map(); - for (const version of data.versions) { - filesByVersion.set(version, []); - } - - for (const file of data.files) { - const version = this.matchFileVersion(file.filename, normalized, data.versions); - if (version) { - filesByVersion.get(version)!.push(file); - } - } - - const versions: Version[] = []; - for (const [versionStr, files] of filesByVersion) { - if (files.length === 0) { - versions.push({ - number: versionStr, - publishedAt: null, - licenses: "", - integrity: "", - status: "", - metadata: {}, - }); - continue; - } - - const sdist = files.find((f) => f.filename.endsWith(".tar.gz")); - const file = sdist ?? files[0]!; - const publishedAt = file["upload-time"] ? new Date(file["upload-time"]) : null; - const integrity = file.hashes?.sha256 ? `sha256-${file.hashes.sha256}` : ""; - const status = file.yanked ? "yanked" : ""; - - this.downloadUrls.set(`${normalized}@${versionStr}`, file.url); - - versions.push({ - number: versionStr, - publishedAt, - licenses: "", - integrity, - status, - metadata: {}, - }); - } - - return versions; + const filesByVersion = this.indexFilesByVersion(normalized, data.versions, data.files); + return [...filesByVersion].map(([version, files]) => + this.toVersion(normalized, version, files), + ); } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("pypi", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -188,10 +141,7 @@ export class PyPIRegistry extends Registry { return dependencies; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("pypi", name, version); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name, version); } } @@ -216,10 +166,7 @@ export class PyPIRegistry extends Registry { return maintainers; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("pypi", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -253,11 +200,60 @@ export class PyPIRegistry extends Registry { }; } - /** Match a filename to a version from the known versions list. */ + private indexFilesByVersion( + normalizedName: string, + versions: readonly string[], + files: readonly PyPISimpleFile[], + ): Map { + const filesByVersion = new Map(); + + for (const version of versions) filesByVersion.set(version, []); + for (const file of files) { + const version = this.matchFileVersion(file.filename, normalizedName, versions); + if (version) filesByVersion.get(version)!.push(file); + } + + return filesByVersion; + } + + private toVersion( + normalizedName: string, + version: string, + files: readonly PyPISimpleFile[], + ): Version { + if (files.length === 0) { + return { + number: version, + publishedAt: null, + licenses: "", + integrity: "", + status: "", + metadata: {}, + }; + } + + const sdist = files.find((file) => file.filename.endsWith(".tar.gz")); + const file = sdist ?? files[0]!; + const publishedAt = file["upload-time"] ? new Date(file["upload-time"]) : null; + const integrity = file.hashes?.sha256 ? `sha256-${file.hashes.sha256}` : ""; + const status = file.yanked ? "yanked" : ""; + + this.downloadUrls.set(`${normalizedName}@${version}`, file.url); + + return { + number: version, + publishedAt, + licenses: "", + integrity, + status, + metadata: {}, + }; + } + private matchFileVersion( filename: string, normalizedName: string, - versions: string[], + versions: readonly string[], ): string | undefined { const lower = filename.toLowerCase(); let prefix: string | undefined; @@ -289,13 +285,11 @@ export class PyPIRegistry extends Registry { return undefined; } - /** Normalize package name per PEP 503. */ private normalizeName(name: string): string { - return name.toLowerCase().replace(/[-_.]+/g, "-"); + return name.toLowerCase().replaceAll(/[-_.]+/g, "-"); } - /** Extract repository URL from project_urls. */ - private extractRepository(projectUrls: Record | undefined): string { + private extractRepository(projectUrls: Readonly> | undefined): string { const url = this.findProjectUrl(projectUrls, [ "Repository", "Source", @@ -306,8 +300,10 @@ export class PyPIRegistry extends Registry { return normalizeRepositoryURL(url); } - /** Find a project URL by key, case-insensitive. */ - private findProjectUrl(projectUrls: Record | undefined, keys: string[]): string { + private findProjectUrl( + projectUrls: Readonly> | undefined, + keys: readonly string[], + ): string { if (!projectUrls) return ""; const lowered = new Map(); @@ -323,7 +319,6 @@ export class PyPIRegistry extends Registry { return ""; } - /** Parse comma-separated keywords. */ private parseKeywords(keywords: string | undefined): string[] { if (!keywords) return []; return keywords @@ -332,7 +327,20 @@ export class PyPIRegistry extends Registry { .filter(Boolean); } - /** Parse PEP 508 dependency string into a normalized Dependency. */ + private parseExtraMarker(marker: string): Pick { + const extraMatch = marker.match(/extra\s*==\s*["']([^"']+)["']/); + if (!extraMatch) return { scope: "runtime", optional: false }; + + const extraName = extraMatch[1]!.toLowerCase(); + if (/^dev(elop(ment)?)?$/.test(extraName)) { + return { scope: "development", optional: true }; + } + if (/^test(s|ing)?$/.test(extraName)) { + return { scope: "test", optional: true }; + } + return { scope: "runtime", optional: true }; + } + private parsePEP508(depStr: string): Dependency | null { // PEP 508 format: name [extras] (version_spec) ; markers const semiIdx = depStr.indexOf(";"); @@ -351,28 +359,13 @@ export class PyPIRegistry extends Registry { versionSpec = versionSpec.slice(1, -1).trim(); } - // Only `extra == "..."` markers affect scope. Platform markers don't. - let scope: "runtime" | "development" | "test" | "build" | "optional" = "runtime"; - let optional = false; - - if (markerStr) { - const extraMatch = markerStr.match(/extra\s*==\s*["']([^"']+)["']/); - if (extraMatch) { - optional = true; - const extraName = extraMatch[1]!.toLowerCase(); - if (/^dev(elop(ment)?)?$/.test(extraName)) { - scope = "development"; - } else if (/^test(s|ing)?$/.test(extraName)) { - scope = "test"; - } - } - } + const marker = this.parseExtraMarker(markerStr); return { name: this.normalizeName(depName), requirements: versionSpec, - scope, - optional, + scope: marker.scope, + optional: marker.optional, }; } } diff --git a/src/registries/rubygems.ts b/src/registries/rubygems.ts index 9bd5e53..7afaecd 100644 --- a/src/registries/rubygems.ts +++ b/src/registries/rubygems.ts @@ -1,10 +1,10 @@ import type { Client } from "../core/client.ts"; import type { Dependency, Maintainer, Package, URLBuilder, Version } from "../core/types.ts"; import { Registry, register } from "../core/registry.ts"; -import { HTTPError, NotFoundError } from "../core/errors.ts"; import { combineLicenses, normalizeLicense } from "../core/license.ts"; import { normalizeRepositoryURL } from "../core/repository.ts"; import { buildPURL } from "../core/purl.ts"; +import { rethrowFetchError } from "./error.ts"; /** RubyGems API response for a single gem. */ interface RubyGemsGemResponse { @@ -42,6 +42,25 @@ interface RubyGemsOwnerResponse { email?: string; } +function orEmpty(value: string | undefined): string { + return value || ""; +} + +function gemLicenses(licenses: readonly string[] | undefined): string { + return licenses ? combineLicenses(licenses.map((license) => normalizeLicense(license))) : ""; +} + +function gemRepositoryURL( + sourceCodeURI: string | undefined, + metadata: Readonly> | undefined, + homepageURI: string | undefined, +): string { + const metadataURI = metadata?.["source_code_uri"]; + if (sourceCodeURI) return normalizeRepositoryURL(sourceCodeURI); + if (metadataURI) return normalizeRepositoryURL(metadataURI); + return normalizeRepositoryURL(homepageURI ?? ""); +} + /** RubyGems registry client. */ export class RubyGemsRegistry extends Registry { constructor(baseURL: string, client: Client) { @@ -63,36 +82,23 @@ export class RubyGemsRegistry extends Registry { try { const data = await this.client.getJSON(url, signal); - // Extract licenses - const licenses = data.licenses - ? combineLicenses(data.licenses.map((l) => normalizeLicense(l))) - : ""; - - // Extract repository URL - const repository = normalizeRepositoryURL( - data.source_code_uri || - (data.metadata?.source_code_uri as string) || - data.homepage_uri || - "", - ); + const licenses = gemLicenses(data.licenses); + const repository = gemRepositoryURL(data.source_code_uri, data.metadata, data.homepage_uri); return { name: data.name, - description: data.description || "", - homepage: data.homepage_uri || "", - documentation: data.documentation_uri || "", + description: orEmpty(data.description), + homepage: orEmpty(data.homepage_uri), + documentation: orEmpty(data.documentation_uri), repository, licenses, keywords: [], namespace: "", - latestVersion: data.version || "", - metadata: data.metadata || {}, + latestVersion: orEmpty(data.version), + metadata: data.metadata ?? {}, }; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("gem", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -119,10 +125,7 @@ export class RubyGemsRegistry extends Registry { return versions; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("gem", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } @@ -163,10 +166,7 @@ export class RubyGemsRegistry extends Registry { return dependencies; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("gem", name, version); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name, version); } } @@ -190,10 +190,7 @@ export class RubyGemsRegistry extends Registry { return maintainers; } catch (error) { - if (error instanceof HTTPError && error.isNotFound()) { - throw new NotFoundError("gem", name); - } - throw error; + rethrowFetchError(error, this.ecosystem(), name); } } diff --git a/test/e2e/client.test.ts b/test/e2e/client.test.ts index f61f000..c3ac5c2 100644 --- a/test/e2e/client.test.ts +++ b/test/e2e/client.test.ts @@ -22,7 +22,7 @@ describe("Client", () => { }); await new Promise((resolve, reject) => { - const onError = (error: Error) => reject(error); + const onError = (error: Readonly) => reject(error); server.once("error", onError); server.listen(0, "127.0.0.1", () => { server.off("error", onError); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..6e419bf --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": ".." + }, + "include": ["**/*.ts"], + "exclude": ["../node_modules", "../dist"] +} diff --git a/test/unit/alpm.test.ts b/test/unit/alpm.test.ts index 58e4dc7..e8ffa71 100644 --- a/test/unit/alpm.test.ts +++ b/test/unit/alpm.test.ts @@ -3,8 +3,7 @@ import { NotFoundError, HTTPError } from "../../src/core/errors.ts"; import { create } from "../../src/core/registry.ts"; import "../../src/registries/index.ts"; -/** Helper โ€” minimal Arch official search response. */ -function archSearchResponse(overrides: Record = {}) { +function archSearchResponse(overrides: Readonly> = {}) { return { results: [ { @@ -40,8 +39,7 @@ function archSearchResponse(overrides: Record = {}) { }; } -/** Helper โ€” minimal AUR info response. */ -function aurInfoResponse(overrides: Record = {}) { +function aurInfoResponse(overrides: Readonly> = {}) { return { resultcount: 1, type: "multiinfo", @@ -406,27 +404,24 @@ describe("alpm registry", () => { it("should normalize input casing and whitespace", async () => { const client = new Client(); - vi.spyOn(client, "getJSON").mockResolvedValueOnce(archSearchResponse()); + const getJSON = vi.spyOn(client, "getJSON").mockResolvedValueOnce(archSearchResponse()); const registry = create("alpm", undefined, client); const pkg = await registry.fetchPackage(" Arch/Pacman "); expect(pkg.name).toBe("pacman"); - expect(client.getJSON).toHaveBeenCalledWith( - expect.stringContaining("name=pacman"), - undefined, - ); + expect(getJSON).toHaveBeenCalledWith(expect.stringContaining("name=pacman"), undefined); }); it('should default to "arch" namespace when no slash in name', async () => { const client = new Client(); - vi.spyOn(client, "getJSON").mockResolvedValueOnce(archSearchResponse()); + const getJSON = vi.spyOn(client, "getJSON").mockResolvedValueOnce(archSearchResponse()); const registry = create("alpm", undefined, client); const pkg = await registry.fetchPackage("pacman"); expect(pkg.namespace).toBe("arch"); - expect(client.getJSON).toHaveBeenCalledWith( + expect(getJSON).toHaveBeenCalledWith( expect.stringContaining("archlinux.org/packages/search/json"), undefined, ); @@ -434,12 +429,12 @@ describe("alpm registry", () => { it('should route "aur/..." to AUR API', async () => { const client = new Client(); - vi.spyOn(client, "getJSON").mockResolvedValueOnce(aurInfoResponse()); + const getJSON = vi.spyOn(client, "getJSON").mockResolvedValueOnce(aurInfoResponse()); const registry = create("alpm", undefined, client); await registry.fetchPackage("aur/yay"); - expect(client.getJSON).toHaveBeenCalledWith( + expect(getJSON).toHaveBeenCalledWith( expect.stringContaining("aur.archlinux.org/rpc/v5/info"), undefined, ); diff --git a/test/unit/cached-registry.test.ts b/test/unit/cached-registry.test.ts index de30177..1710a82 100644 --- a/test/unit/cached-registry.test.ts +++ b/test/unit/cached-registry.test.ts @@ -3,7 +3,12 @@ import { readLockfile, cacheKey, DEFAULT_TTL } from "../../src/cache/lockfile.ts import { createHash } from "node:crypto"; import type { URLBuilder } from "../../src/core/types.ts"; import { Registry } from "../../src/core/registry.ts"; -import type { Lockfile } from "../../src/cache/lockfile.ts"; +import type { Lockfile, LockfileEntry } from "../../src/cache/lockfile.ts"; + +type ReadonlyLockfile = { + readonly version: number; + readonly entries: Readonly>>; +}; // Mock storage to avoid real file I/O vi.mock("../../src/cache/storage.ts", () => { @@ -35,12 +40,14 @@ vi.mock("../../src/cache/storage.ts", () => { let mockLockfile: Lockfile = { version: 1, entries: {} }; vi.mock("../../src/cache/lockfile.ts", async () => { - const actual = await vi.importActual("../../src/cache/lockfile.ts"); + const actual = await vi.importActual( + "../../src/cache/lockfile.ts", + ); return { ...actual, - readLockfile: async () => JSON.parse(JSON.stringify(mockLockfile)), - writeLockfile: async (lockfile: Lockfile) => { - mockLockfile = JSON.parse(JSON.stringify(lockfile)); + readLockfile: async () => structuredClone(mockLockfile), + writeLockfile: async (lockfile: ReadonlyLockfile) => { + mockLockfile = structuredClone(lockfile); }, }; }); @@ -59,6 +66,7 @@ function createMockRegistry(ecosystem: string, overrides?: Partial): R name: "test-package", description: "A test package", homepage: "https://example.com", + documentation: "", repository: "https://github.com/example/test", licenses: "MIT", keywords: ["test"], @@ -154,6 +162,7 @@ describe("CachedRegistry", () => { name: "lodash", description: "Utility library", homepage: "https://lodash.com", + documentation: "", repository: "https://github.com/lodash/lodash", licenses: "MIT", keywords: ["utility"], @@ -181,6 +190,7 @@ describe("CachedRegistry", () => { name: "react", description: "React library", homepage: "https://react.dev", + documentation: "", repository: "https://github.com/facebook/react", licenses: "MIT", keywords: ["ui"], @@ -209,6 +219,7 @@ describe("CachedRegistry", () => { name: "pkg", description: "Package", homepage: "", + documentation: "", repository: "", licenses: "MIT", keywords: [], @@ -241,6 +252,7 @@ describe("CachedRegistry", () => { name: "pkg", description: "Package", homepage: "", + documentation: "", repository: "", licenses: "MIT", keywords: [], @@ -555,6 +567,7 @@ describe("CachedRegistry", () => { name, description: `${name} package`, homepage: `https://${name}.com`, + documentation: "", repository: `https://github.com/${name}/${name}`, licenses: "MIT", keywords: [], @@ -581,6 +594,7 @@ describe("CachedRegistry", () => { name: "test", description: "Test", homepage: "", + documentation: "", repository: "", licenses: "MIT", keywords: [], @@ -606,6 +620,7 @@ describe("CachedRegistry", () => { name, description: "", homepage: "", + documentation: "", repository: "", licenses: "MIT", keywords: [], @@ -659,6 +674,7 @@ describe("CachedRegistry", () => { name: "lodash", description: "Utility library", homepage: "", + documentation: "", repository: "", licenses: "MIT", keywords: [], @@ -697,6 +713,7 @@ describe("CachedRegistry", () => { name: "pkg", description: "", homepage: "", + documentation: "", repository: "", licenses: "MIT", keywords: [], @@ -752,6 +769,7 @@ describe("CachedRegistry", () => { name: "pkg", description: "", homepage: "", + documentation: "", repository: "", licenses: "MIT", keywords: [], @@ -786,6 +804,7 @@ describe("CachedRegistry", () => { name: "pkg", description: "", homepage: "", + documentation: "", repository: "", licenses: "MIT", keywords: [], @@ -877,6 +896,7 @@ describe("CachedRegistry", () => { name: "pkg", description: "Package", homepage: "", + documentation: "", repository: "", licenses: "MIT", keywords: [], @@ -911,6 +931,7 @@ describe("CachedRegistry", () => { name, description: "", homepage: "", + documentation: "", repository: "", licenses: "MIT", keywords: [], diff --git a/test/unit/helpers.test.ts b/test/unit/helpers.test.ts index 2d0248b..ce041c8 100644 --- a/test/unit/helpers.test.ts +++ b/test/unit/helpers.test.ts @@ -24,7 +24,9 @@ function version( }; } -function pkg(overrides: Partial = {}): Package { +function pkg( + overrides: Readonly>> = {}, +): Package { return { name: "test", description: "", diff --git a/test/unit/info-command.test.ts b/test/unit/info-command.test.ts index b9358ca..941ab7f 100644 --- a/test/unit/info-command.test.ts +++ b/test/unit/info-command.test.ts @@ -3,7 +3,16 @@ import type { Package } from "../../src/core/types.ts"; import type { Registry } from "../../src/core/registry.ts"; import { outputPackageInfo } from "../../src/commands/info.ts"; -function createPackage(overrides: Partial = {}): Package { +function createPackage( + overrides: Readonly< + Partial< + Pick< + Package, + "documentation" | "homepage" | "latestVersion" | "licenses" | "name" | "repository" + > + > + > = {}, +): Package { return { name: "serde", description: "Serialization framework", @@ -36,8 +45,8 @@ function createRegistry(registryUrl: string, docsUrl: string): Registry { }; } -function loggedLines(log: ReturnType): string[] { - return log.mock.calls.map(([message]) => String(message)); +function loggedLines(calls: readonly (readonly unknown[])[]): string[] { + return calls.map(([message]) => String(message)); } describe("outputPackageInfo", () => { @@ -59,7 +68,9 @@ describe("outputPackageInfo", () => { createRegistry("https://crates.io/crates/serde", "https://docs.rs/serde/1.0.220"), ); - expect(loggedLines(log)).toContain(" \x1b[90mDocs:\x1b[0m https://docs.rs/serde"); + expect(loggedLines(log.mock.calls)).toContain( + " \x1B[90mDocs:\x1B[0m https://docs.rs/serde", + ); }); it("skips docs when the resolved url falls back to homepage", () => { @@ -76,7 +87,7 @@ describe("outputPackageInfo", () => { createRegistry("https://pypi.org/project/requests", "https://pypi.org/project/requests"), ); - expect(loggedLines(log).some((line) => line.includes("Docs:"))).toBe(false); + expect(loggedLines(log.mock.calls).some((line) => line.includes("Docs:"))).toBe(false); }); it("skips docs when the fallback matches the registry page", () => { @@ -96,7 +107,7 @@ describe("outputPackageInfo", () => { ), ); - expect(loggedLines(log).some((line) => line.includes("Docs:"))).toBe(false); + expect(loggedLines(log.mock.calls).some((line) => line.includes("Docs:"))).toBe(false); }); it("skips docs when documentation matches repository", () => { @@ -112,6 +123,6 @@ describe("outputPackageInfo", () => { createRegistry("https://crates.io/crates/serde", "https://docs.rs/serde/1.0.220"), ); - expect(loggedLines(log).some((line) => line.includes("Docs:"))).toBe(false); + expect(loggedLines(log.mock.calls).some((line) => line.includes("Docs:"))).toBe(false); }); }); diff --git a/test/unit/registry.test.ts b/test/unit/registry.test.ts index d8bf3d9..5f936a4 100644 --- a/test/unit/registry.test.ts +++ b/test/unit/registry.test.ts @@ -3,14 +3,14 @@ import { UnknownEcosystemError } from "../../src/core/errors.ts"; import { Registry, create, ecosystems, has, register } from "../../src/core/registry.ts"; class TestRegistry extends Registry { - constructor(baseURL: string, client: Client) { + constructor(baseURL: string, client: unknown) { super(); this.baseURL = baseURL; this.client = client; } readonly baseURL: string; - readonly client: Client; + readonly client: unknown; ecosystem(): string { return "test"; @@ -108,8 +108,14 @@ describe("registry", () => { }); it("should include the ecosystem in an unknown-ecosystem error", () => { - expect(() => create("unknown-eco")).toThrow( - expect.objectContaining({ ecosystem: "unknown-eco" }), - ); + expect.assertions(2); + + try { + create("unknown-eco"); + } catch (error) { + expect(error).toBeInstanceOf(UnknownEcosystemError); + if (!(error instanceof UnknownEcosystemError)) throw error; + expect(error.ecosystem).toBe("unknown-eco"); + } }); });