Skip to content

fix: don't flag a license change when there is no previous version#2792

Draft
jp-knj wants to merge 1 commit into
npmx-dev:mainfrom
jp-knj:fix/2720-license-change-no-previous-version
Draft

fix: don't flag a license change when there is no previous version#2792
jp-knj wants to merge 1 commit into
npmx-dev:mainfrom
jp-knj:fix/2720-license-change-no-previous-version

Conversation

@jp-knj
Copy link
Copy Markdown

@jp-knj jp-knj commented May 25, 2026

Fixes #2720.

Summary

Fix false-positive license change warnings for packages that have no previous version.

Added unit tests for the new-package case and normal license-change comparisons.

Test plan

  • New unit tests at test/unit/server/api/registry/license-change/pkg.get.spec.ts
  • pnpm lint clean
  • pnpm test:types passes
  • Manual: /package/vsxtools/v/0.0.1 no longer shows a license-change warning; a multi-version package with an actual license change still does

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment May 25, 2026 2:23pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview May 25, 2026 2:23pm
npmx-lunaria Ignored Ignored May 25, 2026 2:23pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes the license-change endpoint to only report license changes when a valid prior version exists, addressing a bug where single-version packages incorrectly showed change warnings. The handler is guarded with a version-index check, and comprehensive tests validate the corrected behaviour across multiple scenarios.

Changes

License-change handler fix and validation

Layer / File(s) Summary
Handler guard for valid prior-version comparison
server/api/registry/license-change/[...pkg].get.ts
The handler wraps licence extraction and change assignment in a conditional block that only executes when currentVersionIndex > 0, preventing invalid comparisons for first versions or missing indices.
Test suite for license-change endpoint
test/unit/server/api/registry/license-change/pkg.get.spec.ts
Comprehensive Vitest suite that stubs H3 routing helpers, mocks fetchNpmPackage, and covers eight behavioural cases: missing package param (400), null change for new single-version packages, licence change detection, unchanged licence, default latest-version comparison, explicit predecessor lookup via version query param, oldest-version handling, and missing-version handling.

Suggested reviewers

  • ghostdevv
  • 43081j
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately and concisely describes the main change: preventing false license change warnings when no previous version exists.
Linked Issues check ✅ Passed The code changes fully address issue #2720 by guarding licence comparisons with currentVersionIndex > 0, preventing false positives for single-version packages.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing issue #2720: the handler fix and comprehensive unit tests covering the regression and related edge cases.
Description check ✅ Passed The pull request description is directly related to the changeset, explaining the bug fix for false-positive license change warnings and referencing the linked issue #2720.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Hello! Thank you for opening your first PR to npmx, @jp-knj! 🚀

Here’s what will happen next:

  1. Our GitHub bots will run to check your changes.
    If they spot any issues you will see some error messages on this PR.
    Don’t hesitate to ask any questions if you’re not sure what these mean!

  2. In a few minutes, you’ll be able to see a preview of your changes on Vercel

  3. One or more of our maintainers will take a look and may ask you to make changes.
    We try to be responsive, but don’t worry if this takes a few days.

@jp-knj jp-knj marked this pull request as draft May 25, 2026 13:26
@codecov
Copy link
Copy Markdown

codecov Bot commented May 25, 2026

Codecov Report

