Repository: vercel/next.js
Pull request: [devtool] highlight all link in error message

Improve the link highlight in error message, let normal links can also be parsed as link, but the regular link color still remains the same as message color. Only the nextjs docs link color remains the same as blue

| After | Before |
|:--|:--|
| <video src="https://github.com/user-attachments/assets/0cb6c3e9-0f62-4996-86b6-df4a73878753"> | <video src="https://github.com/user-attachments/assets/af70f83f-91ef-4f54-9901-6d645f852958"> |

[slack-ref](https://vercel.slack.com/archives/C03KAR5DCKC/p1762717389583779)

Changed files:
filename: packages/next/src/next-devtools/dev-overlay/components/hot-linked-text/index.tsx
patch: @@ -5,7 +5,7 @@ const linkRegex = /https?:\/\/[^\s/$.?#].[^\s)'"]*/i
 
 export const HotlinkedText: React.FC<{
   text: string
-  matcher?: (text: string) => boolean
+  matcher?: (text: string) => string | null
 }> = function HotlinkedText(props) {
   const { text, matcher } = props
 
@@ -24,17 +24,27 @@ export const HotlinkedText: React.FC<{
                 if (linkRegex.test(rawPart)) {
                   const link = linkRegex.exec(rawPart)!
                   const href = link[0]
-                  // If link matcher is present but the link doesn't match, don't turn it into a link
-                  if (typeof matcher === 'function' && !matcher(href)) {
-                    return (
-                      <React.Fragment key={`link-${outerIndex}-${index}`}>
-                        {rawPart}
-                      </React.Fragment>
-                    )
+                  // If link matcher is present, check if it returns a className
+                  let linkClassName: string | null = null
+                  if (typeof matcher === 'function') {
+                    linkClassName = matcher(href)
+                    // If matcher returns null, don't turn it into a link
+                    if (linkClassName === null) {
+                      return (
+                        <React.Fragment key={`link-${outerIndex}-${index}`}>
+                          {rawPart}
+                        </React.Fragment>
+                      )
+                    }
                   }
                   return (
                     <React.Fragment key={`link-${outerIndex}-${index}`}>
-                      <a href={href} target="_blank" rel="noreferrer noopener">
+                      <a
+                        href={href}
+                        target="_blank"
+                        rel="noreferrer noopener"
+                        className={linkClassName || undefined}
+                      >
                         {rawPart}
                       </a>
                     </React.Fragment>

filename: packages/next/src/next-devtools/dev-overlay/container/errors.tsx
patch: @@ -29,12 +29,18 @@ interface ErrorsProps extends ErrorBaseProps {
   onClose: () => void
 }
 
-function isNextjsLink(text: string): boolean {
-  return text.startsWith('https://nextjs.org')
+function matchLinkType(text: string): string | null {
+  if (text.startsWith('https://nextjs.org')) {
+    return 'nextjs-link'
+  }
+  if (text.startsWith('https://') || text.startsWith('http://')) {
+    return 'external-link'
+  }
+  return null
 }
 
 function HydrationErrorDescription({ message }: { message: string }) {
-  return <HotlinkedText text={message} matcher={isNextjsLink} />
+  return <HotlinkedText text={message} matcher={matchLinkType} />
 }
 
 function GenericErrorDescription({ error }: { error: Error }) {
@@ -51,7 +57,7 @@ function GenericErrorDescription({ error }: { error: Error }) {
 
   return (
     <>
-      <HotlinkedText text={message} matcher={isNextjsLink} />
+      <HotlinkedText text={message} matcher={matchLinkType} />
     </>
   )
 }
@@ -412,4 +418,7 @@ export const styles = `
     border-radius: var(--rounded-md-2);
     border: 1px solid var(--color-gray-alpha-400);
   }
+  .external-link, .external-link:hover {
+    color:inherit;
+  }
 `

filename: test/development/acceptance-app/ReactRefreshLogBox.test.ts
patch: @@ -889,7 +889,6 @@ describe('ReactRefreshLogBox app', () => {
        }
       `)
     }
-    // Do not highlight example.com but do highlight nextjs.org
     expect(
       await session.evaluate(
         () =>
@@ -898,7 +897,7 @@ describe('ReactRefreshLogBox app', () => {
             .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
             .length
       )
-    ).toBe(1)
+    ).toBe(2)
     expect(
       await session.evaluate(
         () =>
@@ -922,7 +921,7 @@ describe('ReactRefreshLogBox app', () => {
               ) as any
           ).href
       )
-    ).toBe(null)
+    ).toBe('http://example.com/')
   })
 
   // TODO-APP: Catch errors that happen before useEffect