Skip to content
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
20 changes: 17 additions & 3 deletions app/components/Package/InstallScripts.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { getOutdatedTooltip, getVersionClass } from '~/utils/npm/outdated-dependencies'
import type { RouteLocationRaw } from 'vue-router'

const props = defineProps<{
packageName: string
Expand All @@ -11,12 +12,25 @@ const props = defineProps<{
}
}>()

function getCodeLink(filePath: string): string {
return `/code/${props.packageName}/v/${props.version}/${filePath}`
function getCodeLink(filePath: string): RouteLocationRaw {
const split = props.packageName.split('/')

return {
name: 'code',
params: {
org: split.length === 2 ? split[0] : null,
packageName: split.length === 2 ? split[1]! : split[0]!,
version: props.version,
filePath: filePath,
},
}
}

const scriptParts = computed(() => {
const parts: Record<string, { prefix: string | null; filePath: string | null; link: string }> = {}
const parts: Record<
string,
{ prefix: string | null; filePath: string | null; link: RouteLocationRaw }
> = {}
for (const scriptName of props.installScripts.scripts) {
const content = props.installScripts.content?.[scriptName]
if (!content) continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ 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(.*)?',
'/package/code/:packageName/v/:version/:filePath(.*)?',
// '/code/@:org?/:packageName/v/:version/:filePath(.*)?',
],
})

const route = useRoute('code')
Expand All @@ -19,23 +23,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 +37,31 @@ 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({
org: route.params.org,
packageName: route.params.packageName,
version: '{version}',
filePath: filePath.value,
}),
)

// Fetch file tree
const { data: fileTree, status: treeStatus } = useFetch<PackageFileTreeResponse>(
Expand Down Expand Up @@ -192,17 +201,13 @@ 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 getCurrentCodeUrlWithPath(path?: string): string {
return getCodeUrl({
...route.params,
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${getCodeUrl(route.params)}`)

// Toggle markdown view mode
const markdownViewModes = [
Expand Down Expand Up @@ -350,7 +349,7 @@ defineOgImageComponent('Default', {
>
<NuxtLink
v-if="filePath"
:to="getCodeUrl()"
:to="getCurrentCodeUrlWithPath()"
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="getCurrentCodeUrlWithPath(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="getCurrentCodeUrlWithPath()"
: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="getCurrentCodeUrlWithPath()"
: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="getCurrentCodeUrlWithPath()"
:base-route="route"
/>
</Teleport>
</ClientOnly>
Expand Down
30 changes: 22 additions & 8 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { detectPublishSecurityDowngradeForVersion } from '~/utils/publish-securi
import { useModal } from '~/composables/useModal'
import { useAtproto } from '~/composables/atproto/useAtproto'
import { togglePackageLike } from '~/utils/atproto/likes'
import type { RouteLocationRaw } from 'vue-router'

defineOgImageComponent('Package', {
name: () => packageName.value,
Expand Down Expand Up @@ -512,17 +513,29 @@ useSeoMeta({
twitterDescription: () => pkg.value?.description ?? '',
})

const codeLink = computed((): RouteLocationRaw | 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] : undefined,
packageName: split.length === 2 ? split[1]! : split[0]!,
version: resolvedVersion.value,
filePath: '',
},
}
})

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

navigateTo(codeLink.value)
},
{ dedupe: true },
)
Expand Down Expand Up @@ -658,8 +671,9 @@ onKeyStroke(
{{ $t('package.links.docs') }}
</LinkBase>
<LinkBase
v-if="codeLink"
variant="button-secondary"
:to="{ name: 'code', params: { path: [pkg.name, 'v', resolvedVersion] } }"
:to="codeLink"
aria-keyshortcuts="."
classicon="i-carbon:code"
>
Expand Down
Loading
Loading