❌ Patch coverage is 55.55556% with 4 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/api/registry/license-change/[...pkg].get.ts 60.00% 0 Missing and 2 partials ⚠️
shared/utils/npm.ts 50.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/api/registry/license-change/`[...pkg].get.ts:
- Around line 50-51: Normalize license values the same way as the timeline
handler: instead of using String(...) for currentLicense and previousLicense,
detect if versions[currentVersionIndex]?.license or
versions[previousVersionIndex]?.license is an object and, if so, use its .type
property (fallback to 'UNKNOWN' if missing); otherwise use the string value.
Update the assignments that set currentLicense and previousLicense (referencing
versions, currentVersionIndex, previousVersionIndex) to perform this object
check and extraction so change detection reports the actual license.type rather
than "[object Object]".

In `@test/unit/server/api/registry/license-change/pkg.get.spec.ts`:
- Around line 37-190: Add a new test in this spec to exercise object-shaped
licenses: create a case (similar to the suggested snippet) that sets routerParam
= 'my-pkg', mocks fetchNpmPackageMock via makePackument to return versions where
license values are objects (e.g. { type: 'MIT' } and { type: 'Apache-2.0', url:
'...' }), call handler(fakeEvent), and assert the returned change equals { from:
'MIT', to: 'Apache-2.0' }; this ensures the handler's license normalization
logic (used when reading packument versions) correctly extracts the type field
from object licenses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c0f1dfca-89e7-461a-88fa-6539b5ef9d3d

📥 Commits

Reviewing files that changed from the base of the PR and between 113c2dd and ed87967.

📒 Files selected for processing (2)
  • server/api/registry/license-change/[...pkg].get.ts
  • test/unit/server/api/registry/license-change/pkg.get.spec.ts

Comment on lines +50 to +51
const currentLicense = String(versions[currentVersionIndex]?.license || 'UNKNOWN')
const previousLicense = String(versions[previousVersionIndex]?.license || 'UNKNOWN')
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle license objects consistently with the timeline handler.

The String() conversion doesn't properly handle licenses that are objects (e.g., { type: 'MIT', url: '...' }). When license is an object, String(license) produces "[object Object]" rather than extracting the type field. This could result in incorrect change detection or misleading from/to values.

The timeline handler (server/api/registry/timeline/[...pkg].get.ts:66-69) already implements the correct normalization pattern: check if the license is an object and extract the .type property.

🔧 Proposed fix to normalize license objects
 if (currentVersionIndex > 0) {
-  const currentLicense = String(versions[currentVersionIndex]?.license || 'UNKNOWN')
-  const previousLicense = String(versions[previousVersionIndex]?.license || 'UNKNOWN')
+  let currentLicense = versions[currentVersionIndex]?.license
+  if (currentLicense && typeof currentLicense === 'object' && 'type' in currentLicense) {
+    currentLicense = currentLicense.type
+  }
+  const currentLicenseStr = typeof currentLicense === 'string' ? currentLicense : 'UNKNOWN'
+
+  let previousLicense = versions[previousVersionIndex]?.license
+  if (previousLicense && typeof previousLicense === 'object' && 'type' in previousLicense) {
+    previousLicense = previousLicense.type
+  }
+  const previousLicenseStr = typeof previousLicense === 'string' ? previousLicense : 'UNKNOWN'

-  if (currentLicense !== previousLicense) {
+  if (currentLicenseStr !== previousLicenseStr) {
     change = {
-      from: previousLicense,
-      to: currentLicense,
+      from: previousLicenseStr,
+      to: currentLicenseStr,
     }
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const currentLicense = String(versions[currentVersionIndex]?.license || 'UNKNOWN')
const previousLicense = String(versions[previousVersionIndex]?.license || 'UNKNOWN')
let currentLicense = versions[currentVersionIndex]?.license
if (currentLicense && typeof currentLicense === 'object' && 'type' in currentLicense) {
currentLicense = currentLicense.type
}
const currentLicenseStr = typeof currentLicense === 'string' ? currentLicense : 'UNKNOWN'
let previousLicense = versions[previousVersionIndex]?.license
if (previousLicense && typeof previousLicense === 'object' && 'type' in previousLicense) {
previousLicense = previousLicense.type
}
const previousLicenseStr = typeof previousLicense === 'string' ? previousLicense : 'UNKNOWN'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/registry/license-change/`[...pkg].get.ts around lines 50 - 51,
Normalize license values the same way as the timeline handler: instead of using
String(...) for currentLicense and previousLicense, detect if
versions[currentVersionIndex]?.license or
versions[previousVersionIndex]?.license is an object and, if so, use its .type
property (fallback to 'UNKNOWN' if missing); otherwise use the string value.
Update the assignments that set currentLicense and previousLicense (referencing
versions, currentVersionIndex, previousVersionIndex) to perform this object
check and extraction so change detection reports the actual license.type rather
than "[object Object]".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can use normalizeLicense from

