Skip to content

Commit 2052c21

Browse files
committed
merge main
2 parents 45f9bc6 + 0553821 commit 2052c21

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+1807
-697
lines changed

.github/workflows/ci.yml

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ jobs:
150150
run: pnpm install
151151

152152
- name: 🏗️ Build project
153-
run: pnpm build:playwright
153+
run: pnpm build:test
154+
env:
155+
VALIDATE_HTML: true
154156

155157
- name: 🖥️ Test project (browser)
156158
run: pnpm test:browser:prebuilt
@@ -178,7 +180,7 @@ jobs:
178180
run: pnpm install
179181

180182
- name: 🏗️ Build project
181-
run: NODE_ENV=test pnpm build
183+
run: pnpm build:test
182184

183185
- name: ♿ Accessibility audit (Lighthouse - ${{ matrix.mode }} mode)
184186
run: ./scripts/lighthouse-a11y.sh
@@ -210,3 +212,25 @@ jobs:
210212

211213
- name: 🧹 Check for unused production code
212214
run: pnpm knip --production
215+
216+
i18n:
217+
name: 🌐 i18n validation
218+
runs-on: ubuntu-24.04-arm
219+
220+
steps:
221+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
222+
223+
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
224+
with:
225+
node-version: lts/*
226+
227+
- uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c # 1e1c8eafbd745f64b1ef30a7d7ed7965034c486c
228+
name: 🟧 Install pnpm
229+
with:
230+
cache: true
231+
232+
- name: 📦 Install dependencies (root only, no scripts)
233+
run: pnpm install --filter . --ignore-scripts
234+
235+
- name: 🌐 Check for missing or dynamic i18n keys
236+
run: pnpm i18n:report

.github/workflows/welcome.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: welcome
2+
3+
on:
4+
pull_request_target:
5+
types:
6+
- closed
7+
8+
permissions: {}
9+
10+
jobs:
11+
welcome:
12+
permissions:
13+
pull-requests: write # to comment on PRs
14+
if: github.repository == 'npmx-dev/npmx.dev' && github.event.pull_request.merged == true
15+
runs-on: ubuntu-slim
16+
name: 🎉 Welcome new contributor
17+
steps:
18+
- name: 🎉 Welcome new contributor
19+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
20+
with:
21+
script: |
22+
const pr = context.payload.pull_request;
23+
const author = pr.user.login;
24+
25+
// Check if this is the author's first merged PR
26+
const { data: prs } = await github.rest.search.issuesAndPullRequests({
27+
q: `repo:${context.repo.owner}/${context.repo.repo} type:pr is:merged author:${author}`,
28+
});
29+
30+
// If the only merged PR is this one, it's their first contribution
31+
if (prs.total_count !== 1) {
32+
console.log(`@${author} already has ${prs.total_count} merged PRs — skipping welcome comment.`);
33+
return;
34+
}
35+
36+
const emojis = ['🎉', '🥳', '🎊', '🚀', '⭐', '💫', '✨', '💪', '👏', '🙌', '🤩', '💥'];
37+
const emoji = emojis[Math.floor(Math.random() * emojis.length)];
38+
39+
const body = [
40+
`Thanks for your first contribution, @${author}! ${emoji}`,
41+
'',
42+
`We'd love to welcome you to the npmx community. Come and say hi on [Discord](https://chat.npmx.dev)! And once you've joined, visit [npmx.wamellow.com](https://npmx.wamellow.com/) to claim the **contributor** role.`,
43+
].join('\n');
44+
45+
await github.rest.issues.createComment({
46+
owner: context.repo.owner,
47+
repo: context.repo.repo,
48+
issue_number: pr.number,
49+
body,
50+
});
51+
52+
console.log(`Welcomed new contributor @${author} on PR #${pr.number}`);

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ test-results/
3939

4040
# generated files
4141
shared/types/lexicons
42+
43+
# output
44+
.vercel

CONTRIBUTING.md

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ This focus helps guide our project decisions as a community and what we choose t
3939
- [Import order](#import-order)
4040
- [Naming conventions](#naming-conventions)
4141
- [Vue components](#vue-components)
42+
- [Internal linking](#internal-linking)
4243
- [RTL Support](#rtl-support)
4344
- [Localization (i18n)](#localization-i18n)
4445
- [Approach](#approach)
@@ -278,6 +279,79 @@ const props = defineProps<{
278279
279280
Ideally, extract utilities into separate files so they can be unit tested. 🙏
280281

282+
### Internal linking
283+
284+
Always 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`.
285+
286+
```vue
287+
<!-- Good: named route -->
288+
<NuxtLink :to="{ name: 'settings' }">Settings</NuxtLink>
289+
290+
<!-- Bad: string path -->
291+
<NuxtLink to="/settings">Settings</NuxtLink>
292+
```
293+
294+
The same applies to programmatic navigation:
295+
296+
```typescript
297+
// Good
298+
navigateTo({ name: 'compare' })
299+
router.push({ name: 'search' })
300+
301+
// Bad
302+
navigateTo('/compare')
303+
router.push('/search')
304+
```
305+
306+
For routes with parameters, pass them explicitly:
307+
308+
```vue
309+
<NuxtLink :to="{ name: '~username', params: { username } }">Profile</NuxtLink>
310+
<NuxtLink :to="{ name: 'org', params: { org: orgName } }">Organization</NuxtLink>
311+
```
312+
313+
Query parameters work as expected:
314+
315+
```vue
316+
<NuxtLink :to="{ name: 'compare', query: { packages: pkg.name } }">Compare</NuxtLink>
317+
```
318+
319+
#### Package routes
320+
321+
For package links, use the auto-imported `packageRoute()` utility from `app/utils/router.ts`. It handles scoped/unscoped packages and optional versions:
322+
323+
```vue
324+
<!-- Links to /package/vue -->
325+
<NuxtLink :to="packageRoute('vue')">vue</NuxtLink>
326+
327+
<!-- Links to /package/@nuxt/kit -->
328+
<NuxtLink :to="packageRoute('@nuxt/kit')">@nuxt/kit</NuxtLink>
329+
330+
<!-- Links to /package/vue/v/3.5.0 -->
331+
<NuxtLink :to="packageRoute('vue', '3.5.0')">vue@3.5.0</NuxtLink>
332+
```
333+
334+
> [!IMPORTANT]
335+
> Never construct package URLs as strings. The route structure uses separate `org` and `name` params, and `packageRoute()` handles the splitting correctly.
336+
337+
#### Available route names
338+
339+
| Route name | URL pattern | Parameters |
340+
| ----------------- | --------------------------------- | ------------------------- |
341+
| `index` | `/` | &mdash; |
342+
| `about` | `/about` | &mdash; |
343+
| `compare` | `/compare` | &mdash; |
344+
| `privacy` | `/privacy` | &mdash; |
345+
| `search` | `/search` | &mdash; |
346+
| `settings` | `/settings` | &mdash; |
347+
| `package` | `/package/:org?/:name` | `org?`, `name` |
348+
| `package-version` | `/package/:org?/:name/v/:version` | `org?`, `name`, `version` |
349+
| `code` | `/package-code/:path+` | `path` (array) |
350+
| `docs` | `/package-docs/:path+` | `path` (array) |
351+
| `org` | `/org/:org` | `org` |
352+
| `~username` | `/~:username` | `username` |
353+
| `~username-orgs` | `/~:username/orgs` | `username` |
354+
281355
## RTL Support
282356

283357
We support `right-to-left` languages, we need to make sure that the UI is working correctly in both directions.
@@ -334,7 +408,7 @@ To add a new locale:
334408
cp i18n/locales/uk-UA.json lunaria/files/uk-UA.json
335409
```
336410

337-
> [!IMPORTANT]
411+
> **Important:**
338412
> This file must be committed. Lunaria uses git history to track translation progress, so the build will fail if this file is missing.
339413
340414
5. If the language is `right-to-left`, add `dir: 'rtl'` (see `ar-EG` in config for example)

app/app.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ onKeyDown(
6262
return
6363
}
6464
65-
router.push('/search')
65+
router.push({ name: 'search' })
6666
},
6767
{ dedupe: true },
6868
)

app/components/AppFooter.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ const isHome = computed(() => route.name === 'index')
1515
</div>
1616
<!-- Desktop: Show all links. Mobile: Links are in MobileMenu -->
1717
<div class="hidden sm:flex items-center gap-6">
18-
<NuxtLink to="/about" class="link-subtle font-mono text-xs flex items-center">
18+
<NuxtLink :to="{ name: 'about' }" class="link-subtle font-mono text-xs flex items-center">
1919
{{ $t('footer.about') }}
2020
</NuxtLink>
2121
<NuxtLink
22-
to="/blog"
22+
:to="{ name: 'blog' }"
2323
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center"
2424
>
2525
{{ $t('footer.blog') }}
2626
</NuxtLink>
2727
<NuxtLink
28-
to="/privacy"
28+
:to="{ name: 'privacy' }"
2929
class="link-subtle font-mono text-xs min-h-11 flex items-center gap-1 lowercase"
3030
>
3131
{{ $t('privacy_policy.title') }}

app/components/AppHeader.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ onKeyStroke(
6565
e => isKeyWithoutModifiers(e, ',') && !isEditableElement(e.target),
6666
e => {
6767
e.preventDefault()
68-
navigateTo('/settings')
68+
navigateTo({ name: 'settings' })
6969
},
7070
{ dedupe: true },
7171
)
@@ -78,7 +78,7 @@ onKeyStroke(
7878
!e.defaultPrevented,
7979
e => {
8080
e.preventDefault()
81-
navigateTo('/compare')
81+
navigateTo({ name: 'compare' })
8282
},
8383
{ dedupe: true },
8484
)
@@ -106,7 +106,7 @@ onKeyStroke(
106106
<!-- Desktop: Logo (navigates home) -->
107107
<div v-if="showLogo" class="hidden sm:flex flex-shrink-0 items-center">
108108
<NuxtLink
109-
to="/"
109+
:to="{ name: 'index' }"
110110
:aria-label="$t('header.home')"
111111
dir="ltr"
112112
class="inline-flex items-center gap-1 header-logo font-mono text-lg font-medium text-fg hover:text-fg/90 transition-colors duration-200 rounded"
@@ -152,7 +152,7 @@ onKeyStroke(
152152
<div class="flex-shrink-0 flex items-center gap-0.5 sm:gap-2">
153153
<!-- Desktop: Compare link -->
154154
<NuxtLink
155-
to="/compare"
155+
:to="{ name: 'compare' }"
156156
class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded"
157157
aria-keyshortcuts="c"
158158
>
@@ -167,7 +167,7 @@ onKeyStroke(
167167

168168
<!-- Desktop: Settings link -->
169169
<NuxtLink
170-
to="/settings"
170+
:to="{ name: 'settings' }"
171171
class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded"
172172
aria-keyshortcuts=","
173173
>

app/components/Code/DirectoryListing.vue

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
<script setup lang="ts">
22
import type { PackageFileTree } from '#shared/types'
3+
import type { RouteLocationRaw } from 'vue-router'
34
import { getFileIcon } from '~/utils/file-icons'
45
import { formatBytes } from '~/utils/formatters'
56
67
const props = defineProps<{
78
tree: PackageFileTree[]
89
currentPath: string
910
baseUrl: string
11+
/** Base path segments for the code route (e.g., ['nuxt', 'v', '4.2.0']) */
12+
basePath: string[]
1013
}>()
1114
1215
// Get the current directory's contents
@@ -36,6 +39,18 @@ const parentPath = computed(() => {
3639
if (parts.length <= 1) return ''
3740
return parts.slice(0, -1).join('/')
3841
})
42+
43+
// Build route object for a path
44+
function getCodeRoute(nodePath?: string): RouteLocationRaw {
45+
if (!nodePath) {
46+
return { name: 'code', params: { path: props.basePath as [string, ...string[]] } }
47+
}
48+
const pathSegments = [...props.basePath, ...nodePath.split('/')]
49+
return {
50+
name: 'code',
51+
params: { path: pathSegments as [string, ...string[]] },
52+
}
53+
}
3954
</script>
4055

