diff --git a/packages/vike/utils/getBetterError.spec.ts b/packages/vike/utils/getBetterError.spec.ts index 6cb2f751aa2..f5dbbacfc09 100644 --- a/packages/vike/utils/getBetterError.spec.ts +++ b/packages/vike/utils/getBetterError.spec.ts @@ -272,11 +272,15 @@ describe('getBetterError', () => { }) describe('getOriginalError', () => { - it('returns the original error when no chain exists', () => { + it('returns a clone of the original error when no chain exists', () => { const error = new Error('Test') const result = getBetterError(error, {}) - expect(result.getOriginalError()).toBe(error) + const originalError = result.getOriginalError() + // getOriginalError returns a clone, not the same reference + expect(originalError).not.toBe(error) + // But it has the same message + expect(originalError.message).toBe(error.message) }) it('preserves getOriginalError chain', () => { @@ -284,8 +288,9 @@ describe('getBetterError', () => { const secondError = getBetterError(firstError, { message: 'Second error' }) const thirdError = getBetterError(secondError, { message: 'Third error' }) - expect(thirdError.getOriginalError()).toBe(firstError) - expect(secondError.getOriginalError()).toBe(firstError) + // Both should return clones with the original message + expect(thirdError.getOriginalError().message).toBe('First error') + expect(secondError.getOriginalError().message).toBe('First error') }) it('chains through multiple transformations', () => { @@ -294,23 +299,24 @@ describe('getBetterError', () => { const withAppend = getBetterError(withPrepend, { message: { append: ' [2]' } }) const withReplace = getBetterError(withAppend, { message: 'Replaced' }) - expect(withReplace.getOriginalError()).toBe(original) + expect(withReplace.getOriginalError().message).toBe('Original') }) }) - describe('structuredClone behavior', () => { - it('only preserves message and stack from Error objects', () => { + describe('error mutation behavior', () => { + it('preserves custom properties on Error objects', () => { const customError = new Error('Custom error') customError.code = 'E_CUSTOM' customError.statusCode = 500 const result = getBetterError(customError, {}) - // structuredClone only preserves message and stack from Error objects + // Custom properties are preserved because we mutate the error instead of cloning + // (to avoid breaking Vite's ssrFixStacktrace() internal rewroteStacktraces.has(err) check) expect(result.message).toBe('Custom error') expect(result.stack).toBeDefined() - expect(result.code).toBeUndefined() - expect(result.statusCode).toBeUndefined() + expect(result.code).toBe('E_CUSTOM') + expect(result.statusCode).toBe(500) }) }) diff --git a/packages/vike/utils/getBetterError.ts b/packages/vike/utils/getBetterError.ts index dba19909608..4043e4105e2 100644 --- a/packages/vike/utils/getBetterError.ts +++ b/packages/vike/utils/getBetterError.ts @@ -9,24 +9,29 @@ export { getBetterError } import { isObject } from './isObject.js' import { assertIsNotBrowser } from './assertIsNotBrowser.js' import { objectAssign } from './objectAssign.js' +import { shallowClone } from './shallowClone.js' assertIsNotBrowser() function getBetterError( err: unknown, modifications: { message?: string | { prepend?: string; append?: string }; stack?: string; hideStack?: true }, ) { + const errOriginal = shallowClone(err) + let errBetter: { message: string; stack: string; hideStack?: true } // Normalize if (!isObject(err)) { - warnMalformed(err) + warnMalformed(errOriginal) errBetter = new Error(String(err)) as Required } else { - errBetter = structuredClone(err) as any + // We mutate instead of structuredClone(err) to avoid breaking Vite's ssrFixStacktrace() internal rewroteStacktraces.has(err) check + // https://github.com/vitejs/vite/blob/dafd726032daa98d0e614f97aebe9d4dbffe2ea7/packages/vite/src/node/ssr/ssrStacktrace.ts#L95 + errBetter = err as any } errBetter.message ??= '' if (!errBetter.stack) { - warnMalformed(err) + warnMalformed(errOriginal) errBetter.stack = new Error(errBetter.message).stack! } @@ -48,7 +53,7 @@ function getBetterError( const stack = errBetter.stack.slice(messagePrevIdx + messagePrev.length) errBetter.stack = messageNext + stack } else { - warnMalformed(err) + warnMalformed(errOriginal) } } else { if (modsMessage?.append) { @@ -65,12 +70,12 @@ function getBetterError( } // Enable users to retrieve the original error - objectAssign(errBetter, { getOriginalError: () => (err as any)?.getOriginalError?.() ?? err }) + objectAssign(errBetter, { getOriginalError: () => (errOriginal as any)?.getOriginalError?.() ?? errOriginal }) return errBetter } // TO-DO/eventually: think about whether logging this warning is a good idea -function warnMalformed(err: unknown) { - console.warn('Malformed error: ', err) +function warnMalformed(errOriginal: unknown) { + console.warn('Malformed error: ', errOriginal) } diff --git a/packages/vike/utils/shallowClone.ts b/packages/vike/utils/shallowClone.ts new file mode 100644 index 00000000000..a3b455bb72c --- /dev/null +++ b/packages/vike/utils/shallowClone.ts @@ -0,0 +1,12 @@ +export { shallowClone } + +import { isObject } from './isObject.js' + +// - AFAICT it's the most accurate. +// - The structuredClone() built-in isn't usable: https://www.typescriptlang.org/play/?ssl=7&ssc=1&pln=9&pc=1#code/G4QwTgBApmkLwQHZQO4QKKwPZgBQHIBhAVwGcAXLAW2mzHwEoIRTnEBPAKAh95jAB0AYywATKBAT50AfUIBVAMoAVAPIBZfN149+AiiHJlCYiQgCsABkucRiCrTCEANlmSjJECmGJCjYKFEXNyhcfgZOAHpIiAAZLABzVgADYkRxADMAS3dk2zdSLGcoAVcEgn5g92FTfAAaRyrAmvEIqJjlAAswLBRWEERHHE5vX39AptwAby9qKAAxNKFcJimAXzWGIA +function shallowClone(obj: Obj): Obj { + if (!isObject(obj)) return obj + const clone = Object.create(Object.getPrototypeOf(obj)) + Object.defineProperties(clone, Object.getOwnPropertyDescriptors(obj)) + return clone +}