@mandujs/core 0.5.3 → 0.5.5

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.
package/README.md CHANGED
@@ -1,200 +1,200 @@
1
- <p align="center">
2
- <img src="https://raw.githubusercontent.com/konamgil/mandu/main/mandu_only_simbol.png" alt="Mandu" width="200" />
3
- </p>
4
-
5
- <h1 align="center">@mandujs/core</h1>
6
-
7
- <p align="center">
8
- <strong>Mandu Framework Core</strong><br/>
9
- Spec, Generator, Guard, Runtime, Filling
10
- </p>
11
-
12
- <p align="center">
13
- English | <a href="./README.ko.md"><strong>한국어</strong></a>
14
- </p>
15
-
16
- ## Installation
17
-
18
- ```bash
19
- bun add @mandujs/core
20
- ```
21
-
22
- > Typically used through `@mandujs/cli`. Direct usage is for advanced use cases.
23
-
24
- ## Module Structure
25
-
26
- ```
27
- @mandujs/core
28
- ├── spec/ # Spec schema and loading
29
- ├── generator/ # Code generation
30
- ├── guard/ # Architecture checking and auto-correction
31
- ├── runtime/ # Server and router
32
- └── report/ # Guard report generation
33
- ```
34
-
35
- ## Spec Module
36
-
37
- Route manifest schema definition and loading.
38
-
39
- ```typescript
40
- import { loadManifest, RoutesManifest, RouteSpec } from "@mandujs/core";
41
-
42
- // Load and validate manifest
43
- const result = await loadManifest("spec/routes.manifest.json");
44
-
45
- if (result.success && result.data) {
46
- const manifest: RoutesManifest = result.data;
47
- manifest.routes.forEach((route: RouteSpec) => {
48
- console.log(route.id, route.pattern, route.kind);
49
- });
50
- }
51
- ```
52
-
53
- ### Lock File
54
-
55
- ```typescript
56
- import { writeLock, readLock } from "@mandujs/core";
57
-
58
- // Write lock file
59
- const lock = await writeLock("spec/spec.lock.json", manifest);
60
- console.log(lock.routesHash);
61
-
62
- // Read lock file
63
- const existing = await readLock("spec/spec.lock.json");
64
- ```
65
-
66
- ## Generator Module
67
-
68
- Spec-based code generation.
69
-
70
- ```typescript
71
- import { generateRoutes, GenerateResult } from "@mandujs/core";
72
-
73
- const result: GenerateResult = await generateRoutes(manifest, "./");
74
-
75
- console.log("Created:", result.created);
76
- console.log("Skipped:", result.skipped); // Existing slot files
77
- ```
78
-
79
- ### Template Functions
80
-
81
- ```typescript
82
- import {
83
- generateApiHandler,
84
- generateApiHandlerWithSlot,
85
- generateSlotLogic,
86
- generatePageComponent
87
- } from "@mandujs/core";
88
-
89
- // Generate API handler
90
- const code = generateApiHandler(route);
91
-
92
- // API handler with slot
93
- const codeWithSlot = generateApiHandlerWithSlot(route);
94
-
95
- // Slot logic file
96
- const slotCode = generateSlotLogic(route);
97
- ```
98
-
99
- ## Guard Module
100
-
101
- Architecture rule checking and auto-correction.
102
-
103
- ```typescript
104
- import {
105
- runGuardCheck,
106
- runAutoCorrect,
107
- GuardResult,
108
- GuardViolation
109
- } from "@mandujs/core";
110
-
111
- // Run check
112
- const result: GuardResult = await runGuardCheck(manifest, "./");
113
-
114
- if (!result.passed) {
115
- result.violations.forEach((v: GuardViolation) => {
116
- console.log(`${v.rule}: ${v.message}`);
117
- });
118
-
119
- // Run auto-correction
120
- const corrected = await runAutoCorrect(result.violations, manifest, "./");
121
- console.log("Fixed:", corrected.steps);
122
- console.log("Remaining violations:", corrected.remainingViolations);
123
- }
124
- ```
125
-
126
- ### Guard Rules
127
-
128
- | Rule ID | Description | Auto-correctable |
129
- |---------|-------------|------------------|
130
- | `SPEC_HASH_MISMATCH` | Spec and lock hash mismatch | ✅ |
131
- | `GENERATED_MANUAL_EDIT` | Manual edit to generated file | ✅ |
132
- | `HANDLER_NOT_FOUND` | Handler file not found | ❌ |
133
- | `COMPONENT_NOT_FOUND` | Component file not found | ❌ |
134
- | `SLOT_NOT_FOUND` | Slot file not found | ✅ |
135
-
136
- ## Runtime Module
137
-
138
- Server startup and routing.
139
-
140
- ```typescript
141
- import {
142
- startServer,
143
- registerApiHandler,
144
- registerPageLoader
145
- } from "@mandujs/core";
146
-
147
- // Register API handler
148
- registerApiHandler("getUsers", async (req) => {
149
- return { users: [] };
150
- });
151
-
152
- // Register page loader
153
- registerPageLoader("homePage", () => import("./pages/Home"));
154
-
155
- // Start server
156
- const server = startServer(manifest, { port: 3000 });
157
-
158
- // Stop
159
- server.stop();
160
- ```
161
-
162
- ## Report Module
163
-
164
- Guard result report generation.
165
-
166
- ```typescript
167
- import { buildGuardReport } from "@mandujs/core";
168
-
169
- const report = buildGuardReport(guardResult, lockPath);
170
- console.log(report); // Formatted text report
171
- ```
172
-
173
- ## Types
174
-
175
- ```typescript
176
- import type {
177
- RoutesManifest,
178
- RouteSpec,
179
- RouteKind,
180
- SpecLock,
181
- GuardResult,
182
- GuardViolation,
183
- GenerateResult,
184
- AutoCorrectResult,
185
- } from "@mandujs/core";
186
- ```
187
-
188
- ## Requirements
189
-
190
- - Bun >= 1.0.0
191
- - React >= 18.0.0
192
- - Zod >= 3.0.0
193
-
194
- ## Related Packages
195
-
196
- - [@mandujs/cli](https://www.npmjs.com/package/@mandujs/cli) - CLI tool
197
-
198
- ## License
199
-
200
- MIT
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/konamgil/mandu/main/mandu_only_simbol.png" alt="Mandu" width="200" />
3
+ </p>
4
+
5
+ <h1 align="center">@mandujs/core</h1>
6
+
7
+ <p align="center">
8
+ <strong>Mandu Framework Core</strong><br/>
9
+ Spec, Generator, Guard, Runtime, Filling
10
+ </p>
11
+
12
+ <p align="center">
13
+ English | <a href="./README.ko.md"><strong>한국어</strong></a>
14
+ </p>
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ bun add @mandujs/core
20
+ ```
21
+
22
+ > Typically used through `@mandujs/cli`. Direct usage is for advanced use cases.
23
+
24
+ ## Module Structure
25
+
26
+ ```
27
+ @mandujs/core
28
+ ├── spec/ # Spec schema and loading
29
+ ├── generator/ # Code generation
30
+ ├── guard/ # Architecture checking and auto-correction
31
+ ├── runtime/ # Server and router
32
+ └── report/ # Guard report generation
33
+ ```
34
+
35
+ ## Spec Module
36
+
37
+ Route manifest schema definition and loading.
38
+
39
+ ```typescript
40
+ import { loadManifest, RoutesManifest, RouteSpec } from "@mandujs/core";
41
+
42
+ // Load and validate manifest
43
+ const result = await loadManifest("spec/routes.manifest.json");
44
+
45
+ if (result.success && result.data) {
46
+ const manifest: RoutesManifest = result.data;
47
+ manifest.routes.forEach((route: RouteSpec) => {
48
+ console.log(route.id, route.pattern, route.kind);
49
+ });
50
+ }
51
+ ```
52
+
53
+ ### Lock File
54
+
55
+ ```typescript
56
+ import { writeLock, readLock } from "@mandujs/core";
57
+
58
+ // Write lock file
59
+ const lock = await writeLock("spec/spec.lock.json", manifest);
60
+ console.log(lock.routesHash);
61
+
62
+ // Read lock file
63
+ const existing = await readLock("spec/spec.lock.json");
64
+ ```
65
+
66
+ ## Generator Module
67
+
68
+ Spec-based code generation.
69
+
70
+ ```typescript
71
+ import { generateRoutes, GenerateResult } from "@mandujs/core";
72
+
73
+ const result: GenerateResult = await generateRoutes(manifest, "./");
74
+
75
+ console.log("Created:", result.created);
76
+ console.log("Skipped:", result.skipped); // Existing slot files
77
+ ```
78
+
79
+ ### Template Functions
80
+
81
+ ```typescript
82
+ import {
83
+ generateApiHandler,
84
+ generateApiHandlerWithSlot,
85
+ generateSlotLogic,
86
+ generatePageComponent
87
+ } from "@mandujs/core";
88
+
89
+ // Generate API handler
90
+ const code = generateApiHandler(route);
91
+
92
+ // API handler with slot
93
+ const codeWithSlot = generateApiHandlerWithSlot(route);
94
+
95
+ // Slot logic file
96
+ const slotCode = generateSlotLogic(route);
97
+ ```
98
+
99
+ ## Guard Module
100
+
101
+ Architecture rule checking and auto-correction.
102
+
103
+ ```typescript
104
+ import {
105
+ runGuardCheck,
106
+ runAutoCorrect,
107
+ GuardResult,
108
+ GuardViolation
109
+ } from "@mandujs/core";
110
+
111
+ // Run check
112
+ const result: GuardResult = await runGuardCheck(manifest, "./");
113
+
114
+ if (!result.passed) {
115
+ result.violations.forEach((v: GuardViolation) => {
116
+ console.log(`${v.rule}: ${v.message}`);
117
+ });
118
+
119
+ // Run auto-correction
120
+ const corrected = await runAutoCorrect(result.violations, manifest, "./");
121
+ console.log("Fixed:", corrected.steps);
122
+ console.log("Remaining violations:", corrected.remainingViolations);
123
+ }
124
+ ```
125
+
126
+ ### Guard Rules
127
+
128
+ | Rule ID | Description | Auto-correctable |
129
+ |---------|-------------|------------------|
130
+ | `SPEC_HASH_MISMATCH` | Spec and lock hash mismatch | ✅ |
131
+ | `GENERATED_MANUAL_EDIT` | Manual edit to generated file | ✅ |
132
+ | `HANDLER_NOT_FOUND` | Handler file not found | ❌ |
133
+ | `COMPONENT_NOT_FOUND` | Component file not found | ❌ |
134
+ | `SLOT_NOT_FOUND` | Slot file not found | ✅ |
135
+
136
+ ## Runtime Module
137
+
138
+ Server startup and routing.
139
+
140
+ ```typescript
141
+ import {
142
+ startServer,
143
+ registerApiHandler,
144
+ registerPageLoader
145
+ } from "@mandujs/core";
146
+
147
+ // Register API handler
148
+ registerApiHandler("getUsers", async (req) => {
149
+ return { users: [] };
150
+ });
151
+
152
+ // Register page loader
153
+ registerPageLoader("homePage", () => import("./pages/Home"));
154
+
155
+ // Start server
156
+ const server = startServer(manifest, { port: 3000 });
157
+
158
+ // Stop
159
+ server.stop();
160
+ ```
161
+
162
+ ## Report Module
163
+
164
+ Guard result report generation.
165
+
166
+ ```typescript
167
+ import { buildGuardReport } from "@mandujs/core";
168
+
169
+ const report = buildGuardReport(guardResult, lockPath);
170
+ console.log(report); // Formatted text report
171
+ ```
172
+
173
+ ## Types
174
+
175
+ ```typescript
176
+ import type {
177
+ RoutesManifest,
178
+ RouteSpec,
179
+ RouteKind,
180
+ SpecLock,
181
+ GuardResult,
182
+ GuardViolation,
183
+ GenerateResult,
184
+ AutoCorrectResult,
185
+ } from "@mandujs/core";
186
+ ```
187
+
188
+ ## Requirements
189
+
190
+ - Bun >= 1.0.0
191
+ - React >= 18.0.0
192
+ - Zod >= 3.0.0
193
+
194
+ ## Related Packages
195
+
196
+ - [@mandujs/cli](https://www.npmjs.com/package/@mandujs/cli) - CLI tool
197
+
198
+ ## License
199
+
200
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -122,7 +122,8 @@ function scheduleHydration(element, id, priority, data) {
122
122
  }
123
123
 
124
124
  /**
125
- * 단일 Island hydrate
125
+ * 단일 Island hydrate (또는 mount)
126
+ * SSR 플레이스홀더를 Island 컴포넌트로 교체
126
127
  */
127
128
  async function hydrateIsland(element, id, data) {
128
129
  const loader = islandRegistry.get(id);
@@ -141,7 +142,7 @@ async function hydrateIsland(element, id, data) {
141
142
  }
142
143
 
143
144
  const { definition } = islandDef;
144
- const { hydrateRoot } = await import('react-dom/client');
145
+ const { createRoot } = await import('react-dom/client');
145
146
  const React = await import('react');
146
147
 
147
148
  // Island 컴포넌트
@@ -150,8 +151,10 @@ async function hydrateIsland(element, id, data) {
150
151
  return definition.render(setupResult);
151
152
  }
152
153
 
153
- // Hydrate
154
- const root = hydrateRoot(element, React.createElement(IslandComponent));
154
+ // Mount (createRoot 사용 - SSR 플레이스홀더 교체)
155
+ // hydrateRoot 대신 createRoot 사용: Island는 SSR과 다른 컨텐츠를 렌더링할 수 있음
156
+ const root = createRoot(element);
157
+ root.render(React.createElement(IslandComponent));
155
158
  hydratedRoots.set(id, root);
156
159
 
157
160
  // 완료 표시
@@ -205,8 +205,8 @@ export class ContractValidator {
205
205
  */
206
206
  getStatusCodes(): number[] {
207
207
  return Object.keys(this.contract.response)
208
- .map((k) => parseInt(k, 10))
209
- .filter((n) => !isNaN(n));
208
+ .filter((k) => /^\d+$/.test(k))
209
+ .map((k) => parseInt(k, 10));
210
210
  }
211
211
 
212
212
  /**
@@ -49,7 +49,13 @@ export class CookieManager {
49
49
  for (const pair of pairs) {
50
50
  const [name, ...rest] = pair.trim().split("=");
51
51
  if (name) {
52
- cookies.set(name, decodeURIComponent(rest.join("=")));
52
+ const rawValue = rest.join("=");
53
+ try {
54
+ cookies.set(name, decodeURIComponent(rawValue));
55
+ } catch {
56
+ // 잘못된 URL 인코딩 시 원본 값 사용
57
+ cookies.set(name, rawValue);
58
+ }
53
59
  }
54
60
  }
55
61
  }
@@ -19,6 +19,22 @@ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" |
19
19
  /** Loader function type - SSR 데이터 로딩 */
20
20
  export type Loader<T = unknown> = (ctx: ManduContext) => T | Promise<T>;
21
21
 
22
+ /** Loader 실행 옵션 */
23
+ export interface LoaderOptions<T = unknown> {
24
+ /** 타임아웃 (ms), 기본값 5000 */
25
+ timeout?: number;
26
+ /** 타임아웃 또는 에러 시 반환할 fallback 데이터 */
27
+ fallback?: T;
28
+ }
29
+
30
+ /** Loader 타임아웃 에러 */
31
+ export class LoaderTimeoutError extends Error {
32
+ constructor(timeout: number) {
33
+ super(`Loader timed out after ${timeout}ms`);
34
+ this.name = "LoaderTimeoutError";
35
+ }
36
+ }
37
+
22
38
  interface FillingConfig<TLoaderData = unknown> {
23
39
  handlers: Map<HttpMethod, Handler>;
24
40
  guards: Guard[];
@@ -78,12 +94,37 @@ export class ManduFilling<TLoaderData = unknown> {
78
94
  /**
79
95
  * Execute loader and return data
80
96
  * @internal Used by SSR runtime
97
+ * @param ctx ManduContext
98
+ * @param options Loader 실행 옵션 (timeout, fallback)
81
99
  */
82
- async executeLoader(ctx: ManduContext): Promise<TLoaderData | undefined> {
100
+ async executeLoader(
101
+ ctx: ManduContext,
102
+ options: LoaderOptions<TLoaderData> = {}
103
+ ): Promise<TLoaderData | undefined> {
83
104
  if (!this.config.loader) {
84
105
  return undefined;
85
106
  }
86
- return await this.config.loader(ctx);
107
+
108
+ const { timeout = 5000, fallback } = options;
109
+
110
+ try {
111
+ const loaderPromise = Promise.resolve(this.config.loader(ctx));
112
+
113
+ const timeoutPromise = new Promise<never>((_, reject) => {
114
+ setTimeout(() => reject(new LoaderTimeoutError(timeout)), timeout);
115
+ });
116
+
117
+ return await Promise.race([loaderPromise, timeoutPromise]);
118
+ } catch (error) {
119
+ if (fallback !== undefined) {
120
+ console.warn(
121
+ `[Mandu] Loader failed, using fallback:`,
122
+ error instanceof Error ? error.message : String(error)
123
+ );
124
+ return fallback;
125
+ }
126
+ throw error;
127
+ }
87
128
  }
88
129
 
89
130
  /**
@@ -203,7 +244,11 @@ export class ManduFilling<TLoaderData = unknown> {
203
244
  return result as Response;
204
245
  }
205
246
  if (!ctx.shouldContinue) {
206
- return ctx.getResponse()!;
247
+ const response = ctx.getResponse();
248
+ if (!response) {
249
+ throw new Error("Guard set shouldContinue=false but no response was provided");
250
+ }
251
+ return response;
207
252
  }
208
253
  }
209
254
 
@@ -215,7 +260,11 @@ export class ManduFilling<TLoaderData = unknown> {
215
260
  return result as Response;
216
261
  }
217
262
  if (!ctx.shouldContinue) {
218
- return ctx.getResponse()!;
263
+ const response = ctx.getResponse();
264
+ if (!response) {
265
+ throw new Error("Guard set shouldContinue=false but no response was provided");
266
+ }
267
+ return response;
219
268
  }
220
269
  }
221
270
 
@@ -4,5 +4,5 @@
4
4
 
5
5
  export { ManduContext, NEXT_SYMBOL, ValidationError, CookieManager } from "./context";
6
6
  export type { CookieOptions } from "./context";
7
- export { ManduFilling, Mandu } from "./filling";
8
- export type { Handler, Guard, HttpMethod } from "./filling";
7
+ export { ManduFilling, Mandu, LoaderTimeoutError } from "./filling";
8
+ export type { Handler, Guard, HttpMethod, Loader, LoaderOptions } from "./filling";
@@ -20,6 +20,8 @@ export interface GenerateResult {
20
20
  deleted: string[];
21
21
  skipped: string[];
22
22
  errors: string[];
23
+ /** 삭제 실패 등 치명적이지 않은 경고 */
24
+ warnings: string[];
23
25
  }
24
26
 
25
27
  /**
@@ -130,6 +132,7 @@ export async function generateRoutes(
130
132
  deleted: [],
131
133
  skipped: [],
132
134
  errors: [],
135
+ warnings: [],
133
136
  };
134
137
 
135
138
  const serverRoutesDir = path.join(rootDir, "apps/server/generated/routes");
@@ -289,8 +292,14 @@ export async function generateRoutes(
289
292
  for (const file of existingServerFiles) {
290
293
  if (!expectedServerFiles.has(file)) {
291
294
  const filePath = path.join(serverRoutesDir, file);
292
- await fs.unlink(filePath);
293
- result.deleted.push(filePath);
295
+ try {
296
+ await fs.unlink(filePath);
297
+ result.deleted.push(filePath);
298
+ } catch (error) {
299
+ result.warnings.push(
300
+ `Failed to delete ${filePath}: ${error instanceof Error ? error.message : String(error)}`
301
+ );
302
+ }
294
303
  }
295
304
  }
296
305
 
@@ -298,8 +307,14 @@ export async function generateRoutes(
298
307
  for (const file of existingWebFiles) {
299
308
  if (!expectedWebFiles.has(file)) {
300
309
  const filePath = path.join(webRoutesDir, file);
301
- await fs.unlink(filePath);
302
- result.deleted.push(filePath);
310
+ try {
311
+ await fs.unlink(filePath);
312
+ result.deleted.push(filePath);
313
+ } catch (error) {
314
+ result.warnings.push(
315
+ `Failed to delete ${filePath}: ${error instanceof Error ? error.message : String(error)}`
316
+ );
317
+ }
303
318
  }
304
319
  }
305
320
 
@@ -308,8 +323,14 @@ export async function generateRoutes(
308
323
  for (const file of existingTypeFiles) {
309
324
  if (!expectedTypeFiles.has(file) && file !== "index.ts") {
310
325
  const filePath = path.join(typesDir, file);
311
- await fs.unlink(filePath);
312
- result.deleted.push(filePath);
326
+ try {
327
+ await fs.unlink(filePath);
328
+ result.deleted.push(filePath);
329
+ } catch (error) {
330
+ result.warnings.push(
331
+ `Failed to delete ${filePath}: ${error instanceof Error ? error.message : String(error)}`
332
+ );
333
+ }
313
334
  }
314
335
  }
315
336
 
@@ -1,3 +1,3 @@
1
- export * from "./generate";
2
- export * from "./templates";
3
- export * from "./contract-glue";
1
+ export * from "./generate";
2
+ export * from "./templates";
3
+ export * from "./contract-glue";
@@ -1 +1 @@
1
- export * from "./build";
1
+ export * from "./build";
@@ -1,5 +1,5 @@
1
- export * from "./ssr";
2
- export * from "./router";
3
- export * from "./server";
4
- export * from "./cors";
5
- export * from "./env";
1
+ export * from "./ssr";
2
+ export * from "./router";
3
+ export * from "./server";
4
+ export * from "./cors";
5
+ export * from "./env";