Skip to content

feat: implement version lens#99

Open
9romise wants to merge 6 commits intomainfrom
version-lens
Open

feat: implement version lens#99
9romise wants to merge 6 commits intomainfrom
version-lens

Conversation

@9romise
Copy link
Copy Markdown
Member

@9romise 9romise commented Apr 3, 2026

close #2, close #32

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a version lens CodeLens feature to the VS Code extension. The implementation adds configuration properties (npmx.versionLens.enabled and npmx.versionLens.hideWhenLatest) to control CodeLens display behaviour. A new language service plugin (npmx-version-lens) provides CodeLens entries for dependency version ranges, computing available upgrade tiers (patch, minor, major) and constructing upgrade commands. A new replaceText command enables text replacement in the editor via workspace edits. Utility functions calculate upgrade tier targets from package metadata, whilst command wiring integrates the new replace-text command handler into the extension's activation lifecycle.

Possibly related PRs

  • PR #87: Establishes the language service plugin scaffold and core plugin registration array in packages/language-service/src/index.ts, which this PR directly extends by appending the new createNpmxVersionLensService plugin.
  • PR #91: Adds other language service plugins by following the same pattern of importing a create* function and appending it to the plugin array in packages/language-service/src/index.ts.
  • PR #95: Refactors the shared directory into an npmx-shared package, affecting imports for command constants like the newly added REPLACE_TEXT_COMMAND used in extension command wiring.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description directly references the linked issues (#2 and #32) that outline the version lens feature being implemented.
