Repository: vercel/next.js
Pull request: perf: revert to use the light safe stringify for is-error

`is-error` is a helper that being used accorss client and server now, in #84909 we switched the underlying safe stringify error implementation to the precompiled dependency `safe-stable-stringify`. But it increased the client bundle size a bit.

The change in #84909 was only for applying the `safe-stable-stringify` for client file logger with mcp, not  really necessary need to change for is-error helper

Note: Save minified around 12kb. it's actually 7.5kb on bundlephobia, but because we're using ncc to compile it to cjs dependency that the size seems slightly larger than their metrics. The saved gzip size would be around 2-3.x kb

Changed files:
filename: packages/next/src/lib/is-error.ts
patch: @@ -1,5 +1,4 @@
 import { isPlainObject } from '../shared/lib/is-plain-object'
-import safeStringify from 'next/dist/compiled/safe-stable-stringify'
 
 // We allow some additional attached properties for Next.js errors
 export interface NextError extends Error {
@@ -10,6 +9,28 @@ export interface NextError extends Error {
   digest?: number
 }
 
+/**
+ * This is a safe stringify function that handles circular references.
+ * We're using a simpler version here to avoid introducing
+ * the dependency `safe-stable-stringify` into production bundle.
+ *
+ * This helper is used both in development and production.
+ */
+function safeStringifyLite(obj: any) {
+  const seen = new WeakSet()
+
+  return JSON.stringify(obj, (_key, value) => {
+    // If value is an object and already seen, replace with "[Circular]"
+    if (typeof value === 'object' && value !== null) {
+      if (seen.has(value)) {
+        return '[Circular]'
+      }
+      seen.add(value)
+    }
+    return value
+  })
+}
+
 /**
  * Checks whether the given value is a NextError.
  * This can be used to print a more detailed error message with properties like `code` & `digest`.
@@ -43,5 +64,5 @@ export function getProperError(err: unknown): Error {
     }
   }
 
-  return new Error(isPlainObject(err) ? safeStringify(err) : err + '')
+  return new Error(isPlainObject(err) ? safeStringifyLite(err) : err + '')
 }

filename: test/development/app-dir/serialize-circular-error/serialize-circular-error.test.ts
patch: @@ -19,7 +19,7 @@ describe('serialize-circular-error', () => {
     `)
     const output = next.cliOutput
     expect(output).toContain(
-      'Error: {"objA":{"other":{"a":"[Circular]"}},"objB":{"a":{"other":"[Circular]"}}}'
+      'Error: {"objA":{"other":{"a":"[Circular]"}},"objB":"[Circular]"}'
     )
   })
 
@@ -44,7 +44,7 @@ describe('serialize-circular-error', () => {
 
     const output = next.cliOutput
     expect(output).toContain(
-      'Error: {"objC":{"other":{"a":"[Circular]"}},"objD":{"a":{"other":"[Circular]"}}}'
+      'Error: {"objC":{"other":{"a":"[Circular]"}},"objD":"[Circular]"}'
     )
   })
 })

filename: test/production/app-dir/browser-chunks/browser-chunks.test.ts
patch: @@ -48,4 +48,18 @@ describe('browser-chunks', () => {
       )
     }
   })
+
+  it('must not include heavy dependencies into browser chunks', () => {
+    const heavyDependencies = sources.filter((source) => {
+      return source.includes('next/dist/compiled/safe-stable-stringify')
+    })
+
+    if (heavyDependencies.length > 0) {
+      const message = `Found the following heavy dependencies:\n  ${heavyDependencies.join('\n  ')}`
+
+      throw new Error(
+        'Did not expect any heavy dependencies in browser chunks.\n' + message
+      )
+    }
+  })
 })