diff --git a/server/utils/readme-loaders.ts b/server/utils/readme-loaders.ts index 8af25d7a9..116e4a1a1 100644 --- a/server/utils/readme-loaders.ts +++ b/server/utils/readme-loaders.ts @@ -1,6 +1,10 @@ import * as v from 'valibot' import { PackageRouteParamsSchema } from '#shared/schemas/package' -import { CACHE_MAX_AGE_ONE_HOUR, NPM_MISSING_README_SENTINEL } from '#shared/utils/constants' +import { + CACHE_MAX_AGE_ONE_HOUR, + NPM_MISSING_README_SENTINEL, + NPM_README_TRUNCATION_THRESHOLD, +} from '#shared/utils/constants' /** Standard README filenames to try when fetching from jsdelivr (case-sensitive CDN) */ const standardReadmeFilenames = [ @@ -75,11 +79,16 @@ export const resolvePackageReadmeSource = defineCachedFunction( const hasValidNpmReadme = readmeContent && readmeContent !== NPM_MISSING_README_SENTINEL - if (!hasValidNpmReadme || !isStandardReadme(readmeFilename)) { + if ( + !hasValidNpmReadme || + !isStandardReadme(readmeFilename) || + readmeContent!.length >= NPM_README_TRUNCATION_THRESHOLD + ) { + const resolvedVersion = version ?? packageData['dist-tags']?.latest const jsdelivrReadme = await fetchReadmeFromJsdelivr( packageName, standardReadmeFilenames, - version, + resolvedVersion, ) if (jsdelivrReadme) { readmeContent = jsdelivrReadme diff --git a/shared/utils/constants.ts b/shared/utils/constants.ts index 7ef81ff83..1d3575673 100644 --- a/shared/utils/constants.ts +++ b/shared/utils/constants.ts @@ -21,6 +21,8 @@ export const ERROR_PACKAGE_REQUIREMENTS_FAILED = export const ERROR_FILE_LIST_FETCH_FAILED = 'Failed to fetch file list.' export const ERROR_CALC_INSTALL_SIZE_FAILED = 'Failed to calculate install size.' export const NPM_MISSING_README_SENTINEL = 'ERROR: No README data found!' +/** The npm registry truncates the packument readme field at 65,536 characters (2^16) */ +export const NPM_README_TRUNCATION_THRESHOLD = 64_000 export const ERROR_JSR_FETCH_FAILED = 'Failed to fetch package from JSR registry.' export const ERROR_NPM_FETCH_FAILED = 'Failed to fetch package from npm registry.' export const ERROR_PROVENANCE_FETCH_FAILED = 'Failed to fetch provenance.' diff --git a/test/unit/server/utils/readme-loaders.spec.ts b/test/unit/server/utils/readme-loaders.spec.ts index 8fb2ce818..1cc237e2b 100644 --- a/test/unit/server/utils/readme-loaders.spec.ts +++ b/test/unit/server/utils/readme-loaders.spec.ts @@ -214,6 +214,29 @@ describe('resolvePackageReadmeSource', () => { }) }) + it('fetches from jsdelivr when packument readme exceeds truncation threshold', async () => { + const truncatedReadme = 'x'.repeat(64_000) + const fullReadme = 'x'.repeat(80_000) + fetchNpmPackageMock.mockResolvedValue({ + 'readme': truncatedReadme, + 'readmeFilename': 'README.md', + 'repository': undefined, + 'versions': {}, + 'dist-tags': { latest: '1.0.0' }, + }) + parseRepositoryInfoMock.mockReturnValue(undefined) + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + text: async () => fullReadme, + }) + vi.stubGlobal('fetch', fetchMock) + + const result = await resolvePackageReadmeSource('pkg') + + expect(result).toMatchObject({ markdown: fullReadme }) + expect(fetchMock).toHaveBeenCalled() + }) + it('uses package repository for repoInfo when markdown is present', async () => { fetchNpmPackageMock.mockResolvedValue({ readme: '# Hi',