Linked Issues check ✅ Passed The pull request successfully implements version lens functionality with CodeLens support for package upgrades (#2, #32), including upgrade tier support and configuration options.
Out of Scope Changes check ✅ Passed All changes directly support the version lens feature: configuration properties, language service plugin, version utility functions, VS Code commands, and test coverage.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch version-lens

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
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

🧹 Nitpick comments (3)
packages/language-service/src/utils/version.ts (1)

59-71: Consider handling invalid semver input gracefully.

new SemVer(resolvedVersion) on line 60 will throw a TypeError if resolvedVersion is not a valid semver string. Whilst the caller should ideally provide valid input, defensive handling would prevent runtime crashes when encountering malformed version specs.

♻️ Suggested defensive handling
 export function resolveUpgradeTiers(pkg: PackageInfo, resolvedVersion: string): UpgradeTier[] {
+  let current: SemVer
+  try {
+    current = new SemVer(resolvedVersion)
+  }
+  catch {
+    return []
+  }
-  const current = new SemVer(resolvedVersion)
   const currentMajor = current.major
   const currentMinor = current.minor
packages/language-service/src/utils/version.test.ts (1)

28-33: Test helper uses as cast; consider a type-safe alternative.

The as PackageInfo cast on line 32 bypasses type checking. Whilst acceptable in test helpers, a more type-safe approach would use satisfies or provide complete mock data.

♻️ Type-safe alternative using Partial
-function createPkg(versions: string[]): PackageInfo {
+function createPkg(versions: string[]): Partial<PackageInfo> & Pick<PackageInfo, 'versionsMeta' | 'distTags'> {
   const versionsMeta: Record<string, object> = {}
   for (const v of versions)
     versionsMeta[v] = {}
-  return { versionsMeta, distTags: { latest: versions.at(-1)! } } as PackageInfo
+  return { versionsMeta, distTags: { latest: versions.at(-1)! } }
 }

As per coding guidelines: "Avoid as type casts—validate instead in TypeScript".

extensions/vscode/src/commands/replace-text.ts (1)

4-15: Consider handling applyEdit failure.

workspace.applyEdit() returns Promise<boolean> indicating whether the edit was applied successfully. Silently ignoring failures could lead to confusing user experiences when edits don't apply (e.g., file changed externally, read-only file).

♻️ Suggested error handling
 export async function replaceText(uri: string, range: LspRange, newText: string) {
   const edit = new WorkspaceEdit()
   edit.replace(
     Uri.parse(uri),
     new Range(
       new Position(range.start.line, range.start.character),
       new Position(range.end.line, range.end.character),
     ),
     newText,
   )
-  await workspace.applyEdit(edit)
+  const success = await workspace.applyEdit(edit)
+  if (!success) {
+    throw new Error(`Failed to apply edit to ${uri}`)
+  }
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1cf001e9-a8a6-4365-a514-09e85dd1d164

📥 Commits

Reviewing files that changed from the base of the PR and between 0aafc62 and a918671.

📒 Files selected for processing (9)
  • extensions/vscode/README.md
  • extensions/vscode/package.json
  • extensions/vscode/src/commands/replace-text.ts
  • extensions/vscode/src/index.ts
  • packages/language-service/src/index.ts
  • packages/language-service/src/plugins/version-lens.ts
  • packages/language-service/src/utils/version.test.ts
  • packages/language-service/src/utils/version.ts
  • packages/shared/src/commands.ts

},
},
create(context): LanguageServicePluginInstance {
async function resolveVersionLensCommand({ uri, specRange, tier }: LenData, range: CodeLens['range']): Promise<CodeLens['command']> {
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n packages/language-service/src/plugins/version-lens.ts | head -150

Repository: npmx-dev/vscode-npmx

Length of output: 5201


Remove the as LenData cast and validate lens.data before destructuring.

Line 118 uses an unchecked type cast that bypasses validation. If lens.data is missing or malformed, the destructuring at line 29 will throw instead of degrading gracefully. Implement a validation function and guard the call:

Suggested implementation
         async resolveCodeLens(lens): Promise<CodeLens> {
-          const command = await resolveVersionLensCommand(lens.data as LenData, lens.range)
+          if (!isLenData(lens.data))
+            return { ...lens, command: UNKNOWN_COMMAND }
+          const command = await resolveVersionLensCommand(lens.data, lens.range)
           return { ...lens, command }
         },
function isLenData(value: unknown): value is LenData {
  if (typeof value !== 'object' || value === null)
    return false
  if (!('uri' in value) || typeof value.uri !== 'string')
    return false
  if (!('specRange' in value) || !Array.isArray(value.specRange) || value.specRange.length !== 2)
    return false
  if (typeof value.specRange[0] !== 'number' || typeof value.specRange[1] !== 'number')
    return false
  if (!('tier' in value) || value.tier === undefined)
    return true
  return typeof value.tier === 'object'
    && value.tier !== null
    && 'type' in value.tier
    && typeof value.tier.type === 'string'
    && 'version' in value.tier
    && typeof value.tier.version === 'string'
}

This aligns with the coding guideline: "Avoid as type casts—validate instead in TypeScript".

Comment on lines +54 to +63
const ignoreList = await getConfig(context, 'npmx.ignore.upgrade')
const targetVersion = resolveUpgrade(dep, pkg, resolvedVersion, ignoreList)
if (!targetVersion)
return { title: '$(check) latest', command: '' }

return {
title: `$(arrow-up) ${targetVersion}`,
command: REPLACE_TEXT_COMMAND,
arguments: [uri, range, targetVersion],
}
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.

⚠️ Potential issue | 🟠 Major

Don't short-circuit the fallback path with hideWhenLatest.

Line 107 hides every dependency that reaches the non-tiered branch, so the fallback logic on Lines 54-63 never runs for those cases. That suppresses the fallback lens entirely and also hides the unknown state when metadata is still missing; hideWhenLatest should only hide confirmed latest cases.

Suggested fix
           const dependencies = await workspaceState.getResolvedDependencies(document.uri)
           if (!dependencies)
             return []

           const lenses: CodeLens[] = []
+          const hideWhenLatest = await getConfig(context, 'npmx.versionLens.hideWhenLatest')
+          const ignoreList = await getConfig(context, 'npmx.ignore.upgrade')

           for (const dep of dependencies) {
             if (dep.resolvedProtocol !== 'npm' || dep.category === 'peerDependencies')
               continue
@@
-            const hideWhenLatest = await getConfig(context, 'npmx.versionLens.hideWhenLatest')
-            if (hideWhenLatest)
+            if (pkg && resolvedVersion && hideWhenLatest && !resolveUpgrade(dep, pkg, resolvedVersion, ignoreList))
               continue

             lenses.push({ range, data: baseData })
           }

Also applies to: 79-109

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

1 participant