Skip to content

Commit fd15403

Browse files
committed
Merge branch 'main' into feat/changelog-1
2 parents ce34993 + 26d967e commit fd15403

26 files changed

Lines changed: 360 additions & 148 deletions

CONTRIBUTING.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ This focus helps guide our project decisions as a community and what we choose t
3131
- [Setup](#setup)
3232
- [Development workflow](#development-workflow)
3333
- [Available commands](#available-commands)
34+
- [Clearing caches during development](#clearing-caches-during-development)
3435
- [Project structure](#project-structure)
3536
- [Local connector CLI](#local-connector-cli)
3637
- [Mock connector (for local development)](#mock-connector-for-local-development)
@@ -124,6 +125,34 @@ pnpm test:a11y # Lighthouse accessibility audits
124125
pnpm test:perf # Lighthouse performance audits (CLS)
125126
```
126127

128+
### Clearing caches during development
129+
130+
Nitro persists `defineCachedEventHandler` results to disk at `.nuxt/cache/nitro/`. This cache **survives dev server restarts**. If you're iterating on a cached API route and want fresh results, delete the relevant cache directory:
131+
132+
```bash
133+
# Clear all Nitro handler caches
134+
rm -rf .nuxt/cache/nitro/handlers/
135+
136+
# Clear a specific handler cache (e.g. picks)
137+
rm -rf .nuxt/cache/nitro/handlers/npmx-picks/
138+
```
139+
140+
Alternatively, you can bypass the cache entirely in development by adding `shouldBypassCache: () => import.meta.dev` to your `defineCachedEventHandler` options:
141+
142+
```ts
143+
export default defineCachedEventHandler(
144+
async event => {
145+
// ...
146+
},
147+
{
148+
maxAge: 60 * 5,
149+
shouldBypassCache: () => import.meta.dev,
150+
},
151+
)
152+
```
153+
154+
The `.cache/` directory is a separate storage mount used for fetch-cache and atproto data.
155+
127156
### Project structure
128157

129158
```
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
4+
defineOptions({
5+
inheritAttrs: false,
6+
})
7+
8+
const props = defineProps<{
9+
copied: boolean
10+
copyText?: string
11+
copiedText?: string
12+
ariaLabelCopy?: string
13+
ariaLabelCopied?: string
14+
buttonAttrs?: HTMLAttributes
15+
}>()
16+
17+
const buttonCopyText = computed(() => props.copyText || $t('common.copy'))
18+
const buttonCopiedText = computed(() => props.copiedText || $t('common.copied'))
19+
const buttonAriaLabelCopy = computed(() => props.ariaLabelCopy || $t('common.copy'))
20+
const buttonAriaLabelCopied = computed(() => props.ariaLabelCopied || $t('common.copied'))
21+
22+
const emit = defineEmits<{
23+
click: []
24+
}>()
25+
26+
function handleClick() {
27+
emit('click')
28+
}
29+
</script>
30+
31+
<template>
32+
<div class="group relative" v-bind="$attrs">
33+
<slot />
34+
<button
35+
type="button"
36+
@click="handleClick"
37+
class="absolute z-20 inset-is-0 top-full inline-flex items-center gap-1 px-2 py-1 rounded border text-xs font-mono whitespace-nowrap transition-all duration-150 opacity-0 -translate-y-1 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto focus-visible:opacity-100 focus-visible:translate-y-0 focus-visible:pointer-events-auto"
38+
:class="[
39+
$style.copyButton,
40+
copied ? 'text-accent bg-accent/10' : 'text-fg-muted bg-bg border-border',
41+
]"
42+
:aria-label="copied ? buttonAriaLabelCopied : buttonAriaLabelCopy"
43+
v-bind="buttonAttrs"
44+
>
45+
<span
46+
:class="copied ? 'i-lucide:check' : 'i-lucide:copy'"
47+
class="w-3.5 h-3.5"
48+
aria-hidden="true"
49+
/>
50+
{{ copied ? buttonCopiedText : buttonCopyText }}
51+
</button>
52+
</div>
53+
</template>
54+
55+
<style module>
56+
.copyButton {
57+
clip: rect(0 0 0 0);
58+
clip-path: inset(50%);
59+
height: 1px;
60+
overflow: hidden;
61+
width: 1px;
62+
transition:
63+
opacity 0.25s 0.1s,
64+
translate 0.15s 0.1s,
65+
clip 0.01s 0.34s allow-discrete,
66+
clip-path 0.01s 0.34s allow-discrete,
67+
height 0.01s 0.34s allow-discrete,
68+
width 0.01s 0.34s allow-discrete;
69+
}
70+
71+
:global(.group):hover .copyButton,
72+
.copyButton:focus-visible {
73+
clip: auto;
74+
clip-path: none;
75+
height: auto;
76+
overflow: visible;
77+
width: auto;
78+
transition:
79+
opacity 0.15s,
80+
translate 0.15s;
81+
}
82+
83+
@media (hover: none) {
84+
.copyButton {
85+
display: none;
86+
}
87+
}
88+
</style>

app/components/Package/Playgrounds.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const providerIcons: Record<string, string> = {
2020
'solid-playground': 'i-simple-icons:solid',
2121
'svelte-playground': 'i-simple-icons:svelte',
2222
'tailwind-playground': 'i-simple-icons:tailwindcss',
23+
'storybook': 'i-simple-icons:storybook',
2324
}
2425
2526
// Map provider id to color class
@@ -37,6 +38,7 @@ const providerColors: Record<string, string> = {
3738
'solid-playground': 'text-provider-solid',
3839
'svelte-playground': 'text-provider-svelte',
3940
'tailwind-playground': 'text-provider-tailwind',
41+
'storybook': 'text-provider-storybook',
4042
}
4143
4244
function getIcon(provider: string): string {

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ const config = computed<VueUiSparklineConfig>(() => {
264264
</script>
265265

266266
<template>
267-
<div class="space-y-8 h-[110px] motion-safe:h-[140px]">
267+
<div class="space-y-8">
268268
<CollapsibleSection id="downloads" :title="$t('package.downloads.title')">
269269
<template #actions>
270270
<ButtonBase
@@ -280,7 +280,7 @@ const config = computed<VueUiSparklineConfig>(() => {
280280
<span v-else-if="isLoadingWeeklyDownloads" class="min-w-6 min-h-6 -m-1 p-1" />
281281
</template>
282282

283-
<div class="w-full overflow-hidden">
283+
<div class="w-full overflow-hidden h-[110px] motion-safe:h-[140px]">
284284
<template v-if="isLoadingWeeklyDownloads || hasWeeklyDownloads">
285285
<ClientOnly>
286286
<VueUiSparkline class="w-full max-w-xs" :dataset :config>

app/composables/npm/usePackage.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ export function transformPackument(
120120
license = license.type
121121
}
122122

123+
// Extract storybook field from the requested version (custom package.json field)
124+
const requestedPkgVersion = requestedVersion ? pkg.versions[requestedVersion] : null
125+
const rawStorybook = requestedPkgVersion?.storybook
126+
const storybook =
127+
rawStorybook && typeof rawStorybook === 'object' && 'url' in rawStorybook
128+
? ({ url: rawStorybook.url } as { url: string })
129+
: undefined
130+
123131
return {
124132
'_id': pkg._id,
125133
'_rev': pkg._rev,
@@ -134,6 +142,7 @@ export function transformPackument(
134142
'keywords': pkg.keywords,
135143
'repository': pkg.repository,
136144
'bugs': pkg.bugs,
145+
...(storybook && { storybook }),
137146
'requestedVersion': versionData,
138147
'versions': filteredVersions,
139148
'securityVersions': securityVersions,

app/composables/usePackageComparison.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export interface PackageComparisonData {
7070
*/
7171
export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
7272
const { t } = useI18n()
73+
const { $npmRegistry } = useNuxtApp()
7374
const numberFormatter = useNumberFormatter()
7475
const compactNumberFormatter = useCompactNumberFormatter()
7576
const bytesFormatter = useBytesFormatter()
@@ -124,9 +125,7 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
124125
namesToFetch.map(async (name): Promise<PackageComparisonData | null> => {
125126
try {
126127
// Fetch basic package info first (required)
127-
const pkgData = await $fetch<Packument>(
128-
`https://registry.npmjs.org/${encodePackageName(name)}`,
129-
)
128+
const { data: pkgData } = await $npmRegistry<Packument>(`/${encodePackageName(name)}`)
130129

131130
const latestVersion = pkgData['dist-tags']?.latest
132131
if (!latestVersion) return null

app/pages/compare.vue

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ definePageMeta({
66
name: 'compare',
77
})
88
9+
const { locale } = useI18n()
910
const router = useRouter()
1011
const canGoBack = useCanGoBack()
12+
const { copied, copy } = useClipboard({ copiedDuring: 2000 })
1113
1214
// Sync packages with URL query param (stable ref - doesn't change on other query changes)
1315
const packagesParam = useRouteQuery<string>('packages', '', { mode: 'replace' })
@@ -79,6 +81,57 @@ const gridHeaders = computed(() =>
7981
gridColumns.value.map(col => (col.version ? `${col.name}@${col.version}` : col.name)),
8082
)
8183
84+
/*
85+
* Convert the comparison grid data to a Markdown table.
86+
*/
87+
function exportComparisonDataAsMarkdown() {
88+
const mdData: Array<Array<string>> = []
89+
const headers = [
90+
'',
91+
...gridHeaders.value,
92+
...(showNoDependency.value ? [$t('compare.no_dependency.label')] : []),
93+
]
94+
mdData.push(headers)
95+
const maxLengths = headers.map(item => item.length)
96+
97+
selectedFacets.value.forEach((facet, index) => {
98+
const label = facet.label
99+
const data = getFacetValues(facet.id)
100+
mdData.push([
101+
label,
102+
...data.map(item =>
103+
item?.type === 'date'
104+
? new Date(item.display).toLocaleDateString(locale.value, {
105+
year: 'numeric',
106+
month: 'short',
107+
day: 'numeric',
108+
})
109+
: item?.display || '',
110+
),
111+
])
112+
mdData?.[index + 1]?.forEach((item, itemIndex) => {
113+
if (item.length > (maxLengths?.[itemIndex] || 0)) {
114+
maxLengths[itemIndex] = item.length
115+
}
116+
})
117+
})
118+
119+
const markdown = mdData.reduce((result, row, index) => {
120+
// replacing pipe `|` with `ǀ` (U+01C0 Latin Letter Dental Click) to avoid breaking tables
121+
result += `| ${row
122+
.map((el, ind) => el.padEnd(maxLengths[ind] || 0, ' ').replace(/\|/g, 'ǀ'))
123+
.join(' | ')} |`
124+
if (index === 0) {
125+
result += `\n|`
126+
maxLengths.forEach(len => (result += ` ${'-'.padEnd(len, '-')} |`))
127+
}
128+
result += `\n`
129+
return result
130+
}, '')
131+
132+
copy(markdown)
133+
}
134+
82135
useSeoMeta({
83136
title: () =>
84137
packages.value.length > 0
@@ -193,7 +246,24 @@ useSeoMeta({
193246

194247
<!-- Comparison grid -->
195248
<section v-if="canCompare" class="mt-10" aria-labelledby="comparison-heading">
196-
<h2 id="comparison-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-4">
249+
<CopyToClipboardButton
250+
v-if="packagesData && packagesData.some(p => p !== null)"
251+
:copied="copied"
252+
:copy-text="$t('compare.packages.copy_as_markdown')"
253+
class="mb-4"
254+
:button-attrs="{ class: 'hidden md:inline-flex' }"
255+
@click="exportComparisonDataAsMarkdown"
256+
>
257+
<h2 id="comparison-heading" class="text-xs text-fg-subtle uppercase tracking-wider">
258+
{{ $t('compare.packages.section_comparison') }}
259+
</h2>
260+
</CopyToClipboardButton>
261+
262+
<h2
263+
v-else
264+
id="comparison-heading"
265+
class="text-xs text-fg-subtle uppercase tracking-wider mb-4"
266+
>
197267
{{ $t('compare.packages.section_comparison') }}
198268
</h2>
199269

@@ -241,7 +311,7 @@ useSeoMeta({
241311
</div>
242312

243313
<h2
244-
id="comparison-heading"
314+
id="trends-comparison-heading"
245315
class="text-xs text-fg-subtle uppercase tracking-wider mb-4 mt-10"
246316
>
247317
{{ $t('compare.facets.trends.title') }}

0 commit comments

Comments
 (0)