diff --git a/src/utils/route-encoding.ts b/src/utils/route-encoding.ts index 49365b8c1..2f23f64ff 100644 --- a/src/utils/route-encoding.ts +++ b/src/utils/route-encoding.ts @@ -11,7 +11,7 @@ export function decodePackageNameSlug(slug: string) { try { const decoded = decodeURIComponent(next) if (decoded === next) { - return decoded + break } next = decoded } catch { @@ -19,6 +19,14 @@ export function decodePackageNameSlug(slug: string) { } } + // Back-compat: legacy links encoded the scope separator as `__` + // (`@scope/name` -> `@scope__name`). Those URLs are still indexed, so + // restore the separator instead of failing package-name validation. + // Matches the stats route (`src/routes/stats/npm/-utils.ts`). + if (next.startsWith('@') && next.includes('__') && !next.includes('/')) { + return next.replace('__', '/') + } + return next } diff --git a/tests/route-encoding.test.ts b/tests/route-encoding.test.ts index 4ef53eea3..ac42d0f15 100644 --- a/tests/route-encoding.test.ts +++ b/tests/route-encoding.test.ts @@ -28,6 +28,34 @@ assert.equal( 'double-encoded package route slugs decode for router/browser compatibility', ) +// Legacy `__` scope-separator slugs (still indexed by search engines) must +// resolve to the real scoped name instead of failing package-name validation. +assert.equal( + decodePackageNameSlug('@firfi__quint-connect'), + '@firfi/quint-connect', + 'legacy __ scoped slug decodes to the scoped package name', +) + +assert.equal( + decodePackageNameSlug('%40apollo__client'), + '@apollo/client', + 'legacy __ scoped slug decodes from the percent-encoded form', +) + +// Only the scope separator is restored; a `__` inside the package name stays. +assert.equal( + decodePackageNameSlug('@depup__trpc__server'), + '@depup/trpc__server', + 'only the leading scope-separator __ is restored', +) + +// Unscoped names that contain `__` are not legacy scoped slugs. +assert.equal( + decodePackageNameSlug('tanstack__intent'), + 'tanstack__intent', + 'unscoped __ names are left intact', +) + assert.equal( getRowFieldId('moderation-note', '@scope/pkg', 'note'), 'moderation-note-scope-pkg-note',