[READ-ONLY] a fast, modern browser for the npm registry
at main 962 lines 37 kB view raw view rendered
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 &mdash; 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 &ndash; 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 &ndash; 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` | `/` | &mdash; | 383| `about` | `/about` | &mdash; | 384| `compare` | `/compare` | &mdash; | 385| `privacy` | `/privacy` | &mdash; | 386| `search` | `/search` | &mdash; | 387| `settings` | `/settings` | &mdash; | 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 &ndash; 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..." &ndash; 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).