@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.
- package/package.json +3 -1
- package/src/brain/architecture/analyzer.ts +3 -5
- package/src/brain/architecture/types.ts +4 -4
- package/src/brain/doctor/analyzer.ts +1 -0
- package/src/brain/doctor/index.ts +1 -1
- package/src/brain/doctor/patcher.ts +10 -6
- package/src/brain/doctor/reporter.ts +4 -4
- package/src/brain/types.ts +14 -10
- package/src/bundler/build.ts +17 -17
- package/src/bundler/css.ts +3 -2
- package/src/bundler/dev.ts +1 -1
- package/src/client/island.ts +10 -9
- package/src/client/router.ts +1 -1
- package/src/config/mcp-ref.ts +6 -6
- package/src/config/metadata.test.ts +1 -1
- package/src/config/metadata.ts +36 -16
- package/src/config/symbols.ts +1 -1
- package/src/config/validate.ts +17 -1
- package/src/content/content.test.ts +3 -3
- package/src/content/loaders/file.ts +3 -0
- package/src/content/loaders/glob.ts +1 -0
- package/src/contract/client-safe.test.ts +1 -1
- package/src/contract/client.test.ts +2 -1
- package/src/contract/client.ts +18 -18
- package/src/contract/define.ts +32 -17
- package/src/contract/handler.ts +11 -11
- package/src/contract/index.ts +2 -5
- package/src/contract/infer.test.ts +2 -1
- package/src/contract/normalize.test.ts +1 -1
- package/src/contract/normalize.ts +17 -11
- package/src/contract/registry.test.ts +1 -1
- package/src/contract/zod-utils.ts +155 -0
- package/src/devtools/client/catchers/error-catcher.ts +3 -3
- package/src/devtools/client/catchers/network-proxy.ts +5 -1
- package/src/devtools/client/components/kitchen-root.tsx +2 -2
- package/src/devtools/client/components/panel/guard-panel.tsx +3 -3
- package/src/devtools/client/state-manager.ts +9 -9
- package/src/devtools/index.ts +8 -8
- package/src/devtools/init.ts +2 -2
- package/src/devtools/protocol.ts +4 -4
- package/src/devtools/server/source-context.ts +9 -3
- package/src/devtools/types.ts +5 -5
- package/src/devtools/worker/redaction-worker.ts +12 -5
- package/src/error/index.ts +1 -1
- package/src/error/result.ts +14 -0
- package/src/filling/deps.ts +5 -2
- package/src/filling/filling.ts +1 -1
- package/src/generator/templates.ts +2 -2
- package/src/guard/contract-guard.test.ts +1 -0
- package/src/guard/file-type.test.ts +1 -1
- package/src/guard/index.ts +1 -1
- package/src/guard/negotiation.ts +29 -1
- package/src/guard/presets/index.ts +3 -0
- package/src/guard/semantic-slots.ts +4 -4
- package/src/index.ts +10 -1
- package/src/intent/index.ts +28 -17
- package/src/island/index.ts +8 -8
- package/src/openapi/generator.ts +49 -31
- package/src/plugins/index.ts +1 -1
- package/src/plugins/registry.ts +28 -18
- package/src/plugins/types.ts +2 -2
- package/src/resource/__tests__/backward-compat.test.ts +2 -2
- package/src/resource/__tests__/edge-cases.test.ts +14 -13
- package/src/resource/__tests__/fixtures.ts +2 -2
- package/src/resource/__tests__/generator.test.ts +1 -1
- package/src/resource/__tests__/performance.test.ts +8 -6
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-routes.ts +34 -40
- package/src/router/fs-types.ts +2 -2
- package/src/router/index.ts +1 -1
- package/src/runtime/boundary.tsx +4 -4
- package/src/runtime/logger.test.ts +3 -3
- package/src/runtime/logger.ts +1 -1
- package/src/runtime/server.ts +18 -16
- package/src/runtime/ssr.ts +1 -1
- package/src/runtime/stable-selector.ts +1 -2
- package/src/runtime/streaming-ssr.ts +15 -6
- package/src/seo/index.ts +5 -0
- package/src/seo/integration/ssr.ts +4 -4
- package/src/seo/render/basic.ts +12 -4
- package/src/seo/render/opengraph.ts +12 -6
- package/src/seo/render/twitter.ts +3 -2
- package/src/seo/resolve/url.ts +7 -0
- package/src/seo/types.ts +13 -0
- package/src/spec/schema.ts +89 -61
- package/src/types/branded.ts +56 -0
- package/src/types/index.ts +1 -0
- package/src/utils/hasher.test.ts +6 -6
- package/src/utils/hasher.ts +2 -2
- package/src/utils/index.ts +1 -1
- package/src/watcher/watcher.ts +2 -2
package/src/openapi/generator.ts
CHANGED
|
@@ -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
|
|
141
|
+
const typeName = getZodTypeName(zodSchema);
|
|
129
142
|
|
|
130
143
|
// Handle ZodOptional
|
|
131
|
-
if (
|
|
132
|
-
const
|
|
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 (
|
|
138
|
-
const
|
|
139
|
-
|
|
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 (
|
|
144
|
-
const
|
|
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 (
|
|
150
|
-
|
|
165
|
+
if (typeName === "ZodEffects") {
|
|
166
|
+
const effectsSchema = getZodEffectsSchema(zodSchema);
|
|
167
|
+
return effectsSchema ? zodToOpenAPISchema(effectsSchema) : {};
|
|
151
168
|
}
|
|
152
169
|
|
|
153
170
|
// Handle ZodString
|
|
154
|
-
if (
|
|
171
|
+
if (typeName === "ZodString") {
|
|
155
172
|
const schema: OpenAPISchema = { type: "string" };
|
|
156
|
-
for (const check of
|
|
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
|
|
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 (
|
|
186
|
+
if (typeName === "ZodNumber") {
|
|
170
187
|
const schema: OpenAPISchema = { type: "number" };
|
|
171
|
-
for (const check of
|
|
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 (
|
|
197
|
+
if (typeName === "ZodBoolean") {
|
|
181
198
|
return { type: "boolean" };
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
// Handle ZodArray
|
|
185
|
-
if (
|
|
202
|
+
if (typeName === "ZodArray") {
|
|
203
|
+
const elementType = getZodArrayElementType(zodSchema);
|
|
186
204
|
return {
|
|
187
205
|
type: "array",
|
|
188
|
-
items: zodToOpenAPISchema(
|
|
206
|
+
items: elementType ? zodToOpenAPISchema(elementType) : {},
|
|
189
207
|
};
|
|
190
208
|
}
|
|
191
209
|
|
|
192
210
|
// Handle ZodObject
|
|
193
|
-
if (
|
|
211
|
+
if (typeName === "ZodObject") {
|
|
194
212
|
const properties: Record<string, OpenAPISchema> = {};
|
|
195
213
|
const required: string[] = [];
|
|
196
214
|
|
|
197
|
-
const shape =
|
|
215
|
+
const shape = getZodObjectShape(zodSchema) ?? {};
|
|
198
216
|
for (const [key, value] of Object.entries(shape)) {
|
|
199
|
-
properties[key] = zodToOpenAPISchema(value
|
|
217
|
+
properties[key] = zodToOpenAPISchema(value);
|
|
200
218
|
|
|
201
219
|
// Check if field is required
|
|
202
|
-
|
|
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 (
|
|
233
|
+
if (typeName === "ZodEnum") {
|
|
217
234
|
return {
|
|
218
235
|
type: "string",
|
|
219
|
-
enum:
|
|
236
|
+
enum: getZodEnumValues(zodSchema) as unknown[],
|
|
220
237
|
};
|
|
221
238
|
}
|
|
222
239
|
|
|
223
240
|
// Handle ZodUnion
|
|
224
|
-
if (
|
|
241
|
+
if (typeName === "ZodUnion") {
|
|
242
|
+
const options = getZodUnionOptions(zodSchema) ?? [];
|
|
225
243
|
return {
|
|
226
|
-
oneOf:
|
|
244
|
+
oneOf: options.map((opt) => zodToOpenAPISchema(opt)),
|
|
227
245
|
};
|
|
228
246
|
}
|
|
229
247
|
|
|
230
248
|
// Handle ZodLiteral
|
|
231
|
-
if (
|
|
232
|
-
const 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 (
|
|
258
|
+
if (typeName === "ZodVoid" || typeName === "ZodUndefined") {
|
|
241
259
|
return {};
|
|
242
260
|
}
|
|
243
261
|
|
|
244
262
|
// Handle ZodAny/ZodUnknown
|
|
245
|
-
if (
|
|
263
|
+
if (typeName === "ZodAny" || typeName === "ZodUnknown") {
|
|
246
264
|
return {};
|
|
247
265
|
}
|
|
248
266
|
|
package/src/plugins/index.ts
CHANGED
package/src/plugins/registry.ts
CHANGED
|
@@ -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
|
-
|
|
31
|
-
plugin: Plugin<TConfig>;
|
|
32
|
-
state:
|
|
33
|
-
config?: TConfig;
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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}`);
|
package/src/plugins/types.ts
CHANGED
|
@@ -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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
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,
|
|
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],
|
package/src/resource/schema.ts
CHANGED
package/src/router/fs-routes.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { loadManduConfig } from "../config";
|
|
|
21
21
|
/**
|
|
22
22
|
* 매니페스트 생성 결과
|
|
23
23
|
*/
|
|
24
|
-
export interface
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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<
|
|
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:
|
|
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<
|
|
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<
|
|
278
|
+
const triggerRescan = async (): Promise<FSGenerateResult> => {
|
|
285
279
|
const result = await generateManifest(rootDir, generateOptions);
|
|
286
280
|
if (onChange) {
|
|
287
281
|
await onChange(result);
|
package/src/router/fs-types.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module router/fs-types
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { RouteKind, HydrationConfig,
|
|
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?:
|
|
96
|
+
methods?: SpecHttpMethod[];
|
|
97
97
|
|
|
98
98
|
/** 페이지 컴포넌트 모듈 경로 */
|
|
99
99
|
componentModule?: string;
|
package/src/router/index.ts
CHANGED
|
@@ -69,7 +69,7 @@ export {
|
|
|
69
69
|
export { FSScanner, createFSScanner, scanRoutes } from "./fs-scanner";
|
|
70
70
|
|
|
71
71
|
// Generator
|
|
72
|
-
export type {
|
|
72
|
+
export type { FSGenerateResult, GenerateOptions, RouteChangeCallback, FSRoutesWatcher } from "./fs-routes";
|
|
73
73
|
|
|
74
74
|
export {
|
|
75
75
|
fsRouteToRouteSpec,
|