Skip to content

feat(code-lens): support package version upgrade#32

Open
9romise wants to merge 4 commits intomainfrom
lens/version
Open

feat(code-lens): support package version upgrade#32
9romise wants to merge 4 commits intomainfrom
lens/version

Conversation

@9romise
Copy link
Member

@9romise 9romise commented Feb 7, 2026

Closes #2

@9romise 9romise marked this pull request as ready for review February 27, 2026 17:21
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a new version lens feature for displaying upgrade availability as CodeLens annotations for package dependencies. The implementation includes a new configuration option (npmx.versionLens.enabled), a code lens provider that registers version upgrade lenses across extractors, a utility function for resolving upgrade targets, an internal command system for text replacement, and refactored upgrade diagnostic logic that consolidates version-target resolution into a shared utility function. Configuration files, extension activation logic, and test coverage are updated accordingly.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description 'Closes #2' is minimal but directly relates to the changeset, which implements a version lens feature to address the linked issue.
Linked Issues check ✅ Passed The pull request successfully implements the version lens feature requested in issue #2, enabling one-click package version upgrades with a code lens interface.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the version lens feature. ESLint configuration updates support the new internal command structure, and all file modifications align with the feature objective.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lens/version

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.

Copy link
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.

🧹 Nitpick comments (6)
src/commands/internal/replace-text.ts (1)

4-8: Consider whether empty string replacement should be supported.

The check !text will reject empty strings, preventing text deletion via this command. If replacing with an empty string is a valid use case, consider changing the condition to text === undefined || text === null or text == null.

For the current version lens feature, this is likely acceptable since replacing a version with an empty string is not intended behaviour.

💡 Optional fix if deletion should be supported
 export const replaceText: TextEditorCommandCallback = (_: TextEditor, edit: TextEditorEdit, range?: Range, text?: string) => {
-  if (!range || !text)
+  if (!range || text == null)
     return
 
   edit.replace(range, text)
 }
src/utils/package.ts (1)

15-19: Consider adding an explicit return type annotation.

The function can return null (from the early return or from maxSatisfying), but this isn't immediately clear from the signature. Adding an explicit return type improves readability and helps catch unintended changes.

💡 Proposed fix
-export function resolveExactVersion(pkg: PackageInfo | undefined, version: string) {
+export function resolveExactVersion(pkg: PackageInfo | undefined, version: string): string | null {
   if (!pkg?.distTags)
     return null
 
   return pkg.distTags[version] ?? maxSatisfying(Object.keys(pkg.versionsMeta ?? {}), version)
 }
tests/utils/package.test.ts (1)

14-17: Consider adding more test cases for resolveExactVersion.

The current test covers the undefined package case. Additional test scenarios would improve confidence in the function:

  • Package with distTags where version matches a tag name (e.g., 'latest').
  • Package with versionsMeta where a semver range can be satisfied.
  • Package where neither distTags nor versionsMeta contains a matching version.
💡 Example additional test cases
it('should resolve dist-tag to exact version', () => {
  const pkg = {
    distTags: { latest: '2.0.0' },
    versionsMeta: { '1.0.0': {}, '2.0.0': {} },
  } as PackageInfo
  expect(resolveExactVersion(pkg, 'latest')).toBe('2.0.0')
})

it('should resolve semver range via maxSatisfying', () => {
  const pkg = {
    distTags: { latest: '2.0.0' },
    versionsMeta: { '1.0.0': {}, '1.5.0': {}, '2.0.0': {} },
  } as PackageInfo
  expect(resolveExactVersion(pkg, '^1.0.0')).toBe('1.5.0')
})
src/utils/upgrade.ts (1)

22-31: Potential non-determinism when multiple dist-tags match the prerelease identifier.

If multiple dist-tags share the same prerelease identifier (e.g., next: 3.0.0-alpha.5 and canary: 3.0.0-alpha.7), the function returns the first match encountered during Object.entries iteration. Object property order in modern JS is generally insertion-order for string keys, but this may not yield the highest available version.

Consider sorting candidates by version descending and returning the greatest, or documenting that only the first match is returned.

♻️ Optional: return the highest matching prerelease version
+import semverGt from 'semver/functions/gt'
+
+  let best: string | undefined
   for (const [tag, tagVersion] of Object.entries(pkg.distTags)) {
     if (tag === 'latest')
       continue
     if (prerelease(tagVersion)?.[0] !== currentPreId)
       continue
     if (lte(tagVersion, exactVersion))
       continue
 
-    return tagVersion
+    if (!best || semverGt(tagVersion, best))
+      best = tagVersion
   }
+  return best
 }
