@mandujs/core 0.18.21 → 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 (61) hide show
  1. package/package.json +3 -1
  2. package/src/brain/doctor/analyzer.ts +1 -0
  3. package/src/brain/doctor/patcher.ts +10 -6
  4. package/src/brain/doctor/reporter.ts +2 -2
  5. package/src/brain/types.ts +14 -10
  6. package/src/bundler/build.ts +17 -17
  7. package/src/bundler/dev.ts +1 -1
  8. package/src/client/island.ts +10 -9
  9. package/src/client/router.ts +1 -1
  10. package/src/config/mcp-ref.ts +6 -6
  11. package/src/config/metadata.test.ts +1 -1
  12. package/src/config/metadata.ts +36 -16
  13. package/src/config/symbols.ts +1 -1
  14. package/src/config/validate.ts +16 -0
  15. package/src/content/content.test.ts +3 -3
  16. package/src/content/loaders/file.ts +3 -0
  17. package/src/content/loaders/glob.ts +1 -0
  18. package/src/contract/client-safe.test.ts +1 -1
  19. package/src/contract/client.ts +18 -18
  20. package/src/contract/define.ts +29 -14
  21. package/src/contract/handler.ts +11 -11
  22. package/src/contract/normalize.test.ts +1 -1
  23. package/src/contract/normalize.ts +16 -10
  24. package/src/contract/registry.test.ts +1 -1
  25. package/src/contract/zod-utils.ts +155 -0
  26. package/src/devtools/client/catchers/network-proxy.ts +5 -1
  27. package/src/devtools/init.ts +2 -2
  28. package/src/devtools/server/source-context.ts +9 -3
  29. package/src/devtools/worker/redaction-worker.ts +12 -5
  30. package/src/error/index.ts +1 -1
  31. package/src/error/result.ts +14 -0
  32. package/src/filling/deps.ts +1 -1
  33. package/src/generator/templates.ts +2 -2
  34. package/src/guard/contract-guard.test.ts +1 -0
  35. package/src/guard/file-type.test.ts +1 -1
  36. package/src/guard/negotiation.ts +29 -1
  37. package/src/index.ts +1 -0
  38. package/src/intent/index.ts +28 -17
  39. package/src/island/index.ts +2 -2
  40. package/src/openapi/generator.ts +49 -31
  41. package/src/plugins/registry.ts +28 -18
  42. package/src/resource/__tests__/backward-compat.test.ts +2 -2
  43. package/src/resource/__tests__/edge-cases.test.ts +14 -13
  44. package/src/resource/__tests__/fixtures.ts +2 -2
  45. package/src/resource/__tests__/generator.test.ts +1 -1
  46. package/src/resource/__tests__/performance.test.ts +8 -6
  47. package/src/resource/schema.ts +1 -1
  48. package/src/router/fs-routes.ts +29 -35
  49. package/src/runtime/logger.test.ts +3 -3
  50. package/src/runtime/logger.ts +1 -1
  51. package/src/runtime/server.ts +8 -6
  52. package/src/runtime/stable-selector.ts +1 -2
  53. package/src/runtime/streaming-ssr.ts +11 -2
  54. package/src/seo/index.ts +5 -0
  55. package/src/seo/integration/ssr.ts +2 -2
  56. package/src/seo/resolve/url.ts +7 -0
  57. package/src/seo/types.ts +13 -0
  58. package/src/spec/schema.ts +82 -54
  59. package/src/types/branded.ts +56 -0
  60. package/src/types/index.ts +1 -0
  61. package/src/utils/hasher.test.ts +6 -6
@@ -25,6 +25,8 @@
25
25
  import { z, type ZodType } from 'zod';
26
26
  import { ManduFillingFactory, type ManduFilling } from '../filling/filling';
27
27
  import type { ManduContext } from '../filling/context';
28
+ import { getZodTypeName, getZodObjectShape, getZodArrayElementType } from '../contract/zod-utils';
29
+ import type { ZodTypeAny } from 'zod';
28
30
 
29
31
  // ============================================================================
30
32
  // Types
@@ -51,7 +53,7 @@ export interface IntentDefinition<TInput = unknown, TOutput = unknown> {
51
53
  guard?: (ctx: ManduContext) => Response | void | Promise<Response | void>;
52
54
  }
53
55
 
54
- export type IntentMap = Record<string, IntentDefinition<any, any>>;
56
+ export type IntentMap = Record<string, IntentDefinition<unknown, unknown>>;
55
57
 
