@mandujs/core 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,314 @@
1
+ import type { GeneratedMap } from "../generator/generate";
2
+ import type { ManduError, RouteContext, ErrorType } from "./types";
3
+ import { ErrorCode, ERROR_MESSAGES, ERROR_SUMMARIES } from "./types";
4
+ import { StackTraceAnalyzer, type StackFrame } from "./stack-analyzer";
5
+
6
+ /**
7
+ * ValidationError 타입 체크 (filling/context.ts에서 정의)
8
+ */
9
+ function isValidationError(error: unknown): error is { errors: unknown[] } {
10
+ return (
11
+ error !== null &&
12
+ typeof error === "object" &&
13
+ "errors" in error &&
14
+ Array.isArray((error as { errors: unknown[] }).errors)
15
+ );
16
+ }
17
+
18
+ /**
19
+ * 에러 분류기
20
+ */
21
+ export class ErrorClassifier {
22
+ private analyzer: StackTraceAnalyzer;
23
+ private routeContext?: RouteContext;
24
+ private isDev: boolean;
25
+
26
+ constructor(
27
+ generatedMap: GeneratedMap | null = null,
28
+ routeContext?: RouteContext,
29
+ rootDir: string = process.cwd()
30
+ ) {
31
+ this.analyzer = new StackTraceAnalyzer(generatedMap, rootDir);
32
+ this.routeContext = routeContext;
33
+ this.isDev = process.env.NODE_ENV !== "production";
34
+ }
35
+
36
+ /**
37
+ * 에러를 ManduError로 분류
38
+ */
39
+ classify(error: unknown): ManduError {
40
+ // ValidationError 체크
41
+ if (isValidationError(error)) {
42
+ return this.createValidationError(error);
43
+ }
44
+
45
+ // 일반 Error 객체
46
+ if (error instanceof Error) {
47
+ return this.classifyError(error);
48
+ }
49
+
50
+ // 그 외 (문자열, 숫자 등)
51
+ return this.createUnknownError(error);
52
+ }
53
+
54
+ /**
55
+ * ValidationError 처리
56
+ */
57
+ private createValidationError(error: { errors: unknown[] }): ManduError {
58
+ const slotFile = this.findSlotFile();
59
+
60
+ return {
61
+ errorType: "LOGIC_ERROR",
62
+ code: ErrorCode.SLOT_VALIDATION_ERROR,
63
+ message: ERROR_MESSAGES[ErrorCode.SLOT_VALIDATION_ERROR],
64
+ summary: ERROR_SUMMARIES[ErrorCode.SLOT_VALIDATION_ERROR],
65
+ fix: {
66
+ file: slotFile || "spec/slots/",
67
+ suggestion: "요청 데이터가 스키마와 일치하는지 확인하세요",
68
+ },
69
+ route: this.routeContext,
70
+ timestamp: new Date().toISOString(),
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Error 객체 분류
76
+ */
77
+ private classifyError(error: Error): ManduError {
78
+ const frames = this.analyzer.parseStack(error.stack);
79
+ const source = this.analyzer.determineErrorSource(frames);
80
+ const blameFrame = this.analyzer.findBlameFrame(frames);
81
+
82
+ let errorType: ErrorType;
83
+ let code: ErrorCode;
84
+ let fixFile: string;
85
+ let suggestion: string;
86
+
87
+ switch (source) {
88
+ case "slot":
89
+ errorType = "LOGIC_ERROR";
90
+ code = ErrorCode.SLOT_RUNTIME_ERROR;
91
+ fixFile = blameFrame?.file || this.findSlotFile() || "spec/slots/";
92
+ suggestion = this.generateSlotSuggestion(error);
93
+ break;
94
+
95
+ case "spec":
96
+ errorType = "SPEC_ERROR";
97
+ code = ErrorCode.SPEC_VALIDATION_ERROR;
98
+ fixFile = blameFrame?.file || "spec/routes.manifest.json";
99
+ suggestion = "Spec 파일의 JSON 구문 또는 스키마를 확인하세요";
100
+ break;
101
+
102
+ case "generated":
103
+ // Generated 파일 에러 → Slot으로 매핑 시도
104
+ if (blameFrame) {
105
+ const slotLocation = this.analyzer.mapToSlotLocation(blameFrame.file, blameFrame.line);
106
+ if (slotLocation) {
107
+ errorType = "LOGIC_ERROR";
108
+ code = ErrorCode.SLOT_RUNTIME_ERROR;
109
+ fixFile = slotLocation.file;
110
+ suggestion = this.generateSlotSuggestion(error);
111
+ break;
112
+ }
113
+ }
114
+ // 매핑 실패 시 프레임워크 버그로 처리
115
+ errorType = "FRAMEWORK_BUG";
116
+ code = ErrorCode.FRAMEWORK_INTERNAL;
117
+ fixFile = blameFrame?.file || "packages/core/";
118
+ suggestion = "Generated 파일에서 예기치 않은 오류 발생. 버그 리포트를 등록해주세요.";
119
+ break;
120
+
121
+ case "framework":
122
+ errorType = "FRAMEWORK_BUG";
123
+ code = this.determineFrameworkCode(blameFrame);
124
+ fixFile = blameFrame?.file || "packages/core/";
125
+ suggestion = "Mandu 프레임워크 내부 오류입니다. GitHub 이슈를 등록해주세요.";
126
+ break;
127
+
128
+ default:
129
+ // Unknown → 보수적으로 LOGIC_ERROR로 분류
130
+ errorType = "LOGIC_ERROR";
131
+ code = ErrorCode.SLOT_HANDLER_ERROR;
132
+ fixFile = this.findSlotFile() || "spec/slots/";
133
+ suggestion = this.generateSlotSuggestion(error);
134
+ }
135
+
136
+ const manduError: ManduError = {
137
+ errorType,
138
+ code,
139
+ message: error.message,
140
+ summary: this.generateSummary(code, blameFrame),
141
+ fix: {
142
+ file: fixFile,
143
+ suggestion,
144
+ line: blameFrame?.line,
145
+ },
146
+ route: this.routeContext,
147
+ timestamp: new Date().toISOString(),
148
+ };
149
+
150
+ // 개발 모드에서 디버그 정보 추가
151
+ if (this.isDev && error.stack) {
152
+ manduError.debug = {
153
+ stack: error.stack,
154
+ originalError: error.message,
155
+ generatedFile: this.analyzer.isGeneratedFile(blameFrame?.file || "")
156
+ ? blameFrame?.file
157
+ : undefined,
158
+ };
159
+ }
160
+
161
+ return manduError;
162
+ }
163
+
164
+ /**
165
+ * 알 수 없는 타입의 에러 처리
166
+ */
167
+ private createUnknownError(error: unknown): ManduError {
168
+ const message = typeof error === "string" ? error : String(error);
169
+
170
+ return {
171
+ errorType: "LOGIC_ERROR",
172
+ code: ErrorCode.SLOT_HANDLER_ERROR,
173
+ message,
174
+ summary: `핸들러 오류 - ${this.findSlotFile() || "slot"} 파일 확인 필요`,
175
+ fix: {
176
+ file: this.findSlotFile() || "spec/slots/",
177
+ suggestion: "핸들러에서 throw된 값을 확인하세요",
178
+ },
179
+ route: this.routeContext,
180
+ timestamp: new Date().toISOString(),
181
+ };
182
+ }
183
+
184
+ /**
185
+ * 라우트 컨텍스트에서 Slot 파일 경로 찾기
186
+ */
187
+ private findSlotFile(): string | null {
188
+ if (!this.routeContext?.id) return null;
189
+ return `spec/slots/${this.routeContext.id}.slot.ts`;
190
+ }
191
+
192
+ /**
193
+ * Slot 에러에 대한 제안 생성
194
+ */
195
+ private generateSlotSuggestion(error: Error): string {
196
+ const message = error.message.toLowerCase();
197
+
198
+ if (message.includes("undefined") || message.includes("null")) {
199
+ return "null/undefined 처리를 확인하세요. ctx.body() 결과나 파라미터 값이 없을 수 있습니다.";
200
+ }
201
+
202
+ if (message.includes("is not a function")) {
203
+ return "호출하려는 함수가 정의되어 있는지 확인하세요.";
204
+ }
205
+
206
+ if (message.includes("cannot read") || message.includes("cannot access")) {
207
+ return "객체의 속성에 접근하기 전에 객체가 존재하는지 확인하세요.";
208
+ }
209
+
210
+ if (message.includes("import") || message.includes("module")) {
211
+ return "import 경로와 모듈 이름이 올바른지 확인하세요.";
212
+ }
213
+
214
+ return "slot 파일의 로직을 검토하세요.";
215
+ }
216
+
217
+ /**
218
+ * 프레임워크 에러 코드 결정
219
+ */
220
+ private determineFrameworkCode(frame: StackFrame | null): ErrorCode {
221
+ if (!frame) return ErrorCode.FRAMEWORK_INTERNAL;
222
+
223
+ const file = frame.file.toLowerCase();
224
+
225
+ if (file.includes("generator") || file.includes("generate")) {
226
+ return ErrorCode.FRAMEWORK_GENERATOR_ERROR;
227
+ }
228
+
229
+ if (file.includes("ssr") || file.includes("render")) {
230
+ return ErrorCode.FRAMEWORK_SSR_ERROR;
231
+ }
232
+
233
+ if (file.includes("router") || file.includes("routing")) {
234
+ return ErrorCode.FRAMEWORK_ROUTER_ERROR;
235
+ }
236
+
237
+ return ErrorCode.FRAMEWORK_INTERNAL;
238
+ }
239
+
240
+ /**
241
+ * 요약 메시지 생성
242
+ */
243
+ private generateSummary(code: ErrorCode, frame: StackFrame | null): string {
244
+ const baseSummary = ERROR_SUMMARIES[code] || "오류 발생";
245
+
246
+ if (frame?.file) {
247
+ const shortFile = frame.file.split("/").pop() || frame.file;
248
+ return `${baseSummary} (${shortFile}:${frame.line || "?"})`;
249
+ }
250
+
251
+ return baseSummary;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * 특정 에러 타입에 대한 ManduError 생성 헬퍼
257
+ */
258
+ export function createSpecError(
259
+ code: ErrorCode,
260
+ message: string,
261
+ file: string = "spec/routes.manifest.json",
262
+ suggestion?: string
263
+ ): ManduError {
264
+ return {
265
+ errorType: "SPEC_ERROR",
266
+ code,
267
+ message,
268
+ summary: ERROR_SUMMARIES[code] || message,
269
+ fix: {
270
+ file,
271
+ suggestion: suggestion || ERROR_MESSAGES[code] || "Spec 파일을 확인하세요",
272
+ },
273
+ timestamp: new Date().toISOString(),
274
+ };
275
+ }
276
+
277
+ export function createLogicError(
278
+ code: ErrorCode,
279
+ message: string,
280
+ slotFile: string,
281
+ routeContext?: RouteContext,
282
+ suggestion?: string
283
+ ): ManduError {
284
+ return {
285
+ errorType: "LOGIC_ERROR",
286
+ code,
287
+ message,
288
+ summary: ERROR_SUMMARIES[code] || message,
289
+ fix: {
290
+ file: slotFile,
291
+ suggestion: suggestion || ERROR_MESSAGES[code] || "Slot 파일을 확인하세요",
292
+ },
293
+ route: routeContext,
294
+ timestamp: new Date().toISOString(),
295
+ };
296
+ }
297
+
298
+ export function createFrameworkBug(
299
+ code: ErrorCode,
300
+ message: string,
301
+ file?: string
302
+ ): ManduError {
303
+ return {
304
+ errorType: "FRAMEWORK_BUG",
305
+ code,
306
+ message,
307
+ summary: ERROR_SUMMARIES[code] || message,
308
+ fix: {
309
+ file: file || "packages/core/",
310
+ suggestion: "Mandu 프레임워크 내부 오류입니다. GitHub 이슈를 등록해주세요.",
311
+ },
312
+ timestamp: new Date().toISOString(),
313
+ };
314
+ }
@@ -0,0 +1,237 @@
1
+ import type { ManduError, RouteContext } from "./types";
2
+ import { ErrorCode } from "./types";
3
+
4
+ /**
5
+ * 포맷 옵션
6
+ */
7
+ export interface FormatOptions {
8
+ /** 개발 모드 (디버그 정보 포함) */
9
+ isDev?: boolean;
10
+ /** 스택 트레이스 포함 */
11
+ includeStack?: boolean;
12
+ /** 색상 사용 (콘솔용) */
13
+ useColors?: boolean;
14
+ }
15
+
16
+ /**
17
+ * ManduError를 JSON 응답용 객체로 포맷
18
+ */
19
+ export function formatErrorResponse(error: ManduError, options: FormatOptions = {}): object {
20
+ const { isDev = process.env.NODE_ENV !== "production" } = options;
21
+
22
+ const response: Record<string, unknown> = {
23
+ errorType: error.errorType,
24
+ code: error.code,
25
+ message: error.message,
26
+ summary: error.summary,
27
+ fix: error.fix,
28
+ };
29
+
30
+ if (error.route) {
31
+ response.route = error.route;
32
+ }
33
+
34
+ // 개발 모드에서만 디버그 정보 포함
35
+ if (isDev && error.debug) {
36
+ response.debug = error.debug;
37
+ }
38
+
39
+ response.timestamp = error.timestamp;
40
+
41
+ return response;
42
+ }
43
+
44
+ /**
45
+ * ManduError를 콘솔 출력용 문자열로 포맷
46
+ */
47
+ export function formatErrorForConsole(error: ManduError, options: FormatOptions = {}): string {
48
+ const { useColors = true, includeStack = true, isDev = true } = options;
49
+
50
+ const lines: string[] = [];
51
+
52
+ // 헤더
53
+ const typeColor = getErrorTypeColor(error.errorType);
54
+ const header = useColors
55
+ ? `${typeColor}[${error.errorType}]${RESET} ${error.code}`
56
+ : `[${error.errorType}] ${error.code}`;
57
+ lines.push(header);
58
+
59
+ // 메시지
60
+ lines.push(` ${error.message}`);
61
+
62
+ // 요약
63
+ if (useColors) {
64
+ lines.push(` ${CYAN}→ ${error.summary}${RESET}`);
65
+ } else {
66
+ lines.push(` → ${error.summary}`);
67
+ }
68
+
69
+ // 수정 안내
70
+ lines.push("");
71
+ if (useColors) {
72
+ lines.push(` ${YELLOW}Fix:${RESET} ${error.fix.file}${error.fix.line ? `:${error.fix.line}` : ""}`);
73
+ lines.push(` ${error.fix.suggestion}`);
74
+ } else {
75
+ lines.push(` Fix: ${error.fix.file}${error.fix.line ? `:${error.fix.line}` : ""}`);
76
+ lines.push(` ${error.fix.suggestion}`);
77
+ }
78
+
79
+ // 라우트 컨텍스트
80
+ if (error.route) {
81
+ lines.push("");
82
+ lines.push(` Route: ${error.route.id} (${error.route.pattern})`);
83
+ }
84
+
85
+ // 디버그 정보 (개발 모드)
86
+ if (isDev && includeStack && error.debug?.stack) {
87
+ lines.push("");
88
+ lines.push(" Stack:");
89
+ const stackLines = error.debug.stack.split("\n").slice(0, 10);
90
+ for (const stackLine of stackLines) {
91
+ lines.push(` ${stackLine}`);
92
+ }
93
+ if (error.debug.stack.split("\n").length > 10) {
94
+ lines.push(" ...(truncated)");
95
+ }
96
+ }
97
+
98
+ return lines.join("\n");
99
+ }
100
+
101
+ /**
102
+ * 404 에러 응답 생성
103
+ */
104
+ export function createNotFoundResponse(
105
+ pathname: string,
106
+ routeContext?: RouteContext
107
+ ): ManduError {
108
+ return {
109
+ errorType: "SPEC_ERROR",
110
+ code: ErrorCode.SPEC_ROUTE_NOT_FOUND,
111
+ message: `Route not found: ${pathname}`,
112
+ summary: "라우트 없음 - spec 파일에 추가 필요",
113
+ fix: {
114
+ file: "spec/routes.manifest.json",
115
+ suggestion: `'${pathname}' 패턴의 라우트를 추가하세요`,
116
+ },
117
+ route: routeContext,
118
+ timestamp: new Date().toISOString(),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * 핸들러 미등록 에러 응답 생성
124
+ */
125
+ export function createHandlerNotFoundResponse(
126
+ routeId: string,
127
+ pattern: string
128
+ ): ManduError {
129
+ return {
130
+ errorType: "FRAMEWORK_BUG",
131
+ code: ErrorCode.FRAMEWORK_ROUTER_ERROR,
132
+ message: `Handler not registered for route: ${routeId}`,
133
+ summary: "핸들러 미등록 - generate 재실행 필요",
134
+ fix: {
135
+ file: `apps/server/generated/routes/${routeId}.route.ts`,
136
+ suggestion: "bunx mandu generate를 실행하세요",
137
+ },
138
+ route: {
139
+ id: routeId,
140
+ pattern,
141
+ },
142
+ timestamp: new Date().toISOString(),
143
+ };
144
+ }
145
+
146
+ /**
147
+ * 페이지 모듈 로드 실패 에러 응답 생성
148
+ */
149
+ export function createPageLoadErrorResponse(
150
+ routeId: string,
151
+ pattern: string,
152
+ originalError?: Error
153
+ ): ManduError {
154
+ const error: ManduError = {
155
+ errorType: "LOGIC_ERROR",
156
+ code: ErrorCode.SLOT_IMPORT_ERROR,
157
+ message: originalError?.message || `Failed to load page module for route: ${routeId}`,
158
+ summary: `페이지 모듈 로드 실패 - ${routeId}.route.tsx 확인 필요`,
159
+ fix: {
160
+ file: `apps/web/generated/routes/${routeId}.route.tsx`,
161
+ suggestion: "import 경로와 컴포넌트 export를 확인하세요",
162
+ },
163
+ route: {
164
+ id: routeId,
165
+ pattern,
166
+ kind: "page",
167
+ },
168
+ timestamp: new Date().toISOString(),
169
+ };
170
+
171
+ if (originalError?.stack && process.env.NODE_ENV !== "production") {
172
+ error.debug = {
173
+ stack: originalError.stack,
174
+ originalError: originalError.message,
175
+ };
176
+ }
177
+
178
+ return error;
179
+ }
180
+
181
+ /**
182
+ * SSR 렌더링 에러 응답 생성
183
+ */
184
+ export function createSSRErrorResponse(
185
+ routeId: string,
186
+ pattern: string,
187
+ originalError?: Error
188
+ ): ManduError {
189
+ const error: ManduError = {
190
+ errorType: "FRAMEWORK_BUG",
191
+ code: ErrorCode.FRAMEWORK_SSR_ERROR,
192
+ message: originalError?.message || `SSR rendering failed for route: ${routeId}`,
193
+ summary: `SSR 렌더링 실패 - 컴포넌트 확인 필요`,
194
+ fix: {
195
+ file: `apps/web/generated/routes/${routeId}.route.tsx`,
196
+ suggestion: "React 컴포넌트가 서버에서 렌더링 가능한지 확인하세요 (브라우저 전용 API 사용 금지)",
197
+ },
198
+ route: {
199
+ id: routeId,
200
+ pattern,
201
+ kind: "page",
202
+ },
203
+ timestamp: new Date().toISOString(),
204
+ };
205
+
206
+ if (originalError?.stack && process.env.NODE_ENV !== "production") {
207
+ error.debug = {
208
+ stack: originalError.stack,
209
+ originalError: originalError.message,
210
+ };
211
+ }
212
+
213
+ return error;
214
+ }
215
+
216
+ // ANSI 색상 코드
217
+ const RED = "\x1b[31m";
218
+ const YELLOW = "\x1b[33m";
219
+ const CYAN = "\x1b[36m";
220
+ const MAGENTA = "\x1b[35m";
221
+ const RESET = "\x1b[0m";
222
+
223
+ /**
224
+ * 에러 타입에 따른 색상 반환
225
+ */
226
+ function getErrorTypeColor(errorType: string): string {
227
+ switch (errorType) {
228
+ case "SPEC_ERROR":
229
+ return YELLOW;
230
+ case "LOGIC_ERROR":
231
+ return RED;
232
+ case "FRAMEWORK_BUG":
233
+ return MAGENTA;
234
+ default:
235
+ return CYAN;
236
+ }
237
+ }
@@ -0,0 +1,25 @@
1
+ // Types
2
+ export type { ManduError, RouteContext, FixTarget, DebugInfo, ErrorType } from "./types";
3
+ export { ErrorCode, ERROR_MESSAGES, ERROR_SUMMARIES } from "./types";
4
+
5
+ // Stack Analyzer
6
+ export { StackTraceAnalyzer, type StackFrame } from "./stack-analyzer";
7
+
8
+ // Classifier
9
+ export {
10
+ ErrorClassifier,
11
+ createSpecError,
12
+ createLogicError,
13
+ createFrameworkBug,
14
+ } from "./classifier";
15
+
16
+ // Formatter
17
+ export {
18
+ formatErrorResponse,
19
+ formatErrorForConsole,
20
+ createNotFoundResponse,
21
+ createHandlerNotFoundResponse,
22
+ createPageLoadErrorResponse,
23
+ createSSRErrorResponse,
24
+ type FormatOptions,
25
+ } from "./formatter";