4156
<template>
@@ -61,7 +76,7 @@ const parentPath = computed(() => {
6176
>
6277
<td class="py-2 px-4">
6378
<NuxtLink
64-
:to="parentPath ? `${baseUrl}/${parentPath}` : baseUrl"
79+
:to="getCodeRoute(parentPath || undefined)"
6580
class="flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors"
6681
>
6782
<span class="i-carbon:folder w-4 h-4 text-yellow-600" />
@@ -79,7 +94,7 @@ const parentPath = computed(() => {
7994
>
8095
<td class="py-2 px-4">
8196
<NuxtLink
82-
:to="`${baseUrl}/${node.path}`"
97+
:to="getCodeRoute(node.path)"
8398
class="flex items-center gap-2 font-mono text-sm hover:text-fg transition-colors"
8499
:class="node.type === 'directory' ? 'text-fg' : 'text-fg-muted'"
85100
>

app/components/Code/FileTree.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
<script setup lang="ts">
22
import type { PackageFileTree } from '#shared/types'
3+
import type { RouteLocationRaw } from 'vue-router'
34
import { getFileIcon } from '~/utils/file-icons'
45
56
const props = defineProps<{
67
tree: PackageFileTree[]
78
currentPath: string
89
baseUrl: string
10+
/** Base path segments for the code route (e.g., ['nuxt', 'v', '4.2.0']) */
11+
basePath: string[]
912
depth?: number
1013
}>()
1114
@@ -18,6 +21,15 @@ function isNodeActive(node: PackageFileTree): boolean {
1821
return false
1922
}
2023
24+
// Build route object for a file path
25+
function getFileRoute(nodePath: string): RouteLocationRaw {
26+
const pathSegments = [...props.basePath, ...nodePath.split('/')]
27+
return {
28+
name: 'code',
29+
params: { path: pathSegments as [string, ...string[]] },
30+
}
31+
}
32+
2133
const { toggleDir, isExpanded, autoExpandAncestors } = useFileTreeState(props.baseUrl)
2234
2335
// Auto-expand directories in the current path
@@ -63,14 +75,15 @@ watch(
6375
:tree="node.children"
6476
:current-path="currentPath"
6577
:base-url="baseUrl"
78+
:base-path="basePath"
6679
:depth="depth + 1"
6780
/>
6881
</template>
6982

7083
<!-- File -->
7184
<template v-else>
7285
<NuxtLink
73-
:to="`${baseUrl}/${node.path}`"
86+
:to="getFileRoute(node.path)"
7487
class="flex items-center gap-1.5 py-1.5 px-3 font-mono text-sm transition-colors hover:bg-bg-muted"
7588
:class="currentPath === node.path ? 'bg-bg-muted text-fg' : 'text-fg-muted'"
7689
:style="{ paddingLeft: `${depth * 12 + 32}px` }"

0 commit comments

Comments
 (0)