Skip to content
Merged
15 changes: 8 additions & 7 deletions app/components/Code/DirectoryListing.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script setup lang="ts">
import type { PackageFileTree } from '#shared/types'
import type { RouteLocationRaw } from 'vue-router'
import type { RouteNamedMap } from 'vue-router/auto-routes'
import { getFileIcon } from '~/utils/file-icons'

const props = defineProps<{
tree: PackageFileTree[]
currentPath: string
baseUrl: string
/** Base path segments for the code route (e.g., ['nuxt', 'v', '4.2.0']) */
basePath: string[]
baseRoute: Pick<RouteNamedMap['code'], 'params'>
}>()

// Get the current directory's contents
Expand Down Expand Up @@ -41,13 +41,14 @@ const parentPath = computed(() => {

// Build route object for a path
function getCodeRoute(nodePath?: string): RouteLocationRaw {
if (!nodePath) {
return { name: 'code', params: { path: props.basePath as [string, ...string[]] } }
}
const pathSegments = [...props.basePath, ...nodePath.split('/')]
return {
name: 'code',
params: { path: pathSegments as [string, ...string[]] },
params: {
org: props.baseRoute.params.org,
packageName: props.baseRoute.params.packageName,
version: props.baseRoute.params.version,
filePath: nodePath ?? '',
},
}
}

Expand Down
14 changes: 9 additions & 5 deletions app/components/Code/FileTree.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script setup lang="ts">
import type { PackageFileTree } from '#shared/types'
import type { RouteLocationRaw } from 'vue-router'
import type { RouteNamedMap } from 'vue-router/auto-routes'
import { getFileIcon } from '~/utils/file-icons'

const props = defineProps<{
tree: PackageFileTree[]
currentPath: string
baseUrl: string
/** Base path segments for the code route (e.g., ['nuxt', 'v', '4.2.0']) */
basePath: string[]
baseRoute: Pick<RouteNamedMap['code'], 'params'>
depth?: number
}>()

Expand All @@ -23,10 +23,14 @@ function isNodeActive(node: PackageFileTree): boolean {

// Build route object for a file path
function getFileRoute(nodePath: string): RouteLocationRaw {
const pathSegments = [...props.basePath, ...nodePath.split('/')]
return {
name: 'code',
params: { path: pathSegments as [string, ...string[]] },
params: {
org: props.baseRoute.params.org,
packageName: props.baseRoute.params.packageName,
version: props.baseRoute.params.version,
filePath: nodePath ?? '',
},
}
}

Expand Down Expand Up @@ -72,7 +76,7 @@ watch(
:tree="node.children"
:current-path="currentPath"
:base-url="baseUrl"
:base-path="basePath"
:base-route="baseRoute"
:depth="depth + 1"
/>
</template>
Expand Down
6 changes: 3 additions & 3 deletions app/components/Code/MobileTreeDrawer.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
import type { PackageFileTree } from '#shared/types'
import type { RouteNamedMap } from 'vue-router/auto-routes'

defineProps<{
tree: PackageFileTree[]
currentPath: string
baseUrl: string
/** Base path segments for the code route (e.g., ['nuxt', 'v', '4.2.0']) */
basePath: string[]
baseRoute: Pick<RouteNamedMap['code'], 'params'>
}>()

const isOpen = shallowRef(false)
Expand Down Expand Up @@ -75,7 +75,7 @@ watch(isOpen, open => (isLocked.value = open))
:tree="tree"
:current-path="currentPath"
:base-url="baseUrl"
:base-path="basePath"
:base-route="baseRoute"
/>
</aside>
</Transition>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import type {

definePageMeta({
name: 'code',
path: '/package-code/:path+',
alias: ['/package/code/:path+', '/code/:path+'],
path: '/package-code/@:org?/:packageName/v/:version/:filePath(.*)?',
alias: [
'/package/code/@:org?/:packageName/v/:version/:filePath(.*)?',
'/code/@:org?/:packageName/v/:version/:filePath(.*)?',
],
})

const route = useRoute('code')
Expand All @@ -19,23 +22,11 @@ const route = useRoute('code')
// /code/nuxt/v/4.2.0/src/index.ts → packageName: "nuxt", version: "4.2.0", filePath: "src/index.ts"
// /code/@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", version: "1.0.0", filePath: null
const parsedRoute = computed(() => {
const segments = route.params.path

// Find the /v/ separator for version
const vIndex = segments.indexOf('v')
if (vIndex === -1 || vIndex >= segments.length - 1) {
// No version specified - redirect or error
return {
packageName: segments.join('/'),
version: null as string | null,
filePath: null as string | null,
}
}

const packageName = segments.slice(0, vIndex).join('/')
const afterVersion = segments.slice(vIndex + 1)
const version = afterVersion[0] ?? null
const filePath = afterVersion.length > 1 ? afterVersion.slice(1).join('/') : null
const packageName = route.params.org
? `@${route.params.org}/${route.params.packageName}`
: route.params.packageName
const version = route.params.version
const filePath = route.params.filePath || null

return { packageName, version, filePath }
})
Expand All @@ -45,14 +36,30 @@ const version = computed(() => parsedRoute.value.version)
const filePathOrig = computed(() => parsedRoute.value.filePath)
const filePath = computed(() => parsedRoute.value.filePath?.replace(/\/$/, ''))

// Navigation helper - build URL for a path
function getCodeUrl(args: {
org?: string
packageName: string
version: string
filePath?: string
}): string {
const base = args.org
? `/package-code/@${args.org}/${args.packageName}/v/${args.version}`
: `/package-code/${args.packageName}/v/${args.version}`
return args.filePath ? `${base}/${args.filePath}` : base
}

// Fetch package data for version list
const { data: pkg } = usePackage(packageName)

// URL pattern for version selector - includes file path if present
const versionUrlPattern = computed(() => {
const base = `/package-code/${packageName.value}/v/{version}`
return filePath.value ? `${base}/${filePath.value}` : base
})
const versionUrlPattern = computed(() =>
getCodeUrl({
packageName: packageName.value,
version: '{version}',
filePath: filePath.value,
}),
)

// Fetch file tree
const { data: fileTree, status: treeStatus } = useFetch<PackageFileTreeResponse>(
Expand Down Expand Up @@ -192,17 +199,15 @@ const breadcrumbs = computed(() => {
})

// Navigation helper - build URL for a path
function getCodeUrl(path?: string): string {
const base = `/package-code/${packageName.value}/v/${version.value}`
return path ? `${base}/${path}` : base
function getCurrentCodeUrl(path?: string): string {
return getCodeUrl({
org: route.params.org,
packageName: packageName.value,
version: version.value!,
filePath: path,
})
}

// Base path segments for route objects (e.g., ['nuxt', 'v', '4.2.0'] or ['@nuxt', 'kit', 'v', '1.0.0'])
const basePath = computed(() => {
const segments = packageName.value.split('/')
return [...segments, 'v', version.value ?? '']
})

// Extract org name from scoped package
const orgName = computed(() => {
const name = packageName.value
Expand Down Expand Up @@ -244,13 +249,7 @@ function copyPermalinkUrl() {
}

// Canonical URL for this code page
const canonicalUrl = computed(() => {
let url = `https://npmx.dev/package-code/${packageName.value}/v/${version.value}`
if (filePath.value) {
url += `/${filePath.value}`
}
return url
})
const canonicalUrl = computed(() => `https://npmx.dev${getCurrentCodeUrl()}`)

// Toggle markdown view mode
const markdownViewModes = [
Expand Down Expand Up @@ -350,7 +349,7 @@ defineOgImageComponent('Default', {
>
<NuxtLink
v-if="filePath"
:to="getCodeUrl()"
:to="getCurrentCodeUrl()"
class="text-fg-muted hover:text-fg transition-colors shrink-0"
>
{{ $t('code.root') }}
Expand All @@ -360,7 +359,7 @@ defineOgImageComponent('Default', {
<span class="text-fg-subtle">/</span>
<NuxtLink
v-if="i < breadcrumbs.length - 1"
:to="getCodeUrl(crumb.path)"
:to="getCurrentCodeUrl(crumb.path)"
class="text-fg-muted hover:text-fg transition-colors"
>
{{ crumb.name }}
Expand Down Expand Up @@ -402,8 +401,8 @@ defineOgImageComponent('Default', {
<CodeFileTree
:tree="fileTree.tree"
:current-path="filePath ?? ''"
:base-url="getCodeUrl()"
:base-path="basePath"
:base-url="getCurrentCodeUrl()"
:base-route="route"
/>
</aside>

Expand Down Expand Up @@ -558,8 +557,8 @@ defineOgImageComponent('Default', {
<CodeDirectoryListing
:tree="fileTree.tree"
:current-path="filePath ?? ''"
:base-url="getCodeUrl()"
:base-path="basePath"
:base-url="getCurrentCodeUrl()"
:base-route="route"
/>
</template>
</div>
Expand All @@ -572,8 +571,8 @@ defineOgImageComponent('Default', {
v-if="fileTree"
:tree="fileTree.tree"
:current-path="filePath ?? ''"
:base-url="getCodeUrl()"
:base-path="basePath"
:base-url="getCurrentCodeUrl()"
:base-route="route"
/>
</Teleport>
</ClientOnly>
Expand Down
29 changes: 21 additions & 8 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -516,17 +516,29 @@ useSeoMeta({
twitterDescription: () => pkg.value?.description ?? '',
})

const linkCode = computed((): Parameters<typeof navigateTo>[0] | null => {
if (pkg.value == null || resolvedVersion.value == null) {
return null
}
const split = pkg.value.name.split('/')
return {
name: 'code',
params: {
org: split.length === 2 ? split[0]?.replace(/^@/, '') : undefined,
packageName: split.length === 2 ? split[1]! : split[0]!,
version: resolvedVersion.value,
filePath: '',
},
}
Comment thread
essenmitsosse marked this conversation as resolved.
})

onKeyStroke(
e => isKeyWithoutModifiers(e, '.') && !isEditableElement(e.target),
e => {
if (pkg.value == null || resolvedVersion.value == null) return
if (linkCode.value === null) return
e.preventDefault()
navigateTo({
name: 'code',
params: {
path: [pkg.value.name, 'v', resolvedVersion.value],
},
})

navigateTo(linkCode.value)
},
{ dedupe: true },
)
Expand Down Expand Up @@ -662,8 +674,9 @@ onKeyStroke(
{{ $t('package.links.docs') }}
</LinkBase>
<LinkBase
v-if="linkCode"
variant="button-secondary"
:to="{ name: 'code', params: { path: [pkg.name, 'v', resolvedVersion] } }"
:to="linkCode"
aria-keyshortcuts="."
classicon="i-carbon:code"
>
Expand Down
20 changes: 15 additions & 5 deletions test/nuxt/a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,9 @@ describe('component accessibility audits', () => {
tree: mockTree,
currentPath: '',
baseUrl: '/package-code/vue',
basePath: ['vue', 'v', '3.0.0'],
baseRoute: {
params: { packageName: 'vue', version: '3.0.0', filePath: '' },
},
},
})
const results = await runAxe(component)
Expand All @@ -934,7 +936,9 @@ describe('component accessibility audits', () => {
tree: mockTree,
currentPath: 'src',
baseUrl: '/package-code/vue',
basePath: ['vue', 'v', '3.0.0'],
baseRoute: {
params: { packageName: 'vue', version: '3.0.0', filePath: '' },
},
},
})
const results = await runAxe(component)
Expand All @@ -959,7 +963,9 @@ describe('component accessibility audits', () => {
tree: mockTree,
currentPath: '',
baseUrl: '/package-code/vue',
basePath: ['vue', 'v', '3.0.0'],
baseRoute: {
params: { packageName: 'vue', version: '3.0.0', filePath: '' },
},
},
})
const results = await runAxe(component)
Expand All @@ -972,7 +978,9 @@ describe('component accessibility audits', () => {
tree: mockTree,
currentPath: 'src/index.ts',
baseUrl: '/package-code/vue',
basePath: ['vue', 'v', '3.0.0'],
baseRoute: {
params: { packageName: 'vue', version: '3.0.0', filePath: '' },
},
},
})
const results = await runAxe(component)
Expand Down Expand Up @@ -1222,7 +1230,9 @@ describe('component accessibility audits', () => {
tree: mockTree,
currentPath: '',
baseUrl: '/package-code/vue',
basePath: ['vue', 'v', '3.0.0'],
baseRoute: {
params: { packageName: 'vue', version: '3.0.0', filePath: '' },
},
},
})
const results = await runAxe(component)
Expand Down
Loading