diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 4d78f60d6..619aae3af 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -32,6 +32,9 @@ jobs: - name: 📦 Install dependencies run: pnpm install + - name: 🎨 Check for non-RTL/non-a11y CSS classes + run: pnpm lint:css + - name: 🌐 Compare translations run: pnpm i18n:check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28e02f523..46a0a62d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,7 @@ jobs: run: pnpm install - name: 🧪 Unit tests - run: pnpm test:unit run --coverage --reporter=junit --outputFile=test-report.junit.xml + run: pnpm test:unit run --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml - name: ⬆︎ Upload test results to Codecov if: ${{ !cancelled() }} @@ -115,13 +115,14 @@ jobs: run: pnpm playwright install chromium-headless-shell - name: 🧪 Component tests - run: pnpm test:nuxt run --coverage --reporter=junit --outputFile=test-report.junit.xml + run: pnpm test:nuxt run --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml - - name: ⬆︎ Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1 + - name: ⬆︎ Upload coverage reports to Codecov + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5 with: - token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: ⬆︎ Upload coverage reports to Codecov uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5 @@ -183,7 +184,7 @@ jobs: run: pnpm build:test - name: ♿ Accessibility audit (Lighthouse - ${{ matrix.mode }} mode) - run: ./scripts/lighthouse-a11y.sh + run: pnpm test:a11y:prebuilt env: LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} LIGHTHOUSE_COLOR_MODE: ${{ matrix.mode }} @@ -234,3 +235,8 @@ jobs: - name: 🌐 Check for missing or dynamic i18n keys run: pnpm i18n:report + + - name: 🌐 Check i18n schema is up to date + run: | + pnpm i18n:schema + git diff --exit-code i18n/schema.json diff --git a/.github/workflows/mirror-tangled.yml b/.github/workflows/mirror-tangled.yml new file mode 100644 index 000000000..83458f90d --- /dev/null +++ b/.github/workflows/mirror-tangled.yml @@ -0,0 +1,37 @@ +name: mirror + +on: + push: + branches: + - main + tags: + - '*' + +permissions: + contents: read + +jobs: + mirror: + name: 🕸️ Mirror to Tangled + if: ${{ github.repository == 'npmx-dev/npmx.dev' }} + runs-on: ubuntu-24.04-arm + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + + - name: 🔑 Configure SSH + env: + TANGLED_SSH_KEY: ${{ secrets.TANGLED_SSH_KEY }} + run: | + mkdir -p ~/.ssh + echo "$TANGLED_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -t ed25519 tangled.org >> ~/.ssh/known_hosts 2>/dev/null + + - name: ⬆︎ Push to Tangled + run: | + git remote add tangled git@tangled.org:npmx.dev/npmx.dev + git push tangled main --force + git push tangled --tags --force diff --git a/.gitignore b/.gitignore index 12ca103c2..99e727c21 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,9 @@ test-results/ # generated files shared/types/lexicons +file-tree-sprite.svg + +**/__screenshots__/** # output .vercel diff --git a/.lighthouserc.cjs b/.lighthouserc.cjs index 5a6957e27..423eb0b4e 100644 --- a/.lighthouserc.cjs +++ b/.lighthouserc.cjs @@ -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', diff --git a/.nuxtrc b/.nuxtrc index 1e780800e..1e1fe8339 100644 --- a/.nuxtrc +++ b/.nuxtrc @@ -1 +1 @@ -setups.@nuxt/test-utils="3.23.0" \ No newline at end of file +setups.@nuxt/test-utils="4.0.0" \ No newline at end of file diff --git a/.oxfmtignore b/.oxfmtignore deleted file mode 100644 index df9b35610..000000000 --- a/.oxfmtignore +++ /dev/null @@ -1,2 +0,0 @@ -# Docus MDC content - formatter breaks component syntax -docs/content/**/*.md diff --git a/.oxlintrc.json b/.oxlintrc.json index b0bc5d6ff..3593ccb94 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,7 +1,7 @@ { "$schema": "https://unpkg.com/oxlint/configuration_schema.json", "plugins": ["unicorn", "typescript", "oxc", "vue", "vitest"], - "jsPlugins": ["@e18e/eslint-plugin"], + "jsPlugins": ["@e18e/eslint-plugin", "eslint-plugin-regexp"], "categories": { "correctness": "error", "suspicious": "warn", @@ -17,7 +17,72 @@ "e18e/prefer-timer-args": "error", "e18e/prefer-date-now": "error", "e18e/prefer-regex-test": "error", - "e18e/prefer-array-some": "error" + "e18e/prefer-array-some": "error", + // RegExp - Possible Errors (most critical) + "regexp/no-contradiction-with-assertion": "error", + "regexp/no-dupe-disjunctions": "error", + "regexp/no-empty-alternative": "error", + "regexp/no-empty-capturing-group": "error", + "regexp/no-empty-character-class": "error", + "regexp/no-empty-group": "error", + "regexp/no-empty-lookarounds-assertion": "error", + "regexp/no-escape-backspace": "error", + "regexp/no-invalid-regexp": "error", + "regexp/no-lazy-ends": "error", + "regexp/no-misleading-capturing-group": "error", + "regexp/no-misleading-unicode-character": "error", + "regexp/no-missing-g-flag": "error", + "regexp/no-optional-assertion": "error", + "regexp/no-potentially-useless-backreference": "error", + "regexp/no-super-linear-backtracking": "error", + "regexp/no-useless-assertions": "error", + "regexp/no-useless-backreference": "error", + "regexp/no-useless-dollar-replacements": "error", + "regexp/strict": "error", + // RegExp - Best Practices + "regexp/confusing-quantifier": "warn", + "regexp/control-character-escape": "error", + "regexp/negation": "error", + "regexp/no-dupe-characters-character-class": "error", + "regexp/no-empty-string-literal": "error", + "regexp/no-extra-lookaround-assertions": "error", + "regexp/no-invisible-character": "error", + "regexp/no-legacy-features": "error", + "regexp/no-non-standard-flag": "error", + "regexp/no-obscure-range": "error", + "regexp/no-octal": "error", + "regexp/no-standalone-backslash": "error", + "regexp/no-trivially-nested-assertion": "error", + "regexp/no-trivially-nested-quantifier": "error", + "regexp/no-unused-capturing-group": "warn", + "regexp/no-useless-character-class": "error", + "regexp/no-useless-flag": "error", + "regexp/no-useless-lazy": "error", + "regexp/no-useless-quantifier": "error", + "regexp/no-useless-range": "error", + "regexp/no-useless-set-operand": "error", + "regexp/no-useless-string-literal": "error", + "regexp/no-useless-two-nums-quantifier": "error", + "regexp/no-zero-quantifier": "error", + "regexp/optimal-lookaround-quantifier": "warn", + "regexp/optimal-quantifier-concatenation": "error", + "regexp/prefer-predefined-assertion": "error", + "regexp/prefer-range": "error", + "regexp/prefer-set-operation": "error", + "regexp/simplify-set-operations": "error", + "regexp/use-ignore-case": "error", + // RegExp - Stylistic Issues (less critical, focused on consistency) + "regexp/match-any": "warn", + "regexp/no-useless-escape": "warn", + "regexp/no-useless-non-capturing-group": "warn", + "regexp/prefer-character-class": "warn", + "regexp/prefer-d": "warn", + "regexp/prefer-plus-quantifier": "warn", + "regexp/prefer-question-quantifier": "warn", + "regexp/prefer-star-quantifier": "warn", + "regexp/prefer-unicode-codepoint-escapes": "warn", + "regexp/prefer-w": "warn", + "regexp/sort-flags": "warn" }, "overrides": [ { diff --git a/.vscode/settings.json b/.vscode/settings.json index 38bd426f2..6ffa1b9bd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { - "i18n-ally.localesPaths": ["./i18n/locales"], + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true, "i18n-ally.keystyle": "nested", + "i18n-ally.localesPaths": ["./i18n/locales"], "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f60611433..858a951e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,7 @@ This focus helps guide our project decisions as a community and what we choose t - [RTL Support](#rtl-support) - [Localization (i18n)](#localization-i18n) - [Approach](#approach) + - [i18n commands](#i18n-commands) - [Adding a new locale](#adding-a-new-locale) - [Update translation](#update-translation) - [Adding translations](#adding-translations) @@ -52,6 +53,8 @@ This focus helps guide our project decisions as a community and what we choose t - [Testing](#testing) - [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) @@ -111,6 +114,8 @@ pnpm test # Run all Vitest tests 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 @@ -378,6 +383,17 @@ npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization. - We use the `no_prefix` strategy (no `/en-US/` or `/fr-FR/` in URLs) - Locale preference is stored in cookies and respected on subsequent visits +### i18n commands + +The following scripts help manage translation files. `en.json` is the reference locale. + +| Command | Description | +| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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`). | +| `pnpm i18n:check:fix [locale]` | Same as check, but adds missing keys to other locales with English placeholders. | +| `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. | +| `pnpm i18n:report:fix` | Removes unused keys from `en.json` and all other locale files. | + ### Adding a new locale We 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. @@ -421,25 +437,7 @@ Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essential We track the current progress of translations with [Lunaria](https://lunaria.dev/) on this site: https://i18n.npmx.dev/ If you see any outdated translations in your language, feel free to update the keys to match the English version. -In order to make sure you have everything up-to-date, you can run: - -```bash -pnpm i18n:check -``` - -For example to check if all Japanese translation keys are up-to-date, run: - -```bash -pnpm i18n:check ja-JP -``` - -To automatically add missing keys with English placeholders, use `--fix`: - -```bash -pnpm i18n:check:fix fr-FR -``` - -This will add missing keys with `"EN TEXT TO REPLACE: {english text}"` as placeholder values, making it easier to see what needs translation. +Use `pnpm i18n:check` and `pnpm i18n:check:fix` to verify and fix your locale (see [i18n commands](#i18n-commands) above for details). #### Country variants (advanced) @@ -527,6 +525,32 @@ See how `es`, `es-ES`, and `es-419` are configured in [config/i18n.ts](./config/ - Use `common.*` for shared strings (loading, retry, close, etc.) - Use component-specific prefixes: `package.card.*`, `settings.*`, `nav.*` - Do not use dashes (`-`) in translation keys; always use underscore (`_`): e.g., `privacy_policy` instead of `privacy-policy` +- **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. + + **Bad:** + + ```vue + +

