Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .lighthouserc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@ module.exports = {
chromePath: findChrome(),
puppeteerScript: './lighthouse-setup.cjs',
settings: {
onlyCategories: ['accessibility'],
onlyCategories: process.env.LH_PERF ? ['performance'] : ['accessibility'],
skipAudits: ['valid-source-maps'],
},
},
assert: {
assertions: {
'categories:accessibility': ['error', { minScore: 1 }],
},
assertions: process.env.LH_PERF
? {
'cumulative-layout-shift': ['error', { maxNumericValue: 0 }],
}
: {
'categories:accessibility': ['error', { minScore: 1 }],
},
},
upload: {
target: 'temporary-public-storage',
Expand Down
34 changes: 28 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ This focus helps guide our project decisions as a community and what we choose t
- [Unit tests](#unit-tests)
- [Component accessibility tests](#component-accessibility-tests)
- [Lighthouse accessibility tests](#lighthouse-accessibility-tests)
- [Lighthouse performance tests](#lighthouse-performance-tests)
- [End to end tests](#end-to-end-tests)
- [Test fixtures (mocking external APIs)](#test-fixtures-mocking-external-apis)
- [Submitting changes](#submitting-changes)
Expand Down Expand Up @@ -114,6 +115,7 @@ pnpm test:unit # Unit tests only
pnpm test:nuxt # Nuxt component tests
pnpm test:browser # Playwright E2E tests
pnpm test:a11y # Lighthouse accessibility audits
pnpm test:perf # Lighthouse performance audits (CLS)
```

### Project structure
Expand Down Expand Up @@ -641,18 +643,38 @@ pnpm test:a11y:prebuilt

# Or run a single color mode manually
pnpm build:test
LIGHTHOUSE_COLOR_MODE=dark ./scripts/lighthouse-a11y.sh
LIGHTHOUSE_COLOR_MODE=dark ./scripts/lighthouse.sh
```

This requires Chrome or Chromium to be installed. The script will auto-detect common installation paths. Results are printed to the terminal and saved in `.lighthouseci/`.

#### Configuration

| File | Purpose |
| ---------------------------- | --------------------------------------------------------- |
| `.lighthouserc.cjs` | Lighthouse CI config (URLs, assertions, Chrome path) |
| `lighthouse-setup.cjs` | Puppeteer script for color mode + client-side API mocking |
| `scripts/lighthouse-a11y.sh` | Shell wrapper that runs the audit for a given color mode |
| File | Purpose |
| ----------------------- | --------------------------------------------------------- |
| `.lighthouserc.cjs` | Lighthouse CI config (URLs, assertions, Chrome path) |
| `lighthouse-setup.cjs` | Puppeteer script for color mode + client-side API mocking |
| `scripts/lighthouse.sh` | Shell wrapper that runs the audit for a given color mode |

### Lighthouse performance tests

The project also runs Lighthouse performance audits to enforce zero Cumulative Layout Shift (CLS). These run separately from the accessibility audits and test the same set of URLs.

#### How it works

The same `.lighthouserc.cjs` config is shared between accessibility and performance audits. When the `LH_PERF` environment variable is set, the config switches from the `accessibility` category to the `performance` category and asserts that CLS is exactly 0.

#### Running locally

```bash
# Build + run performance audit
pnpm test:perf

# Or against an existing test build
pnpm test:perf:prebuilt
```

Unlike the accessibility audits, performance audits do not run in separate light/dark modes.

### End to end tests

Expand Down
22 changes: 9 additions & 13 deletions app/components/Package/WeeklyDownloadStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ const config = computed(() => {
>
<span class="sr-only">{{ $t('package.downloads.analyze') }}</span>
</ButtonBase>
<span v-else-if="isLoadingWeeklyDownloads" class="min-w-6 min-h-6 -m-1 p-1" />
</template>

<div class="w-full overflow-hidden">
Expand All @@ -240,26 +241,21 @@ const config = computed(() => {
</template>
</VueUiSparkline>
<template #fallback>
<!-- Skeleton matching sparkline layout: title row + chart with data label -->
<div class="min-h-[75.195px]">
<!-- Title row: date range (24px height) -->
<!-- Skeleton matching VueUiSparkline layout (title 24px + SVG aspect 500:80) -->
<div class="max-w-xs">
<!-- Title row: fontSize * 2 = 24px -->
<div class="h-6 flex items-center ps-3">
<SkeletonInline class="h-3 w-36" />
</div>
<!-- Chart area: data label left, sparkline right -->
<!-- Chart area: matches SVG viewBox 500:80 -->
<div class="aspect-[500/80] flex items-center">
<!-- Data label (covers ~42% width) -->
<!-- Data label (covers ~42% width, matching dataLabel.offsetX) -->
<div class="w-[42%] flex items-center ps-0.5">
<SkeletonInline class="h-7 w-24" />
</div>
<!-- Sparkline area (~58% width) -->
<div class="flex-1 flex items-end gap-0.5 h-4/5 pe-3">
<SkeletonInline
v-for="i in 16"
:key="i"
class="flex-1 rounded-sm"
:style="{ height: `${25 + ((i * 7) % 50)}%` }"
/>
<!-- Sparkline line placeholder -->
<div class="flex-1 flex items-end pe-3">
<SkeletonInline class="h-px w-full" />
</div>
</div>
</div>
Expand Down
15 changes: 1 addition & 14 deletions app/pages/about.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@
const router = useRouter()
const canGoBack = useCanGoBack()

interface GitHubContributor {
login: string
id: number
avatar_url: string
html_url: string
contributions: number
}

useSeoMeta({
title: () => `${$t('about.title')} - npmx`,
ogTitle: () => `${$t('about.title')} - npmx`,
Expand All @@ -34,12 +26,7 @@ const pmLinks = {
vlt: 'https://www.vlt.sh/',
}

const { data: contributors, status: contributorsStatus } = useFetch<GitHubContributor[]>(
'/api/contributors',
{
lazy: true,
},
)
const { data: contributors, status: contributorsStatus } = useLazyFetch('/api/contributors')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n app/pages/about.vue | head -50

Repository: npmx-dev/npmx.dev

Length of output: 2162


🏁 Script executed:

rg "useLazyFetch" --type vue -A 3 -B 3

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

rg "GitHubContributor\|Contributor.*type\|interface.*Contributor" --type ts --type tsx --type vue -A 3

Repository: npmx-dev/npmx.dev

Length of output: 88


🌐 Web query:

Nuxt 3 useLazyFetch TypeScript generic type support

💡 Result:

useLazyFetch does support TypeScript generics—it has the same generic signature as useFetch. In the Nuxt docs the type is shown as: useLazyFetch<DataT, ErrorT>(..., options?: UseFetchOptions<DataT>) -> Promise<AsyncData<DataT, ErrorT>>. [1]

Basic: type the response

type Post = { id: number; title: string }

const { data, error, status } = await useLazyFetch<Post[]>('/api/posts')
// data: Ref<Post[] | null | undefined> (depending on Nuxt version / mode)

(Generics are documented on useFetch, and useLazyFetch is explicitly “same signature”.) [1][2]

Also type the error (optional)

import type { FetchError } from 'ofetch'

type Post = { id: number; title: string }

const { data, error } = await useLazyFetch<Post[], FetchError>('/api/posts')

(Generic ErrorT is part of the documented signature.) [1][2]

If you wrap it, preserve generics/overloads

If you create a custom wrapper composable, type it as typeof useFetch / typeof useLazyFetch (or forward the full generic parameters), otherwise you often lose Nuxt’s built-in typing/inference. [3]

Sources: Nuxt useLazyFetch docs (type signature) [1], Nuxt useFetch docs (generic signature/details) [2], example of preserving types in a custom wrapper [3].


🏁 Script executed:

rg "type.*Contributor|interface.*Contributor" --type ts --type vue

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

rg "api/contributors" --type ts --type vue -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.vue" \) | xargs grep -l "contributors" | head -10

Repository: npmx-dev/npmx.dev

Length of output: 149


🏁 Script executed:

cat -n server/api/contributors.get.ts

Repository: npmx-dev/npmx.dev

Length of output: 1798


🏁 Script executed:

rg "type|interface" server/api/contributors.get.ts -A 5

Repository: npmx-dev/npmx.dev

Length of output: 190


🏁 Script executed:

find . -path ./node_modules -prune -o -type f \( -name "*.ts" -o -name "*.d.ts" \) -print | xargs grep -l "contributor" -i | grep -v node_modules | head -15

Repository: npmx-dev/npmx.dev

Length of output: 127


Add type-safe generic to useLazyFetch call.

The call to useLazyFetch lacks a generic type parameter, leaving contributors implicitly typed, which violates the strict type-safety guideline. Import and use the existing GitHubContributor type from the API endpoint.

♻️ Suggested fix
+import type { GitHubContributor } from '~/server/api/contributors.get'
+
 const { data: contributors, status: contributorsStatus } =
-  useLazyFetch('/api/contributors')
+  useLazyFetch<GitHubContributor[]>('/api/contributors')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: contributors, status: contributorsStatus } = useLazyFetch('/api/contributors')
import type { GitHubContributor } from '~/server/api/contributors.get'
const { data: contributors, status: contributorsStatus } =
useLazyFetch<GitHubContributor[]>('/api/contributors')

</script>

<template>
Expand Down
Loading
Loading