Skip to content

Commit 724c798

Browse files
committed
Add server-side BuildError
1 parent 86cfa9e commit 724c798

10 files changed

Lines changed: 470 additions & 8 deletions

File tree

‎src/components/scripts/errors/__tests__/assertions.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { describe, expect, test } from 'vitest'
66
import { ClientScriptError } from '@components/scripts/errors/ClientScriptError'
77
import { isError, isErrorEvent, isPromiseRejectionEvent, isClientScriptError } from '@components/scripts/errors/assertions'
8-
import { PromiseRejectionEvent } from '@lib/@types/PromiseRejectionEvent'
8+
import { PromiseRejectionEvent } from '@components/scripts/@types/PromiseRejectionEvent'
99

1010
const voidFn = () => {}
1111

‎src/components/scripts/errors/__tests__/converters.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
normalizeMessage,
1515
} from '@components/scripts/errors/converters'
1616
import { isString } from '@components/scripts/assertions/primitives'
17-
import { PromiseRejectionEvent } from '@lib/@types/PromiseRejectionEvent'
17+
import { PromiseRejectionEvent } from '@components/scripts/@types/PromiseRejectionEvent'
1818

1919
const voidFn = () => {}
2020

‎src/components/scripts/errors/assertions.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Error-related assertions
33
*/
44
import { ClientScriptError } from './ClientScriptError'
5-
import type { PromiseRejectionEvent } from '@lib/@types/PromiseRejectionEvent'
5+
import type { PromiseRejectionEvent } from '@components/scripts/@types/PromiseRejectionEvent'
66

77
export function isError(error: unknown): error is Error {
88
if (typeof error === 'object' && (error as Error).name === 'Error') return true

‎src/components/scripts/errors/converters.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const normalizeMessage = (message: unknown): ClientScriptErrorParams => {
3939
}
4040
}
4141

