enlace-openapi 0.0.1-beta.1 → 0.0.1-beta.3

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