genoc 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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/analyzer/naming.d.ts +24 -0
  4. package/dist/analyzer/naming.js +122 -0
  5. package/dist/analyzer/path-analyzer.d.ts +53 -0
  6. package/dist/analyzer/path-analyzer.js +222 -0
  7. package/dist/analyzer/schema-mapper.d.ts +48 -0
  8. package/dist/analyzer/schema-mapper.js +435 -0
  9. package/dist/cli/app.d.ts +9 -0
  10. package/dist/cli/app.js +60 -0
  11. package/dist/cli/errors.d.ts +3 -0
  12. package/dist/cli/errors.js +6 -0
  13. package/dist/cli/impl.d.ts +3 -0
  14. package/dist/cli/impl.js +45 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +5 -0
  17. package/dist/generator/client-generator.d.ts +21 -0
  18. package/dist/generator/client-generator.js +287 -0
  19. package/dist/generator/contracts-generator.d.ts +16 -0
  20. package/dist/generator/contracts-generator.js +525 -0
  21. package/dist/generator/error-types.d.ts +24 -0
  22. package/dist/generator/error-types.js +94 -0
  23. package/dist/generator/method-generator.d.ts +9 -0
  24. package/dist/generator/method-generator.js +249 -0
  25. package/dist/index.d.ts +7 -0
  26. package/dist/index.js +8 -0
  27. package/dist/parser/ref-resolver.d.ts +24 -0
  28. package/dist/parser/ref-resolver.js +119 -0
  29. package/dist/parser/spec-reader.d.ts +4 -0
  30. package/dist/parser/spec-reader.js +116 -0
  31. package/dist/parser/validators.d.ts +7 -0
  32. package/dist/parser/validators.js +79 -0
  33. package/dist/parser/version/index.d.ts +18 -0
  34. package/dist/parser/version/index.js +16 -0
  35. package/dist/parser/version/normalized-spec.d.ts +199 -0
  36. package/dist/parser/version/normalized-spec.js +1 -0
  37. package/dist/parser/version/registry.d.ts +28 -0
  38. package/dist/parser/version/registry.js +44 -0
  39. package/dist/parser/version/v3.0/index.d.ts +3 -0
  40. package/dist/parser/version/v3.0/index.js +3 -0
  41. package/dist/parser/version/v3.0/normalizer.d.ts +15 -0
  42. package/dist/parser/version/v3.0/normalizer.js +389 -0
  43. package/dist/parser/version/v3.0/strategy.d.ts +27 -0
  44. package/dist/parser/version/v3.0/strategy.js +96 -0
  45. package/dist/parser/version/v3.0/validator.d.ts +13 -0
  46. package/dist/parser/version/v3.0/validator.js +117 -0
  47. package/dist/parser/version/v3.1/index.d.ts +1 -0
  48. package/dist/parser/version/v3.1/index.js +1 -0
  49. package/dist/parser/version/v3.1/strategy.d.ts +42 -0
  50. package/dist/parser/version/v3.1/strategy.js +513 -0
  51. package/dist/parser/version/v3.2/index.d.ts +4 -0
  52. package/dist/parser/version/v3.2/index.js +4 -0
  53. package/dist/parser/version/v3.2/strategy.d.ts +39 -0
  54. package/dist/parser/version/v3.2/strategy.js +57 -0
  55. package/dist/parser/version/version-detector.d.ts +4 -0
  56. package/dist/parser/version/version-detector.js +34 -0
  57. package/dist/parser/version/version-strategy.d.ts +31 -0
  58. package/dist/parser/version/version-strategy.js +1 -0
  59. package/dist/types/client.d.ts +25 -0
  60. package/dist/types/client.js +1 -0
  61. package/dist/types/contracts.d.ts +13 -0
  62. package/dist/types/contracts.js +1 -0
  63. package/dist/types/openapi.d.ts +173 -0
  64. package/dist/types/openapi.js +1 -0
  65. package/dist/utils/case.d.ts +5 -0
  66. package/dist/utils/case.js +51 -0
  67. package/dist/utils/generator-helpers.d.ts +23 -0
  68. package/dist/utils/generator-helpers.js +66 -0
  69. package/dist/utils/string.d.ts +34 -0
  70. package/dist/utils/string.js +182 -0
  71. package/dist/utils/url.d.ts +10 -0
  72. package/dist/utils/url.js +40 -0
  73. package/package.json +60 -0
