···11<script setup lang="ts">
22import type { PackageVersionInfo, SlimVersion } from '#shared/types'
33-import { compare } from 'semver'
33+import { compare, validRange } from 'semver'
44import type { RouteLocationRaw } from 'vue-router'
55import { fetchAllPackageVersions } from '~/utils/npm/api'
66+import { NPMX_DOCS_SITE } from '#shared/utils/constants'
67import {
78 buildVersionToTagsMap,
89 filterExcludedTags,
1010+ filterVersions,
911 getPrereleaseChannel,
1012 getVersionGroupKey,
1113 getVersionGroupLabel,
···8385 () => props.selectedVersion ?? props.distTags.latest ?? undefined,
8486)
85878888+// Semver range filter
8989+const semverFilter = ref('')
9090+// Collect all known versions: initial props + dynamically loaded ones
9191+const allKnownVersions = computed(() => {
9292+ const versions = new Set(Object.keys(props.versions))
9393+ for (const versionList of tagVersions.value.values()) {
9494+ for (const v of versionList) {
9595+ versions.add(v.version)
9696+ }
9797+ }
9898+ for (const group of otherMajorGroups.value) {
9999+ for (const v of group.versions) {
100100+ versions.add(v.version)
101101+ }
102102+ }
103103+ return [...versions]
104104+})
105105+const filteredVersionSet = computed(() =>
106106+ filterVersions(allKnownVersions.value, semverFilter.value),
107107+)
108108+const isFilterActive = computed(() => semverFilter.value.trim() !== '')
109109+const isInvalidRange = computed(
110110+ () => isFilterActive.value && validRange(semverFilter.value.trim()) === null,
111111+)
112112+86113// All tag rows derived from props (SSR-safe)
87114// Deduplicates so each version appears only once, with all its tags
88115const allTagRows = computed(() => {
···135162136163// Visible tag rows: limited to MAX_VISIBLE_TAGS
137164// If package is NOT deprecated, filter out deprecated tags from visible list
165165+// When semver filter is active, also filter by matching version
138166const visibleTagRows = computed(() => {
139139- const rows = isPackageDeprecated.value
167167+ const rowsMaybeFilteredForDeprecation = isPackageDeprecated.value
140168 ? allTagRows.value
141169 : allTagRows.value.filter(row => !row.primaryVersion.deprecated)
170170+ const rows = isFilterActive.value
171171+ ? rowsMaybeFilteredForDeprecation.filter(row =>
172172+ filteredVersionSet.value.has(row.primaryVersion.version),
173173+ )
174174+ : rowsMaybeFilteredForDeprecation
142175 const first = rows.slice(0, MAX_VISIBLE_TAGS)
143176 const latestTagRow = rows.find(row => row.tag === 'latest')
144177 // Ensure 'latest' tag is always included (at the end) if not already present
···150183})
151184152185// Hidden tag rows (all other tags) - shown in "Other versions"
153153-const hiddenTagRows = computed(() =>
154154- allTagRows.value.filter(row => !visibleTagRows.value.includes(row)),
155155-)
186186+// When semver filter is active, also filter by matching version
187187+const hiddenTagRows = computed(() => {
188188+ const hiddenRows = allTagRows.value.filter(row => !visibleTagRows.value.includes(row))
189189+ const rows = isFilterActive.value
190190+ ? hiddenRows.filter(row => filteredVersionSet.value.has(row.primaryVersion.version))
191191+ : hiddenRows
192192+ return rows
193193+})
156194157195// Client-side state for expansion and loaded versions
158196const expandedTags = ref<Set<string>>(new Set())
···165203 Array<{ groupKey: string; label: string; versions: VersionDisplay[] }>
166204>([])
167205const otherVersionsLoading = shallowRef(false)
206206+207207+// Filtered major groups (applies semver filter when active)
208208+const filteredOtherMajorGroups = computed(() => {
209209+ if (!isFilterActive.value) return otherMajorGroups.value
210210+ return otherMajorGroups.value
211211+ .map(group => ({
212212+ ...group,
213213+ versions: group.versions.filter(v => filteredVersionSet.value.has(v.version)),
214214+ }))
215215+ .filter(group => group.versions.length > 0)
216216+})
217217+218218+// Whether the filter is active but nothing matches anywhere
219219+const hasNoFilterMatches = computed(() => {
220220+ if (!isFilterActive.value) return false
221221+ return (
222222+ visibleTagRows.value.length === 0 &&
223223+ hiddenTagRows.value.length === 0 &&
224224+ filteredOtherMajorGroups.value.length === 0
225225+ )
226226+})
168227169228// Cached full version list (local to component instance)
170229const allVersionsCache = shallowRef<PackageVersionInfo[] | null>(null)
···340399 return tagVersions.value.get(tag) ?? []
341400}
342401402402+// Get filtered versions for a tag (applies semver filter when active)
403403+function getFilteredTagVersions(tag: string): VersionDisplay[] {
404404+ const versions = getTagVersions(tag)
405405+ if (!isFilterActive.value) return versions
406406+ return versions.filter(v => filteredVersionSet.value.has(v.version))
407407+}
408408+343409function findClaimingTag(version: string): string | null {
344410 const versionChannel = getPrereleaseChannel(version)
345411···418484 </ButtonBase>
419485 </template>
420486 <div class="space-y-0.5 min-w-0">
487487+ <!-- Semver range filter -->
488488+ <div class="px-1 pb-1">
489489+ <div class="flex items-center gap-1.5">
490490+ <InputBase
491491+ v-model="semverFilter"
492492+ type="text"
493493+ :placeholder="$t('package.versions.filter_placeholder')"
494494+ :aria-label="$t('package.versions.filter_placeholder')"
495495+ :aria-invalid="isInvalidRange ? 'true' : undefined"
496496+ :aria-describedby="isInvalidRange ? 'semver-filter-error' : undefined"
497497+ autocomplete="off"
498498+ class="flex-1 min-w-0"
499499+ :class="isInvalidRange ? '!border-red-500' : ''"
500500+ size="small"
501501+ />
502502+ <TooltipApp interactive position="top">
503503+ <span
504504+ tabindex="0"
505505+ class="i-carbon:information w-3.5 h-3.5 text-fg-subtle cursor-help shrink-0 rounded-sm"
506506+ role="img"
507507+ :aria-label="$t('package.versions.filter_help')"
508508+ />
509509+ <template #content>
510510+ <p class="text-xs text-fg-muted">
511511+ <i18n-t keypath="package.versions.filter_tooltip" tag="span">
512512+ <template #link>
513513+ <LinkBase :to="`${NPMX_DOCS_SITE}/guide/semver-ranges`">{{
514514+ $t('package.versions.filter_tooltip_link')
515515+ }}</LinkBase>
516516+ </template>
517517+ </i18n-t>
518518+ </p>
519519+ </template>
520520+ </TooltipApp>
521521+ </div>
522522+ <p
523523+ v-if="isInvalidRange"
524524+ id="semver-filter-error"
525525+ class="text-red-500 text-3xs mt-1"
526526+ role="alert"
527527+ >
528528+ {{ $t('package.versions.filter_invalid') }}
529529+ </p>
530530+ </div>
531531+532532+ <!-- No matches message -->
533533+ <div
534534+ v-if="hasNoFilterMatches"
535535+ class="px-1 py-2 text-xs text-fg-subtle"
536536+ role="status"
537537+ aria-live="polite"
538538+ >
539539+ {{ $t('package.versions.no_matches') }}
540540+ </div>
541541+421542 <!-- Dist-tag rows (limited to MAX_VISIBLE_TAGS) -->
422543 <div v-for="row in visibleTagRows" :key="row.id">
423544 <div
···512633513634 <!-- Expanded versions -->
514635 <div
515515- v-if="expandedTags.has(row.tag) && getTagVersions(row.tag).length > 1"
636636+ v-if="expandedTags.has(row.tag) && getFilteredTagVersions(row.tag).length > 1"
516637 class="ms-4 ps-2 border-is border-border space-y-0.5 pe-2"
517638 >
518639 <div
519519- v-for="v in getTagVersions(row.tag).slice(1)"
640640+ v-for="v in getFilteredTagVersions(row.tag).slice(1)"
520641 :key="v.version"
521642 class="py-1"
522643 :class="v.version === effectiveCurrentVersion ? 'rounded bg-bg-subtle px-2 -mx-2' : ''"
···533654 "
534655 :title="
535656 v.deprecated
536536- ? $t('package.versions.deprecated_title', { version: v.version })
657657+ ? $t('package.versions.deprecated_title', {
658658+ version: v.version,
659659+ })
537660 : v.version
538661 "
539662 :classicon="v.deprecated ? 'i-carbon-warning-hex' : undefined"
···676799 </div>
677800678801 <!-- Version groups (untagged versions) -->
679679- <template v-if="otherMajorGroups.length > 0">
680680- <div v-for="group in otherMajorGroups" :key="group.groupKey">
802802+ <template v-if="filteredOtherMajorGroups.length > 0">
803803+ <div v-for="group in filteredOtherMajorGroups" :key="group.groupKey">
681804 <!-- Version group header -->
682805 <div
683806 v-if="group.versions.length > 1"
···692815 :aria-expanded="expandedMajorGroups.has(group.groupKey)"
693816 :aria-label="
694817 expandedMajorGroups.has(group.groupKey)
695695- ? $t('package.versions.collapse_major', { major: group.label })
696696- : $t('package.versions.expand_major', { major: group.label })
818818+ ? $t('package.versions.collapse_major', {
819819+ major: group.label,
820820+ })
821821+ : $t('package.versions.expand_major', {
822822+ major: group.label,
823823+ })
697824 "
698825 data-testid="major-group-expand-button"
699826 @click="toggleMajorGroup(group.groupKey)"
···852979 "
853980 :title="
854981 v.deprecated
855855- ? $t('package.versions.deprecated_title', { version: v.version })
982982+ ? $t('package.versions.deprecated_title', {
983983+ version: v.version,
984984+ })
856985 : v.version
857986 "
858987 :classicon="v.deprecated ? 'i-carbon-warning-hex' : undefined"
+29-1
app/utils/versions.ts
···11-import { compare, valid } from 'semver'
11+import { compare, satisfies, validRange, valid } from 'semver'
2233/**
44 * Utilities for handling npm package versions and dist-tags
···179179export function isSameVersionGroup(versionA: string, versionB: string): boolean {
180180 return getVersionGroupKey(versionA) === getVersionGroupKey(versionB)
181181}
182182+183183+/**
184184+ * Filter versions by a semver range string.
185185+ *
186186+ * @param versions - Array of version strings to filter
187187+ * @param range - A semver range string (e.g., "^3.0.0", ">=2.0.0 <3.0.0")
188188+ * @returns Set of version strings that satisfy the range.
189189+ * Returns all versions if range is empty/whitespace.
190190+ * Returns empty set if range is invalid.
191191+ */
192192+export function filterVersions(versions: string[], range: string): Set<string> {
193193+ const trimmed = range.trim()
194194+ if (trimmed === '') {
195195+ return new Set(versions)
196196+ }
197197+198198+ if (!validRange(trimmed)) {
199199+ return new Set()
200200+ }
201201+202202+ const matched = new Set<string>()
203203+ for (const v of versions) {
204204+ if (satisfies(v, trimmed, { includePrerelease: true })) {
205205+ matched.add(v)
206206+ }
207207+ }
208208+ return matched
209209+}
+58
docs/content/2.guide/5.semver-ranges.md
···11+---
22+title: Semver Ranges
33+description: Learn how to use semver ranges to filter package versions on npmx.dev
44+navigation:
55+ icon: i-lucide-filter
66+---
77+88+npm uses [semantic versioning](https://semver.org/) (semver) to manage package versions. A **semver range** is a string that describes a set of version numbers. On npmx, you can type a semver range into the version filter input on any package page to quickly find matching versions.
99+1010+## Version format
1111+1212+Every npm version follows the format **MAJOR.MINOR.PATCH**, for example `3.2.1`:
1313+1414+- **MAJOR** - incremented for breaking changes
1515+- **MINOR** - incremented for new features (backwards-compatible)
1616+- **PATCH** - incremented for bug fixes (backwards-compatible)
1717+1818+Some versions also include a **prerelease** tag, such as `4.0.0-beta.1`.
1919+2020+## Common range syntax
2121+2222+| Range | Meaning | Example matches |
2323+| ---------------- | ------------------------------------------------- | -------------------- |
2424+| `*` | Any version | 0.0.2, 3.1.0, 3.2.6 |
2525+| `^3.0.0` | Compatible with 3.x (same major) | 3.0.0, 3.1.0, 3.9.5 |
2626+| `~3.2.0` | At least 3.2.0, same major.minor | 3.2.0, 3.2.1, 3.2.99 |
2727+| `3.2.x` | At least 3.2.0, same major.minor | 3.2.0, 3.2.1, 3.2.99 |
2828+| `>=2.0.0 <3.0.0` | At least 2.0.0 but below 3.0.0 | 2.0.0, 2.5.3, 2.99.0 |
2929+| `1.2.3` | Exactly this version | 1.2.3 |
3030+| `=1.2.3` | Exactly this version | 1.2.3 |
3131+| `^0.3.1` | At least 0.3.1, same major.minor (0.x is special) | 0.3.1, 0.3.2 |
3232+| `^0.0.4` | Exactly 0.0.4 (0.0.x is special) | 0.0.4 (only) |
3333+3434+## Examples
3535+3636+### Find all 3.x versions
3737+3838+Type `^3.0.0` to see every version compatible with major version 3.
3939+4040+### Find patch releases for a specific minor
4141+4242+Type `~2.4.0` to see only 2.4.x patch releases (2.4.0, 2.4.1, 2.4.2, etc.).
4343+4444+### Find versions in a specific range
4545+4646+Type `>=1.0.0 <2.0.0` to see all 1.x stable releases.
4747+4848+### Find a specific version
4949+5050+Type the exact version number, like `5.3.1`, to check if it exists.
5151+5252+### Find prerelease versions
5353+5454+Type `>=3.0.0-alpha.0` to find alpha, beta, and release candidate versions for a major release.
5555+5656+## Learn more
5757+5858+The full semver range specification is documented at [node-semver](https://github.com/npm/node-semver#ranges).
+7-1
i18n/locales/en.json
···300300 "show_low_usage": "Show low usage versions",
301301 "show_low_usage_tooltip": "Include version groups with less than 1% of total downloads.",
302302 "date_range_tooltip": "Last week of version distributions only",
303303- "y_axis_label": "Downloads"
303303+ "y_axis_label": "Downloads",
304304+ "filter_placeholder": "Filter by semver (e.g. ^3.0.0)",
305305+ "filter_invalid": "Invalid semver range",
306306+ "filter_help": "Semver range filter help",
307307+ "filter_tooltip": "Filter versions using a {link}. For example, ^3.0.0 shows all 3.x versions.",
308308+ "filter_tooltip_link": "semver range",
309309+ "no_matches": "No versions match this range"
304310 },
305311 "dependencies": {
306312 "title": "Dependency ({count}) | Dependencies ({count})",
+7-1
i18n/locales/fr-FR.json
···278278 "more_tagged": "{count} de plus avec tag",
279279 "all_covered": "Toutes les versions sont couvertes par les tags ci-dessus",
280280 "deprecated_title": "{version} (dépréciée)",
281281- "view_all": "Voir la version | Voir les {count} versions"
281281+ "view_all": "Voir la version | Voir les {count} versions",
282282+ "filter_placeholder": "Filtrer par plage semver (ex. ^3.0.0)",
283283+ "filter_invalid": "Plage semver invalide",
284284+ "filter_help": "Infos sur le filtre de plage semver",
285285+ "filter_tooltip": "Filtrer les versions avec une {link}. Par exemple, ^3.0.0 affiche toutes les versions 3.x.",
286286+ "filter_tooltip_link": "plage semver",
287287+ "no_matches": "Aucune version ne correspond à cette plage"
282288 },
283289 "dependencies": {
284290 "title": "Dépendances ({count})",
···299299 "show_low_usage": "Show low usage versions",
300300 "show_low_usage_tooltip": "Include version groups with less than 1% of total downloads.",
301301 "date_range_tooltip": "Last week of version distributions only",
302302- "y_axis_label": "Downloads"
302302+ "y_axis_label": "Downloads",
303303+ "filter_placeholder": "Filter by semver (e.g. ^3.0.0)",
304304+ "filter_invalid": "Invalid semver range",
305305+ "filter_help": "Semver range filter help",
306306+ "filter_tooltip": "Filter versions using a {link}. For example, ^3.0.0 shows all 3.x versions.",
307307+ "filter_tooltip_link": "semver range",
308308+ "no_matches": "No versions match this range"
303309 },
304310 "dependencies": {
305311 "title": "Dependency ({count}) | Dependencies ({count})",
+7-1
lunaria/files/en-US.json
···299299 "show_low_usage": "Show low usage versions",
300300 "show_low_usage_tooltip": "Include version groups with less than 1% of total downloads.",
301301 "date_range_tooltip": "Last week of version distributions only",
302302- "y_axis_label": "Downloads"
302302+ "y_axis_label": "Downloads",
303303+ "filter_placeholder": "Filter by semver (e.g. ^3.0.0)",
304304+ "filter_invalid": "Invalid semver range",
305305+ "filter_help": "Semver range filter help",
306306+ "filter_tooltip": "Filter versions using a {link}. For example, ^3.0.0 shows all 3.x versions.",
307307+ "filter_tooltip_link": "semver range",
308308+ "no_matches": "No versions match this range"
303309 },
304310 "dependencies": {
305311 "title": "Dependency ({count}) | Dependencies ({count})",
+7-1
lunaria/files/fr-FR.json
···277277 "more_tagged": "{count} de plus avec tag",
278278 "all_covered": "Toutes les versions sont couvertes par les tags ci-dessus",
279279 "deprecated_title": "{version} (dépréciée)",
280280- "view_all": "Voir la version | Voir les {count} versions"
280280+ "view_all": "Voir la version | Voir les {count} versions",
281281+ "filter_placeholder": "Filtrer par plage semver (ex. ^3.0.0)",
282282+ "filter_invalid": "Plage semver invalide",
283283+ "filter_help": "Infos sur le filtre de plage semver",
284284+ "filter_tooltip": "Filtrer les versions avec une {link}. Par exemple, ^3.0.0 affiche toutes les versions 3.x.",
285285+ "filter_tooltip_link": "plage semver",
286286+ "no_matches": "Aucune version ne correspond à cette plage"
281287 },
282288 "dependencies": {
283289 "title": "Dépendances ({count})",