forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1# Contributing to npmx.dev
2
3Thank you for your interest in contributing! ❤️ This document provides guidelines and instructions for contributing.
4
5> [!IMPORTANT]
6> Please be respectful and constructive in all interactions. We aim to maintain a welcoming environment for all contributors.
7> [👉 Read more](./CODE_OF_CONDUCT.md)
8
9## Goals
10
11The goal of [npmx.dev](https://npmx.dev) is to build a fast, modern and open-source browser for the npm registry, prioritizing speed, simplicity and a community-driven developer experience.
12
13### Core values
14
15- Speed
16- Simplicity
17- Community-first
18
19### Target audience
20
21npmx is built for open-source developers, by open-source developers.
22
23Our goal is to create tools and capabilities that solve real problems for package maintainers and power users, while also providing a great developer experience for everyone who works in the JavaScript ecosystem.
24
25This focus helps guide our project decisions as a community and what we choose to build.
26
27## Table of Contents
28
29- [Getting started](#getting-started)
30 - [Prerequisites](#prerequisites)
31 - [Setup](#setup)
32- [Development workflow](#development-workflow)
33 - [Available commands](#available-commands)
34 - [Project structure](#project-structure)
35 - [Local connector CLI](#local-connector-cli)
36 - [Mock connector (for local development)](#mock-connector-for-local-development)
37- [Code style](#code-style)
38 - [TypeScript](#typescript)
39 - [Server API patterns](#server-api-patterns)
40 - [Import order](#import-order)
41 - [Naming conventions](#naming-conventions)
42 - [Vue components](#vue-components)
43 - [Internal linking](#internal-linking)
44 - [Cursor and navigation](#cursor-and-navigation)
45- [RTL Support](#rtl-support)
46- [Localization (i18n)](#localization-i18n)
47 - [Approach](#approach)
48 - [i18n commands](#i18n-commands)
49 - [Adding a new locale](#adding-a-new-locale)
50 - [Update translation](#update-translation)
51 - [Adding translations](#adding-translations)
52 - [Translation key conventions](#translation-key-conventions)
53 - [Using i18n-ally (recommended)](#using-i18n-ally-recommended)
54 - [Formatting numbers and dates](#formatting-numbers-and-dates)
55- [Testing](#testing)
56 - [Unit tests](#unit-tests)
57 - [Component accessibility tests](#component-accessibility-tests)
58 - [Lighthouse accessibility tests](#lighthouse-accessibility-tests)
59 - [Lighthouse performance tests](#lighthouse-performance-tests)
60 - [End to end tests](#end-to-end-tests)
61 - [Test fixtures (mocking external APIs)](#test-fixtures-mocking-external-apis)
62- [Submitting changes](#submitting-changes)
63 - [Before submitting](#before-submitting)
64 - [Pull request process](#pull-request-process)
65 - [Commit messages and PR titles](#commit-messages-and-pr-titles)
66- [Pre-commit hooks](#pre-commit-hooks)
67- [Using AI](#using-ai)
68- [Questions](#questions)
69- [License](#license)
70
71## Getting started
72
73### Prerequisites
74
75- [Node.js](https://nodejs.org/) (LTS version recommended)
76- [pnpm](https://pnpm.io/) v10.28.1 or later
77
78### Setup
79
801. fork and clone the repository
812. install dependencies:
82
83 ```bash
84 pnpm install
85 ```
86
873. start the development server:
88
89 ```bash
90 pnpm dev
91 ```
92
934. (optional) if you want to test the admin UI/flow, you can run the local connector:
94
95 ```bash
96 pnpm npmx-connector
97 ```
98
99## Development workflow
100
101### Available commands
102
103```bash
104# Development
105pnpm dev # Start development server
106pnpm build # Production build
107pnpm preview # Preview production build
108
109# Connector
110pnpm npmx-connector # Start the real connector (requires npm login)
111pnpm mock-connector # Start the mock connector (no npm login needed)
112
113# Code Quality
114pnpm lint # Run linter (oxlint + oxfmt)
115pnpm lint:fix # Auto-fix lint issues
116pnpm test:types # TypeScript type checking
117
118# Testing
119pnpm test # Run all Vitest tests
120pnpm test:unit # Unit tests only
121pnpm test:nuxt # Nuxt component tests
122pnpm test:browser # Playwright E2E tests
123pnpm test:a11y # Lighthouse accessibility audits
124pnpm test:perf # Lighthouse performance audits (CLS)
125```
126
127### Project structure
128
129```
130app/ # Nuxt 4 app directory
131├── components/ # Vue components (PascalCase.vue)
132├── composables/ # Vue composables (useFeature.ts)
133├── pages/ # File-based routing
134├── plugins/ # Nuxt plugins
135├── app.vue # Root component
136└── error.vue # Error page
137
138server/ # Nitro server
139├── api/ # API routes
140└── utils/ # Server utilities
141
142shared/ # Shared between app and server
143└── types/ # TypeScript type definitions
144
145cli/ # Local connector CLI (separate workspace)
146test/ # Vitest tests
147├── unit/ # Unit tests (*.spec.ts)
148└── nuxt/ # Nuxt component tests
149tests/ # Playwright E2E tests
150```
151
152> [!TIP]
153> For more about the meaning of these directories, check out the docs on the [Nuxt directory structure](https://nuxt.com/docs/4.x/directory-structure).
154
155### Local connector CLI
156
157The `cli/` workspace contains a local connector that enables authenticated npm operations from the web UI. It runs on your machine and uses your existing npm credentials.
158
159```bash
160# run the connector from the root of the repository
161pnpm npmx-connector
162```
163
164The connector will check your npm authentication, generate a connection token, and listen for requests from npmx.dev.
165
166### Mock connector (for local development)
167
168If you're working on admin features (org management, package access controls, operations queue) and don't want to use your real npm account, you can run the mock connector instead:
169
170```bash
171pnpm mock-connector
172```
173
174This starts a mock connector server pre-populated with sample data (orgs, teams, members, packages). No npm login is required — operations succeed immediately without making real npm CLI calls.
175
176The mock connector prints a connection URL to the terminal, just like the real connector. Click it (or paste the token manually) to connect the UI.
177
178**Options:**
179
180```bash
181pnpm mock-connector # default: port 31415, user "mock-user", sample data
182pnpm mock-connector --port 9999 # custom port
183pnpm mock-connector --user alice # custom username
184pnpm mock-connector --empty # start with no pre-populated data
185```
186
187**Default sample data:**
188
189- **@nuxt**: 4 members (mock-user, danielroe, pi0, antfu), 3 teams (core, docs, triage)
190- **@unjs**: 2 members (mock-user, pi0), 1 team (maintainers)
191- **Packages**: @nuxt/kit, @nuxt/schema, @unjs/nitro with team-based access controls
192
193> [!TIP]
194> Run `pnpm dev` in a separate terminal to start the Nuxt dev server, then click the connection URL from the mock connector to connect.
195
196## Code style
197
198When committing changes, try to keep an eye out for unintended formatting updates. These can make a pull request look noisier than it really is and slow down the review process. Sometimes IDEs automatically reformat files on save, which can unintentionally introduce extra changes.
199
200To help with this, the project uses `oxfmt` to handle formatting via a pre-commit hook. The hook will automatically reformat files when needed. If something can’t be fixed automatically, it will let you know what needs to be updated before you can commit.
201
202If you want to get ahead of any formatting issues, you can also run `pnpm lint:fix` before committing to fix formatting across the whole project.
203
204### npmx name
205
206When displaying the project name anywhere in the UI, use `npmx` in all lowercase letters.
207
208### TypeScript
209
210- We care about good types – never cast things to `any` 💪
211- Validate rather than just assert
212
213### Server API patterns
214
215#### Input validation with Valibot
216
217Use Valibot schemas from `#shared/schemas/` to validate API inputs. This ensures type safety and provides consistent error messages:
218
219```typescript
220import * as v from 'valibot'
221import { PackageRouteParamsSchema } from '#shared/schemas/package'
222
223// In your handler:
224const { packageName, version } = v.parse(PackageRouteParamsSchema, {
225 packageName: rawPackageName,
226 version: rawVersion,
227})
228```
229
230#### Error handling with `handleApiError`
231
232Use the `handleApiError` utility for consistent error handling in API routes. It re-throws H3 errors (like 404s) and wraps other errors with a fallback message:
233
234```typescript
235import { ERROR_NPM_FETCH_FAILED } from '#shared/utils/constants'
236
237try {
238 // API logic...
239} catch (error: unknown) {
240 handleApiError(error, {
241 statusCode: 502,
242 message: ERROR_NPM_FETCH_FAILED,
243 })
244}
245```
246
247#### URL parameter parsing with `parsePackageParams`
248
249Use `parsePackageParams` to extract package name and version from URL segments:
250
251```typescript
252const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? []
253const { rawPackageName, rawVersion } = parsePackageParams(pkgParamSegments)
254```
255
256This handles patterns like `/pkg`, `/pkg/v/1.0.0`, `/@scope/pkg`, and `/@scope/pkg/v/1.0.0`.
257
258#### Constants
259
260Define error messages and other string constants in `#shared/utils/constants.ts` to ensure consistency across the codebase:
261
262```typescript
263export const ERROR_NPM_FETCH_FAILED = 'Failed to fetch package from npm registry.'
264```
265
266### Import order
267
2681. Type imports first (`import type { ... }`)
2692. External packages
2703. Internal aliases (`#shared/types`, `#server/`, etc.)
2714. No blank lines between groups
272
273```typescript
274import type { Packument, NpmSearchResponse } from '#shared/types'
275import type { Tokens } from 'marked'
276import { marked } from 'marked'
277import { hasProtocol } from 'ufo'
278```
279
280### Naming conventions
281
282| Type | Convention | Example |
283| ---------------- | ------------------------ | ------------------------------ |
284| Vue components | PascalCase | `DateTime.vue` |
285| Pages | kebab-case | `search.vue`, `[...name].vue` |
286| Composables | camelCase + `use` prefix | `useNpmRegistry.ts` |
287| Server routes | kebab-case + method | `search.get.ts` |
288| Functions | camelCase | `fetchPackage`, `formatDate` |
289| Constants | SCREAMING_SNAKE_CASE | `NPM_REGISTRY`, `ALLOWED_TAGS` |
290| Types/Interfaces | PascalCase | `NpmSearchResponse` |
291
292> [!TIP]
293> Exports in `app/composables/`, `app/utils/`, and `server/utils/` are auto-imported by Nuxt. To prevent [knip](https://knip.dev/) from flagging them as unused, add a `@public` JSDoc annotation:
294>
295> ```typescript
296> /**
297> * @public
298> */
299> export function myAutoImportedFunction() {
300> // ...
301> }
302> ```
303
304### Vue components
305
306- Use Composition API with `<script setup lang="ts">`
307- Define props with TypeScript: `defineProps<{ text: string }>()`
308- Keep functions under 50 lines
309- Accessibility is a first-class consideration – always consider ARIA attributes and keyboard navigation
310
311```vue
312<script setup lang="ts">
313import type { PackumentVersion } from '#shared/types'
314
315const props = defineProps<{
316 version: PackumentVersion
317}>()
318</script>
319```
320
321Ideally, extract utilities into separate files so they can be unit tested. 🙏
322
323### Internal linking
324
325Always use **object syntax with named routes** for internal navigation. This makes links resilient to URL structure changes and provides type safety via `unplugin-vue-router`.
326
327```vue
328<!-- Good: named route -->
329<NuxtLink :to="{ name: 'settings' }">Settings</NuxtLink>
330
331<!-- Bad: string path -->
332<NuxtLink to="/settings">Settings</NuxtLink>
333```
334
335The same applies to programmatic navigation:
336
337```typescript
338// Good
339navigateTo({ name: 'compare' })
340router.push({ name: 'search' })
341
342// Bad
343navigateTo('/compare')
344router.push('/search')
345```
346
347For routes with parameters, pass them explicitly:
348
349```vue
350<NuxtLink :to="{ name: '~username', params: { username } }">Profile</NuxtLink>
351<NuxtLink :to="{ name: 'org', params: { org: orgName } }">Organization</NuxtLink>
352```
353
354Query parameters work as expected:
355
356```vue
357<NuxtLink :to="{ name: 'compare', query: { packages: pkg.name } }">Compare</NuxtLink>
358```
359
360#### Package routes
361
362For package links, use the auto-imported `packageRoute()` utility from `app/utils/router.ts`. It handles scoped/unscoped packages and optional versions:
363
364```vue
365<!-- Links to /package/vue -->
366<NuxtLink :to="packageRoute('vue')">vue</NuxtLink>
367
368<!-- Links to /package/@nuxt/kit -->
369<NuxtLink :to="packageRoute('@nuxt/kit')">@nuxt/kit</NuxtLink>
370
371<!-- Links to /package/vue/v/3.5.0 -->
372<NuxtLink :to="packageRoute('vue', '3.5.0')">vue@3.5.0</NuxtLink>
373```
374
375> [!IMPORTANT]
376> Never construct package URLs as strings. The route structure uses separate `org` and `name` params, and `packageRoute()` handles the splitting correctly.
377
378#### Available route names
379
380| Route name | URL pattern | Parameters |
381| ----------------- | --------------------------------- | ------------------------- |
382| `index` | `/` | — |
383| `about` | `/about` | — |
384| `compare` | `/compare` | — |
385| `privacy` | `/privacy` | — |
386| `search` | `/search` | — |
387| `settings` | `/settings` | — |
388| `package` | `/package/:org?/:name` | `org?`, `name` |
389| `package-version` | `/package/:org?/:name/v/:version` | `org?`, `name`, `version` |
390| `code` | `/package-code/:path+` | `path` (array) |
391| `docs` | `/package-docs/:path+` | `path` (array) |
392| `org` | `/org/:org` | `org` |
393| `~username` | `/~:username` | `username` |
394| `~username-orgs` | `/~:username/orgs` | `username` |
395
396### Cursor and navigation
397
398**npmx** uses `cursor: pointer` only for links to match users’ everyday experience. For all other interactive elements, including buttons, use the default cursor (_or another appropriate cursor to indicate state_).
399
400> [!NOTE]
401> A link is any element that leads to another content (_go to another page, authorize_)
402> A button is any element that operates an action (_show tooltip, open menu, "like" package, open dropdown_)
403> If you're unsure which element to use - feel free to ask question in the issue or on discord
404
405> [!IMPORTANT]
406> Always Prefer implementing navigation as real links whenever possible. This ensures they can be opened in a new tab, shared or reloaded, and so the same content is available at a stable URL
407
408## RTL Support
409
410We support `right-to-left` languages, we need to make sure that the UI is working correctly in both directions.
411
412Simple approach used by most websites of relying on direction set in HTML element does not work because direction for various items, such as timeline, does not always match direction set in HTML.
413
414We've added some `UnoCSS` utilities styles to help you with that:
415
416- Do not use `left/right` padding and margin: for example `pl-1`. Use `padding-inline-start/end` instead. So `pl-1` should be `ps-1`, `pr-1` should be `pe-1`. The same rules apply to margin.
417- Do not use `rtl-` classes, such as `rtl-left-0`.
418- For icons that should be rotated for RTL, add `class="rtl-flip"`. This can only be used for icons outside of elements with `dir="auto"`.
419- For absolute positioned elements, don't use `left/right`: for example `left-0`. Use `inset-inline-start/end` instead. `UnoCSS` shortcuts are `inset-is` for `inset-inline-start` and `inset-ie` for `inset-inline-end`. Example: `left-0` should be replaced with `inset-is-0`.
420- If you need to change the border radius for an entire left or right side, use `border-inline-start/end`. `UnoCSS` shortcuts are `rounded-is` for left side, `rounded-ie` for right side. Example: `rounded-l-5` should be replaced with `rounded-is-5`.
421- If you need to change the border radius for one corner, use `border-start-end-radius` and similar rules. `UnoCSS` shortcuts are `rounded` + top/bottom as either `-bs` (top) or `-be` (bottom) + left/right as either `-is` (left) or `-ie` (right). Example: `rounded-tl-0` should be replaced with `rounded-bs-is-0`.
422
423## Localization (i18n)
424
425npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization. We aim to make the UI accessible to users in their preferred language.
426
427### Approach
428
429- All user-facing strings should use translation keys via `$t()` in templates and script
430- Translation files live in [`i18n/locales/`](i18n/locales) (e.g., `en-US.json`)
431- We use the `no_prefix` strategy (no `/en-US/` or `/fr-FR/` in URLs)
432- Locale preference is stored in `localStorage` and respected on subsequent visits
433
434### i18n commands
435
436The following scripts help manage translation files. `en.json` is the reference locale.
437
438| Command | Description |
439| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
440| `pnpm i18n:check [locale]` | Compares `en.json` with other locale files. Shows missing and extra keys. Optionally filter output by locale (e.g. `pnpm i18n:check ja-JP`). |
441| `pnpm i18n:check:fix [locale]` | Same as check, but adds missing keys to other locales with English placeholders. |
442| `pnpm i18n:report` | Audits translation keys against code usage in `.vue` and `.ts` files. Reports missing keys (used in code but not in locale), unused keys (in locale but not in code), and dynamic keys. |
443| `pnpm i18n:report:fix` | Removes unused keys from `en.json` and all other locale files. |
444
445### Adding a new locale
446
447We are using localization using country variants (ISO-6391) via [multiple translation files](https://i18n.nuxtjs.org/docs/guide/lazy-load-translations#multiple-files-lazy-loading) to avoid repeating every key per country.
448
449The [config/i18n.ts](./config/i18n.ts) configuration file will be used to register the new locale:
450
451- `countryLocaleVariants` object will be used to register the country variants
452- `locales` object will be used to link the supported locales (country and single one)
453- `buildLocales` function will build the target locales
454
455To add a new locale:
456
4571. Create a new JSON file in [`i18n/locales/`](./i18n/locales) with the locale code as the filename (e.g., `uk-UA.json`, `de-DE.json`)
4582. Copy [`en.json`](./i18n/locales/en.json) and translate the strings
4593. Add the locale to the `locales` array in [config/i18n.ts](./config/i18n.ts):
460
461 ```typescript
462 {
463 code: 'uk-UA', // Must match the filename (without .json)
464 file: 'uk-UA.json',
465 name: 'Українська', // Native name of the language
466 },
467 ```
468
4694. Copy your translation file to `lunaria/files/` for translation tracking:
470
471 ```bash
472 cp i18n/locales/uk-UA.json lunaria/files/uk-UA.json
473 ```
474
475 > ⚠**Important:**
476 > This file must be committed. Lunaria uses git history to track translation progress, so the build will fail if this file is missing.
477
4785. If the language is `right-to-left`, add `dir: 'rtl'` (see `ar-EG` in config for example)
4796. If the language requires special pluralization rules, add a `pluralRule` callback (see `ar-EG` or `ru-RU` in config for examples)
480
481Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization#custom-pluralization) and [Plural Rules](https://cldr.unicode.org/index/cldr-spec/plural-rules#TOC-Determining-Plural-Categories) for more info.
482
483### Update translation
484
485We track the current progress of translations with [Lunaria](https://lunaria.dev/) on this site: https://i18n.npmx.dev/
486If you see any outdated translations in your language, feel free to update the keys to match the English version.
487
488Use `pnpm i18n:check` and `pnpm i18n:check:fix` to verify and fix your locale (see [i18n commands](#i18n-commands) above for details).
489
490#### Country variants (advanced)
491
492Most languages only need a single locale file. Country variants are only needed when you want to support regional differences (e.g., `es-ES` for Spain vs `es-419` for Latin America).
493
494If you need country variants:
495
4961. Create a base language file (e.g., `es.json`) with all translations
4972. Create country variant files (e.g., `es-ES.json`, `es-419.json`) with only the differing translations
4983. Register the base language in `locales` and add variants to `countryLocaleVariants`
499
500See how `es`, `es-ES`, and `es-419` are configured in [config/i18n.ts](./config/i18n.ts) for a complete example.
501
502### Adding translations
503
5041. Add your translation key to `i18n/locales/en.json` first (American English is the source of truth)
5052. Use the key in your component:
506
507 ```vue
508 <template>
509 <p>{{ $t('my.translation.key') }}</p>
510 </template>
511 ```
512
513 Or in script:
514
515 ```typescript
516 <script setup lang="ts">
517 const message = computed(() => $t('my.translation.key'))
518 </script>
519 ```
520
5213. For dynamic values, use interpolation:
522
523 ```json
524 { "greeting": "Hello, {name}!" }
525 ```
526
527 ```vue
528 <p>{{ $t('greeting', { name: userName }) }}</p>
529 ```
530
5314. Don't concatenate string messages in the Vue templates, some languages can have different word order. Use placeholders instead.
532
533 **Bad:**
534
535 ```vue
536 <p>{{ $t('hello') }} {{ userName }}</p>
537 ```
538
539 **Good:**
540
541 ```vue
542 <p>{{ $t('greeting', { name: userName }) }}</p>
543 ```
544
545 **Complex content:**
546
547 If you need to include HTML or components inside the translation, use [`i18n-t`](https://vue-i18n.intlify.dev/guide/advanced/component.html) component. This is especially useful when the order of elements might change between languages.
548
549 ```json
550 {
551 "agreement": "I accept the {terms} and {privacy}.",
552 "terms_link": "Terms of Service",
553 "privacy_policy": "Privacy Policy"
554 }
555 ```
556
557 ```vue
558 <i18n-t keypath="agreement" tag="p">
559 <template #terms>
560 <NuxtLink to="/terms">{{ $t('terms_link') }}</NuxtLink>
561 </template>
562 <template #privacy>
563 <strong>{{ $t('privacy_policy') }}</strong>
564 </template>
565 </i18n-t>
566 ```
567
568### Translation key conventions
569
570- Use dot notation for hierarchy: `section.subsection.key`
571- Keep keys descriptive but concise
572- Group related keys together
573- Use `common.*` for shared strings (loading, retry, close, etc.)
574- Use component-specific prefixes: `package.card.*`, `settings.*`, `nav.*`
575- Do not use dashes (`-`) in translation keys; always use underscore (`_`): e.g., `privacy_policy` instead of `privacy-policy`
576- **Always use static string literals as translation keys.** Our i18n scripts (`pnpm i18n:report`) rely on static analysis to detect unused and missing keys. Dynamic keys cannot be analyzed and will be flagged as errors.
577
578 **Bad:**
579
580 ```vue
581 <!-- Template literal -->
582 <p>{{ $t(`package.tabs.${tab}`) }}</p>
583
584 <!-- Variable -->
585 <p>{{ $t(myKey) }}</p>
586 ```
587
588 **Good:**
589
590 ```typescript
591 const { t } = useI18n()
592
593 const tabLabels = computed(() => ({
594 readme: t('package.tabs.readme'),
595 versions: t('package.tabs.versions'),
596 }))
597 ```
598
599 ```vue
600 <p>{{ tabLabels[tab] }}</p>
601 ```
602
603### Using i18n-ally (recommended)
604
605We recommend the [i18n-ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) VSCode extension for a better development experience:
606
607- Inline translation previews in your code
608- Auto-completion for translation keys
609- Missing translation detection
610- Easy navigation to translation files
611
612The extension is included in our workspace recommendations, so VSCode should prompt you to install it.
613
614### Formatting numbers and dates
615
616Use vue-i18n's built-in formatters for locale-aware formatting:
617
618```vue
619<template>
620 <p>{{ $n(12345) }}</p>
621 <!-- "12,345" in en-US, "12 345" in fr-FR -->
622 <p>{{ $d(new Date()) }}</p>
623 <!-- locale-aware date -->
624</template>
625```
626
627## Testing
628
629### Unit tests
630
631Write unit tests for core functionality using Vitest:
632
633```typescript
634import { describe, it, expect } from 'vitest'
635
636describe('featureName', () => {
637 it('should handle expected case', () => {
638 expect(result).toBe(expected)
639 })
640})
641```
642
643> [!TIP]
644> If you need access to the Nuxt context in your unit or component test, place your test in the `test/nuxt/` directory and run with `pnpm test:nuxt`
645
646### Component accessibility tests
647
648All Vue components should have accessibility tests in `test/nuxt/a11y.spec.ts`. These tests use [axe-core](https://github.com/dequelabs/axe-core) to catch common accessibility violations and run in a real browser environment via Playwright.
649
650```typescript
651import { MyComponent } from '#components'
652
653describe('MyComponent', () => {
654 it('should have no accessibility violations', async () => {
655 const component = await mountSuspended(MyComponent, {
656 props: {
657 /* required props */
658 },
659 })
660 const results = await runAxe(component)
661 expect(results.violations).toEqual([])
662 })
663})
664```
665
666The `runAxe` helper handles DOM isolation and disables page-level rules that don't apply to isolated component testing.
667
668A coverage test in `test/unit/a11y-component-coverage.spec.ts` ensures all components are either tested or explicitly skipped with justification. When you add a new component, this test will fail until you add accessibility tests for it.
669
670> [!IMPORTANT]
671> Just because axe-core doesn't find any obvious issues, it does not mean a component is accessible. Please do additional checks and use best practices.
672
673### Lighthouse accessibility tests
674
675In addition to component-level axe audits, the project runs full-page accessibility audits using [Lighthouse CI](https://github.com/GoogleChrome/lighthouse-ci). These test the rendered pages in both light and dark mode against Lighthouse's accessibility category, requiring a perfect score.
676
677#### How it works
678
6791. The project is built in test mode (`pnpm build:test`), which activates server-side fixture mocking
6802. Lighthouse CI starts a preview server and audits three URLs: `/`, `/search?q=nuxt`, and `/package/nuxt`
6813. A Puppeteer setup script (`lighthouse-setup.cjs`) runs before each audit to set the color mode and intercept client-side API requests using the same fixtures as the E2E tests
682
683#### Running locally
684
685```bash
686# Build + run both light and dark audits
687pnpm test:a11y
688
689# Or against an existing test build
690pnpm test:a11y:prebuilt
691
692# Or run a single color mode manually
693pnpm build:test
694LIGHTHOUSE_COLOR_MODE=dark ./scripts/lighthouse.sh
695```
696
697This 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/`.
698
699#### Configuration
700
701| File | Purpose |
702| ----------------------- | --------------------------------------------------------- |
703| `.lighthouserc.cjs` | Lighthouse CI config (URLs, assertions, Chrome path) |
704| `lighthouse-setup.cjs` | Puppeteer script for color mode + client-side API mocking |
705| `scripts/lighthouse.sh` | Shell wrapper that runs the audit for a given color mode |
706
707### Lighthouse performance tests
708
709The 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.
710
711#### How it works
712
713The 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.
714
715#### Running locally
716
717```bash
718# Build + run performance audit
719pnpm test:perf
720
721# Or against an existing test build
722pnpm test:perf:prebuilt
723```
724
725Unlike the accessibility audits, performance audits do not run in separate light/dark modes.
726
727### End to end tests
728
729Write end-to-end tests using Playwright:
730
731```bash
732pnpm test:browser # Run tests
733pnpm test:browser:ui # Run with Playwright UI
734```
735
736Make sure to read about [Playwright best practices](https://playwright.dev/docs/best-practices) and don't rely on classes/IDs but try to follow user-replicable behaviour (like selecting an element based on text content instead).
737
738### Test fixtures (mocking external APIs)
739
740E2E tests use a fixture system to mock external API requests, ensuring tests are deterministic and don't hit real APIs. This is handled at two levels:
741
742**Server-side mocking** (`modules/fixtures.ts` + `modules/runtime/server/cache.ts`):
743
744- Intercepts all `$fetch` calls during SSR
745- Serves pre-recorded fixture data from `test/fixtures/`
746- Enabled via `NUXT_TEST_FIXTURES=true` or Nuxt test mode
747
748**Client-side mocking** (`test/fixtures/mock-routes.cjs`):
749
750- Shared URL matching and response generation logic used by both Playwright E2E tests and Lighthouse CI
751- Playwright tests (`test/e2e/test-utils.ts`) use this via `page.route()` interception
752- Lighthouse tests (`lighthouse-setup.cjs`) use this via Puppeteer request interception
753- All E2E test files import from `./test-utils` instead of `@nuxt/test-utils/playwright`
754- Throws a clear error if an unmocked external request is detected
755
756#### Fixture files
757
758Fixtures are stored in `test/fixtures/` with this structure:
759
760```
761test/fixtures/
762├── npm-registry/
763│ ├── packuments/ # Package metadata (vue.json, @nuxt/kit.json)
764│ ├── search/ # Search results (vue.json, nuxt.json)
765│ └── orgs/ # Org package lists (nuxt.json)
766├── npm-api/
767│ └── downloads/ # Download stats
768└── users/ # User package lists
769```
770
771#### Adding new fixtures
772
7731. **Generate fixtures** using the script:
774
775 ```bash
776 pnpm generate:fixtures vue lodash @nuxt/kit
777 ```
778
7792. **Or manually create** a JSON file in the appropriate directory
780
781#### Environment variables
782
783| Variable | Purpose |
784| --------------------------------- | ---------------------------------- |
785| `NUXT_TEST_FIXTURES=true` | Enable server-side fixture mocking |
786| `NUXT_TEST_FIXTURES_VERBOSE=true` | Enable detailed fixture logging |
787
788#### When tests fail due to missing fixtures
789
790If a test fails with an error like:
791
792```
793UNMOCKED EXTERNAL API REQUEST DETECTED
794API: npm registry
795URL: https://registry.npmjs.org/some-package
796```
797
798You need to either:
799
8001. Add a fixture file for that package/endpoint
8012. Update the mock handlers in `test/fixtures/mock-routes.cjs` (client) or `modules/runtime/server/cache.ts` (server)
802
803### Testing connector features
804
805Features that require authentication through the local connector (org management, package collaborators, operations queue) are tested using a mock connector server.
806
807#### Architecture
808
809The mock connector infrastructure is shared between the CLI, E2E tests, and Vitest component tests:
810
811```
812cli/src/
813├── types.ts # ConnectorEndpoints contract (shared by real + mock)
814├── mock-state.ts # MockConnectorStateManager (canonical source)
815├── mock-app.ts # H3 mock app + MockConnectorServer class
816└── mock-server.ts # CLI entry point (pnpm mock-connector)
817
818test/test-utils/ # Re-exports from cli/src/ for test convenience
819test/e2e/helpers/ # E2E-specific wrappers (fixtures, global setup)
820```
821
822Both the real server (`cli/src/server.ts`) and the mock server (`cli/src/mock-app.ts`) conform to the `ConnectorEndpoints` interface defined in `cli/src/types.ts`. This ensures the API contract is enforced by TypeScript. When adding a new endpoint, update `ConnectorEndpoints` first, then implement it in both servers.
823
824#### Vitest component tests (`test/nuxt/`)
825
826- Mock the `useConnector` composable with reactive state
827- Use `document.body` queries for components using Teleport
828- See `test/nuxt/components/HeaderConnectorModal.spec.ts` for an example
829
830```typescript
831// Create mock state
832const mockState = ref({ connected: false, npmUser: null, ... })
833
834// Mock the composable
835vi.mock('~/composables/useConnector', () => ({
836 useConnector: () => ({
837 isConnected: computed(() => mockState.value.connected),
838 // ... other properties
839 }),
840}))
841```
842
843#### Playwright E2E tests (`test/e2e/`)
844
845- A mock HTTP server starts automatically via Playwright's global setup
846- Use the `mockConnector` fixture to set up test data and the `gotoConnected` helper to navigate with authentication
847
848```typescript
849test('shows org members', async ({ page, gotoConnected, mockConnector }) => {
850 // Set up test data
851 await mockConnector.setOrgData('@testorg', {
852 users: { testuser: 'owner', member1: 'admin' },
853 })
854
855 // Navigate with connector authentication
856 await gotoConnected('/@testorg')
857
858 // Test assertions
859 await expect(page.getByRole('link', { name: '@testuser' })).toBeVisible()
860})
861```
862
863The mock connector supports test endpoints for state manipulation:
864
865- `/__test__/reset` - Reset all mock state
866- `/__test__/org` - Set org users, teams, and team members
867- `/__test__/user-orgs` - Set user's organizations
868- `/__test__/user-packages` - Set user's packages
869- `/__test__/package` - Set package collaborators
870
871## Submitting changes
872
873### Before submitting
874
8751. ensure your code follows the style guidelines
8762. run linting: `pnpm lint:fix`
8773. run type checking: `pnpm test:types`
8784. run tests: `pnpm test`
8795. write or update tests for your changes
880
881### Pull request process
882
8831. create a feature branch from `main`
8842. make your changes with clear, descriptive commits
8853. push your branch and open a pull request
8864. ensure CI checks pass (lint, type check, tests)
8875. request review from maintainers
888
889### Commit messages and PR titles
890
891Write clear, concise PR titles that explain the "why" behind changes.
892
893We use [Conventional Commits](https://www.conventionalcommits.org/). Since we squash on merge, the PR title becomes the commit message in `main`, so it's important to get it right.
894
895Format: `type(scope): description`
896
897**Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`
898
899**Scopes (optional):** `docs`, `i18n`, `deps`
900
901**Examples:**
902
903- `fix: resolve search pagination issue`
904- `feat: add package version comparison`
905- `fix(i18n): update French translations`
906- `chore(deps): update vite to v6`
907
908Where front end changes are made, please include before and after screenshots in your pull request description.
909
910> [!NOTE]
911> Use lowercase letters in your pull request title. Individual commit messages within your PR don't need to follow this format since they'll be squashed.
912
913### PR descriptions
914
915If your pull request directly addresses an open issue, use the following inside your PR description.
916
917```text
918Resolves | Fixes | Closes: #xxx
919```
920
921Replace `#xxx` with either a URL to the issue, or the number of the issue. For example:
922
923```text
924Fixes #123
925```
926
927or
928
929```text
930Closes https://github.com/npmx-dev/npmx.dev/issues/123
931```
932
933This provides the following benefits:
934
935- it links the pull request to the issue (the merge icon will appear in the issue), so everybody can see there is an open PR
936- when the pull request is merged, the linked issue is automatically closed
937
938## Pre-commit hooks
939
940The project uses `lint-staged` with `simple-git-hooks` to automatically lint files on commit.
941
942## Using AI
943
944You're welcome to use AI tools to help you contribute. But there are two important ground rules:
945
946### 1. Never let an LLM speak for you
947
948When you write a comment, issue, or PR description, use your own words. Grammar and spelling don't matter – real connection does. AI-generated summaries tend to be long-winded, dense, and often inaccurate. Simplicity is an art. The goal is not to sound impressive, but to communicate clearly.
949
950### 2. Never let an LLM think for you
951
952Feel free to use AI to write code, tests, or point you in the right direction. But always understand what it's written before contributing it. Take personal responsibility for your contributions. Don't say "ChatGPT says..." – tell us what _you_ think.
953
954For more context, see [Using AI in open source](https://roe.dev/blog/using-ai-in-open-source).
955
956## Questions?
957
958If you have questions or need help, feel free to open an issue for discussion or join our [Discord server](https://chat.npmx.dev).
959
960## License
961
962By contributing to npmx.dev, you agree that your contributions will be licensed under the [MIT License](LICENSE).