[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: replace links in markdown instead of via javascript (#1339)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Alex Savelyev <91429106+alexdln@users.noreply.github.com>
Co-authored-by: Daniel Roe <daniel@roe.dev>

+71 -19
+17 -19
app/components/Readme.vue
··· 3 3 html: string 4 4 }>() 5 5 6 - const router = useRouter() 7 6 const { copy } = useClipboard() 8 7 9 8 // Combined click handler for: ··· 12 11 function handleClick(event: MouseEvent) { 13 12 const target = event.target as HTMLElement | undefined 14 13 if (!target) return 14 + 15 + if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button) { 16 + return 17 + } 15 18 16 19 // Handle copy button clicks 17 20 const copyTarget = target.closest('[data-copy]') ··· 48 51 if (!href) return 49 52 50 53 // Handle relative anchor links 51 - if (href.startsWith('#')) { 54 + if (href.startsWith('#') || href.startsWith('/')) { 52 55 event.preventDefault() 53 - router.push(href) 56 + navigateTo(href) 54 57 return 55 - } 56 - 57 - const match = href.match(/^(?:https?:\/\/)?(?:www\.)?npmjs\.(?:com|org)(\/.+)$/) 58 - if (!match || !match[1]) return 59 - 60 - const route = router.resolve(match[1]) 61 - if (route) { 62 - event.preventDefault() 63 - router.push(route) 64 58 } 65 59 } 66 60 </script> ··· 141 135 } 142 136 143 137 .readme :deep(a) { 144 - color: var(--fg); 145 - text-decoration: underline; 146 - text-underline-offset: 4px; 147 - text-decoration-color: var(--fg-subtle); 148 - transition: text-decoration-color 0.2s ease; 138 + @apply underline-offset-[0.2rem] underline decoration-1 decoration-fg/30 font-mono text-fg transition-colors duration-200; 149 139 } 150 - 151 140 .readme :deep(a:hover) { 152 - text-decoration-color: var(--accent); 141 + @apply decoration-accent text-accent; 142 + } 143 + .readme :deep(a:focus-visible) { 144 + @apply decoration-accent text-accent; 145 + } 146 + 147 + .readme :deep(a[target='_blank']::after) { 148 + /* I don't know what kind of sorcery this is, but it ensures this icon can't wrap to a new line on its own. */ 149 + content: '__'; 150 + @apply inline i-carbon:launch rtl-flip ms-1 opacity-50; 153 151 } 154 152 155 153 .readme :deep(code) {
+31
server/utils/readme.ts
··· 183 183 .replace(/^-|-$/g, '') // Trim leading/trailing hyphens 184 184 } 185 185 186 + /** These path on npmjs.com don't belong to packages or search, so we shouldn't try to replace them with npmx.dev urls */ 187 + const reservedPathsNpmJs = [ 188 + 'products', 189 + 'login', 190 + 'signup', 191 + 'advisories', 192 + 'blog', 193 + 'about', 194 + 'press', 195 + 'policies', 196 + ] 197 + 198 + const isNpmJsUrlThatCanBeRedirected = (url: URL) => { 199 + if (url.host !== 'www.npmjs.com' && url.host !== 'npmjs.com') { 200 + return false 201 + } 202 + 203 + if ( 204 + url.pathname === '/' || 205 + reservedPathsNpmJs.some(path => url.pathname.startsWith(`/${path}`)) 206 + ) { 207 + return false 208 + } 209 + 210 + return true 211 + } 212 + 186 213 /** 187 214 * Resolve a relative URL to an absolute URL. 188 215 * If repository info is available, resolve to provider's raw file URLs. ··· 199 226 try { 200 227 const parsed = new URL(url, 'https://example.com') 201 228 if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { 229 + // Redirect npmjs urls to ourself 230 + if (isNpmJsUrlThatCanBeRedirected(parsed)) { 231 + return parsed.pathname + parsed.search + parsed.hash 232 + } 202 233 return url 203 234 } 204 235 } catch {
+23
test/unit/server/utils/readme.spec.ts
··· 306 306 ) 307 307 }) 308 308 }) 309 + 310 + describe('npm.js urls', () => { 311 + it('redirects npmjs.com urls to local', async () => { 312 + const markdown = `[Some npmjs.com link](https://www.npmjs.com/package/test-pkg)` 313 + const result = await renderReadmeHtml(markdown, 'test-pkg') 314 + 315 + expect(result.html).toContain('href="/package/test-pkg"') 316 + }) 317 + 318 + it('redirects npmjs.com urls to local (no www and http)', async () => { 319 + const markdown = `[Some npmjs.com link](http://npmjs.com/package/test-pkg)` 320 + const result = await renderReadmeHtml(markdown, 'test-pkg') 321 + 322 + expect(result.html).toContain('href="/package/test-pkg"') 323 + }) 324 + 325 + it('does not redirect npmjs.com to local if they are in the list of exceptions', async () => { 326 + const markdown = `[Root Contributing](https://www.npmjs.com/products)` 327 + const result = await renderReadmeHtml(markdown, 'test-pkg') 328 + 329 + expect(result.html).toContain('href="https://www.npmjs.com/products"') 330 + }) 331 + }) 309 332 }) 310 333 311 334 describe('Markdown Content Extraction', () => {