diff --git a/app/composables/usePackageComparison.ts b/app/composables/usePackageComparison.ts index c0798bc11d..568e24787d 100644 --- a/app/composables/usePackageComparison.ts +++ b/app/composables/usePackageComparison.ts @@ -1,3 +1,4 @@ +import { normalizeLicense } from '#shared/utils/npm' import { getDependencyCount } from '~/utils/npm/dependency-count' /** Special identifier for the "What Would James Do?" comparison column */ @@ -193,10 +194,7 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter) { severity: vulnsSeverity, }, metadata: { - license: - typeof pkgData.license === 'object' && 'type' in pkgData.license - ? pkgData.license.type - : pkgData.license, + license: normalizeLicense(pkgData.license), // Use version-specific publish time, NOT time.modified (which can be // updated by metadata changes like maintainer additions) lastUpdated: pkgData.time?.[latestVersion], diff --git a/server/api/registry/badge/[type]/[...pkg].get.ts b/server/api/registry/badge/[type]/[...pkg].get.ts index b99e22ddfe..bfd1bce60c 100644 --- a/server/api/registry/badge/[type]/[...pkg].get.ts +++ b/server/api/registry/badge/[type]/[...pkg].get.ts @@ -5,7 +5,7 @@ import { createError, getRouterParam, getQuery, setHeader } from 'h3' import { PackageRouteParamsSchema } from '#shared/schemas/package' import { CACHE_MAX_AGE_ONE_HOUR, ERROR_NPM_FETCH_FAILED } from '#shared/utils/constants' import { fetchNpmPackage } from '#server/utils/npm' -import { assertValidPackageName } from '#shared/utils/npm' +import { assertValidPackageName, normalizeLicense } from '#shared/utils/npm' import { fetchPackageWithTypesAndFiles } from '#server/utils/file-tree' import { handleApiError } from '#server/utils/error-handler' @@ -528,7 +528,7 @@ const badgeStrategies = { 'license': async (pkgData: globalThis.Packument) => { const latest = getLatestVersion(pkgData) const versionData = latest ? pkgData.versions?.[latest] : undefined - const value = versionData?.license ?? 'unknown' + const value = normalizeLicense(versionData?.license) ?? 'unknown' return { label: 'license', value, color: COLORS.green } }, diff --git a/server/api/registry/package-meta/[...pkg].get.ts b/server/api/registry/package-meta/[...pkg].get.ts index d627b1cbc1..fca3910fc1 100644 --- a/server/api/registry/package-meta/[...pkg].get.ts +++ b/server/api/registry/package-meta/[...pkg].get.ts @@ -1,3 +1,5 @@ +import { normalizeLicense } from '#shared/utils/npm' + /** * Returns lightweight package metadata for search results. * @@ -66,14 +68,7 @@ export default defineCachedEventHandler( author = typeof a === 'string' ? { name: a } : { name: a.name, email: a.email, url: a.url } } - // Normalize license to a string - // TODO: @npm/types types license as string, but some old packages use - // the deprecated { type, url } object format - const license = packument.license - ? typeof packument.license === 'string' - ? packument.license - : (packument.license as { type: string }).type - : undefined + const license = normalizeLicense(packument.license) return { name: packument.name, diff --git a/server/api/registry/timeline/[...pkg].get.ts b/server/api/registry/timeline/[...pkg].get.ts index aa4455513f..948decc7bf 100644 --- a/server/api/registry/timeline/[...pkg].get.ts +++ b/server/api/registry/timeline/[...pkg].get.ts @@ -1,3 +1,4 @@ +import { normalizeLicense } from '#shared/utils/npm' import { hasBuiltInTypes } from '~~/shared/utils/package-analysis' const DEFAULT_LIMIT = 25 @@ -68,15 +69,11 @@ export default defineCachedEventHandler( .filter(v => packument.time[v]) .map(v => { const version = packument.versions[v]! - let license = version.license - if (license && typeof license === 'object' && 'type' in license) { - license = (license as { type: string }).type - } return { version: v, time: packument.time[v]!, - license: typeof license === 'string' ? license : undefined, + license: normalizeLicense(version.license), type: typeof version.type === 'string' ? version.type : undefined, hasTypes: hasBuiltInTypes(version) || undefined, hasTrustedPublisher: version._npmUser?.trustedPublisher ? true : undefined, diff --git a/test/unit/server/api/registry/badge/pkg.get.spec.ts b/test/unit/server/api/registry/badge/pkg.get.spec.ts new file mode 100644 index 0000000000..ba15e4f2a2 --- /dev/null +++ b/test/unit/server/api/registry/badge/pkg.get.spec.ts @@ -0,0 +1,61 @@ +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import type { H3Event } from 'h3' +import type { Packument } from '#shared/types/npm-registry' +import { parsePackageParams } from '#server/utils/parse-package-params' + +const fetchNpmPackageMock = vi.hoisted(() => vi.fn()) +const getRouterParamMock = vi.hoisted(() => vi.fn()) +const getQueryMock = vi.hoisted(() => vi.fn()) +const setHeaderMock = vi.hoisted(() => vi.fn()) + +vi.mock('#server/utils/npm', () => ({ + fetchNpmPackage: fetchNpmPackageMock, +})) + +vi.mock('h3', () => ({ + createError: vi.fn((options: unknown) => options), + getRouterParam: getRouterParamMock, + getQuery: getQueryMock, + setHeader: setHeaderMock, +})) + +vi.stubGlobal('defineCachedEventHandler', (fn: Function) => fn) +vi.stubGlobal('parsePackageParams', parsePackageParams) + +const handler = (await import('#server/api/registry/badge/[type]/[...pkg].get')).default + +const fakeEvent = {} as H3Event + +afterAll(() => { + vi.unstubAllGlobals() +}) + +describe('badge API', () => { + beforeEach(() => { + vi.clearAllMocks() + getQueryMock.mockReturnValue({}) + getRouterParamMock.mockImplementation((_event: unknown, name: string) => { + if (name === 'type') return 'license' + if (name === 'pkg') return 'my-pkg' + return undefined + }) + }) + + it('normalizes object-shaped licenses before rendering SVG', async () => { + fetchNpmPackageMock.mockResolvedValue({ + '_id': 'my-pkg', + '_rev': '1', + 'name': 'my-pkg', + 'dist-tags': { latest: '1.0.0' }, + 'versions': { + '1.0.0': { version: '1.0.0', license: { type: 'MIT' } }, + }, + 'time': {}, + } as unknown as Packument) + + const svg = await handler(fakeEvent) + + expect(svg).toContain('>MIT<') + expect(svg).not.toContain('[object Object]') + }) +})