function normalizeLicense(license?: PackumentLicense): string | undefined {
to handle that part. You could probably put it in some shared utils file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gameroman Here's the confirmed plan. A couple of findings from the investigation:

  • PackumentLicense in usePackage.ts is only used by the local normalizeLicense — so that import can be removed along with the function.

  • Nuxt auto-imports only covers ~/composables/** (not shared/utils/), so the handler will need an explicit import — consistent with what other server files already do (e.g. import { ... } from '#shared/utils/constants').

  • Export normalizeLicense from shared/utils/npm.ts

  • Remove the local definition (and now-unused PackumentLicense import) from app/composables/npm/usePackage.ts

  • Add an explicit import and use normalizeLicense in the license-change handler


🧠 Learnings used
Learnt from: Moshyfawn
Repo: npmx-dev/npmx.dev PR: 1796
File: server/api/registry/badge/[type]/[...pkg].get.ts:419-431
Timestamp: 2026-03-01T19:21:58.834Z
Learning: In the npmx.dev codebase, handleApiError is defined with a never return type and always throws. When calling this function, do not add an explicit throw or return after the call; TypeScript's control-flow analysis recognizes it as non-returning. This guidance applies to all TS files that call handleApiError and should be checked wherever the function is used (e.g., places importing and invoking it).

Learnt from: serhalp
Repo: npmx-dev/npmx.dev PR: 2459
File: test/unit/server/utils/likes-leaderboard.spec.ts:137-165
Timestamp: 2026-04-26T00:02:20.876Z
Learning: In this Nuxt project (npmx-dev/npmx.dev), the `Packument` type is globally available via Nuxt auto-imports from `shared/types/` (exported from `shared/types/npm-registry.ts`). Therefore, do not raise or require missing `import type { Packument } from '`#shared/types`'` (or any equivalent) when `Packument` is referenced, including in unit test files.

Learnt from: WilcoSp
Repo: npmx-dev/npmx.dev PR: 2717
File: server/api/changelog/md/[provider]/[owner]/[repo]/[...path].get.ts:34-34
Timestamp: 2026-05-15T07:45:36.306Z
Learning: HTTP 5xx status-code convention in the npmx-dev/npmx.dev codebase: use 502 exclusively when the upstream API is exhausted due to upstream API key/rate limits (e.g., throw/return ERROR_UNGH_API_KEY_EXHAUSTED). For all other server-side errors, use 500. During code review, treat existing 500 usage in changelog and other API endpoints as intentional—do not flag it as incorrect unless the error specifically corresponds to the upstream key/rate-limit exhaustion case that should map to 502.

Failed to handle agent chat message. Please try again.

Comment on lines +37 to +190
describe('license-change API', () => {
beforeEach(() => {
vi.clearAllMocks()
routerParam = undefined
queryParams = {}
})

it('throws 400 when package name param is missing', async () => {
routerParam = undefined
await expect(handler(fakeEvent)).rejects.toMatchObject({ statusCode: 400 })
})

it('reports no change for a new package with a single version (issue #2720)', async () => {
routerParam = 'vsxtools'

fetchNpmPackageMock.mockResolvedValue(
makePackument({
versions: { '0.0.1': { license: 'MIT' } },
time: { '0.0.1': '2024-01-01T00:00:00Z' },
}),
)

const result = await handler(fakeEvent)
expect(result.change).toBeNull()
})

it('reports a change when the license changed between versions', async () => {
routerParam = 'my-pkg'

fetchNpmPackageMock.mockResolvedValue(
makePackument({
versions: {
'1.0.0': { license: 'MIT' },
'2.0.0': { license: 'ISC' },
},
time: {
'1.0.0': '2024-01-01T00:00:00Z',
'2.0.0': '2024-06-01T00:00:00Z',
},
}),
)

const result = await handler(fakeEvent)
expect(result.change).toEqual({ from: 'MIT', to: 'ISC' })
})

it('reports no change when the license is unchanged between versions', async () => {
routerParam = 'my-pkg'

fetchNpmPackageMock.mockResolvedValue(
makePackument({
versions: {
'1.0.0': { license: 'MIT' },
'2.0.0': { license: 'MIT' },
},
time: {
'1.0.0': '2024-01-01T00:00:00Z',
'2.0.0': '2024-06-01T00:00:00Z',
},
}),
)

const result = await handler(fakeEvent)
expect(result.change).toBeNull()
})

it('defaults to the latest (chronologically newest) version', async () => {
routerParam = 'my-pkg'
queryParams = {}

fetchNpmPackageMock.mockResolvedValue(
makePackument({
versions: {
'1.0.0': { license: 'MIT' },
'2.0.0': { license: 'MIT' },
'3.0.0': { license: 'Apache-2.0' },
},
time: {
'1.0.0': '2024-01-01T00:00:00Z',
'2.0.0': '2024-06-01T00:00:00Z',
'3.0.0': '2025-01-01T00:00:00Z',
},
}),
)

const result = await handler(fakeEvent)
expect(result.change).toEqual({ from: 'MIT', to: 'Apache-2.0' })
})

it('compares the requested version against its predecessor', async () => {
routerParam = 'my-pkg'
queryParams = { version: '2.0.0' }

fetchNpmPackageMock.mockResolvedValue(
makePackument({
versions: {
'1.0.0': { license: 'MIT' },
'2.0.0': { license: 'ISC' },
'3.0.0': { license: 'Apache-2.0' },
},
time: {
'1.0.0': '2024-01-01T00:00:00Z',
'2.0.0': '2024-06-01T00:00:00Z',
'3.0.0': '2025-01-01T00:00:00Z',
},
}),
)

const result = await handler(fakeEvent)
expect(result.change).toEqual({ from: 'MIT', to: 'ISC' })
})

it('reports no change when the requested version is the oldest', async () => {
routerParam = 'my-pkg'
queryParams = { version: '1.0.0' }

fetchNpmPackageMock.mockResolvedValue(
makePackument({
versions: {
'1.0.0': { license: 'MIT' },
'2.0.0': { license: 'ISC' },
},
time: {
'1.0.0': '2024-01-01T00:00:00Z',
'2.0.0': '2024-06-01T00:00:00Z',
},
}),
)

const result = await handler(fakeEvent)
expect(result.change).toBeNull()
})

it('reports no change when the requested version is not found', async () => {
routerParam = 'my-pkg'
queryParams = { version: '9.9.9' }

fetchNpmPackageMock.mockResolvedValue(
makePackument({
versions: {
'1.0.0': { license: 'MIT' },
'2.0.0': { license: 'ISC' },
},
time: {
'1.0.0': '2024-01-01T00:00:00Z',
'2.0.0': '2024-06-01T00:00:00Z',
},
}),
)

const result = await handler(fakeEvent)
expect(result.change).toBeNull()
})
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add test coverage for license objects.

The test suite thoroughly validates the guard logic for single-version, multi-version, and version-not-found cases. However, it doesn't cover the scenario where a license is an object (e.g., { type: 'MIT' }) rather than a string. According to shared/types/npm-registry.ts, PackumentLicense can be string | { type: string; url?: string }.

Adding a test case for object licenses would ensure the handler normalizes them correctly (extracting the type field) and prevent regressions.

📋 Suggested test case for license objects
it('handles license objects by extracting the type field', async () => {
  routerParam = 'my-pkg'

  fetchNpmPackageMock.mockResolvedValue(
    makePackument({
      versions: {
        '1.0.0': { license: { type: 'MIT' } },
        '2.0.0': { license: { type: 'Apache-2.0', url: 'https://...' } },
      },
      time: {
        '1.0.0': '2024-01-01T00:00:00Z',
        '2.0.0': '2024-06-01T00:00:00Z',
      },
    }),
  )

  const result = await handler(fakeEvent)
  expect(result.change).toEqual({ from: 'MIT', to: 'Apache-2.0' })
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/server/api/registry/license-change/pkg.get.spec.ts` around lines 37
- 190, Add a new test in this spec to exercise object-shaped licenses: create a
case (similar to the suggested snippet) that sets routerParam = 'my-pkg', mocks
fetchNpmPackageMock via makePackument to return versions where license values
are objects (e.g. { type: 'MIT' } and { type: 'Apache-2.0', url: '...' }), call
handler(fakeEvent), and assert the returned change equals { from: 'MIT', to:
'Apache-2.0' }; this ensures the handler's license normalization logic (used
when reading packument versions) correctly extracts the type field from object
licenses.

jp-knj added a commit to jp-knj/npmx.dev that referenced this pull request May 25, 2026
The license field can be an object ({ type, url }), where String() would
yield "[object Object]". Reuse normalizeLicense (moved from usePackage to
shared/utils/npm) to extract the type, so change detection compares real
license values. Adds an object-license test case.

Addresses review feedback on npmx-dev#2792.
@jp-knj jp-knj force-pushed the fix/2720-license-change-no-previous-version branch from 2d1cf32 to 8181098 Compare May 25, 2026 14:19
…rsion

Don't flag a license change when there's no real previous version (new
single-version packages were diffing against a phantom 'UNKNOWN'), and
normalize object-shaped licenses ({ type, url }) so comparisons use the
real value instead of "[object Object]". Adds unit tests.

Closes npmx-dev#2720
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Package page shows license changed warning for new package

2 participants