tests/utils/upgrade.test.ts (1)

18-26: Consider adding edge-case tests for completeness.

The parameterised tests cover the main scenarios well. Consider adding tests for:

  1. Empty distTags object ({}) — ensures graceful handling when no tags are published.
  2. Missing latest tag — verifies behaviour when only prerelease tags exist.
🧪 Optional: additional edge-case tests
it('should return undefined when distTags is empty', () => {
  const emptyPkg = createPackageInfo({})
  expect(resolveUpgradeTargetVersion(emptyPkg, '1.0.0')).toBeUndefined()
})

it('should return undefined when only prerelease tags exist and version is stable', () => {
  const preOnlyPkg = createPackageInfo({ next: '2.0.0-alpha.1' })
  expect(resolveUpgradeTargetVersion(preOnlyPkg, '1.0.0')).toBeUndefined()
})
src/providers/code-lens/version.ts (1)

55-60: Consider handling rejected promises to avoid stale "checking..." state.

If getPackageInfo returns a Promise that rejects, the lens will remain in the "checking..." state until the document changes. Using finally triggers a refresh regardless of success or failure, which is good, but the user may see "checking..." followed by "unknown" repeatedly if the network is flaky.

This is minor since the refresh will eventually update the state, but you may want to add explicit error handling or logging for debugging purposes.

♻️ Optional: add error handling for rejected promises
     if (pkg instanceof Promise) {
       lens.command = { title: '$(sync~spin) checking...', command: '' }
-      pkg.finally(() => this.scheduleRefresh())
+      pkg
+        .catch(() => { /* Network error handled on next refresh */ })
+        .finally(() => this.scheduleRefresh())
       return lens
     }

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5716f0 and 206418f.

📒 Files selected for processing (16)
  • README.md
  • eslint.config.ts
  • package.json
  • playground/.vscode/settings.json
  • src/commands/internal/index.ts
  • src/commands/internal/replace-text.ts
  • src/index.ts
  • src/providers/code-lens/index.ts
  • src/providers/code-lens/version.ts
  • src/providers/diagnostics/rules/upgrade.ts
  • src/state.ts
  • src/utils/package.ts
  • src/utils/upgrade.ts
  • tests/diagnostics/upgrade.test.ts
  • tests/utils/package.test.ts
  • tests/utils/upgrade.test.ts

@RYGRIT
Copy link
Contributor

RYGRIT commented Mar 1, 2026

Based on my previous explorations, while reviewing this PR today, I suddenly thought that perhaps we should display the image below by default and then guide users to enable CodeLens configuration.

image

If you feel that displaying "latest" after the version number has too much of an impact, you can also add a tooltip similar to an arrow to the left of the line number.

Another idea I have is that currently only the latest version is being shown. I think it's necessary to add minor and patch versions as well, since not all users seem to want to update to the latest version, something like this.

image

/cc @9romise @nitodeco

@9romise
Copy link
Member Author

9romise commented Mar 1, 2026

Based on my previous explorations, while reviewing this PR today, I suddenly thought that perhaps we should display the image below by default and then guide users to enable CodeLens configuration.

It would be great if this kind of prompt could also be clicked to upgrade.

Another idea I have is that currently only the latest version is being shown. I think it's necessary to add minor and patch versions as well, since not all users seem to want to update to the latest version, something like this.

This is a really good point. I think we should move in that direction.

@RYGRIT
Copy link
Contributor

RYGRIT commented Mar 2, 2026

It would be great if this kind of prompt could also be clicked to upgrade.

Yes, but as far as I know, that's not possible right now.

@RYGRIT
Copy link
Contributor

RYGRIT commented Mar 2, 2026

Is this a bug? The latest version number of ESLint is 10.0.2, but my current version number is 10.0.0, but CodeLens shows "latest".

image

@9romise
Copy link
Member Author

9romise commented Mar 2, 2026

Is this a bug? The latest version number of ESLint is 10.0.2, but my current version number is 10.0.0, but CodeLens shows "latest".

No. The spec ^10.0.0 tells npm to install the max satisfying version — so 10.0.2 is correct. See https://github.com/npm/node-semver#caret-ranges-123-025-004

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.

Update to latest

2 participants