@mandujs/core 0.18.20 → 0.18.22

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.
Files changed (91) hide show
  1. package/package.json +3 -1
  2. package/src/brain/architecture/analyzer.ts +3 -5
  3. package/src/brain/architecture/types.ts +4 -4
  4. package/src/brain/doctor/analyzer.ts +1 -0
  5. package/src/brain/doctor/index.ts +1 -1
  6. package/src/brain/doctor/patcher.ts +10 -6
  7. package/src/brain/doctor/reporter.ts +4 -4
  8. package/src/brain/types.ts +14 -10
  9. package/src/bundler/build.ts +17 -17
  10. package/src/bundler/css.ts +3 -2
  11. package/src/bundler/dev.ts +1 -1
  12. package/src/client/island.ts +10 -9
  13. package/src/client/router.ts +1 -1
  14. package/src/config/mcp-ref.ts +6 -6
  15. package/src/config/metadata.test.ts +1 -1
  16. package/src/config/metadata.ts +36 -16
  17. package/src/config/symbols.ts +1 -1
  18. package/src/config/validate.ts +17 -1
  19. package/src/content/content.test.ts +3 -3
  20. package/src/content/loaders/file.ts +3 -0
  21. package/src/content/loaders/glob.ts +1 -0
  22. package/src/contract/client-safe.test.ts +1 -1
  23. package/src/contract/client.test.ts +2 -1
  24. package/src/contract/client.ts +18 -18
  25. package/src/contract/define.ts +32 -17
  26. package/src/contract/handler.ts +11 -11
  27. package/src/contract/index.ts +2 -5
  28. package/src/contract/infer.test.ts +2 -1
  29. package/src/contract/normalize.test.ts +1 -1
  30. package/src/contract/normalize.ts +17 -11
  31. package/src/contract/registry.test.ts +1 -1
  32. package/src/contract/zod-utils.ts +155 -0
  33. package/src/devtools/client/catchers/error-catcher.ts +3 -3
  34. package/src/devtools/client/catchers/network-proxy.ts +5 -1
  35. package/src/devtools/client/components/kitchen-root.tsx +2 -2
  36. package/src/devtools/client/components/panel/guard-panel.tsx +3 -3
  37. package/src/devtools/client/state-manager.ts +9 -9
  38. package/src/devtools/index.ts +8 -8
  39. package/src/devtools/init.ts +2 -2
  40. package/src/devtools/protocol.ts +4 -4
  41. package/src/devtools/server/source-context.ts +9 -3
  42. package/src/devtools/types.ts +5 -5
  43. package/src/devtools/worker/redaction-worker.ts +12 -5
  44. package/src/error/index.ts +1 -1
  45. package/src/error/result.ts +14 -0
  46. package/src/filling/deps.ts +5 -2
  47. package/src/filling/filling.ts +1 -1
  48. package/src/generator/templates.ts +2 -2
  49. package/src/guard/contract-guard.test.ts +1 -0
  50. package/src/guard/file-type.test.ts +1 -1
  51. package/src/guard/index.ts +1 -1
  52. package/src/guard/negotiation.ts +29 -1
  53. package/src/guard/presets/index.ts +3 -0
  54. package/src/guard/semantic-slots.ts +4 -4
  55. package/src/index.ts +10 -1
  56. package/src/intent/index.ts +28 -17
  57. package/src/island/index.ts +8 -8
  58. package/src/openapi/generator.ts +49 -31
  59. package/src/plugins/index.ts +1 -1
  60. package/src/plugins/registry.ts +28 -18
  61. package/src/plugins/types.ts +2 -2
  62. package/src/resource/__tests__/backward-compat.test.ts +2 -2
  63. package/src/resource/__tests__/edge-cases.test.ts +14 -13
  64. package/src/resource/__tests__/fixtures.ts +2 -2
  65. package/src/resource/__tests__/generator.test.ts +1 -1
  66. package/src/resource/__tests__/performance.test.ts +8 -6
  67. package/src/resource/schema.ts +1 -1
  68. package/src/router/fs-routes.ts +34 -40
  69. package/src/router/fs-types.ts +2 -2
  70. package/src/router/index.ts +1 -1
  71. package/src/runtime/boundary.tsx +4 -4
  72. package/src/runtime/logger.test.ts +3 -3
  73. package/src/runtime/logger.ts +1 -1
  74. package/src/runtime/server.ts +18 -16
  75. package/src/runtime/ssr.ts +1 -1
  76. package/src/runtime/stable-selector.ts +1 -2
  77. package/src/runtime/streaming-ssr.ts +15 -6
  78. package/src/seo/index.ts +5 -0
  79. package/src/seo/integration/ssr.ts +4 -4
  80. package/src/seo/render/basic.ts +12 -4
  81. package/src/seo/render/opengraph.ts +12 -6
  82. package/src/seo/render/twitter.ts +3 -2
  83. package/src/seo/resolve/url.ts +7 -0
  84. package/src/seo/types.ts +13 -0
  85. package/src/spec/schema.ts +89 -61
  86. package/src/types/branded.ts +56 -0
  87. package/src/types/index.ts +1 -0
  88. package/src/utils/hasher.test.ts +6 -6
  89. package/src/utils/hasher.ts +2 -2
  90. package/src/utils/index.ts +1 -1
  91. package/src/watcher/watcher.ts +2 -2
