Skip to content

Commit fd1a816

Browse files
committed
Merge branch 'main' into feat/bookmark-packages
2 parents ae46238 + 5076001 commit fd1a816

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+937
-254
lines changed

.github/workflows/autofix.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ jobs:
4242
- name: 🏃 Update component test snapshots
4343
run: pnpm test:nuxt -u
4444

45-
- name: 🖥️ Update browser test snapshots
46-
run: pnpm test:browser --update-snapshots
45+
# TODO: re-enable when we have snapshots in browser tests
46+
# - name: 🖥️ Update browser test snapshots
47+
# run: pnpm test:browser --update-snapshots
4748

4849
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization.
242242
### Approach
243243

244244
- All user-facing strings should use translation keys via `$t()` in templates and script
245-
- Translation files live in `i18n/locales/` (e.g., `en-US.json`)
245+
- Translation files live in [`i18n/locales/`](i18n/locales) (e.g., `en-US.json`)
246246
- We use the `no_prefix` strategy (no `/en-US/` or `/fr-FR/` in URLs)
247247
- Locale preference is stored in cookies and respected on subsequent visits
248248

app/components/AppFooter.vue

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,51 @@
11
<template>
22
<footer class="border-t border-border mt-auto">
33
<div class="container py-3 sm:py-8 flex flex-col gap-2 sm:gap-4 text-fg-subtle text-sm">
4-
<div class="flex flex-col sm:flex-row items-center justify-start gap-2 sm:gap-4">
5-
<p class="font-mono m-0 hidden sm:block">{{ $t('tagline') }}</p>
6-
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
7-
<div class="flex items-center justify-start gap-3 sm:gap-6">
4+
<div
5+
class="flex flex-col sm:flex-row items-center sm:items-baseline justify-between gap-2 sm:gap-4"
6+
>
7+
<p class="font-mono text-balance m-0 hidden sm:block">{{ $t('tagline') }}</p>
8+
<div class="flex items-center gap-3 sm:gap-6">
89
<NuxtLink
910
to="/about"
1011
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center"
1112
>
12-
<span>{{ $t('footer.about') }}</span>
13+
{{ $t('footer.about') }}
1314
</NuxtLink>
1415
<a
1516
href="https://docs.npmx.dev"
1617
target="_blank"
1718
rel="noopener noreferrer"
18-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center justify-start gap-1"
19+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
1920
>
20-
<span>{{ $t('footer.docs') }}</span>
21+
{{ $t('footer.docs') }}
2122
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
2223
</a>
2324
<a
2425
href="https://repo.npmx.dev"
2526
target="_blank"
2627
rel="noopener noreferrer"
27-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex justify-start items-center gap-1"
28+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
2829
>
29-
<span>{{ $t('footer.source') }}</span>
30+
{{ $t('footer.source') }}
3031
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
3132
</a>
3233
<a
3334
href="https://social.npmx.dev"
3435
target="_blank"
3536
rel="noopener noreferrer"
36-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center justify-start gap-1"
37+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
3738
>
38-
<span>{{ $t('footer.social') }}</span>
39+
{{ $t('footer.social') }}
3940
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
4041
</a>
4142
<a
4243
href="https://chat.npmx.dev"
4344
target="_blank"
4445
rel="noopener noreferrer"
45-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center justify-start gap-1"
46+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
4647
>
47-
<span>{{ $t('footer.chat') }}</span>
48+
{{ $t('footer.chat') }}
4849
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
4950
</a>
5051
</div>

