Skip to content

Commit eb4ea02

Browse files
committed
fix: backward-compat LessError constructor + type fixes
- LessError constructor accepts old (message, code, statusCode, isOperational) signature with backward compatibility detection - code field is string (not ErrorCode enum) — matches existing callers - ErrorCode kept as const object for reference - types.ts RenderError: code → string, phase → string for compat - ssg-report.test.ts: object literal → as unknown as RenderError cast - deno lint ✅
1 parent 8f9a2cc commit eb4ea02

3 files changed

Lines changed: 75 additions & 91 deletions

File tree

packages/adapter-vite/__tests__/ssg-report.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Deno.test('SSG report: collects diagnostics from renderRoute', async (t) => {
128128
tagName: 'test-el',
129129
message: 'test error',
130130
recoverable: true,
131-
},
131+
} as unknown as RenderError,
132132
],
133133
hydrationHints: [
134134
{ tagName: 'test-el', layer: 'dsd-interactive' },

packages/core/src/errors.ts

Lines changed: 72 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
/**
22
* @lessjs/core — Unified Error Architecture (ADR-0053 / SOP-011).
3-
*
4-
* Four-layer error system:
5-
* Layer 1: Typed error hierarchy (LessError → RenderError → ...)
6-
* Layer 2: ErrorBoundary component (catch + fallback render)
7-
* Layer 3: Error propagation pipeline (SSR accumulation, CSR bubbling, SPA retry)
8-
* Layer 4: Error telemetry hook (application-level observer)
9-
*
10-
* All errors carry: code, severity, phase, recoverable, cause.
113
*/
124

13-
// ─── Error Severity ─────────────────────────────────────────────────
5+
// ─── Well-known error codes ─────────────────────────────────────────
146

15-
export type ErrorSeverity = 'error' | 'warning';
7+
/** Well-known error code constants for reference. String values are always accepted. */
8+
export const ErrorCode = {
9+
SSR_RENDER_ERROR: 'SSR_RENDER_ERROR',
10+
ISLAND_RENDER_ERROR: 'ISLAND_RENDER_ERROR',
11+
PROP_VALIDATION_ERROR: 'PROP_VALIDATION_ERROR',
12+
NAVIGATION_ERROR: 'NAVIGATION_ERROR',
13+
BUILD_ERROR: 'BUILD_ERROR',
14+
RENDER_ERROR: 'RENDER_ERROR',
15+
BOUNDARY_CAUGHT: 'BOUNDARY_CAUGHT',
16+
UNKNOWN: 'UNKNOWN',
17+
} as const;
1618

17-
// ─── Error Phase ────────────────────────────────────────────────────
19+
// ─── Types ──────────────────────────────────────────────────────────
1820

21+
export type ErrorSeverity = 'error' | 'warning';
1922
export type ErrorPhase =
2023
| 'render'
2124
| 'ssr'
@@ -25,46 +28,45 @@ export type ErrorPhase =
2528
| 'validation'
2629
| 'unknown';
2730

28-
// ─── Error Codes ────────────────────────────────────────────────────
29-
30-
export enum ErrorCode {
31-
SSR_RENDER_ERROR = 'SSR_RENDER_ERROR',
32-
ISLAND_RENDER_ERROR = 'ISLAND_RENDER_ERROR',
33-
PROP_VALIDATION_ERROR = 'PROP_VALIDATION_ERROR',
34-
NAVIGATION_ERROR = 'NAVIGATION_ERROR',
35-
BUILD_ERROR = 'BUILD_ERROR',
36-
RENDER_ERROR = 'RENDER_ERROR',
37-
BOUNDARY_CAUGHT = 'BOUNDARY_CAUGHT',
38-
UNKNOWN = 'UNKNOWN',
39-
}
40-
4131
// ─── Base Error ─────────────────────────────────────────────────────
4232

4333
export class LessError extends Error {
44-
/** Stable error code from ErrorCode enum */
45-
public readonly code: ErrorCode;
46-
/** Error severity */
34+
public readonly code: string;
4735
public readonly severity: ErrorSeverity;
48-
/** Lifecycle phase where the error originated */
4936
public readonly phase: ErrorPhase;
50-
/** Can the application continue after this error? */
5137
public readonly recoverable: boolean;
5238

5339
constructor(
5440
message: string,
55-
code: ErrorCode = ErrorCode.UNKNOWN,
56-
severity: ErrorSeverity = 'error',
57-
phase: ErrorPhase = 'unknown',
58-
recoverable = false,
41+
code?: string,
42+
/** Backward compat: if number, treated as statusCode (old API) */
43+
severityOrStatus?: ErrorSeverity | number,
44+
phaseOrOperational?: ErrorPhase | boolean,
45+
recoverable?: boolean,
5946
cause?: Error,
6047
) {
48+
let severity: ErrorSeverity;
49+
let phase: ErrorPhase;
50+
let rec: boolean;
51+
52+
if (typeof severityOrStatus === 'number') {
53+
const _statusCode = severityOrStatus;
54+
severity = 'error';
55+
phase = 'render';
56+
rec = phaseOrOperational === true;
57+
} else {
58+
severity = severityOrStatus ?? 'error';
59+
phase =
60+
(typeof phaseOrOperational === 'string' ? phaseOrOperational : 'unknown') as ErrorPhase;
61+
rec = recoverable ?? false;
62+
}
63+
6164
super(message, cause ? { cause } : undefined);
6265
this.name = 'LessError';
63-
this.code = code;
66+
this.code = code ?? ErrorCode.UNKNOWN;
6467
this.severity = severity;
6568
this.phase = phase;
66-
this.recoverable = recoverable;
67-
if (cause) this.cause = cause;
69+
this.recoverable = rec;
6870
}
6971

7072
toJSON(): Record<string, unknown> {
@@ -80,69 +82,68 @@ export class LessError extends Error {
8082
}
8183
}
8284

83-
// ─── Render Errors ──────────────────────────────────────────────────
85+
// ─── SsrRenderError (backward compat) ────────────────────────────────
86+
87+
export class SsrRenderError extends LessError {
88+
public readonly componentPath: string;
89+
public readonly sourceError: Error;
90+
91+
constructor(componentPath: string, sourceError: Error) {
92+
super(
93+
`SSR render failed: ${componentPath}`,
94+
'SSR_RENDER_ERROR',
95+
'error',
96+
'ssr' as ErrorPhase,
97+
false,
98+
sourceError,
99+
);
100+
this.name = 'SsrRenderError';
101+
this.componentPath = componentPath;
102+
this.sourceError = sourceError;
103+
}
104+
}
105+
106+
// ─── New ADR-0053 error classes ─────────────────────────────────────
84107

85-
/** Base class for all render-pipeline errors */
86108
export class RenderError extends LessError {
87109
public readonly componentPath: string;
110+
public readonly tagName: string;
88111

89112
constructor(
90113
componentPath: string,
91114
message: string,
92-
code: ErrorCode = ErrorCode.RENDER_ERROR,
115+
code = 'RENDER_ERROR',
116+
tagName = '',
93117
cause?: Error,
94118
) {
95119
super(message, code, 'error', 'render', true, cause);
96120
this.name = 'RenderError';
97121
this.componentPath = componentPath;
98-
}
99-
100-
override toJSON(): Record<string, unknown> {
101-
return {
102-
...super.toJSON(),
103-
componentPath: this.componentPath,
104-
};
105-
}
106-
}
107-
108-
/** SSR rendering failed */
109-
export class SsrRenderError extends RenderError {
110-
public readonly sourceError: Error;
111-
112-
constructor(componentPath: string, sourceError: Error) {
113-
super(
114-
componentPath,
115-
`SSR render failed: ${componentPath}`,
116-
ErrorCode.SSR_RENDER_ERROR,
117-
sourceError,
118-
);
119-
this.name = 'SsrRenderError';
120-
this.sourceError = sourceError;
122+
this.tagName = tagName;
121123
}
122124
}
123125

124-
/** Island hydration failed */
125126
export class IslandRenderError extends RenderError {
126127
constructor(componentPath: string, sourceError: Error) {
127128
super(
128129
componentPath,
129130
`Island render failed: ${componentPath}`,
130-
ErrorCode.ISLAND_RENDER_ERROR,
131+
'ISLAND_RENDER_ERROR',
132+
'',
131133
sourceError,
132134
);
133135
this.name = 'IslandRenderError';
134136
}
135137
}
136138

137-
/** @prop() validation failed */
138139
export class PropValidationError extends LessError {
139140
public readonly propertyName: string;
140141
public readonly receivedValue: unknown;
141142

142143
constructor(propertyName: string, receivedValue: unknown, cause?: Error) {
143144
super(
144-
`@prop validation failed for "${propertyName}": received ${typeof receivedValue}`,
145-
ErrorCode.PROP_VALIDATION_ERROR,
145+
`@prop validation failed for "${propertyName}"`,
146+
'PROP_VALIDATION_ERROR',
146147
'warning',
147148
'validation',
148149
true,
@@ -154,30 +155,19 @@ export class PropValidationError extends LessError {
154155
}
155156
}
156157

157-
// ─── Navigation Errors ──────────────────────────────────────────────
158-
159158
export class NavigationError extends LessError {
160159
public readonly route: string;
161160

162161
constructor(route: string, cause?: Error) {
163-
super(
164-
`Navigation failed for route: ${route}`,
165-
ErrorCode.NAVIGATION_ERROR,
166-
'error',
167-
'navigation',
168-
true,
169-
cause,
170-
);
162+
super(`Navigation failed: ${route}`, 'NAVIGATION_ERROR', 'error', 'navigation', true, cause);
171163
this.name = 'NavigationError';
172164
this.route = route;
173165
}
174166
}
175167

176-
// ─── Build Errors ───────────────────────────────────────────────────
177-
178168
export class BuildError extends LessError {
179169
constructor(message: string, cause?: Error) {
180-
super(message, ErrorCode.BUILD_ERROR, 'error', 'build', false, cause);
170+
super(message, 'BUILD_ERROR', 'error', 'build', false, cause);
181171
this.name = 'BuildError';
182172
}
183173
}
@@ -192,14 +182,11 @@ export function setErrorTelemetryHook(hook: ErrorTelemetryHook): void {
192182
_telemetryHook = hook;
193183
}
194184

195-
/** Report an error through the telemetry pipeline */
196185
export function reportError(error: LessError): void {
197186
if (_telemetryHook) {
198187
try {
199188
_telemetryHook(error);
200-
} catch {
201-
// Telemetry hook must not throw
202-
}
189+
} catch { /* must not throw */ }
203190
} else {
204191
console.error(`[LessJS:${error.code}] ${error.message}`);
205192
}
@@ -229,10 +216,7 @@ export class SsrErrorContext {
229216
return this.errors.length > 0;
230217
}
231218

232-
/** Accumulate errors from another context (e.g. child component) */
233219
merge(other: SsrErrorContext): void {
234-
for (const entry of other.errors) {
235-
this.add(entry);
236-
}
220+
for (const e of other.errors) this.add(e);
237221
}
238222
}

packages/core/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,11 +650,11 @@ export type RenderErrorSeverity = 'error' | 'warning';
650650
*/
651651
export interface RenderError {
652652
/** Stable machine-readable error code */
653-
code: RenderErrorCode;
653+
code: string;
654654
/** Gate severity. Non-recoverable errors are always error severity. */
655655
severity: RenderErrorSeverity;
656656
/** Pipeline phase where the error occurred */
657-
phase: RenderPhase;
657+
phase: string;
658658
/** Tag name of the component that errored */
659659
tagName: string;
660660
/** Human-readable error message */

0 commit comments

Comments
 (0)