@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.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/rpc.plugin.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs3 from "fs";
|
|
3
|
+
import path4 from "path";
|
|
4
|
+
import { Project } from "ts-morph";
|
|
4
5
|
|
|
5
6
|
// src/constants/defaults.ts
|
|
6
7
|
var DEFAULT_OPTIONS = {
|
|
@@ -25,29 +26,66 @@ var BUILTIN_UTILITY_TYPES = /* @__PURE__ */ new Set([
|
|
|
25
26
|
var BUILTIN_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "any", "void", "unknown"]);
|
|
26
27
|
var GENERIC_TYPES = /* @__PURE__ */ new Set(["Array", "Promise", "Partial"]);
|
|
27
28
|
|
|
29
|
+
// src/utils/hash-utils.ts
|
|
30
|
+
import { createHash } from "crypto";
|
|
31
|
+
import { existsSync, readFileSync } from "fs";
|
|
32
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
33
|
+
import path from "path";
|
|
34
|
+
var CHECKSUM_FILENAME = ".rpc-checksum";
|
|
35
|
+
function computeHash(filePaths) {
|
|
36
|
+
const sorted = [...filePaths].sort();
|
|
37
|
+
const hasher = createHash("sha256");
|
|
38
|
+
hasher.update(`files:${sorted.length}
|
|
39
|
+
`);
|
|
40
|
+
for (const filePath of sorted) {
|
|
41
|
+
hasher.update(readFileSync(filePath, "utf-8"));
|
|
42
|
+
hasher.update("\0");
|
|
43
|
+
}
|
|
44
|
+
return hasher.digest("hex");
|
|
45
|
+
}
|
|
46
|
+
function readChecksum(outputDir) {
|
|
47
|
+
const checksumPath = path.join(outputDir, CHECKSUM_FILENAME);
|
|
48
|
+
if (!existsSync(checksumPath)) return null;
|
|
49
|
+
try {
|
|
50
|
+
const raw = readFileSync(checksumPath, "utf-8");
|
|
51
|
+
const data = JSON.parse(raw);
|
|
52
|
+
if (typeof data.hash !== "string" || !Array.isArray(data.files)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return data;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function writeChecksum(outputDir, data) {
|
|
61
|
+
await mkdir(outputDir, { recursive: true });
|
|
62
|
+
const checksumPath = path.join(outputDir, CHECKSUM_FILENAME);
|
|
63
|
+
await writeFile(checksumPath, JSON.stringify(data, null, 2), "utf-8");
|
|
64
|
+
}
|
|
65
|
+
|
|
28
66
|
// src/services/client-generator.service.ts
|
|
29
67
|
import fs from "fs/promises";
|
|
30
|
-
import
|
|
68
|
+
import path2 from "path";
|
|
31
69
|
|
|
32
70
|
// src/utils/path-utils.ts
|
|
33
71
|
function buildFullPath(basePath, parameters) {
|
|
34
72
|
if (!basePath || typeof basePath !== "string") return "/";
|
|
35
|
-
let
|
|
73
|
+
let path5 = basePath;
|
|
36
74
|
if (parameters && Array.isArray(parameters)) {
|
|
37
75
|
for (const param of parameters) {
|
|
38
76
|
if (param.data && typeof param.data === "string" && param.data.startsWith(":")) {
|
|
39
77
|
const paramName = param.data.slice(1);
|
|
40
|
-
|
|
78
|
+
path5 = path5.replace(`:${paramName}`, `\${${paramName}}`);
|
|
41
79
|
}
|
|
42
80
|
}
|
|
43
81
|
}
|
|
44
|
-
return
|
|
82
|
+
return path5;
|
|
45
83
|
}
|
|
46
84
|
function buildFullApiPath(route) {
|
|
47
85
|
const prefix = route.prefix || "";
|
|
48
86
|
const version = route.version || "";
|
|
49
87
|
const routePath = route.route || "";
|
|
50
|
-
const
|
|
88
|
+
const path5 = route.path || "";
|
|
51
89
|
let fullPath = "";
|
|
52
90
|
if (prefix && prefix !== "/") {
|
|
53
91
|
fullPath += prefix.replace(/^\/+|\/+$/g, "");
|
|
@@ -58,11 +96,12 @@ function buildFullApiPath(route) {
|
|
|
58
96
|
if (routePath && routePath !== "/") {
|
|
59
97
|
fullPath += `/${routePath.replace(/^\/+|\/+$/g, "")}`;
|
|
60
98
|
}
|
|
61
|
-
if (
|
|
62
|
-
fullPath += `/${
|
|
63
|
-
} else if (
|
|
99
|
+
if (path5 && path5 !== "/") {
|
|
100
|
+
fullPath += `/${path5.replace(/^\/+|\/+$/g, "")}`;
|
|
101
|
+
} else if (path5 === "/") {
|
|
64
102
|
fullPath += "/";
|
|
65
103
|
}
|
|
104
|
+
if (fullPath && !fullPath.startsWith("/")) fullPath = "/" + fullPath;
|
|
66
105
|
return fullPath || "/";
|
|
67
106
|
}
|
|
68
107
|
|
|
@@ -88,7 +127,7 @@ var ClientGeneratorService = class {
|
|
|
88
127
|
await fs.mkdir(this.outputDir, { recursive: true });
|
|
89
128
|
await this.generateClientFile(routes, schemas);
|
|
90
129
|
const generatedInfo = {
|
|
91
|
-
clientFile:
|
|
130
|
+
clientFile: path2.join(this.outputDir, "client.ts"),
|
|
92
131
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
93
132
|
};
|
|
94
133
|
return generatedInfo;
|
|
@@ -98,7 +137,7 @@ var ClientGeneratorService = class {
|
|
|
98
137
|
*/
|
|
99
138
|
async generateClientFile(routes, schemas) {
|
|
100
139
|
const clientContent = this.generateClientContent(routes, schemas);
|
|
101
|
-
const clientPath =
|
|
140
|
+
const clientPath = path2.join(this.outputDir, "client.ts");
|
|
102
141
|
await fs.writeFile(clientPath, clientContent, "utf-8");
|
|
103
142
|
}
|
|
104
143
|
/**
|
|
@@ -110,15 +149,6 @@ var ClientGeneratorService = class {
|
|
|
110
149
|
// TYPES SECTION
|
|
111
150
|
// ============================================================================
|
|
112
151
|
|
|
113
|
-
/**
|
|
114
|
-
* API Response wrapper
|
|
115
|
-
*/
|
|
116
|
-
export interface ApiResponse<T = any> {
|
|
117
|
-
data: T
|
|
118
|
-
message?: string
|
|
119
|
-
success: boolean
|
|
120
|
-
}
|
|
121
|
-
|
|
122
152
|
/**
|
|
123
153
|
* API Error class
|
|
124
154
|
*/
|
|
@@ -231,7 +261,7 @@ export class ApiClient {
|
|
|
231
261
|
method: string,
|
|
232
262
|
path: string,
|
|
233
263
|
options: RequestOptions<any, any, any, any> = {}
|
|
234
|
-
): Promise<
|
|
264
|
+
): Promise<T> {
|
|
235
265
|
const { params, query, body, headers = {} } = options as any
|
|
236
266
|
|
|
237
267
|
// Build the final URL with path parameters
|
|
@@ -267,6 +297,14 @@ export class ApiClient {
|
|
|
267
297
|
|
|
268
298
|
try {
|
|
269
299
|
const response = await this.fetchFn(url.toString(), requestOptions)
|
|
300
|
+
|
|
301
|
+
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
|
302
|
+
if (!response.ok) {
|
|
303
|
+
throw new ApiError(response.status, 'Request failed')
|
|
304
|
+
}
|
|
305
|
+
return undefined as T
|
|
306
|
+
}
|
|
307
|
+
|
|
270
308
|
const responseData = await response.json()
|
|
271
309
|
|
|
272
310
|
if (!response.ok) {
|
|
@@ -304,8 +342,9 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
304
342
|
const methodName = camelCase(safeToString(route.handler));
|
|
305
343
|
const httpMethod = safeToString(route.method).toLowerCase();
|
|
306
344
|
const { pathParams, queryParams, bodyParams } = this.analyzeRouteParameters(route);
|
|
345
|
+
const returnType = this.extractReturnType(route.returns);
|
|
307
346
|
const hasRequiredParams = pathParams.length > 0 || queryParams.some((p) => p.required) || bodyParams.length > 0 && httpMethod !== "get";
|
|
308
|
-
methods += ` ${methodName}: async (options${hasRequiredParams ? "" : "?"}: RequestOptions<`;
|
|
347
|
+
methods += ` ${methodName}: async <Result = ${returnType}>(options${hasRequiredParams ? "" : "?"}: RequestOptions<`;
|
|
309
348
|
if (pathParams.length > 0) {
|
|
310
349
|
const pathParamTypes = pathParams.map((p) => {
|
|
311
350
|
const paramName = p.name;
|
|
@@ -339,8 +378,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
339
378
|
}
|
|
340
379
|
methods += ", ";
|
|
341
380
|
methods += "undefined";
|
|
342
|
-
|
|
343
|
-
methods += `>): Promise<ApiResponse<${returnType}>> => {
|
|
381
|
+
methods += `>) => {
|
|
344
382
|
`;
|
|
345
383
|
let requestPath = buildFullApiPath(route);
|
|
346
384
|
if (pathParams.length > 0) {
|
|
@@ -350,7 +388,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
350
388
|
requestPath = requestPath.replace(placeholder, `:${paramName}`);
|
|
351
389
|
}
|
|
352
390
|
}
|
|
353
|
-
methods += ` return this.request
|
|
391
|
+
methods += ` return this.request<Result>('${httpMethod.toUpperCase()}', \`${requestPath}\`, options)
|
|
354
392
|
`;
|
|
355
393
|
methods += ` },
|
|
356
394
|
`;
|
|
@@ -409,71 +447,216 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
409
447
|
*/
|
|
410
448
|
analyzeRouteParameters(route) {
|
|
411
449
|
const parameters = route.parameters || [];
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
return !!pathSegment && typeof pathSegment === "string" && route.path.includes(`:${pathSegment}`);
|
|
416
|
-
};
|
|
417
|
-
const pathParams = parameters.filter((p) => isInPath(p)).map((p) => ({ ...p, required: true }));
|
|
418
|
-
const rawBody = parameters.filter((p) => !isInPath(p) && method !== "get");
|
|
419
|
-
const bodyParams = rawBody.map((p) => ({
|
|
420
|
-
...p,
|
|
421
|
-
required: true
|
|
422
|
-
}));
|
|
423
|
-
const queryParams = parameters.filter((p) => !isInPath(p) && method === "get").map((p) => ({
|
|
424
|
-
...p,
|
|
425
|
-
required: p.required === true
|
|
426
|
-
// default false if not provided
|
|
427
|
-
}));
|
|
450
|
+
const pathParams = parameters.filter((p) => p.decoratorType === "param").map((p) => ({ ...p, required: true }));
|
|
451
|
+
const bodyParams = parameters.filter((p) => p.decoratorType === "body").map((p) => ({ ...p, required: true }));
|
|
452
|
+
const queryParams = parameters.filter((p) => p.decoratorType === "query").map((p) => ({ ...p, required: p.required === true }));
|
|
428
453
|
return { pathParams, queryParams, bodyParams };
|
|
429
454
|
}
|
|
430
455
|
};
|
|
431
456
|
|
|
457
|
+
// src/services/openapi-generator.service.ts
|
|
458
|
+
import fs2 from "fs/promises";
|
|
459
|
+
import path3 from "path";
|
|
460
|
+
var OpenApiGeneratorService = class {
|
|
461
|
+
constructor(outputDir) {
|
|
462
|
+
this.outputDir = outputDir;
|
|
463
|
+
}
|
|
464
|
+
async generateSpec(routes, schemas, options) {
|
|
465
|
+
await fs2.mkdir(this.outputDir, { recursive: true });
|
|
466
|
+
const schemaMap = this.buildSchemaMap(schemas);
|
|
467
|
+
const spec = this.buildSpec(routes, schemaMap, options);
|
|
468
|
+
const outputPath = path3.join(this.outputDir, options.outputFile);
|
|
469
|
+
await fs2.writeFile(outputPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
470
|
+
return outputPath;
|
|
471
|
+
}
|
|
472
|
+
buildSpec(routes, schemaMap, options) {
|
|
473
|
+
const spec = {
|
|
474
|
+
openapi: "3.0.3",
|
|
475
|
+
info: {
|
|
476
|
+
title: options.title,
|
|
477
|
+
version: options.version,
|
|
478
|
+
description: options.description
|
|
479
|
+
},
|
|
480
|
+
paths: {},
|
|
481
|
+
components: { schemas: schemaMap }
|
|
482
|
+
};
|
|
483
|
+
if (options.servers.length > 0) {
|
|
484
|
+
spec.servers = options.servers.map((s) => ({ ...s }));
|
|
485
|
+
}
|
|
486
|
+
for (const route of routes) {
|
|
487
|
+
const apiPath = this.toOpenApiPath(buildFullApiPath(route));
|
|
488
|
+
const method = safeToString(route.method).toLowerCase();
|
|
489
|
+
if (!spec.paths[apiPath]) {
|
|
490
|
+
spec.paths[apiPath] = {};
|
|
491
|
+
}
|
|
492
|
+
spec.paths[apiPath][method] = this.buildOperation(route, schemaMap);
|
|
493
|
+
}
|
|
494
|
+
return spec;
|
|
495
|
+
}
|
|
496
|
+
buildOperation(route, schemaMap) {
|
|
497
|
+
const controllerName = safeToString(route.controller).replace(/Controller$/, "");
|
|
498
|
+
const handlerName = safeToString(route.handler);
|
|
499
|
+
const parameters = route.parameters || [];
|
|
500
|
+
const operation = {
|
|
501
|
+
operationId: handlerName,
|
|
502
|
+
tags: [controllerName],
|
|
503
|
+
responses: this.buildResponses(route.returns, schemaMap)
|
|
504
|
+
};
|
|
505
|
+
const openApiParams = this.buildParameters(parameters);
|
|
506
|
+
if (openApiParams.length > 0) {
|
|
507
|
+
operation.parameters = openApiParams;
|
|
508
|
+
}
|
|
509
|
+
const requestBody = this.buildRequestBody(parameters, schemaMap);
|
|
510
|
+
if (requestBody) {
|
|
511
|
+
operation.requestBody = requestBody;
|
|
512
|
+
}
|
|
513
|
+
return operation;
|
|
514
|
+
}
|
|
515
|
+
buildParameters(parameters) {
|
|
516
|
+
const result = [];
|
|
517
|
+
for (const param of parameters) {
|
|
518
|
+
if (param.decoratorType === "param") {
|
|
519
|
+
result.push({
|
|
520
|
+
name: param.data ?? param.name,
|
|
521
|
+
in: "path",
|
|
522
|
+
required: true,
|
|
523
|
+
schema: this.tsTypeToJsonSchema(param.type)
|
|
524
|
+
});
|
|
525
|
+
} else if (param.decoratorType === "query") {
|
|
526
|
+
if (param.data) {
|
|
527
|
+
result.push({
|
|
528
|
+
name: param.data,
|
|
529
|
+
in: "query",
|
|
530
|
+
required: param.required === true,
|
|
531
|
+
schema: this.tsTypeToJsonSchema(param.type)
|
|
532
|
+
});
|
|
533
|
+
} else {
|
|
534
|
+
result.push({
|
|
535
|
+
name: param.name,
|
|
536
|
+
in: "query",
|
|
537
|
+
required: param.required === true,
|
|
538
|
+
schema: this.tsTypeToJsonSchema(param.type)
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
buildRequestBody(parameters, schemaMap) {
|
|
546
|
+
const bodyParams = parameters.filter((p) => p.decoratorType === "body");
|
|
547
|
+
if (bodyParams.length === 0) return null;
|
|
548
|
+
const bodyParam = bodyParams[0];
|
|
549
|
+
const typeName = this.extractBaseTypeName(bodyParam.type);
|
|
550
|
+
let schema;
|
|
551
|
+
if (typeName && schemaMap[typeName]) {
|
|
552
|
+
schema = { $ref: `#/components/schemas/${typeName}` };
|
|
553
|
+
} else {
|
|
554
|
+
schema = { type: "object" };
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
required: true,
|
|
558
|
+
content: {
|
|
559
|
+
"application/json": { schema }
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
buildResponses(returns, schemaMap) {
|
|
564
|
+
const responseSchema = this.resolveResponseSchema(returns, schemaMap);
|
|
565
|
+
if (!responseSchema) {
|
|
566
|
+
return { "200": { description: "Successful response" } };
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
"200": {
|
|
570
|
+
description: "Successful response",
|
|
571
|
+
content: {
|
|
572
|
+
"application/json": { schema: responseSchema }
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
resolveResponseSchema(returns, schemaMap) {
|
|
578
|
+
if (!returns) return null;
|
|
579
|
+
let innerType = returns;
|
|
580
|
+
const promiseMatch = returns.match(/^Promise<(.+)>$/);
|
|
581
|
+
if (promiseMatch) {
|
|
582
|
+
innerType = promiseMatch[1];
|
|
583
|
+
}
|
|
584
|
+
const isArray = innerType.endsWith("[]");
|
|
585
|
+
const baseType = isArray ? innerType.slice(0, -2) : innerType;
|
|
586
|
+
if (["string", "number", "boolean"].includes(baseType)) {
|
|
587
|
+
const primitiveSchema = this.tsTypeToJsonSchema(baseType);
|
|
588
|
+
return isArray ? { type: "array", items: primitiveSchema } : primitiveSchema;
|
|
589
|
+
}
|
|
590
|
+
if (["void", "any", "unknown"].includes(baseType)) return null;
|
|
591
|
+
if (schemaMap[baseType]) {
|
|
592
|
+
const ref = { $ref: `#/components/schemas/${baseType}` };
|
|
593
|
+
return isArray ? { type: "array", items: ref } : ref;
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
buildSchemaMap(schemas) {
|
|
598
|
+
const result = {};
|
|
599
|
+
for (const schemaInfo of schemas) {
|
|
600
|
+
const definition = schemaInfo.schema?.definitions?.[schemaInfo.type];
|
|
601
|
+
if (definition) {
|
|
602
|
+
result[schemaInfo.type] = definition;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Converts Express-style `:param` path to OpenAPI `{param}` syntax
|
|
609
|
+
*/
|
|
610
|
+
toOpenApiPath(expressPath) {
|
|
611
|
+
return expressPath.replace(/:(\w+)/g, "{$1}");
|
|
612
|
+
}
|
|
613
|
+
tsTypeToJsonSchema(tsType) {
|
|
614
|
+
switch (tsType) {
|
|
615
|
+
case "number":
|
|
616
|
+
return { type: "number" };
|
|
617
|
+
case "boolean":
|
|
618
|
+
return { type: "boolean" };
|
|
619
|
+
case "string":
|
|
620
|
+
default:
|
|
621
|
+
return { type: "string" };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Extracts the base type name from a TS type string, stripping
|
|
626
|
+
* wrappers like `Partial<...>`, `...[]`, `Promise<...>`.
|
|
627
|
+
*/
|
|
628
|
+
extractBaseTypeName(tsType) {
|
|
629
|
+
if (!tsType) return null;
|
|
630
|
+
let type = tsType;
|
|
631
|
+
const promiseMatch = type.match(/^Promise<(.+)>$/);
|
|
632
|
+
if (promiseMatch) type = promiseMatch[1];
|
|
633
|
+
type = type.replace(/\[\]$/, "");
|
|
634
|
+
const genericMatch = type.match(/^\w+<(\w+)>$/);
|
|
635
|
+
if (genericMatch) type = genericMatch[1];
|
|
636
|
+
if (["string", "number", "boolean", "any", "void", "unknown", "object"].includes(type)) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
return type;
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
432
643
|
// src/services/route-analyzer.service.ts
|
|
433
644
|
import { RouteRegistry } from "honestjs";
|
|
434
|
-
import { Project } from "ts-morph";
|
|
435
645
|
var RouteAnalyzerService = class {
|
|
436
|
-
constructor(controllerPattern, tsConfigPath) {
|
|
437
|
-
this.controllerPattern = controllerPattern;
|
|
438
|
-
this.tsConfigPath = tsConfigPath;
|
|
439
|
-
}
|
|
440
|
-
// Track projects for cleanup
|
|
441
|
-
projects = [];
|
|
442
646
|
/**
|
|
443
647
|
* Analyzes controller methods to extract type information
|
|
444
648
|
*/
|
|
445
|
-
async analyzeControllerMethods() {
|
|
649
|
+
async analyzeControllerMethods(project) {
|
|
446
650
|
const routes = RouteRegistry.getRoutes();
|
|
447
651
|
if (!routes?.length) {
|
|
448
652
|
return [];
|
|
449
653
|
}
|
|
450
|
-
const project = this.createProject();
|
|
451
654
|
const controllers = this.findControllerClasses(project);
|
|
452
655
|
if (controllers.size === 0) {
|
|
453
656
|
return [];
|
|
454
657
|
}
|
|
455
658
|
return this.processRoutes(routes, controllers);
|
|
456
659
|
}
|
|
457
|
-
/**
|
|
458
|
-
* Creates a new ts-morph project
|
|
459
|
-
*/
|
|
460
|
-
createProject() {
|
|
461
|
-
const project = new Project({
|
|
462
|
-
tsConfigFilePath: this.tsConfigPath
|
|
463
|
-
});
|
|
464
|
-
project.addSourceFilesAtPaths([this.controllerPattern]);
|
|
465
|
-
this.projects.push(project);
|
|
466
|
-
return project;
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Cleanup resources to prevent memory leaks
|
|
470
|
-
*/
|
|
471
|
-
dispose() {
|
|
472
|
-
this.projects.forEach((project) => {
|
|
473
|
-
project.getSourceFiles().forEach((file) => project.removeSourceFile(file));
|
|
474
|
-
});
|
|
475
|
-
this.projects = [];
|
|
476
|
-
}
|
|
477
660
|
/**
|
|
478
661
|
* Finds controller classes in the project
|
|
479
662
|
*/
|
|
@@ -496,23 +679,17 @@ var RouteAnalyzerService = class {
|
|
|
496
679
|
*/
|
|
497
680
|
processRoutes(routes, controllers) {
|
|
498
681
|
const analyzedRoutes = [];
|
|
499
|
-
const errors = [];
|
|
500
682
|
for (const route of routes) {
|
|
501
683
|
try {
|
|
502
684
|
const extendedRoute = this.createExtendedRoute(route, controllers);
|
|
503
685
|
analyzedRoutes.push(extendedRoute);
|
|
504
686
|
} catch (routeError) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
console.error(
|
|
508
|
-
`Error processing route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
|
|
687
|
+
console.warn(
|
|
688
|
+
`${LOG_PREFIX} Skipping route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
|
|
509
689
|
routeError
|
|
510
690
|
);
|
|
511
691
|
}
|
|
512
692
|
}
|
|
513
|
-
if (errors.length > 0) {
|
|
514
|
-
throw new Error(`Failed to process ${errors.length} routes: ${errors.map((e) => e.message).join(", ")}`);
|
|
515
|
-
}
|
|
516
693
|
return analyzedRoutes;
|
|
517
694
|
}
|
|
518
695
|
/**
|
|
@@ -565,6 +742,7 @@ var RouteAnalyzerService = class {
|
|
|
565
742
|
const sortedParams = [...parameters].sort((a, b) => a.index - b.index);
|
|
566
743
|
for (const param of sortedParams) {
|
|
567
744
|
const index = param.index;
|
|
745
|
+
const decoratorType = param.name;
|
|
568
746
|
if (index < declaredParams.length) {
|
|
569
747
|
const declaredParam = declaredParams[index];
|
|
570
748
|
const paramName = declaredParam.getName();
|
|
@@ -572,6 +750,7 @@ var RouteAnalyzerService = class {
|
|
|
572
750
|
result.push({
|
|
573
751
|
index,
|
|
574
752
|
name: paramName,
|
|
753
|
+
decoratorType,
|
|
575
754
|
type: paramType,
|
|
576
755
|
required: true,
|
|
577
756
|
data: param.data,
|
|
@@ -582,6 +761,7 @@ var RouteAnalyzerService = class {
|
|
|
582
761
|
result.push({
|
|
583
762
|
index,
|
|
584
763
|
name: `param${index}`,
|
|
764
|
+
decoratorType,
|
|
585
765
|
type: param.metatype?.name || "unknown",
|
|
586
766
|
required: true,
|
|
587
767
|
data: param.data,
|
|
@@ -596,7 +776,6 @@ var RouteAnalyzerService = class {
|
|
|
596
776
|
|
|
597
777
|
// src/services/schema-generator.service.ts
|
|
598
778
|
import { createGenerator } from "ts-json-schema-generator";
|
|
599
|
-
import { Project as Project2 } from "ts-morph";
|
|
600
779
|
|
|
601
780
|
// src/utils/schema-utils.ts
|
|
602
781
|
function mapJsonSchemaTypeToTypeScript(schema) {
|
|
@@ -663,32 +842,6 @@ function extractNamedType(type) {
|
|
|
663
842
|
if (BUILTIN_TYPES.has(name)) return null;
|
|
664
843
|
return name;
|
|
665
844
|
}
|
|
666
|
-
function generateTypeImports(routes) {
|
|
667
|
-
const types = /* @__PURE__ */ new Set();
|
|
668
|
-
for (const route of routes) {
|
|
669
|
-
if (route.parameters) {
|
|
670
|
-
for (const param of route.parameters) {
|
|
671
|
-
if (param.type && !["string", "number", "boolean"].includes(param.type)) {
|
|
672
|
-
const typeMatch = param.type.match(/(\w+)(?:<.*>)?/);
|
|
673
|
-
if (typeMatch) {
|
|
674
|
-
const typeName = typeMatch[1];
|
|
675
|
-
if (!BUILTIN_UTILITY_TYPES.has(typeName)) {
|
|
676
|
-
types.add(typeName);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
if (route.returns) {
|
|
683
|
-
const returnType = route.returns.replace(/Promise<(.+)>/, "$1");
|
|
684
|
-
const baseType = returnType.replace(/\[\]$/, "");
|
|
685
|
-
if (!["string", "number", "boolean", "any", "void", "unknown"].includes(baseType)) {
|
|
686
|
-
types.add(baseType);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
return Array.from(types).join(", ");
|
|
691
|
-
}
|
|
692
845
|
|
|
693
846
|
// src/services/schema-generator.service.ts
|
|
694
847
|
var SchemaGeneratorService = class {
|
|
@@ -696,37 +849,14 @@ var SchemaGeneratorService = class {
|
|
|
696
849
|
this.controllerPattern = controllerPattern;
|
|
697
850
|
this.tsConfigPath = tsConfigPath;
|
|
698
851
|
}
|
|
699
|
-
// Track projects for cleanup
|
|
700
|
-
projects = [];
|
|
701
852
|
/**
|
|
702
853
|
* Generates JSON schemas from types used in controllers
|
|
703
854
|
*/
|
|
704
|
-
async generateSchemas() {
|
|
705
|
-
const project = this.createProject();
|
|
855
|
+
async generateSchemas(project) {
|
|
706
856
|
const sourceFiles = project.getSourceFiles(this.controllerPattern);
|
|
707
857
|
const collectedTypes = this.collectTypesFromControllers(sourceFiles);
|
|
708
858
|
return this.processTypes(collectedTypes);
|
|
709
859
|
}
|
|
710
|
-
/**
|
|
711
|
-
* Creates a new ts-morph project
|
|
712
|
-
*/
|
|
713
|
-
createProject() {
|
|
714
|
-
const project = new Project2({
|
|
715
|
-
tsConfigFilePath: this.tsConfigPath
|
|
716
|
-
});
|
|
717
|
-
project.addSourceFilesAtPaths([this.controllerPattern]);
|
|
718
|
-
this.projects.push(project);
|
|
719
|
-
return project;
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Cleanup resources to prevent memory leaks
|
|
723
|
-
*/
|
|
724
|
-
dispose() {
|
|
725
|
-
this.projects.forEach((project) => {
|
|
726
|
-
project.getSourceFiles().forEach((file) => project.removeSourceFile(file));
|
|
727
|
-
});
|
|
728
|
-
this.projects = [];
|
|
729
|
-
}
|
|
730
860
|
/**
|
|
731
861
|
* Collects types from controller files
|
|
732
862
|
*/
|
|
@@ -808,20 +938,37 @@ var RPCPlugin = class {
|
|
|
808
938
|
routeAnalyzer;
|
|
809
939
|
schemaGenerator;
|
|
810
940
|
clientGenerator;
|
|
941
|
+
openApiGenerator;
|
|
942
|
+
openApiOptions;
|
|
943
|
+
// Shared ts-morph project
|
|
944
|
+
project = null;
|
|
811
945
|
// Internal state
|
|
812
946
|
analyzedRoutes = [];
|
|
813
947
|
analyzedSchemas = [];
|
|
814
948
|
generatedInfo = null;
|
|
815
949
|
constructor(options = {}) {
|
|
816
950
|
this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
|
|
817
|
-
this.tsConfigPath = options.tsConfigPath ??
|
|
818
|
-
this.outputDir = options.outputDir ??
|
|
951
|
+
this.tsConfigPath = options.tsConfigPath ?? path4.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
|
|
952
|
+
this.outputDir = options.outputDir ?? path4.resolve(process.cwd(), DEFAULT_OPTIONS.outputDir);
|
|
819
953
|
this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
|
|
820
|
-
this.routeAnalyzer = new RouteAnalyzerService(
|
|
954
|
+
this.routeAnalyzer = new RouteAnalyzerService();
|
|
821
955
|
this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath);
|
|
822
956
|
this.clientGenerator = new ClientGeneratorService(this.outputDir);
|
|
957
|
+
this.openApiOptions = this.resolveOpenApiOptions(options.openapi);
|
|
958
|
+
this.openApiGenerator = this.openApiOptions ? new OpenApiGeneratorService(this.outputDir) : null;
|
|
823
959
|
this.validateConfiguration();
|
|
824
960
|
}
|
|
961
|
+
resolveOpenApiOptions(input) {
|
|
962
|
+
if (!input) return null;
|
|
963
|
+
const opts = input === true ? {} : input;
|
|
964
|
+
return {
|
|
965
|
+
title: opts.title ?? "API",
|
|
966
|
+
version: opts.version ?? "1.0.0",
|
|
967
|
+
description: opts.description ?? "",
|
|
968
|
+
servers: opts.servers ?? [],
|
|
969
|
+
outputFile: opts.outputFile ?? "openapi.json"
|
|
970
|
+
};
|
|
971
|
+
}
|
|
825
972
|
/**
|
|
826
973
|
* Validates the plugin configuration
|
|
827
974
|
*/
|
|
@@ -833,7 +980,7 @@ var RPCPlugin = class {
|
|
|
833
980
|
if (!this.tsConfigPath?.trim()) {
|
|
834
981
|
errors.push("TypeScript config path cannot be empty");
|
|
835
982
|
} else {
|
|
836
|
-
if (!
|
|
983
|
+
if (!fs3.existsSync(this.tsConfigPath)) {
|
|
837
984
|
errors.push(`TypeScript config file not found at: ${this.tsConfigPath}`);
|
|
838
985
|
}
|
|
839
986
|
}
|
|
@@ -858,15 +1005,37 @@ var RPCPlugin = class {
|
|
|
858
1005
|
/**
|
|
859
1006
|
* Main analysis method that coordinates all three components
|
|
860
1007
|
*/
|
|
861
|
-
async analyzeEverything() {
|
|
1008
|
+
async analyzeEverything(force = false) {
|
|
862
1009
|
try {
|
|
863
1010
|
this.log("Starting comprehensive RPC analysis...");
|
|
864
1011
|
this.analyzedRoutes = [];
|
|
865
1012
|
this.analyzedSchemas = [];
|
|
866
1013
|
this.generatedInfo = null;
|
|
867
|
-
this.
|
|
868
|
-
this.
|
|
1014
|
+
this.dispose();
|
|
1015
|
+
this.project = new Project({ tsConfigFilePath: this.tsConfigPath });
|
|
1016
|
+
this.project.addSourceFilesAtPaths([this.controllerPattern]);
|
|
1017
|
+
const filePaths = this.project.getSourceFiles().map((f) => f.getFilePath());
|
|
1018
|
+
if (!force) {
|
|
1019
|
+
const currentHash = computeHash(filePaths);
|
|
1020
|
+
const stored = readChecksum(this.outputDir);
|
|
1021
|
+
if (stored && stored.hash === currentHash && this.outputFilesExist()) {
|
|
1022
|
+
this.log("Source files unchanged \u2014 skipping regeneration");
|
|
1023
|
+
this.dispose();
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
this.analyzedRoutes = await this.routeAnalyzer.analyzeControllerMethods(this.project);
|
|
1028
|
+
this.analyzedSchemas = await this.schemaGenerator.generateSchemas(this.project);
|
|
869
1029
|
this.generatedInfo = await this.clientGenerator.generateClient(this.analyzedRoutes, this.analyzedSchemas);
|
|
1030
|
+
if (this.openApiGenerator && this.openApiOptions) {
|
|
1031
|
+
const specPath = await this.openApiGenerator.generateSpec(
|
|
1032
|
+
this.analyzedRoutes,
|
|
1033
|
+
this.analyzedSchemas,
|
|
1034
|
+
this.openApiOptions
|
|
1035
|
+
);
|
|
1036
|
+
this.log(`OpenAPI spec generated: ${specPath}`);
|
|
1037
|
+
}
|
|
1038
|
+
await writeChecksum(this.outputDir, { hash: computeHash(filePaths), files: filePaths });
|
|
870
1039
|
this.log(
|
|
871
1040
|
`\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
|
|
872
1041
|
);
|
|
@@ -877,10 +1046,11 @@ var RPCPlugin = class {
|
|
|
877
1046
|
}
|
|
878
1047
|
}
|
|
879
1048
|
/**
|
|
880
|
-
* Manually trigger analysis (useful for testing or re-generation)
|
|
1049
|
+
* Manually trigger analysis (useful for testing or re-generation).
|
|
1050
|
+
* Defaults to force=true to bypass cache; pass false to use caching.
|
|
881
1051
|
*/
|
|
882
|
-
async analyze() {
|
|
883
|
-
await this.analyzeEverything();
|
|
1052
|
+
async analyze(force = true) {
|
|
1053
|
+
await this.analyzeEverything(force);
|
|
884
1054
|
}
|
|
885
1055
|
/**
|
|
886
1056
|
* Get the analyzed routes
|
|
@@ -900,13 +1070,24 @@ var RPCPlugin = class {
|
|
|
900
1070
|
getGenerationInfo() {
|
|
901
1071
|
return this.generatedInfo;
|
|
902
1072
|
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Checks whether expected output files exist on disk
|
|
1075
|
+
*/
|
|
1076
|
+
outputFilesExist() {
|
|
1077
|
+
if (!fs3.existsSync(path4.join(this.outputDir, "client.ts"))) return false;
|
|
1078
|
+
if (this.openApiOptions) {
|
|
1079
|
+
return fs3.existsSync(path4.join(this.outputDir, this.openApiOptions.outputFile));
|
|
1080
|
+
}
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
903
1083
|
/**
|
|
904
1084
|
* Cleanup resources to prevent memory leaks
|
|
905
1085
|
*/
|
|
906
1086
|
dispose() {
|
|
907
|
-
this.
|
|
908
|
-
|
|
909
|
-
|
|
1087
|
+
if (this.project) {
|
|
1088
|
+
this.project.getSourceFiles().forEach((file) => this.project.removeSourceFile(file));
|
|
1089
|
+
this.project = null;
|
|
1090
|
+
}
|
|
910
1091
|
}
|
|
911
1092
|
// ============================================================================
|
|
912
1093
|
// LOGGING UTILITIES
|
|
@@ -931,16 +1112,19 @@ export {
|
|
|
931
1112
|
DEFAULT_OPTIONS,
|
|
932
1113
|
GENERIC_TYPES,
|
|
933
1114
|
LOG_PREFIX,
|
|
1115
|
+
OpenApiGeneratorService,
|
|
934
1116
|
RPCPlugin,
|
|
935
1117
|
RouteAnalyzerService,
|
|
936
1118
|
SchemaGeneratorService,
|
|
937
1119
|
buildFullApiPath,
|
|
938
1120
|
buildFullPath,
|
|
939
1121
|
camelCase,
|
|
1122
|
+
computeHash,
|
|
940
1123
|
extractNamedType,
|
|
941
|
-
generateTypeImports,
|
|
942
1124
|
generateTypeScriptInterface,
|
|
943
1125
|
mapJsonSchemaTypeToTypeScript,
|
|
944
|
-
|
|
1126
|
+
readChecksum,
|
|
1127
|
+
safeToString,
|
|
1128
|
+
writeChecksum
|
|
945
1129
|
};
|
|
946
1130
|
//# sourceMappingURL=index.mjs.map
|