diff --git a/src/core/types.ts b/src/core/types.ts index 35502f0..ceb2fe9 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -50,6 +50,7 @@ export interface URLBuilder { registry(name: string, version?: string): string download(name: string, version: string): string documentation(name: string, version?: string): string + readme(name: string, version?: string): string purl(name: string, version?: string): string } diff --git a/src/helpers.ts b/src/helpers.ts index dbf644a..b6c12b1 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -106,3 +106,12 @@ export function selectVersion(versions: Version[], options?: { 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. + * + * Returns the ecosystem-specific URL where the package README can be fetched. + */ +export function resolveReadmeUrl(pkg: Package, urls: URLBuilder, version?: string): string { + return urls.readme(pkg.name, version) +} diff --git a/src/index.ts b/src/index.ts index c2d35f2..47d5e81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,7 @@ export { bulkFetchPackages, selectVersion, resolveDocsUrl, + resolveReadmeUrl, } from './helpers.ts' // Types diff --git a/src/registries/cargo.ts b/src/registries/cargo.ts index 57150f5..32fd3df 100644 --- a/src/registries/cargo.ts +++ b/src/registries/cargo.ts @@ -272,6 +272,11 @@ class CargoRegistry implements Registry { documentation: (name: string, version?: string) => { return version ? `https://docs.rs/${name}/${version}` : `https://docs.rs/${name}` }, + readme: (name: string, version?: string) => { + return version + ? `https://crates.io/api/v1/crates/${name}/${version}/readme` + : `https://crates.io/crates/${name}` + }, purl: (name: string, version?: string) => { const versionSuffix = version ? `@${version}` : '' return `pkg:cargo/${name}${versionSuffix}` diff --git a/src/registries/npm.ts b/src/registries/npm.ts index 7d01d5d..5f08014 100644 --- a/src/registries/npm.ts +++ b/src/registries/npm.ts @@ -325,6 +325,10 @@ class NpmRegistry implements Registry { documentation: (name: string, _version?: string) => { return `https://www.npmjs.com/package/${name}` }, + readme: (name: string, version?: string) => { + const ver = version ? `@${version}` : '' + return `https://cdn.jsdelivr.net/npm/${name}${ver}/README.md` + }, purl: (name: string, version?: string) => { const versionSuffix = version ? `@${version}` : '' return `pkg:npm/${name}${versionSuffix}` diff --git a/src/registries/packagist.ts b/src/registries/packagist.ts index 9ccfd50..5f1430e 100644 --- a/src/registries/packagist.ts +++ b/src/registries/packagist.ts @@ -248,6 +248,10 @@ class PackagistRegistry implements Registry { const [vendor, pkg] = this.parseName(name) return `https://packagist.org/packages/${vendor}/${pkg}` }, + readme: (name: string, _version?: string) => { + const [vendor, pkg] = this.parseName(name) + return `https://packagist.org/packages/${vendor}/${pkg}` + }, purl: (name: string, version?: string) => { const versionSuffix = version ? `@${version}` : '' return `pkg:composer/${name}${versionSuffix}` diff --git a/src/registries/pypi.ts b/src/registries/pypi.ts index 66edd80..f726bf7 100644 --- a/src/registries/pypi.ts +++ b/src/registries/pypi.ts @@ -220,6 +220,12 @@ class PyPIRegistry implements Registry { const normalized = this.normalizeName(name) return `https://pypi.org/project/${normalized}` }, + readme: (name: string, version?: string) => { + const normalized = this.normalizeName(name) + return version + ? `https://pypi.org/project/${normalized}/${version}/` + : `https://pypi.org/project/${normalized}/` + }, purl: (name: string, version?: string) => { const normalized = this.normalizeName(name) const versionSuffix = version ? `@${version}` : '' diff --git a/src/registries/rubygems.ts b/src/registries/rubygems.ts index b41680f..935779b 100644 --- a/src/registries/rubygems.ts +++ b/src/registries/rubygems.ts @@ -218,6 +218,10 @@ class RubyGemsRegistry implements Registry { const versionSuffix = version ? `/${version}` : '' return `https://www.rubydoc.info/gems/${name}${versionSuffix}` }, + readme: (name: string, version?: string) => { + const base = `https://rubygems.org/gems/${name}` + return version ? `${base}/versions/${version}` : base + }, purl: (name: string, version?: string) => { const versionSuffix = version ? `@${version}` : '' return `pkg:gem/${name}${versionSuffix}` diff --git a/test/unit/cached-registry.test.ts b/test/unit/cached-registry.test.ts index f08ac3d..e00e657 100644 --- a/test/unit/cached-registry.test.ts +++ b/test/unit/cached-registry.test.ts @@ -52,6 +52,7 @@ function createMockRegistry(ecosystem: string, overrides?: Partial): R registry: () => `https://${ecosystem}.example.com`, download: () => `https://${ecosystem}.example.com/download`, documentation: () => `https://${ecosystem}.example.com/docs`, + readme: () => `https://${ecosystem}.example.com/readme`, purl: () => `pkg:${ecosystem}/`, }), fetchPackage: async () => ({ diff --git a/test/unit/helpers.test.ts b/test/unit/helpers.test.ts index 1d04671..a18fe8e 100644 --- a/test/unit/helpers.test.ts +++ b/test/unit/helpers.test.ts @@ -1,5 +1,5 @@ import type { Package, Version, URLBuilder } from '../../src/core/types.ts' -import { selectVersion, resolveDocsUrl } from '../../src/helpers.ts' +import { selectVersion, resolveDocsUrl, resolveReadmeUrl } from '../../src/helpers.ts' function version(num: string, status: '' | 'yanked' | 'deprecated' | 'retracted' = '', date = '2024-01-01T00:00:00Z'): Version { return { @@ -32,6 +32,7 @@ const stubUrls: URLBuilder = { registry: () => '', download: () => '', documentation: (name: string, ver?: string) => `https://docs.example.com/${name}/${ver ?? 'latest'}`, + readme: (name: string, ver?: string) => `https://readme.example.com/${name}/${ver ?? 'latest'}`, purl: () => '', } @@ -135,3 +136,20 @@ describe('resolveDocsUrl', () => { expect(resolveDocsUrl(p, stubUrls)).toBe('https://docs.rs/serde') }) }) + +describe('resolveReadmeUrl', () => { + it('returns ecosystem-specific readme URL with version', () => { + const p = pkg() + expect(resolveReadmeUrl(p, stubUrls, '1.0.0')).toBe('https://readme.example.com/test/1.0.0') + }) + + it('returns ecosystem-specific readme URL without version', () => { + const p = pkg() + expect(resolveReadmeUrl(p, stubUrls)).toBe('https://readme.example.com/test/latest') + }) + + it('uses package name for URL construction', () => { + const p = pkg({ name: 'serde' }) + expect(resolveReadmeUrl(p, stubUrls, '1.0.220')).toBe('https://readme.example.com/serde/1.0.220') + }) +}) diff --git a/test/unit/registry.test.ts b/test/unit/registry.test.ts index 35920d5..8fd137d 100644 --- a/test/unit/registry.test.ts +++ b/test/unit/registry.test.ts @@ -26,6 +26,7 @@ describe('registry', () => { registry: () => '', download: () => '', documentation: () => '', + readme: () => '', purl: () => '', }), }) @@ -55,6 +56,7 @@ describe('registry', () => { registry: () => '', download: () => '', documentation: () => '', + readme: () => '', purl: () => '', }), }) @@ -87,6 +89,7 @@ describe('registry', () => { registry: () => '', download: () => '', documentation: () => '', + readme: () => '', purl: () => '', }), }) @@ -134,6 +137,7 @@ describe('registry', () => { registry: () => baseURL, download: () => '', documentation: () => '', + readme: () => '', purl: () => '', }), }) @@ -164,6 +168,7 @@ describe('registry', () => { registry: () => baseURL, download: () => '', documentation: () => '', + readme: () => '', purl: () => '', }), }) @@ -196,6 +201,7 @@ describe('registry', () => { registry: () => '', download: () => '', documentation: () => '', + readme: () => '', purl: () => '', }), }) @@ -236,6 +242,7 @@ describe('registry', () => { registry: () => '', download: () => '', documentation: () => '', + readme: () => '', purl: () => '', }), }) @@ -269,6 +276,7 @@ describe('registry', () => { registry: () => '', download: () => '', documentation: () => '', + readme: () => '', purl: () => '', }), }) @@ -302,6 +310,7 @@ describe('registry', () => { registry: () => '', download: () => '', documentation: () => '', + readme: () => '', purl: () => '', }), })