Skip to content

feat: route pkg@version from deps, direct nav, and search for any valid specifier#1723

Open
serhalp wants to merge 5 commits intomainfrom
serhalp/fix-version-sorting
Open

feat: route pkg@version from deps, direct nav, and search for any valid specifier#1723
serhalp wants to merge 5 commits intomainfrom
serhalp/fix-version-sorting

Conversation

@serhalp
Copy link
Copy Markdown
Member

@serhalp serhalp commented Feb 27, 2026

🔗 Linked issue

Closes #1120
Closes #1416

🧭 Context

We have two seemingly separate issues:

  • Dependency links with version ranges (e.g. ^18.0.0 || ^19.0.0, >15 <=16.0.2) currently navigate to a non-existent version page and result in a 404.
  • Searching for nuxt@4.0.1 or nuxt@>=4 or nuxt@>=4<6 (convenient if pasting from somewhere) does not result in a match. There's a proposed temporary solution in feat: strip version info from the search query #1626 to strip the version segment from the query.

📚 Description

  • Keep links to a single package version as is, but for all other version specifiers, link to the package page with the "Filter by semver" input pre-populated! (In the future if/when we have a dedicated versions page we could link to that instead.)
  • Rather than implementing this logic at the "where to link to" layer, I implemented this in the routing layer, i.e. https://npmx.dev/package/nuxt/v/:version will route to the specific versioned package page if version is a specific version string and route to the package page with ?semver=<version> otherwise.
  • ... which means we get a fix to Add possibility to search for specific version of a package #1416 almost for free! This PR makes it so that searching for nuxt@4.0.1 or nuxt@>=4 or nuxt@>=4<6 and pressing Enter now navigates directly to the package version page for a specific version or to the semver-filtered package page for a semver specifier, instead of returning no results. This just works for both exact versions and semver ranges, because of the above routing change.
npmx.smart.pkg@v.routing.mp4

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 27, 2026

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

Project Deployment Actions Updated (UTC)
docs.npmx.dev Ready Ready Preview, Comment Apr 8, 2026 2:18pm
npmx.dev Ready Ready Preview, Comment Apr 8, 2026 2:18pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Apr 8, 2026 2:18pm

Request Review

@serhalp serhalp changed the title feat: intelligently route pkg@version from deps, direct nav, and search for any valid specifier feat: route pkg@version from deps, direct nav, and search for any valid specifier Feb 27, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Feb 27, 2026

Codecov Report

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

Files with missing lines Patch % Lines
shared/utils/parse-package-param.ts 0.00% 3 Missing and 3 partials ⚠️
app/pages/search.vue 0.00% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@trueberryless
Copy link
Copy Markdown
Contributor

Awesome idea, I really like this one. Two birds with one stone 🚀

@serhalp serhalp marked this pull request as ready for review February 28, 2026 17:03
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Introduces parsing and handling for package specifiers of the form "pkg@version": a new shared utility parsePackageSpecifier was added and used in search, server cache, and tests; the router now routes exact single semver versions to package-version and sends non-exact semver specifiers to the package route via query.semver with a default #versions hash; search Enter-key handling now recognises pkg@version and navigates accordingly. Versions component initialisation now seeds its semver filter from route.query.semver.

Possibly related PRs

Suggested labels

needs review

Suggested reviewers

  • danielroe
  • shuuji3
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description clearly relates to the changeset, explaining the rationale for routing changes and addressing two specific GitHub issues.

✏️ 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 serhalp/fix-version-sorting

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


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.

🧹 Nitpick comments (3)
shared/utils/parse-package-param.ts (1)

54-61: Trailing @ edge case may produce unexpected results.

When the input ends with @ but has no version (e.g., 'esbuild@'), the function returns { name: 'esbuild@' } because the version slice is empty. This retains the trailing @ in the package name, which is not a valid npm package name.

Consider whether this should instead return { name: 'esbuild' } to strip the trailing @:

💡 Suggested fix
 export function parsePackageSpecifier(input: string): { name: string; version?: string } {
   const atIndex = input.startsWith('@') ? input.indexOf('@', 1) : input.indexOf('@')
   if (atIndex > 0) {
     const version = input.slice(atIndex + 1)
     if (version) return { name: input.slice(0, atIndex), version }
+    // Trailing @ with no version - return name without the @
+    return { name: input.slice(0, atIndex) }
   }
   return { name: input }
 }
modules/runtime/server/cache.ts (1)

222-238: Consider using parsePackageSpecifier to reduce code duplication.

This function contains inline parsing logic for extracting the package name and version specifier (lines 226-238) that duplicates the logic in parsePackageSpecifier. The same pattern appears in processSingleVersionsMeta (lines 300-312).