56
58
  export interface IntentMeta {
57
59
  __intent: true;
@@ -86,7 +88,7 @@ export function intent(intents: IntentMap): ManduFilling & IntentMeta {
86
88
  const docs: IntentDocumentation[] = [];
87
89
 
88
90
  // 메서드별로 핸들러 그룹화
89
- const methodHandlers: Record<HttpMethod, IntentDefinition<any, any>[]> = {
91
+ const methodHandlers: Record<HttpMethod, IntentDefinition<unknown, unknown>[]> = {
90
92
  GET: [],
91
93
  POST: [],
92
94
  PUT: [],
@@ -115,12 +117,13 @@ export function intent(intents: IntentMap): ManduFilling & IntentMeta {
115
117
  }
116
118
 
117
119
  // 각 메서드에 대해 핸들러 등록
118
- const registerMethod = (method: HttpMethod, handlers: IntentDefinition<any, any>[]) => {
120
+ const registerMethod = (method: HttpMethod, handlers: IntentDefinition<unknown, unknown>[]) => {
119
121
  if (handlers.length === 0) return;
120
122
 
121
123
  const methodLower = method.toLowerCase() as Lowercase<HttpMethod>;
124
+ const fillingWithMethods = filling as ManduFilling & Record<Lowercase<HttpMethod>, (handler: (ctx: ManduContext) => Response | Promise<Response>) => ManduFilling>;
122
125
 
123
- (filling as any)[methodLower](async (ctx: ManduContext) => {
126
+ fillingWithMethods[methodLower](async (ctx: ManduContext) => {
124
127
  // 경로 매칭 (path가 있는 경우)
125
128
  for (const def of handlers) {
126
129
  if (def.path && !matchPath(ctx.url, def.path)) {
@@ -135,11 +138,15 @@ export function intent(intents: IntentMap): ManduFilling & IntentMeta {
135
138
  }
136
139
  }
137
140
 
138
- // Input 검증
141
+ // Input 검증 (ctx.body() throws ValidationError on schema mismatch)
139
142
  if (def.input && ['POST', 'PUT', 'PATCH'].includes(method)) {
140
- const bodyResult = await ctx.body(def.input);
141
- if (!bodyResult.success) {
142
- return ctx.error('Validation failed', bodyResult.error);
143
+ try {
144
+ await ctx.body(def.input);
145
+ } catch (validationError) {
146
+ return ctx.error(
147
+ 'Validation failed',
148
+ validationError instanceof Error ? validationError : new Error(String(validationError))
149
+ );
143
150
  }
144
151
  }
145
152
 
@@ -247,26 +254,30 @@ export function generateOpenAPIFromIntent(
247
254
  */
248
255
  function zodToJsonSchema(schema: ZodType<unknown>): Record<string, unknown> {
249
256
  // 실제 구현은 zod-to-json-schema 라이브러리 사용 권장
250
- const def = (schema as any)._def;
257
+ const typeName = getZodTypeName(schema as ZodTypeAny);
251
258
 
252
- if (def.typeName === 'ZodString') {
259
+ if (typeName === 'ZodString') {
253
260
  return { type: 'string' };
254
261
  }
255
- if (def.typeName === 'ZodNumber') {
262
+ if (typeName === 'ZodNumber') {
256
263
  return { type: 'number' };
257
264
  }
258
- if (def.typeName === 'ZodBoolean') {
265
+ if (typeName === 'ZodBoolean') {
259
266
  return { type: 'boolean' };
260
267
  }
261
- if (def.typeName === 'ZodObject') {
268
+ if (typeName === 'ZodObject') {
262
269
  const properties: Record<string, unknown> = {};
263
- for (const [key, value] of Object.entries(def.shape())) {
264
- properties[key] = zodToJsonSchema(value as ZodType<unknown>);
270
+ const shape = getZodObjectShape(schema as ZodTypeAny);
271
+ if (shape) {
272
+ for (const [key, value] of Object.entries(shape)) {
273
+ properties[key] = zodToJsonSchema(value as ZodType<unknown>);
274
+ }
265
275
  }
266
276
  return { type: 'object', properties };
267
277
  }
268
- if (def.typeName === 'ZodArray') {
269
- return { type: 'array', items: zodToJsonSchema(def.type) };
278
+ if (typeName === 'ZodArray') {
279
+ const elementType = getZodArrayElementType(schema as ZodTypeAny);
280
+ return { type: 'array', items: elementType ? zodToJsonSchema(elementType as ZodType<unknown>) : {} };
270
281
  }
271
282
 
272
283
  return { type: 'unknown' };
@@ -133,10 +133,10 @@ export function island<P extends Record<string, unknown>>(
133
133
  // isIsland() - Island 컴포넌트 체크
134
134
  // ============================================================================
135
135
 
136
- export function isIsland(component: unknown): component is IslandComponent<unknown> {
136
+ export function isIsland(component: unknown): component is IslandComponent<any> {
137
137
  return (
138
138
  typeof component === 'function' &&
139
- (component as IslandComponent<unknown>).__island === true
139
+ (component as IslandComponent<any>).__island === true
140
140
  );
141
141
  }
142
142
 
@@ -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
 
@@ -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}`);
@@ -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
  // ============================================