@mandujs/core 0.3.2 → 0.3.3

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.
@@ -1,234 +1,256 @@
1
- /**
2
- * Mandu Filling - 만두소 🥟
3
- * 체이닝 API로 비즈니스 로직 정의
4
- */
5
-
6
- import { ManduContext, NEXT_SYMBOL, ValidationError } from "./context";
7
-
8
- /** Handler function type */
9
- export type Handler = (ctx: ManduContext) => Response | Promise<Response>;
10
-
11
- /** Guard function type - returns next() or Response */
12
- export type Guard = (ctx: ManduContext) => symbol | Response | Promise<symbol | Response>;
13
-
14
- /** HTTP methods */
15
- export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
16
-
17
- interface FillingConfig {
18
- handlers: Map<HttpMethod, Handler>;
19
- guards: Guard[];
20
- methodGuards: Map<HttpMethod, Guard[]>;
21
- }
22
-
23
- /**
24
- * Mandu Filling Builder
25
- * @example
26
- * ```typescript
27
- * export default Mandu.filling()
28
- * .guard(authCheck)
29
- * .get(ctx => ctx.ok({ message: 'Hello!' }))
30
- * .post(ctx => ctx.created({ id: 1 }))
31
- * ```
32
- */
33
- export class ManduFilling {
34
- private config: FillingConfig = {
35
- handlers: new Map(),
36
- guards: [],
37
- methodGuards: new Map(),
38
- };
39
-
40
- // ============================================
41
- // 🥟 HTTP Method Handlers
42
- // ============================================
43
-
44
- /** Handle GET requests */
45
- get(handler: Handler): this {
46
- this.config.handlers.set("GET", handler);
47
- return this;
48
- }
49
-
50
- /** Handle POST requests */
51
- post(handler: Handler): this {
52
- this.config.handlers.set("POST", handler);
53
- return this;
54
- }
55
-
56
- /** Handle PUT requests */
57
- put(handler: Handler): this {
58
- this.config.handlers.set("PUT", handler);
59
- return this;
60
- }
61
-
62
- /** Handle PATCH requests */
63
- patch(handler: Handler): this {
64
- this.config.handlers.set("PATCH", handler);
65
- return this;
66
- }
67
-
68
- /** Handle DELETE requests */
69
- delete(handler: Handler): this {
70
- this.config.handlers.set("DELETE", handler);
71
- return this;
72
- }
73
-
74
- /** Handle HEAD requests */
75
- head(handler: Handler): this {
76
- this.config.handlers.set("HEAD", handler);
77
- return this;
78
- }
79
-
80
- /** Handle OPTIONS requests */
81
- options(handler: Handler): this {
82
- this.config.handlers.set("OPTIONS", handler);
83
- return this;
84
- }
85
-
86
- /** Handle all methods with single handler */
87
- all(handler: Handler): this {
88
- const methods: HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
89
- methods.forEach((method) => this.config.handlers.set(method, handler));
90
- return this;
91
- }
92
-
93
- // ============================================
94
- // 🥟 Guards (만두 찜기)
95
- // ============================================
96
-
97
- /**
98
- * Add guard for all methods or specific methods
99
- * @example
100
- * .guard(authCheck) // all methods
101
- * .guard(authCheck, 'POST', 'PUT') // specific methods
102
- */
103
- guard(guardFn: Guard, ...methods: HttpMethod[]): this {
104
- if (methods.length === 0) {
105
- // Apply to all methods
106
- this.config.guards.push(guardFn);
107
- } else {
108
- // Apply to specific methods
109
- methods.forEach((method) => {
110
- const guards = this.config.methodGuards.get(method) || [];
111
- guards.push(guardFn);
112
- this.config.methodGuards.set(method, guards);
113
- });
114
- }
115
- return this;
116
- }
117
-
118
- /** Alias for guard - more semantic for middleware */
119
- use(guardFn: Guard, ...methods: HttpMethod[]): this {
120
- return this.guard(guardFn, ...methods);
121
- }
122
-
123
- // ============================================
124
- // 🥟 Execution
125
- // ============================================
126
-
127
- /**
128
- * Handle incoming request
129
- * Called by generated route handler
130
- */
131
- async handle(request: Request, params: Record<string, string> = {}): Promise<Response> {
132
- const ctx = new ManduContext(request, params);
133
- const method = request.method.toUpperCase() as HttpMethod;
134
-
135
- try {
136
- // Run global guards
137
- for (const guard of this.config.guards) {
138
- const result = await guard(ctx);
139
- if (result !== NEXT_SYMBOL) {
140
- return result as Response;
141
- }
142
- if (!ctx.shouldContinue) {
143
- return ctx.getResponse()!;
144
- }
145
- }
146
-
147
- // Run method-specific guards
148
- const methodGuards = this.config.methodGuards.get(method) || [];
149
- for (const guard of methodGuards) {
150
- const result = await guard(ctx);
151
- if (result !== NEXT_SYMBOL) {
152
- return result as Response;
153
- }
154
- if (!ctx.shouldContinue) {
155
- return ctx.getResponse()!;
156
- }
157
- }
158
-
159
- // Get handler for method
160
- const handler = this.config.handlers.get(method);
161
- if (!handler) {
162
- return ctx.json(
163
- {
164
- status: "error",
165
- message: `Method ${method} not allowed`,
166
- allowed: Array.from(this.config.handlers.keys()),
167
- },
168
- 405
169
- );
170
- }
171
-
172
- // Execute handler
173
- return await handler(ctx);
174
- } catch (error) {
175
- // Handle validation errors
176
- if (error instanceof ValidationError) {
177
- return ctx.json(
178
- {
179
- status: "error",
180
- message: "Validation failed",
181
- errors: error.errors,
182
- },
183
- 400
184
- );
185
- }
186
-
187
- // Handle other errors
188
- console.error(`[Mandu] Handler error:`, error);
189
- return ctx.fail(
190
- error instanceof Error ? error.message : "Internal Server Error"
191
- );
192
- }
193
- }
194
-
195
- /**
196
- * Get list of registered methods
197
- */
198
- getMethods(): HttpMethod[] {
199
- return Array.from(this.config.handlers.keys());
200
- }
201
-
202
- /**
203
- * Check if method is registered
204
- */
205
- hasMethod(method: HttpMethod): boolean {
206
- return this.config.handlers.has(method);
207
- }
208
- }
209
-
210
- /**
211
- * Mandu namespace with factory methods
212
- */
213
- export const Mandu = {
214
- /**
215
- * Create a new filling (slot logic builder)
216
- * @example
217
- * ```typescript
218
- * import { Mandu } from '@mandujs/core'
219
- *
220
- * export default Mandu.filling()
221
- * .get(ctx => ctx.ok({ message: 'Hello!' }))
222
- * ```
223
- */
224
- filling(): ManduFilling {
225
- return new ManduFilling();
226
- },
227
-
228
- /**
229
- * Create context manually (for testing)
230
- */
231
- context(request: Request, params?: Record<string, string>): ManduContext {
232
- return new ManduContext(request, params);
233
- },
234
- };
1
+ /**
2
+ * Mandu Filling - 만두소 🥟
3
+ * 체이닝 API로 비즈니스 로직 정의
4
+ */
5
+
6
+ import { ManduContext, NEXT_SYMBOL, ValidationError } from "./context";
7
+ import { ErrorClassifier, formatErrorResponse, ErrorCode } from "../error";
8
+
9
+ /** Handler function type */
10
+ export type Handler = (ctx: ManduContext) => Response | Promise<Response>;
11
+
12
+ /** Guard function type - returns next() or Response */
13
+ export type Guard = (ctx: ManduContext) => symbol | Response | Promise<symbol | Response>;
14
+
15
+ /** HTTP methods */
16
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
17
+
18
+ interface FillingConfig {
19
+ handlers: Map<HttpMethod, Handler>;
20
+ guards: Guard[];
21
+ methodGuards: Map<HttpMethod, Guard[]>;
22
+ }
23
+
24
+ /**
25
+ * Mandu Filling Builder
26
+ * @example
27
+ * ```typescript
28
+ * export default Mandu.filling()
29
+ * .guard(authCheck)
30
+ * .get(ctx => ctx.ok({ message: 'Hello!' }))
31
+ * .post(ctx => ctx.created({ id: 1 }))
32
+ * ```
33
+ */
34
+ export class ManduFilling {
35
+ private config: FillingConfig = {
36
+ handlers: new Map(),
37
+ guards: [],
38
+ methodGuards: new Map(),
39
+ };
40
+
41
+ // ============================================
42
+ // 🥟 HTTP Method Handlers
43
+ // ============================================
44
+
45
+ /** Handle GET requests */
46
+ get(handler: Handler): this {
47
+ this.config.handlers.set("GET", handler);
48
+ return this;
49
+ }
50
+
51
+ /** Handle POST requests */
52
+ post(handler: Handler): this {
53
+ this.config.handlers.set("POST", handler);
54
+ return this;
55
+ }
56
+
57
+ /** Handle PUT requests */
58
+ put(handler: Handler): this {
59
+ this.config.handlers.set("PUT", handler);
60
+ return this;
61
+ }
62
+
63
+ /** Handle PATCH requests */
64
+ patch(handler: Handler): this {
65
+ this.config.handlers.set("PATCH", handler);
66
+ return this;
67
+ }
68
+
69
+ /** Handle DELETE requests */
70
+ delete(handler: Handler): this {
71
+ this.config.handlers.set("DELETE", handler);
72
+ return this;
73
+ }
74
+
75
+ /** Handle HEAD requests */
76
+ head(handler: Handler): this {
77
+ this.config.handlers.set("HEAD", handler);
78
+ return this;
79
+ }
80
+
81
+ /** Handle OPTIONS requests */
82
+ options(handler: Handler): this {
83
+ this.config.handlers.set("OPTIONS", handler);
84
+ return this;
85
+ }
86
+
87
+ /** Handle all methods with single handler */
88
+ all(handler: Handler): this {
89
+ const methods: HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
90
+ methods.forEach((method) => this.config.handlers.set(method, handler));
91
+ return this;
92
+ }
93
+
94
+ // ============================================
95
+ // 🥟 Guards (만두 찜기)
96
+ // ============================================
97
+
98
+ /**
99
+ * Add guard for all methods or specific methods
100
+ * @example
101
+ * .guard(authCheck) // all methods
102
+ * .guard(authCheck, 'POST', 'PUT') // specific methods
103
+ */
104
+ guard(guardFn: Guard, ...methods: HttpMethod[]): this {
105
+ if (methods.length === 0) {
106
+ // Apply to all methods
107
+ this.config.guards.push(guardFn);
108
+ } else {
109
+ // Apply to specific methods
110
+ methods.forEach((method) => {
111
+ const guards = this.config.methodGuards.get(method) || [];
112
+ guards.push(guardFn);
113
+ this.config.methodGuards.set(method, guards);
114
+ });
115
+ }
116
+ return this;
117
+ }
118
+
119
+ /** Alias for guard - more semantic for middleware */
120
+ use(guardFn: Guard, ...methods: HttpMethod[]): this {
121
+ return this.guard(guardFn, ...methods);
122
+ }
123
+
124
+ // ============================================
125
+ // 🥟 Execution
126
+ // ============================================
127
+
128
+ /**
129
+ * Handle incoming request
130
+ * Called by generated route handler
131
+ * @param request The incoming request
132
+ * @param params URL path parameters
133
+ * @param routeContext Route context for error reporting
134
+ */
135
+ async handle(
136
+ request: Request,
137
+ params: Record<string, string> = {},
138
+ routeContext?: { routeId: string; pattern: string }
139
+ ): Promise<Response> {
140
+ const ctx = new ManduContext(request, params);
141
+ const method = request.method.toUpperCase() as HttpMethod;
142
+
143
+ try {
144
+ // Run global guards
145
+ for (const guard of this.config.guards) {
146
+ const result = await guard(ctx);
147
+ if (result !== NEXT_SYMBOL) {
148
+ return result as Response;
149
+ }
150
+ if (!ctx.shouldContinue) {
151
+ return ctx.getResponse()!;
152
+ }
153
+ }
154
+
155
+ // Run method-specific guards
156
+ const methodGuards = this.config.methodGuards.get(method) || [];
157
+ for (const guard of methodGuards) {
158
+ const result = await guard(ctx);
159
+ if (result !== NEXT_SYMBOL) {
160
+ return result as Response;
161
+ }
162
+ if (!ctx.shouldContinue) {
163
+ return ctx.getResponse()!;
164
+ }
165
+ }
166
+
167
+ // Get handler for method
168
+ const handler = this.config.handlers.get(method);
169
+ if (!handler) {
170
+ return ctx.json(
171
+ {
172
+ status: "error",
173
+ message: `Method ${method} not allowed`,
174
+ allowed: Array.from(this.config.handlers.keys()),
175
+ },
176
+ 405
177
+ );
178
+ }
179
+
180
+ // Execute handler
181
+ return await handler(ctx);
182
+ } catch (error) {
183
+ // Handle validation errors with enhanced error format
184
+ if (error instanceof ValidationError) {
185
+ return ctx.json(
186
+ {
187
+ errorType: "LOGIC_ERROR",
188
+ code: ErrorCode.SLOT_VALIDATION_ERROR,
189
+ message: "Validation failed",
190
+ summary: "입력 검증 실패 - 요청 데이터 확인 필요",
191
+ fix: {
192
+ file: routeContext ? `spec/slots/${routeContext.routeId}.slot.ts` : "spec/slots/",
193
+ suggestion: "요청 데이터가 스키마와 일치하는지 확인하세요",
194
+ },
195
+ route: routeContext,
196
+ errors: error.errors,
197
+ timestamp: new Date().toISOString(),
198
+ },
199
+ 400
200
+ );
201
+ }
202
+
203
+ // Handle other errors with error classification
204
+ const classifier = new ErrorClassifier(null, routeContext);
205
+ const manduError = classifier.classify(error);
206
+
207
+ console.error(`[Mandu] ${manduError.errorType}:`, manduError.message);
208
+
209
+ const response = formatErrorResponse(manduError, {
210
+ isDev: process.env.NODE_ENV !== "production",
211
+ });
212
+
213
+ return ctx.json(response, 500);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Get list of registered methods
219
+ */
220
+ getMethods(): HttpMethod[] {
221
+ return Array.from(this.config.handlers.keys());
222
+ }
223
+
224
+ /**
225
+ * Check if method is registered
226
+ */
227
+ hasMethod(method: HttpMethod): boolean {
228
+ return this.config.handlers.has(method);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Mandu namespace with factory methods
234
+ */
235
+ export const Mandu = {
236
+ /**
237
+ * Create a new filling (slot logic builder)
238
+ * @example
239
+ * ```typescript
240
+ * import { Mandu } from '@mandujs/core'
241
+ *
242
+ * export default Mandu.filling()
243
+ * .get(ctx => ctx.ok({ message: 'Hello!' }))
244
+ * ```
245
+ */
246
+ filling(): ManduFilling {
247
+ return new ManduFilling();
248
+ },
249
+
250
+ /**
251
+ * Create context manually (for testing)
252
+ */
253
+ context(request: Request, params?: Record<string, string>): ManduContext {
254
+ return new ManduContext(request, params);
255
+ },
256
+ };
@@ -1,7 +1,7 @@
1
- /**
2
- * Mandu Filling Module - 만두소 🥟
3
- */
4
-
5
- export { ManduContext, NEXT_SYMBOL, ValidationError } from "./context";
6
- export { ManduFilling, Mandu } from "./filling";
7
- export type { Handler, Guard, HttpMethod } from "./filling";
1
+ /**
2
+ * Mandu Filling Module - 만두소 🥟
3
+ */
4
+
5
+ export { ManduContext, NEXT_SYMBOL, ValidationError } from "./context";
6
+ export { ManduFilling, Mandu } from "./filling";
7
+ export type { Handler, Guard, HttpMethod } from "./filling";
@@ -1,5 +1,6 @@
1
1
  import type { RoutesManifest, RouteSpec } from "../spec/schema";
2
2
  import { generateApiHandler, generatePageComponent, generateSlotLogic } from "./templates";
3
+ import { computeHash } from "../spec/lock";
3
4
  import path from "path";
4
5
  import fs from "fs/promises";
5
6
 
@@ -20,10 +21,64 @@ export interface GenerateResult {
20
21
  errors: string[];
21
22
  }
22
23
 
24
+ /**
25
+ * Spec 파일 정보
26
+ */
27
+ export interface SpecSource {
28
+ /** Spec 파일 경로 */
29
+ path: string;
30
+ /** SHA256 해시 */
31
+ hash: string;
32
+ }
33
+
34
+ /**
35
+ * Spec 내 라우트 위치 정보
36
+ */
37
+ export interface SpecLocation {
38
+ /** Spec 파일 경로 */
39
+ file: string;
40
+ /** routes 배열 내 인덱스 */
41
+ routeIndex: number;
42
+ /** JSON 경로 (예: "routes[0]") */
43
+ jsonPath: string;
44
+ }
45
+
46
+ /**
47
+ * Slot 파일 매핑 정보
48
+ */
49
+ export interface SlotMapping {
50
+ /** Slot 파일 경로 */
51
+ slotPath: string;
52
+ }
53
+
54
+ /**
55
+ * Generated 파일 엔트리
56
+ */
57
+ export interface GeneratedFileEntry {
58
+ /** 라우트 ID */
59
+ routeId: string;
60
+ /** 라우트 종류 */
61
+ kind: "api" | "page";
62
+ /** Spec 내 위치 */
63
+ specLocation: SpecLocation;
64
+ /** Slot 매핑 (있는 경우) */
65
+ slotMapping?: SlotMapping;
66
+ }
67
+
68
+ /**
69
+ * Generated Map 구조
70
+ */
23
71
  export interface GeneratedMap {
72
+ /** 버전 */
24
73
  version: number;
74
+ /** 생성 시각 */
25
75
  generatedAt: string;
26
- files: Record<string, { routeId: string; kind: string }>;
76
+ /** Spec 소스 정보 */
77
+ specSource: SpecSource;
78
+ /** 생성된 파일 매핑 */
79
+ files: Record<string, GeneratedFileEntry>;
80
+ /** 프레임워크 내부 파일 패턴 */
81
+ frameworkPaths: string[];
27
82
  }
28
83
 
29
84
  async function ensureDir(dirPath: string): Promise<void> {
@@ -66,14 +121,37 @@ export async function generateRoutes(
66
121
  const generatedMap: GeneratedMap = {
67
122
  version: manifest.version,
68
123
  generatedAt: new Date().toISOString(),
124
+ specSource: {
125
+ path: "spec/routes.manifest.json",
126
+ hash: computeHash(manifest),
127
+ },
69
128
  files: {},
129
+ frameworkPaths: [
130
+ "@mandujs/core",
131
+ "packages/core/src",
132
+ "node_modules/@mandujs",
133
+ ],
70
134
  };
71
135
 
72
136
  const expectedServerFiles = new Set<string>();
73
137
  const expectedWebFiles = new Set<string>();
74
138
 
75
- for (const route of manifest.routes) {
139
+ for (let routeIndex = 0; routeIndex < manifest.routes.length; routeIndex++) {
140
+ const route = manifest.routes[routeIndex];
141
+
76
142
  try {
143
+ // Spec 위치 정보
144
+ const specLocation: SpecLocation = {
145
+ file: "spec/routes.manifest.json",
146
+ routeIndex,
147
+ jsonPath: `routes[${routeIndex}]`,
148
+ };
149
+
150
+ // Slot 매핑 정보 (있는 경우)
151
+ const slotMapping: SlotMapping | undefined = route.slotModule
152
+ ? { slotPath: route.slotModule }
153
+ : undefined;
154
+
77
155
  // Server handler
78
156
  const serverFileName = `${route.id}.route.ts`;
79
157
  const serverFilePath = path.join(serverRoutesDir, serverFileName);
@@ -85,7 +163,9 @@ export async function generateRoutes(
85
163
 
86
164
  generatedMap.files[`apps/server/generated/routes/${serverFileName}`] = {
87
165
  routeId: route.id,
88
- kind: route.kind,
166
+ kind: route.kind as "api" | "page",
167
+ specLocation,
168
+ slotMapping,
89
169
  };
90
170
 
91
171
  // Slot file (only if slotModule is specified)
@@ -119,6 +199,8 @@ export async function generateRoutes(
119
199
  generatedMap.files[`apps/web/generated/routes/${webFileName}`] = {
120
200
  routeId: route.id,
121
201
  kind: route.kind,
202
+ specLocation,
203
+ slotMapping,
122
204
  };
123
205
  }
124
206
  } catch (error) {
@@ -1,2 +1,2 @@
1
- export * from "./generate";
2
- export * from "./templates";
1
+ export * from "./generate";
2
+ export * from "./templates";