@@ -12,6 +12,19 @@ import type {
12
12
  SchemaExamples,
13
13
  } from "../contract/schema";
14
14
  import path from "path";
15
+ import {
16
+ getZodTypeName,
17
+ getZodInnerType,
18
+ getZodArrayElementType,
19
+ getZodEffectsSchema,
20
+ getZodObjectShape,
21
+ getZodChecks,
22
+ getZodEnumValues,
23
+ getZodUnionOptions,
24
+ getZodLiteralValue,
25
+ getZodDefaultValue,
26
+ isZodRequired,
27
+ } from "../contract/zod-utils";
15
28
 
16
29
  // ============================================
17
30
  // OpenAPI Types
@@ -125,50 +138,54 @@ export interface OpenAPIDocument {
125
138
  * consider using zod-to-openapi or similar library.
126
139
  */
127
140
  export function zodToOpenAPISchema(zodSchema: z.ZodTypeAny): OpenAPISchema {
128
- const def = zodSchema._def;
141
+ const typeName = getZodTypeName(zodSchema);
129
142
 
130
143
  // Handle ZodOptional
131
- if (def.typeName === "ZodOptional") {
132
- const innerSchema = zodToOpenAPISchema(def.innerType);
144
+ if (typeName === "ZodOptional") {
145
+ const inner = getZodInnerType(zodSchema);
146
+ const innerSchema = inner ? zodToOpenAPISchema(inner) : {};
133
147
  return { ...innerSchema, nullable: true };
134
148
  }
135
149
 
136
150
  // Handle ZodDefault
137
- if (def.typeName === "ZodDefault") {
138
- const innerSchema = zodToOpenAPISchema(def.innerType);
139
- return { ...innerSchema, default: def.defaultValue() };
151
+ if (typeName === "ZodDefault") {
152
+ const inner = getZodInnerType(zodSchema);
153
+ const innerSchema = inner ? zodToOpenAPISchema(inner) : {};
154
+ return { ...innerSchema, default: getZodDefaultValue(zodSchema) };
140
155
  }
141
156
 
142
157
  // Handle ZodNullable
143
- if (def.typeName === "ZodNullable") {
144
- const innerSchema = zodToOpenAPISchema(def.innerType);
158
+ if (typeName === "ZodNullable") {
159
+ const inner = getZodInnerType(zodSchema);
160
+ const innerSchema = inner ? zodToOpenAPISchema(inner) : {};
145
161
  return { ...innerSchema, nullable: true };
146
162
  }
147
163
 
148
164
  // Handle ZodEffects (coerce, transform, etc.)
149
- if (def.typeName === "ZodEffects") {
150
- return zodToOpenAPISchema(def.schema);
165
+ if (typeName === "ZodEffects") {
166
+ const effectsSchema = getZodEffectsSchema(zodSchema);
167
+ return effectsSchema ? zodToOpenAPISchema(effectsSchema) : {};
151
168
  }
152
169
 
153
170
  // Handle ZodString
154
- if (def.typeName === "ZodString") {
171
+ if (typeName === "ZodString") {
155
172
  const schema: OpenAPISchema = { type: "string" };
156
- for (const check of def.checks || []) {
173
+ for (const check of getZodChecks(zodSchema)) {
157
174
  if (check.kind === "email") schema.format = "email";
158
175
  if (check.kind === "uuid") schema.format = "uuid";
159
176
  if (check.kind === "url") schema.format = "uri";
160
177
  if (check.kind === "datetime") schema.format = "date-time";
161
178
  if (check.kind === "min") schema.minLength = check.value;
162
179
  if (check.kind === "max") schema.maxLength = check.value;
163
- if (check.kind === "regex") schema.pattern = check.regex.source;
180
+ if (check.kind === "regex") schema.pattern = check.regex?.source;
164
181
  }
165
182
  return schema;
166
183
  }
167
184
 
168
185
  // Handle ZodNumber
169
- if (def.typeName === "ZodNumber") {
186
+ if (typeName === "ZodNumber") {
170
187
  const schema: OpenAPISchema = { type: "number" };
171
- for (const check of def.checks || []) {
188
+ for (const check of getZodChecks(zodSchema)) {
172
189
  if (check.kind === "int") schema.type = "integer";
173
190
  if (check.kind === "min") schema.minimum = check.value;
174
191
  if (check.kind === "max") schema.maximum = check.value;
@@ -177,30 +194,30 @@ export function zodToOpenAPISchema(zodSchema: z.ZodTypeAny): OpenAPISchema {
177
194
  }
178
195
 
179
196
  // Handle ZodBoolean
180
- if (def.typeName === "ZodBoolean") {
197
+ if (typeName === "ZodBoolean") {
181
198
  return { type: "boolean" };
182
199
  }
183
200
 
184
201
  // Handle ZodArray
185
- if (def.typeName === "ZodArray") {
202
+ if (typeName === "ZodArray") {
203
+ const elementType = getZodArrayElementType(zodSchema);
186
204
  return {
187
205
  type: "array",
188
- items: zodToOpenAPISchema(def.type),
206
+ items: elementType ? zodToOpenAPISchema(elementType) : {},
189
207
  };
190
208
  }
191
209
 
192
210
  // Handle ZodObject
193
- if (def.typeName === "ZodObject") {
211
+ if (typeName === "ZodObject") {
194
212
  const properties: Record<string, OpenAPISchema> = {};
195
213
  const required: string[] = [];
196
214
 
197
- const shape = def.shape();
215
+ const shape = getZodObjectShape(zodSchema) ?? {};
198
216
  for (const [key, value] of Object.entries(shape)) {
199
- properties[key] = zodToOpenAPISchema(value as z.ZodTypeAny);
217
+ properties[key] = zodToOpenAPISchema(value);
200
218
 
201
219
  // Check if field is required
202
- const fieldDef = (value as z.ZodTypeAny)._def;
203
- if (fieldDef.typeName !== "ZodOptional" && fieldDef.typeName !== "ZodDefault") {
220
+ if (isZodRequired(value)) {
204
221
  required.push(key);
205
222
  }
206
223
  }
@@ -213,23 +230,24 @@ export function zodToOpenAPISchema(zodSchema: z.ZodTypeAny): OpenAPISchema {
213
230
  }
214
231
 
215
232
  // Handle ZodEnum
216
- if (def.typeName === "ZodEnum") {
233
+ if (typeName === "ZodEnum") {
217
234
  return {
218
235
  type: "string",
219
- enum: def.values,
236
+ enum: getZodEnumValues(zodSchema) as unknown[],
220
237
  };
221
238
  }
222
239
 
223
240
  // Handle ZodUnion
224
- if (def.typeName === "ZodUnion") {
241
+ if (typeName === "ZodUnion") {
242
+ const options = getZodUnionOptions(zodSchema) ?? [];
225
243
  return {
226
- oneOf: def.options.map((opt: z.ZodTypeAny) => zodToOpenAPISchema(opt)),
244
+ oneOf: options.map((opt) => zodToOpenAPISchema(opt)),
227
245
  };
228
246
  }
229
247
 
230
248
  // Handle ZodLiteral
231
- if (def.typeName === "ZodLiteral") {
232
- const value = def.value;
249
+ if (typeName === "ZodLiteral") {
250
+ const value = getZodLiteralValue(zodSchema);
233
251
  return {
234
252
  type: typeof value as string,
235
253
  enum: [value],
@@ -237,12 +255,12 @@ export function zodToOpenAPISchema(zodSchema: z.ZodTypeAny): OpenAPISchema {
237
255
  }
238
256
 
239
257
  // Handle ZodVoid/ZodUndefined (no content)
240
- if (def.typeName === "ZodVoid" || def.typeName === "ZodUndefined") {
258
+ if (typeName === "ZodVoid" || typeName === "ZodUndefined") {
241
259
  return {};
242
260
  }
243
261
 
244
262
  // Handle ZodAny/ZodUnknown
245
- if (def.typeName === "ZodAny" || def.typeName === "ZodUnknown") {
263
+ if (typeName === "ZodAny" || typeName === "ZodUnknown") {
246
264
  return {};
247
265
  }
248
266
 
@@ -24,7 +24,7 @@ export type {
24
24
  GuardPresetPlugin,
25
25
  GuardRule,
26
26
  GuardRuleContext,
27
- GuardViolation,
27
+ PluginGuardViolation,
28
28
  LayerDefinition,
29
29
  ImportInfo,
30
30
  ExportInfo,
@@ -25,15 +25,19 @@ import type {
25
25
  type PluginState = "pending" | "loaded" | "error" | "unloaded";
26
26
 
27
27
  /**
28
- * 등록된 플러그인 정보
28
+ * 등록된 플러그인 정보 (discriminated union by state)
29
+ *
30
+ * state에 따라 접근 가능한 필드가 타입 레벨에서 결정됨:
31
+ * - pending: plugin, config만 존재
32
+ * - loaded: plugin, config, loadedAt 존재
33
+ * - error: plugin, config, error 존재
34
+ * - unloaded: plugin만 존재
29
35
  */
30
- interface RegisteredPlugin<TConfig = unknown> {
31
- plugin: Plugin<TConfig>;
32
- state: PluginState;
33
- config?: TConfig;
34
- error?: Error;
35
- loadedAt?: Date;
36
- }
36
+ type RegisteredPlugin<TConfig = unknown> =
37
+ | { state: "pending"; plugin: Plugin<TConfig>; config?: TConfig }
38
+ | { state: "loaded"; plugin: Plugin<TConfig>; config?: TConfig; loadedAt: Date }
39
+ | { state: "error"; plugin: Plugin<TConfig>; config?: TConfig; error: Error }
40
+ | { state: "unloaded"; plugin: Plugin<TConfig> };
37
41
 
38
42
  /**
39
43
  * 소유자 정보 포함 리소스
@@ -89,7 +93,7 @@ export class PluginRegistry {
89
93
  const error = new Error(
90
94
  `Invalid config for plugin "${id}": ${result.error.message}`
91
95
  );
92
- this.plugins.set(id, { plugin, state: "error", error });
96
+ this.plugins.set(id, { plugin: plugin as Plugin<unknown>, state: "error", config: validatedConfig, error });
93
97
  throw error;
94
98
  }
95
99
  validatedConfig = result.data;
@@ -97,7 +101,7 @@ export class PluginRegistry {
97
101
 
98
102
  // 등록
99
103
  this.plugins.set(id, {
100
- plugin,
104
+ plugin: plugin as Plugin<unknown>,
101
105
  state: "pending",
102
106
  config: validatedConfig,
103
107
  });
@@ -112,16 +116,23 @@ export class PluginRegistry {
112
116
  await plugin.onLoad();
113
117
  }
114
118
 
115
- const entry = this.plugins.get(id)!;
116
- entry.state = "loaded";
117
- entry.loadedAt = new Date();
119
+ this.plugins.set(id, {
120
+ state: "loaded",
121
+ plugin: plugin as Plugin<unknown>,
122
+ config: validatedConfig,
123
+ loadedAt: new Date(),
124
+ });
118
125
 
119
126
  this.logger.info(`Plugin loaded: ${id} (v${plugin.meta.version})`);
120
127
  } catch (error) {
121
- const entry = this.plugins.get(id)!;
122
- entry.state = "error";
123
- entry.error = error instanceof Error ? error : new Error(String(error));
124
- throw entry.error;
128
+ const pluginError = error instanceof Error ? error : new Error(String(error));
129
+ this.plugins.set(id, {
130
+ state: "error",
131
+ plugin: plugin as Plugin<unknown>,
132
+ config: validatedConfig,
133
+ error: pluginError,
134
+ });
135
+ throw pluginError;
125
136
  }
126
137
  }
127
138
 
@@ -142,7 +153,6 @@ export class PluginRegistry {
142
153
  // 등록된 리소스 정리
143
154
  this.removeOwnedResources(id);
144
155
 
145
- entry.state = "unloaded";
146
156
  this.plugins.delete(id);
147
157
 
148
158
  this.logger.info(`Plugin unloaded: ${id}`);
@@ -174,7 +174,7 @@ export interface GuardRule {
174
174
  name: string;
175
175
  description?: string;
176
176
  severity: "error" | "warn" | "off";
177
- check: (context: GuardRuleContext) => GuardViolation[];
177
+ check: (context: GuardRuleContext) => PluginGuardViolation[];
178
178
  }
179
179
 
180
180
  /**
@@ -211,7 +211,7 @@ export interface ExportInfo {
211
211
  /**
212
212
  * Guard 위반
213
213
  */
214
- export interface GuardViolation {
214
+ export interface PluginGuardViolation {
215
215
  ruleId: string;
216
216
  message: string;
217
217
  severity: "error" | "warn";
@@ -277,7 +277,7 @@ describe("Backward Compatibility - Error Handling", () => {
277
277
  validateResourceDefinition({
278
278
  name: "",
279
279
  fields: {},
280
- } as any);
280
+ } as unknown as Parameters<typeof validateResourceDefinition>[0]); // intentionally invalid
281
281
  expect(false).toBe(true); // Should not reach here
282
282
  } catch (error) {
283
283
  expect(error).toBeDefined();
@@ -292,7 +292,7 @@ describe("Backward Compatibility - Error Handling", () => {
292
292
  validateResourceDefinition({
293
293
  name: "test",
294
294
  fields: {},
295
- } as any);
295
+ } as unknown as Parameters<typeof validateResourceDefinition>[0]); // intentionally invalid
296
296
  expect(false).toBe(true); // Should not reach here
297
297
  } catch (error) {
298
298
  expect(error).toBeDefined();
@@ -10,6 +10,7 @@ import {
10
10
  validateResourceDefinition,
11
11
  FieldTypes,
12
12
  type ResourceDefinition,
13
+ type ResourceField,
13
14
  } from "../schema";
14
15
 
15
16
  describe("Edge Cases - Resource Names", () => {
@@ -30,7 +31,7 @@ describe("Edge Cases - Resource Names", () => {
30
31
  fields: {
31
32
  id: { type: "uuid", required: true },
32
33
  },
33
- } as any;
34
+ } as unknown as ResourceDefinition; // intentionally invalid
34
35
 
35
36
  expect(() => validateResourceDefinition(definition)).toThrow(/Resource name is required/);
36
37
  });
@@ -41,7 +42,7 @@ describe("Edge Cases - Resource Names", () => {
41
42
  fields: {
42
43
  id: { type: "uuid", required: true },
43
44
  },
44
- } as any;
45
+ } as unknown as ResourceDefinition; // intentionally invalid
45
46
 
46
47
  expect(() => validateResourceDefinition(definition)).toThrow(/Invalid resource name/);
47
48
  });
@@ -52,7 +53,7 @@ describe("Edge Cases - Resource Names", () => {
52
53
  fields: {
53
54
  id: { type: "uuid", required: true },
54
55
  },
55
- } as any;
56
+ } as unknown as ResourceDefinition; // intentionally invalid
56
57
 
57
58
  expect(() => validateResourceDefinition(definition)).toThrow(/Invalid resource name/);
58
59
  });
@@ -63,7 +64,7 @@ describe("Edge Cases - Resource Names", () => {
63
64
  fields: {
64
65
  id: { type: "uuid", required: true },
65
66
  },
66
- } as any;
67
+ } as unknown as ResourceDefinition; // intentionally invalid
67
68
 
68
69
  expect(() => validateResourceDefinition(definition)).toThrow(/Invalid resource name/);
69
70
  });
@@ -121,7 +122,7 @@ describe("Edge Cases - Field Names", () => {
121
122
  fields: {
122
123
  _privateField: { type: "string", required: true },
123
124
  },
124
- } as any;
125
+ } as unknown as ResourceDefinition; // intentionally invalid
125
126
 
126
127
  // Field names must start with a letter (not underscore)
127
128
  expect(() => validateResourceDefinition(definition)).toThrow(/Invalid field name/);
@@ -133,7 +134,7 @@ describe("Edge Cases - Field Names", () => {
133
134
  fields: {
134
135
  "1field": { type: "string", required: true },
135
136
  },
136
- } as any;
137
+ } as unknown as ResourceDefinition; // intentionally invalid
137
138
 
138
139
  expect(() => validateResourceDefinition(definition)).toThrow(/Invalid field name/);
139
140
  });
@@ -206,7 +207,7 @@ describe("Edge Cases - Field Types", () => {
206
207
  fields: {
207
208
  id: { type: "unsupported_type", required: true },
208
209
  },
209
- } as any;
210
+ } as unknown as ResourceDefinition; // intentionally invalid
210
211
 
211
212
  expect(() => validateResourceDefinition(definition)).toThrow(/Invalid field type/);
212
213
  });
@@ -230,7 +231,7 @@ describe("Edge Cases - Field Types", () => {
230
231
  fields: {
231
232
  tags: { type: "array", required: true },
232
233
  },
233
- } as any;
234
+ } as unknown as ResourceDefinition; // intentionally invalid
234
235
 
235
236
  expect(() => validateResourceDefinition(definition)).toThrow(/missing "items" property/);
236
237
  });
@@ -433,9 +434,9 @@ describe("Edge Cases - Resource Options", () => {
433
434
 
434
435
  describe("Edge Cases - Large Schemas", () => {
435
436
  test("should handle resource with many fields (50 fields)", () => {
436
- const fields: Record<string, any> = {};
437
+ const fields: Record<string, ResourceField> = {};
437
438
  for (let i = 0; i < 50; i++) {
438
- fields[`field${i}`] = { type: "string", required: i % 2 === 0 };
439
+ fields[`field${i}`] = { type: "string" as const, required: i % 2 === 0 };
439
440
  }
440
441
 
441
442
  const definition: ResourceDefinition = {
@@ -483,7 +484,7 @@ describe("Edge Cases - Boundary Conditions", () => {
483
484
  const definition = {
484
485
  name: "test",
485
486
  fields: {},
486
- } as any;
487
+ } as unknown as ResourceDefinition; // intentionally invalid
487
488
 
488
489
  expect(() => validateResourceDefinition(definition)).toThrow(
489
490
  /must have at least one field/
@@ -494,7 +495,7 @@ describe("Edge Cases - Boundary Conditions", () => {
494
495
  const definition = {
495
496
  name: "test",
496
497
  fields: null,
497
- } as any;
498
+ } as unknown as ResourceDefinition; // intentionally invalid
498
499
 
499
500
  expect(() => validateResourceDefinition(definition)).toThrow(
500
501
  /must have at least one field/
@@ -505,7 +506,7 @@ describe("Edge Cases - Boundary Conditions", () => {
505
506
  const definition = {
506
507
  name: "test",
507
508
  fields: undefined,
508
- } as any;
509
+ } as unknown as ResourceDefinition; // intentionally invalid
509
510
 
510
511
  expect(() => validateResourceDefinition(definition)).toThrow(
511
512
  /must have at least one field/
@@ -166,7 +166,7 @@ export const invalidResourceFixtures = {
166
166
  fields: {
167
167
  id: { type: "uuid", required: true },
168
168
  },
169
- } as any,
169
+ } as unknown as ResourceDefinition, // intentionally invalid: missing name
170
170
 
171
171
  invalidName: {
172
172
  name: "123-invalid",
@@ -190,7 +190,7 @@ export const invalidResourceFixtures = {
190
190
  invalidFieldType: {
191
191
  name: "valid",
192
192
  fields: {
193
- field: { type: "invalid" as any, required: true },
193
+ field: { type: "invalid" as unknown as "string", required: true }, // intentionally invalid type
194
194
  },
195
195
  } as ResourceDefinition,
196
196
 
@@ -30,7 +30,7 @@ afterAll(async () => {
30
30
  /**
31
31
  * Create a test parsed resource (no file import needed)
32
32
  */
33
- function createTestParsedResource(resourceName: string, definition: any): ParsedResource {
33
+ function createTestParsedResource(resourceName: string, definition: ParsedResource["definition"]): ParsedResource {
34
34
  return {
35
35
  definition,
36
36
  filePath: path.join(testDir, "spec", "resources", `${resourceName}.resource.ts`),
@@ -7,6 +7,8 @@
7
7
  import { describe, test, expect, beforeAll, afterAll } from "bun:test";
8
8
  import { generateResourceArtifacts } from "../generator";
9
9
  import type { ParsedResource } from "../parser";
10
+ import type { ResourceField, ResourceDefinition } from "../schema";
11
+ import { validateResourceDefinition } from "../schema";
10
12
  import path from "path";
11
13
  import fs from "fs/promises";
12
14
  import os from "os";
@@ -29,7 +31,7 @@ afterAll(async () => {
29
31
  /**
30
32
  * Create a test parsed resource
31
33
  */
32
- function createTestParsedResource(resourceName: string, definition: any): ParsedResource {
34
+ function createTestParsedResource(resourceName: string, definition: ParsedResource["definition"]): ParsedResource {
33
35
  return {
34
36
  definition,
35
37
  filePath: path.join(testDir, "spec", "resources", `${resourceName}.resource.ts`),
@@ -70,10 +72,10 @@ describe("Performance - Resource Generation", () => {
70
72
  });
71
73
 
72
74
  test("should handle resource with 50 fields in < 1000ms", async () => {
73
- const fields: Record<string, any> = {};
75
+ const fields: Record<string, ResourceField> = {};
74
76
  for (let i = 0; i < 50; i++) {
75
77
  fields[`field${i}`] = {
76
- type: i % 5 === 0 ? "number" : "string",
78
+ type: i % 5 === 0 ? "number" as const : "string" as const,
77
79
  required: i % 2 === 0,
78
80
  default: i % 3 === 0 ? `value${i}` : undefined,
79
81
  };
@@ -169,7 +171,7 @@ describe("Performance - Schema Validation", () => {
169
171
  test("should validate complex schema (50 fields) in < 50ms", async () => {
170
172
  const { validateResourceDefinition } = await import("../schema");
171
173
 
172
- const fields: Record<string, any> = {};
174
+ const fields: Record<string, ResourceField> = {};
173
175
  for (let i = 0; i < 50; i++) {
174
176
  fields[`field${i}`] = {
175
177
  type: (["string", "number", "boolean", "date", "email"] as const)[i % 5],
@@ -177,7 +179,7 @@ describe("Performance - Schema Validation", () => {
177
179
  };
178
180
  }
179
181
 
180
- const definition = {
182
+ const definition: ResourceDefinition = {
181
183
  name: "complex",
182
184
  fields,
183
185
  options: {
@@ -280,7 +282,7 @@ describe("Performance - Comparison Benchmarks", () => {
280
282
  );
281
283
 
282
284
  // Large resource
283
- const largeFields: Record<string, any> = {};
285
+ const largeFields: Record<string, ResourceField> = {};
284
286
  for (let i = 0; i < 30; i++) {
285
287
  largeFields[`field${i}`] = {
286
288
  type: (["string", "number", "boolean"] as const)[i % 3],
@@ -40,7 +40,7 @@ export interface ResourceField {
40
40
  /** 배열 타입인 경우 요소 타입 */
41
41
  items?: FieldType;
42
42
  /** 커스텀 Zod 스키마 (고급 사용) */
43
- schema?: z.ZodType<any>;
43
+ schema?: z.ZodTypeAny;
44
44
  }
45
45
 
46
46
  // ============================================
@@ -21,7 +21,7 @@ import { loadManduConfig } from "../config";
21
21
  /**
22
22
  * 매니페스트 생성 결과
23
23
  */
24
- export interface GenerateResult {
24
+ export interface FSGenerateResult {
25
25
  /** 생성된 매니페스트 */
26
26
  manifest: RoutesManifest;
27
27
 
@@ -51,49 +51,43 @@ export interface GenerateOptions {
51
51
  * FSRouteConfig를 RouteSpec으로 변환
52
52
  */
53
53
  export function fsRouteToRouteSpec(fsRoute: FSRouteConfig): RouteSpec {
54
- const routeSpec: RouteSpec = {
54
+ const base = {
55
55
  id: fsRoute.id,
56
56
  pattern: fsRoute.pattern,
57
- kind: fsRoute.kind,
58
57
  module: fsRoute.module,
59
58
  };
60
59
 
61
- // 페이지 라우트의 경우
62
60
  if (fsRoute.kind === "page") {
63
- routeSpec.componentModule = fsRoute.componentModule;
64
-
65
- // Island (클라이언트 모듈)
66
- if (fsRoute.clientModule) {
67
- routeSpec.clientModule = fsRoute.clientModule;
68
- routeSpec.hydration = fsRoute.hydration ?? {
69
- strategy: "island",
70
- priority: "visible",
71
- preload: false,
72
- };
73
- }
74
-
75
- // Layout 체인
76
- if (fsRoute.layoutChain && fsRoute.layoutChain.length > 0) {
77
- routeSpec.layoutChain = fsRoute.layoutChain;
78
- }
79
-
80
- // Loading UI
81
- if (fsRoute.loadingModule) {
82
- routeSpec.loadingModule = fsRoute.loadingModule;
83
- }
84
-
85
- // Error UI
86
- if (fsRoute.errorModule) {
87
- routeSpec.errorModule = fsRoute.errorModule;
88
- }
61
+ const pageRoute: RouteSpec = {
62
+ ...base,
63
+ kind: "page" as const,
64
+ componentModule: fsRoute.componentModule ?? "",
65
+ ...(fsRoute.clientModule
66
+ ? {
67
+ clientModule: fsRoute.clientModule,
68
+ hydration: fsRoute.hydration ?? {
69
+ strategy: "island" as const,
70
+ priority: "visible" as const,
71
+ preload: false,
72
+ },
73
+ }
74
+ : {}),
75
+ ...(fsRoute.layoutChain && fsRoute.layoutChain.length > 0
76
+ ? { layoutChain: fsRoute.layoutChain }
77
+ : {}),
78
+ ...(fsRoute.loadingModule ? { loadingModule: fsRoute.loadingModule } : {}),
79
+ ...(fsRoute.errorModule ? { errorModule: fsRoute.errorModule } : {}),
80
+ };
81
+ return pageRoute;
89
82
  }
90
83
 
91
- // API 라우트의 경우
92
- if (fsRoute.kind === "api" && fsRoute.methods) {
93
- routeSpec.methods = fsRoute.methods;
94
- }
95
-
96
- return routeSpec;
84
+ // API 라우트
85
+ const apiRoute: RouteSpec = {
86
+ ...base,
87
+ kind: "api" as const,
88
+ ...(fsRoute.methods ? { methods: fsRoute.methods } : {}),
89
+ };
90
+ return apiRoute;
97
91
  }
98
92
 
99
93
  /**
@@ -176,7 +170,7 @@ async function resolveScannerConfig(
176
170
  export async function generateManifest(
177
171
  rootDir: string,
178
172
  options: GenerateOptions = {}
179
- ): Promise<GenerateResult> {
173
+ ): Promise<FSGenerateResult> {
180
174
  const scannerConfig = await resolveScannerConfig(rootDir, options.scanner);
181
175
 
182
176
  // FS Routes 스캔
@@ -215,7 +209,7 @@ export async function generateManifest(
215
209
  /**
216
210
  * 라우트 변경 콜백
217
211
  */
218
- export type RouteChangeCallback = (result: GenerateResult) => void | Promise<void>;
212
+ export type RouteChangeCallback = (result: FSGenerateResult) => void | Promise<void>;
219
213
 
220
214
  /**
221
215
  * FS Routes 감시자 인터페이스
@@ -225,7 +219,7 @@ export interface FSRoutesWatcher {
225
219
  close(): void;
226
220
 
227
221
  /** 수동 재스캔 */
228
- rescan(): Promise<GenerateResult>;
222
+ rescan(): Promise<FSGenerateResult>;
229
223
  }
230
224
 
231
225
  /**
@@ -281,7 +275,7 @@ export async function watchFSRoutes(
281
275
 
282
276
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
283
277
 
284
- const triggerRescan = async (): Promise<GenerateResult> => {
278
+ const triggerRescan = async (): Promise<FSGenerateResult> => {
285
279
  const result = await generateManifest(rootDir, generateOptions);
286
280
  if (onChange) {
287
281
  await onChange(result);
@@ -6,7 +6,7 @@
6
6
  * @module router/fs-types
7
7
  */
8
8
 
9
- import type { RouteKind, HydrationConfig, HttpMethod } from "../spec/schema";
9
+ import type { RouteKind, HydrationConfig, SpecHttpMethod } from "../spec/schema";
10
10
 
11
11
  // ═══════════════════════════════════════════════════════════════════════════
12
12
  // Segment Types
@@ -93,7 +93,7 @@ export interface FSRouteConfig {
93
93
  kind: RouteKind;
94
94
 
95
95
  /** HTTP 메서드 (API 라우트용) */
96
- methods?: HttpMethod[];
96
+ methods?: SpecHttpMethod[];
97
97
 
98
98
  /** 페이지 컴포넌트 모듈 경로 */
99
99
  componentModule?: string;
@@ -69,7 +69,7 @@ export {
69
69
  export { FSScanner, createFSScanner, scanRoutes } from "./fs-scanner";
70
70
 
71
71
  // Generator
72
- export type { GenerateResult, GenerateOptions, RouteChangeCallback, FSRoutesWatcher } from "./fs-routes";
72
+ export type { FSGenerateResult, GenerateOptions, RouteChangeCallback, FSRoutesWatcher } from "./fs-routes";
73
73
 
74
74
  export {
75
75
  fsRouteToRouteSpec,