Uh oh!
There was an error while loading. Please reload this page.
feat: route pkg@version from deps, direct nav, and search for any valid specifier - #1723
feat: route pkg@version from deps, direct nav, and search for any valid specifier#1723serhalp wants to merge 7 commits into
pkg@version from deps, direct nav, and search for any valid specifier#1723Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
pkg@version from deps, direct nav, and search for any valid specifierpkg@version from deps, direct nav, and search for any valid specifier❌ 271 Tests Failed:
View the top 3 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
trueberryless
commented
Feb 28, 2026
Awesome idea, I really like this one. Two birds with one stone 🚀 |
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughIntroduces parsing and handling for package specifiers of the form "pkg@version": a new shared utility Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsTimed 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. Comment |
There was a problem hiding this comment.
🧹 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 theversionslice 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 usingparsePackageSpecifierto 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 inprocessSingleVersionsMeta(lines 300-312).Since
parsePackageSpecifierwas 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 forundefinedversion.The test covers
nullversion but notundefined. Since the function signature acceptsstring | null | undefined, it would be good to explicitly test theundefinedcase 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
📒 Files selected for processing (8)
app/components/Package/Versions.vueapp/pages/search.vueapp/utils/router.tsmodules/runtime/server/cache.tsshared/utils/parse-package-param.tstest/e2e/search-at-version.spec.tstest/unit/app/utils/router.spec.tstest/unit/shared/utils/parse-package-param.spec.ts
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
commented
Mar 12, 2026
Hi! Sorry if this is a bit out of context 😅 |
serhalp
commented
Mar 14, 2026
|
…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
56fa96e to
dffa4f2CompareThere was a problem hiding this comment.
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
📒 Files selected for processing (4)
app/components/Package/Versions.vueapp/pages/search.vueapp/utils/router.tsdocs/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
| // Semver range filter (initialized from ?semver= query param if present) | ||
| const semverFilter = ref((typeof route.query.semver === 'string' ? route.query.semver : '') || '') |
There was a problem hiding this comment.
🧩 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.vueRepository: npmx-dev/npmx.dev
Length of output: 163
🏁 Script executed:
cat -n app/components/Package/Versions.vue | head -150Repository: npmx-dev/npmx.dev
Length of output: 5561
🏁 Script executed:
rg -n 'watch\(|onBeforeRouteUpdate' app/components/Package/Versions.vueRepository: npmx-dev/npmx.dev
Length of output: 188
🏁 Script executed:
rg -n 'Versions\.vue\|<Versions' app/pages/ -A 3Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
rg -n ':key=' app/pages/package/\[\[org\]\]/\[name\]\.vue | head -20Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
rg -n 'import.*Versions' app/ -l | grep -v node_modulesRepository: npmx-dev/npmx.dev
Length of output: 344
🏁 Script executed:
rg -n '<Versions' app/ -B 2 -A 2Repository: 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 -200Repository: npmx-dev/npmx.dev
Length of output: 6760
🏁 Script executed:
rg -n '<Versions' app/pages/package/\[\[org\]\]/\[name\]/index.vue -B 2 -A 5Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
wc -l app/pages/package/\[\[org\]\]/\[name\]/index.vueRepository: npmx-dev/npmx.dev
Length of output: 106
🏁 Script executed:
cat app/pages/package/\[\[org\]\]/\[name\]/index.vueRepository: npmx-dev/npmx.dev
Length of output: 221
🏁 Script executed:
find app/pages/package -type f -name '*.vue'| xargs wc -l | sort -rnRepository: 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 3Repository: npmx-dev/npmx.dev
Length of output: 2760
🏁 Script executed:
rg -n '<Versions' app/pages/package/\[\[org\]\]/\[name\].vue -B 5 -A 5Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
sed -n '964,980p' app/pages/package/\[\[org\]\]/\[name\].vueRepository: npmx-dev/npmx.dev
Length of output: 718
🏁 Script executed:
rg -n 'PackageVersions' app/pages/package/\[\[org\]\]/\[name\].vue -A 10Repository: 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 -50Repository: npmx-dev/npmx.dev
Length of output: 1819
🏁 Script executed:
rg -n 'onBeforeRouteUpdate\|watch.*route' app/components/Package/Versions.vueRepository: 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.
| /** | ||
| * Parse a scoped package name with optional version. | ||
| * Handles formats like: @scope/name, @scope/name@version, name, name@version | ||
| */ | ||
| function parseScopedPackageWithVersion(input: string): { name: string; version?: string } { | ||
| if (input.startsWith('@')) { | ||
| // Scoped package: @scope/name or @scope/name@version | ||
| const slashIndex = input.indexOf('/') | ||
| if (slashIndex === -1) { | ||
| // Invalid format like just "@scope" | ||
| return { name: input } | ||
| } | ||
| const afterSlash = input.slice(slashIndex + 1) | ||
| const atIndex = afterSlash.indexOf('@') | ||
| if (atIndex === -1) { | ||
| // @scope/name (no version) | ||
| return { name: input } | ||
| } | ||
| // @scope/name@version | ||
| return { | ||
| name: input.slice(0, slashIndex + 1 + atIndex), | ||
| version: afterSlash.slice(atIndex + 1), | ||
| } | ||
| } | ||
| // Unscoped package: name or name@version | ||
| const atIndex = input.indexOf('@') | ||
| if (atIndex === -1) { | ||
| return { name: input } | ||
| } | ||
| return { | ||
| name: input.slice(0, atIndex), | ||
| version: input.slice(atIndex + 1), | ||
| } | ||
| } | ||
gameroman
commented
Aug 4, 2026
Hi @serhalp would you like to continue working on this? ❤️ |
trueberryless
commented
Aug 11, 2026
I took the liberty to update the PR with changes that happened on main in the meantime (mainly consolidating the |
| // npms.io API - return mock package score data | ||
| if (host === 'api.npms.io') { | ||
| const packageMatch = decodeURIComponent(pathname).match(/^\/v2\/package\/(.+)$/) | ||
| if (packageMatch?.[1]) { | ||
| return { | ||
| data: { | ||
| analyzedAt: new Date().toISOString(), | ||
| collected: { | ||
| metadata: { name: packageMatch[1] }, | ||
| }, | ||
| score: { | ||
| final: 0.75, | ||
| detail: { | ||
| quality: 0.8, | ||
| popularity: 0.7, | ||
| maintenance: 0.75, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
| } | ||
| // jsdelivr CDN - return 404 for README files, etc. | ||
| if (host === 'cdn.jsdelivr.net') { | ||
| // Return null data which will cause a 404 - README files are optional | ||
| return { data: null } | ||
| } | ||
| // jsdelivr data API - return mock file listing | ||
| if (host === 'data.jsdelivr.com') { | ||
| const packageMatch = decodeURIComponent(pathname).match(/^\/v1\/packages\/npm\/(.+)$/) | ||
| if (packageMatch?.[1]) { | ||
| const pkgWithVersion = packageMatch[1] | ||
| const parsed = parsePackageSpec(pkgWithVersion) | ||
| return { | ||
| data: { | ||
| type: 'npm', | ||
| name: parsed.name, | ||
| version: parsed.version || 'latest', | ||
| files: [ | ||
| { name: 'package.json', hash: 'abc123', size: 1000 }, | ||
| { name: 'index.js', hash: 'def456', size: 500 }, | ||
| { name: 'README.md', hash: 'ghi789', size: 2000 }, | ||
| ], | ||
| }, | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Is this related to this PR or some merge artifact?
There was a problem hiding this comment.
It was already present before my merge commits: https://github.com/npmx-dev/npmx.dev/pull/1723/changes/BASE..dffa4f2d185b93d6107e20589a219328efcf3407#diff-352c8868c0ceed7c416a532e448dc5865ca568d25b33f6f911099d9e885f4af0
🔗 Linked issue
Closes#1120
Closes#1416
🧭 Context
We have two seemingly separate issues:
^18.0.0 || ^19.0.0,>15 <=16.0.2) currently navigate to a non-existent version page and result in a 404.nuxt@4.0.1ornuxt@>=4ornuxt@>=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
https://npmx.dev/package/nuxt/v/:versionwill route to the specific versioned package page ifversionis a specific version string and route to the package page with?semver=<version>otherwise.nuxt@4.0.1ornuxt@>=4ornuxt@>=4<6and 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