Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions packages/vike/utils/getBetterError.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,20 +272,25 @@ 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', () => {
const firstError = new Error('First error')
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', () => {
Expand All @@ -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)
})
})

Expand Down
19 changes: 12 additions & 7 deletions packages/vike/utils/getBetterError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Error>
} 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!
}

Expand All @@ -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) {
Expand All @@ -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)
}
12 changes: 12 additions & 0 deletions packages/vike/utils/shallowClone.ts
Original file line number Diff line number Diff line change
@@ -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): Obj {
if (!isObject(obj)) return obj
const clone = Object.create(Object.getPrototypeOf(obj))
Object.defineProperties(clone, Object.getOwnPropertyDescriptors(obj))
return clone
}
Loading