app/components/PackageDependencies.vue

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { useVulnerabilityTree } from '~/composables/useVulnerabilityTree'
2+
import { useDependencyAnalysis } from '~/composables/useDependencyAnalysis'
33
import { SEVERITY_TEXT_COLORS, getHighestSeverity } from '#shared/utils/severity'
44
55
const props = defineProps<{
@@ -15,7 +15,7 @@ const props = defineProps<{
1515
const outdatedDeps = useOutdatedDependencies(() => props.dependencies)
1616
1717
// Get vulnerability info from shared cache (already fetched by PackageVulnerabilityTree)
18-
const { data: vulnTree } = useVulnerabilityTree(
18+
const { data: vulnTree } = useDependencyAnalysis(
1919
() => props.packageName,
2020
() => props.version,
2121
)
@@ -26,6 +26,12 @@ function getVulnerableDepInfo(depName: string) {
2626
return vulnTree.value.vulnerablePackages.find(p => p.name === depName && p.depth === 'direct')
2727
}
2828
29+
// Check if a dependency is deprecated (only direct deps)
30+
function getDeprecatedDepInfo(depName: string) {
31+
if (!vulnTree.value) return null
32+
return vulnTree.value.deprecatedPackages.find(p => p.name === depName && p.depth === 'direct')
33+
}
34+
2935
// Expanded state for each section
3036
const depsExpanded = shallowRef(false)
3137
const peerDepsExpanded = shallowRef(false)
@@ -80,7 +86,7 @@ const sortedOptionalDependencies = computed(() => {
8086
>
8187
{{ $t('package.dependencies.title', { count: sortedDependencies.length }) }}
8288
<span
83-
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
89+
class="i-carbon:link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
8490
aria-hidden="true"
8591
/>
8692
</a>
@@ -89,7 +95,7 @@ const sortedOptionalDependencies = computed(() => {
8995
<li
9096
v-for="[dep, version] in sortedDependencies.slice(0, depsExpanded ? undefined : 10)"
9197
:key="dep"
92-
class="flex items-center justify-between py-1 text-sm gap-2"
98+
class="flex items-center justify-start py-1 text-sm gap-2"
9399
>
94100
<NuxtLink
95101
:to="{ name: 'package', params: { package: dep.split('/') } }"
@@ -105,8 +111,9 @@ const sortedOptionalDependencies = computed(() => {
105111
:title="getOutdatedTooltip(outdatedDeps[dep])"
106112
aria-hidden="true"
107113
>
108-
<span class="i-carbon-warning-alt w-3 h-3 block" />
114+
<span class="i-carbon:warning-alt w-3 h-3 block" />
109115
</span>
116+
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
110117
<NuxtLink
111118
v-if="getVulnerableDepInfo(dep)"
112119
:to="{
@@ -117,12 +124,24 @@ const sortedOptionalDependencies = computed(() => {
117124
:class="SEVERITY_TEXT_COLORS[getHighestSeverity(getVulnerableDepInfo(dep)!.counts)]"
118125
:title="`${getVulnerableDepInfo(dep)!.counts.total} vulnerabilities`"
119126
>
120-
<span class="i-carbon-security w-3 h-3 block" aria-hidden="true" />
127+
<span class="i-carbon:security w-3 h-3 block" aria-hidden="true" />
121128
<span class="sr-only">{{ $t('package.dependencies.view_vulnerabilities') }}</span>
122129
</NuxtLink>
130+
<NuxtLink
131+
v-if="getDeprecatedDepInfo(dep)"
132+
:to="{
133+
name: 'package',
134+
params: { package: [...dep.split('/'), 'v', getDeprecatedDepInfo(dep)!.version] },
135+
}"
136+
class="shrink-0 text-purple-500"
137+
:title="getDeprecatedDepInfo(dep)!.message"
138+
>
139+
<span class="i-carbon-warning-hex w-3 h-3 block" aria-hidden="true" />
140+
<span class="sr-only">{{ $t('package.deprecated.label') }}</span>
141+
</NuxtLink>
123142
<NuxtLink
124143
:to="{ name: 'package', params: { package: [...dep.split('/'), 'v', version] } }"
125-
class="font-mono text-xs text-right truncate"
144+
class="font-mono text-xs text-end truncate"
126145
:class="getVersionClass(outdatedDeps[dep])"
127146
:title="outdatedDeps[dep] ? getOutdatedTooltip(outdatedDeps[dep]) : version"
128147
>
@@ -164,7 +183,7 @@ const sortedOptionalDependencies = computed(() => {
164183
>
165184
{{ $t('package.peer_dependencies.title', { count: sortedPeerDependencies.length }) }}
166185
<span
167-
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
186+
class="i-carbon:link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
168187
aria-hidden="true"
169188
/>
170189
</a>
@@ -176,7 +195,7 @@ const sortedOptionalDependencies = computed(() => {
176195
<li
177196
v-for="peer in sortedPeerDependencies.slice(0, peerDepsExpanded ? undefined : 10)"
178197
:key="peer.name"
179-
class="flex items-center justify-between py-1 text-sm gap-2"
198+
class="flex items-center justify-start py-1 text-sm gap-2"
180199
>
181200
<div class="flex items-center gap-2 min-w-0">
182201
<NuxtLink
@@ -193,12 +212,13 @@ const sortedOptionalDependencies = computed(() => {
193212
{{ $t('package.dependencies.optional') }}
194213
</span>
195214
</div>
215+
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
196216
<NuxtLink
197217
:to="{
198218
name: 'package',
199219
params: { package: [...peer.name.split('/'), 'v', peer.version] },
200220
}"
201-
class="font-mono text-xs text-fg-subtle max-w-[40%] text-right truncate"
221+
class="font-mono text-xs text-fg-subtle max-w-[40%] text-end truncate"
202222
:title="peer.version"
203223
>
204224
{{ peer.version }}
@@ -234,7 +254,7 @@ const sortedOptionalDependencies = computed(() => {
234254
$t('package.optional_dependencies.title', { count: sortedOptionalDependencies.length })
235255
}}
236256
<span
237-
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
257+
class="i-carbon:link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
238258
aria-hidden="true"
239259
/>
240260
</a>
@@ -249,17 +269,18 @@ const sortedOptionalDependencies = computed(() => {
249269
optionalDepsExpanded ? undefined : 10,
250270
)"
251271
:key="dep"
252-
class="flex items-center justify-between py-1 text-sm gap-2"
272+
class="flex items-center justify-start py-1 text-sm gap-2"
253273
>
254274
<NuxtLink
255275
:to="{ name: 'package', params: { package: dep.split('/') } }"
256276
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0"
257277
>
258278
{{ dep }}
259279
</NuxtLink>
280+
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
260281
<NuxtLink
261282
:to="{ name: 'package', params: { package: [...dep.split('/'), 'v', version] } }"
262-
class="font-mono text-xs text-fg-subtle max-w-[50%] text-right truncate"
283+
class="font-mono text-xs text-fg-subtle max-w-[50%] text-end truncate"
263284
:title="version"
264285
>
265286
{{ version }}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<script setup lang="ts">
2+
import type { DependencyDepth } from '#shared/types'
3+
4+
const props = defineProps<{
5+
packageName: string
6+
version: string
7+
}>()
8+
9+
const { data: analysisData, status } = useDependencyAnalysis(
10+
() => props.packageName,
11+
() => props.version,
12+
)
13+
14+
const isExpanded = shallowRef(false)
15+
const showAll = shallowRef(false)
16+
17+
const hasDeprecated = computed(
18+
() => analysisData.value && analysisData.value.deprecatedPackages.length > 0,
19+
)
20+
21+
// Banner color - purple for deprecated
22+
const bannerColor = 'border-purple-600/40 bg-purple-500/10 text-purple-700 dark:text-purple-400'
23+
24+
// Styling for each depth level
25+
const depthStyles = {
26+
root: {
27+
bg: 'bg-purple-500/5 border-l-2 border-l-purple-600',
28+
text: 'text-fg',
29+
},
30+
direct: {
31+
bg: 'bg-purple-500/5 border-l-2 border-l-purple-500',
32+
text: 'text-fg-muted',
33+
},
34+
transitive: {
35+
bg: 'bg-purple-500/5 border-l-2 border-l-purple-400',
36+
text: 'text-fg-muted',
37+
},
38+
} as const
39+
40+
function getDepthStyle(depth: DependencyDepth) {
41+
return depthStyles[depth] || depthStyles.transitive
42+
}
43+
</script>
44+
45+
<template>
46+
<section
47+
v-if="status === 'success' && hasDeprecated"
48+
aria-labelledby="deprecated-tree-heading"
49+
class="relative"
50+
>
51+
<div class="rounded-lg border overflow-hidden" :class="bannerColor">
52+
<!-- Header -->
53+
<button
54+
type="button"
55+
class="w-full flex items-center justify-between gap-3 px-4 py-3 text-left transition-colors duration-200 hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-fg/50"
56+
:aria-expanded="isExpanded"
57+
aria-controls="deprecated-tree-details"
58+
@click="isExpanded = !isExpanded"
59+
>
60+
<div class="flex items-center gap-2 min-w-0">
61+
<span class="i-carbon-warning-hex w-4 h-4 shrink-0" aria-hidden="true" />
62+
<span class="font-mono text-sm font-medium truncate">
63+
{{ $t('package.deprecated.tree_found', analysisData!.deprecatedPackages.length) }}
64+
</span>
65+
</div>
66+
<span
67+
class="i-carbon-chevron-down w-4 h-4 transition-transform duration-200 shrink-0"
68+
:class="{ 'rotate-180': isExpanded }"
69+
aria-hidden="true"
70+
/>
71+
</button>
72+
73+
<!-- Expandable details -->
74+
<div
75+
v-show="isExpanded"
76+
id="deprecated-tree-details"
77+
class="border-t border-border bg-bg-subtle"
78+
>
79+
<ul class="divide-y divide-border list-none m-0 p-0">
80+
<li
81+
v-for="pkg in analysisData!.deprecatedPackages.slice(0, showAll ? undefined : 5)"
82+
:key="`${pkg.name}@${pkg.version}`"
83+
class="px-4 py-3"
84+
:class="getDepthStyle(pkg.depth).bg"
85+
>
86+
<div class="flex items-center gap-2 mb-1">
87+
<!-- Path badge -->
88+
<DependencyPathPopup v-if="pkg.path && pkg.path.length > 1" :path="pkg.path" />
89+
90+
<NuxtLink
91+
:to="{
92+
name: 'package',
93+
params: { package: [...pkg.name.split('/'), 'v', pkg.version] },
94+
}"
95+
class="font-mono text-sm font-medium hover:underline truncate"
96+
:class="getDepthStyle(pkg.depth).text"
97+
>
98+
{{ pkg.name }}@{{ pkg.version }}
99+
</NuxtLink>
100+
</div>
101+
<p class="text-xs text-fg-muted m-0 line-clamp-2">
102+
{{ pkg.message }}
103+
</p>
104+
</li>
105+
</ul>
106+
107+
<button
108+
v-if="analysisData!.deprecatedPackages.length > 5 && !showAll"
109+
type="button"
110+
class="w-full px-4 py-2 text-xs font-mono text-fg-muted hover:text-fg border-t border-border transition-colors duration-200"
111+
@click="showAll = true"
112+
>
113+
{{
114+
$t('package.deprecated.show_all', { count: analysisData!.deprecatedPackages.length })
115+
}}
116+
</button>
117+
</div>
118+
</div>
119+
</section>
120+
</template>

0 commit comments

Comments
 (0)