nestjs-openapi 0.1.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.
@@ -0,0 +1,1831 @@
1
+ import { Schema, Effect, Logger, Layer, LogLevel } from 'effect';
2
+ import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'node:fs';
3
+ import { join, dirname, resolve } from 'node:path';
4
+ import { Project } from 'ts-morph';
5
+ import { globSync, glob } from 'glob';
6
+ import yaml from 'js-yaml';
7
+ import { C as ConfigNotFoundError, a as ConfigLoadError, b as ConfigValidationError, p as transformMethods, A as extractClassConstraints, B as getRequiredProperties, D as mergeValidationConstraints, E as EntryNotFoundError, g as getModules, o as getControllerMethodInfos } from './nestjs-openapi.B1bBy_tG.mjs';
8
+ import { createGenerator } from 'ts-json-schema-generator';
9
+ import { randomUUID } from 'node:crypto';
10
+ import { pathToFileURL } from 'node:url';
11
+ import { execSync } from 'child_process';
12
+
13
+ const buildFlow = (flow, includeAuthUrl, includeTokenUrl) => ({
14
+ ...includeAuthUrl && flow.authorizationUrl && { authorizationUrl: flow.authorizationUrl },
15
+ ...includeTokenUrl && flow.tokenUrl && { tokenUrl: flow.tokenUrl },
16
+ ...flow.refreshUrl && { refreshUrl: flow.refreshUrl },
17
+ scopes: flow.scopes ?? {}
18
+ });
19
+ const buildOAuth2Flows = (flows) => ({
20
+ ...flows.implicit && { implicit: buildFlow(flows.implicit, true, false) },
21
+ ...flows.password && { password: buildFlow(flows.password, false, true) },
22
+ ...flows.clientCredentials && {
23
+ clientCredentials: buildFlow(flows.clientCredentials, false, true)
24
+ },
25
+ ...flows.authorizationCode && {
26
+ authorizationCode: buildFlow(flows.authorizationCode, true, true)
27
+ }
28
+ });
29
+ const transformSecurityScheme = (config) => {
30
+ const scheme = {
31
+ type: config.type,
32
+ ...config.description && { description: config.description }
33
+ };
34
+ switch (config.type) {
35
+ case "http":
36
+ return {
37
+ ...scheme,
38
+ ...config.scheme && { scheme: config.scheme },
39
+ ...config.bearerFormat && { bearerFormat: config.bearerFormat }
40
+ };
41
+ case "apiKey":
42
+ return {
43
+ ...scheme,
44
+ ...config.in && { in: config.in },
45
+ ...config.parameterName && { name: config.parameterName }
46
+ };
47
+ case "oauth2":
48
+ if (!config.flows) return scheme;
49
+ return { ...scheme, flows: buildOAuth2Flows(config.flows) };
50
+ case "openIdConnect":
51
+ return {
52
+ ...scheme,
53
+ ...config.openIdConnectUrl && {
54
+ openIdConnectUrl: config.openIdConnectUrl
55
+ }
56
+ };
57
+ default:
58
+ return scheme;
59
+ }
60
+ };
61
+ const buildSecuritySchemes = (configs) => {
62
+ const schemes = {};
63
+ for (const config of configs) {
64
+ schemes[config.name] = transformSecurityScheme(config);
65
+ }
66
+ return schemes;
67
+ };
68
+
69
+ class SchemaGenerationError extends Schema.TaggedError()(
70
+ "SchemaGenerationError",
71
+ {
72
+ message: Schema.String,
73
+ cause: Schema.optional(Schema.Unknown)
74
+ }
75
+ ) {
76
+ static fromError(error, context) {
77
+ const baseMessage = error instanceof Error ? error.message : "Unknown schema generation error";
78
+ const message = context ? `${baseMessage} (${context})` : baseMessage;
79
+ return new SchemaGenerationError({
80
+ message,
81
+ cause: error
82
+ });
83
+ }
84
+ static noFilesFound(patterns) {
85
+ return new SchemaGenerationError({
86
+ message: `No DTO files found matching patterns: ${patterns.join(", ")}`
87
+ });
88
+ }
89
+ }
90
+ const generateSchemas = Effect.fn("SchemaGenerator.generate")(function* (options) {
91
+ yield* Effect.logDebug("Starting schema generation").pipe(
92
+ Effect.annotateLogs({
93
+ dtoGlob: options.dtoGlob.join(", "),
94
+ tsconfig: options.tsconfig
95
+ })
96
+ );
97
+ const patterns = options.dtoGlob.map((pattern) => ({
98
+ pattern,
99
+ combinable: !pattern.startsWith("/") && !pattern.includes("..")
100
+ }));
101
+ const combinable = patterns.filter((p) => p.combinable);
102
+ const nonCombinable = patterns.filter((p) => !p.combinable);
103
+ const groupedPatterns = [];
104
+ if (combinable.length > 0) {
105
+ groupedPatterns.push(
106
+ combinable.length === 1 ? combinable[0].pattern : `{${combinable.map((p) => p.pattern).join(",")}}`
107
+ );
108
+ }
109
+ groupedPatterns.push(...nonCombinable.map((p) => p.pattern));
110
+ const schemaResults = yield* Effect.all(
111
+ groupedPatterns.map(
112
+ (pattern) => generateSchemasFromGlob(pattern, options.tsconfig, options.basePath)
113
+ ),
114
+ { concurrency: "unbounded" }
115
+ );
116
+ const allDefinitions = schemaResults.reduce(
117
+ (acc, schemas) => ({ ...acc, ...schemas.definitions }),
118
+ {}
119
+ );
120
+ yield* Effect.logDebug("Schema generation complete").pipe(
121
+ Effect.annotateLogs({
122
+ definitionCount: Object.keys(allDefinitions).length
123
+ })
124
+ );
125
+ return { definitions: allDefinitions };
126
+ });
127
+ const generateSchemasFromGlob = Effect.fn("SchemaGenerator.generateFromGlob")(
128
+ function* (pattern, tsconfig, basePath) {
129
+ const isBraceAbsolute = pattern.startsWith("{") && pattern.slice(1, -1).split(",").map((entry) => entry.trim()).every((entry) => entry.startsWith("/"));
130
+ const absolutePattern = pattern.startsWith("/") || isBraceAbsolute ? pattern : join(basePath, pattern);
131
+ yield* Effect.annotateCurrentSpan("pattern", absolutePattern);
132
+ const matchedFiles = globSync(absolutePattern);
133
+ if (matchedFiles.length === 0) {
134
+ yield* Effect.logDebug("No files matched pattern, skipping").pipe(
135
+ Effect.annotateLogs({ pattern: absolutePattern })
136
+ );
137
+ return { definitions: {} };
138
+ }
139
+ return yield* Effect.try({
140
+ try: () => {
141
+ const config = {
142
+ path: absolutePattern,
143
+ tsconfig,
144
+ type: "*",
145
+ // Generate schemas for all exported types
146
+ skipTypeCheck: true,
147
+ // Note: topRef must NOT be set to false, as it prevents interface schemas from being generated
148
+ expose: "export",
149
+ // Only export explicitly exported types
150
+ jsDoc: "extended",
151
+ // Include JSDoc comments
152
+ sortProps: true,
153
+ strictTuples: false,
154
+ encodeRefs: false,
155
+ additionalProperties: false
156
+ };
157
+ const generator = createGenerator(config);
158
+ const schema = generator.createSchema(config.type);
159
+ return convertToGeneratedSchemas(schema);
160
+ },
161
+ catch: (error) => SchemaGenerationError.fromError(error, `pattern: ${pattern}`)
162
+ });
163
+ }
164
+ );
165
+ const convertToGeneratedSchemas = (schema) => {
166
+ const definitions = {};
167
+ const defs = schema.$defs ?? schema.definitions ?? {};
168
+ for (const [name, def] of Object.entries(defs)) {
169
+ definitions[name] = def;
170
+ }
171
+ return { definitions };
172
+ };
173
+ const generateSchemasFromFiles = Effect.fn(
174
+ "SchemaGenerator.generateFromFiles"
175
+ )(function* (filePaths, tsconfig) {
176
+ if (filePaths.length === 0) {
177
+ return { definitions: {} };
178
+ }
179
+ yield* Effect.logDebug("Generating schemas from resolved files").pipe(
180
+ Effect.annotateLogs({
181
+ fileCount: filePaths.length
182
+ })
183
+ );
184
+ const batchedResult = yield* generateSchemasWithTempTsconfig(
185
+ filePaths,
186
+ tsconfig
187
+ ).pipe(
188
+ Effect.catchTag(
189
+ "SchemaGenerationError",
190
+ () => generateSchemasFromFilesIndividual(filePaths, tsconfig)
191
+ )
192
+ );
193
+ yield* Effect.logDebug("Additional schema generation complete").pipe(
194
+ Effect.annotateLogs({
195
+ definitionCount: Object.keys(batchedResult.definitions).length
196
+ })
197
+ );
198
+ return batchedResult;
199
+ });
200
+ const generateSchemasWithTempTsconfig = (filePaths, tsconfig) => Effect.try({
201
+ try: () => {
202
+ const originalConfig = JSON.parse(readFileSync(tsconfig, "utf-8"));
203
+ const tempConfig = {
204
+ ...originalConfig,
205
+ compilerOptions: {
206
+ ...originalConfig.compilerOptions,
207
+ skipLibCheck: true,
208
+ skipDefaultLibCheck: true,
209
+ noEmit: true
210
+ },
211
+ files: [...filePaths],
212
+ // Prevent loading other files from the project
213
+ include: void 0,
214
+ exclude: void 0
215
+ };
216
+ const tempTsconfigPath = join(
217
+ dirname(tsconfig),
218
+ `.tsconfig.schema-gen.${randomUUID()}.json`
219
+ );
220
+ try {
221
+ writeFileSync(tempTsconfigPath, JSON.stringify(tempConfig, null, 2));
222
+ const pattern = filePaths.length === 1 ? filePaths[0] : `{${filePaths.join(",")}}`;
223
+ const config = {
224
+ path: pattern,
225
+ tsconfig: tempTsconfigPath,
226
+ type: "*",
227
+ skipTypeCheck: true,
228
+ expose: "export",
229
+ jsDoc: "extended",
230
+ sortProps: true,
231
+ strictTuples: false,
232
+ encodeRefs: false,
233
+ additionalProperties: false
234
+ };
235
+ const generator = createGenerator(config);
236
+ const schema = generator.createSchema(config.type);
237
+ return convertToGeneratedSchemas(schema);
238
+ } finally {
239
+ if (existsSync(tempTsconfigPath)) {
240
+ unlinkSync(tempTsconfigPath);
241
+ }
242
+ }
243
+ },
244
+ catch: (error) => SchemaGenerationError.fromError(
245
+ error,
246
+ `files: ${filePaths.slice(0, 3).join(", ")}${filePaths.length > 3 ? ` (+${filePaths.length - 3} more)` : ""}`
247
+ )
248
+ });
249
+ const generateSchemasFromFilesIndividual = Effect.fn(
250
+ "SchemaGenerator.generateFromFilesIndividual"
251
+ )(function* (filePaths, tsconfig) {
252
+ const schemaResults = yield* Effect.all(
253
+ filePaths.map(
254
+ (filePath) => generateSchemasFromFile(filePath, tsconfig).pipe(
255
+ // Intentional: continue even if individual files fail
256
+ Effect.catchTag(
257
+ "SchemaGenerationError",
258
+ () => Effect.succeed({ definitions: {} })
259
+ )
260
+ )
261
+ ),
262
+ { concurrency: "unbounded" }
263
+ );
264
+ const allDefinitions = schemaResults.reduce(
265
+ (acc, schemas) => ({ ...acc, ...schemas.definitions }),
266
+ {}
267
+ );
268
+ return { definitions: allDefinitions };
269
+ });
270
+ const generateSchemasFromFile = Effect.fn("SchemaGenerator.generateFromFile")(
271
+ function* (filePath, tsconfig) {
272
+ return yield* Effect.try({
273
+ try: () => {
274
+ const config = {
275
+ path: filePath,
276
+ tsconfig,
277
+ type: "*",
278
+ skipTypeCheck: true,
279
+ expose: "export",
280
+ jsDoc: "extended",
281
+ sortProps: true,
282
+ strictTuples: false,
283
+ encodeRefs: false,
284
+ additionalProperties: false
285
+ };
286
+ const generator = createGenerator(config);
287
+ const schema = generator.createSchema(config.type);
288
+ return convertToGeneratedSchemas(schema);
289
+ },
290
+ catch: (error) => SchemaGenerationError.fromError(error, `file: ${filePath}`)
291
+ });
292
+ }
293
+ );
294
+
295
+ const UGLY_REF_PATTERN = /^(?:structure|class)-\d+(-\d+)*$/;
296
+ const CONTAINS_UGLY_REF_PATTERN = /(?:structure|class)-\d+(-\d+)*/;
297
+ const STRUCTURE_REF_PATTERN = UGLY_REF_PATTERN;
298
+ const CONTAINS_STRUCTURE_REF_PATTERN = CONTAINS_UGLY_REF_PATTERN;
299
+ const containsUglyStructureRef = (name) => {
300
+ const decoded = decodeURIComponent(name);
301
+ return CONTAINS_STRUCTURE_REF_PATTERN.test(decoded);
302
+ };
303
+ const traverseSchema = (schema, transform) => {
304
+ const updated = { ...transform(schema) };
305
+ if (schema.properties) {
306
+ updated.properties = Object.fromEntries(
307
+ Object.entries(schema.properties).map(([key, value]) => [
308
+ key,
309
+ traverseSchema(value, transform)
310
+ ])
311
+ );
312
+ }
313
+ if (schema.items) {
314
+ updated.items = traverseSchema(schema.items, transform);
315
+ }
316
+ if (schema.oneOf) {
317
+ updated.oneOf = schema.oneOf.map((s) => traverseSchema(s, transform));
318
+ }
319
+ if (schema.anyOf) {
320
+ updated.anyOf = schema.anyOf.map((s) => traverseSchema(s, transform));
321
+ }
322
+ if (schema.allOf) {
323
+ updated.allOf = schema.allOf.map((s) => traverseSchema(s, transform));
324
+ }
325
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
326
+ updated.additionalProperties = traverseSchema(
327
+ schema.additionalProperties,
328
+ transform
329
+ );
330
+ }
331
+ return updated;
332
+ };
333
+ const extractRefName = (ref) => {
334
+ const match = ref.match(/^#\/(?:definitions|components\/schemas)\/(.+)$/);
335
+ return match ? match[1] : ref;
336
+ };
337
+ const toPascalCase = (str) => {
338
+ if (!str) return str;
339
+ return str.charAt(0).toUpperCase() + str.slice(1);
340
+ };
341
+ const extractStructureRef = (name) => {
342
+ if (STRUCTURE_REF_PATTERN.test(name)) {
343
+ return name;
344
+ }
345
+ const match = CONTAINS_STRUCTURE_REF_PATTERN.exec(name);
346
+ return match ? match[0] : null;
347
+ };
348
+ const findStructureUsages = (schemas) => {
349
+ const usages = /* @__PURE__ */ new Map();
350
+ const recordUsage = (structureRef, parent, property, fullRef) => {
351
+ const isWrapped = fullRef !== structureRef;
352
+ const wrapper = isWrapped ? fullRef.match(/^([^<]+)</)?.[1] : void 0;
353
+ const usage = {
354
+ parent,
355
+ property,
356
+ fullRef,
357
+ isWrapped,
358
+ wrapper
359
+ };
360
+ const existing = usages.get(structureRef) ?? [];
361
+ existing.push(usage);
362
+ usages.set(structureRef, existing);
363
+ };
364
+ for (const [schemaName, schema] of Object.entries(schemas.definitions)) {
365
+ if (STRUCTURE_REF_PATTERN.test(schemaName)) {
366
+ continue;
367
+ }
368
+ if (schema.properties) {
369
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
370
+ if (propSchema.$ref) {
371
+ const refName = extractRefName(propSchema.$ref);
372
+ const structureRef = extractStructureRef(refName);
373
+ if (structureRef) {
374
+ recordUsage(structureRef, schemaName, propName, refName);
375
+ }
376
+ }
377
+ if (propSchema.items?.$ref) {
378
+ const refName = extractRefName(propSchema.items.$ref);
379
+ const structureRef = extractStructureRef(refName);
380
+ if (structureRef) {
381
+ recordUsage(structureRef, schemaName, propName, refName);
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+ return usages;
388
+ };
389
+ const replaceStructureInName = (name, structureRef, newName) => {
390
+ return name.replace(structureRef, newName);
391
+ };
392
+ const updateRefsWithMapping = (schema, structureMapping) => traverseSchema(schema, (s) => {
393
+ if (!s.$ref) return s;
394
+ let refName = extractRefName(s.$ref);
395
+ for (const [structureRef, newName] of structureMapping) {
396
+ if (refName.includes(structureRef)) {
397
+ refName = replaceStructureInName(refName, structureRef, newName);
398
+ }
399
+ }
400
+ return { ...s, $ref: `#/components/schemas/${refName}` };
401
+ });
402
+ const findUniqueName = (propertyName, parentName, usedNames) => {
403
+ if (!usedNames.has(propertyName)) return propertyName;
404
+ const withParent = `${parentName}${propertyName}`;
405
+ if (!usedNames.has(withParent)) return withParent;
406
+ let suffix = 1;
407
+ while (usedNames.has(`${withParent}_${suffix}`)) {
408
+ suffix++;
409
+ }
410
+ return `${withParent}_${suffix}`;
411
+ };
412
+ const normalizeStructureRefs = (schemas) => {
413
+ const reservedNames = /* @__PURE__ */ new Set();
414
+ for (const name of Object.keys(schemas.definitions)) {
415
+ if (!STRUCTURE_REF_PATTERN.test(name) && !containsUglyStructureRef(name)) {
416
+ reservedNames.add(name);
417
+ }
418
+ }
419
+ const structureUsages = findStructureUsages(schemas);
420
+ const structureMapping = /* @__PURE__ */ new Map();
421
+ const usedNames = new Set(reservedNames);
422
+ for (const [structureRef, usages] of structureUsages) {
423
+ if (usages.length === 0) continue;
424
+ const { parent, property } = usages[0];
425
+ const propertyPascal = toPascalCase(property);
426
+ const newName = findUniqueName(propertyPascal, parent, usedNames);
427
+ structureMapping.set(structureRef, newName);
428
+ usedNames.add(newName);
429
+ }
430
+ const normalizedDefinitions = {};
431
+ for (const [originalName, schema] of Object.entries(schemas.definitions)) {
432
+ const newName = resolveNewSchemaName(originalName, structureMapping);
433
+ if (newName === null) continue;
434
+ const updatedSchema = updateRefsWithMapping(schema, structureMapping);
435
+ normalizedDefinitions[newName] = updatedSchema;
436
+ }
437
+ return { definitions: normalizedDefinitions };
438
+ };
439
+ const resolveNewSchemaName = (originalName, structureMapping) => {
440
+ if (STRUCTURE_REF_PATTERN.test(originalName)) {
441
+ return structureMapping.get(originalName) ?? null;
442
+ }
443
+ if (containsUglyStructureRef(originalName)) {
444
+ for (const [structureRef, mappedName] of structureMapping) {
445
+ if (originalName.includes(structureRef)) {
446
+ return replaceStructureInName(originalName, structureRef, mappedName);
447
+ }
448
+ }
449
+ }
450
+ return originalName;
451
+ };
452
+
453
+ const extractReferencedSchemas = (paths) => {
454
+ const refs = /* @__PURE__ */ new Set();
455
+ const extractFromSchema = (schema) => {
456
+ if (!schema) return;
457
+ if (schema.$ref) {
458
+ const match = schema.$ref.match(/^#\/components\/schemas\/(.+)$/);
459
+ if (match) {
460
+ refs.add(match[1]);
461
+ }
462
+ }
463
+ if (schema.items) {
464
+ extractFromSchema(schema.items);
465
+ }
466
+ if (schema.oneOf) {
467
+ schema.oneOf.forEach(extractFromSchema);
468
+ }
469
+ if (schema.properties) {
470
+ Object.values(schema.properties).forEach(extractFromSchema);
471
+ }
472
+ };
473
+ for (const pathMethods of Object.values(paths)) {
474
+ for (const operation of Object.values(pathMethods)) {
475
+ operation.parameters?.forEach((param) => {
476
+ extractFromSchema(param.schema);
477
+ });
478
+ if (operation.requestBody?.content) {
479
+ Object.values(operation.requestBody.content).forEach((content) => {
480
+ extractFromSchema(content.schema);
481
+ });
482
+ }
483
+ Object.values(operation.responses).forEach((response) => {
484
+ if (response.content) {
485
+ Object.values(response.content).forEach((content) => {
486
+ extractFromSchema(content.schema);
487
+ });
488
+ }
489
+ });
490
+ }
491
+ }
492
+ return refs;
493
+ };
494
+ const extractNestedReferences = (schemas, knownSchemas) => {
495
+ const refs = /* @__PURE__ */ new Set();
496
+ const extractFromSchema = (schema) => {
497
+ if (!schema) return;
498
+ if (schema.$ref) {
499
+ const match = schema.$ref.match(
500
+ /^#\/(?:components\/schemas|definitions)\/(.+)$/
501
+ );
502
+ if (match && !knownSchemas.has(match[1])) {
503
+ refs.add(match[1]);
504
+ }
505
+ }
506
+ if (schema.items) {
507
+ extractFromSchema(schema.items);
508
+ }
509
+ if (schema.oneOf) {
510
+ schema.oneOf.forEach(extractFromSchema);
511
+ }
512
+ if (schema.anyOf) {
513
+ schema.anyOf.forEach(extractFromSchema);
514
+ }
515
+ if (schema.allOf) {
516
+ schema.allOf.forEach(extractFromSchema);
517
+ }
518
+ if (schema.properties) {
519
+ Object.values(schema.properties).forEach(extractFromSchema);
520
+ }
521
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
522
+ extractFromSchema(schema.additionalProperties);
523
+ }
524
+ };
525
+ for (const schema of Object.values(schemas)) {
526
+ extractFromSchema(schema);
527
+ }
528
+ return refs;
529
+ };
530
+ const convertToOpenApiSchema = (schema) => {
531
+ const result = {};
532
+ if (schema.type) result["type"] = schema.type;
533
+ if (schema.format) result["format"] = schema.format;
534
+ if (schema.$ref) {
535
+ result["$ref"] = schema.$ref.replace(
536
+ "#/definitions/",
537
+ "#/components/schemas/"
538
+ );
539
+ }
540
+ if (schema.description) result["description"] = schema.description;
541
+ if (schema.enum) result["enum"] = schema.enum;
542
+ if (schema.const !== void 0) result["const"] = schema.const;
543
+ if (schema.minLength !== void 0) result["minLength"] = schema.minLength;
544
+ if (schema.maxLength !== void 0) result["maxLength"] = schema.maxLength;
545
+ if (schema.pattern !== void 0) result["pattern"] = schema.pattern;
546
+ if (schema.minimum !== void 0) result["minimum"] = schema.minimum;
547
+ if (schema.maximum !== void 0) result["maximum"] = schema.maximum;
548
+ if (schema.exclusiveMinimum !== void 0)
549
+ result["exclusiveMinimum"] = schema.exclusiveMinimum;
550
+ if (schema.exclusiveMaximum !== void 0)
551
+ result["exclusiveMaximum"] = schema.exclusiveMaximum;
552
+ if (schema.minItems !== void 0) result["minItems"] = schema.minItems;
553
+ if (schema.maxItems !== void 0) result["maxItems"] = schema.maxItems;
554
+ if (schema.default !== void 0) result["default"] = schema.default;
555
+ if (schema.items) {
556
+ result["items"] = convertToOpenApiSchema(schema.items);
557
+ }
558
+ if (schema.oneOf) {
559
+ result["oneOf"] = schema.oneOf.map(convertToOpenApiSchema);
560
+ }
561
+ if (schema.anyOf) {
562
+ result["anyOf"] = schema.anyOf.map(convertToOpenApiSchema);
563
+ }
564
+ if (schema.allOf) {
565
+ result["allOf"] = schema.allOf.map(convertToOpenApiSchema);
566
+ }
567
+ if (schema.properties) {
568
+ result["properties"] = Object.fromEntries(
569
+ Object.entries(schema.properties).map(([key, value]) => [
570
+ key,
571
+ convertToOpenApiSchema(value)
572
+ ])
573
+ );
574
+ if (schema.additionalProperties === void 0) {
575
+ result["additionalProperties"] = false;
576
+ }
577
+ }
578
+ if (schema.additionalProperties !== void 0) {
579
+ if (typeof schema.additionalProperties === "boolean") {
580
+ result["additionalProperties"] = schema.additionalProperties;
581
+ } else {
582
+ result["additionalProperties"] = convertToOpenApiSchema(
583
+ schema.additionalProperties
584
+ );
585
+ }
586
+ }
587
+ if (schema.required) {
588
+ result["required"] = [...schema.required];
589
+ }
590
+ return result;
591
+ };
592
+ const mergeSchemas = (paths, generatedSchemas) => {
593
+ const referencedSchemas = extractReferencedSchemas(paths);
594
+ const schemas = {};
595
+ const processedSchemas = /* @__PURE__ */ new Set();
596
+ const toProcess = [...referencedSchemas];
597
+ while (toProcess.length > 0) {
598
+ const schemaName = toProcess.pop();
599
+ if (processedSchemas.has(schemaName)) {
600
+ continue;
601
+ }
602
+ processedSchemas.add(schemaName);
603
+ const jsonSchema = generatedSchemas.definitions[schemaName];
604
+ if (jsonSchema) {
605
+ schemas[schemaName] = convertToOpenApiSchema(jsonSchema);
606
+ const nestedRefs = extractNestedReferences(
607
+ { [schemaName]: jsonSchema },
608
+ processedSchemas
609
+ );
610
+ for (const ref of nestedRefs) {
611
+ if (!processedSchemas.has(ref)) {
612
+ toProcess.push(ref);
613
+ }
614
+ }
615
+ }
616
+ }
617
+ return { paths, schemas };
618
+ };
619
+
620
+ const createDecoratorFilter = (excludeDecorators) => {
621
+ if (excludeDecorators.length === 0) {
622
+ return () => true;
623
+ }
624
+ const excludeSet = new Set(excludeDecorators);
625
+ return (method) => {
626
+ const hasExcludedDecorator = method.decorators.some(
627
+ (decorator) => excludeSet.has(decorator)
628
+ );
629
+ return !hasExcludedDecorator;
630
+ };
631
+ };
632
+ const createPathFilter = (pathFilter) => {
633
+ if (typeof pathFilter === "function") {
634
+ return (method) => pathFilter(method.path);
635
+ }
636
+ return (method) => pathFilter.test(method.path);
637
+ };
638
+ const combineFilters = (filters) => {
639
+ if (filters.length === 0) {
640
+ return () => true;
641
+ }
642
+ if (filters.length === 1) {
643
+ return filters[0];
644
+ }
645
+ return (method) => filters.every((filter) => filter(method));
646
+ };
647
+ const createFilterPipeline = (options) => {
648
+ const filters = [];
649
+ if (options.excludeDecorators && options.excludeDecorators.length > 0) {
650
+ filters.push(createDecoratorFilter(options.excludeDecorators));
651
+ }
652
+ if (options.pathFilter) {
653
+ filters.push(createPathFilter(options.pathFilter));
654
+ }
655
+ if (filters.length === 0) {
656
+ return void 0;
657
+ }
658
+ return combineFilters(filters);
659
+ };
660
+ const filterMethods = (methods, options) => {
661
+ const filter = createFilterPipeline(options);
662
+ if (!filter) {
663
+ return methods;
664
+ }
665
+ return methods.filter(filter);
666
+ };
667
+
668
+ const transformSchemaToV31 = (schema) => {
669
+ const transformedOneOf = schema.oneOf?.map(transformSchemaToV31);
670
+ const transformedAnyOf = schema.anyOf?.map(transformSchemaToV31);
671
+ const transformedAllOf = schema.allOf?.map(transformSchemaToV31);
672
+ const transformedItems = schema.items ? transformSchemaToV31(schema.items) : void 0;
673
+ const transformedProperties = schema.properties ? Object.fromEntries(
674
+ Object.entries(schema.properties).map(([key, value]) => [
675
+ key,
676
+ transformSchemaToV31(value)
677
+ ])
678
+ ) : void 0;
679
+ const hasNullable = schema.nullable && schema.type && typeof schema.type === "string";
680
+ const transformedType = hasNullable ? [schema.type, "null"] : schema.type;
681
+ const { nullable: _nullable, ...restWithoutNullable } = schema;
682
+ return {
683
+ ...restWithoutNullable,
684
+ type: transformedType,
685
+ ...transformedOneOf && { oneOf: transformedOneOf },
686
+ ...transformedAnyOf && { anyOf: transformedAnyOf },
687
+ ...transformedAllOf && { allOf: transformedAllOf },
688
+ ...transformedItems && { items: transformedItems },
689
+ ...transformedProperties && { properties: transformedProperties }
690
+ };
691
+ };
692
+ const transformSchemasForVersion = (schemas, version) => {
693
+ if (version === "3.0.3") {
694
+ return schemas;
695
+ }
696
+ return Object.fromEntries(
697
+ Object.entries(schemas).map(([key, schema]) => [
698
+ key,
699
+ transformSchemaToV31(schema)
700
+ ])
701
+ );
702
+ };
703
+ const transformSpecForVersion = (spec, version) => {
704
+ if (version === "3.0.3") {
705
+ return { ...spec, openapi: version };
706
+ }
707
+ const transformedSchemas = spec.components?.schemas ? transformSchemasForVersion(spec.components.schemas, version) : void 0;
708
+ return {
709
+ ...spec,
710
+ openapi: version,
711
+ ...transformedSchemas && {
712
+ components: {
713
+ ...spec.components,
714
+ schemas: transformedSchemas
715
+ }
716
+ }
717
+ };
718
+ };
719
+
720
+ const ParameterLocation = Schema.Literal(
721
+ "path",
722
+ "query",
723
+ "header",
724
+ "cookie",
725
+ "body"
726
+ );
727
+ const ResolvedParameter = Schema.Struct({
728
+ name: Schema.String,
729
+ location: ParameterLocation,
730
+ tsType: Schema.String,
731
+ required: Schema.Boolean,
732
+ description: Schema.OptionFromNullOr(Schema.String)
733
+ // Note: constraints uses plain interface to avoid Schema initialization overhead
734
+ });
735
+ const ReturnTypeInfo = Schema.Struct({
736
+ type: Schema.OptionFromNullOr(Schema.String),
737
+ inline: Schema.OptionFromNullOr(Schema.String),
738
+ container: Schema.OptionFromNullOr(Schema.Literal("array")),
739
+ filePath: Schema.OptionFromNullOr(Schema.String)
740
+ });
741
+ const HttpMethod = Schema.Literal(
742
+ "GET",
743
+ "POST",
744
+ "PUT",
745
+ "PATCH",
746
+ "DELETE",
747
+ "OPTIONS",
748
+ "HEAD",
749
+ "ALL"
750
+ );
751
+ const OperationMetadata = Schema.Struct({
752
+ /** Custom summary from @ApiOperation({ summary: '...' }) */
753
+ summary: Schema.OptionFromNullOr(Schema.String),
754
+ /** Description from @ApiOperation({ description: '...' }) */
755
+ description: Schema.OptionFromNullOr(Schema.String),
756
+ /** Custom operationId from @ApiOperation({ operationId: '...' }) */
757
+ operationId: Schema.OptionFromNullOr(Schema.String),
758
+ /** Deprecated flag from @ApiOperation({ deprecated: true }) */
759
+ deprecated: Schema.OptionFromNullOr(Schema.Boolean)
760
+ });
761
+ const ResponseMetadata = Schema.Struct({
762
+ /** HTTP status code (e.g., 200, 201, 400, 404) */
763
+ statusCode: Schema.Number,
764
+ /** Response description */
765
+ description: Schema.OptionFromNullOr(Schema.String),
766
+ /** Response type name (e.g., "UserDto") */
767
+ type: Schema.OptionFromNullOr(Schema.String),
768
+ /** Whether the response type is an array */
769
+ isArray: Schema.Boolean
770
+ });
771
+ const MethodSecurityRequirement = Schema.Struct({
772
+ /** Security scheme name (e.g., 'bearer', 'jwt', 'oauth2') */
773
+ schemeName: Schema.String,
774
+ /** Required scopes (empty array for schemes without scopes) */
775
+ scopes: Schema.Array(Schema.String)
776
+ });
777
+ Schema.Struct({
778
+ httpMethod: HttpMethod,
779
+ path: Schema.String,
780
+ methodName: Schema.String,
781
+ controllerName: Schema.String,
782
+ controllerTags: Schema.Array(Schema.String),
783
+ returnType: ReturnTypeInfo,
784
+ parameters: Schema.Array(ResolvedParameter),
785
+ /** All decorator names on the method (for filtering) */
786
+ decorators: Schema.Array(Schema.String),
787
+ /** Metadata from @ApiOperation decorator */
788
+ operation: OperationMetadata,
789
+ /** Response metadata from @ApiResponse decorators */
790
+ responses: Schema.Array(ResponseMetadata),
791
+ /** Custom HTTP code from @HttpCode decorator */
792
+ httpCode: Schema.OptionFromNullOr(Schema.Number),
793
+ /** Content types from @ApiConsumes decorator (request body content types) */
794
+ consumes: Schema.Array(Schema.String),
795
+ /** Content types from @ApiProduces decorator (response content types) */
796
+ produces: Schema.Array(Schema.String),
797
+ /**
798
+ * Security requirements from decorators (@ApiBearerAuth, @ApiSecurity, etc.)
799
+ * Combines controller-level and method-level security.
800
+ * Multiple requirements = AND logic (all required).
801
+ * Empty array = no security decorators found (inherits global security).
802
+ */
803
+ security: Schema.Array(MethodSecurityRequirement)
804
+ });
805
+ const OpenApiSchemaObject = Schema.suspend(
806
+ () => Schema.Struct({
807
+ type: Schema.optional(Schema.String),
808
+ format: Schema.optional(Schema.String),
809
+ $ref: Schema.optional(Schema.String),
810
+ oneOf: Schema.optional(Schema.Array(OpenApiSchemaObject)),
811
+ items: Schema.optional(OpenApiSchemaObject),
812
+ properties: Schema.optional(
813
+ Schema.Record({ key: Schema.String, value: OpenApiSchemaObject })
814
+ ),
815
+ required: Schema.optional(Schema.Array(Schema.String))
816
+ })
817
+ );
818
+ const OpenApiParameter = Schema.Struct({
819
+ name: Schema.String,
820
+ in: Schema.Literal("path", "query", "header", "cookie"),
821
+ description: Schema.optional(Schema.String),
822
+ required: Schema.Boolean,
823
+ schema: OpenApiSchemaObject
824
+ });
825
+ const OpenApiRequestBody = Schema.Struct({
826
+ description: Schema.optional(Schema.String),
827
+ required: Schema.optional(Schema.Boolean),
828
+ content: Schema.Record({
829
+ key: Schema.String,
830
+ value: Schema.Struct({
831
+ schema: Schema.Unknown
832
+ })
833
+ })
834
+ });
835
+ const OpenApiResponse = Schema.Struct({
836
+ description: Schema.String,
837
+ content: Schema.optional(
838
+ Schema.Record({
839
+ key: Schema.String,
840
+ value: Schema.Struct({
841
+ schema: Schema.Unknown
842
+ })
843
+ })
844
+ )
845
+ });
846
+ Schema.Struct({
847
+ operationId: Schema.String,
848
+ summary: Schema.optional(Schema.String),
849
+ description: Schema.optional(Schema.String),
850
+ deprecated: Schema.optional(Schema.Boolean),
851
+ parameters: Schema.optional(Schema.Array(OpenApiParameter)),
852
+ requestBody: Schema.optional(OpenApiRequestBody),
853
+ responses: Schema.Record({
854
+ key: Schema.String,
855
+ value: OpenApiResponse
856
+ }),
857
+ tags: Schema.optional(Schema.Array(Schema.String)),
858
+ /** Per-operation security requirements */
859
+ security: Schema.optional(
860
+ Schema.Array(
861
+ Schema.Record({ key: Schema.String, value: Schema.Array(Schema.String) })
862
+ )
863
+ )
864
+ });
865
+ const OpenApiContactConfig = Schema.Struct({
866
+ name: Schema.optional(Schema.String),
867
+ email: Schema.optional(Schema.String),
868
+ url: Schema.optional(Schema.String)
869
+ });
870
+ const OpenApiLicenseConfig = Schema.Struct({
871
+ name: Schema.String,
872
+ url: Schema.optional(Schema.String)
873
+ });
874
+ const OpenApiInfoConfig = Schema.Struct({
875
+ title: Schema.String,
876
+ version: Schema.String,
877
+ description: Schema.optional(Schema.String),
878
+ contact: Schema.optional(OpenApiContactConfig),
879
+ license: Schema.optional(OpenApiLicenseConfig)
880
+ });
881
+ const OpenApiServerConfig = Schema.Struct({
882
+ url: Schema.String,
883
+ description: Schema.optional(Schema.String)
884
+ });
885
+ const OpenApiTagConfig = Schema.Struct({
886
+ name: Schema.String,
887
+ description: Schema.optional(Schema.String)
888
+ });
889
+ const SecuritySchemeType = Schema.Literal(
890
+ "apiKey",
891
+ "http",
892
+ "oauth2",
893
+ "openIdConnect"
894
+ );
895
+ const SecuritySchemeIn = Schema.Literal("query", "header", "cookie");
896
+ const SecuritySchemeConfig = Schema.Struct({
897
+ name: Schema.String,
898
+ type: SecuritySchemeType,
899
+ scheme: Schema.optional(Schema.String),
900
+ bearerFormat: Schema.optional(Schema.String),
901
+ in: Schema.optional(SecuritySchemeIn),
902
+ parameterName: Schema.optional(Schema.String),
903
+ description: Schema.optional(Schema.String)
904
+ });
905
+ const SecurityRequirement = Schema.Record({
906
+ key: Schema.String,
907
+ value: Schema.Array(Schema.String)
908
+ });
909
+ const OutputFormat = Schema.Literal("json", "yaml");
910
+ const FilesConfig = Schema.Struct({
911
+ entry: Schema.optional(
912
+ Schema.Union(Schema.String, Schema.Array(Schema.String))
913
+ ),
914
+ tsconfig: Schema.optional(Schema.String),
915
+ dtoGlob: Schema.optional(
916
+ Schema.Union(Schema.String, Schema.Array(Schema.String))
917
+ ),
918
+ include: Schema.optional(Schema.Array(Schema.String)),
919
+ exclude: Schema.optional(Schema.Array(Schema.String))
920
+ });
921
+ const SecurityConfig = Schema.Struct({
922
+ schemes: Schema.optional(Schema.Array(SecuritySchemeConfig)),
923
+ global: Schema.optional(Schema.Array(SecurityRequirement))
924
+ });
925
+ const OpenApiVersion = Schema.Literal("3.0.3", "3.1.0", "3.2.0");
926
+ const OpenApiConfig = Schema.Struct({
927
+ version: Schema.optional(OpenApiVersion),
928
+ info: OpenApiInfoConfig,
929
+ servers: Schema.optional(Schema.Array(OpenApiServerConfig)),
930
+ tags: Schema.optional(Schema.Array(OpenApiTagConfig)),
931
+ security: Schema.optional(SecurityConfig)
932
+ });
933
+ const QueryOptionsConfig = Schema.Struct({
934
+ style: Schema.optional(Schema.Literal("inline", "ref"))
935
+ });
936
+ const PathFilterFunction = Schema.declare(
937
+ (input) => typeof input === "function",
938
+ {
939
+ identifier: "PathFilterFunction",
940
+ description: "A function that takes a path string and returns a boolean"
941
+ }
942
+ );
943
+ const PathFilter = Schema.Union(
944
+ Schema.instanceOf(RegExp),
945
+ PathFilterFunction
946
+ );
947
+ const OptionsConfig = Schema.Struct({
948
+ basePath: Schema.optional(Schema.String),
949
+ extractValidation: Schema.optional(Schema.Boolean),
950
+ excludeDecorators: Schema.optional(Schema.Array(Schema.String)),
951
+ query: Schema.optional(QueryOptionsConfig),
952
+ pathFilter: Schema.optional(PathFilter)
953
+ });
954
+ const OpenApiGeneratorConfig = Schema.Struct({
955
+ extends: Schema.optional(Schema.String),
956
+ files: Schema.optional(FilesConfig),
957
+ output: Schema.String,
958
+ format: Schema.optional(OutputFormat),
959
+ openapi: OpenApiConfig,
960
+ options: Schema.optional(OptionsConfig)
961
+ });
962
+ Schema.Struct({
963
+ tsconfig: Schema.String,
964
+ entry: Schema.Array(Schema.String),
965
+ include: Schema.Array(Schema.String),
966
+ exclude: Schema.Array(Schema.String),
967
+ excludeDecorators: Schema.Array(Schema.String),
968
+ dtoGlob: Schema.Array(Schema.String),
969
+ extractValidation: Schema.Boolean,
970
+ basePath: Schema.optional(Schema.String),
971
+ pathFilter: Schema.optional(PathFilter),
972
+ version: Schema.optional(Schema.String),
973
+ info: OpenApiInfoConfig,
974
+ servers: Schema.Array(OpenApiServerConfig),
975
+ securitySchemes: Schema.Array(SecuritySchemeConfig),
976
+ securityRequirements: Schema.Array(SecurityRequirement),
977
+ tags: Schema.Array(OpenApiTagConfig),
978
+ output: Schema.String,
979
+ format: OutputFormat
980
+ });
981
+
982
+ const deepMerge = (parent, child) => {
983
+ const result = { ...parent };
984
+ for (const key of Object.keys(child)) {
985
+ const childValue = child[key];
986
+ const parentValue = parent[key];
987
+ if (childValue === void 0) {
988
+ continue;
989
+ }
990
+ if (childValue !== null && typeof childValue === "object" && !Array.isArray(childValue) && parentValue !== null && typeof parentValue === "object" && !Array.isArray(parentValue)) {
991
+ result[key] = deepMerge(
992
+ parentValue,
993
+ childValue
994
+ );
995
+ } else {
996
+ result[key] = childValue;
997
+ }
998
+ }
999
+ return result;
1000
+ };
1001
+ function defineConfig(config) {
1002
+ return config;
1003
+ }
1004
+ const CONFIG_FILE_NAMES = [
1005
+ "openapi.config.ts",
1006
+ "openapi.config.js",
1007
+ "openapi.config.mjs",
1008
+ "openapi.config.cjs"
1009
+ ];
1010
+ const DEFAULT_ENTRY$1 = "src/app.module.ts";
1011
+ const DEFAULT_DTO_GLOB = [
1012
+ "**/*.dto.ts",
1013
+ "**/*.entity.ts",
1014
+ "**/*.model.ts",
1015
+ "**/*.schema.ts"
1016
+ ];
1017
+ const DEFAULT_CONFIG = {
1018
+ files: {
1019
+ include: [],
1020
+ exclude: ["**/*.spec.ts", "**/*.test.ts", "**/node_modules/**"]
1021
+ },
1022
+ options: {
1023
+ excludeDecorators: ["ApiExcludeEndpoint", "ApiExcludeController"],
1024
+ extractValidation: true
1025
+ },
1026
+ format: "json",
1027
+ openapi: {
1028
+ servers: [],
1029
+ tags: [],
1030
+ security: {
1031
+ schemes: [],
1032
+ global: []
1033
+ }
1034
+ }
1035
+ };
1036
+ const findConfigFile = (startDir = process.cwd()) => Effect.gen(function* () {
1037
+ let currentDir = resolve(startDir);
1038
+ const root = dirname(currentDir);
1039
+ while (currentDir !== root) {
1040
+ for (const fileName of CONFIG_FILE_NAMES) {
1041
+ const configPath = resolve(currentDir, fileName);
1042
+ if (existsSync(configPath)) {
1043
+ return configPath;
1044
+ }
1045
+ }
1046
+ const parentDir = dirname(currentDir);
1047
+ if (parentDir === currentDir) break;
1048
+ currentDir = parentDir;
1049
+ }
1050
+ return yield* ConfigNotFoundError.notFound(startDir);
1051
+ });
1052
+ const validateConfig = (config, filePath) => Schema.decodeUnknown(OpenApiGeneratorConfig)(config).pipe(
1053
+ Effect.mapError((parseError) => {
1054
+ const issues = parseError.message.split("\n").filter((line) => line.trim());
1055
+ return ConfigValidationError.fromIssues(filePath, issues);
1056
+ })
1057
+ );
1058
+ const unwrapTsxDoubleDefault = (value) => value && typeof value === "object" && "default" in value && Object.keys(value).length === 1 ? value.default : value;
1059
+ const loadRawConfigFromFile = (configPath) => Effect.gen(function* () {
1060
+ const absolutePath = resolve(configPath);
1061
+ if (!existsSync(absolutePath)) {
1062
+ return yield* ConfigNotFoundError.pathNotFound(absolutePath);
1063
+ }
1064
+ yield* Effect.logDebug("Loading config file").pipe(
1065
+ Effect.annotateLogs({ path: absolutePath })
1066
+ );
1067
+ const module = yield* Effect.tryPromise({
1068
+ try: async () => {
1069
+ const fileUrl = pathToFileURL(absolutePath).href;
1070
+ const cacheBustUrl = `${fileUrl}?t=${Date.now()}-${Math.random().toString(36).slice(2)}`;
1071
+ return await import(cacheBustUrl);
1072
+ },
1073
+ catch: (error) => ConfigLoadError.importFailed(absolutePath, error)
1074
+ });
1075
+ const rawConfig = unwrapTsxDoubleDefault(module.default ?? module.config);
1076
+ if (!rawConfig) {
1077
+ return yield* ConfigLoadError.noExport(absolutePath);
1078
+ }
1079
+ return rawConfig;
1080
+ });
1081
+ const resolveConfigExtends = (rawConfig, configPath, visited = /* @__PURE__ */ new Set()) => Effect.gen(function* () {
1082
+ const absolutePath = resolve(configPath);
1083
+ if (visited.has(absolutePath)) {
1084
+ return yield* ConfigLoadError.importFailed(
1085
+ absolutePath,
1086
+ new Error(`Circular extends detected: ${absolutePath}`)
1087
+ );
1088
+ }
1089
+ visited.add(absolutePath);
1090
+ const extendsPath = rawConfig.extends;
1091
+ if (!extendsPath) {
1092
+ return rawConfig;
1093
+ }
1094
+ const parentConfigPath = resolve(dirname(absolutePath), extendsPath);
1095
+ yield* Effect.logDebug("Resolving config extends").pipe(
1096
+ Effect.annotateLogs({ parent: parentConfigPath })
1097
+ );
1098
+ const parentRawConfig = yield* loadRawConfigFromFile(parentConfigPath);
1099
+ const resolvedParent = yield* resolveConfigExtends(
1100
+ parentRawConfig,
1101
+ parentConfigPath,
1102
+ visited
1103
+ );
1104
+ const { extends: _, ...childWithoutExtends } = rawConfig;
1105
+ return deepMerge(
1106
+ resolvedParent,
1107
+ childWithoutExtends
1108
+ );
1109
+ });
1110
+ const loadConfigFromFile = (configPath) => Effect.gen(function* () {
1111
+ const rawConfig = yield* loadRawConfigFromFile(configPath);
1112
+ const mergedConfig = yield* resolveConfigExtends(rawConfig, configPath);
1113
+ return yield* validateConfig(mergedConfig, configPath);
1114
+ });
1115
+ const loadConfig = (configPath, cwd = process.cwd()) => Effect.gen(function* () {
1116
+ const resolvedPath = configPath ? Effect.succeed(configPath) : findConfigFile(cwd);
1117
+ const path = yield* resolvedPath;
1118
+ return yield* loadConfigFromFile(path);
1119
+ });
1120
+ const resolveConfig = (config) => {
1121
+ const files = config.files ?? {};
1122
+ const options = config.options ?? {};
1123
+ const openapi = config.openapi;
1124
+ const security = openapi.security ?? {};
1125
+ const rawEntry = files.entry ?? DEFAULT_ENTRY$1;
1126
+ const entry = Array.isArray(rawEntry) ? rawEntry : [rawEntry];
1127
+ const rawDtoGlob = files.dtoGlob;
1128
+ const dtoGlob = rawDtoGlob ? Array.isArray(rawDtoGlob) ? rawDtoGlob : [rawDtoGlob] : [...DEFAULT_DTO_GLOB];
1129
+ const tsconfig = files.tsconfig;
1130
+ if (!tsconfig) {
1131
+ throw new Error("tsconfig is required in files configuration");
1132
+ }
1133
+ return {
1134
+ tsconfig,
1135
+ entry,
1136
+ include: files.include ?? DEFAULT_CONFIG.files.include,
1137
+ exclude: files.exclude ?? DEFAULT_CONFIG.files.exclude,
1138
+ excludeDecorators: options.excludeDecorators ?? DEFAULT_CONFIG.options.excludeDecorators,
1139
+ dtoGlob,
1140
+ extractValidation: options.extractValidation ?? DEFAULT_CONFIG.options.extractValidation,
1141
+ basePath: options.basePath,
1142
+ version: openapi.version,
1143
+ info: openapi.info,
1144
+ servers: openapi.servers ?? DEFAULT_CONFIG.openapi.servers,
1145
+ securitySchemes: security.schemes ?? DEFAULT_CONFIG.openapi.security.schemes,
1146
+ securityRequirements: security.global ?? DEFAULT_CONFIG.openapi.security.global,
1147
+ tags: openapi.tags ?? DEFAULT_CONFIG.openapi.tags,
1148
+ output: config.output,
1149
+ format: config.format ?? DEFAULT_CONFIG.format
1150
+ };
1151
+ };
1152
+ const loadAndResolveConfig = (configPath, cwd = process.cwd()) => Effect.gen(function* () {
1153
+ const config = yield* loadConfig(configPath, cwd);
1154
+ return yield* Effect.try({
1155
+ try: () => resolveConfig(config),
1156
+ catch: (error) => ConfigValidationError.fromIssues(configPath ?? "unknown", [
1157
+ error instanceof Error ? error.message : String(error)
1158
+ ])
1159
+ });
1160
+ });
1161
+
1162
+ function findRefs(obj, path = "") {
1163
+ const refs = [];
1164
+ if (!obj || typeof obj !== "object") {
1165
+ return refs;
1166
+ }
1167
+ const record = obj;
1168
+ if (typeof record.$ref === "string") {
1169
+ refs.push({ ref: record.$ref, path });
1170
+ }
1171
+ for (const [key, value] of Object.entries(record)) {
1172
+ const newPath = path ? `${path}.${key}` : key;
1173
+ refs.push(...findRefs(value, newPath));
1174
+ }
1175
+ return refs;
1176
+ }
1177
+ function validateSpec(spec) {
1178
+ const definedSchemas = new Set(Object.keys(spec.components?.schemas ?? {}));
1179
+ const allRefs = findRefs(spec);
1180
+ const schemaRefs = allRefs.filter(
1181
+ (r) => r.ref.startsWith("#/components/schemas/")
1182
+ );
1183
+ const brokenRefs = [];
1184
+ const missingSchemas = /* @__PURE__ */ new Map();
1185
+ for (const { ref, path } of schemaRefs) {
1186
+ const schemaName = ref.replace("#/components/schemas/", "");
1187
+ if (!definedSchemas.has(schemaName)) {
1188
+ brokenRefs.push({
1189
+ ref,
1190
+ path,
1191
+ missingSchema: schemaName
1192
+ });
1193
+ missingSchemas.set(schemaName, (missingSchemas.get(schemaName) ?? 0) + 1);
1194
+ }
1195
+ }
1196
+ return {
1197
+ valid: brokenRefs.length === 0,
1198
+ totalRefs: schemaRefs.length,
1199
+ brokenRefCount: brokenRefs.length,
1200
+ brokenRefs,
1201
+ missingSchemas
1202
+ };
1203
+ }
1204
+ function categorizeBrokenRefs(missingSchemas) {
1205
+ const primitives = [];
1206
+ const unionTypes = [];
1207
+ const queryParams = [];
1208
+ const other = [];
1209
+ const primitiveTypes = /* @__PURE__ */ new Set([
1210
+ "string",
1211
+ "number",
1212
+ "boolean",
1213
+ "object",
1214
+ "null",
1215
+ "undefined",
1216
+ "void",
1217
+ "any",
1218
+ "unknown",
1219
+ "never"
1220
+ ]);
1221
+ for (const schema of missingSchemas.keys()) {
1222
+ if (primitiveTypes.has(schema.toLowerCase())) {
1223
+ primitives.push(schema);
1224
+ continue;
1225
+ }
1226
+ if (schema.includes(" | ") || schema.includes("|")) {
1227
+ unionTypes.push(schema);
1228
+ continue;
1229
+ }
1230
+ if (schema.endsWith("QueryParams") || schema.endsWith("PathParams") || schema.endsWith("Params")) {
1231
+ queryParams.push(schema);
1232
+ continue;
1233
+ }
1234
+ other.push(schema);
1235
+ }
1236
+ return {
1237
+ primitives,
1238
+ unionTypes,
1239
+ queryParams,
1240
+ other
1241
+ };
1242
+ }
1243
+ function formatValidationResult(result) {
1244
+ if (result.valid) {
1245
+ return `Spec is valid: ${result.totalRefs} schema refs, all resolved`;
1246
+ }
1247
+ const lines = [
1248
+ `Found ${result.brokenRefCount} broken refs (${result.missingSchemas.size} missing schemas):`
1249
+ ];
1250
+ const categories = categorizeBrokenRefs(result.missingSchemas);
1251
+ if (categories.primitives.length > 0) {
1252
+ lines.push(`
1253
+ Primitive types (should not be $refs):`);
1254
+ for (const name of categories.primitives) {
1255
+ const count = result.missingSchemas.get(name) ?? 0;
1256
+ lines.push(` - ${name} (${count} usages)`);
1257
+ }
1258
+ }
1259
+ if (categories.unionTypes.length > 0) {
1260
+ lines.push(`
1261
+ Union types (need special handling):`);
1262
+ for (const name of categories.unionTypes) {
1263
+ const count = result.missingSchemas.get(name) ?? 0;
1264
+ lines.push(` - ${name} (${count} usages)`);
1265
+ }
1266
+ }
1267
+ if (categories.queryParams.length > 0) {
1268
+ lines.push(`
1269
+ Query/Path params (may need dtoGlob coverage):`);
1270
+ for (const name of categories.queryParams) {
1271
+ const count = result.missingSchemas.get(name) ?? 0;
1272
+ lines.push(` - ${name} (${count} usages)`);
1273
+ }
1274
+ }
1275
+ if (categories.other.length > 0) {
1276
+ lines.push(`
1277
+ Other missing schemas (check dtoGlob patterns):`);
1278
+ for (const name of categories.other.slice(0, 20)) {
1279
+ const count = result.missingSchemas.get(name) ?? 0;
1280
+ lines.push(` - ${name} (${count} usages)`);
1281
+ }
1282
+ if (categories.other.length > 20) {
1283
+ lines.push(` ... and ${categories.other.length - 20} more`);
1284
+ }
1285
+ }
1286
+ return lines.join("\n");
1287
+ }
1288
+
1289
+ function resolveTypeLocations(project, missingTypes) {
1290
+ const resolved = /* @__PURE__ */ new Map();
1291
+ const typeIndex = buildTypeIndex(project);
1292
+ for (const typeName of missingTypes) {
1293
+ const baseTypeName = typeName.replace(/<.*>$/, "");
1294
+ const filePath = typeIndex.get(baseTypeName);
1295
+ if (filePath && !filePath.includes("node_modules")) {
1296
+ resolved.set(typeName, filePath);
1297
+ }
1298
+ }
1299
+ return resolved;
1300
+ }
1301
+ function buildTypeIndex(project) {
1302
+ const index = /* @__PURE__ */ new Map();
1303
+ for (const sourceFile of project.getSourceFiles()) {
1304
+ const filePath = sourceFile.getFilePath();
1305
+ if (filePath.includes("node_modules")) continue;
1306
+ for (const cls of sourceFile.getClasses()) {
1307
+ if (cls.isExported()) {
1308
+ const name = cls.getName();
1309
+ if (name) {
1310
+ index.set(name, filePath);
1311
+ }
1312
+ }
1313
+ }
1314
+ for (const iface of sourceFile.getInterfaces()) {
1315
+ if (iface.isExported()) {
1316
+ const name = iface.getName();
1317
+ index.set(name, filePath);
1318
+ }
1319
+ }
1320
+ for (const typeAlias of sourceFile.getTypeAliases()) {
1321
+ if (typeAlias.isExported()) {
1322
+ const name = typeAlias.getName();
1323
+ index.set(name, filePath);
1324
+ }
1325
+ }
1326
+ for (const enumDecl of sourceFile.getEnums()) {
1327
+ if (enumDecl.isExported()) {
1328
+ const name = enumDecl.getName();
1329
+ index.set(name, filePath);
1330
+ }
1331
+ }
1332
+ }
1333
+ return index;
1334
+ }
1335
+ function createTypeResolverProject(tsconfig) {
1336
+ return new Project({
1337
+ tsConfigFilePath: tsconfig,
1338
+ skipAddingFilesFromTsConfig: false,
1339
+ // Need all files for type resolution
1340
+ compilerOptions: {
1341
+ skipLibCheck: true,
1342
+ skipDefaultLibCheck: true,
1343
+ declaration: false,
1344
+ noEmit: true
1345
+ }
1346
+ });
1347
+ }
1348
+ function hasRipgrep() {
1349
+ try {
1350
+ execSync("rg --version", { encoding: "utf-8", stdio: "pipe" });
1351
+ return true;
1352
+ } catch {
1353
+ return false;
1354
+ }
1355
+ }
1356
+ function resolveTypeLocationsFast(baseDir, missingTypes) {
1357
+ const resolved = /* @__PURE__ */ new Map();
1358
+ const typeNames = [...missingTypes].map((t) => t.replace(/<.*>$/, ""));
1359
+ const typePattern = typeNames.join("|");
1360
+ const pattern = `export\\s+(class|interface|type|enum)\\s+(${typePattern})\\b`;
1361
+ const excludeDirs = [
1362
+ "node_modules",
1363
+ "dist",
1364
+ ".git",
1365
+ "coverage",
1366
+ "__snapshots__",
1367
+ ".turbo",
1368
+ ".next",
1369
+ "build"
1370
+ ];
1371
+ try {
1372
+ let result;
1373
+ if (hasRipgrep()) {
1374
+ const excludeArgs = excludeDirs.map((d) => `-g '!${d}/'`).join(" ");
1375
+ result = execSync(
1376
+ `rg -H --no-heading -t ts ${excludeArgs} '${pattern}' "${baseDir}" 2>/dev/null`,
1377
+ { encoding: "utf-8", timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }
1378
+ );
1379
+ } else {
1380
+ const excludeArgs = excludeDirs.map((d) => `--exclude-dir=${d}`).join(" ");
1381
+ result = execSync(
1382
+ `grep -r -H -E "${pattern}" --include="*.ts" ${excludeArgs} "${baseDir}" 2>/dev/null`,
1383
+ { encoding: "utf-8", timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }
1384
+ );
1385
+ }
1386
+ for (const line of result.split("\n")) {
1387
+ if (!line.trim()) continue;
1388
+ const colonIdx = line.indexOf(":");
1389
+ if (colonIdx === -1) continue;
1390
+ const filePath = line.substring(0, colonIdx);
1391
+ if (filePath.includes("node_modules")) continue;
1392
+ for (const typeName of typeNames) {
1393
+ const typeRegex = new RegExp(
1394
+ `export\\s+(class|interface|type|enum)\\s+${typeName}\\b`
1395
+ );
1396
+ if (typeRegex.test(line) && !resolved.has(typeName)) {
1397
+ resolved.set(typeName, filePath);
1398
+ }
1399
+ }
1400
+ }
1401
+ } catch {
1402
+ }
1403
+ return resolved;
1404
+ }
1405
+
1406
+ const DEFAULT_ENTRY = "src/app.module.ts";
1407
+ const mergeSecurityWithGlobal = (paths, globalSecurity) => {
1408
+ if (!globalSecurity || globalSecurity.length === 0) {
1409
+ return paths;
1410
+ }
1411
+ const mergedPaths = {};
1412
+ for (const [path, methods] of Object.entries(paths)) {
1413
+ const mergedMethods = {};
1414
+ for (const [method, operation] of Object.entries(methods)) {
1415
+ if (operation.security && operation.security.length > 0) {
1416
+ const merged = {};
1417
+ for (const globalReq of globalSecurity) {
1418
+ for (const [scheme, scopes] of Object.entries(globalReq)) {
1419
+ merged[scheme] = [...merged[scheme] ?? [], ...scopes];
1420
+ }
1421
+ }
1422
+ for (const decoratorReq of operation.security) {
1423
+ for (const [scheme, scopes] of Object.entries(decoratorReq)) {
1424
+ merged[scheme] = [...merged[scheme] ?? [], ...scopes];
1425
+ }
1426
+ }
1427
+ for (const scheme of Object.keys(merged)) {
1428
+ merged[scheme] = [...new Set(merged[scheme])];
1429
+ }
1430
+ mergedMethods[method] = {
1431
+ ...operation,
1432
+ security: [merged]
1433
+ };
1434
+ } else {
1435
+ mergedMethods[method] = operation;
1436
+ }
1437
+ }
1438
+ mergedPaths[path] = mergedMethods;
1439
+ }
1440
+ return mergedPaths;
1441
+ };
1442
+ const OPENAPI_FIELD_ORDER = [
1443
+ "openapi",
1444
+ "info",
1445
+ "servers",
1446
+ "paths",
1447
+ "components",
1448
+ "tags",
1449
+ "security"
1450
+ ];
1451
+ const sortObjectKeysDeep = (obj, isTopLevel = false) => {
1452
+ if (obj === null || typeof obj !== "object") {
1453
+ return obj;
1454
+ }
1455
+ if (Array.isArray(obj)) {
1456
+ return obj.map((item) => sortObjectKeysDeep(item, false));
1457
+ }
1458
+ const sorted = {};
1459
+ const objRecord = obj;
1460
+ const keys = Object.keys(objRecord);
1461
+ const sortedKeys = isTopLevel ? keys.sort((a, b) => {
1462
+ const aIndex = OPENAPI_FIELD_ORDER.indexOf(a);
1463
+ const bIndex = OPENAPI_FIELD_ORDER.indexOf(b);
1464
+ if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
1465
+ if (aIndex !== -1) return -1;
1466
+ if (bIndex !== -1) return 1;
1467
+ return a.localeCompare(b);
1468
+ }) : keys.sort();
1469
+ for (const key of sortedKeys) {
1470
+ sorted[key] = sortObjectKeysDeep(objRecord[key], false);
1471
+ }
1472
+ return sorted;
1473
+ };
1474
+ const findMissingSchemaRefs = (paths, schemas) => {
1475
+ const defined = new Set(Object.keys(schemas));
1476
+ const missing = /* @__PURE__ */ new Set();
1477
+ const findRefs = (obj) => {
1478
+ if (!obj || typeof obj !== "object") return;
1479
+ const record = obj;
1480
+ if (typeof record.$ref === "string") {
1481
+ const ref = record.$ref;
1482
+ if (ref.startsWith("#/components/schemas/")) {
1483
+ const schemaName = ref.replace("#/components/schemas/", "");
1484
+ if (!defined.has(schemaName)) {
1485
+ missing.add(schemaName);
1486
+ }
1487
+ }
1488
+ }
1489
+ for (const value of Object.values(record)) {
1490
+ findRefs(value);
1491
+ }
1492
+ };
1493
+ findRefs(paths);
1494
+ return missing;
1495
+ };
1496
+ const extractValidationConstraints = async (dtoGlobPatterns, basePath, tsconfig, schemas) => {
1497
+ const absolutePatterns = dtoGlobPatterns.map(
1498
+ (pattern) => pattern.startsWith("/") ? pattern : join(basePath, pattern)
1499
+ );
1500
+ const fileArrays = await Promise.all(
1501
+ absolutePatterns.map(
1502
+ (pattern) => glob(pattern, { absolute: true, nodir: true })
1503
+ )
1504
+ );
1505
+ const dtoFiles = fileArrays.flat();
1506
+ if (dtoFiles.length === 0) {
1507
+ return schemas;
1508
+ }
1509
+ const project = new Project({
1510
+ tsConfigFilePath: tsconfig,
1511
+ skipAddingFilesFromTsConfig: true,
1512
+ compilerOptions: {
1513
+ // Skip type checking for performance - we only need AST structure
1514
+ skipLibCheck: true,
1515
+ skipDefaultLibCheck: true,
1516
+ allowJs: false,
1517
+ declaration: false,
1518
+ noEmit: true
1519
+ }
1520
+ });
1521
+ project.addSourceFilesAtPaths(dtoFiles);
1522
+ const classConstraints = /* @__PURE__ */ new Map();
1523
+ const classRequired = /* @__PURE__ */ new Map();
1524
+ for (const sourceFile of project.getSourceFiles()) {
1525
+ for (const classDecl of sourceFile.getClasses()) {
1526
+ const className = classDecl.getName();
1527
+ if (!className) continue;
1528
+ const constraints = extractClassConstraints(classDecl);
1529
+ const required = getRequiredProperties(classDecl);
1530
+ if (Object.keys(constraints).length > 0) {
1531
+ classConstraints.set(className, constraints);
1532
+ }
1533
+ if (required.length > 0) {
1534
+ classRequired.set(className, required);
1535
+ }
1536
+ }
1537
+ }
1538
+ return mergeValidationConstraints(schemas, classConstraints, classRequired);
1539
+ };
1540
+ const findTsConfig = (startDir) => {
1541
+ let currentDir = resolve(startDir);
1542
+ const root = dirname(currentDir);
1543
+ while (currentDir !== root) {
1544
+ const tsconfigPath = join(currentDir, "tsconfig.json");
1545
+ if (existsSync(tsconfigPath)) {
1546
+ return tsconfigPath;
1547
+ }
1548
+ const parentDir = dirname(currentDir);
1549
+ if (parentDir === currentDir) break;
1550
+ currentDir = parentDir;
1551
+ }
1552
+ return void 0;
1553
+ };
1554
+ const extractMethodInfosFromEntry = (tsconfig, entry, extractOptions = {}) => Effect.gen(function* () {
1555
+ const project = new Project({
1556
+ tsConfigFilePath: tsconfig,
1557
+ skipAddingFilesFromTsConfig: true,
1558
+ compilerOptions: {
1559
+ skipLibCheck: true,
1560
+ skipDefaultLibCheck: true,
1561
+ allowJs: false,
1562
+ declaration: false,
1563
+ noEmit: true
1564
+ }
1565
+ });
1566
+ project.addSourceFilesAtPaths(entry);
1567
+ const entrySourceFile = project.getSourceFile(entry);
1568
+ if (!entrySourceFile) {
1569
+ return yield* EntryNotFoundError.fileNotFound(entry);
1570
+ }
1571
+ let entryClass = entrySourceFile.getClass("AppModule");
1572
+ if (!entryClass) {
1573
+ for (const cls of entrySourceFile.getClasses()) {
1574
+ const hasModuleDecorator = cls.getDecorators().some((d) => d.getName() === "Module");
1575
+ if (hasModuleDecorator) {
1576
+ entryClass = cls;
1577
+ break;
1578
+ }
1579
+ }
1580
+ }
1581
+ if (!entryClass) {
1582
+ return yield* EntryNotFoundError.classNotFound(entry, "Module");
1583
+ }
1584
+ const modules = yield* getModules(entryClass);
1585
+ const methodInfos = modules.flatMap(
1586
+ (mod) => mod.controllers.flatMap(
1587
+ (controller) => getControllerMethodInfos(controller, extractOptions)
1588
+ )
1589
+ );
1590
+ return methodInfos;
1591
+ });
1592
+ const extractMethodInfosEffect = (tsconfig, entries, extractOptions = {}) => Effect.gen(function* () {
1593
+ const allMethodInfos = yield* Effect.forEach(
1594
+ entries,
1595
+ (entry) => extractMethodInfosFromEntry(tsconfig, entry, extractOptions),
1596
+ { concurrency: "unbounded" }
1597
+ );
1598
+ const seen = /* @__PURE__ */ new Set();
1599
+ const deduped = [];
1600
+ for (const methodInfos of allMethodInfos) {
1601
+ for (const info of methodInfos) {
1602
+ const key = `${info.httpMethod}:${info.path}`;
1603
+ if (!seen.has(key)) {
1604
+ seen.add(key);
1605
+ deduped.push(info);
1606
+ }
1607
+ }
1608
+ }
1609
+ return deduped;
1610
+ });
1611
+ const generate = async (configPath, overrides) => {
1612
+ const debug = overrides?.debug ?? false;
1613
+ const loggerLayer = debug ? Logger.replace(Logger.defaultLogger, Logger.prettyLoggerDefault).pipe(
1614
+ Layer.merge(Logger.minimumLogLevel(LogLevel.Debug))
1615
+ ) : Logger.minimumLogLevel(LogLevel.Info);
1616
+ const runEffect = (effect) => Effect.runPromise(effect.pipe(Effect.provide(loggerLayer)));
1617
+ const absoluteConfigPath = resolve(configPath);
1618
+ const configDir = dirname(absoluteConfigPath);
1619
+ const config = await runEffect(
1620
+ loadConfigFromFile(absoluteConfigPath).pipe(
1621
+ Effect.tap(
1622
+ () => Effect.logDebug("Config loaded").pipe(
1623
+ Effect.annotateLogs({ configPath: absoluteConfigPath })
1624
+ )
1625
+ ),
1626
+ Effect.mapError((e) => new Error(e.message))
1627
+ )
1628
+ );
1629
+ const files = config.files ?? {};
1630
+ const options = config.options ?? {};
1631
+ const openapi = config.openapi;
1632
+ const security = openapi.security ?? {};
1633
+ const rawEntry = files.entry ?? DEFAULT_ENTRY;
1634
+ const entries = (Array.isArray(rawEntry) ? rawEntry : [rawEntry]).map(
1635
+ (e) => resolve(configDir, e)
1636
+ );
1637
+ const output = resolve(configDir, config.output);
1638
+ const tsconfig = files.tsconfig ? resolve(configDir, files.tsconfig) : findTsConfig(dirname(entries[0]));
1639
+ if (!tsconfig) {
1640
+ throw new Error(
1641
+ `Could not find tsconfig.json. Please specify files.tsconfig in your config file.`
1642
+ );
1643
+ }
1644
+ if (!existsSync(tsconfig)) {
1645
+ throw new Error(`tsconfig.json not found at: ${tsconfig}`);
1646
+ }
1647
+ const extractOptions = {
1648
+ query: options.query
1649
+ };
1650
+ const dtoGlobArray = files.dtoGlob ? Array.isArray(files.dtoGlob) ? files.dtoGlob : [files.dtoGlob] : null;
1651
+ const [extractedMethodInfos, initialSchemas] = await Promise.all([
1652
+ runEffect(
1653
+ extractMethodInfosEffect(tsconfig, entries, extractOptions).pipe(
1654
+ Effect.tap(
1655
+ (methods) => Effect.logDebug("Method extraction complete").pipe(
1656
+ Effect.annotateLogs({ methodCount: methods.length, entries })
1657
+ )
1658
+ ),
1659
+ Effect.mapError((error) => new Error(error.message))
1660
+ )
1661
+ ),
1662
+ dtoGlobArray ? runEffect(
1663
+ generateSchemas({
1664
+ dtoGlob: dtoGlobArray,
1665
+ tsconfig,
1666
+ basePath: configDir
1667
+ }).pipe(
1668
+ Effect.tap(
1669
+ (schemas2) => Effect.logDebug("Schema generation complete").pipe(
1670
+ Effect.annotateLogs({
1671
+ schemaCount: Object.keys(schemas2.definitions).length,
1672
+ dtoGlob: dtoGlobArray
1673
+ })
1674
+ )
1675
+ ),
1676
+ Effect.mapError((error) => new Error(error.message))
1677
+ )
1678
+ ) : Promise.resolve(null)
1679
+ ]);
1680
+ const filteredMethodInfos = filterMethods(extractedMethodInfos, {
1681
+ excludeDecorators: options.excludeDecorators,
1682
+ pathFilter: options.pathFilter
1683
+ });
1684
+ let paths = transformMethods(filteredMethodInfos);
1685
+ if (options.basePath) {
1686
+ const prefix = options.basePath.startsWith("/") ? options.basePath : `/${options.basePath}`;
1687
+ const prefixedPaths = {};
1688
+ for (const [path, methods] of Object.entries(paths)) {
1689
+ const prefixedPath = path.startsWith("/") ? `${prefix}${path}` : `${prefix}/${path}`;
1690
+ prefixedPaths[prefixedPath] = methods;
1691
+ }
1692
+ paths = prefixedPaths;
1693
+ }
1694
+ paths = mergeSecurityWithGlobal(
1695
+ paths,
1696
+ security.global
1697
+ );
1698
+ let schemas = {};
1699
+ if (initialSchemas && dtoGlobArray) {
1700
+ let generatedSchemas = initialSchemas;
1701
+ const shouldExtractValidation = options.extractValidation !== false;
1702
+ if (shouldExtractValidation) {
1703
+ generatedSchemas = await extractValidationConstraints(
1704
+ dtoGlobArray,
1705
+ configDir,
1706
+ tsconfig,
1707
+ generatedSchemas
1708
+ );
1709
+ }
1710
+ generatedSchemas = normalizeStructureRefs(generatedSchemas);
1711
+ let mergeResult = mergeSchemas(
1712
+ paths,
1713
+ generatedSchemas
1714
+ );
1715
+ schemas = mergeResult.schemas;
1716
+ const missingRefs = findMissingSchemaRefs(
1717
+ paths,
1718
+ schemas
1719
+ );
1720
+ if (missingRefs.size > 0) {
1721
+ const tsconfigDir = dirname(tsconfig);
1722
+ const resolvedLocations = resolveTypeLocationsFast(
1723
+ tsconfigDir,
1724
+ missingRefs
1725
+ );
1726
+ const unresolvedTypes = new Set(
1727
+ [...missingRefs].filter(
1728
+ (t) => !resolvedLocations.has(t.replace(/<.*>$/, ""))
1729
+ )
1730
+ );
1731
+ if (unresolvedTypes.size > 0) {
1732
+ const project = createTypeResolverProject(tsconfig);
1733
+ const morphResolved = resolveTypeLocations(project, unresolvedTypes);
1734
+ for (const [type, path] of morphResolved) {
1735
+ resolvedLocations.set(type, path);
1736
+ }
1737
+ }
1738
+ if (resolvedLocations.size > 0) {
1739
+ const additionalFiles = [...new Set(resolvedLocations.values())];
1740
+ const additionalSchemas = await runEffect(
1741
+ generateSchemasFromFiles(additionalFiles, tsconfig)
1742
+ );
1743
+ if (Object.keys(additionalSchemas.definitions).length > 0) {
1744
+ const normalizedAdditional = normalizeStructureRefs(additionalSchemas);
1745
+ const combinedSchemas = {
1746
+ definitions: {
1747
+ ...generatedSchemas.definitions,
1748
+ ...normalizedAdditional.definitions
1749
+ }
1750
+ };
1751
+ mergeResult = mergeSchemas(
1752
+ paths,
1753
+ combinedSchemas
1754
+ );
1755
+ schemas = mergeResult.schemas;
1756
+ }
1757
+ }
1758
+ }
1759
+ }
1760
+ const securitySchemes = security.schemes && security.schemes.length > 0 ? buildSecuritySchemes(security.schemes) : void 0;
1761
+ const hasSchemas = Object.keys(schemas).length > 0;
1762
+ const hasSecuritySchemes = securitySchemes && Object.keys(securitySchemes).length > 0;
1763
+ const components = hasSchemas || hasSecuritySchemes ? {
1764
+ ...hasSchemas && { schemas },
1765
+ ...hasSecuritySchemes && { securitySchemes }
1766
+ } : void 0;
1767
+ const openApiVersion = openapi.version ?? "3.0.3";
1768
+ let spec = {
1769
+ openapi: openApiVersion,
1770
+ info: {
1771
+ title: openapi.info.title,
1772
+ version: openapi.info.version,
1773
+ ...openapi.info.description && {
1774
+ description: openapi.info.description
1775
+ },
1776
+ ...openapi.info.contact && { contact: openapi.info.contact },
1777
+ ...openapi.info.license && { license: openapi.info.license }
1778
+ },
1779
+ servers: openapi.servers ?? [],
1780
+ paths,
1781
+ ...components && { components },
1782
+ tags: openapi.tags ?? [],
1783
+ ...security.global && security.global.length > 0 && {
1784
+ security: security.global
1785
+ }
1786
+ };
1787
+ if (openApiVersion !== "3.0.3") {
1788
+ spec = transformSpecForVersion(spec, openApiVersion);
1789
+ }
1790
+ const sortedSpec = sortObjectKeysDeep(spec, true);
1791
+ const outputDir = dirname(output);
1792
+ if (!existsSync(outputDir)) {
1793
+ mkdirSync(outputDir, { recursive: true });
1794
+ }
1795
+ const format = overrides?.format ?? config.format ?? "json";
1796
+ if (format === "json") {
1797
+ writeFileSync(output, JSON.stringify(sortedSpec, null, 2), "utf-8");
1798
+ } else {
1799
+ writeFileSync(
1800
+ output,
1801
+ yaml.dump(sortedSpec, {
1802
+ indent: 2,
1803
+ lineWidth: -1,
1804
+ // Disable line wrapping
1805
+ noRefs: true,
1806
+ // Disable anchor/alias references
1807
+ quotingType: '"',
1808
+ // Use double quotes for strings
1809
+ forceQuotes: false
1810
+ // Only quote when necessary
1811
+ }),
1812
+ "utf-8"
1813
+ );
1814
+ }
1815
+ const pathCount = Object.keys(paths).length;
1816
+ const operationCount = Object.values(paths).reduce(
1817
+ (acc, methods) => acc + Object.keys(methods).length,
1818
+ 0
1819
+ );
1820
+ const schemaCount = Object.keys(schemas).length;
1821
+ const validation = validateSpec(sortedSpec);
1822
+ return {
1823
+ outputPath: output,
1824
+ pathCount,
1825
+ operationCount,
1826
+ schemaCount,
1827
+ validation
1828
+ };
1829
+ };
1830
+
1831
+ export { loadConfig as a, loadAndResolveConfig as b, categorizeBrokenRefs as c, defineConfig as d, formatValidationResult as e, findConfigFile as f, generate as g, loadConfigFromFile as l, resolveConfig as r, validateSpec as v };