42-
export interface stackMetadata {
42+
interface stackMetadata {
4343
fileName?: string | undefined
4444
lineNumber?: string | undefined
4545
columnNumber?: string | undefined

‎src/components/scripts/errors/handler.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ export function handleScriptError(
3939
},
4040
})
4141
} else {
42-
// For e2e testing to capture any error without needing timeouts
43-
if (window._throw) throw clientError
44-
// Otherwise log it for debugging
42+
// Log it for debugging
4543
console.error(
4644
`[${context.scriptName}]${context.operation ? ` ${context.operation}` : ''}:`,
4745
clientError,

‎src/lib/errors/BuildError.ts‎

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* Custom error class for build-time errors with enhanced context
3+
*
4+
* Used for errors that occur during build processes, compilation, file operations,
5+
* and other build-time activities.
6+
*/
7+
8+
export interface BuildErrorParams {
9+
message: string
10+
stack?: string | undefined
11+
cause?: unknown
12+
/** Build phase where the error occurred (e.g., 'compilation', 'optimization', 'bundling') */
13+
phase?: string | undefined
14+
/** File path where the error occurred */
15+
filePath?: string | undefined
16+
/** Build tool or process that encountered the error */
17+
tool?: string | undefined
18+
/** Line number in source file (for compilation errors) */
19+
lineNumber?: number | undefined
20+
/** Column number in source file (for compilation errors) */
21+
columnNumber?: number | undefined
22+
/** Exit code from build process */
23+
exitCode?: number | undefined
24+
}
25+
26+
/**
27+
* Normalize various input types to BuildErrorParams
28+
*/
29+
function normalizeMessage(message: unknown): BuildErrorParams {
30+
if (message instanceof Error) {
31+
return {
32+
message: message.message,
33+
stack: message.stack,
34+
cause: message.cause
35+
}
36+
}
37+
38+
if (typeof message === 'string') {
39+
return { message }
40+
}
41+
42+
if (message === undefined || message === null) {
43+
return { message: '' }
44+
}
45+
46+
return { message: String(message) }
47+
}
48+
49+
export class BuildError extends Error {
50+
phase?: string | undefined
51+
filePath?: string | undefined
52+
tool?: string | undefined
53+
lineNumber?: number | undefined
54+
columnNumber?: number | undefined
55+
exitCode?: number | undefined
56+
57+
constructor(message?: unknown, context?: Partial<BuildErrorParams>) {
58+
const data = normalizeMessage(message)
59+
super(data.message || '')
60+
61+
/**
62+
* Set error name as constructor name and make it not enumerable to
63+
* keep native Error behavior
64+
*/
65+
Object.defineProperty(this, 'name', {
66+
value: 'BuildError',
67+
enumerable: false,
68+
configurable: true,
69+
})
70+
71+
/** Fix the extended error prototype chain because TypeScript __extends can't */
72+
Object.setPrototypeOf(this, new.target.prototype)
73+
74+
/** Remove constructor from stack trace in V8 */
75+
if ('captureStackTrace' in Error) Error.captureStackTrace(this, BuildError)
76+
77+
/** V8 collects last 10 stack traces by default, this collects all */
78+
if ('stackTraceLimit' in Error) Error.stackTraceLimit = Infinity
79+
80+
// Set core error properties
81+
this.message = data.message || ''
82+
this.cause = data.cause
83+
84+
// Set build-specific context from constructor parameter
85+
if (context) {
86+
this.phase = context.phase
87+
this.tool = context.tool
88+
this.exitCode = context.exitCode
89+
this.filePath = context.filePath
90+
this.lineNumber = context.lineNumber
91+
this.columnNumber = context.columnNumber
92+
}
93+
}
94+
95+
/**
96+
* Create a BuildError for compilation failures
97+
*/
98+
static compilation(message: string, filePath?: string, lineNumber?: number, columnNumber?: number): BuildError {
99+
return new BuildError(message, {
100+
phase: 'compilation',
101+
tool: 'typescript',
102+
filePath,
103+
lineNumber,
104+
columnNumber
105+
})
106+
}
107+
108+
/**
109+
* Create a BuildError for file operation failures
110+
*/
111+
static fileOperation(message: string, filePath?: string, operation?: string): BuildError {
112+
return new BuildError(message, {
113+
phase: 'file-operation',
114+
tool: operation || 'filesystem',
115+
filePath
116+
})
117+
}
118+
119+
/**
120+
* Create a BuildError for bundling/build tool failures
121+
*/
122+
static bundling(message: string, tool?: string, exitCode?: number): BuildError {
123+
return new BuildError(message, {
124+
phase: 'bundling',
125+
tool: tool || 'bundler',
126+
exitCode
127+
})
128+
}
129+
130+
toJSON() {
131+
return {
132+
error: {
133+
name: this.name,
134+
message: this.message,
135+
stack: this.stack,
136+
phase: this.phase,
137+
filePath: this.filePath,
138+
tool: this.tool,
139+
lineNumber: this.lineNumber,
140+
columnNumber: this.columnNumber,
141+
exitCode: this.exitCode
142+
},
143+
}
144+
}
145+
146+
/**
147+
* Format error for console output with build context
148+
*/
149+
override toString(): string {
150+
let output = `${this.name}: ${this.message}`
151+
152+
if (this.phase) output += ` [${this.phase}]`
153+
if (this.tool) output += ` (${this.tool})`
154+
if (this.filePath) {
155+
output += `\n at ${this.filePath}`
156+
if (this.lineNumber) {
157+
output += `:${this.lineNumber}`
158+
if (this.columnNumber) output += `:${this.columnNumber}`
159+
}
160+
}
161+
if (this.exitCode !== undefined) output += `\n Exit code: ${this.exitCode}`
162+
163+
return output
164+
}
165+
}
166+
167+
/**
168+
* Type guard to check if an error is a BuildError
169+
*/
170+
export function isBuildError(error: unknown): error is BuildError {
171+
return error instanceof BuildError
172+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* Tests for error handling routines and custom errors
3+
*/
4+
import { describe, expect, test } from 'vitest'
5+
import { BuildError, isBuildError } from '@lib/errors/BuildError'
6+
7+
describe(`BuildError class is constructible`, () => {
8+
test(`Class is properly constructed`, () => {
9+
try {
10+
throw new BuildError(`Test error`)
11+
} catch (err) {
12+
if (!isBuildError(err)) throw new Error()
13+
// The name property should be set to the error`s name
14+
expect(err.name).toBe(`BuildError`)
15+
16+
// The error should be an instance of its class
17+
expect(err).toBeInstanceOf(BuildError)
18+
19+
// The error should be an instance of builtin Error
20+
expect(err).toBeInstanceOf(Error)
21+
22+
// toString should return the default error message formatting
23+
expect(err.toString()).toBe(`BuildError: Test error`)
24+
25+
// The error should have recorded a stack
26+
expect(err.stack).toEqual(expect.any(String))
27+
}
28+
})
29+
})
30+
31+
describe(`BuildError has proper inheritance and props set`, () => {
32+
test(`Instance`, () => {
33+
expect(BuildError).toHaveInProtoChain(Error)
34+
})
35+
36+
test(`Extended`, () => {
37+
class SubError extends BuildError {}
38+
const sut = new SubError(`test message`)
39+
expect(SubError).toHaveInProtoChain(Error, BuildError)
40+
//expect(sut).toHaveProperty(`name`, `SubError`)
41+
expect(sut).toHaveProperty(`message`, `test message`)
42+
})
43+
44+
test(`Extended with constructor`, () => {
45+
class HttpError extends BuildError {
46+
constructor(
47+
/* eslint-disable-next-line no-unused-vars */
48+
public code: number,
49+
message?: string
50+
) {
51+
super(message)
52+
}
53+
}
54+
const sut = new HttpError(404, `test message`)
55+
expect(HttpError).toHaveInProtoChain(Error, BuildError)
56+
//expect(sut).toHaveProperty(`name`, `HttpError`)
57+
expect(sut).toHaveProperty(`message`, `test message`)
58+
expect(sut).toHaveProperty(`code`, 404)
59+
})
60+
61+
test(`Extended with name`, () => {
62+
class RenamedError extends BuildError {
63+
constructor(name: string, message?: string) {
64+
super(message)
65+
Object.defineProperty(this, `name`, { value: name })
66+
}
67+
}
68+
const sut = new RenamedError(`test`, `test message`)
69+
expect(RenamedError).toHaveInProtoChain(Error, BuildError)
70+
expect(sut).toHaveProperty(`name`, `test`)
71+
expect(sut).toHaveProperty(`message`, `test message`)
72+
})
73+
})
74+
75+
describe(`BuildError construction works with new`, () => {
76+
test(`Basic properties`, () => {
77+
const sut = new BuildError(`test message`)
78+
expect(sut).toHaveProperty(`name`, `BuildError`)
79+
expect(sut).toHaveProperty(`message`, `test message`)
80+
})
81+
82+
test(`Without message`, () => {
83+
const sut = new BuildError()
84+
expect(sut).toHaveProperty(`name`, `BuildError`)
85+
expect(sut).toHaveProperty(`message`, ``)
86+
})
87+
88+
test(`With build-specific properties`, () => {
89+
const sut = new BuildError(`test message`, {
90+
phase: 'compilation',
91+
tool: 'astro',
92+
exitCode: 1,
93+
filePath: '/src/test.ts',
94+
lineNumber: 10,
95+
columnNumber: 5
96+
})
97+
expect(sut).toHaveProperty(`name`, `BuildError`)
98+
expect(sut).toHaveProperty(`message`, `test message`)
99+
expect(sut.phase).toBe('compilation')
100+
expect(sut.tool).toBe('astro')
101+
expect(sut.exitCode).toBe(1)
102+
expect(sut.filePath).toBe('/src/test.ts')
103+
expect(sut.lineNumber).toBe(10)
104+
expect(sut.columnNumber).toBe(5)
105+
})
106+
})
107+
108+
describe(`toString behavior in logging`, () => {
109+
test(`Outputs error with message when coerced to string`, () => {
110+
expect(`${new BuildError(`Hello`)}`).toMatch(`BuildError: Hello`)
111+
})
112+
})
113+
114+
describe(`toJSON behavior overridden`, () => {
115+
test(`Outputs structured JSON error with toJSON`, () => {
116+
const sut = new BuildError(`test message`)
117+
expect(sut.toJSON()).toEqual(
118+
expect.objectContaining({
119+
error: expect.objectContaining({
120+
message: expect.any(String),
121+
name: expect.any(String),
122+
stack: expect.any(String),
123+
}),
124+
})
125+
)
126+
})
127+
})
128+
129+
describe(`Static factory methods`, () => {
130+
test(`compilation creates error with compilation phase`, () => {
131+
const sut = BuildError.compilation(
132+
`TypeScript compilation failed`,
133+
'/src/component.ts',
134+
15,
135+
5
136+
)
137+
expect(sut.message).toBe('TypeScript compilation failed')
138+
expect(sut.phase).toBe('compilation')
139+
expect(sut.filePath).toBe('/src/component.ts')
140+
expect(sut.lineNumber).toBe(15)
141+
expect(sut.columnNumber).toBe(5)
142+
})
143+
144+
test(`fileOperation creates error with file-operation phase`, () => {
145+
const sut = BuildError.fileOperation(
146+
`Could not read config file`,
147+
'/astro.config.ts',
148+
'read'
149+
)
150+
expect(sut.message).toBe('Could not read config file')
151+
expect(sut.phase).toBe('file-operation')
152+
expect(sut.filePath).toBe('/astro.config.ts')
153+
})
154+
155+
test(`bundling creates error with bundling phase`, () => {
156+
const sut = BuildError.bundling(
157+
`Bundling failed`,
158+
'vite',
159+
1
160+
)
161+
expect(sut.message).toBe('Bundling failed')
162+
expect(sut.phase).toBe('bundling')
163+
expect(sut.tool).toBe('vite')
164+
expect(sut.exitCode).toBe(1)
165+
})
166+
})

0 commit comments

Comments
 (0)