@honestjs/rpc-plugin 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -10
- package/dist/index.d.mts +88 -51
- package/dist/index.d.ts +88 -51
- package/dist/index.js +337 -150
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +329 -145
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -36,23 +36,27 @@ __export(index_exports, {
|
|
|
36
36
|
DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
|
|
37
37
|
GENERIC_TYPES: () => GENERIC_TYPES,
|
|
38
38
|
LOG_PREFIX: () => LOG_PREFIX,
|
|
39
|
+
OpenApiGeneratorService: () => OpenApiGeneratorService,
|
|
39
40
|
RPCPlugin: () => RPCPlugin,
|
|
40
41
|
RouteAnalyzerService: () => RouteAnalyzerService,
|
|
41
42
|
SchemaGeneratorService: () => SchemaGeneratorService,
|
|
42
43
|
buildFullApiPath: () => buildFullApiPath,
|
|
43
44
|
buildFullPath: () => buildFullPath,
|
|
44
45
|
camelCase: () => camelCase,
|
|
46
|
+
computeHash: () => computeHash,
|
|
45
47
|
extractNamedType: () => extractNamedType,
|
|
46
|
-
generateTypeImports: () => generateTypeImports,
|
|
47
48
|
generateTypeScriptInterface: () => generateTypeScriptInterface,
|
|
48
49
|
mapJsonSchemaTypeToTypeScript: () => mapJsonSchemaTypeToTypeScript,
|
|
49
|
-
|
|
50
|
+
readChecksum: () => readChecksum,
|
|
51
|
+
safeToString: () => safeToString,
|
|
52
|
+
writeChecksum: () => writeChecksum
|
|
50
53
|
});
|
|
51
54
|
module.exports = __toCommonJS(index_exports);
|
|
52
55
|
|
|
53
56
|
// src/rpc.plugin.ts
|
|
54
|
-
var
|
|
55
|
-
var
|
|
57
|
+
var import_fs2 = __toESM(require("fs"));
|
|
58
|
+
var import_path4 = __toESM(require("path"));
|
|
59
|
+
var import_ts_morph = require("ts-morph");
|
|
56
60
|
|
|
57
61
|
// src/constants/defaults.ts
|
|
58
62
|
var DEFAULT_OPTIONS = {
|
|
@@ -77,29 +81,66 @@ var BUILTIN_UTILITY_TYPES = /* @__PURE__ */ new Set([
|
|
|
77
81
|
var BUILTIN_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "any", "void", "unknown"]);
|
|
78
82
|
var GENERIC_TYPES = /* @__PURE__ */ new Set(["Array", "Promise", "Partial"]);
|
|
79
83
|
|
|
80
|
-
// src/
|
|
81
|
-
var
|
|
84
|
+
// src/utils/hash-utils.ts
|
|
85
|
+
var import_crypto = require("crypto");
|
|
86
|
+
var import_fs = require("fs");
|
|
87
|
+
var import_promises = require("fs/promises");
|
|
82
88
|
var import_path = __toESM(require("path"));
|
|
89
|
+
var CHECKSUM_FILENAME = ".rpc-checksum";
|
|
90
|
+
function computeHash(filePaths) {
|
|
91
|
+
const sorted = [...filePaths].sort();
|
|
92
|
+
const hasher = (0, import_crypto.createHash)("sha256");
|
|
93
|
+
hasher.update(`files:${sorted.length}
|
|
94
|
+
`);
|
|
95
|
+
for (const filePath of sorted) {
|
|
96
|
+
hasher.update((0, import_fs.readFileSync)(filePath, "utf-8"));
|
|
97
|
+
hasher.update("\0");
|
|
98
|
+
}
|
|
99
|
+
return hasher.digest("hex");
|
|
100
|
+
}
|
|
101
|
+
function readChecksum(outputDir) {
|
|
102
|
+
const checksumPath = import_path.default.join(outputDir, CHECKSUM_FILENAME);
|
|
103
|
+
if (!(0, import_fs.existsSync)(checksumPath)) return null;
|
|
104
|
+
try {
|
|
105
|
+
const raw = (0, import_fs.readFileSync)(checksumPath, "utf-8");
|
|
106
|
+
const data = JSON.parse(raw);
|
|
107
|
+
if (typeof data.hash !== "string" || !Array.isArray(data.files)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return data;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function writeChecksum(outputDir, data) {
|
|
116
|
+
await (0, import_promises.mkdir)(outputDir, { recursive: true });
|
|
117
|
+
const checksumPath = import_path.default.join(outputDir, CHECKSUM_FILENAME);
|
|
118
|
+
await (0, import_promises.writeFile)(checksumPath, JSON.stringify(data, null, 2), "utf-8");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/services/client-generator.service.ts
|
|
122
|
+
var import_promises2 = __toESM(require("fs/promises"));
|
|
123
|
+
var import_path2 = __toESM(require("path"));
|
|
83
124
|
|
|
84
125
|
// src/utils/path-utils.ts
|
|
85
126
|
function buildFullPath(basePath, parameters) {
|
|
86
127
|
if (!basePath || typeof basePath !== "string") return "/";
|
|
87
|
-
let
|
|
128
|
+
let path5 = basePath;
|
|
88
129
|
if (parameters && Array.isArray(parameters)) {
|
|
89
130
|
for (const param of parameters) {
|
|
90
131
|
if (param.data && typeof param.data === "string" && param.data.startsWith(":")) {
|
|
91
132
|
const paramName = param.data.slice(1);
|
|
92
|
-
|
|
133
|
+
path5 = path5.replace(`:${paramName}`, `\${${paramName}}`);
|
|
93
134
|
}
|
|
94
135
|
}
|
|
95
136
|
}
|
|
96
|
-
return
|
|
137
|
+
return path5;
|
|
97
138
|
}
|
|
98
139
|
function buildFullApiPath(route) {
|
|
99
140
|
const prefix = route.prefix || "";
|
|
100
141
|
const version = route.version || "";
|
|
101
142
|
const routePath = route.route || "";
|
|
102
|
-
const
|
|
143
|
+
const path5 = route.path || "";
|
|
103
144
|
let fullPath = "";
|
|
104
145
|
if (prefix && prefix !== "/") {
|
|
105
146
|
fullPath += prefix.replace(/^\/+|\/+$/g, "");
|
|
@@ -110,11 +151,12 @@ function buildFullApiPath(route) {
|
|
|
110
151
|
if (routePath && routePath !== "/") {
|
|
111
152
|
fullPath += `/${routePath.replace(/^\/+|\/+$/g, "")}`;
|
|
112
153
|
}
|
|
113
|
-
if (
|
|
114
|
-
fullPath += `/${
|
|
115
|
-
} else if (
|
|
154
|
+
if (path5 && path5 !== "/") {
|
|
155
|
+
fullPath += `/${path5.replace(/^\/+|\/+$/g, "")}`;
|
|
156
|
+
} else if (path5 === "/") {
|
|
116
157
|
fullPath += "/";
|
|
117
158
|
}
|
|
159
|
+
if (fullPath && !fullPath.startsWith("/")) fullPath = "/" + fullPath;
|
|
118
160
|
return fullPath || "/";
|
|
119
161
|
}
|
|
120
162
|
|
|
@@ -137,10 +179,10 @@ var ClientGeneratorService = class {
|
|
|
137
179
|
* Generates the TypeScript RPC client
|
|
138
180
|
*/
|
|
139
181
|
async generateClient(routes, schemas) {
|
|
140
|
-
await
|
|
182
|
+
await import_promises2.default.mkdir(this.outputDir, { recursive: true });
|
|
141
183
|
await this.generateClientFile(routes, schemas);
|
|
142
184
|
const generatedInfo = {
|
|
143
|
-
clientFile:
|
|
185
|
+
clientFile: import_path2.default.join(this.outputDir, "client.ts"),
|
|
144
186
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
145
187
|
};
|
|
146
188
|
return generatedInfo;
|
|
@@ -150,8 +192,8 @@ var ClientGeneratorService = class {
|
|
|
150
192
|
*/
|
|
151
193
|
async generateClientFile(routes, schemas) {
|
|
152
194
|
const clientContent = this.generateClientContent(routes, schemas);
|
|
153
|
-
const clientPath =
|
|
154
|
-
await
|
|
195
|
+
const clientPath = import_path2.default.join(this.outputDir, "client.ts");
|
|
196
|
+
await import_promises2.default.writeFile(clientPath, clientContent, "utf-8");
|
|
155
197
|
}
|
|
156
198
|
/**
|
|
157
199
|
* Generates the client TypeScript content with types included
|
|
@@ -162,15 +204,6 @@ var ClientGeneratorService = class {
|
|
|
162
204
|
// TYPES SECTION
|
|
163
205
|
// ============================================================================
|
|
164
206
|
|
|
165
|
-
/**
|
|
166
|
-
* API Response wrapper
|
|
167
|
-
*/
|
|
168
|
-
export interface ApiResponse<T = any> {
|
|
169
|
-
data: T
|
|
170
|
-
message?: string
|
|
171
|
-
success: boolean
|
|
172
|
-
}
|
|
173
|
-
|
|
174
207
|
/**
|
|
175
208
|
* API Error class
|
|
176
209
|
*/
|
|
@@ -283,7 +316,7 @@ export class ApiClient {
|
|
|
283
316
|
method: string,
|
|
284
317
|
path: string,
|
|
285
318
|
options: RequestOptions<any, any, any, any> = {}
|
|
286
|
-
): Promise<
|
|
319
|
+
): Promise<T> {
|
|
287
320
|
const { params, query, body, headers = {} } = options as any
|
|
288
321
|
|
|
289
322
|
// Build the final URL with path parameters
|
|
@@ -319,6 +352,14 @@ export class ApiClient {
|
|
|
319
352
|
|
|
320
353
|
try {
|
|
321
354
|
const response = await this.fetchFn(url.toString(), requestOptions)
|
|
355
|
+
|
|
356
|
+
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
|
357
|
+
if (!response.ok) {
|
|
358
|
+
throw new ApiError(response.status, 'Request failed')
|
|
359
|
+
}
|
|
360
|
+
return undefined as T
|
|
361
|
+
}
|
|
362
|
+
|
|
322
363
|
const responseData = await response.json()
|
|
323
364
|
|
|
324
365
|
if (!response.ok) {
|
|
@@ -356,8 +397,9 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
356
397
|
const methodName = camelCase(safeToString(route.handler));
|
|
357
398
|
const httpMethod = safeToString(route.method).toLowerCase();
|
|
358
399
|
const { pathParams, queryParams, bodyParams } = this.analyzeRouteParameters(route);
|
|
400
|
+
const returnType = this.extractReturnType(route.returns);
|
|
359
401
|
const hasRequiredParams = pathParams.length > 0 || queryParams.some((p) => p.required) || bodyParams.length > 0 && httpMethod !== "get";
|
|
360
|
-
methods += ` ${methodName}: async (options${hasRequiredParams ? "" : "?"}: RequestOptions<`;
|
|
402
|
+
methods += ` ${methodName}: async <Result = ${returnType}>(options${hasRequiredParams ? "" : "?"}: RequestOptions<`;
|
|
361
403
|
if (pathParams.length > 0) {
|
|
362
404
|
const pathParamTypes = pathParams.map((p) => {
|
|
363
405
|
const paramName = p.name;
|
|
@@ -391,8 +433,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
391
433
|
}
|
|
392
434
|
methods += ", ";
|
|
393
435
|
methods += "undefined";
|
|
394
|
-
|
|
395
|
-
methods += `>): Promise<ApiResponse<${returnType}>> => {
|
|
436
|
+
methods += `>) => {
|
|
396
437
|
`;
|
|
397
438
|
let requestPath = buildFullApiPath(route);
|
|
398
439
|
if (pathParams.length > 0) {
|
|
@@ -402,7 +443,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
402
443
|
requestPath = requestPath.replace(placeholder, `:${paramName}`);
|
|
403
444
|
}
|
|
404
445
|
}
|
|
405
|
-
methods += ` return this.request
|
|
446
|
+
methods += ` return this.request<Result>('${httpMethod.toUpperCase()}', \`${requestPath}\`, options)
|
|
406
447
|
`;
|
|
407
448
|
methods += ` },
|
|
408
449
|
`;
|
|
@@ -461,71 +502,216 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
461
502
|
*/
|
|
462
503
|
analyzeRouteParameters(route) {
|
|
463
504
|
const parameters = route.parameters || [];
|
|
464
|
-
const
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
return !!pathSegment && typeof pathSegment === "string" && route.path.includes(`:${pathSegment}`);
|
|
468
|
-
};
|
|
469
|
-
const pathParams = parameters.filter((p) => isInPath(p)).map((p) => ({ ...p, required: true }));
|
|
470
|
-
const rawBody = parameters.filter((p) => !isInPath(p) && method !== "get");
|
|
471
|
-
const bodyParams = rawBody.map((p) => ({
|
|
472
|
-
...p,
|
|
473
|
-
required: true
|
|
474
|
-
}));
|
|
475
|
-
const queryParams = parameters.filter((p) => !isInPath(p) && method === "get").map((p) => ({
|
|
476
|
-
...p,
|
|
477
|
-
required: p.required === true
|
|
478
|
-
// default false if not provided
|
|
479
|
-
}));
|
|
505
|
+
const pathParams = parameters.filter((p) => p.decoratorType === "param").map((p) => ({ ...p, required: true }));
|
|
506
|
+
const bodyParams = parameters.filter((p) => p.decoratorType === "body").map((p) => ({ ...p, required: true }));
|
|
507
|
+
const queryParams = parameters.filter((p) => p.decoratorType === "query").map((p) => ({ ...p, required: p.required === true }));
|
|
480
508
|
return { pathParams, queryParams, bodyParams };
|
|
481
509
|
}
|
|
482
510
|
};
|
|
483
511
|
|
|
512
|
+
// src/services/openapi-generator.service.ts
|
|
513
|
+
var import_promises3 = __toESM(require("fs/promises"));
|
|
514
|
+
var import_path3 = __toESM(require("path"));
|
|
515
|
+
var OpenApiGeneratorService = class {
|
|
516
|
+
constructor(outputDir) {
|
|
517
|
+
this.outputDir = outputDir;
|
|
518
|
+
}
|
|
519
|
+
async generateSpec(routes, schemas, options) {
|
|
520
|
+
await import_promises3.default.mkdir(this.outputDir, { recursive: true });
|
|
521
|
+
const schemaMap = this.buildSchemaMap(schemas);
|
|
522
|
+
const spec = this.buildSpec(routes, schemaMap, options);
|
|
523
|
+
const outputPath = import_path3.default.join(this.outputDir, options.outputFile);
|
|
524
|
+
await import_promises3.default.writeFile(outputPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
525
|
+
return outputPath;
|
|
526
|
+
}
|
|
527
|
+
buildSpec(routes, schemaMap, options) {
|
|
528
|
+
const spec = {
|
|
529
|
+
openapi: "3.0.3",
|
|
530
|
+
info: {
|
|
531
|
+
title: options.title,
|
|
532
|
+
version: options.version,
|
|
533
|
+
description: options.description
|
|
534
|
+
},
|
|
535
|
+
paths: {},
|
|
536
|
+
components: { schemas: schemaMap }
|
|
537
|
+
};
|
|
538
|
+
if (options.servers.length > 0) {
|
|
539
|
+
spec.servers = options.servers.map((s) => ({ ...s }));
|
|
540
|
+
}
|
|
541
|
+
for (const route of routes) {
|
|
542
|
+
const apiPath = this.toOpenApiPath(buildFullApiPath(route));
|
|
543
|
+
const method = safeToString(route.method).toLowerCase();
|
|
544
|
+
if (!spec.paths[apiPath]) {
|
|
545
|
+
spec.paths[apiPath] = {};
|
|
546
|
+
}
|
|
547
|
+
spec.paths[apiPath][method] = this.buildOperation(route, schemaMap);
|
|
548
|
+
}
|
|
549
|
+
return spec;
|
|
550
|
+
}
|
|
551
|
+
buildOperation(route, schemaMap) {
|
|
552
|
+
const controllerName = safeToString(route.controller).replace(/Controller$/, "");
|
|
553
|
+
const handlerName = safeToString(route.handler);
|
|
554
|
+
const parameters = route.parameters || [];
|
|
555
|
+
const operation = {
|
|
556
|
+
operationId: handlerName,
|
|
557
|
+
tags: [controllerName],
|
|
558
|
+
responses: this.buildResponses(route.returns, schemaMap)
|
|
559
|
+
};
|
|
560
|
+
const openApiParams = this.buildParameters(parameters);
|
|
561
|
+
if (openApiParams.length > 0) {
|
|
562
|
+
operation.parameters = openApiParams;
|
|
563
|
+
}
|
|
564
|
+
const requestBody = this.buildRequestBody(parameters, schemaMap);
|
|
565
|
+
if (requestBody) {
|
|
566
|
+
operation.requestBody = requestBody;
|
|
567
|
+
}
|
|
568
|
+
return operation;
|
|
569
|
+
}
|
|
570
|
+
buildParameters(parameters) {
|
|
571
|
+
const result = [];
|
|
572
|
+
for (const param of parameters) {
|
|
573
|
+
if (param.decoratorType === "param") {
|
|
574
|
+
result.push({
|
|
575
|
+
name: param.data ?? param.name,
|
|
576
|
+
in: "path",
|
|
577
|
+
required: true,
|
|
578
|
+
schema: this.tsTypeToJsonSchema(param.type)
|
|
579
|
+
});
|
|
580
|
+
} else if (param.decoratorType === "query") {
|
|
581
|
+
if (param.data) {
|
|
582
|
+
result.push({
|
|
583
|
+
name: param.data,
|
|
584
|
+
in: "query",
|
|
585
|
+
required: param.required === true,
|
|
586
|
+
schema: this.tsTypeToJsonSchema(param.type)
|
|
587
|
+
});
|
|
588
|
+
} else {
|
|
589
|
+
result.push({
|
|
590
|
+
name: param.name,
|
|
591
|
+
in: "query",
|
|
592
|
+
required: param.required === true,
|
|
593
|
+
schema: this.tsTypeToJsonSchema(param.type)
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
600
|
+
buildRequestBody(parameters, schemaMap) {
|
|
601
|
+
const bodyParams = parameters.filter((p) => p.decoratorType === "body");
|
|
602
|
+
if (bodyParams.length === 0) return null;
|
|
603
|
+
const bodyParam = bodyParams[0];
|
|
604
|
+
const typeName = this.extractBaseTypeName(bodyParam.type);
|
|
605
|
+
let schema;
|
|
606
|
+
if (typeName && schemaMap[typeName]) {
|
|
607
|
+
schema = { $ref: `#/components/schemas/${typeName}` };
|
|
608
|
+
} else {
|
|
609
|
+
schema = { type: "object" };
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
required: true,
|
|
613
|
+
content: {
|
|
614
|
+
"application/json": { schema }
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
buildResponses(returns, schemaMap) {
|
|
619
|
+
const responseSchema = this.resolveResponseSchema(returns, schemaMap);
|
|
620
|
+
if (!responseSchema) {
|
|
621
|
+
return { "200": { description: "Successful response" } };
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
"200": {
|
|
625
|
+
description: "Successful response",
|
|
626
|
+
content: {
|
|
627
|
+
"application/json": { schema: responseSchema }
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
resolveResponseSchema(returns, schemaMap) {
|
|
633
|
+
if (!returns) return null;
|
|
634
|
+
let innerType = returns;
|
|
635
|
+
const promiseMatch = returns.match(/^Promise<(.+)>$/);
|
|
636
|
+
if (promiseMatch) {
|
|
637
|
+
innerType = promiseMatch[1];
|
|
638
|
+
}
|
|
639
|
+
const isArray = innerType.endsWith("[]");
|
|
640
|
+
const baseType = isArray ? innerType.slice(0, -2) : innerType;
|
|
641
|
+
if (["string", "number", "boolean"].includes(baseType)) {
|
|
642
|
+
const primitiveSchema = this.tsTypeToJsonSchema(baseType);
|
|
643
|
+
return isArray ? { type: "array", items: primitiveSchema } : primitiveSchema;
|
|
644
|
+
}
|
|
645
|
+
if (["void", "any", "unknown"].includes(baseType)) return null;
|
|
646
|
+
if (schemaMap[baseType]) {
|
|
647
|
+
const ref = { $ref: `#/components/schemas/${baseType}` };
|
|
648
|
+
return isArray ? { type: "array", items: ref } : ref;
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
buildSchemaMap(schemas) {
|
|
653
|
+
const result = {};
|
|
654
|
+
for (const schemaInfo of schemas) {
|
|
655
|
+
const definition = schemaInfo.schema?.definitions?.[schemaInfo.type];
|
|
656
|
+
if (definition) {
|
|
657
|
+
result[schemaInfo.type] = definition;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Converts Express-style `:param` path to OpenAPI `{param}` syntax
|
|
664
|
+
*/
|
|
665
|
+
toOpenApiPath(expressPath) {
|
|
666
|
+
return expressPath.replace(/:(\w+)/g, "{$1}");
|
|
667
|
+
}
|
|
668
|
+
tsTypeToJsonSchema(tsType) {
|
|
669
|
+
switch (tsType) {
|
|
670
|
+
case "number":
|
|
671
|
+
return { type: "number" };
|
|
672
|
+
case "boolean":
|
|
673
|
+
return { type: "boolean" };
|
|
674
|
+
case "string":
|
|
675
|
+
default:
|
|
676
|
+
return { type: "string" };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Extracts the base type name from a TS type string, stripping
|
|
681
|
+
* wrappers like `Partial<...>`, `...[]`, `Promise<...>`.
|
|
682
|
+
*/
|
|
683
|
+
extractBaseTypeName(tsType) {
|
|
684
|
+
if (!tsType) return null;
|
|
685
|
+
let type = tsType;
|
|
686
|
+
const promiseMatch = type.match(/^Promise<(.+)>$/);
|
|
687
|
+
if (promiseMatch) type = promiseMatch[1];
|
|
688
|
+
type = type.replace(/\[\]$/, "");
|
|
689
|
+
const genericMatch = type.match(/^\w+<(\w+)>$/);
|
|
690
|
+
if (genericMatch) type = genericMatch[1];
|
|
691
|
+
if (["string", "number", "boolean", "any", "void", "unknown", "object"].includes(type)) {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
return type;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
484
698
|
// src/services/route-analyzer.service.ts
|
|
485
699
|
var import_honestjs = require("honestjs");
|
|
486
|
-
var import_ts_morph = require("ts-morph");
|
|
487
700
|
var RouteAnalyzerService = class {
|
|
488
|
-
constructor(controllerPattern, tsConfigPath) {
|
|
489
|
-
this.controllerPattern = controllerPattern;
|
|
490
|
-
this.tsConfigPath = tsConfigPath;
|
|
491
|
-
}
|
|
492
|
-
// Track projects for cleanup
|
|
493
|
-
projects = [];
|
|
494
701
|
/**
|
|
495
702
|
* Analyzes controller methods to extract type information
|
|
496
703
|
*/
|
|
497
|
-
async analyzeControllerMethods() {
|
|
704
|
+
async analyzeControllerMethods(project) {
|
|
498
705
|
const routes = import_honestjs.RouteRegistry.getRoutes();
|
|
499
706
|
if (!routes?.length) {
|
|
500
707
|
return [];
|
|
501
708
|
}
|
|
502
|
-
const project = this.createProject();
|
|
503
709
|
const controllers = this.findControllerClasses(project);
|
|
504
710
|
if (controllers.size === 0) {
|
|
505
711
|
return [];
|
|
506
712
|
}
|
|
507
713
|
return this.processRoutes(routes, controllers);
|
|
508
714
|
}
|
|
509
|
-
/**
|
|
510
|
-
* Creates a new ts-morph project
|
|
511
|
-
*/
|
|
512
|
-
createProject() {
|
|
513
|
-
const project = new import_ts_morph.Project({
|
|
514
|
-
tsConfigFilePath: this.tsConfigPath
|
|
515
|
-
});
|
|
516
|
-
project.addSourceFilesAtPaths([this.controllerPattern]);
|
|
517
|
-
this.projects.push(project);
|
|
518
|
-
return project;
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Cleanup resources to prevent memory leaks
|
|
522
|
-
*/
|
|
523
|
-
dispose() {
|
|
524
|
-
this.projects.forEach((project) => {
|
|
525
|
-
project.getSourceFiles().forEach((file) => project.removeSourceFile(file));
|
|
526
|
-
});
|
|
527
|
-
this.projects = [];
|
|
528
|
-
}
|
|
529
715
|
/**
|
|
530
716
|
* Finds controller classes in the project
|
|
531
717
|
*/
|
|
@@ -548,23 +734,17 @@ var RouteAnalyzerService = class {
|
|
|
548
734
|
*/
|
|
549
735
|
processRoutes(routes, controllers) {
|
|
550
736
|
const analyzedRoutes = [];
|
|
551
|
-
const errors = [];
|
|
552
737
|
for (const route of routes) {
|
|
553
738
|
try {
|
|
554
739
|
const extendedRoute = this.createExtendedRoute(route, controllers);
|
|
555
740
|
analyzedRoutes.push(extendedRoute);
|
|
556
741
|
} catch (routeError) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
console.error(
|
|
560
|
-
`Error processing route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
|
|
742
|
+
console.warn(
|
|
743
|
+
`${LOG_PREFIX} Skipping route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
|
|
561
744
|
routeError
|
|
562
745
|
);
|
|
563
746
|
}
|
|
564
747
|
}
|
|
565
|
-
if (errors.length > 0) {
|
|
566
|
-
throw new Error(`Failed to process ${errors.length} routes: ${errors.map((e) => e.message).join(", ")}`);
|
|
567
|
-
}
|
|
568
748
|
return analyzedRoutes;
|
|
569
749
|
}
|
|
570
750
|
/**
|
|
@@ -617,6 +797,7 @@ var RouteAnalyzerService = class {
|
|
|
617
797
|
const sortedParams = [...parameters].sort((a, b) => a.index - b.index);
|
|
618
798
|
for (const param of sortedParams) {
|
|
619
799
|
const index = param.index;
|
|
800
|
+
const decoratorType = param.name;
|
|
620
801
|
if (index < declaredParams.length) {
|
|
621
802
|
const declaredParam = declaredParams[index];
|
|
622
803
|
const paramName = declaredParam.getName();
|
|
@@ -624,6 +805,7 @@ var RouteAnalyzerService = class {
|
|
|
624
805
|
result.push({
|
|
625
806
|
index,
|
|
626
807
|
name: paramName,
|
|
808
|
+
decoratorType,
|
|
627
809
|
type: paramType,
|
|
628
810
|
required: true,
|
|
629
811
|
data: param.data,
|
|
@@ -634,6 +816,7 @@ var RouteAnalyzerService = class {
|
|
|
634
816
|
result.push({
|
|
635
817
|
index,
|
|
636
818
|
name: `param${index}`,
|
|
819
|
+
decoratorType,
|
|
637
820
|
type: param.metatype?.name || "unknown",
|
|
638
821
|
required: true,
|
|
639
822
|
data: param.data,
|
|
@@ -648,7 +831,6 @@ var RouteAnalyzerService = class {
|
|
|
648
831
|
|
|
649
832
|
// src/services/schema-generator.service.ts
|
|
650
833
|
var import_ts_json_schema_generator = require("ts-json-schema-generator");
|
|
651
|
-
var import_ts_morph2 = require("ts-morph");
|
|
652
834
|
|
|
653
835
|
// src/utils/schema-utils.ts
|
|
654
836
|
function mapJsonSchemaTypeToTypeScript(schema) {
|
|
@@ -715,32 +897,6 @@ function extractNamedType(type) {
|
|
|
715
897
|
if (BUILTIN_TYPES.has(name)) return null;
|
|
716
898
|
return name;
|
|
717
899
|
}
|
|
718
|
-
function generateTypeImports(routes) {
|
|
719
|
-
const types = /* @__PURE__ */ new Set();
|
|
720
|
-
for (const route of routes) {
|
|
721
|
-
if (route.parameters) {
|
|
722
|
-
for (const param of route.parameters) {
|
|
723
|
-
if (param.type && !["string", "number", "boolean"].includes(param.type)) {
|
|
724
|
-
const typeMatch = param.type.match(/(\w+)(?:<.*>)?/);
|
|
725
|
-
if (typeMatch) {
|
|
726
|
-
const typeName = typeMatch[1];
|
|
727
|
-
if (!BUILTIN_UTILITY_TYPES.has(typeName)) {
|
|
728
|
-
types.add(typeName);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
if (route.returns) {
|
|
735
|
-
const returnType = route.returns.replace(/Promise<(.+)>/, "$1");
|
|
736
|
-
const baseType = returnType.replace(/\[\]$/, "");
|
|
737
|
-
if (!["string", "number", "boolean", "any", "void", "unknown"].includes(baseType)) {
|
|
738
|
-
types.add(baseType);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
return Array.from(types).join(", ");
|
|
743
|
-
}
|
|
744
900
|
|
|
745
901
|
// src/services/schema-generator.service.ts
|
|
746
902
|
var SchemaGeneratorService = class {
|
|
@@ -748,37 +904,14 @@ var SchemaGeneratorService = class {
|
|
|
748
904
|
this.controllerPattern = controllerPattern;
|
|
749
905
|
this.tsConfigPath = tsConfigPath;
|
|
750
906
|
}
|
|
751
|
-
// Track projects for cleanup
|
|
752
|
-
projects = [];
|
|
753
907
|
/**
|
|
754
908
|
* Generates JSON schemas from types used in controllers
|
|
755
909
|
*/
|
|
756
|
-
async generateSchemas() {
|
|
757
|
-
const project = this.createProject();
|
|
910
|
+
async generateSchemas(project) {
|
|
758
911
|
const sourceFiles = project.getSourceFiles(this.controllerPattern);
|
|
759
912
|
const collectedTypes = this.collectTypesFromControllers(sourceFiles);
|
|
760
913
|
return this.processTypes(collectedTypes);
|
|
761
914
|
}
|
|
762
|
-
/**
|
|
763
|
-
* Creates a new ts-morph project
|
|
764
|
-
*/
|
|
765
|
-
createProject() {
|
|
766
|
-
const project = new import_ts_morph2.Project({
|
|
767
|
-
tsConfigFilePath: this.tsConfigPath
|
|
768
|
-
});
|
|
769
|
-
project.addSourceFilesAtPaths([this.controllerPattern]);
|
|
770
|
-
this.projects.push(project);
|
|
771
|
-
return project;
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Cleanup resources to prevent memory leaks
|
|
775
|
-
*/
|
|
776
|
-
dispose() {
|
|
777
|
-
this.projects.forEach((project) => {
|
|
778
|
-
project.getSourceFiles().forEach((file) => project.removeSourceFile(file));
|
|
779
|
-
});
|
|
780
|
-
this.projects = [];
|
|
781
|
-
}
|
|
782
915
|
/**
|
|
783
916
|
* Collects types from controller files
|
|
784
917
|
*/
|
|
@@ -860,20 +993,37 @@ var RPCPlugin = class {
|
|
|
860
993
|
routeAnalyzer;
|
|
861
994
|
schemaGenerator;
|
|
862
995
|
clientGenerator;
|
|
996
|
+
openApiGenerator;
|
|
997
|
+
openApiOptions;
|
|
998
|
+
// Shared ts-morph project
|
|
999
|
+
project = null;
|
|
863
1000
|
// Internal state
|
|
864
1001
|
analyzedRoutes = [];
|
|
865
1002
|
analyzedSchemas = [];
|
|
866
1003
|
generatedInfo = null;
|
|
867
1004
|
constructor(options = {}) {
|
|
868
1005
|
this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
|
|
869
|
-
this.tsConfigPath = options.tsConfigPath ??
|
|
870
|
-
this.outputDir = options.outputDir ??
|
|
1006
|
+
this.tsConfigPath = options.tsConfigPath ?? import_path4.default.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
|
|
1007
|
+
this.outputDir = options.outputDir ?? import_path4.default.resolve(process.cwd(), DEFAULT_OPTIONS.outputDir);
|
|
871
1008
|
this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
|
|
872
|
-
this.routeAnalyzer = new RouteAnalyzerService(
|
|
1009
|
+
this.routeAnalyzer = new RouteAnalyzerService();
|
|
873
1010
|
this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath);
|
|
874
1011
|
this.clientGenerator = new ClientGeneratorService(this.outputDir);
|
|
1012
|
+
this.openApiOptions = this.resolveOpenApiOptions(options.openapi);
|
|
1013
|
+
this.openApiGenerator = this.openApiOptions ? new OpenApiGeneratorService(this.outputDir) : null;
|
|
875
1014
|
this.validateConfiguration();
|
|
876
1015
|
}
|
|
1016
|
+
resolveOpenApiOptions(input) {
|
|
1017
|
+
if (!input) return null;
|
|
1018
|
+
const opts = input === true ? {} : input;
|
|
1019
|
+
return {
|
|
1020
|
+
title: opts.title ?? "API",
|
|
1021
|
+
version: opts.version ?? "1.0.0",
|
|
1022
|
+
description: opts.description ?? "",
|
|
1023
|
+
servers: opts.servers ?? [],
|
|
1024
|
+
outputFile: opts.outputFile ?? "openapi.json"
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
877
1027
|
/**
|
|
878
1028
|
* Validates the plugin configuration
|
|
879
1029
|
*/
|
|
@@ -885,7 +1035,7 @@ var RPCPlugin = class {
|
|
|
885
1035
|
if (!this.tsConfigPath?.trim()) {
|
|
886
1036
|
errors.push("TypeScript config path cannot be empty");
|
|
887
1037
|
} else {
|
|
888
|
-
if (!
|
|
1038
|
+
if (!import_fs2.default.existsSync(this.tsConfigPath)) {
|
|
889
1039
|
errors.push(`TypeScript config file not found at: ${this.tsConfigPath}`);
|
|
890
1040
|
}
|
|
891
1041
|
}
|
|
@@ -910,15 +1060,37 @@ var RPCPlugin = class {
|
|
|
910
1060
|
/**
|
|
911
1061
|
* Main analysis method that coordinates all three components
|
|
912
1062
|
*/
|
|
913
|
-
async analyzeEverything() {
|
|
1063
|
+
async analyzeEverything(force = false) {
|
|
914
1064
|
try {
|
|
915
1065
|
this.log("Starting comprehensive RPC analysis...");
|
|
916
1066
|
this.analyzedRoutes = [];
|
|
917
1067
|
this.analyzedSchemas = [];
|
|
918
1068
|
this.generatedInfo = null;
|
|
919
|
-
this.
|
|
920
|
-
this.
|
|
1069
|
+
this.dispose();
|
|
1070
|
+
this.project = new import_ts_morph.Project({ tsConfigFilePath: this.tsConfigPath });
|
|
1071
|
+
this.project.addSourceFilesAtPaths([this.controllerPattern]);
|
|
1072
|
+
const filePaths = this.project.getSourceFiles().map((f) => f.getFilePath());
|
|
1073
|
+
if (!force) {
|
|
1074
|
+
const currentHash = computeHash(filePaths);
|
|
1075
|
+
const stored = readChecksum(this.outputDir);
|
|
1076
|
+
if (stored && stored.hash === currentHash && this.outputFilesExist()) {
|
|
1077
|
+
this.log("Source files unchanged \u2014 skipping regeneration");
|
|
1078
|
+
this.dispose();
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
this.analyzedRoutes = await this.routeAnalyzer.analyzeControllerMethods(this.project);
|
|
1083
|
+
this.analyzedSchemas = await this.schemaGenerator.generateSchemas(this.project);
|
|
921
1084
|
this.generatedInfo = await this.clientGenerator.generateClient(this.analyzedRoutes, this.analyzedSchemas);
|
|
1085
|
+
if (this.openApiGenerator && this.openApiOptions) {
|
|
1086
|
+
const specPath = await this.openApiGenerator.generateSpec(
|
|
1087
|
+
this.analyzedRoutes,
|
|
1088
|
+
this.analyzedSchemas,
|
|
1089
|
+
this.openApiOptions
|
|
1090
|
+
);
|
|
1091
|
+
this.log(`OpenAPI spec generated: ${specPath}`);
|
|
1092
|
+
}
|
|
1093
|
+
await writeChecksum(this.outputDir, { hash: computeHash(filePaths), files: filePaths });
|
|
922
1094
|
this.log(
|
|
923
1095
|
`\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
|
|
924
1096
|
);
|
|
@@ -929,10 +1101,11 @@ var RPCPlugin = class {
|
|
|
929
1101
|
}
|
|
930
1102
|
}
|
|
931
1103
|
/**
|
|
932
|
-
* Manually trigger analysis (useful for testing or re-generation)
|
|
1104
|
+
* Manually trigger analysis (useful for testing or re-generation).
|
|
1105
|
+
* Defaults to force=true to bypass cache; pass false to use caching.
|
|
933
1106
|
*/
|
|
934
|
-
async analyze() {
|
|
935
|
-
await this.analyzeEverything();
|
|
1107
|
+
async analyze(force = true) {
|
|
1108
|
+
await this.analyzeEverything(force);
|
|
936
1109
|
}
|
|
937
1110
|
/**
|
|
938
1111
|
* Get the analyzed routes
|
|
@@ -952,13 +1125,24 @@ var RPCPlugin = class {
|
|
|
952
1125
|
getGenerationInfo() {
|
|
953
1126
|
return this.generatedInfo;
|
|
954
1127
|
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Checks whether expected output files exist on disk
|
|
1130
|
+
*/
|
|
1131
|
+
outputFilesExist() {
|
|
1132
|
+
if (!import_fs2.default.existsSync(import_path4.default.join(this.outputDir, "client.ts"))) return false;
|
|
1133
|
+
if (this.openApiOptions) {
|
|
1134
|
+
return import_fs2.default.existsSync(import_path4.default.join(this.outputDir, this.openApiOptions.outputFile));
|
|
1135
|
+
}
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
955
1138
|
/**
|
|
956
1139
|
* Cleanup resources to prevent memory leaks
|
|
957
1140
|
*/
|
|
958
1141
|
dispose() {
|
|
959
|
-
this.
|
|
960
|
-
|
|
961
|
-
|
|
1142
|
+
if (this.project) {
|
|
1143
|
+
this.project.getSourceFiles().forEach((file) => this.project.removeSourceFile(file));
|
|
1144
|
+
this.project = null;
|
|
1145
|
+
}
|
|
962
1146
|
}
|
|
963
1147
|
// ============================================================================
|
|
964
1148
|
// LOGGING UTILITIES
|
|
@@ -984,16 +1168,19 @@ var RPCPlugin = class {
|
|
|
984
1168
|
DEFAULT_OPTIONS,
|
|
985
1169
|
GENERIC_TYPES,
|
|
986
1170
|
LOG_PREFIX,
|
|
1171
|
+
OpenApiGeneratorService,
|
|
987
1172
|
RPCPlugin,
|
|
988
1173
|
RouteAnalyzerService,
|
|
989
1174
|
SchemaGeneratorService,
|
|
990
1175
|
buildFullApiPath,
|
|
991
1176
|
buildFullPath,
|
|
992
1177
|
camelCase,
|
|
1178
|
+
computeHash,
|
|
993
1179
|
extractNamedType,
|
|
994
|
-
generateTypeImports,
|
|
995
1180
|
generateTypeScriptInterface,
|
|
996
1181
|
mapJsonSchemaTypeToTypeScript,
|
|
997
|
-
|
|
1182
|
+
readChecksum,
|
|
1183
|
+
safeToString,
|
|
1184
|
+
writeChecksum
|
|
998
1185
|
});
|
|
999
1186
|
//# sourceMappingURL=index.js.map
|