{{ $t(`package.tabs.${tab}`) }}

+ + +

{{ $t(myKey) }}

+ ``` + + **Good:** + + ```typescript + const { t } = useI18n() + + const tabLabels = computed(() => ({ + readme: t('package.tabs.readme'), + versions: t('package.tabs.versions'), + })) + ``` + + ```vue +

{{ tabLabels[tab] }}

+ ``` ### Using i18n-ally (recommended) @@ -598,6 +622,60 @@ A coverage test in `test/unit/a11y-component-coverage.spec.ts` ensures all compo > [!IMPORTANT] > 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. +### Lighthouse accessibility tests + +In 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. + +#### How it works + +1. The project is built in test mode (`pnpm build:test`), which activates server-side fixture mocking +2. Lighthouse CI starts a preview server and audits three URLs: `/`, `/search?q=nuxt`, and `/package/nuxt` +3. 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 + +#### Running locally + +```bash +# Build + run both light and dark audits +pnpm test:a11y + +# Or against an existing test build +pnpm test:a11y:prebuilt + +# Or run a single color mode manually +pnpm build:test +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.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 Write end-to-end tests using Playwright: @@ -619,10 +697,12 @@ E2E tests use a fixture system to mock external API requests, ensuring tests are - Serves pre-recorded fixture data from `test/fixtures/` - Enabled via `NUXT_TEST_FIXTURES=true` or Nuxt test mode -**Client-side mocking** (`test/e2e/test-utils.ts`): +**Client-side mocking** (`test/fixtures/mock-routes.cjs`): -- Uses Playwright's route interception to mock browser requests -- All test files import from `./test-utils` instead of `@nuxt/test-utils/playwright` +- Shared URL matching and response generation logic used by both Playwright E2E tests and Lighthouse CI +- Playwright tests (`test/e2e/test-utils.ts`) use this via `page.route()` interception +- Lighthouse tests (`lighthouse-setup.cjs`) use this via Puppeteer request interception +- All E2E test files import from `./test-utils` instead of `@nuxt/test-utils/playwright` - Throws a clear error if an unmocked external request is detected #### Fixture files @@ -670,7 +750,7 @@ URL: https://registry.npmjs.org/some-package You need to either: 1. Add a fixture file for that package/endpoint -2. Update the mock handlers in `test/e2e/test-utils.ts` (client) or `modules/runtime/server/cache.ts` (server) +2. Update the mock handlers in `test/fixtures/mock-routes.cjs` (client) or `modules/runtime/server/cache.ts` (server) ## Submitting changes @@ -709,8 +789,10 @@ Format: `type(scope): description` - `fix(i18n): update French translations` - `chore(deps): update vite to v6` +Where front end changes are made, please include before and after screenshots in your pull request description. + > [!NOTE] -> The subject must start with a lowercase letter. Individual commit messages within your PR don't need to follow this format since they'll be squashed. +> 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. ### PR descriptions diff --git a/README.md b/README.md index 627e0afbb..222ec8c60 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ npmx.dev supports npm permalinks – just replace `npmjs.com` with `npmx.dev | `npmjs.com/org/nuxt` | [`npmx.dev/org/nuxt`](https://npmx.dev/org/nuxt) | > [!TIP] -> Want automatic redirects? Try the [npmx-replace browser extension](https://github.com/tylersayshi/npmx-replace-extension). +> Want automatic redirects? Try the [npmx-replace browser extension](https://github.com/tylersayshi/npmx-replace-extension) (Chrome only for now) or the separate [npmx-redirect extension](https://github.com/iaverages/npmx-redirect) for [Chrome](https://chromewebstore.google.com/detail/lbhjgfgpnlihfmobnohoipeljollhlnb) / [Firefox](https://addons.mozilla.org/en-GB/firefox/addon/npmx-redirect/). #### Not yet supported @@ -150,14 +150,16 @@ We welcome contributions – please do feel free to explore the project and ## Related projects -- [npmx-replace-extension](https://github.com/tylersayshi/npmx-replace-extension) – Browser extension to redirect npmjs.com to npmx.dev +- [npmx-replace-extension](https://github.com/tylersayshi/npmx-replace-extension) – Browser extension to redirect npmjs.com to npmx.dev (Chrome only for now) - [JSR](https://jsr.io/) – The open-source package registry for modern JavaScript and TypeScript - [npm-userscript](https://github.com/bluwy/npm-userscript) – Browser userscript with various improvements and fixes for npmjs.com - [npm-alt](https://npm.willow.sh/) – An alternative npm package browser - [npkg.lorypelli.dev](https://npkg.lorypelli.dev/) – An alternative frontend to npm made with as little client-side JavaScript as possible - [vscode-npmx](https://github.com/npmx-dev/vscode-npmx) – VSCode extension for npmx - [nxjt](https://nxjt.netlify.app) – npmx Jump To: Quickly navigate to npmx common webpages. +- [npmx-weekly](https://npmx-weekly.trueberryless.org/) – A weekly newsletter for the npmx ecosystem. Add your own content via suggestions in the weekly PR on [GitHub](https://github.com/trueberryless-org/npmx-weekly/pulls?q=is%3Aopen+is%3Apr+label%3A%22%F0%9F%95%94+weekly+post%22). - [npmx-digest](https://npmx-digest.trueberryless.org/) – An automated news aggregation website that summarizes npmx activity from GitHub and Bluesky every 8 hours. +- [npmx-redirect](https://github.com/iaverages/npmx-redirect) – Browser extension that automatically redirects npmjs.com URLs to npmx.dev. If you're building something cool, let us know! 🙏 diff --git a/app/app.vue b/app/app.vue index bb95597ba..2b0a58740 100644 --- a/app/app.vue +++ b/app/app.vue @@ -121,7 +121,9 @@ if (import.meta.client) {