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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ What npmx offers:
- **Version range resolution** – dependency ranges (e.g., `^1.0.0`) resolve to actual installed versions
- **Claim new packages** – register new package names directly from search results (via local connector)
- **Clickable version tags** – navigate directly to any version from the versions list
- **Package access management** – view and manage team/collaborator access for scoped packages (via local connector)

### User & org pages

Expand Down Expand Up @@ -91,7 +92,7 @@ What npmx offers:
| Multi-provider repo support | ❌ | ✅ |
| Version range resolution | ❌ | ✅ |
| Dependents list | ✅ | 🚧 |
| Package admin (access/owners) | ✅ | 🚧 |
| Package admin (access/owners) | ✅ | |
| Org/team management | ✅ | 🚧 |
| 2FA/account settings | ✅ | ❌ |
| Claim new package names | ✅ | ✅ |
Expand All @@ -118,8 +119,6 @@ npmx.dev supports npm permalinks – just replace `npmjs.com` with `npmx.dev

#### Not yet supported

- `/package/<name>/access` &ndash; package access settings
- `/package/<name>/dependents` &ndash; dependent packages list
- `/settings/*` &ndash; account settings pages

### Simpler URLs
Expand Down
22 changes: 21 additions & 1 deletion app/components/Package/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const props = defineProps<{
latestVersion?: SlimVersion | null
provenanceData?: ProvenanceDetails | null
provenanceStatus?: string | null
page: 'main' | 'docs' | 'code' | 'diff'
page: 'main' | 'docs' | 'code' | 'diff' | 'access'
versionUrlPattern: string
}>()

Expand Down Expand Up @@ -108,6 +108,18 @@ const mainLink = computed((): RouteLocationRaw | null => {
return packageRoute(props.pkg.name, props.resolvedVersion)
})

const accessLink = computed((): RouteLocationRaw | null => {
if (props.pkg == null) return null
const split = props.pkg.name.split('/')
return {
name: 'package-access',
params: {
org: split.length === 2 ? split[0] : undefined,
name: split.length === 2 ? split[1]! : split[0]!,
},
}
})

const diffLink = computed((): RouteLocationRaw | null => {
if (
props.pkg == null ||
Expand Down Expand Up @@ -343,6 +355,14 @@ const fundingUrl = computed(() => {
>
{{ $t('compare.compare_versions') }}
</LinkBase>
<LinkBase
v-if="accessLink"
:to="accessLink"
class="decoration-none border-b-2 p-1 hover:border-accent/50 focus-visible:[outline-offset:-2px]!"
:class="page === 'access' ? 'border-accent text-accent!' : 'border-transparent'"
>
{{ $t('package.links.access') }}
</LinkBase>
</nav>
</div>
</div>
Expand Down
93 changes: 93 additions & 0 deletions app/pages/package/[[org]]/[name]/access.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<script setup lang="ts">
definePageMeta({
name: 'package-access',
scrollMargin: 200,
})

const route = useRoute('package-access')

const packageName = computed(() => {
const { org, name } = route.params
return org ? `${org}/${name}` : name
})

const { data: pkg } = usePackage(packageName)

const resolvedVersion = computed(() => {
const latest = pkg.value?.['dist-tags']?.latest
if (!latest) return null
return latest
})

const displayVersion = computed(() => pkg.value?.requestedVersion ?? null)

const latestVersion = computed(() => {
if (!pkg.value) return null
const latestTag = pkg.value['dist-tags']?.latest
if (!latestTag) return null
return pkg.value.versions[latestTag] ?? null
})

const versionUrlPattern = computed(() => {
const split = packageName.value.split('/')
if (split.length === 2) {
return `/package/${split[0]}/${split[1]}/v/{version}`
}
return `/package/${packageName.value}/v/{version}`
})

const { isConnected } = useConnector()
const connectorModal = import.meta.client ? useModal('connector-modal') : null

useSeoMeta({
title: () => `Access - ${packageName.value} - npmx`,
description: () => `Manage access and collaborators for ${packageName.value}`,
})
Comment on lines +42 to +45
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and inspect lines 42-45
if [ -f "app/pages/package/[[org]]/[name]/access.vue" ]; then
  echo "=== File exists ==="
  wc -l "app/pages/package/[[org]]/[name]/access.vue"
  echo "=== Lines 40-50 ==="
  sed -n '40,50p' "app/pages/package/[[org]]/[name]/access.vue"
else
  echo "File not found at literal path, searching for access.vue in package pages..."
  find app/pages -name "access.vue" -type f
fi

Repository: npmx-dev/npmx.dev

Length of output: 449


🏁 Script executed:

# Search for the translation keys in the locale files
echo "=== Searching for package.access.page_title ==="
rg "package\.access\.page_title" i18n/locales/

echo "=== Searching for package.access.page_subtitle ==="
rg "package\.access\.page_subtitle" i18n/locales/

echo "=== Checking what access-related keys exist ==="
rg "package\.access" i18n/locales/ -A 1

Repository: npmx-dev/npmx.dev

Length of output: 207


🏁 Script executed:

# Check what page_title and page_subtitle keys exist
echo "=== Existing page_title keys ==="
rg '"[^"]*page_title' i18n/locales/en-US.json | head -20

echo "=== Existing page_subtitle keys ==="
rg '"[^"]*page_subtitle' i18n/locales/en-US.json | head -20

echo "=== Check if en-US.json exists ==="
ls -la i18n/locales/ | head -10

Repository: npmx-dev/npmx.dev

Length of output: 704


🏁 Script executed:

# Search for "Access" in the locale files to see if there are any access-related translations
echo "=== Searching for 'Access' in locale files ==="
rg -i '"Access' i18n/locales/en-US.json

echo "=== Searching for collaborators translations ==="
rg -i 'collaborator' i18n/locales/en-US.json

Repository: npmx-dev/npmx.dev

Length of output: 156


🏁 Script executed:

# Check how other package pages (like /details or /settings) handle useSeoMeta
echo "=== Checking details.vue for SEO metadata pattern ==="
find app/pages/package -name "*.vue" -type f | head -10

echo "=== Looking for useSeoMeta usage in package pages ==="
rg -l "useSeoMeta" app/pages/package/ -t vue

Repository: npmx-dev/npmx.dev

Length of output: 432


🏁 Script executed:

# Find the correct English locale file
echo "=== List all locale files ==="
ls i18n/locales/ | grep -E "^en"

echo "=== Check size of en.json if it exists ==="
if [ -f "i18n/locales/en.json" ]; then
  wc -l i18n/locales/en.json
fi

Repository: npmx-dev/npmx.dev

Length of output: 189


🏁 Script executed:

# Search for useSeoMeta in all vue files in package pages
echo "=== Finding useSeoMeta usage in package pages ==="
grep -r "useSeoMeta" app/pages/package/ --include="*.vue"

Repository: npmx-dev/npmx.dev

Length of output: 217


🏁 Script executed:

# Check the main package page to see SEO metadata pattern
echo "=== Check [name].vue for useSeoMeta pattern ==="
grep -A 3 "useSeoMeta" "app/pages/package/[[org]]/[name].vue"

echo "=== Check index.vue for useSeoMeta pattern ==="
grep -A 3 "useSeoMeta" "app/pages/package/[[org]]/[name]/index.vue"

echo "=== Check versions.vue for useSeoMeta pattern ==="
grep -A 3 "useSeoMeta" "app/pages/package/[[org]]/[name]/versions.vue"

Repository: npmx-dev/npmx.dev

Length of output: 473


🏁 Script executed:

# Check what's in en.json - look for page-related keys
echo "=== en.json structure ==="
head -100 i18n/locales/en.json | cat -n

echo "=== Search for 'page' keys in en.json ==="
rg '"[^"]*page' i18n/locales/en.json | head -30

Repository: npmx-dev/npmx.dev

Length of output: 6258


🏁 Script executed:

# Search for any use of $t() in useSeoMeta across the entire codebase
echo "=== Looking for $t() usage in useSeoMeta ==="
grep -r "useSeoMeta" app/ --include="*.vue" -A 10 | grep -E "(useSeoMeta|t\()" | head -20

# Also check if useSeoMeta is used with $t() anywhere
grep -r "\$t(" app/pages/ --include="*.vue" | head -10

Repository: npmx-dev/npmx.dev

Length of output: 2094


🏁 Script executed:

# Check the en.json file to understand the structure and naming conventions
echo "=== Looking at en.json full content ==="
cat i18n/locales/en.json

Repository: npmx-dev/npmx.dev

Length of output: 50373


Localise SEO metadata instead of hard-coding English copy.

useSeoMeta currently bypasses translations, so metadata stays English in non-English locales. Other pages like about.vue and accessibility.vue already follow this localisation pattern.

🌍 Suggested fix
 useSeoMeta({
-  title: () => `Access - ${packageName.value} - npmx`,
-  description: () => `Manage access and collaborators for ${packageName.value}`,
+  title: () => `${$t('package.access.page_title')} - ${packageName.value} - npmx`,
+  description: () => $t('package.access.page_subtitle', { name: packageName.value }),
 })
📝 Committable suggestion

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

Suggested change
useSeoMeta({
title: () => `Access - ${packageName.value} - npmx`,
description: () => `Manage access and collaborators for ${packageName.value}`,
})
useSeoMeta({
title: () => `${$t('package.access.page_title')} - ${packageName.value} - npmx`,
description: () => $t('package.access.page_subtitle', { name: packageName.value }),
})

</script>

<template>
<main class="flex-1 pb-8">
<PackageHeader
:pkg="pkg ?? null"
:resolved-version="resolvedVersion"
:display-version="displayVersion"
:latest-version="latestVersion"
:version-url-pattern="versionUrlPattern"
page="access"
/>

<div class="container py-6">
<h1 class="font-mono text-xl font-semibold mb-1">
{{ $t('package.access.page_title') }}
</h1>
<p class="text-sm text-fg-muted mb-6">
{{ $t('package.access.page_subtitle', { name: packageName }) }}
</p>

<ClientOnly>
<template v-if="isConnected">
<PackageAccessControls :package-name="packageName" />
</template>
<template v-else>
<div class="py-12 text-center">
<span class="i-lucide:lock w-12 h-12 mx-auto mb-4 text-fg-subtle block" />
<p class="text-fg-muted mb-2 font-medium">
{{ $t('package.access.connect_required') }}
</p>
<p class="text-sm text-fg-subtle mb-6">
{{ $t('package.access.connect_hint') }}
</p>
<ButtonBase variant="primary" @click="connectorModal?.open()">
{{ $t('connector.modal.connect') }}
</ButtonBase>
</div>
</template>
<template #fallback>
<div class="space-y-4">
<SkeletonInline v-for="i in 4" :key="i" class="h-12 w-full rounded-md" />
</div>
</template>
</ClientOnly>
</div>
</main>
</template>
7 changes: 6 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@
"docs": "docs",
"fund": "fund",
"compare": "compare",
"compare_this_package": "compare this package"
"compare_this_package": "compare this package",
"access": "access"
},
"likes": {
"like": "Like this package",
Expand Down Expand Up @@ -587,6 +588,10 @@
"show_all": "show {count} deprecated package | show all {count} deprecated packages"
},
"access": {
"page_title": "Access & Collaborators",
"page_subtitle": "Manage team and collaborator access for {name}",
"connect_required": "Connect your npm CLI to manage access",
"connect_hint": "Use the local connector to view and manage package collaborators and team access.",
"title": "Team Access",
"refresh": "Refresh team access",
"list_label": "Team access list",
Expand Down
15 changes: 15 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,9 @@
},
"compare_this_package": {
"type": "string"
},
"access": {
"type": "string"
}
},
"additionalProperties": false
Expand Down Expand Up @@ -1765,6 +1768,18 @@
"access": {
"type": "object",
"properties": {
"page_title": {
"type": "string"
},
"page_subtitle": {
"type": "string"
},
"connect_required": {
"type": "string"
},
"connect_hint": {
"type": "string"
},
"title": {
"type": "string"
},
Expand Down
Loading