Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions app/composables/usePackageComparison.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -193,10 +194,7 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
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],
Expand Down
4 changes: 2 additions & 2 deletions server/api/registry/badge/[type]/[...pkg].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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 }
},

Expand Down
11 changes: 3 additions & 8 deletions server/api/registry/package-meta/[...pkg].get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { normalizeLicense } from '#shared/utils/npm'

/**
* Returns lightweight package metadata for search results.
*
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 2 additions & 5 deletions server/api/registry/timeline/[...pkg].get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { normalizeLicense } from '#shared/utils/npm'
import { hasBuiltInTypes } from '~~/shared/utils/package-analysis'

const DEFAULT_LIMIT = 25
Expand Down Expand Up @@ -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,
Expand Down
61 changes: 61 additions & 0 deletions test/unit/server/api/registry/badge/pkg.get.spec.ts
Original file line number Diff line number Diff line change
@@ -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]')
})
})
Loading