@@ -0,0 +1,48 @@
1
+ import { RefResolver } from '../parser/ref-resolver.js';
2
+ import type { TypeMappingResult } from '../types/contracts.js';
3
+ import type { SchemaObject, ReferenceObject } from '../types/openapi.js';
4
+ /**
5
+ * Callback to customize how $ref strings are converted to TypeScript type names.
6
+ */
7
+ export type TypeNameGenerator = (refString: string) => string;
8
+ /**
9
+ * SchemaMapper converts OpenAPI 3.1 Schema Objects to TypeScript type strings.
10
+ *
11
+ * Handles primitives, objects, arrays, enums, combinators (allOf/oneOf/anyOf),
12
+ * references, nullable types, and readOnly/writeOnly context filtering.
13
+ */
14
+ export declare class SchemaMapper {
15
+ private readonly resolver;
16
+ private readonly typeNameGenerator;
17
+ private readonly discriminatorTargets;
18
+ private readonly reservedNames;
19
+ private readonly brandedTypes;
20
+ private static nullableWarned;
21
+ constructor(resolver: RefResolver, typeNameGenerator?: TypeNameGenerator, discriminatorTargets?: Map<string, {
22
+ propertyName: string;
23
+ literalValue: string;
24
+ }>, reservedNames?: Set<string>);
25
+ getBrandedTypes(): Map<string, {
26
+ name: string;
27
+ format: string;
28
+ baseType: string;
29
+ }>;
30
+ /**
31
+ * Convert an OpenAPI Schema Object to a TypeScript type string.
32
+ *
33
+ * @param schema - The schema to convert
34
+ * @param name - Optional name for the schema (produces object literal type for objects)
35
+ * @param context - Optional context for readOnly/writeOnly filtering
36
+ * @returns TypeMappingResult with tsType string and imports array
37
+ */
38
+ mapSchema(schema: SchemaObject | ReferenceObject, name?: string, context?: 'request' | 'response'): TypeMappingResult;
39
+ private getBrandTypeName;
40
+ private mapInternal;
41
+ private resolveDiscriminatorInfo;
42
+ private mapEnumValues;
43
+ private mapCombinator;
44
+ private mapDiscriminatedUnion;
45
+ private resolveDiscriminantValue;
46
+ private mapArray;
47
+ private mapObject;
48
+ }
@@ -0,0 +1,435 @@
1
+ import { RefResolver } from '../parser/ref-resolver.js';
2
+ import { formatToBrandTypeName } from '../utils/case.js';
3
+ import { quoteKey } from '../utils/string.js';
4
+ /**
5
+ * Default: extracts the last segment of the JSON pointer.
6
+ * "#/components/schemas/User" -> "User"
7
+ */
8
+ function defaultTypeNameGenerator(refString) {
9
+ const segments = refString.split('/');
10
+ return segments[segments.length - 1] || 'unknown';
11
+ }
12
+ function isRefObject(obj) {
13
+ return (obj !== null &&
14
+ typeof obj === 'object' &&
15
+ !Array.isArray(obj) &&
16
+ '$ref' in obj);
17
+ }
18
+ function isComplexType(tsType) {
19
+ return (tsType.includes(' ') || tsType.includes('{') || tsType.includes('|') || tsType.includes('&'));
20
+ }
21
+ function needsParens(tsType) {
22
+ return tsType.includes(' | ') || tsType.includes(' & ');
23
+ }
24
+ /**
25
+ * SchemaMapper converts OpenAPI 3.1 Schema Objects to TypeScript type strings.
26
+ *
27
+ * Handles primitives, objects, arrays, enums, combinators (allOf/oneOf/anyOf),
28
+ * references, nullable types, and readOnly/writeOnly context filtering.
29
+ */
30
+ export class SchemaMapper {
31
+ resolver;
32
+ typeNameGenerator;
33
+ discriminatorTargets;
34
+ reservedNames;
35
+ brandedTypes = new Map();
36
+ static nullableWarned = false;
37
+ constructor(resolver, typeNameGenerator, discriminatorTargets, reservedNames) {
38
+ this.resolver = resolver;
39
+ this.typeNameGenerator = typeNameGenerator ?? defaultTypeNameGenerator;
40
+ this.discriminatorTargets = discriminatorTargets ?? new Map();
41
+ this.reservedNames = reservedNames ?? new Set();
42
+ }
43
+ getBrandedTypes() {
44
+ return this.brandedTypes;
45
+ }
46
+ /**
47
+ * Convert an OpenAPI Schema Object to a TypeScript type string.
48
+ *
49
+ * @param schema - The schema to convert
50
+ * @param name - Optional name for the schema (produces object literal type for objects)
51
+ * @param context - Optional context for readOnly/writeOnly filtering
52
+ * @returns TypeMappingResult with tsType string and imports array
53
+ */
54
+ mapSchema(schema, name, context) {
55
+ if (typeof schema === 'boolean') {
56
+ return { tsType: schema ? 'unknown' : 'never', imports: [] };
57
+ }
58
+ const visited = new Set();
59
+ const result = this.mapInternal(schema, name, context, visited);
60
+ if (name && this.discriminatorTargets.has(name)) {
61
+ const target = this.discriminatorTargets.get(name);
62
+ result.tsType += ` & { ${quoteKey(target.propertyName)}: '${target.literalValue}' }`;
63
+ }
64
+ return result;
65
+ }
66
+ getBrandTypeName(format, openApiType) {
67
+ if (!format || format.trim() === '')
68
+ return null;
69
+ if (format === 'binary' || format === 'byte')
70
+ return null;
71
+ const brandName = formatToBrandTypeName(format, openApiType);
72
+ if (this.reservedNames.has(brandName))
73
+ return null;
74
+ return brandName;
75
+ }
76
+ mapInternal(schema, name, context, visited) {
77
+ if (isRefObject(schema)) {
78
+ const refStr = schema.$ref;
79
+ const refName = this.typeNameGenerator(refStr);
80
+ let resolved;
81
+ try {
82
+ resolved = this.resolver.resolveSchema(schema);
83
+ }
84
+ catch {
85
+ // unresolvable — skip discriminator detection
86
+ }
87
+ if (resolved) {
88
+ const discInfo = this.resolveDiscriminatorInfo(resolved, refStr);
89
+ if (discInfo) {
90
+ const expanded = this.mapInternal(resolved, undefined, context, visited);
91
+ expanded.tsType += ` & { ${quoteKey(discInfo.propertyName)}: '${discInfo.literalValue}' }`;
92
+ return expanded;
93
+ }
94
+ }
95
+ return { tsType: refName, imports: [refName] };
96
+ }
97
+ const s = schema;
98
+ if (visited.has(s)) {
99
+ if (name) {
100
+ return { tsType: name, imports: [] };
101
+ }
102
+ return { tsType: 'unknown', imports: [] };
103
+ }
104
+ visited.add(s);
105
+ // TODO: Remove deprecated nullable warning and handling when OpenAPI 3.1 support is complete
106
+ // The 'nullable' property is deprecated in OpenAPI 3.1 in favor of type arrays like ["string", "null"]
107
+ // This warning should be removed once full type array support is implemented
108
+ if (s.nullable === true && !SchemaMapper.nullableWarned) {
109
+ // TODO: Replace with structured logging solution
110
+ // oxlint-disable-next-line no-console
111
+ console.warn('Warning: \'nullable\' is deprecated in OpenAPI 3.1. Use \'type: ["string", "null"]\' instead.');
112
+ SchemaMapper.nullableWarned = true;
113
+ }
114
+ if (s.enum !== undefined && s.enum.length > 0) {
115
+ let tsType = this.mapEnumValues(s.enum);
116
+ if (s.nullable === true) {
117
+ tsType = `${tsType} | null`;
118
+ }
119
+ return { tsType, imports: [] };
120
+ }
121
+ if (s.allOf !== undefined && s.allOf.length > 0) {
122
+ const result = this.mapCombinator(s.allOf, '&', context, visited);
123
+ if (s.nullable === true) {
124
+ return {
125
+ tsType: needsParens(result.tsType)
126
+ ? `(${result.tsType}) | null`
127
+ : `${result.tsType} | null`,
128
+ imports: result.imports,
129
+ };
130
+ }
131
+ return result;
132
+ }
133
+ if (s.oneOf !== undefined && s.oneOf.length > 0) {
134
+ const result = s.discriminator
135
+ ? this.mapDiscriminatedUnion(s.oneOf, s.discriminator, context, visited)
136
+ : this.mapCombinator(s.oneOf, '|', context, visited);
137
+ if (s.nullable === true) {
138
+ return {
139
+ tsType: needsParens(result.tsType)
140
+ ? `(${result.tsType}) | null`
141
+ : `${result.tsType} | null`,
142
+ imports: result.imports,
143
+ };
144
+ }
145
+ return result;
146
+ }
147
+ if (s.anyOf !== undefined && s.anyOf.length > 0) {
148
+ const result = s.discriminator
149
+ ? this.mapDiscriminatedUnion(s.anyOf, s.discriminator, context, visited)
150
+ : this.mapCombinator(s.anyOf, '|', context, visited);
151
+ if (s.nullable === true) {
152
+ return {
153
+ tsType: needsParens(result.tsType)
154
+ ? `(${result.tsType}) | null`
155
+ : `${result.tsType} | null`,
156
+ imports: result.imports,
157
+ };
158
+ }
159
+ return result;
160
+ }
161
+ if (s.type === undefined) {
162
+ return { tsType: 'unknown', imports: [] };
163
+ }
164
+ if (Array.isArray(s.type)) {
165
+ const nonNull = s.type.filter((t) => t !== 'null');
166
+ const hasNull = s.type.includes('null');
167
+ if (nonNull.length === 0) {
168
+ return { tsType: 'null', imports: [] };
169
+ }
170
+ const baseResult = this.mapInternal({ ...s, type: nonNull[0] }, name, context, new Set(visited));
171
+ if (hasNull) {
172
+ return {
173
+ tsType: `${baseResult.tsType} | null`,
174
+ imports: baseResult.imports,
175
+ };
176
+ }
177
+ return baseResult;
178
+ }
179
+ switch (s.type) {
180
+ case 'string': {
181
+ const brandName = this.getBrandTypeName(s.format, 'string');
182
+ if (brandName) {
183
+ this.brandedTypes.set(`${s.format}:string`, {
184
+ name: brandName,
185
+ format: s.format,
186
+ baseType: 'string',
187
+ });
188
+ return {
189
+ tsType: s.nullable === true ? `${brandName} | null` : brandName,
190
+ imports: [brandName],
191
+ };
192
+ }
193
+ return {
194
+ tsType: s.nullable === true ? 'string | null' : 'string',
195
+ imports: [],
196
+ };
197
+ }
198
+ case 'number':
199
+ case 'integer': {
200
+ const brandName = this.getBrandTypeName(s.format, s.type);
201
+ if (brandName) {
202
+ this.brandedTypes.set(`${s.format}:number`, {
203
+ name: brandName,
204
+ format: s.format,
205
+ baseType: 'number',
206
+ });
207
+ return {
208
+ tsType: s.nullable === true ? `${brandName} | null` : brandName,
209
+ imports: [brandName],
210
+ };
211
+ }
212
+ return {
213
+ tsType: s.nullable === true ? 'number | null' : 'number',
214
+ imports: [],
215
+ };
216
+ }
217
+ case 'boolean':
218
+ return {
219
+ tsType: s.nullable === true ? 'boolean | null' : 'boolean',
220
+ imports: [],
221
+ };
222
+ case 'null':
223
+ return { tsType: 'null', imports: [] };
224
+ case 'array':
225
+ return this.mapArray(s, context, visited);
226
+ case 'object':
227
+ return this.mapObject(s, name, context, visited);
228
+ default:
229
+ return { tsType: 'unknown', imports: [] };
230
+ }
231
+ }
232
+ resolveDiscriminatorInfo(schema, refStr) {
233
+ const schemaName = refStr.split('/').pop();
234
+ const target = this.discriminatorTargets.get(schemaName);
235
+ if (target)
236
+ return target;
237
+ if (!schema.allOf || schema.allOf.length === 0)
238
+ return undefined;
239
+ for (const item of schema.allOf) {
240
+ if (isRefObject(item)) {
241
+ const resolved = this.resolver.resolveSchema(item);
242
+ if (resolved.discriminator) {
243
+ const disc = resolved.discriminator;
244
+ if (disc.mapping) {
245
+ for (const [value, ref] of Object.entries(disc.mapping)) {
246
+ if (ref === refStr) {
247
+ return { propertyName: disc.propertyName, literalValue: value };
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ return undefined;
255
+ }
256
+ mapEnumValues(values) {
257
+ return values
258
+ .map((v) => {
259
+ if (typeof v === 'string')
260
+ return `'${v}'`;
261
+ if (typeof v === 'number')
262
+ return String(v);
263
+ if (typeof v === 'boolean')
264
+ return String(v);
265
+ if (v === null)
266
+ return 'null';
267
+ return 'unknown';
268
+ })
269
+ .join(' | ');
270
+ }
271
+ mapCombinator(schemas, kind, context, visited) {
272
+ const results = schemas.map((s) => this.mapInternal(s, undefined, context, visited));
273
+ const allImports = [];
274
+ for (const r of results) {
275
+ allImports.push(...r.imports);
276
+ }
277
+ const separator = kind === '&' ? ' & ' : ' | ';
278
+ const parts = results.map((r) => {
279
+ if (kind === '&' && r.tsType.includes(' | ')) {
280
+ return `(${r.tsType})`;
281
+ }
282
+ return r.tsType;
283
+ });
284
+ return { tsType: parts.join(separator), imports: allImports };
285
+ }
286
+ mapDiscriminatedUnion(schemas, discriminator, context, visited) {
287
+ const propertyName = discriminator.propertyName;
288
+ const mapping = discriminator.mapping;
289
+ const allImports = [];
290
+ const parts = [];
291
+ for (const schema of schemas) {
292
+ const discriminantValue = this.resolveDiscriminantValue(schema, propertyName, mapping);
293
+ const variantResult = this.mapInternal(schema, undefined, context, visited);
294
+ allImports.push(...variantResult.imports);
295
+ const quotedProp = quoteKey(propertyName);
296
+ parts.push(`({ ${quotedProp}: '${discriminantValue}' } & ${variantResult.tsType})`);
297
+ }
298
+ return { tsType: parts.join(' | '), imports: allImports };
299
+ }
300
+ resolveDiscriminantValue(schema, propertyName, mapping) {
301
+ // Try explicit mapping first
302
+ if (mapping && isRefObject(schema)) {
303
+ const refStr = schema.$ref;
304
+ for (const [value, ref] of Object.entries(mapping)) {
305
+ if (ref === refStr)
306
+ return value;
307
+ }
308
+ }
309
+ // Infer from const on the discriminator property
310
+ const resolved = isRefObject(schema)
311
+ ? this.resolver.resolve(schema)
312
+ : schema;
313
+ const propSchema = resolved.properties?.[propertyName];
314
+ if (propSchema && propSchema.const !== undefined) {
315
+ return String(propSchema.const);
316
+ }
317
+ // Fallback: use the type name from $ref
318
+ if (isRefObject(schema)) {
319
+ const refStr = schema.$ref;
320
+ return this.typeNameGenerator(refStr);
321
+ }
322
+ return 'unknown';
323
+ }
324
+ mapArray(schema, context, visited) {
325
+ if (!schema.items) {
326
+ return {
327
+ tsType: schema.nullable === true ? 'unknown[] | null' : 'unknown[]',
328
+ imports: [],
329
+ };
330
+ }
331
+ const itemResult = this.mapInternal(schema.items, undefined, context, visited);
332
+ const tsType = isComplexType(itemResult.tsType)
333
+ ? `Array<${itemResult.tsType}>`
334
+ : `${itemResult.tsType}[]`;
335
+ return {
336
+ tsType: schema.nullable === true ? `${tsType} | null` : tsType,
337
+ imports: itemResult.imports,
338
+ };
339
+ }
340
+ mapObject(schema, name, context, visited) {
341
+ const properties = schema.properties ?? {};
342
+ const requiredSet = new Set(schema.required ?? []);
343
+ const propNames = Object.keys(properties);
344
+ const filteredPropNames = propNames.filter((propName) => {
345
+ const propSchema = properties[propName];
346
+ const resolved = isRefObject(propSchema)
347
+ ? this.resolver.resolve(propSchema)
348
+ : propSchema;
349
+ if (context === 'response' && resolved.writeOnly === true)
350
+ return false;
351
+ if (context === 'request' && resolved.readOnly === true)
352
+ return false;
353
+ return true;
354
+ });
355
+ const propEntries = [];
356
+ const allImports = [];
357
+ if (schema.discriminator) {
358
+ const discPropName = schema.discriminator.propertyName;
359
+ const discIndex = filteredPropNames.indexOf(discPropName);
360
+ if (discIndex !== -1) {
361
+ filteredPropNames.splice(discIndex, 1);
362
+ }
363
+ propEntries.push(`"${discPropName}": string`);
364
+ }
365
+ for (const propName of filteredPropNames) {
366
+ const propSchema = properties[propName];
367
+ const propResult = this.mapInternal(propSchema, undefined, context, visited);
368
+ allImports.push(...propResult.imports);
369
+ const optional = requiredSet.has(propName) ? '' : '?';
370
+ const quotedName = quoteKey(propName);
371
+ propEntries.push(`${quotedName}${optional}: ${propResult.tsType}`);
372
+ }
373
+ const additionalProps = schema.additionalProperties;
374
+ let indexSignature = null;
375
+ if (additionalProps === false) {
376
+ void 0;
377
+ }
378
+ else if (additionalProps === true) {
379
+ indexSignature = '[key: string]: unknown';
380
+ }
381
+ else if (additionalProps !== undefined && typeof additionalProps === 'object') {
382
+ const addPropResult = this.mapInternal(additionalProps, undefined, context, visited);
383
+ allImports.push(...addPropResult.imports);
384
+ indexSignature = `[key: string]: ${addPropResult.tsType}`;
385
+ }
386
+ const hasProps = propEntries.length > 0;
387
+ const hasIndex = indexSignature !== null;
388
+ let result;
389
+ if (!hasProps && !hasIndex) {
390
+ if (name) {
391
+ result = { tsType: `{}`, imports: allImports };
392
+ }
393
+ else {
394
+ result = { tsType: 'Record<string, unknown>', imports: allImports };
395
+ }
396
+ }
397
+ else if (!hasProps && hasIndex) {
398
+ const valueType = indexSignature.replace('[key: string]: ', '');
399
+ if (name) {
400
+ result = {
401
+ tsType: `{ ${indexSignature}; }`,
402
+ imports: allImports,
403
+ };
404
+ }
405
+ else {
406
+ result = {
407
+ tsType: `Record<string, ${valueType}>`,
408
+ imports: allImports,
409
+ };
410
+ }
411
+ }
412
+ else {
413
+ const propsBody = propEntries.join('; ') + ';';
414
+ if (hasIndex) {
415
+ result = {
416
+ tsType: `{ ${propsBody} } & { ${indexSignature}; }`,
417
+ imports: allImports,
418
+ };
419
+ }
420
+ else {
421
+ result = {
422
+ tsType: `{ ${propsBody} }`,
423
+ imports: allImports,
424
+ };
425
+ }
426
+ }
427
+ if (schema.nullable === true) {
428
+ return {
429
+ tsType: `${result.tsType} | null`,
430
+ imports: result.imports,
431
+ };
432
+ }
433
+ return result;
434
+ }
435
+ }
@@ -0,0 +1,9 @@
1
+ interface Flags {
2
+ outputDir: string;
3
+ methodNameStrategy: 'path-based' | 'operationId' | 'operationId-with-fallback';
4
+ specVersion?: string;
5
+ strictVersion: boolean;
6
+ }
7
+ export type AppFlags = Flags;
8
+ export declare const app: import("@stricli/core").Application<import("@stricli/core").CommandContext>;
9
+ export {};
@@ -0,0 +1,60 @@
1
+ import { readFileSync } from 'fs';
2
+ import { buildCommand, buildApplication } from '@stricli/core';
3
+ const VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url)).toString()).version;
4
+ const command = buildCommand({
5
+ loader: async () => import('./impl.js'),
6
+ parameters: {
7
+ flags: {
8
+ outputDir: {
9
+ kind: 'parsed',
10
+ parse: String,
11
+ brief: 'Directory to write generated files',
12
+ placeholder: 'dir',
13
+ },
14
+ methodNameStrategy: {
15
+ kind: 'enum',
16
+ values: ['path-based', 'operationId', 'operationId-with-fallback'],
17
+ default: 'path-based',
18
+ brief: 'Method naming strategy',
19
+ },
20
+ specVersion: {
21
+ kind: 'parsed',
22
+ parse: String,
23
+ brief: 'Override auto-detected OpenAPI version (e.g. "3.0", "3.1")',
24
+ optional: true,
25
+ placeholder: 'version',
26
+ },
27
+ strictVersion: {
28
+ kind: 'boolean',
29
+ default: true,
30
+ brief: 'Enable strict version checking',
31
+ },
32
+ },
33
+ positional: {
34
+ kind: 'tuple',
35
+ parameters: [
36
+ {
37
+ brief: 'Path or URL to OpenAPI 3.0 or 3.1 spec (JSON/YAML)',
38
+ parse: String,
39
+ placeholder: 'spec',
40
+ },
41
+ ],
42
+ },
43
+ },
44
+ docs: {
45
+ brief: 'Generate typed HTTP clients from OpenAPI specifications',
46
+ fullDescription: 'Generate typed TypeScript HTTP clients from OpenAPI 3.0 / 3.1 specs (JSON/YAML, file or URL).',
47
+ },
48
+ });
49
+ export const app = buildApplication(command, {
50
+ name: 'genoc',
51
+ versionInfo: {
52
+ currentVersion: VERSION,
53
+ },
54
+ scanner: {
55
+ caseStyle: 'allow-kebab-for-camel',
56
+ },
57
+ documentation: {
58
+ caseStyle: 'convert-camel-to-kebab',
59
+ },
60
+ });
@@ -0,0 +1,3 @@
1
+ export declare class UserError extends Error {
2
+ constructor(message: string);
3
+ }
@@ -0,0 +1,6 @@
1
+ export class UserError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'UserError';
5
+ }
6
+ }
@@ -0,0 +1,3 @@
1
+ import type { CommandContext } from '@stricli/core';
2
+ import type { AppFlags as Flags } from './app.js';
3
+ export default function (this: CommandContext, flags: Flags, spec: string): Promise<void | Error>;
@@ -0,0 +1,45 @@
1
+ import { generateFullOutput } from '../generator/client-generator.js';
2
+ import { load } from '../parser/spec-reader.js';
3
+ import { validateSpec } from '../parser/validators.js';
4
+ import { defaultRegistry } from '../parser/version/index.js';
5
+ import { UserError } from './errors.js';
6
+ export default async function (flags, spec) {
7
+ try {
8
+ this.process.stdout.write(`Loading spec from ${spec}...\n`);
9
+ const doc = await load(spec);
10
+ this.process.stdout.write(`Loaded OpenAPI ${doc.openapi} spec\n`);
11
+ const strategy = flags.specVersion
12
+ ? defaultRegistry.get(flags.specVersion)
13
+ : defaultRegistry.detectAndResolve(doc);
14
+ if (flags.specVersion && flags.strictVersion !== false) {
15
+ const detected = defaultRegistry.detectAndResolve(doc);
16
+ if (detected.version() !== flags.specVersion) {
17
+ this.process.stderr.write(`Warning: Specified version ${flags.specVersion} does not match detected version ${detected.version()}\n`);
18
+ }
19
+ }
20
+ const validation = validateSpec(doc, strategy);
21
+ if (!validation.valid) {
22
+ throw new UserError(`Invalid OpenAPI specification:\n${validation.errors.map((e) => ` - ${e}`).join('\n')}`);
23
+ }
24
+ const config = {
25
+ input: spec,
26
+ outputDir: flags.outputDir,
27
+ methodNameStrategy: flags.methodNameStrategy || 'path-based',
28
+ specVersion: flags.specVersion,
29
+ strictVersion: flags.strictVersion,
30
+ };
31
+ const preserveRefSiblings = strategy.version() === '3.1';
32
+ this.process.stdout.write('Generating client...\n');
33
+ await generateFullOutput(doc, config, { preserveRefSiblings });
34
+ this.process.stdout.write(`✅ Success! Generated client files:\n`);
35
+ this.process.stdout.write(` - ${flags.outputDir}/contracts.ts\n`);
36
+ this.process.stdout.write(` - ${flags.outputDir}/client.ts\n`);
37
+ }
38
+ catch (error) {
39
+ if (error instanceof UserError) {
40
+ return error;
41
+ }
42
+ const message = error instanceof Error ? error.message : String(error);
43
+ return new Error(message);
44
+ }
45
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '@stricli/core';
3
+ import { app } from './app.js';
4
+ await run(app, process.argv.slice(2), { process });
5
+ process.exit(typeof process.exitCode === 'number' ? process.exitCode : 0);
@@ -0,0 +1,21 @@
1
+ import type { GeneratorConfig } from '../types/client.js';
2
+ import type { OpenAPIDocument } from '../types/openapi.js';
3
+ export type ApiClient = {
4
+ [key: string]: (...args: any[]) => Promise<any>;
5
+ };
6
+ /** Options for controlling generation behavior. */
7
+ export interface GenerationOptions {
8
+ /** When true, sibling properties alongside $ref are preserved (OpenAPI 3.1 behavior). */
9
+ preserveRefSiblings?: boolean;
10
+ }
11
+ /**
12
+ * Generate both the contracts and client file content from an OpenAPI document.
13
+ */
14
+ export declare function generateClient(doc: OpenAPIDocument, config: GeneratorConfig, options?: GenerationOptions): {
15
+ contracts: string;
16
+ client: string;
17
+ };
18
+ /**
19
+ * Generate and write both output files to disk.
20
+ */
21
+ export declare function generateFullOutput(doc: OpenAPIDocument, config: GeneratorConfig, options?: GenerationOptions): Promise<void>;