Skip to content

Commit b646bd2

Browse files
committed
feat: dependencies page
1 parent 093e33e commit b646bd2

File tree

12 files changed

+441
-17
lines changed

12 files changed

+441
-17
lines changed

app/components/CollapsibleSection.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { shallowRef, computed } from 'vue'
33
44
interface Props {
55
title: string
6+
titleHref?: string
67
isLoading?: boolean
78
headingLevel?: `h${number}`
89
id: string
@@ -61,6 +62,11 @@ const ariaLabel = computed(() => {
6162
const action = isOpen.value ? 'Collapse' : 'Expand'
6263
return props.title ? `${action} ${props.title}` : action
6364
})
65+
66+
const linkHref = computed(() => props.titleHref ?? `#${props.id}`)
67+
68+
const hasCustomHref = computed(() => !!props.titleHref)
69+
6470
useHead({
6571
style: [
6672
{
@@ -104,12 +110,13 @@ useHead({
104110
</button>
105111

106112
<a
107-
:href="`#${id}`"
113+
:href="linkHref"
108114
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
109115
>
110116
{{ title }}
111117
<span
112-
class="i-carbon:link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
118+
class="w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
119+
:class="hasCustomHref ? 'i-carbon:arrow-right' : 'i-carbon:link'"
113120
aria-hidden="true"
114121
/>
115122
</a>

app/components/PackageDependencies.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const sortedOptionalDependencies = computed(() => {
7474
v-if="sortedDependencies.length > 0"
7575
id="dependencies"
7676
:title="$t('package.dependencies.title', { count: sortedDependencies.length })"
77+
:title-href="`/deps/${props.packageName}/v/${props.version}`"
7778
>
7879
<ul class="space-y-1 list-none m-0 p-0" :aria-label="$t('package.dependencies.list_label')">
7980
<li

app/pages/deps/[...path].vue

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
<script setup lang="ts">
2+
import type { PackageDependenciesResponse } from '#shared/types'
3+
import { formatBytes } from '#shared/utils/format'
4+
5+
definePageMeta({
6+
name: 'dependencies',
7+
alias: ['/package/dependencies/:path(.*)*'],
8+
})
9+
10+
const route = useRoute('dependencies')
11+
12+
// Parse package name, version, and file path from URL
13+
// Patterns:
14+
// /dependencies/nuxt/v/4.2.0 → packageName: "nuxt", version: "4.2.0"
15+
// /dependencies/@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", version: "1.0.0"
16+
const parsedRoute = computed(() => {
17+
const segments = route.params.path || []
18+
19+
// Find the /v/ separator for version
20+
const vIndex = segments.indexOf('v')
21+
if (vIndex === -1 || vIndex >= segments.length - 1) {
22+
// No version specified - redirect or error
23+
return {
24+
packageName: segments.join('/'),
25+
version: null as string | null,
26+
}
27+
}
28+
29+
const packageName = segments.slice(0, vIndex).join('/')
30+
const afterVersion = segments.slice(vIndex + 1)
31+
const version = afterVersion[0] ?? null
32+
33+
return { packageName, version }
34+
})
35+
36+
const packageName = computed(() => parsedRoute.value.packageName)
37+
const version = computed(() => parsedRoute.value.version)
38+
39+
// Fetch package data for version list
40+
const { data: pkg } = usePackage(packageName)
41+
42+
// URL pattern for version selector
43+
const versionUrlPattern = computed(() => `/dependencies/${packageName.value}/v/{version}`)
44+
45+
// Fetch dependencies data
46+
const { data: dependencies, status: dependenciesStatus } = useFetch<PackageDependenciesResponse>(
47+
() => `/api/registry/dependencies/${packageName.value}/v/${version.value}`,
48+
{
49+
immediate: !!version.value,
50+
},
51+
)
52+
53+
// Extract org name from scoped package
54+
const orgName = computed(() => {
55+
const name = packageName.value
56+
if (!name.startsWith('@')) return null
57+
const match = name.match(/^@([^/]+)\//)
58+
return match ? match[1] : null
59+
})
60+
61+
// Build route object for package link (with optional version)
62+
function packageRoute(ver?: string | null) {
63+
const segments = packageName.value.split('/')
64+
if (ver) {
65+
segments.push('v', ver)
66+
}
67+
return { name: 'package' as const, params: { package: segments } }
68+
}
69+
70+
// Group dependencies by depth
71+
const directDependencies = computed(
72+
() => dependencies.value?.dependencies.filter(dep => dep.depth === 'direct') ?? [],
73+
)
74+
75+
// Sort dev dependencies alphabetically
76+
const devDependencies = computed(() => {
77+
if (!dependencies.value?.devDependencies) return []
78+
return Object.entries(dependencies.value.devDependencies).sort(([a], [b]) => a.localeCompare(b))
79+
})
80+
81+
// Calculate total size
82+
const totalDirectSize = computed(() =>
83+
directDependencies.value.reduce((sum, dep) => sum + (dep.size || 0), 0),
84+
)
85+
86+
// SEO meta
87+
const pageTitle = computed(() => {
88+
if (packageName.value && version.value) {
89+
return `Dependencies - ${packageName.value}@${version.value} - npmx`
90+
}
91+
return 'Dependencies - npmx'
92+
})
93+
94+
const pageDescription = computed(() => {
95+
if (packageName.value && version.value) {
96+
return `Browse dependency tree for ${packageName.value}@${version.value}`
97+
}
98+
return 'Browse package dependencies'
99+
})
100+
101+
const canonicalUrl = computed(() => {
102+
if (packageName.value && version.value) {
103+
return `https://npmx.dev/dependencies/${packageName.value}/v/${version.value}`
104+
}
105+
return 'https://npmx.dev/dependencies'
106+
})
107+
108+
useHead({
109+
link: [{ rel: 'canonical', href: canonicalUrl }],
110+
})
111+
112+
useSeoMeta({
113+
title: pageTitle,
114+
description: pageDescription,
115+
})
116+
</script>
117+
118+
<template>
119+
<main class="flex-1 flex flex-col">
120+
<!-- Header -->
121+
<header class="border-b border-border bg-bg sticky top-14 z-20">
122+
<div class="container py-4">
123+
<!-- Package info and navigation -->
124+
<div class="flex items-center gap-2 mb-3 flex-wrap min-w-0">
125+
<NuxtLink
126+
:to="packageRoute(version)"
127+
class="font-mono text-lg font-medium hover:text-fg transition-colors min-w-0 truncate max-w-[60vw] sm:max-w-none"
128+
:title="packageName"
129+
>
130+
<span v-if="orgName" class="text-fg-muted">@{{ orgName }}/</span
131+
>{{ orgName ? packageName.replace(`@${orgName}/`, '') : packageName }}
132+
</NuxtLink>
133+
<!-- Version selector -->
134+
<VersionSelector
135+
v-if="version && pkg?.versions && pkg?.['dist-tags']"
136+
:package-name="packageName"
137+
:current-version="version"
138+
:versions="pkg.versions"
139+
:dist-tags="pkg['dist-tags']"
140+
:url-pattern="versionUrlPattern"
141+
/>
142+
<span
143+
v-else-if="version"
144+
class="px-2 py-0.5 font-mono text-sm bg-bg-muted border border-border rounded truncate max-w-32 sm:max-w-48"
145+
:title="`v${version}`"
146+
>
147+
v{{ version }}
148+
</span>
149+
<span class="text-fg-subtle shrink-0">/</span>
150+
<span class="font-mono text-sm text-fg-muted shrink-0">{{
151+
$t('dependencies.title')
152+
}}</span>
153+
</div>
154+
</div>
155+
</header>
156+
157+
<!-- Error: no version -->
158+
<div v-if="!version" class="container py-20 text-center">
159+
<p class="text-fg-muted mb-4">{{ $t('dependencies.version_required') }}</p>
160+
<NuxtLink :to="packageRoute()" class="btn">{{ $t('dependencies.go_to_package') }}</NuxtLink>
161+
</div>
162+
163+
<!-- Loading state -->
164+
<div v-else-if="dependenciesStatus === 'pending'" class="container py-20 text-center">
165+
<div class="i-svg-spinners-ring-resize w-8 h-8 mx-auto text-fg-muted" />
166+
<p class="mt-4 text-fg-muted">{{ $t('dependencies.loading') }}</p>
167+
</div>
168+
169+
<!-- Error state -->
170+
<div
171+
v-else-if="dependenciesStatus === 'error'"
172+
class="container py-20 text-center"
173+
role="alert"
174+
>
175+
<p class="text-fg-muted mb-4">{{ $t('dependencies.error') }}</p>
176+
<NuxtLink :to="packageRoute(version)" class="btn">{{
177+
$t('dependencies.back_to_package')
178+
}}</NuxtLink>
179+
</div>
180+
181+
<!-- Main content -->
182+
<div v-else-if="dependencies" class="container py-6 w-full">
183+
<!-- Dependencies Grid -->
184+
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
185+
<!-- Dependencies -->
186+
<section class="mb-8">
187+
<h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
188+
{{ $t('dependencies.title') }} ({{ dependencies.direct }})
189+
</h2>
190+
191+
<div v-if="directDependencies.length === 0" class="text-fg-muted text-center py-8">
192+
{{ $t('dependencies.no_deps') }}
193+
</div>
194+
<ul v-else class="space-y-1 list-none m-0 p-0">
195+
<li
196+
v-for="dep in directDependencies"
197+
:key="`${dep.name}@${dep.version}`"
198+
class="flex items-center justify-between py-1 text-sm gap-2"
199+
>
200+
<div class="flex items-center gap-2 min-w-0">
201+
<NuxtLink
202+
:to="`/${dep.name}`"
203+
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate"
204+
>
205+
{{ dep.name }}
206+
</NuxtLink>
207+
<span
208+
v-if="dep.optional"
209+
class="px-1 py-0.5 font-mono text-[10px] text-fg-subtle bg-bg-muted border border-border rounded shrink-0"
210+
:title="$t('dependencies.optional')"
211+
>
212+
{{ $t('dependencies.optional') }}
213+
</span>
214+
</div>
215+
<span
216+
class="font-mono text-xs text-fg-subtle max-w-[50%] text-right truncate"
217+
:title="dep.version"
218+
>
219+
{{ dep.version }}
220+
</span>
221+
</li>
222+
</ul>
223+
</section>
224+
225+
<!-- Dev Dependencies -->
226+
<section v-if="devDependencies.length > 0">
227+
<h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
228+
{{ $t('dependencies.dev_title') }} ({{ devDependencies.length }})
229+
</h2>
230+
231+
<ul class="space-y-1 list-none m-0 p-0">
232+
<li
233+
v-for="[dep, version] in devDependencies"
234+
:key="`${dep}@${version}`"
235+
class="flex items-center justify-between py-1 text-sm gap-2"
236+
>
237+
<NuxtLink
238+
:to="`/${dep}`"
239+
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0"
240+
>
241+
{{ dep }}
242+
</NuxtLink>
243+
<span
244+
class="font-mono text-xs text-fg-subtle max-w-[50%] text-right truncate"
245+
:title="version"
246+
>
247+
{{ version }}
248+
</span>
249+
</li>
250+
</ul>
251+
</section>
252+
</div>
253+
</div>
254+
</main>
255+
</template>

lunaria/files/ar-EG.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,5 +725,16 @@
725725
"empty": "لا توجد مؤسسات",
726726
"view_all": "عرض الكل"
727727
}
728+
},
729+
"dependencies": {
730+
"title": "التبعيات",
731+
"dev_title": "تبعيات التطوير",
732+
"loading": "جارٍ تحميل التبعيات...",
733+
"error": "فشل تحميل التبعيات",
734+
"no_deps": "لا توجد تبعيات",
735+
"version_required": "مطلوب إصدار لعرض التبعيات",
736+
"back_to_package": "العودة إلى الحزمة",
737+
"go_to_package": "الانتقال إلى الحزمة",
738+
"optional": "اختياري"
728739
}
729740
}

lunaria/files/en-US.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,5 +839,16 @@
839839
"security": "Security & Compliance"
840840
}
841841
}
842+
},
843+
"dependencies": {
844+
"title": "Dependencies",
845+
"dev_title": "Dev Dependencies",
846+
"loading": "Loading dependencies...",
847+
"error": "Failed to load dependencies",
848+
"no_deps": "No dependencies",
849+
"version_required": "Version is required to view dependencies",
850+
"back_to_package": "Back to package",
851+
"go_to_package": "Go to package",
852+
"optional": "Optional"
842853
}
843854
}

lunaria/files/es-419.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,5 +779,16 @@
779779
"what_is_atmosphere": "¿Qué es una cuenta de la Atmosphere?",
780780
"connect_bluesky": "Conectar con Bluesky"
781781
}
782+
},
783+
"dependencies": {
784+
"title": "Dependencias",
785+
"dev_title": "Dependencias de Desarrollo",
786+
"loading": "Cargando dependencias...",
787+
"error": "Error al cargar dependencias",
788+
"no_deps": "Sin dependencias",
789+
"version_required": "Se requiere una versión para ver las dependencias",
790+
"back_to_package": "Volver al paquete",
791+
"go_to_package": "Ir al paquete",
792+
"optional": "Opcional"
782793
}
783794
}

lunaria/files/es-ES.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,5 +779,16 @@
779779
"what_is_atmosphere": "¿Qué es una cuenta de la Atmosphere?",
780780
"connect_bluesky": "Conectar con Bluesky"
781781
}
782+
},
783+
"dependencies": {
784+
"title": "Dependencias",
785+
"dev_title": "Dependencias de Desarrollo",
786+
"loading": "Cargando dependencias...",
787+
"error": "Error al cargar dependencias",
788+
"no_deps": "Sin dependencias",
789+
"version_required": "Se requiere una versión para ver las dependencias",
790+
"back_to_package": "Volver al paquete",
791+
"go_to_package": "Ir al paquete",
792+
"optional": "Opcional"
782793
}
783794
}

0 commit comments

Comments
 (0)