[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: new routing schema (#674)

Co-authored-by: Daniel Roe <daniel@roe.dev>

authored by

Mikołaj Misztal
Daniel Roe
and committed by
GitHub
4bf73171 e46df532

+150 -134
+1 -1
.lighthouserc.cjs
··· 29 29 url: [ 30 30 'http://localhost:3000/', 31 31 'http://localhost:3000/search?q=nuxt', 32 - 'http://localhost:3000/nuxt', 32 + 'http://localhost:3000/package/nuxt', 33 33 ], 34 34 numberOfRuns: 1, 35 35 chromePath: findChrome(),
+1 -1
app/components/Header/PackagesDropdown.vue
··· 94 94 <ul v-else-if="packages.length > 0" class="py-1 max-h-80 overflow-y-auto"> 95 95 <li v-for="pkg in packages" :key="pkg"> 96 96 <NuxtLink 97 - :to="`/${pkg}`" 97 + :to="`/package/${pkg}`" 98 98 class="block px-3 py-2 font-mono text-sm text-fg hover:bg-bg-subtle transition-colors truncate" 99 99 > 100 100 {{ pkg }}
+1 -1
app/components/Package/MetricsBadges.vue
··· 45 45 const typesHref = computed(() => { 46 46 if (!analysis.value) return null 47 47 if (analysis.value.types?.kind === '@types') { 48 - return `/${analysis.value.types.packageName}` 48 + return `/package/${analysis.value.types.packageName}` 49 49 } 50 50 return null 51 51 })
+2 -2
app/components/Package/SkillsModal.vue
··· 8 8 }>() 9 9 10 10 function getSkillSourceUrl(skill: SkillListItem): string { 11 - const base = `/code/${props.packageName}` 11 + const base = `/package-code/${props.packageName}` 12 12 const versionPath = props.version ? `/v/${props.version}` : '' 13 13 return `${base}${versionPath}/skills/${skill.dirName}/SKILL.md` 14 14 } ··· 101 101 </template> 102 102 </i18n-t> 103 103 <a 104 - href="/skills-npm" 104 + href="/package/skills-npm" 105 105 class="inline-flex items-center gap-1 text-xs text-fg-subtle hover:text-fg transition-colors shrink-0" 106 106 > 107 107 {{ $t('package.skills.learn_more') }}
+2 -2
app/components/Terminal/Install.vue
··· 149 149 ></code 150 150 > 151 151 <NuxtLink 152 - :to="`/${typesPackageName}`" 152 + :to="`/package/${typesPackageName}`" 153 153 class="text-fg-subtle hover:text-fg-muted text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded" 154 154 :title="$t('package.get_started.view_types', { package: typesPackageName })" 155 155 > ··· 228 228 }}</span> 229 229 </button> 230 230 <NuxtLink 231 - :to="`/${createPackageInfo.packageName}`" 231 + :to="`/package/${createPackageInfo.packageName}`" 232 232 class="text-fg-subtle hover:text-fg-muted text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded" 233 233 :title="`View ${createPackageInfo.packageName}`" 234 234 >
+1 -1
app/components/VersionSelector.vue
··· 620 620 <!-- Link to package page for full version list --> 621 621 <div class="border-t border-border mt-1 pt-1 px-3 py-2"> 622 622 <NuxtLink 623 - :to="`/${packageName}`" 623 + :to="`/package/${packageName}`" 624 624 class="text-xs text-fg-subtle hover:text-fg transition-[color] focus-visible:outline-none focus-visible:text-fg" 625 625 @click="isOpen = false" 626 626 >
+1 -1
app/components/compare/PackageSelector.vue
··· 64 64 class="inline-flex items-center gap-2 px-3 py-1.5 bg-bg-subtle border border-border rounded-md" 65 65 > 66 66 <NuxtLink 67 - :to="`/${pkg}`" 67 + :to="`/package/${pkg}`" 68 68 class="font-mono text-sm text-fg hover:text-accent transition-colors" 69 69 > 70 70 {{ pkg }}
+28 -7
app/middleware/canonical-redirects.global.ts
··· 1 1 /** 2 2 * Redirect legacy URLs to canonical paths (client-side only) 3 3 * 4 - * - /package/* → /* 5 - * - /package/code/* → /code/* 4 + * - /package/code/* → /package-code/* 5 + * - /code/* → /package-code/* 6 + * - /package/docs/* → /package-docs/* 7 + * - /docs/* → /package-docs/* 6 8 * - /org/* → /@* 9 + * - /* → /package/* (Unless its an existing page) 7 10 */ 8 11 export default defineNuxtRouteMiddleware(to => { 9 12 // Only redirect on client-side to avoid breaking crawlers mid-transition ··· 11 14 12 15 const path = to.path 13 16 14 - // /package/code/* → /code/* 17 + // /package/code/* → /package-code/* 15 18 if (path.startsWith('/package/code/')) { 16 - return navigateTo(path.replace('/package/code/', '/code/'), { replace: true }) 19 + return navigateTo(path.replace('/package/code/', '/package-code/'), { replace: true }) 20 + } 21 + // /code/* → /package-code/* 22 + if (path.startsWith('/code/')) { 23 + return navigateTo(path.replace('/code/', '/package-code/'), { replace: true }) 17 24 } 18 25 19 - // /package/* → /* 20 - if (path.startsWith('/package/')) { 21 - return navigateTo(path.replace('/package/', '/'), { replace: true }) 26 + // /package/docs/* → /package-docs/* 27 + if (path.startsWith('/package/docs/')) { 28 + return navigateTo(path.replace('/package/docs/', '/package-docs/'), { replace: true }) 29 + } 30 + // /docs/* → /package-docs/* 31 + if (path.startsWith('/docs/')) { 32 + return navigateTo(path.replace('/docs/', '/package-docs/'), { replace: true }) 22 33 } 23 34 24 35 // /org/* → /@* 25 36 if (path.startsWith('/org/')) { 26 37 return navigateTo(path.replace('/org/', '/@'), { replace: true }) 38 + } 39 + 40 + // Keep this one last as it will catch everything 41 + // /* → /package/* (Unless its an existing page) 42 + if (path.startsWith('/') && !path.startsWith('/package/')) { 43 + const router = useRouter() 44 + const resolved = router.resolve(path) 45 + if (resolved?.matched?.length === 1 && resolved.matched[0]?.path === '/:package(.*)*') { 46 + return navigateTo(`/package${path}`, { replace: true }) 47 + } 27 48 } 28 49 })
+14 -24
app/pages/[...package].vue app/pages/package/[...package].vue
··· 14 14 15 15 definePageMeta({ 16 16 name: 'package', 17 - alias: ['/package/:package(.*)*'], 17 + alias: ['/:package(.*)*'], 18 18 }) 19 19 20 20 const router = useRouter() ··· 254 254 const docsLink = computed(() => { 255 255 if (!resolvedVersion.value) return null 256 256 257 - return { 258 - name: 'docs' as const, 259 - params: { 260 - path: [...pkg.value!.name.split('/'), 'v', resolvedVersion.value], 261 - }, 262 - } 257 + return `/package-docs/${pkg.value!.name}/v/${resolvedVersion.value}` 263 258 }) 264 259 265 260 const fundingUrl = computed(() => { ··· 335 330 336 331 // Canonical URL for this package page 337 332 const canonicalUrl = computed(() => { 338 - const base = `https://npmx.dev/${packageName.value}` 333 + const base = `https://npmx.dev/package/${packageName.value}` 339 334 return requestedVersion.value ? `${base}/v/${requestedVersion.value}` : base 340 335 }) 341 336 ··· 457 452 458 453 <NuxtLink 459 454 v-if="requestedVersion && resolvedVersion !== requestedVersion" 460 - :to="`/${pkg.name}/v/${resolvedVersion}`" 455 + :to="`/package/${pkg.name}/v/${resolvedVersion}`" 461 456 :title="$t('package.view_permalink')" 462 457 >{{ resolvedVersion }}</NuxtLink 463 458 > ··· 519 514 </kbd> 520 515 </NuxtLink> 521 516 <NuxtLink 522 - :to="{ 523 - name: 'code', 524 - params: { 525 - path: [...pkg.name.split('/'), 'v', resolvedVersion], 526 - }, 527 - }" 517 + :to="`/package-code/${pkg.name}/v/${resolvedVersion}`" 528 518 class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-transparent text-fg-subtle hover:text-fg hover:bg-bg hover:shadow hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5" 529 519 aria-keyshortcuts="." 530 520 > ··· 675 665 </li> 676 666 <li v-if="resolvedVersion" class="sm:hidden"> 677 667 <NuxtLink 678 - :to="{ 679 - name: 'code', 680 - params: { 681 - path: [...pkg.name.split('/'), 'v', resolvedVersion], 682 - }, 683 - }" 668 + :to="`/package-code/${pkg.name}/v/${resolvedVersion}`" 684 669 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 685 670 > 686 671 <span class="i-carbon:code w-4 h-4" aria-hidden="true" /> ··· 977 962 <Readme v-if="readmeData?.html" :html="readmeData.html" @click="handleClick" /> 978 963 <p v-else class="text-fg-subtle italic"> 979 964 {{ $t('package.readme.no_readme') }} 980 - <a v-if="repositoryUrl" :href="repositoryUrl" rel="noopener noreferrer" class="link">{{ 981 - $t('package.readme.view_on_github') 982 - }}</a> 965 + <a 966 + v-if="repositoryUrl" 967 + :href="repositoryUrl" 968 + target="_blank" 969 + rel="noopener noreferrer" 970 + class="link" 971 + >{{ $t('package.readme.view_on_github') }}</a 972 + > 983 973 </p> 984 974 </section> 985 975
+6 -5
app/pages/code/[...path].vue app/pages/package-code/[...path].vue
··· 8 8 9 9 definePageMeta({ 10 10 name: 'code', 11 - alias: ['/package/code/:path(.*)*'], 11 + path: '/package-code/:path+', 12 + alias: ['/package/code/:path+', '/code/:path+'], 12 13 }) 13 14 14 15 const route = useRoute('code') ··· 19 20 // /code/nuxt/v/4.2.0/src/index.ts → packageName: "nuxt", version: "4.2.0", filePath: "src/index.ts" 20 21 // /code/@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", version: "1.0.0", filePath: null 21 22 const parsedRoute = computed(() => { 22 - const segments = route.params.path || [] 23 + const segments = route.params.path 23 24 24 25 // Find the /v/ separator for version 25 26 const vIndex = segments.indexOf('v') ··· 50 51 51 52 // URL pattern for version selector - includes file path if present 52 53 const versionUrlPattern = computed(() => { 53 - const base = `/code/${packageName.value}/v/{version}` 54 + const base = `/package-code/${packageName.value}/v/{version}` 54 55 return filePath.value ? `${base}/${filePath.value}` : base 55 56 }) 56 57 ··· 193 194 194 195 // Navigation helper - build URL for a path 195 196 function getCodeUrl(path?: string): string { 196 - const base = `/code/${packageName.value}/v/${version.value}` 197 + const base = `/package-code/${packageName.value}/v/${version.value}` 197 198 return path ? `${base}/${path}` : base 198 199 } 199 200 ··· 248 249 249 250 // Canonical URL for this code page 250 251 const canonicalUrl = computed(() => { 251 - let url = `https://npmx.dev/code/${packageName.value}/v/${version.value}` 252 + let url = `https://npmx.dev/package-code/${packageName.value}/v/${version.value}` 252 253 if (filePath.value) { 253 254 url += `/${filePath.value}` 254 255 }
+8 -6
app/pages/docs/[...path].vue app/pages/package-docs/[...path].vue
··· 5 5 6 6 definePageMeta({ 7 7 name: 'docs', 8 + path: '/package-docs/:path+', 9 + alias: ['/package/docs/:path+', '/docs/:path+'], 8 10 }) 9 11 10 12 const route = useRoute('docs') 11 13 const router = useRouter() 12 14 13 15 const parsedRoute = computed(() => { 14 - const segments = route.params.path?.filter(Boolean) || [] 16 + const segments = route.params.path?.filter(Boolean) 15 17 const vIndex = segments.indexOf('v') 16 18 17 19 if (vIndex === -1 || vIndex >= segments.length - 1) { ··· 45 47 if (version) { 46 48 setResponseHeader(useRequestEvent()!, 'Cache-Control', 'no-cache') 47 49 app.runWithContext(() => 48 - navigateTo('/docs/' + packageName.value + '/v/' + version, { redirectCode: 302 }), 50 + navigateTo('/package-docs/' + packageName.value + '/v/' + version, { redirectCode: 302 }), 49 51 ) 50 52 } 51 53 } ··· 54 56 [requestedVersion, latestVersion, packageName], 55 57 ([version, latest, name]) => { 56 58 if (!version && latest && name) { 57 - router.replace(`/docs/${name}/v/${latest}`) 59 + router.replace(`/package-docs/${name}/v/${latest}`) 58 60 } 59 61 }, 60 62 { immediate: true }, ··· 120 122 <div class="flex items-center gap-3 min-w-0"> 121 123 <NuxtLink 122 124 v-if="packageName" 123 - :to="`/${packageName}`" 125 + :to="{ name: 'package', params: { package: [packageName] } }" 124 126 class="font-mono text-lg sm:text-xl font-semibold text-fg hover:text-fg-muted transition-colors truncate" 125 127 > 126 128 {{ packageName }} ··· 131 133 :current-version="resolvedVersion" 132 134 :versions="pkg.versions" 133 135 :dist-tags="pkg['dist-tags']" 134 - :url-pattern="`/docs/${packageName}/v/{version}`" 136 + :url-pattern="`/package-docs/${packageName}/v/{version}`" 135 137 /> 136 138 <span v-else-if="resolvedVersion" class="text-fg-subtle font-mono text-sm shrink-0"> 137 139 {{ resolvedVersion }} ··· 179 181 <div class="flex gap-4 mt-4"> 180 182 <NuxtLink 181 183 v-if="packageName" 182 - :to="`/${packageName}`" 184 + :to="{ name: 'package', params: { package: [packageName] } }" 183 185 class="link-subtle font-mono text-sm" 184 186 > 185 187 View package
+4 -4
docs/content/2.guide/1.features.md
··· 92 92 93 93 ### Custom badges 94 94 95 - You can add custom npmx badges to your markdown files using the following syntax: `[![Open on npmx.dev](https://npmx.dev/api/registry/badge/YOUR_PACKAGE)](https://npmx.dev/YOUR_PACKAGE)` 95 + You can add custom npmx badges to your markdown files using the following syntax: `[![Open on npmx.dev](https://npmx.dev/api/registry/badge/YOUR_PACKAGE)](https://npmx.dev/package/YOUR_PACKAGE)` 96 96 97 97 Do not forget to replace `YOUR_PACKAGE` with the actual package name. 98 98 ··· 100 100 101 101 ``` 102 102 # Default 103 - [![Open on npmx.dev](https://npmx.dev/api/registry/badge/nuxt)](https://npmx.dev/nuxt) 103 + [![Open on npmx.dev](https://npmx.dev/api/registry/badge/nuxt)](https://npmx.dev/package/nuxt) 104 104 105 105 # Organization packages 106 - [![Open on npmx.dev](https://npmx.dev/api/registry/badge/@nuxt/kit)](https://npmx.dev/@nuxt/kit) 106 + [![Open on npmx.dev](https://npmx.dev/api/registry/badge/@nuxt/kit)](https://npmx.dev/package/@nuxt/kit) 107 107 108 108 # Version-specific badges 109 - [![Open on npmx.dev](https://npmx.dev/api/registry/badge/nuxt/v/3.12.0)](https://npmx.dev/nuxt/v/3.12.0) 109 + [![Open on npmx.dev](https://npmx.dev/api/registry/badge/nuxt/v/3.12.0)](https://npmx.dev/package/nuxt/v/3.12.0) 110 110 ```
+3 -3
nuxt.config.ts
··· 99 99 '/search': { isr: false, cache: false }, 100 100 '/api/auth/**': { isr: false, cache: false }, 101 101 // infinite cache (versioned - doesn't change) 102 - '/code/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 103 - '/docs/:pkg/v/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 104 - '/docs/:scope/:pkg/v/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 102 + '/package-code/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 103 + '/package-docs/:pkg/v/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 104 + '/package-docs/:scope/:pkg/v/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 105 105 '/api/registry/docs/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 106 106 '/api/registry/file/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 107 107 '/api/registry/files/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
+2 -2
server/utils/code-highlight.ts
··· 187 187 const dep = dependencies?.[packageName] 188 188 if (dep) { 189 189 // Link to code browser with resolved version 190 - return `/code/${packageName}/v/${dep.version}` 190 + return `/package-code/${packageName}/v/${dep.version}` 191 191 } 192 192 // Fall back to package page if not a known dependency 193 - return `/${packageName}` 193 + return `/package/${packageName}` 194 194 } 195 195 196 196 // Match: from keyword span followed by string span containing module specifier
+1 -1
server/utils/import-resolver.ts
··· 212 212 return (specifier: string) => { 213 213 const resolved = resolveRelativeImport(specifier, currentFile, files) 214 214 if (resolved) { 215 - return `/code/${packageName}/v/${version}/${resolved.path}` 215 + return `/package-code/${packageName}/v/${version}/${resolved.path}` 216 216 } 217 217 return null 218 218 }
+11 -11
test/e2e/create-command.spec.ts
··· 6 6 7 7 test.describe('Visibility', () => { 8 8 test('/vite - should show create command (same maintainers)', async ({ page, goto }) => { 9 - await goto('/vite', { waitUntil: 'domcontentloaded' }) 9 + await goto('/package/vite', { waitUntil: 'domcontentloaded' }) 10 10 11 11 // Create command section should be visible (SSR) 12 12 // Use specific container to avoid matching README code blocks ··· 15 15 await expect(createCommandSection.locator('code')).toContainText(/create vite/i) 16 16 17 17 // Link to create-vite should be present (uses sr-only text, so check attachment not visibility) 18 - await expect(page.locator('a[href="/create-vite"]').first()).toBeAttached() 18 + await expect(page.locator('a[href="/package/create-vite"]').first()).toBeAttached() 19 19 }) 20 20 21 21 test('/next - should show create command (shared maintainer, same repo)', async ({ 22 22 page, 23 23 goto, 24 24 }) => { 25 - await goto('/next', { waitUntil: 'domcontentloaded' }) 25 + await goto('/package/next', { waitUntil: 'domcontentloaded' }) 26 26 27 27 // Create command section should be visible (SSR) 28 28 // Use specific container to avoid matching README code blocks ··· 31 31 await expect(createCommandSection.locator('code')).toContainText(/create next-app/i) 32 32 33 33 // Link to create-next-app should be present (uses sr-only text, so check attachment not visibility) 34 - await expect(page.locator('a[href="/create-next-app"]').first()).toBeAttached() 34 + await expect(page.locator('a[href="/package/create-next-app"]').first()).toBeAttached() 35 35 }) 36 36 37 37 test('/nuxt - should show create command (same maintainer, same org)', async ({ 38 38 page, 39 39 goto, 40 40 }) => { 41 - await goto('/nuxt', { waitUntil: 'domcontentloaded' }) 41 + await goto('/package/nuxt', { waitUntil: 'domcontentloaded' }) 42 42 43 43 // Create command section should be visible (SSR) 44 44 // nuxt has create-nuxt package, so command is "npm create nuxt" ··· 52 52 page, 53 53 goto, 54 54 }) => { 55 - await goto('/color', { waitUntil: 'domcontentloaded' }) 55 + await goto('/package/color', { waitUntil: 'domcontentloaded' }) 56 56 57 57 // Wait for package to load 58 58 await expect(page.locator('h1').filter({ hasText: 'color' })).toBeVisible() ··· 67 67 page, 68 68 goto, 69 69 }) => { 70 - await goto('/lodash', { waitUntil: 'domcontentloaded' }) 70 + await goto('/package/lodash', { waitUntil: 'domcontentloaded' }) 71 71 72 72 // Wait for package to load 73 73 await expect(page.locator('h1').filter({ hasText: 'lodash' })).toBeVisible() ··· 81 81 82 82 test.describe('Copy Functionality', () => { 83 83 test('hovering create command shows copy button', async ({ page, goto }) => { 84 - await goto('/vite', { waitUntil: 'hydration' }) 84 + await goto('/package/vite', { waitUntil: 'hydration' }) 85 85 86 86 await expect(page.locator('h1')).toContainText('vite', { timeout: 15000 }) 87 87 ··· 112 112 // Grant clipboard permissions 113 113 await context.grantPermissions(['clipboard-read', 'clipboard-write']) 114 114 115 - await goto('/vite', { waitUntil: 'hydration' }) 115 + await goto('/package/vite', { waitUntil: 'hydration' }) 116 116 await expect(page.locator('h1')).toContainText('vite', { timeout: 15000 }) 117 117 118 118 await expect(page.locator('main header').locator('text=/v\\d+\\.\\d+/')).toBeVisible({ ··· 142 142 143 143 test.describe('Install Command Copy', () => { 144 144 test('hovering install command shows copy button', async ({ page, goto }) => { 145 - await goto('/lodash', { waitUntil: 'hydration' }) 145 + await goto('/package/lodash', { waitUntil: 'hydration' }) 146 146 147 147 // Find the install command container 148 148 const installCommandContainer = page.locator('.group\\/installcmd').first() ··· 167 167 // Grant clipboard permissions 168 168 await context.grantPermissions(['clipboard-read', 'clipboard-write']) 169 169 170 - await goto('/lodash', { waitUntil: 'hydration' }) 170 + await goto('/package/lodash', { waitUntil: 'hydration' }) 171 171 172 172 // Find and hover over the install command container 173 173 const installCommandContainer = page.locator('.group\\/installcmd').first()
+13 -13
test/e2e/docs.spec.ts
··· 3 3 test.describe('API Documentation Pages', () => { 4 4 test('docs page loads and shows content for a package', async ({ page, goto }) => { 5 5 // Use a small, stable package with TypeScript types 6 - await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' }) 6 + await goto('/package-docs/ufo/v/1.6.3', { waitUntil: 'networkidle' }) 7 7 8 8 // Page title should include package name 9 9 await expect(page).toHaveTitle(/ufo.*docs/i) ··· 24 24 }) 25 25 26 26 test('docs page shows TOC sidebar on desktop', async ({ page, goto }) => { 27 - await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' }) 27 + await goto('/package-docs/ufo/v/1.6.3', { waitUntil: 'networkidle' }) 28 28 29 29 // TOC sidebar should be visible (on desktop viewport) 30 30 const tocSidebar = page.locator('aside') ··· 38 38 }) 39 39 40 40 test('TOC links navigate to sections', async ({ page, goto }) => { 41 - await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' }) 41 + await goto('/package-docs/ufo/v/1.6.3', { waitUntil: 'networkidle' }) 42 42 43 43 // Click on Functions in TOC 44 44 const functionsLink = page.locator('aside a[href="#section-function"]') ··· 53 53 }) 54 54 55 55 test('clicking symbol name scrolls to symbol', async ({ page, goto }) => { 56 - await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' }) 56 + await goto('/package-docs/ufo/v/1.6.3', { waitUntil: 'networkidle' }) 57 57 58 58 // Find a symbol link in the TOC 59 59 const symbolLink = page.locator('aside a[href^="#function-"]').first() ··· 67 67 }) 68 68 69 69 test('docs page without version redirects to latest', async ({ page, goto }) => { 70 - await goto('/docs/ufo', { waitUntil: 'networkidle' }) 70 + await goto('/package-docs/ufo', { waitUntil: 'networkidle' }) 71 71 72 72 // Should redirect to include version 73 - await expect(page).toHaveURL(/\/docs\/ufo\/v\//) 73 + await expect(page).toHaveURL(/\/package-docs\/ufo\/v\//) 74 74 }) 75 75 76 76 test('package link in header navigates to package page', async ({ page, goto }) => { 77 - await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' }) 77 + await goto('/package-docs/ufo/v/1.6.3', { waitUntil: 'networkidle' }) 78 78 79 79 // Click on package name in header 80 80 const packageLink = page.locator('header a').filter({ hasText: 'ufo' }) 81 81 await packageLink.click() 82 82 83 83 // Should navigate to package page (URL ends with /ufo) 84 - await expect(page).toHaveURL(/\/ufo$/) 84 + await expect(page).toHaveURL(/\/package\/ufo$/) 85 85 }) 86 86 87 87 test('docs page handles package gracefully when types unavailable', async ({ page, goto }) => { 88 88 // Use a simple JS package - the page should load without crashing 89 89 // regardless of whether it has types or shows an error state 90 - await goto('/docs/is-odd/v/3.0.1', { waitUntil: 'networkidle' }) 90 + await goto('/package-docs/is-odd/v/3.0.1', { waitUntil: 'networkidle' }) 91 91 92 92 // Header should always show the package name 93 93 await expect(page.locator('header').getByText('is-odd')).toBeVisible() ··· 105 105 106 106 test.describe('Version Selector', () => { 107 107 test('version selector dropdown shows versions', async ({ page, goto }) => { 108 - await goto('/docs/ufo/v/1.6.3', { waitUntil: 'hydration' }) 108 + await goto('/package-docs/ufo/v/1.6.3', { waitUntil: 'hydration' }) 109 109 110 110 // Find and click the version selector button (wait for it to be visible) 111 111 const versionButton = page.locator('header button').filter({ hasText: '1.6.3' }) ··· 123 123 }) 124 124 125 125 test('selecting a version navigates to that version', async ({ page, goto }) => { 126 - await goto('/docs/ufo/v/1.6.3', { waitUntil: 'hydration' }) 126 + await goto('/package-docs/ufo/v/1.6.3', { waitUntil: 'hydration' }) 127 127 128 128 // Find and click the version selector button (wait for it to be visible) 129 129 const versionButton = page.locator('header button').filter({ hasText: '1.6.3' }) ··· 132 132 await versionButton.click() 133 133 134 134 // Find a version link that's not the current version by checking the href 135 - const versionLinks = page.locator('[role="option"] a[href*="/docs/ufo/v/"]') 135 + const versionLinks = page.locator('[role="option"] a[href*="/package-docs/ufo/v/"]') 136 136 const count = await versionLinks.count() 137 137 138 138 // Find first link that doesn't point to 1.6.3 ··· 157 157 }) 158 158 159 159 test('escape key closes version dropdown', async ({ page, goto }) => { 160 - await goto('/docs/ufo/v/1.6.3', { waitUntil: 'hydration' }) 160 + await goto('/package-docs/ufo/v/1.6.3', { waitUntil: 'hydration' }) 161 161 162 162 // Wait for version button to be visible 163 163 const versionButton = page.locator('header button').filter({ hasText: '1.6.3' })
+4 -4
test/e2e/interactions.spec.ts
··· 21 21 await page.keyboard.press('ArrowUp') 22 22 23 23 // Enter navigates to the selected result 24 - // URL is /vue not /package/vue (cleaner URLs) 24 + // URL is /package/vue not /vue 25 25 await page.keyboard.press('Enter') 26 - await expect(page).toHaveURL(/\/vue/) 26 + await expect(page).toHaveURL(/\/package\/vue/) 27 27 }) 28 28 29 29 test('/search?q=vue → "/" focuses the search input from results', async ({ page, goto }) => { ··· 109 109 page, 110 110 goto, 111 111 }) => { 112 - await goto('/vue', { waitUntil: 'hydration' }) 112 + await goto('/package/vue', { waitUntil: 'hydration' }) 113 113 114 114 await page.keyboard.press('c') 115 115 ··· 136 136 page, 137 137 goto, 138 138 }) => { 139 - await goto('/vue', { waitUntil: 'hydration' }) 139 + await goto('/package/vue', { waitUntil: 'hydration' }) 140 140 141 141 await page.keyboard.press('Shift+c') 142 142 await expect(page).toHaveURL(/\/vue/)
+1 -1
test/e2e/package-manager-select.spec.ts
··· 2 2 3 3 test.describe('Package Page', () => { 4 4 test('/vue → package manager select dropdown works', async ({ page, goto }) => { 5 - await goto('/vue', { waitUntil: 'hydration' }) 5 + await goto('/package/vue', { waitUntil: 'hydration' }) 6 6 7 7 await expect(page.locator('h1')).toContainText('vue', { timeout: 15000 }) 8 8
+7 -7
test/nuxt/a11y.spec.ts
··· 631 631 props: { 632 632 tree: mockTree, 633 633 currentPath: '', 634 - baseUrl: '/code/vue', 634 + baseUrl: '/package-code/vue', 635 635 }, 636 636 }) 637 637 const results = await runAxe(component) ··· 643 643 props: { 644 644 tree: mockTree, 645 645 currentPath: 'src', 646 - baseUrl: '/code/vue', 646 + baseUrl: '/package-code/vue', 647 647 }, 648 648 }) 649 649 const results = await runAxe(component) ··· 667 667 props: { 668 668 tree: mockTree, 669 669 currentPath: '', 670 - baseUrl: '/code/vue', 670 + baseUrl: '/package-code/vue', 671 671 }, 672 672 }) 673 673 const results = await runAxe(component) ··· 679 679 props: { 680 680 tree: mockTree, 681 681 currentPath: 'src/index.ts', 682 - baseUrl: '/code/vue', 682 + baseUrl: '/package-code/vue', 683 683 }, 684 684 }) 685 685 const results = await runAxe(component) ··· 894 894 props: { 895 895 tree: mockTree, 896 896 currentPath: '', 897 - baseUrl: '/code/vue', 897 + baseUrl: '/package-code/vue', 898 898 }, 899 899 }) 900 900 const results = await runAxe(component) ··· 1779 1779 currentVersion: '3.5.0', 1780 1780 versions: mockVersions, 1781 1781 distTags: mockDistTags, 1782 - urlPattern: '/vue/v/{version}', 1782 + urlPattern: '/package/vue/v/{version}', 1783 1783 }, 1784 1784 }) 1785 1785 const results = await runAxe(component) ··· 1793 1793 currentVersion: '3.4.0', 1794 1794 versions: mockVersions, 1795 1795 distTags: mockDistTags, 1796 - urlPattern: '/vue/v/{version}', 1796 + urlPattern: '/package/vue/v/{version}', 1797 1797 }, 1798 1798 }) 1799 1799 const results = await runAxe(component)
+36 -34
test/nuxt/components/VersionSelector.spec.ts
··· 26 26 currentVersion: '1.0.0', 27 27 versions: { '1.0.0': {} }, 28 28 distTags: { latest: '1.0.0' }, 29 - urlPattern: '/docs/test-package/v/{version}', 29 + urlPattern: '/package-docs/test-package/v/{version}', 30 30 }, 31 31 }) 32 32 ··· 42 42 currentVersion: '2.0.0', 43 43 versions: { '2.0.0': {} }, 44 44 distTags: { latest: '2.0.0' }, 45 - urlPattern: '/docs/test-package/v/{version}', 45 + urlPattern: '/package-docs/test-package/v/{version}', 46 46 }, 47 47 }) 48 48 ··· 56 56 currentVersion: '1.0.0', 57 57 versions: { '1.0.0': {}, '2.0.0': {} }, 58 58 distTags: { latest: '2.0.0', old: '1.0.0' }, 59 - urlPattern: '/docs/test-package/v/{version}', 59 + urlPattern: '/package-docs/test-package/v/{version}', 60 60 }, 61 61 }) 62 62 ··· 72 72 currentVersion: '1.0.0', 73 73 versions: { '1.0.0': {} }, 74 74 distTags: { latest: '1.0.0' }, 75 - urlPattern: '/docs/test-package/v/{version}', 75 + urlPattern: '/package-docs/test-package/v/{version}', 76 76 }, 77 77 }) 78 78 ··· 89 89 currentVersion: '1.0.0', 90 90 versions: { '1.0.0': {} }, 91 91 distTags: { latest: '1.0.0' }, 92 - urlPattern: '/docs/test-package/v/{version}', 92 + urlPattern: '/package-docs/test-package/v/{version}', 93 93 }, 94 94 }) 95 95 ··· 107 107 currentVersion: '1.0.0', 108 108 versions: { '1.0.0': {} }, 109 109 distTags: { latest: '1.0.0' }, 110 - urlPattern: '/docs/test-package/v/{version}', 110 + urlPattern: '/package-docs/test-package/v/{version}', 111 111 }, 112 112 }) 113 113 ··· 129 129 latest: '2.0.0', 130 130 old: '1.0.0', 131 131 }, 132 - urlPattern: '/docs/test-package/v/{version}', 132 + urlPattern: '/package-docs/test-package/v/{version}', 133 133 }, 134 134 }) 135 135 ··· 148 148 currentVersion: '1.0.0', 149 149 versions: { '1.0.0': {}, '2.0.0': {}, '3.0.0': {} }, 150 150 distTags: { latest: '3.0.0' }, 151 - urlPattern: '/docs/test-package/v/{version}', 151 + urlPattern: '/package-docs/test-package/v/{version}', 152 152 }, 153 153 }) 154 154 ··· 167 167 currentVersion: '1.0.0', 168 168 versions: { '1.0.0': {} }, 169 169 distTags: { latest: '1.0.0' }, 170 - urlPattern: '/docs/test-package/v/{version}', 170 + urlPattern: '/package-docs/test-package/v/{version}', 171 171 }, 172 172 }) 173 173 ··· 184 184 currentVersion: '1.0.0', 185 185 versions: { '1.0.0': {} }, 186 186 distTags: { latest: '1.0.0' }, 187 - urlPattern: '/docs/test-package/v/{version}', 187 + urlPattern: '/package-docs/test-package/v/{version}', 188 188 }, 189 189 }) 190 190 ··· 206 206 latest: '2.0.0', 207 207 old: '1.0.0', 208 208 }, 209 - urlPattern: '/docs/test-package/v/{version}', 209 + urlPattern: '/package-docs/test-package/v/{version}', 210 210 }, 211 211 }) 212 212 ··· 232 232 currentVersion: '1.0.0', 233 233 versions: { '1.0.0': {} }, 234 234 distTags: { latest: '1.0.0' }, 235 - urlPattern: '/docs/test-package/v/{version}', 235 + urlPattern: '/package-docs/test-package/v/{version}', 236 236 }, 237 237 }) 238 238 ··· 256 256 beta: '2.0.0', 257 257 old: '1.0.0', 258 258 }, 259 - urlPattern: '/docs/test-package/v/{version}', 259 + urlPattern: '/package-docs/test-package/v/{version}', 260 260 }, 261 261 }) 262 262 ··· 287 287 latest: '2.0.0', 288 288 old: '1.0.0', 289 289 }, 290 - urlPattern: '/docs/test-package/v/{version}', 290 + urlPattern: '/package-docs/test-package/v/{version}', 291 291 }, 292 292 }) 293 293 ··· 313 313 latest: '2.0.0', 314 314 old: '1.0.0', 315 315 }, 316 - urlPattern: '/code/test-package/v/{version}/src/index.ts', 316 + urlPattern: '/package-code/test-package/v/{version}/src/index.ts', 317 317 }, 318 318 }) 319 319 ··· 321 321 await button.trigger('click') 322 322 323 323 const versionLink = component.findAll('a').find(a => a.text().includes('1.0.0')) 324 - expect(versionLink?.attributes('href')).toBe('/code/test-package/v/1.0.0/src/index.ts') 324 + expect(versionLink?.attributes('href')).toBe( 325 + '/package-code/test-package/v/1.0.0/src/index.ts', 326 + ) 325 327 }) 326 328 }) 327 329 ··· 333 335 currentVersion: '1.0.0', 334 336 versions: { '1.0.0': {} }, 335 337 distTags: { latest: '1.0.0' }, 336 - urlPattern: '/docs/test-package/v/{version}', 338 + urlPattern: '/package-docs/test-package/v/{version}', 337 339 }, 338 340 }) 339 341 ··· 357 359 currentVersion: '1.0.0', 358 360 versions: { '1.0.0': {} }, 359 361 distTags: { latest: '1.0.0' }, 360 - urlPattern: '/docs/test-package/v/{version}', 362 + urlPattern: '/package-docs/test-package/v/{version}', 361 363 }, 362 364 }) 363 365 ··· 386 388 currentVersion: '1.2.0', 387 389 versions: { '1.2.0': {} }, 388 390 distTags: { latest: '1.2.0' }, 389 - urlPattern: '/docs/test-package/v/{version}', 391 + urlPattern: '/package-docs/test-package/v/{version}', 390 392 }, 391 393 }) 392 394 ··· 439 441 currentVersion: '0.10.1', 440 442 versions: { '0.10.1': {} }, 441 443 distTags: { latest: '0.10.1' }, 442 - urlPattern: '/docs/test-package/v/{version}', 444 + urlPattern: '/package-docs/test-package/v/{version}', 443 445 }, 444 446 }) 445 447 ··· 480 482 latest: '1.0.0', 481 483 stable: '1.0.0', 482 484 }, 483 - urlPattern: '/docs/test-package/v/{version}', 485 + urlPattern: '/package-docs/test-package/v/{version}', 484 486 }, 485 487 }) 486 488 ··· 499 501 currentVersion: '1.0.0', 500 502 versions: { '1.0.0': {} }, 501 503 distTags: { latest: '1.0.0' }, 502 - urlPattern: '/docs/test-package/v/{version}', 504 + urlPattern: '/package-docs/test-package/v/{version}', 503 505 }, 504 506 }) 505 507 ··· 529 531 currentVersion: '1.0.0', 530 532 versions: { '1.0.0': {} }, 531 533 distTags: { latest: '1.0.0' }, 532 - urlPattern: '/docs/test-package/v/{version}', 534 + urlPattern: '/package-docs/test-package/v/{version}', 533 535 }, 534 536 }) 535 537 ··· 559 561 currentVersion: '1.0.0', 560 562 versions: { '1.0.0': {} }, 561 563 distTags: { latest: '1.0.0' }, 562 - urlPattern: '/docs/test-package/v/{version}', 564 + urlPattern: '/package-docs/test-package/v/{version}', 563 565 }, 564 566 }) 565 567 ··· 574 576 currentVersion: '1.0.0', 575 577 versions: { '1.0.0': {} }, 576 578 distTags: { latest: '1.0.0' }, 577 - urlPattern: '/docs/test-package/v/{version}', 579 + urlPattern: '/package-docs/test-package/v/{version}', 578 580 }, 579 581 }) 580 582 ··· 591 593 currentVersion: '1.0.0', 592 594 versions: { '1.0.0': {} }, 593 595 distTags: { latest: '1.0.0' }, 594 - urlPattern: '/docs/test-package/v/{version}', 596 + urlPattern: '/package-docs/test-package/v/{version}', 595 597 }, 596 598 }) 597 599 ··· 608 610 currentVersion: '1.0.0', 609 611 versions: { '1.0.0': {} }, 610 612 distTags: { latest: '1.0.0' }, 611 - urlPattern: '/docs/test-package/v/{version}', 613 + urlPattern: '/package-docs/test-package/v/{version}', 612 614 }, 613 615 }) 614 616 ··· 629 631 latest: '2.0.0', 630 632 old: '1.0.0', 631 633 }, 632 - urlPattern: '/docs/test-package/v/{version}', 634 + urlPattern: '/package-docs/test-package/v/{version}', 633 635 }, 634 636 }) 635 637 ··· 647 649 currentVersion: '1.0.0', 648 650 versions: { '1.0.0': {} }, 649 651 distTags: { latest: '1.0.0' }, 650 - urlPattern: '/docs/test-package/v/{version}', 652 + urlPattern: '/package-docs/test-package/v/{version}', 651 653 }, 652 654 }) 653 655 ··· 666 668 currentVersion: '1.0.0', 667 669 versions: { '1.0.0': {} }, 668 670 distTags: { latest: '1.0.0' }, 669 - urlPattern: '/docs/test-package/v/{version}', 671 + urlPattern: '/package-docs/test-package/v/{version}', 670 672 }, 671 673 }) 672 674 ··· 685 687 currentVersion: '1.0.0', 686 688 versions: { '1.0.0': {} }, 687 689 distTags: { latest: '1.0.0' }, 688 - urlPattern: '/docs/test-package/v/{version}', 690 + urlPattern: '/package-docs/test-package/v/{version}', 689 691 }, 690 692 }) 691 693 ··· 707 709 currentVersion: '1.0.0', 708 710 versions: { '1.0.0': {} }, 709 711 distTags: { latest: '1.0.0' }, 710 - urlPattern: '/docs/test-package/v/{version}', 712 + urlPattern: '/package-docs/test-package/v/{version}', 711 713 }, 712 714 }) 713 715 ··· 743 745 latest: '2.0.0', 744 746 old: '1.0.0', 745 747 }, 746 - urlPattern: '/docs/test-package/v/{version}', 748 + urlPattern: '/package-docs/test-package/v/{version}', 747 749 }, 748 750 }) 749 751 ··· 783 785 currentVersion: '1.0.0', 784 786 versions: { '1.0.0': {} }, 785 787 distTags: { latest: '1.0.0' }, 786 - urlPattern: '/docs/test-package/v/{version}', 788 + urlPattern: '/package-docs/test-package/v/{version}', 787 789 }, 788 790 attachTo: document.body, 789 791 })
+1 -1
test/nuxt/components/compare/PackageSelector.spec.ts
··· 41 41 }, 42 42 }) 43 43 44 - const link = component.find('a[href="/lodash"]') 44 + const link = component.find('a[href="/package/lodash"]') 45 45 expect(link.exists()).toBe(true) 46 46 }) 47 47
+2 -2
test/unit/server/utils/import-resolver.spec.ts
··· 157 157 158 158 const url = resolver('./utils') 159 159 160 - expect(url).toBe('/code/pkg-name/v/1.2.3/dist/utils.js') 160 + expect(url).toBe('/package-code/pkg-name/v/1.2.3/dist/utils.js') 161 161 }) 162 162 163 163 it('returns null when the import cannot be resolved', () => { ··· 175 175 176 176 const url = resolver('./utils') 177 177 178 - expect(url).toBe('/code/@scope/pkg/v/1.2.3/dist/utils.js') 178 + expect(url).toBe('/package-code/@scope/pkg/v/1.2.3/dist/utils.js') 179 179 }) 180 180 })