Since parsePackageSpecifier was introduced to centralise this parsing, consider refactoring these functions to use it for consistency and maintainability.

♻️ Suggested refactor
 async function processSingleFastNpmMeta(
   packageQuery: string,
   storage: ReturnType<typeof useStorage>,
   metadata: boolean,
 ): Promise<Record<string, unknown>> {
-  let packageName = packageQuery
-  let specifier = 'latest'
-
-  if (packageName.startsWith('@')) {
-    const atIndex = packageName.indexOf('@', 1)
-    if (atIndex !== -1) {
-      specifier = packageName.slice(atIndex + 1)
-      packageName = packageName.slice(0, atIndex)
-    }
-  } else {
-    const atIndex = packageName.indexOf('@')
-    if (atIndex !== -1) {
-      specifier = packageName.slice(atIndex + 1)
-      packageName = packageName.slice(0, atIndex)
-    }
-  }
+  const { name: packageName, version } = parsePackageSpecifier(packageQuery)
+  const specifier = version ?? 'latest'
test/unit/app/utils/router.spec.ts (1)

100-107: Consider adding a test case for undefined version.

The test covers null version but not undefined. Since the function signature accepts string | null | undefined, it would be good to explicitly test the undefined case for completeness.

💡 Suggested addition
   describe('with null/undefined version', () => {
     it('returns package route for null version', () => {
       expect(packageRoute('react', null)).toEqual({
         name: 'package',
         params: { org: '', name: 'react' },
       })
     })
+
+    it('returns package route for undefined version', () => {
+      expect(packageRoute('react', undefined)).toEqual({
+        name: 'package',
+        params: { org: '', name: 'react' },
+      })
+    })
   })

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85ac3d7 and 2db1c4d.

📒 Files selected for processing (8)
  • app/components/Package/Versions.vue
  • app/pages/search.vue
  • app/utils/router.ts
  • modules/runtime/server/cache.ts
  • shared/utils/parse-package-param.ts
  • test/e2e/search-at-version.spec.ts
  • test/unit/app/utils/router.spec.ts
  • test/unit/shared/utils/parse-package-param.spec.ts

@serhalp serhalp requested a review from danielroe February 28, 2026 18:21
@chronoB
Copy link
Copy Markdown
Contributor

chronoB commented Mar 1, 2026

I like that! ❤️ I just checked it out locally and jumping to the correct version by pressing enter is amazing.

The problem I see is I do not know about that functionality if I don't press enter and the search results do not indicate that this is even possible (because the package is not shown). Maybe a combination with stripping the version for the search as in #1626 could help so that the correct package will also be shown in the results?

@Us3r-gitHub
Copy link
Copy Markdown

npmx.smart.pkg@v.routing.mp4

Hi! Sorry if this is a bit out of context 😅
Just curious—what app did you use to record the demo?
Thanks!

@serhalp
Copy link
Copy Markdown
Member Author

serhalp commented Mar 14, 2026

npmx.smart.pkg@v.routing.mp4

Hi! Sorry if this is a bit out of context 😅 Just curious—what app did you use to record the demo? Thanks!

😁 @Us3r-gitHub https://screen.studio/

serhalp added 5 commits April 8, 2026 09:38
…nstead of 404

Dependency links with version ranges (e.g. "^18.0.0 || ^19.0.0", ">15 <=16.0.2") previously
navigated to a non-existent version page and 404'd.

Now `packageRoute()` distinguishes exact versions from ranges: exact versions link to the version
page, while ranges link to the package page with `?semver=<range>#versions`, pre-populating the
existing "Filter by semver" input.

Closes #1120
Searching for `esbuild@0.25.12` or `@angular/core@^18` and pressing Enter now navigates directly to
the package version page (or semver filter for ranges) instead of returning no results.

Since in the previous commit I added support for linking to the package page with the a pre-filled
semver version specifier in the query string to populate the version filter, this supports both
exact versions and semver ranges in the search input.

Closes #1416
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: 1

🧹 Nitpick comments (1)
app/components/Package/Versions.vue (1)

108-109: Minor: Redundant || '' fallback.

The ternary expression already returns '' for non-string cases, making the trailing || '' unnecessary.

✨ Suggested simplification
-const semverFilter = ref((typeof route.query.semver === 'string' ? route.query.semver : '') || '')
+const semverFilter = ref(typeof route.query.semver === 'string' ? route.query.semver : '')

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2800da71-7154-45bc-a870-b32110a5fec8

📥 Commits

Reviewing files that changed from the base of the PR and between 56fa96e and dffa4f2.

📒 Files selected for processing (4)
  • app/components/Package/Versions.vue
  • app/pages/search.vue
  • app/utils/router.ts
  • docs/content/2.guide/1.features.md
✅ Files skipped from review due to trivial changes (2)
  • app/pages/search.vue
  • docs/content/2.guide/1.features.md

Comment on lines +108 to +109
// Semver range filter (initialized from ?semver= query param if present)
const semverFilter = ref((typeof route.query.semver === 'string' ? route.query.semver : '') || '')
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:

#!/bin/bash
# Check if there are any watchers or other mechanisms that sync route.query.semver to semverFilter
rg -n 'route\.query\.semver' app/components/Package/Versions.vue

Repository: npmx-dev/npmx.dev

Length of output: 163


🏁 Script executed:

cat -n app/components/Package/Versions.vue | head -150

Repository: npmx-dev/npmx.dev

Length of output: 5561


🏁 Script executed:

rg -n 'watch\(|onBeforeRouteUpdate' app/components/Package/Versions.vue

Repository: npmx-dev/npmx.dev

Length of output: 188


🏁 Script executed:

rg -n 'Versions\.vue\|<Versions' app/pages/ -A 3

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

rg -n ':key=' app/pages/package/\[\[org\]\]/\[name\]\.vue | head -20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

rg -n 'import.*Versions' app/ -l | grep -v node_modules

Repository: npmx-dev/npmx.dev

Length of output: 344


🏁 Script executed:

rg -n '<Versions' app/ -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 587


🏁 Script executed:

fd -name '\[name\].vue' app/pages/package/

Repository: npmx-dev/npmx.dev

Length of output: 291


🏁 Script executed:

find app/pages/package -name '[name].vue' -o -name 'index.vue'

Repository: npmx-dev/npmx.dev

Length of output: 103


🏁 Script executed:

cat -n app/pages/package/\[\[org\]\]/\[name\].vue | head -200

Repository: npmx-dev/npmx.dev

Length of output: 6760


🏁 Script executed:

rg -n '<Versions' app/pages/package/\[\[org\]\]/\[name\]/index.vue -B 2 -A 5

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

wc -l app/pages/package/\[\[org\]\]/\[name\]/index.vue

Repository: npmx-dev/npmx.dev

Length of output: 106


🏁 Script executed:

cat app/pages/package/\[\[org\]\]/\[name\]/index.vue

Repository: npmx-dev/npmx.dev

Length of output: 221


🏁 Script executed:

find app/pages/package -type f -name '*.vue' | xargs wc -l | sort -rn

Repository: npmx-dev/npmx.dev

Length of output: 271


🏁 Script executed:

ls -la app/pages/package/\[\[org\]\]/

Repository: npmx-dev/npmx.dev

Length of output: 292


🏁 Script executed:

rg -n 'Versions' app/pages/package/\[\[org\]\]/\[name\].vue -B 3 -A 3

Repository: npmx-dev/npmx.dev

Length of output: 2760


🏁 Script executed:

rg -n '<Versions' app/pages/package/\[\[org\]\]/\[name\].vue -B 5 -A 5

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

sed -n '964,980p' app/pages/package/\[\[org\]\]/\[name\].vue

Repository: npmx-dev/npmx.dev

Length of output: 718


🏁 Script executed:

rg -n 'PackageVersions' app/pages/package/\[\[org\]\]/\[name\].vue -A 10

Repository: npmx-dev/npmx.dev

Length of output: 1041


🏁 Script executed:

cat -n app/components/Package/Versions.vue | sed -n '100,350p'

Repository: npmx-dev/npmx.dev

Length of output: 10420


🏁 Script executed:

cat -n app/components/Package/Versions.vue | sed -n '350,550p'

Repository: npmx-dev/npmx.dev

Length of output: 8047


🏁 Script executed:

cat -n app/components/Package/Versions.vue | tail -50

Repository: npmx-dev/npmx.dev

Length of output: 1819


🏁 Script executed:

rg -n 'onBeforeRouteUpdate\|watch.*route' app/components/Package/Versions.vue

Repository: npmx-dev/npmx.dev

Length of output: 43


Watch route.query.semver to sync filter state on navigation.

The semverFilter is initialised once at component setup from route.query.semver. Without a watcher on the route parameter, internal navigation (e.g., from /package/foo?semver=^1 to /package/foo?semver=^2) won't update the filter because the component instance reuses without unmounting. Add a watcher on route.query.semver to keep the filter in sync with URL changes.

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.

Add possibility to search for specific version of a package Dependency links with version union ("1.0 || 2.0") resolves to 404 page

4 participants