@mandujs/core 0.22.0 → 0.23.0
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 +8 -1
- package/src/config/mandu.ts +172 -110
- package/src/config/validate.ts +357 -290
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -0
- package/src/desktop/__tests__/window.test.ts +79 -3
- package/src/desktop/webview-fallback.ts +583 -0
- package/src/desktop/window.ts +527 -492
- package/src/perf/hmr-markers.ts +12 -0
- package/src/runtime/server.ts +133 -8
- package/src/testing/db.ts +157 -0
- package/src/testing/index.ts +59 -1
- package/src/testing/mocks.ts +203 -0
- package/src/testing/server.ts +196 -0
- package/src/testing/session.ts +190 -0
- package/src/testing/snapshot.ts +444 -0
package/src/config/validate.ts
CHANGED
|
@@ -1,290 +1,357 @@
|
|
|
1
|
-
import { z, ZodError, ZodIssueCode } from "zod";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { pathToFileURL } from "url";
|
|
4
|
-
import { CONFIG_FILES, coerceConfig } from "./mandu";
|
|
5
|
-
import { readJsonFile } from "../utils/bun";
|
|
6
|
-
import type { ManduAdapter } from "../runtime/adapter";
|
|
7
|
-
import type { ManduPlugin, ManduHooks } from "../plugins/hooks";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* DNA-003: Strict mode schema helper
|
|
11
|
-
*
|
|
12
|
-
* Creates a schema that warns about unknown keys instead of failing
|
|
13
|
-
* This provides the benefits of .strict() while maintaining compatibility
|
|
14
|
-
*/
|
|
15
|
-
function strictWithWarnings<T extends z.ZodRawShape>(
|
|
16
|
-
schema: z.ZodObject<T>,
|
|
17
|
-
schemaName: string
|
|
18
|
-
): z.ZodEffects<z.ZodObject<T>> {
|
|
19
|
-
return schema.superRefine((data, ctx) => {
|
|
20
|
-
if (typeof data !== "object" || data === null) return;
|
|
21
|
-
|
|
22
|
-
const knownKeys = new Set(Object.keys(schema.shape));
|
|
23
|
-
const unknownKeys = Object.keys(data).filter((key) => !knownKeys.has(key));
|
|
24
|
-
|
|
25
|
-
if (unknownKeys.length > 0 && process.env.MANDU_STRICT !== "0") {
|
|
26
|
-
// In strict mode (default), add warnings to issues
|
|
27
|
-
for (const key of unknownKeys) {
|
|
28
|
-
ctx.addIssue({
|
|
29
|
-
code: ZodIssueCode.unrecognized_keys,
|
|
30
|
-
keys: [key],
|
|
31
|
-
message: `Unknown key '${key}' in ${schemaName}. Did you mean one of: ${[...knownKeys].join(", ")}?`,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Server 설정 스키마 (strict)
|
|
40
|
-
*/
|
|
41
|
-
const ServerConfigSchema = z
|
|
42
|
-
.object({
|
|
43
|
-
port: z.number().min(1).max(65535).default(3000),
|
|
44
|
-
hostname: z.string().default("localhost"),
|
|
45
|
-
cors: z
|
|
46
|
-
.union([
|
|
47
|
-
z.boolean(),
|
|
48
|
-
z.object({
|
|
49
|
-
origin: z.union([z.string(), z.array(z.string())]).optional(),
|
|
50
|
-
methods: z.array(z.string()).optional(),
|
|
51
|
-
credentials: z.boolean().optional(),
|
|
52
|
-
}).strict(),
|
|
53
|
-
])
|
|
54
|
-
.default(false),
|
|
55
|
-
streaming: z.boolean().default(false),
|
|
56
|
-
rateLimit: z
|
|
57
|
-
.union([
|
|
58
|
-
z.boolean(),
|
|
59
|
-
z.object({
|
|
60
|
-
windowMs: z.number().int().positive().optional(),
|
|
61
|
-
max: z.number().int().positive().optional(),
|
|
62
|
-
message: z.string().min(1).optional(),
|
|
63
|
-
statusCode: z.number().int().min(400).max(599).optional(),
|
|
64
|
-
headers: z.boolean().optional(),
|
|
65
|
-
}).strict(),
|
|
66
|
-
])
|
|
67
|
-
.default(false),
|
|
68
|
-
})
|
|
69
|
-
.strict();
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Guard 설정 스키마 (strict)
|
|
73
|
-
*/
|
|
74
|
-
const GuardConfigSchema = z
|
|
75
|
-
.object({
|
|
76
|
-
preset: z.enum(["mandu", "fsd", "clean", "hexagonal", "atomic", "cqrs"]).default("mandu"),
|
|
77
|
-
srcDir: z.string().default("src"),
|
|
78
|
-
exclude: z.array(z.string()).default([]),
|
|
79
|
-
realtime: z.boolean().default(true),
|
|
80
|
-
rules: z.record(z.enum(["error", "warn", "warning", "off"])).optional(),
|
|
81
|
-
})
|
|
82
|
-
.strict();
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Build 설정 스키마 (strict)
|
|
86
|
-
*/
|
|
87
|
-
const BuildConfigSchema = z
|
|
88
|
-
.object({
|
|
89
|
-
outDir: z.string().default(".mandu"),
|
|
90
|
-
minify: z.boolean().default(true),
|
|
91
|
-
sourcemap: z.boolean().default(false),
|
|
92
|
-
splitting: z.boolean().default(false),
|
|
93
|
-
})
|
|
94
|
-
.strict();
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Dev 설정 스키마 (strict)
|
|
98
|
-
*/
|
|
99
|
-
const DevConfigSchema = z
|
|
100
|
-
.object({
|
|
101
|
-
hmr: z.boolean().default(true),
|
|
102
|
-
watchDirs: z.array(z.string()).default([]),
|
|
103
|
-
observability: z.boolean().default(true),
|
|
104
|
-
})
|
|
105
|
-
.strict();
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* FS Routes 설정 스키마 (strict)
|
|
109
|
-
*/
|
|
110
|
-
const FsRoutesConfigSchema = z
|
|
111
|
-
.object({
|
|
112
|
-
routesDir: z.string().default("app"),
|
|
113
|
-
extensions: z.array(z.string()).default([".tsx", ".ts", ".jsx", ".js"]),
|
|
114
|
-
exclude: z.array(z.string()).default([]),
|
|
115
|
-
islandSuffix: z.string().default(".island"),
|
|
116
|
-
})
|
|
117
|
-
.strict();
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* SEO 설정 스키마 (strict)
|
|
121
|
-
*/
|
|
122
|
-
const SeoConfigSchema = z
|
|
123
|
-
.object({
|
|
124
|
-
enabled: z.boolean().default(true),
|
|
125
|
-
defaultTitle: z.string().optional(),
|
|
126
|
-
titleTemplate: z.string().optional(),
|
|
127
|
-
})
|
|
128
|
-
.strict();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
1
|
+
import { z, ZodError, ZodIssueCode } from "zod";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import { CONFIG_FILES, coerceConfig } from "./mandu";
|
|
5
|
+
import { readJsonFile } from "../utils/bun";
|
|
6
|
+
import type { ManduAdapter } from "../runtime/adapter";
|
|
7
|
+
import type { ManduPlugin, ManduHooks } from "../plugins/hooks";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* DNA-003: Strict mode schema helper
|
|
11
|
+
*
|
|
12
|
+
* Creates a schema that warns about unknown keys instead of failing
|
|
13
|
+
* This provides the benefits of .strict() while maintaining compatibility
|
|
14
|
+
*/
|
|
15
|
+
function strictWithWarnings<T extends z.ZodRawShape>(
|
|
16
|
+
schema: z.ZodObject<T>,
|
|
17
|
+
schemaName: string
|
|
18
|
+
): z.ZodEffects<z.ZodObject<T>> {
|
|
19
|
+
return schema.superRefine((data, ctx) => {
|
|
20
|
+
if (typeof data !== "object" || data === null) return;
|
|
21
|
+
|
|
22
|
+
const knownKeys = new Set(Object.keys(schema.shape));
|
|
23
|
+
const unknownKeys = Object.keys(data).filter((key) => !knownKeys.has(key));
|
|
24
|
+
|
|
25
|
+
if (unknownKeys.length > 0 && process.env.MANDU_STRICT !== "0") {
|
|
26
|
+
// In strict mode (default), add warnings to issues
|
|
27
|
+
for (const key of unknownKeys) {
|
|
28
|
+
ctx.addIssue({
|
|
29
|
+
code: ZodIssueCode.unrecognized_keys,
|
|
30
|
+
keys: [key],
|
|
31
|
+
message: `Unknown key '${key}' in ${schemaName}. Did you mean one of: ${[...knownKeys].join(", ")}?`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Server 설정 스키마 (strict)
|
|
40
|
+
*/
|
|
41
|
+
const ServerConfigSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
port: z.number().min(1).max(65535).default(3000),
|
|
44
|
+
hostname: z.string().default("localhost"),
|
|
45
|
+
cors: z
|
|
46
|
+
.union([
|
|
47
|
+
z.boolean(),
|
|
48
|
+
z.object({
|
|
49
|
+
origin: z.union([z.string(), z.array(z.string())]).optional(),
|
|
50
|
+
methods: z.array(z.string()).optional(),
|
|
51
|
+
credentials: z.boolean().optional(),
|
|
52
|
+
}).strict(),
|
|
53
|
+
])
|
|
54
|
+
.default(false),
|
|
55
|
+
streaming: z.boolean().default(false),
|
|
56
|
+
rateLimit: z
|
|
57
|
+
.union([
|
|
58
|
+
z.boolean(),
|
|
59
|
+
z.object({
|
|
60
|
+
windowMs: z.number().int().positive().optional(),
|
|
61
|
+
max: z.number().int().positive().optional(),
|
|
62
|
+
message: z.string().min(1).optional(),
|
|
63
|
+
statusCode: z.number().int().min(400).max(599).optional(),
|
|
64
|
+
headers: z.boolean().optional(),
|
|
65
|
+
}).strict(),
|
|
66
|
+
])
|
|
67
|
+
.default(false),
|
|
68
|
+
})
|
|
69
|
+
.strict();
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Guard 설정 스키마 (strict)
|
|
73
|
+
*/
|
|
74
|
+
const GuardConfigSchema = z
|
|
75
|
+
.object({
|
|
76
|
+
preset: z.enum(["mandu", "fsd", "clean", "hexagonal", "atomic", "cqrs"]).default("mandu"),
|
|
77
|
+
srcDir: z.string().default("src"),
|
|
78
|
+
exclude: z.array(z.string()).default([]),
|
|
79
|
+
realtime: z.boolean().default(true),
|
|
80
|
+
rules: z.record(z.enum(["error", "warn", "warning", "off"])).optional(),
|
|
81
|
+
})
|
|
82
|
+
.strict();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build 설정 스키마 (strict)
|
|
86
|
+
*/
|
|
87
|
+
const BuildConfigSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
outDir: z.string().default(".mandu"),
|
|
90
|
+
minify: z.boolean().default(true),
|
|
91
|
+
sourcemap: z.boolean().default(false),
|
|
92
|
+
splitting: z.boolean().default(false),
|
|
93
|
+
})
|
|
94
|
+
.strict();
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Dev 설정 스키마 (strict)
|
|
98
|
+
*/
|
|
99
|
+
const DevConfigSchema = z
|
|
100
|
+
.object({
|
|
101
|
+
hmr: z.boolean().default(true),
|
|
102
|
+
watchDirs: z.array(z.string()).default([]),
|
|
103
|
+
observability: z.boolean().default(true),
|
|
104
|
+
})
|
|
105
|
+
.strict();
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* FS Routes 설정 스키마 (strict)
|
|
109
|
+
*/
|
|
110
|
+
const FsRoutesConfigSchema = z
|
|
111
|
+
.object({
|
|
112
|
+
routesDir: z.string().default("app"),
|
|
113
|
+
extensions: z.array(z.string()).default([".tsx", ".ts", ".jsx", ".js"]),
|
|
114
|
+
exclude: z.array(z.string()).default([]),
|
|
115
|
+
islandSuffix: z.string().default(".island"),
|
|
116
|
+
})
|
|
117
|
+
.strict();
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* SEO 설정 스키마 (strict)
|
|
121
|
+
*/
|
|
122
|
+
const SeoConfigSchema = z
|
|
123
|
+
.object({
|
|
124
|
+
enabled: z.boolean().default(true),
|
|
125
|
+
defaultTitle: z.string().optional(),
|
|
126
|
+
titleTemplate: z.string().optional(),
|
|
127
|
+
})
|
|
128
|
+
.strict();
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Test 설정 스키마 (Phase 12.1 — strict)
|
|
132
|
+
*
|
|
133
|
+
* `.strict()` is applied at every nested object so stale / misspelt keys
|
|
134
|
+
* are caught at config-load time, not when the CLI trips over them. Each
|
|
135
|
+
* default mirrors the TypeScript documentation in `./mandu.ts`.
|
|
136
|
+
*/
|
|
137
|
+
const TestUnitConfigSchema = z
|
|
138
|
+
.object({
|
|
139
|
+
include: z.array(z.string().min(1)).default(["**/*.test.ts", "**/*.test.tsx"]),
|
|
140
|
+
exclude: z
|
|
141
|
+
.array(z.string().min(1))
|
|
142
|
+
.default(["node_modules/**", ".mandu/**", "dist/**"]),
|
|
143
|
+
timeout: z.number().int().positive().default(30_000),
|
|
144
|
+
})
|
|
145
|
+
.strict();
|
|
146
|
+
|
|
147
|
+
const TestIntegrationConfigSchema = z
|
|
148
|
+
.object({
|
|
149
|
+
include: z
|
|
150
|
+
.array(z.string().min(1))
|
|
151
|
+
.default(["tests/integration/**/*.test.ts", "tests/integration/**/*.test.tsx"]),
|
|
152
|
+
exclude: z
|
|
153
|
+
.array(z.string().min(1))
|
|
154
|
+
.default(["node_modules/**", ".mandu/**", "dist/**"]),
|
|
155
|
+
dbUrl: z.string().min(1).default("sqlite::memory:"),
|
|
156
|
+
sessionStore: z.enum(["memory", "sqlite"]).default("memory"),
|
|
157
|
+
timeout: z.number().int().positive().default(60_000),
|
|
158
|
+
})
|
|
159
|
+
.strict();
|
|
160
|
+
|
|
161
|
+
const TestE2EConfigSchema = z
|
|
162
|
+
.object({
|
|
163
|
+
reserved: z.literal(true).optional(),
|
|
164
|
+
})
|
|
165
|
+
.strict();
|
|
166
|
+
|
|
167
|
+
const TestCoverageConfigSchema = z
|
|
168
|
+
.object({
|
|
169
|
+
lines: z.number().min(0).max(100).optional(),
|
|
170
|
+
branches: z.number().min(0).max(100).optional(),
|
|
171
|
+
})
|
|
172
|
+
.strict();
|
|
173
|
+
|
|
174
|
+
const TestConfigSchema = z
|
|
175
|
+
.object({
|
|
176
|
+
unit: TestUnitConfigSchema.default({}),
|
|
177
|
+
integration: TestIntegrationConfigSchema.default({}),
|
|
178
|
+
e2e: TestE2EConfigSchema.default({}),
|
|
179
|
+
coverage: TestCoverageConfigSchema.default({}),
|
|
180
|
+
})
|
|
181
|
+
.strict();
|
|
182
|
+
|
|
183
|
+
const AdapterConfigSchema = z.custom<ManduAdapter | undefined>(
|
|
184
|
+
(value) =>
|
|
185
|
+
value === undefined ||
|
|
186
|
+
(typeof value === "object" &&
|
|
187
|
+
value !== null &&
|
|
188
|
+
typeof (value as { name?: unknown }).name === "string" &&
|
|
189
|
+
typeof (value as { createServer?: unknown }).createServer === "function"),
|
|
190
|
+
{
|
|
191
|
+
message: "adapter must be a ManduAdapter with name and createServer()",
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Mandu 설정 스키마 (DNA-003: strict mode)
|
|
197
|
+
*
|
|
198
|
+
* 알 수 없는 키가 있으면 오류 발생 → 오타 즉시 감지
|
|
199
|
+
* MANDU_STRICT=0 으로 비활성화 가능
|
|
200
|
+
*/
|
|
201
|
+
/**
|
|
202
|
+
* Plugin schema — array of objects with `name` (string) and optional `hooks`/`setup`.
|
|
203
|
+
* Validated structurally; hook functions are opaque to Zod.
|
|
204
|
+
*/
|
|
205
|
+
const ManduPluginSchema = z.custom<ManduPlugin>(
|
|
206
|
+
(v) =>
|
|
207
|
+
typeof v === "object" &&
|
|
208
|
+
v !== null &&
|
|
209
|
+
typeof (v as { name?: unknown }).name === "string",
|
|
210
|
+
{ message: "Each plugin must be an object with a `name` string" }
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const ManduHooksSchema = z.custom<Partial<ManduHooks>>(
|
|
214
|
+
(v) => typeof v === "object" && v !== null,
|
|
215
|
+
{ message: "hooks must be an object" }
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
export const ManduConfigSchema = z
|
|
219
|
+
.object({
|
|
220
|
+
adapter: AdapterConfigSchema.optional(),
|
|
221
|
+
server: ServerConfigSchema.default({}),
|
|
222
|
+
guard: GuardConfigSchema.default({}),
|
|
223
|
+
build: BuildConfigSchema.default({}),
|
|
224
|
+
dev: DevConfigSchema.default({}),
|
|
225
|
+
fsRoutes: FsRoutesConfigSchema.default({}),
|
|
226
|
+
seo: SeoConfigSchema.default({}),
|
|
227
|
+
test: TestConfigSchema.default({}),
|
|
228
|
+
plugins: z.array(ManduPluginSchema).optional(),
|
|
229
|
+
hooks: ManduHooksSchema.optional(),
|
|
230
|
+
})
|
|
231
|
+
.strict();
|
|
232
|
+
|
|
233
|
+
export type ValidatedManduConfig = z.infer<typeof ManduConfigSchema>;
|
|
234
|
+
|
|
235
|
+
/** Validated `test` block (convenience re-export for fixtures/CLI). */
|
|
236
|
+
export type ValidatedTestConfig = z.infer<typeof TestConfigSchema>;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve the `test` block with defaults filled in.
|
|
240
|
+
*
|
|
241
|
+
* Use this from fixtures / CLI test runners that need a guaranteed-shaped
|
|
242
|
+
* object without having to validate the whole config.
|
|
243
|
+
*/
|
|
244
|
+
export function resolveTestConfig(raw?: unknown): ValidatedTestConfig {
|
|
245
|
+
return TestConfigSchema.parse(raw ?? {});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 검증 결과
|
|
250
|
+
*/
|
|
251
|
+
export interface ValidationResult {
|
|
252
|
+
valid: boolean;
|
|
253
|
+
config?: ValidatedManduConfig;
|
|
254
|
+
errors?: Array<{
|
|
255
|
+
path: string;
|
|
256
|
+
message: string;
|
|
257
|
+
}>;
|
|
258
|
+
source?: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Assertion function: narrows unknown config to ValidatedManduConfig or throws.
|
|
263
|
+
*
|
|
264
|
+
* Useful in code paths that receive untrusted config objects and need
|
|
265
|
+
* to guarantee the type after the call without a separate null-check.
|
|
266
|
+
*/
|
|
267
|
+
export function assertValidConfig(cfg: unknown): asserts cfg is ValidatedManduConfig {
|
|
268
|
+
const result = ManduConfigSchema.safeParse(cfg);
|
|
269
|
+
if (!result.success) {
|
|
270
|
+
const messages = result.error.errors.map(
|
|
271
|
+
(e) => `${e.path.join(".")}: ${e.message}`
|
|
272
|
+
);
|
|
273
|
+
throw new Error(`Invalid ManduConfig:\n ${messages.join("\n ")}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 설정 파일 검증
|
|
279
|
+
*/
|
|
280
|
+
export async function validateConfig(rootDir: string): Promise<ValidationResult> {
|
|
281
|
+
for (const fileName of CONFIG_FILES) {
|
|
282
|
+
const filePath = path.join(rootDir, fileName);
|
|
283
|
+
if (!(await Bun.file(filePath).exists())) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
let raw: unknown;
|
|
289
|
+
if (fileName.endsWith(".json")) {
|
|
290
|
+
raw = await readJsonFile(filePath);
|
|
291
|
+
} else {
|
|
292
|
+
const module = await import(pathToFileURL(filePath).href);
|
|
293
|
+
raw = module?.default ?? module;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const config = ManduConfigSchema.parse(coerceConfig(raw ?? {}, fileName));
|
|
297
|
+
return { valid: true, config, source: fileName };
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error instanceof ZodError) {
|
|
300
|
+
const errors = error.errors.map((e) => ({
|
|
301
|
+
path: e.path.join("."),
|
|
302
|
+
message: e.message,
|
|
303
|
+
}));
|
|
304
|
+
return {
|
|
305
|
+
valid: false,
|
|
306
|
+
errors: [
|
|
307
|
+
{ path: "", message: `Config validation failed in '${filePath}'. Fix the following field errors:` },
|
|
308
|
+
...errors,
|
|
309
|
+
],
|
|
310
|
+
source: fileName,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Differentiate file-not-found from parse/import errors
|
|
315
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
316
|
+
const isModuleError = errMsg.includes("Cannot find module") || errMsg.includes("MODULE_NOT_FOUND");
|
|
317
|
+
const isSyntaxError = error instanceof SyntaxError || errMsg.includes("SyntaxError");
|
|
318
|
+
|
|
319
|
+
let detail: string;
|
|
320
|
+
if (isModuleError) {
|
|
321
|
+
detail = `Could not resolve config file '${filePath}'. Check that the file exists and all its imports are installed.`;
|
|
322
|
+
} else if (isSyntaxError) {
|
|
323
|
+
detail = `Syntax error while parsing '${filePath}': ${errMsg}. Verify the file contains valid TypeScript/JSON.`;
|
|
324
|
+
} else {
|
|
325
|
+
detail = `Failed to load config from '${filePath}': ${errMsg}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
valid: false,
|
|
330
|
+
errors: [{ path: "", message: detail }],
|
|
331
|
+
source: fileName,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 설정 파일 없음 - 기본값 사용
|
|
337
|
+
return { valid: true, config: ManduConfigSchema.parse({}) };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* CLI용 검증 및 리포트
|
|
342
|
+
*/
|
|
343
|
+
export async function validateAndReport(rootDir: string): Promise<ValidatedManduConfig | null> {
|
|
344
|
+
const result = await validateConfig(rootDir);
|
|
345
|
+
|
|
346
|
+
if (!result.valid) {
|
|
347
|
+
console.error(`\n❌ Invalid config${result.source ? ` (${result.source})` : ""}:\n`);
|
|
348
|
+
for (const error of result.errors || []) {
|
|
349
|
+
const location = error.path ? ` ${error.path}: ` : " ";
|
|
350
|
+
console.error(`${location}${error.message}`);
|
|
351
|
+
}
|
|
352
|
+
console.error("");
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return result.config!;
|
|
357
|
+
}
|