Skip to content

Commit 9ca57f5

Browse files
authored
Merge branch 'main' into vt/sort-dependents
2 parents 58f5462 + 1ffdc05 commit 9ca57f5

25 files changed

Lines changed: 876 additions & 134 deletions

README.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,18 @@ The aim of [npmx.dev](https://npmx.dev) is to provide a better browser for the n
2929
- **Provenance indicators** – verified build badges for packages with npm provenance
3030
- **Multi-provider repository support** – stars/forks from GitHub, GitLab, Bitbucket, Codeberg, Gitee, and Sourcehut
3131
- **JSR availability** – see if scoped packages are also available on JSR
32-
- **Package badges** – module format (ESM/CJS/dual), TypeScript types, and engine constraints
32+
- **Package badges** – module format (ESM/CJS/dual), TypeScript types (with `@types/*` links), and engine constraints
3333
- **Outdated dependency indicators** – visual cues showing which dependencies are behind
3434
- **Vulnerability warnings** – security advisories from the OSV database
3535
- **Download statistics** – weekly download counts with sparkline charts
36-
- **Install size** – total install size including dependencies
36+
- **Install size** – total install size (including transitive dependencies)
3737
- **Playground links** – quick access to StackBlitz, CodeSandbox, and other demo environments from READMEs
3838
- **Infinite search** – auto-load additional search pages as you scroll
39-
- **Keyboard navigation** – press `/` to focus search, arrow keys to navigate results, Enter to select
39+
- **Keyboard navigation** – press `/` to focus search, `.` to open code viewer, arrow keys to navigate results
40+
- **Deprecation notices** – clear warnings for deprecated packages and versions
41+
- **Version range resolution** – dependency ranges (e.g., `^1.0.0`) resolve to actual installed versions
4042
- **Claim new packages** – register new package names directly from search results (via local connector)
43+
- **Clickable version tags** – navigate directly to any version from the versions list
4144

4245
### User & org pages
4346

@@ -65,10 +68,12 @@ The aim of [npmx.dev](https://npmx.dev) is to provide a better browser for the n
6568
| Install size calculation |||
6669
| JSR cross-reference |||
6770
| Vulnerability warnings |||
71+
| Deprecation notices |||
6872
| Download charts |||
6973
| Playground links |||
7074
| Keyboard navigation |||
7175
| Multi-provider repo support |||
76+
| Version range resolution |||
7277
| Dependents list || 🚧 |
7378
| Package admin (access/owners) || 🚧 |
7479
| Org/team management || 🚧 |
@@ -105,12 +110,13 @@ npmx.dev supports npm permalinks – just replace `npmjs.com` with `npmx.dev
105110

106111
npmx.dev also supports shorter, cleaner URLs:
107112

108-
| Pattern | Example |
109-
| -------------- | -------------------------------------------------- |
110-
| `/<package>` | [`/nuxt`](https://npmx.dev/nuxt) |
111-
| `/@scope/name` | [`/@nuxt/kit`](https://npmx.dev/@nuxt/kit) |
112-
| `/@org` | [`/@nuxt`](https://npmx.dev/@nuxt) |
113-
| `/~username` | [`/~sindresorhus`](https://npmx.dev/~sindresorhus) |
113+
| Pattern | Example |
114+
| ------------------ | -------------------------------------------------- |
115+
| `/<package>` | [`/nuxt`](https://npmx.dev/nuxt) |
116+
| `/<pkg>@<version>` | [`/vue@3.4.0`](https://npmx.dev/vue@3.4.0) |
117+
| `/@scope/name` | [`/@nuxt/kit`](https://npmx.dev/@nuxt/kit) |
118+
| `/@org` | [`/@nuxt`](https://npmx.dev/@nuxt) |
119+
| `/~username` | [`/~sindresorhus`](https://npmx.dev/~sindresorhus) |
114120

115121
## Tech stack
116122

@@ -122,7 +128,7 @@ npmx.dev also supports shorter, cleaner URLs:
122128

123129
## Contributing
124130

125-
I'd welcome contributions &ndash; please do feel free to poke around and improve things. See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on how to get up and running!
131+
We welcome contributions &ndash; please do feel free to poke around and improve things. See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on how to get up and running!
126132

127133
## Related projects
128134

@@ -132,7 +138,7 @@ I'd welcome contributions &ndash; please do feel free to poke around and improve
132138
- [npm-alt](https://npm.willow.sh/) &ndash; An alternative npm package browser
133139
- [npkg.lorypelli.dev](https://npkg.lorypelli.dev/) &ndash; An alternative frontend to npm made with as little client-side JavaScript as possible
134140

135-
If you're building something cool, let me know! 🙏
141+
If you're building something cool, let us know! 🙏
136142

137143
## License
138144

app/components/AppFooter.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ onUnmounted(() => {
8383
: // JS-controlled: fixed position, hidden by default, transition only after mount
8484
isScrollable
8585
? [
86-
'fixed bottom-0 left-0 right-0 z-40 translate-y-full',
86+
'fixed bottom-0 left-0 right-0 z-40',
8787
isMounted && 'transition-transform duration-300 ease-out',
88-
isVisible && 'translate-y-0',
88+
isVisible ? 'translate-y-0' : 'translate-y-full',
8989
]
9090
: 'mt-auto',
9191
]"

app/components/AppHeader.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ withDefaults(
3030
<NuxtLink
3131
to="/search"
3232
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
33+
aria-keyshortcuts="/"
3334
>
3435
search
3536
<kbd
3637
class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
37-
>/</kbd
38+
aria-hidden="true"
3839
>
40+
/
41+
</kbd>
3942
</NuxtLink>
4043
</li>
4144
<li v-if="showConnector" class="flex">

app/components/PackageCard.vue

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@ const emit = defineEmits<{
3131
@focus="index != null && emit('focus', index)"
3232
@mouseenter="index != null && emit('focus', index)"
3333
>
34-
<header class="flex items-start justify-between gap-4 mb-2">
34+
<header class="flex items-start justify-between gap-2 sm:gap-4 mb-2">
3535
<component
3636
:is="headingLevel ?? 'h3'"
37-
class="font-mono text-base font-medium text-fg group-hover:text-fg transition-colors duration-200 min-w-0 break-all"
37+
class="font-mono text-sm sm:text-base font-medium text-fg group-hover:text-fg transition-colors duration-200 min-w-0 break-words"
3838
>
3939
{{ result.package.name }}
4040
</component>
41-
<div class="flex items-center gap-1.5 shrink-0 max-w-32">
41+
<div class="flex items-center gap-1.5 shrink-0">
4242
<span
4343
v-if="result.package.version"
44-
class="font-mono text-xs text-fg-subtle truncate"
44+
class="font-mono text-xs text-fg-subtle truncate max-w-20 sm:max-w-32"
4545
:title="result.package.version"
4646
>
4747
v{{ result.package.version }}
@@ -56,11 +56,14 @@ const emit = defineEmits<{
5656
</div>
5757
</header>
5858

59-
<p v-if="result.package.description" class="text-fg-muted text-sm line-clamp-2 mb-3">
59+
<p
60+
v-if="result.package.description"
61+
class="text-fg-muted text-xs sm:text-sm line-clamp-2 mb-2 sm:mb-3"
62+
>
6063
<MarkdownText :text="result.package.description" />
6164
</p>
6265

63-
<footer class="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-fg-subtle">
66+
<footer class="flex flex-wrap items-center gap-x-3 sm:gap-x-4 gap-y-2 text-xs text-fg-subtle">
6467
<dl v-if="showPublisher || result.package.date" class="flex items-center gap-4 m-0">
6568
<div
6669
v-if="showPublisher && result.package.publisher?.username"

app/components/PackageDependencies.vue

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
<script setup lang="ts">
2-
import { useOutdatedDependencies, getOutdatedTooltip } from '~/composables/useNpmRegistry'
3-
import type { OutdatedDependencyInfo } from '~/composables/useNpmRegistry'
4-
52
const props = defineProps<{
63
packageName: string
74
dependencies?: Record<string, string>
@@ -13,20 +10,6 @@ const props = defineProps<{
1310
// Fetch outdated info for dependencies
1411
const outdatedDeps = useOutdatedDependencies(() => props.dependencies)
1512
16-
/**
17-
* Get CSS class for a dependency version based on outdated status
18-
*/
19-
function getVersionClass(info: OutdatedDependencyInfo | undefined): string {
20-
if (!info) return 'text-fg-subtle'
21-
22-
// Red for major versions behind
23-
if (info.majorsBehind > 0) return 'text-red-500 cursor-help'
24-
// Orange for minor versions behind
25-
if (info.minorsBehind > 0) return 'text-orange-500 cursor-help'
26-
// Yellow for patch versions behind
27-
return 'text-yellow-500 cursor-help'
28-
}
29-
3013
// Expanded state for each section
3114
const depsExpanded = ref(false)
3215
const peerDepsExpanded = ref(false)
@@ -89,15 +72,16 @@ const sortedOptionalDependencies = computed(() => {
8972
:title="getOutdatedTooltip(outdatedDeps[dep])"
9073
aria-hidden="true"
9174
>
92-
<span class="i-carbon-warning-alt w-3 h-3" />
75+
<span class="i-carbon-warning-alt w-3 h-3 block" />
9376
</span>
94-
<span
77+
<NuxtLink
78+
:to="{ name: 'package', params: { package: [...dep.split('/'), 'v', version] } }"
9579
class="font-mono text-xs text-right truncate"
9680
:class="getVersionClass(outdatedDeps[dep])"
9781
:title="outdatedDeps[dep] ? getOutdatedTooltip(outdatedDeps[dep]) : version"
9882
>
9983
{{ version }}
100-
</span>
84+
</NuxtLink>
10185
<span v-if="outdatedDeps[dep]" class="sr-only">
10286
({{ getOutdatedTooltip(outdatedDeps[dep]) }})
10387
</span>
@@ -143,12 +127,16 @@ const sortedOptionalDependencies = computed(() => {
143127
optional
144128
</span>
145129
</div>
146-
<span
130+
<NuxtLink
131+
:to="{
132+
name: 'package',
133+
params: { package: [...peer.name.split('/'), 'v', peer.version] },
134+
}"
147135
class="font-mono text-xs text-fg-subtle max-w-[40%] text-right truncate"
148136
:title="peer.version"
149137
>
150138
{{ peer.version }}
151-
</span>
139+
</NuxtLink>
152140
</li>
153141
</ul>
154142
<button
@@ -187,12 +175,13 @@ const sortedOptionalDependencies = computed(() => {
187175
>
188176
{{ dep }}
189177
</NuxtLink>
190-
<span
178+
<NuxtLink
179+
:to="{ name: 'package', params: { package: [...dep.split('/'), 'v', version] } }"
191180
class="font-mono text-xs text-fg-subtle max-w-[50%] text-right truncate"
192181
:title="version"
193182
>
194183
{{ version }}
195-
</span>
184+
</NuxtLink>
196185
</li>
197186
</ul>
198187
<button
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
packageName: string
4+
installScripts: {
5+
scripts: ('preinstall' | 'install' | 'postinstall')[]
6+
content?: Record<string, string>
7+
npxDependencies: Record<string, string>
8+
}
9+
}>()
10+
11+
const outdatedNpxDeps = useOutdatedDependencies(() => props.installScripts.npxDependencies)
12+
const hasNpxDeps = computed(() => Object.keys(props.installScripts.npxDependencies).length > 0)
13+
const sortedNpxDeps = computed(() => {
14+
return Object.entries(props.installScripts.npxDependencies).sort(([a], [b]) => a.localeCompare(b))
15+
})
16+
17+
const isExpanded = ref(false)
18+
</script>
19+
20+
<template>
21+
<section aria-labelledby="install-scripts-heading">
22+
<h2
23+
id="install-scripts-heading"
24+
class="text-xs text-fg-subtle uppercase tracking-wider mb-3 flex items-center gap-2"
25+
>
26+
<span class="i-carbon-warning-alt w-3 h-3 text-yellow-500" aria-hidden="true" />
27+
Install Scripts
28+
</h2>
29+
30+
<!-- Script list: name as label, content below -->
31+
<dl class="space-y-2 m-0">
32+
<div v-for="scriptName in installScripts.scripts" :key="scriptName">
33+
<dt class="font-mono text-xs text-fg-muted">{{ scriptName }}</dt>
34+
<dd
35+
tabindex="0"
36+
class="font-mono text-sm text-fg-subtle m-0 truncate focus:whitespace-normal focus:overflow-visible cursor-help rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
37+
:title="installScripts.content?.[scriptName]"
38+
>
39+
{{ installScripts.content?.[scriptName] || '(script)' }}
40+
</dd>
41+
</div>
42+
</dl>
43+
44+
<!-- npx packages (expandable) -->
45+
<div v-if="hasNpxDeps" class="mt-3">
46+
<button
47+
type="button"
48+
class="flex items-center gap-1.5 text-xs text-fg-muted hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
49+
:aria-expanded="isExpanded"
50+
aria-controls="npx-packages-details"
51+
@click="isExpanded = !isExpanded"
52+
>
53+
<span
54+
class="i-carbon-chevron-right w-3 h-3 transition-transform duration-200"
55+
:class="{ 'rotate-90': isExpanded }"
56+
aria-hidden="true"
57+
/>
58+
{{ sortedNpxDeps.length }} npx package{{ sortedNpxDeps.length !== 1 ? 's' : '' }}
59+
</button>
60+
61+
<ul
62+
v-show="isExpanded"
63+
id="npx-packages-details"
64+
class="mt-2 space-y-1 list-none m-0 p-0 pl-4"
65+
>
66+
<li
67+
v-for="[dep, version] in sortedNpxDeps"
68+
:key="dep"
69+
class="flex items-center justify-between py-0.5 text-sm gap-2"
70+
>
71+
<NuxtLink
72+
:to="{ name: 'package', params: { package: dep.split('/') } }"
73+
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0"
74+
>
75+
{{ dep }}
76+
</NuxtLink>
77+
<span class="flex items-center gap-1">
78+
<span
79+
v-if="
80+
outdatedNpxDeps[dep] &&
81+
outdatedNpxDeps[dep].resolved !== outdatedNpxDeps[dep].latest
82+
"
83+
class="shrink-0"
84+
:class="getVersionClass(outdatedNpxDeps[dep])"
85+
:title="getOutdatedTooltip(outdatedNpxDeps[dep])"
86+
aria-hidden="true"
87+
>
88+
<span class="i-carbon-warning-alt w-3 h-3 block" />
89+
</span>
90+
<span
91+
class="font-mono text-xs text-right truncate"
92+
:class="getVersionClass(outdatedNpxDeps[dep])"
93+
:title="
94+
outdatedNpxDeps[dep]
95+
? outdatedNpxDeps[dep].resolved === outdatedNpxDeps[dep].latest
96+
? `currently ${outdatedNpxDeps[dep].latest}`
97+
: getOutdatedTooltip(outdatedNpxDeps[dep])
98+
: version
99+
"
100+
>
101+
{{ version }}
102+
</span>
103+
</span>
104+
</li>
105+
</ul>
106+
</div>
107+
</section>
108+
</template>

app/components/PackageListControls.vue

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,40 +52,42 @@ const showFilteredCount = computed(() => {
5252
<!-- Filter input -->
5353
<div class="flex-1 relative">
5454
<label for="package-filter" class="sr-only">Filter packages</label>
55-
<span
56-
class="absolute left-3 top-1/2 -translate-y-1/2 text-fg-subtle pointer-events-none"
55+
<div
56+
class="absolute h-full w-10 flex items-center justify-center text-fg-subtle pointer-events-none"
5757
aria-hidden="true"
5858
>
59-
<span class="i-carbon-search inline-block w-4 h-4" />
60-
</span>
59+
<div class="i-carbon-search inline-block w-4 h-4" />
60+
</div>
6161
<input
6262
id="package-filter"
6363
v-model="filterValue"
6464
type="search"
6565
:placeholder="placeholder ?? 'Filter packages...'"
6666
autocomplete="off"
67-
class="w-full bg-bg-subtle border border-border rounded-lg pl-9 pr-4 py-2 font-mono text-sm text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:(border-border-hover outline-none)"
67+
class="w-full bg-bg-subtle border border-border rounded-lg pl-10 pr-4 py-2 font-mono text-sm text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:(border-border-hover outline-none)"
6868
/>
6969
</div>
7070

7171
<!-- Sort select -->
72-
<div class="relative shrink-0">
72+
<div class="relative shrink-0 flex">
7373
<label for="package-sort" class="sr-only">Sort packages</label>
74-
<select
75-
id="package-sort"
76-
v-model="sortValue"
77-
class="appearance-none bg-bg-subtle border border-border rounded-lg pl-3 pr-8 py-2 font-mono text-sm text-fg cursor-pointer transition-colors duration-200 focus:(border-border-hover outline-none) hover:border-border-hover"
78-
>
79-
<option v-for="option in sortOptions" :key="option.value" :value="option.value">
80-
{{ option.label }}
81-
</option>
82-
</select>
83-
<span
84-
class="absolute right-3 top-1/2 -translate-y-1/2 text-fg-subtle pointer-events-none"
85-
aria-hidden="true"
86-
>
87-
<span class="i-carbon-chevron-down w-4 h-4" />
88-
</span>
74+
<div class="relative">
75+
<select
76+
id="package-sort"
77+
v-model="sortValue"
78+
class="appearance-none bg-bg-subtle border border-border rounded-lg pl-3 pr-8 py-2 font-mono text-sm text-fg cursor-pointer transition-colors duration-200 focus:(border-border-hover outline-none) hover:border-border-hover"
79+
>
80+
<option v-for="option in sortOptions" :key="option.value" :value="option.value">
81+
{{ option.label }}
82+
</option>
83+
</select>
84+
<div
85+
class="absolute right-3 top-1/2 -translate-y-1/2 text-fg-subtle pointer-events-none"
86+
aria-hidden="true"
87+
>
88+
<div class="i-carbon-chevron-down w-4 h-4" />
89+
</div>
90+
</div>
8991
</div>
9092
</div>
9193

0 commit comments

Comments
 (0)