enlace-openapi 0.0.1-beta.1

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,103 @@
1
+ #!/usr/bin/env node
2
+ type JSONSchema = {
3
+ type?: string;
4
+ items?: JSONSchema;
5
+ properties?: Record<string, JSONSchema>;
6
+ required?: string[];
7
+ $ref?: string;
8
+ oneOf?: JSONSchema[];
9
+ anyOf?: JSONSchema[];
10
+ allOf?: JSONSchema[];
11
+ enum?: (string | number | boolean | null)[];
12
+ const?: unknown;
13
+ additionalProperties?: boolean | JSONSchema;
14
+ nullable?: boolean;
15
+ format?: string;
16
+ description?: string;
17
+ };
18
+ type OpenAPIOperation = {
19
+ operationId?: string;
20
+ summary?: string;
21
+ description?: string;
22
+ tags?: string[];
23
+ parameters?: OpenAPIParameter[];
24
+ requestBody?: {
25
+ required?: boolean;
26
+ content: {
27
+ "application/json": {
28
+ schema: JSONSchema;
29
+ };
30
+ };
31
+ };
32
+ responses: Record<string, {
33
+ description: string;
34
+ content?: {
35
+ "application/json": {
36
+ schema: JSONSchema;
37
+ };
38
+ };
39
+ }>;
40
+ };
41
+ type OpenAPIParameter = {
42
+ name: string;
43
+ in: "path" | "query" | "header" | "cookie";
44
+ required?: boolean;
45
+ schema: JSONSchema;
46
+ description?: string;
47
+ };
48
+ type OpenAPIPathItem = {
49
+ get?: OpenAPIOperation;
50
+ post?: OpenAPIOperation;
51
+ put?: OpenAPIOperation;
52
+ patch?: OpenAPIOperation;
53
+ delete?: OpenAPIOperation;
54
+ parameters?: OpenAPIParameter[];
55
+ };
56
+ type OpenAPISpec = {
57
+ openapi: "3.0.0";
58
+ info: {
59
+ title: string;
60
+ version: string;
61
+ description?: string;
62
+ };
63
+ servers?: {
64
+ url: string;
65
+ description?: string;
66
+ }[];
67
+ paths: Record<string, OpenAPIPathItem>;
68
+ components?: {
69
+ schemas?: Record<string, JSONSchema>;
70
+ };
71
+ };
72
+ type ParsedEndpoint = {
73
+ path: string;
74
+ method: "get" | "post" | "put" | "patch" | "delete";
75
+ responseSchema: JSONSchema;
76
+ requestBodySchema?: JSONSchema;
77
+ errorSchema?: JSONSchema;
78
+ pathParams: string[];
79
+ };
80
+ type CLIOptions = {
81
+ schema: string;
82
+ type: string;
83
+ output?: string;
84
+ title?: string;
85
+ version?: string;
86
+ baseUrl?: string;
87
+ };
88
+
89
+ type ParseResult = {
90
+ endpoints: ParsedEndpoint[];
91
+ schemas: Map<string, JSONSchema>;
92
+ };
93
+ declare function parseSchema(schemaFilePath: string, typeName: string): ParseResult;
94
+
95
+ type GeneratorOptions = {
96
+ title?: string;
97
+ version?: string;
98
+ description?: string;
99
+ baseUrl?: string;
100
+ };
101
+ declare function generateOpenAPISpec(endpoints: ParsedEndpoint[], schemas: Map<string, JSONSchema>, options?: GeneratorOptions): OpenAPISpec;
102
+
103
+ export { type CLIOptions, type JSONSchema, type OpenAPIOperation, type OpenAPIParameter, type OpenAPIPathItem, type OpenAPISpec, type ParsedEndpoint, generateOpenAPISpec, parseSchema };
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ type JSONSchema = {
3
+ type?: string;
4
+ items?: JSONSchema;
5
+ properties?: Record<string, JSONSchema>;
6
+ required?: string[];
7
+ $ref?: string;
8
+ oneOf?: JSONSchema[];
9
+ anyOf?: JSONSchema[];
10
+ allOf?: JSONSchema[];
11
+ enum?: (string | number | boolean | null)[];
12
+ const?: unknown;
13
+ additionalProperties?: boolean | JSONSchema;
14
+ nullable?: boolean;
15
+ format?: string;
16
+ description?: string;
17
+ };
18
+ type OpenAPIOperation = {
19
+ operationId?: string;
20
+ summary?: string;
21
+ description?: string;
22
+ tags?: string[];
23
+ parameters?: OpenAPIParameter[];
24
+ requestBody?: {
25
+ required?: boolean;
26
+ content: {
27
+ "application/json": {
28
+ schema: JSONSchema;
29
+ };
30
+ };
31
+ };
32
+ responses: Record<string, {
33
+ description: string;
34
+ content?: {
35
+ "application/json": {
36
+ schema: JSONSchema;
37
+ };
38
+ };
39
+ }>;
40
+ };
41
+ type OpenAPIParameter = {
42
+ name: string;
43
+ in: "path" | "query" | "header" | "cookie";
44
+ required?: boolean;
45
+ schema: JSONSchema;
46
+ description?: string;
47
+ };
48
+ type OpenAPIPathItem = {
49
+ get?: OpenAPIOperation;
50
+ post?: OpenAPIOperation;
51
+ put?: OpenAPIOperation;
52
+ patch?: OpenAPIOperation;
53
+ delete?: OpenAPIOperation;
54
+ parameters?: OpenAPIParameter[];
55
+ };
56
+ type OpenAPISpec = {
57
+ openapi: "3.0.0";
58
+ info: {
59
+ title: string;
60
+ version: string;
61
+ description?: string;
62
+ };
63
+ servers?: {
64
+ url: string;
65
+ description?: string;
66
+ }[];
67
+ paths: Record<string, OpenAPIPathItem>;
68
+ components?: {
69
+ schemas?: Record<string, JSONSchema>;
70
+ };
71
+ };
72
+ type ParsedEndpoint = {
73
+ path: string;
74
+ method: "get" | "post" | "put" | "patch" | "delete";
75
+ responseSchema: JSONSchema;
76
+ requestBodySchema?: JSONSchema;
77
+ errorSchema?: JSONSchema;
78
+ pathParams: string[];
79
+ };
80
+ type CLIOptions = {
81
+ schema: string;
82
+ type: string;
83
+ output?: string;
84
+ title?: string;
85
+ version?: string;
86
+ baseUrl?: string;
87
+ };
88
+
89
+ type ParseResult = {
90
+ endpoints: ParsedEndpoint[];
91
+ schemas: Map<string, JSONSchema>;
92
+ };
93
+ declare function parseSchema(schemaFilePath: string, typeName: string): ParseResult;
94
+
95
+ type GeneratorOptions = {
96
+ title?: string;
97
+ version?: string;
98
+ description?: string;
99
+ baseUrl?: string;
100
+ };
101
+ declare function generateOpenAPISpec(endpoints: ParsedEndpoint[], schemas: Map<string, JSONSchema>, options?: GeneratorOptions): OpenAPISpec;
102
+
103
+ export { type CLIOptions, type JSONSchema, type OpenAPIOperation, type OpenAPIParameter, type OpenAPIPathItem, type OpenAPISpec, type ParsedEndpoint, generateOpenAPISpec, parseSchema };
package/dist/index.js ADDED
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var src_exports = {};
33
+ __export(src_exports, {
34
+ generateOpenAPISpec: () => generateOpenAPISpec,
35
+ parseSchema: () => parseSchema
36
+ });
37
+ module.exports = __toCommonJS(src_exports);
38
+ var import_commander = require("commander");
39
+ var import_fs = __toESM(require("fs"));
40
+
41
+ // src/parser.ts
42
+ var import_typescript2 = __toESM(require("typescript"));
43
+ var import_path = __toESM(require("path"));
44
+
45
+ // src/type-to-schema.ts
46
+ var import_typescript = __toESM(require("typescript"));
47
+ function createSchemaContext(checker) {
48
+ return {
49
+ checker,
50
+ schemas: /* @__PURE__ */ new Map(),
51
+ visitedTypes: /* @__PURE__ */ new Set()
52
+ };
53
+ }
54
+ function typeToSchema(type, ctx) {
55
+ const { checker } = ctx;
56
+ if (type.flags & import_typescript.default.TypeFlags.String) {
57
+ return { type: "string" };
58
+ }
59
+ if (type.flags & import_typescript.default.TypeFlags.Number) {
60
+ return { type: "number" };
61
+ }
62
+ if (type.flags & import_typescript.default.TypeFlags.Boolean) {
63
+ return { type: "boolean" };
64
+ }
65
+ if (type.flags & import_typescript.default.TypeFlags.Null) {
66
+ return { type: "null" };
67
+ }
68
+ if (type.flags & import_typescript.default.TypeFlags.Undefined || type.flags & import_typescript.default.TypeFlags.Void) {
69
+ return {};
70
+ }
71
+ if (type.flags & import_typescript.default.TypeFlags.Any || type.flags & import_typescript.default.TypeFlags.Unknown) {
72
+ return {};
73
+ }
74
+ if (type.flags & import_typescript.default.TypeFlags.Never) {
75
+ return {};
76
+ }
77
+ if (type.isStringLiteral()) {
78
+ return { type: "string", const: type.value };
79
+ }
80
+ if (type.isNumberLiteral()) {
81
+ return { type: "number", const: type.value };
82
+ }
83
+ if (type.flags & import_typescript.default.TypeFlags.BooleanLiteral) {
84
+ const intrinsicName = type.intrinsicName;
85
+ return { type: "boolean", const: intrinsicName === "true" };
86
+ }
87
+ if (type.isUnion()) {
88
+ const nonNullTypes = type.types.filter(
89
+ (t) => !(t.flags & import_typescript.default.TypeFlags.Null) && !(t.flags & import_typescript.default.TypeFlags.Undefined)
90
+ );
91
+ const hasNull = type.types.some((t) => t.flags & import_typescript.default.TypeFlags.Null);
92
+ if (nonNullTypes.length === 1 && hasNull) {
93
+ const schema = typeToSchema(nonNullTypes[0], ctx);
94
+ return { ...schema, nullable: true };
95
+ }
96
+ if (nonNullTypes.every((t) => t.isStringLiteral())) {
97
+ return {
98
+ type: "string",
99
+ enum: nonNullTypes.map((t) => t.value)
100
+ };
101
+ }
102
+ if (nonNullTypes.every((t) => t.isNumberLiteral())) {
103
+ return {
104
+ type: "number",
105
+ enum: nonNullTypes.map((t) => t.value)
106
+ };
107
+ }
108
+ return {
109
+ oneOf: nonNullTypes.map((t) => typeToSchema(t, ctx))
110
+ };
111
+ }
112
+ if (type.isIntersection()) {
113
+ return intersectionTypeToSchema(type, ctx);
114
+ }
115
+ if (checker.isArrayType(type)) {
116
+ const typeArgs = type.typeArguments;
117
+ if (typeArgs?.[0]) {
118
+ return {
119
+ type: "array",
120
+ items: typeToSchema(typeArgs[0], ctx)
121
+ };
122
+ }
123
+ return { type: "array" };
124
+ }
125
+ if (type.flags & import_typescript.default.TypeFlags.Object) {
126
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
127
+ const typeName = symbol?.getName();
128
+ if (typeName === "Date") {
129
+ return { type: "string", format: "date-time" };
130
+ }
131
+ if (typeName && typeName !== "__type" && typeName !== "Array" && !typeName.startsWith("__")) {
132
+ if (ctx.visitedTypes.has(type)) {
133
+ return { $ref: `#/components/schemas/${typeName}` };
134
+ }
135
+ if (!ctx.schemas.has(typeName)) {
136
+ ctx.visitedTypes.add(type);
137
+ const schema = objectTypeToSchema(type, ctx);
138
+ ctx.schemas.set(typeName, schema);
139
+ ctx.visitedTypes.delete(type);
140
+ }
141
+ return { $ref: `#/components/schemas/${typeName}` };
142
+ }
143
+ return objectTypeToSchema(type, ctx);
144
+ }
145
+ return {};
146
+ }
147
+ function objectTypeToSchema(type, ctx) {
148
+ const { checker } = ctx;
149
+ const properties = {};
150
+ const required = [];
151
+ const props = type.getProperties();
152
+ for (const prop of props) {
153
+ const propName = prop.getName();
154
+ const propType = checker.getTypeOfSymbol(prop);
155
+ const isOptional = prop.flags & import_typescript.default.SymbolFlags.Optional;
156
+ properties[propName] = typeToSchema(propType, ctx);
157
+ if (!isOptional) {
158
+ required.push(propName);
159
+ }
160
+ }
161
+ const schema = {
162
+ type: "object",
163
+ properties
164
+ };
165
+ if (required.length > 0) {
166
+ schema.required = required;
167
+ }
168
+ return schema;
169
+ }
170
+ function intersectionTypeToSchema(type, ctx) {
171
+ const { checker } = ctx;
172
+ const properties = {};
173
+ const required = [];
174
+ for (const t of type.types) {
175
+ const props = t.getProperties();
176
+ for (const prop of props) {
177
+ const propName = prop.getName();
178
+ if (propName.startsWith("__")) continue;
179
+ const propType = checker.getTypeOfSymbol(prop);
180
+ const isOptional = prop.flags & import_typescript.default.SymbolFlags.Optional;
181
+ properties[propName] = typeToSchema(propType, ctx);
182
+ if (!isOptional && !required.includes(propName)) {
183
+ required.push(propName);
184
+ }
185
+ }
186
+ }
187
+ const schema = {
188
+ type: "object",
189
+ properties
190
+ };
191
+ if (required.length > 0) {
192
+ schema.required = required;
193
+ }
194
+ return schema;
195
+ }
196
+
197
+ // src/parser.ts
198
+ var HTTP_METHODS = ["$get", "$post", "$put", "$patch", "$delete"];
199
+ function isHttpMethod(key) {
200
+ return HTTP_METHODS.includes(key);
201
+ }
202
+ function methodKeyToHttp(key) {
203
+ return key.slice(1);
204
+ }
205
+ function parseSchema(schemaFilePath, typeName) {
206
+ const absolutePath = import_path.default.resolve(schemaFilePath);
207
+ const schemaDir = import_path.default.dirname(absolutePath);
208
+ const configPath = import_typescript2.default.findConfigFile(schemaDir, import_typescript2.default.sys.fileExists, "tsconfig.json");
209
+ let compilerOptions = {
210
+ target: import_typescript2.default.ScriptTarget.ESNext,
211
+ module: import_typescript2.default.ModuleKind.NodeNext,
212
+ moduleResolution: import_typescript2.default.ModuleResolutionKind.NodeNext,
213
+ strict: true
214
+ };
215
+ if (configPath) {
216
+ const configFile = import_typescript2.default.readConfigFile(configPath, import_typescript2.default.sys.readFile);
217
+ if (!configFile.error) {
218
+ const parsed = import_typescript2.default.parseJsonConfigFileContent(
219
+ configFile.config,
220
+ import_typescript2.default.sys,
221
+ import_path.default.dirname(configPath)
222
+ );
223
+ compilerOptions = { ...compilerOptions, ...parsed.options };
224
+ }
225
+ }
226
+ const program2 = import_typescript2.default.createProgram([absolutePath], compilerOptions);
227
+ const checker = program2.getTypeChecker();
228
+ const sourceFile = program2.getSourceFile(absolutePath);
229
+ if (!sourceFile) {
230
+ throw new Error(`Could not find source file: ${absolutePath}`);
231
+ }
232
+ const schemaType = findExportedType(sourceFile, typeName, checker);
233
+ if (!schemaType) {
234
+ throw new Error(`Could not find exported type '${typeName}' in ${schemaFilePath}`);
235
+ }
236
+ const ctx = createSchemaContext(checker);
237
+ const endpoints = [];
238
+ walkSchemaType(schemaType, "", [], ctx, endpoints, checker);
239
+ return {
240
+ endpoints,
241
+ schemas: ctx.schemas
242
+ };
243
+ }
244
+ function findExportedType(sourceFile, typeName, checker) {
245
+ const symbol = checker.getSymbolAtLocation(sourceFile);
246
+ if (!symbol) return void 0;
247
+ const exports2 = checker.getExportsOfModule(symbol);
248
+ const typeSymbol = exports2.find((exp) => exp.getName() === typeName);
249
+ if (!typeSymbol) return void 0;
250
+ const declaredType = checker.getDeclaredTypeOfSymbol(typeSymbol);
251
+ return declaredType;
252
+ }
253
+ function walkSchemaType(type, currentPath, pathParams, ctx, endpoints, checker) {
254
+ const properties = type.getProperties();
255
+ for (const prop of properties) {
256
+ const propName = prop.getName();
257
+ const propType = checker.getTypeOfSymbol(prop);
258
+ if (isHttpMethod(propName)) {
259
+ const endpoint = parseEndpoint(
260
+ propType,
261
+ currentPath || "/",
262
+ methodKeyToHttp(propName),
263
+ pathParams,
264
+ ctx
265
+ );
266
+ endpoints.push(endpoint);
267
+ } else if (propName === "_") {
268
+ const paramName = getParamNameFromPath(currentPath);
269
+ const newPath = `${currentPath}/{${paramName}}`;
270
+ walkSchemaType(propType, newPath, [...pathParams, paramName], ctx, endpoints, checker);
271
+ } else {
272
+ const newPath = `${currentPath}/${propName}`;
273
+ walkSchemaType(propType, newPath, pathParams, ctx, endpoints, checker);
274
+ }
275
+ }
276
+ }
277
+ function getParamNameFromPath(currentPath) {
278
+ const segments = currentPath.split("/").filter(Boolean);
279
+ const lastSegment = segments[segments.length - 1];
280
+ if (lastSegment) {
281
+ const singular = lastSegment.endsWith("s") ? lastSegment.slice(0, -1) : lastSegment;
282
+ return `${singular}Id`;
283
+ }
284
+ return "id";
285
+ }
286
+ function parseEndpoint(type, path2, method, pathParams, ctx) {
287
+ if (type.isIntersection()) {
288
+ for (const t of type.types) {
289
+ if (isEndpointStructure(t)) {
290
+ return parseEndpointType(type, path2, method, pathParams, ctx);
291
+ }
292
+ }
293
+ }
294
+ if (isEndpointStructure(type)) {
295
+ return parseEndpointType(type, path2, method, pathParams, ctx);
296
+ }
297
+ return {
298
+ path: path2,
299
+ method,
300
+ responseSchema: typeToSchema(type, ctx),
301
+ pathParams
302
+ };
303
+ }
304
+ function isEndpointStructure(type) {
305
+ const props = type.getProperties();
306
+ const propNames = new Set(props.map((p) => p.getName()));
307
+ if (!propNames.has("data")) {
308
+ return false;
309
+ }
310
+ propNames.delete("data");
311
+ propNames.delete("body");
312
+ propNames.delete("error");
313
+ const remainingProps = [...propNames].filter(
314
+ (name) => !name.startsWith("__@") && !name.includes("Brand")
315
+ );
316
+ return remainingProps.length === 0;
317
+ }
318
+ function parseEndpointType(type, path2, method, pathParams, ctx) {
319
+ const { checker } = ctx;
320
+ let dataType;
321
+ let bodyType;
322
+ let errorType;
323
+ const typesToCheck = type.isIntersection() ? type.types : [type];
324
+ for (const t of typesToCheck) {
325
+ const props = t.getProperties();
326
+ for (const prop of props) {
327
+ const name = prop.getName();
328
+ if (name === "data") {
329
+ dataType = checker.getTypeOfSymbol(prop);
330
+ } else if (name === "body") {
331
+ bodyType = checker.getTypeOfSymbol(prop);
332
+ } else if (name === "error") {
333
+ errorType = checker.getTypeOfSymbol(prop);
334
+ }
335
+ }
336
+ }
337
+ const endpoint = {
338
+ path: path2,
339
+ method,
340
+ responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
341
+ pathParams
342
+ };
343
+ if (bodyType && !(bodyType.flags & import_typescript2.default.TypeFlags.Never)) {
344
+ endpoint.requestBodySchema = typeToSchema(bodyType, ctx);
345
+ }
346
+ if (errorType && !(errorType.flags & import_typescript2.default.TypeFlags.Never) && !(errorType.flags & import_typescript2.default.TypeFlags.Unknown)) {
347
+ endpoint.errorSchema = typeToSchema(errorType, ctx);
348
+ }
349
+ return endpoint;
350
+ }
351
+
352
+ // src/generator.ts
353
+ function generateOpenAPISpec(endpoints, schemas, options = {}) {
354
+ const {
355
+ title = "API",
356
+ version = "1.0.0",
357
+ description,
358
+ baseUrl
359
+ } = options;
360
+ const paths = {};
361
+ for (const endpoint of endpoints) {
362
+ if (!paths[endpoint.path]) {
363
+ paths[endpoint.path] = {};
364
+ }
365
+ const pathItem = paths[endpoint.path];
366
+ const operation = createOperation(endpoint);
367
+ pathItem[endpoint.method] = operation;
368
+ if (endpoint.pathParams.length > 0 && !pathItem.parameters) {
369
+ pathItem.parameters = endpoint.pathParams.map((param) => ({
370
+ name: param,
371
+ in: "path",
372
+ required: true,
373
+ schema: { type: "string" }
374
+ }));
375
+ }
376
+ }
377
+ const spec = {
378
+ openapi: "3.0.0",
379
+ info: {
380
+ title,
381
+ version
382
+ },
383
+ paths
384
+ };
385
+ if (description) {
386
+ spec.info.description = description;
387
+ }
388
+ if (baseUrl) {
389
+ spec.servers = [{ url: baseUrl }];
390
+ }
391
+ if (schemas.size > 0) {
392
+ spec.components = {
393
+ schemas: Object.fromEntries(schemas)
394
+ };
395
+ }
396
+ return spec;
397
+ }
398
+ function createOperation(endpoint) {
399
+ const operation = {
400
+ responses: {
401
+ "200": {
402
+ description: "Successful response"
403
+ }
404
+ }
405
+ };
406
+ if (hasContent(endpoint.responseSchema)) {
407
+ operation.responses["200"].content = {
408
+ "application/json": {
409
+ schema: endpoint.responseSchema
410
+ }
411
+ };
412
+ }
413
+ if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
414
+ operation.requestBody = {
415
+ required: true,
416
+ content: {
417
+ "application/json": {
418
+ schema: endpoint.requestBodySchema
419
+ }
420
+ }
421
+ };
422
+ }
423
+ if (endpoint.errorSchema && hasContent(endpoint.errorSchema)) {
424
+ operation.responses["400"] = {
425
+ description: "Error response",
426
+ content: {
427
+ "application/json": {
428
+ schema: endpoint.errorSchema
429
+ }
430
+ }
431
+ };
432
+ }
433
+ return operation;
434
+ }
435
+ function hasContent(schema) {
436
+ return Object.keys(schema).length > 0;
437
+ }
438
+
439
+ // src/index.ts
440
+ import_commander.program.name("enlace-openapi").description("Generate OpenAPI spec from TypeScript API schema").requiredOption("-s, --schema <path>", "Path to TypeScript file containing the schema type").option("-t, --type <name>", "Name of the schema type to use", "ApiSchema").option("-o, --output <path>", "Output file path (default: stdout)").option("--title <title>", "API title for OpenAPI info").option("--version <version>", "API version for OpenAPI info", "1.0.0").option("--base-url <url>", "Base URL for servers array").action((options) => {
441
+ try {
442
+ const { endpoints, schemas } = parseSchema(options.schema, options.type);
443
+ const spec = generateOpenAPISpec(endpoints, schemas, {
444
+ title: options.title,
445
+ version: options.version,
446
+ baseUrl: options.baseUrl
447
+ });
448
+ const output = JSON.stringify(spec, null, 2);
449
+ if (options.output) {
450
+ import_fs.default.writeFileSync(options.output, output);
451
+ console.log(`OpenAPI spec written to ${options.output}`);
452
+ } else {
453
+ console.log(output);
454
+ }
455
+ } catch (error) {
456
+ console.error("Error:", error instanceof Error ? error.message : error);
457
+ process.exit(1);
458
+ }
459
+ });
460
+ import_commander.program.parse();
461
+ // Annotate the CommonJS export names for ESM import in node:
462
+ 0 && (module.exports = {
463
+ generateOpenAPISpec,
464
+ parseSchema
465
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { program } from "commander";
5
+ import fs from "fs";
6
+
7
+ // src/parser.ts
8
+ import ts2 from "typescript";
9
+ import path from "path";
10
+
11
+ // src/type-to-schema.ts
12
+ import ts from "typescript";
13
+ function createSchemaContext(checker) {
14
+ return {
15
+ checker,
16
+ schemas: /* @__PURE__ */ new Map(),
17
+ visitedTypes: /* @__PURE__ */ new Set()
18
+ };
19
+ }
20
+ function typeToSchema(type, ctx) {
21
+ const { checker } = ctx;
22
+ if (type.flags & ts.TypeFlags.String) {
23
+ return { type: "string" };
24
+ }
25
+ if (type.flags & ts.TypeFlags.Number) {
26
+ return { type: "number" };
27
+ }
28
+ if (type.flags & ts.TypeFlags.Boolean) {
29
+ return { type: "boolean" };
30
+ }
31
+ if (type.flags & ts.TypeFlags.Null) {
32
+ return { type: "null" };
33
+ }
34
+ if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
35
+ return {};
36
+ }
37
+ if (type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown) {
38
+ return {};
39
+ }
40
+ if (type.flags & ts.TypeFlags.Never) {
41
+ return {};
42
+ }
43
+ if (type.isStringLiteral()) {
44
+ return { type: "string", const: type.value };
45
+ }
46
+ if (type.isNumberLiteral()) {
47
+ return { type: "number", const: type.value };
48
+ }
49
+ if (type.flags & ts.TypeFlags.BooleanLiteral) {
50
+ const intrinsicName = type.intrinsicName;
51
+ return { type: "boolean", const: intrinsicName === "true" };
52
+ }
53
+ if (type.isUnion()) {
54
+ const nonNullTypes = type.types.filter(
55
+ (t) => !(t.flags & ts.TypeFlags.Null) && !(t.flags & ts.TypeFlags.Undefined)
56
+ );
57
+ const hasNull = type.types.some((t) => t.flags & ts.TypeFlags.Null);
58
+ if (nonNullTypes.length === 1 && hasNull) {
59
+ const schema = typeToSchema(nonNullTypes[0], ctx);
60
+ return { ...schema, nullable: true };
61
+ }
62
+ if (nonNullTypes.every((t) => t.isStringLiteral())) {
63
+ return {
64
+ type: "string",
65
+ enum: nonNullTypes.map((t) => t.value)
66
+ };
67
+ }
68
+ if (nonNullTypes.every((t) => t.isNumberLiteral())) {
69
+ return {
70
+ type: "number",
71
+ enum: nonNullTypes.map((t) => t.value)
72
+ };
73
+ }
74
+ return {
75
+ oneOf: nonNullTypes.map((t) => typeToSchema(t, ctx))
76
+ };
77
+ }
78
+ if (type.isIntersection()) {
79
+ return intersectionTypeToSchema(type, ctx);
80
+ }
81
+ if (checker.isArrayType(type)) {
82
+ const typeArgs = type.typeArguments;
83
+ if (typeArgs?.[0]) {
84
+ return {
85
+ type: "array",
86
+ items: typeToSchema(typeArgs[0], ctx)
87
+ };
88
+ }
89
+ return { type: "array" };
90
+ }
91
+ if (type.flags & ts.TypeFlags.Object) {
92
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
93
+ const typeName = symbol?.getName();
94
+ if (typeName === "Date") {
95
+ return { type: "string", format: "date-time" };
96
+ }
97
+ if (typeName && typeName !== "__type" && typeName !== "Array" && !typeName.startsWith("__")) {
98
+ if (ctx.visitedTypes.has(type)) {
99
+ return { $ref: `#/components/schemas/${typeName}` };
100
+ }
101
+ if (!ctx.schemas.has(typeName)) {
102
+ ctx.visitedTypes.add(type);
103
+ const schema = objectTypeToSchema(type, ctx);
104
+ ctx.schemas.set(typeName, schema);
105
+ ctx.visitedTypes.delete(type);
106
+ }
107
+ return { $ref: `#/components/schemas/${typeName}` };
108
+ }
109
+ return objectTypeToSchema(type, ctx);
110
+ }
111
+ return {};
112
+ }
113
+ function objectTypeToSchema(type, ctx) {
114
+ const { checker } = ctx;
115
+ const properties = {};
116
+ const required = [];
117
+ const props = type.getProperties();
118
+ for (const prop of props) {
119
+ const propName = prop.getName();
120
+ const propType = checker.getTypeOfSymbol(prop);
121
+ const isOptional = prop.flags & ts.SymbolFlags.Optional;
122
+ properties[propName] = typeToSchema(propType, ctx);
123
+ if (!isOptional) {
124
+ required.push(propName);
125
+ }
126
+ }
127
+ const schema = {
128
+ type: "object",
129
+ properties
130
+ };
131
+ if (required.length > 0) {
132
+ schema.required = required;
133
+ }
134
+ return schema;
135
+ }
136
+ function intersectionTypeToSchema(type, ctx) {
137
+ const { checker } = ctx;
138
+ const properties = {};
139
+ const required = [];
140
+ for (const t of type.types) {
141
+ const props = t.getProperties();
142
+ for (const prop of props) {
143
+ const propName = prop.getName();
144
+ if (propName.startsWith("__")) continue;
145
+ const propType = checker.getTypeOfSymbol(prop);
146
+ const isOptional = prop.flags & ts.SymbolFlags.Optional;
147
+ properties[propName] = typeToSchema(propType, ctx);
148
+ if (!isOptional && !required.includes(propName)) {
149
+ required.push(propName);
150
+ }
151
+ }
152
+ }
153
+ const schema = {
154
+ type: "object",
155
+ properties
156
+ };
157
+ if (required.length > 0) {
158
+ schema.required = required;
159
+ }
160
+ return schema;
161
+ }
162
+
163
+ // src/parser.ts
164
+ var HTTP_METHODS = ["$get", "$post", "$put", "$patch", "$delete"];
165
+ function isHttpMethod(key) {
166
+ return HTTP_METHODS.includes(key);
167
+ }
168
+ function methodKeyToHttp(key) {
169
+ return key.slice(1);
170
+ }
171
+ function parseSchema(schemaFilePath, typeName) {
172
+ const absolutePath = path.resolve(schemaFilePath);
173
+ const schemaDir = path.dirname(absolutePath);
174
+ const configPath = ts2.findConfigFile(schemaDir, ts2.sys.fileExists, "tsconfig.json");
175
+ let compilerOptions = {
176
+ target: ts2.ScriptTarget.ESNext,
177
+ module: ts2.ModuleKind.NodeNext,
178
+ moduleResolution: ts2.ModuleResolutionKind.NodeNext,
179
+ strict: true
180
+ };
181
+ if (configPath) {
182
+ const configFile = ts2.readConfigFile(configPath, ts2.sys.readFile);
183
+ if (!configFile.error) {
184
+ const parsed = ts2.parseJsonConfigFileContent(
185
+ configFile.config,
186
+ ts2.sys,
187
+ path.dirname(configPath)
188
+ );
189
+ compilerOptions = { ...compilerOptions, ...parsed.options };
190
+ }
191
+ }
192
+ const program2 = ts2.createProgram([absolutePath], compilerOptions);
193
+ const checker = program2.getTypeChecker();
194
+ const sourceFile = program2.getSourceFile(absolutePath);
195
+ if (!sourceFile) {
196
+ throw new Error(`Could not find source file: ${absolutePath}`);
197
+ }
198
+ const schemaType = findExportedType(sourceFile, typeName, checker);
199
+ if (!schemaType) {
200
+ throw new Error(`Could not find exported type '${typeName}' in ${schemaFilePath}`);
201
+ }
202
+ const ctx = createSchemaContext(checker);
203
+ const endpoints = [];
204
+ walkSchemaType(schemaType, "", [], ctx, endpoints, checker);
205
+ return {
206
+ endpoints,
207
+ schemas: ctx.schemas
208
+ };
209
+ }
210
+ function findExportedType(sourceFile, typeName, checker) {
211
+ const symbol = checker.getSymbolAtLocation(sourceFile);
212
+ if (!symbol) return void 0;
213
+ const exports = checker.getExportsOfModule(symbol);
214
+ const typeSymbol = exports.find((exp) => exp.getName() === typeName);
215
+ if (!typeSymbol) return void 0;
216
+ const declaredType = checker.getDeclaredTypeOfSymbol(typeSymbol);
217
+ return declaredType;
218
+ }
219
+ function walkSchemaType(type, currentPath, pathParams, ctx, endpoints, checker) {
220
+ const properties = type.getProperties();
221
+ for (const prop of properties) {
222
+ const propName = prop.getName();
223
+ const propType = checker.getTypeOfSymbol(prop);
224
+ if (isHttpMethod(propName)) {
225
+ const endpoint = parseEndpoint(
226
+ propType,
227
+ currentPath || "/",
228
+ methodKeyToHttp(propName),
229
+ pathParams,
230
+ ctx
231
+ );
232
+ endpoints.push(endpoint);
233
+ } else if (propName === "_") {
234
+ const paramName = getParamNameFromPath(currentPath);
235
+ const newPath = `${currentPath}/{${paramName}}`;
236
+ walkSchemaType(propType, newPath, [...pathParams, paramName], ctx, endpoints, checker);
237
+ } else {
238
+ const newPath = `${currentPath}/${propName}`;
239
+ walkSchemaType(propType, newPath, pathParams, ctx, endpoints, checker);
240
+ }
241
+ }
242
+ }
243
+ function getParamNameFromPath(currentPath) {
244
+ const segments = currentPath.split("/").filter(Boolean);
245
+ const lastSegment = segments[segments.length - 1];
246
+ if (lastSegment) {
247
+ const singular = lastSegment.endsWith("s") ? lastSegment.slice(0, -1) : lastSegment;
248
+ return `${singular}Id`;
249
+ }
250
+ return "id";
251
+ }
252
+ function parseEndpoint(type, path2, method, pathParams, ctx) {
253
+ if (type.isIntersection()) {
254
+ for (const t of type.types) {
255
+ if (isEndpointStructure(t)) {
256
+ return parseEndpointType(type, path2, method, pathParams, ctx);
257
+ }
258
+ }
259
+ }
260
+ if (isEndpointStructure(type)) {
261
+ return parseEndpointType(type, path2, method, pathParams, ctx);
262
+ }
263
+ return {
264
+ path: path2,
265
+ method,
266
+ responseSchema: typeToSchema(type, ctx),
267
+ pathParams
268
+ };
269
+ }
270
+ function isEndpointStructure(type) {
271
+ const props = type.getProperties();
272
+ const propNames = new Set(props.map((p) => p.getName()));
273
+ if (!propNames.has("data")) {
274
+ return false;
275
+ }
276
+ propNames.delete("data");
277
+ propNames.delete("body");
278
+ propNames.delete("error");
279
+ const remainingProps = [...propNames].filter(
280
+ (name) => !name.startsWith("__@") && !name.includes("Brand")
281
+ );
282
+ return remainingProps.length === 0;
283
+ }
284
+ function parseEndpointType(type, path2, method, pathParams, ctx) {
285
+ const { checker } = ctx;
286
+ let dataType;
287
+ let bodyType;
288
+ let errorType;
289
+ const typesToCheck = type.isIntersection() ? type.types : [type];
290
+ for (const t of typesToCheck) {
291
+ const props = t.getProperties();
292
+ for (const prop of props) {
293
+ const name = prop.getName();
294
+ if (name === "data") {
295
+ dataType = checker.getTypeOfSymbol(prop);
296
+ } else if (name === "body") {
297
+ bodyType = checker.getTypeOfSymbol(prop);
298
+ } else if (name === "error") {
299
+ errorType = checker.getTypeOfSymbol(prop);
300
+ }
301
+ }
302
+ }
303
+ const endpoint = {
304
+ path: path2,
305
+ method,
306
+ responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
307
+ pathParams
308
+ };
309
+ if (bodyType && !(bodyType.flags & ts2.TypeFlags.Never)) {
310
+ endpoint.requestBodySchema = typeToSchema(bodyType, ctx);
311
+ }
312
+ if (errorType && !(errorType.flags & ts2.TypeFlags.Never) && !(errorType.flags & ts2.TypeFlags.Unknown)) {
313
+ endpoint.errorSchema = typeToSchema(errorType, ctx);
314
+ }
315
+ return endpoint;
316
+ }
317
+
318
+ // src/generator.ts
319
+ function generateOpenAPISpec(endpoints, schemas, options = {}) {
320
+ const {
321
+ title = "API",
322
+ version = "1.0.0",
323
+ description,
324
+ baseUrl
325
+ } = options;
326
+ const paths = {};
327
+ for (const endpoint of endpoints) {
328
+ if (!paths[endpoint.path]) {
329
+ paths[endpoint.path] = {};
330
+ }
331
+ const pathItem = paths[endpoint.path];
332
+ const operation = createOperation(endpoint);
333
+ pathItem[endpoint.method] = operation;
334
+ if (endpoint.pathParams.length > 0 && !pathItem.parameters) {
335
+ pathItem.parameters = endpoint.pathParams.map((param) => ({
336
+ name: param,
337
+ in: "path",
338
+ required: true,
339
+ schema: { type: "string" }
340
+ }));
341
+ }
342
+ }
343
+ const spec = {
344
+ openapi: "3.0.0",
345
+ info: {
346
+ title,
347
+ version
348
+ },
349
+ paths
350
+ };
351
+ if (description) {
352
+ spec.info.description = description;
353
+ }
354
+ if (baseUrl) {
355
+ spec.servers = [{ url: baseUrl }];
356
+ }
357
+ if (schemas.size > 0) {
358
+ spec.components = {
359
+ schemas: Object.fromEntries(schemas)
360
+ };
361
+ }
362
+ return spec;
363
+ }
364
+ function createOperation(endpoint) {
365
+ const operation = {
366
+ responses: {
367
+ "200": {
368
+ description: "Successful response"
369
+ }
370
+ }
371
+ };
372
+ if (hasContent(endpoint.responseSchema)) {
373
+ operation.responses["200"].content = {
374
+ "application/json": {
375
+ schema: endpoint.responseSchema
376
+ }
377
+ };
378
+ }
379
+ if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
380
+ operation.requestBody = {
381
+ required: true,
382
+ content: {
383
+ "application/json": {
384
+ schema: endpoint.requestBodySchema
385
+ }
386
+ }
387
+ };
388
+ }
389
+ if (endpoint.errorSchema && hasContent(endpoint.errorSchema)) {
390
+ operation.responses["400"] = {
391
+ description: "Error response",
392
+ content: {
393
+ "application/json": {
394
+ schema: endpoint.errorSchema
395
+ }
396
+ }
397
+ };
398
+ }
399
+ return operation;
400
+ }
401
+ function hasContent(schema) {
402
+ return Object.keys(schema).length > 0;
403
+ }
404
+
405
+ // src/index.ts
406
+ program.name("enlace-openapi").description("Generate OpenAPI spec from TypeScript API schema").requiredOption("-s, --schema <path>", "Path to TypeScript file containing the schema type").option("-t, --type <name>", "Name of the schema type to use", "ApiSchema").option("-o, --output <path>", "Output file path (default: stdout)").option("--title <title>", "API title for OpenAPI info").option("--version <version>", "API version for OpenAPI info", "1.0.0").option("--base-url <url>", "Base URL for servers array").action((options) => {
407
+ try {
408
+ const { endpoints, schemas } = parseSchema(options.schema, options.type);
409
+ const spec = generateOpenAPISpec(endpoints, schemas, {
410
+ title: options.title,
411
+ version: options.version,
412
+ baseUrl: options.baseUrl
413
+ });
414
+ const output = JSON.stringify(spec, null, 2);
415
+ if (options.output) {
416
+ fs.writeFileSync(options.output, output);
417
+ console.log(`OpenAPI spec written to ${options.output}`);
418
+ } else {
419
+ console.log(output);
420
+ }
421
+ } catch (error) {
422
+ console.error("Error:", error instanceof Error ? error.message : error);
423
+ process.exit(1);
424
+ }
425
+ });
426
+ program.parse();
427
+ export {
428
+ generateOpenAPISpec,
429
+ parseSchema
430
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "enlace-openapi",
3
+ "version": "0.0.1-beta.1",
4
+ "license": "MIT",
5
+ "bin": {
6
+ "enlace-openapi": "./dist/index.mjs"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "dev": "tsup --watch",
20
+ "build": "tsup",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "eslint src --max-warnings 0",
23
+ "prepublishOnly": "npm run build && npm run typecheck && npm run lint"
24
+ },
25
+ "dependencies": {
26
+ "commander": "^12.1.0",
27
+ "typescript": "^5.9.2"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.19.2"
31
+ }
32
+ }