@honestjs/rpc-plugin 1.2.0 → 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 +97 -3
- package/dist/index.d.mts +88 -43
- package/dist/index.d.ts +88 -43
- package/dist/index.js +332 -136
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +324 -131
- 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
|
|
@@ -310,6 +352,14 @@ export class ApiClient {
|
|
|
310
352
|
|
|
311
353
|
try {
|
|
312
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
|
+
|
|
313
363
|
const responseData = await response.json()
|
|
314
364
|
|
|
315
365
|
if (!response.ok) {
|
|
@@ -452,71 +502,216 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
452
502
|
*/
|
|
453
503
|
analyzeRouteParameters(route) {
|
|
454
504
|
const parameters = route.parameters || [];
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
return !!pathSegment && typeof pathSegment === "string" && route.path.includes(`:${pathSegment}`);
|
|
459
|
-
};
|
|
460
|
-
const pathParams = parameters.filter((p) => isInPath(p)).map((p) => ({ ...p, required: true }));
|
|
461
|
-
const rawBody = parameters.filter((p) => !isInPath(p) && method !== "get");
|
|
462
|
-
const bodyParams = rawBody.map((p) => ({
|
|
463
|
-
...p,
|
|
464
|
-
required: true
|
|
465
|
-
}));
|
|
466
|
-
const queryParams = parameters.filter((p) => !isInPath(p) && method === "get").map((p) => ({
|
|
467
|
-
...p,
|
|
468
|
-
required: p.required === true
|
|
469
|
-
// default false if not provided
|
|
470
|
-
}));
|
|
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 }));
|
|
471
508
|
return { pathParams, queryParams, bodyParams };
|
|
472
509
|
}
|
|
473
510
|
};
|
|
474
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
|
+
|
|
475
698
|
// src/services/route-analyzer.service.ts
|
|
476
699
|
var import_honestjs = require("honestjs");
|
|
477
|
-
var import_ts_morph = require("ts-morph");
|
|
478
700
|
var RouteAnalyzerService = class {
|
|
479
|
-
constructor(controllerPattern, tsConfigPath) {
|
|
480
|
-
this.controllerPattern = controllerPattern;
|
|
481
|
-
this.tsConfigPath = tsConfigPath;
|
|
482
|
-
}
|
|
483
|
-
// Track projects for cleanup
|
|
484
|
-
projects = [];
|
|
485
701
|
/**
|
|
486
702
|
* Analyzes controller methods to extract type information
|
|
487
703
|
*/
|
|
488
|
-
async analyzeControllerMethods() {
|
|
704
|
+
async analyzeControllerMethods(project) {
|
|
489
705
|
const routes = import_honestjs.RouteRegistry.getRoutes();
|
|
490
706
|
if (!routes?.length) {
|
|
491
707
|
return [];
|
|
492
708
|
}
|
|
493
|
-
const project = this.createProject();
|
|
494
709
|
const controllers = this.findControllerClasses(project);
|
|
495
710
|
if (controllers.size === 0) {
|
|
496
711
|
return [];
|
|
497
712
|
}
|
|
498
713
|
return this.processRoutes(routes, controllers);
|
|
499
714
|
}
|
|
500
|
-
/**
|
|
501
|
-
* Creates a new ts-morph project
|
|
502
|
-
*/
|
|
503
|
-
createProject() {
|
|
504
|
-
const project = new import_ts_morph.Project({
|
|
505
|
-
tsConfigFilePath: this.tsConfigPath
|
|
506
|
-
});
|
|
507
|
-
project.addSourceFilesAtPaths([this.controllerPattern]);
|
|
508
|
-
this.projects.push(project);
|
|
509
|
-
return project;
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Cleanup resources to prevent memory leaks
|
|
513
|
-
*/
|
|
514
|
-
dispose() {
|
|
515
|
-
this.projects.forEach((project) => {
|
|
516
|
-
project.getSourceFiles().forEach((file) => project.removeSourceFile(file));
|
|
517
|
-
});
|
|
518
|
-
this.projects = [];
|
|
519
|
-
}
|
|
520
715
|
/**
|
|
521
716
|
* Finds controller classes in the project
|
|
522
717
|
*/
|
|
@@ -539,23 +734,17 @@ var RouteAnalyzerService = class {
|
|
|
539
734
|
*/
|
|
540
735
|
processRoutes(routes, controllers) {
|
|
541
736
|
const analyzedRoutes = [];
|
|
542
|
-
const errors = [];
|
|
543
737
|
for (const route of routes) {
|
|
544
738
|
try {
|
|
545
739
|
const extendedRoute = this.createExtendedRoute(route, controllers);
|
|
546
740
|
analyzedRoutes.push(extendedRoute);
|
|
547
741
|
} catch (routeError) {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
console.error(
|
|
551
|
-
`Error processing route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
|
|
742
|
+
console.warn(
|
|
743
|
+
`${LOG_PREFIX} Skipping route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
|
|
552
744
|
routeError
|
|
553
745
|
);
|
|
554
746
|
}
|
|
555
747
|
}
|
|
556
|
-
if (errors.length > 0) {
|
|
557
|
-
throw new Error(`Failed to process ${errors.length} routes: ${errors.map((e) => e.message).join(", ")}`);
|
|
558
|
-
}
|
|
559
748
|
return analyzedRoutes;
|
|
560
749
|
}
|
|
561
750
|
/**
|
|
@@ -608,6 +797,7 @@ var RouteAnalyzerService = class {
|
|
|
608
797
|
const sortedParams = [...parameters].sort((a, b) => a.index - b.index);
|
|
609
798
|
for (const param of sortedParams) {
|
|
610
799
|
const index = param.index;
|
|
800
|
+
const decoratorType = param.name;
|
|
611
801
|
if (index < declaredParams.length) {
|
|
612
802
|
const declaredParam = declaredParams[index];
|
|
613
803
|
const paramName = declaredParam.getName();
|
|
@@ -615,6 +805,7 @@ var RouteAnalyzerService = class {
|
|
|
615
805
|
result.push({
|
|
616
806
|
index,
|
|
617
807
|
name: paramName,
|
|
808
|
+
decoratorType,
|
|
618
809
|
type: paramType,
|
|
619
810
|
required: true,
|
|
620
811
|
data: param.data,
|
|
@@ -625,6 +816,7 @@ var RouteAnalyzerService = class {
|
|
|
625
816
|
result.push({
|
|
626
817
|
index,
|
|
627
818
|
name: `param${index}`,
|
|
819
|
+
decoratorType,
|
|
628
820
|
type: param.metatype?.name || "unknown",
|
|
629
821
|
required: true,
|
|
630
822
|
data: param.data,
|
|
@@ -639,7 +831,6 @@ var RouteAnalyzerService = class {
|
|
|
639
831
|
|
|
640
832
|
// src/services/schema-generator.service.ts
|
|
641
833
|
var import_ts_json_schema_generator = require("ts-json-schema-generator");
|
|
642
|
-
var import_ts_morph2 = require("ts-morph");
|
|
643
834
|
|
|
644
835
|
// src/utils/schema-utils.ts
|
|
645
836
|
function mapJsonSchemaTypeToTypeScript(schema) {
|
|
@@ -706,32 +897,6 @@ function extractNamedType(type) {
|
|
|
706
897
|
if (BUILTIN_TYPES.has(name)) return null;
|
|
707
898
|
return name;
|
|
708
899
|
}
|
|
709
|
-
function generateTypeImports(routes) {
|
|
710
|
-
const types = /* @__PURE__ */ new Set();
|
|
711
|
-
for (const route of routes) {
|
|
712
|
-
if (route.parameters) {
|
|
713
|
-
for (const param of route.parameters) {
|
|
714
|
-
if (param.type && !["string", "number", "boolean"].includes(param.type)) {
|
|
715
|
-
const typeMatch = param.type.match(/(\w+)(?:<.*>)?/);
|
|
716
|
-
if (typeMatch) {
|
|
717
|
-
const typeName = typeMatch[1];
|
|
718
|
-
if (!BUILTIN_UTILITY_TYPES.has(typeName)) {
|
|
719
|
-
types.add(typeName);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
if (route.returns) {
|
|
726
|
-
const returnType = route.returns.replace(/Promise<(.+)>/, "$1");
|
|
727
|
-
const baseType = returnType.replace(/\[\]$/, "");
|
|
728
|
-
if (!["string", "number", "boolean", "any", "void", "unknown"].includes(baseType)) {
|
|
729
|
-
types.add(baseType);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
return Array.from(types).join(", ");
|
|
734
|
-
}
|
|
735
900
|
|
|
736
901
|
// src/services/schema-generator.service.ts
|
|
737
902
|
var SchemaGeneratorService = class {
|
|
@@ -739,37 +904,14 @@ var SchemaGeneratorService = class {
|
|
|
739
904
|
this.controllerPattern = controllerPattern;
|
|
740
905
|
this.tsConfigPath = tsConfigPath;
|
|
741
906
|
}
|
|
742
|
-
// Track projects for cleanup
|
|
743
|
-
projects = [];
|
|
744
907
|
/**
|
|
745
908
|
* Generates JSON schemas from types used in controllers
|
|
746
909
|
*/
|
|
747
|
-
async generateSchemas() {
|
|
748
|
-
const project = this.createProject();
|
|
910
|
+
async generateSchemas(project) {
|
|
749
911
|
const sourceFiles = project.getSourceFiles(this.controllerPattern);
|
|
750
912
|
const collectedTypes = this.collectTypesFromControllers(sourceFiles);
|
|
751
913
|
return this.processTypes(collectedTypes);
|
|
752
914
|
}
|
|
753
|
-
/**
|
|
754
|
-
* Creates a new ts-morph project
|
|
755
|
-
*/
|
|
756
|
-
createProject() {
|
|
757
|
-
const project = new import_ts_morph2.Project({
|
|
758
|
-
tsConfigFilePath: this.tsConfigPath
|
|
759
|
-
});
|
|
760
|
-
project.addSourceFilesAtPaths([this.controllerPattern]);
|
|
761
|
-
this.projects.push(project);
|
|
762
|
-
return project;
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Cleanup resources to prevent memory leaks
|
|
766
|
-
*/
|
|
767
|
-
dispose() {
|
|
768
|
-
this.projects.forEach((project) => {
|
|
769
|
-
project.getSourceFiles().forEach((file) => project.removeSourceFile(file));
|
|
770
|
-
});
|
|
771
|
-
this.projects = [];
|
|
772
|
-
}
|
|
773
915
|
/**
|
|
774
916
|
* Collects types from controller files
|
|
775
917
|
*/
|
|
@@ -851,20 +993,37 @@ var RPCPlugin = class {
|
|
|
851
993
|
routeAnalyzer;
|
|
852
994
|
schemaGenerator;
|
|
853
995
|
clientGenerator;
|
|
996
|
+
openApiGenerator;
|
|
997
|
+
openApiOptions;
|
|
998
|
+
// Shared ts-morph project
|
|
999
|
+
project = null;
|
|
854
1000
|
// Internal state
|
|
855
1001
|
analyzedRoutes = [];
|
|
856
1002
|
analyzedSchemas = [];
|
|
857
1003
|
generatedInfo = null;
|
|
858
1004
|
constructor(options = {}) {
|
|
859
1005
|
this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
|
|
860
|
-
this.tsConfigPath = options.tsConfigPath ??
|
|
861
|
-
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);
|
|
862
1008
|
this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
|
|
863
|
-
this.routeAnalyzer = new RouteAnalyzerService(
|
|
1009
|
+
this.routeAnalyzer = new RouteAnalyzerService();
|
|
864
1010
|
this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath);
|
|
865
1011
|
this.clientGenerator = new ClientGeneratorService(this.outputDir);
|
|
1012
|
+
this.openApiOptions = this.resolveOpenApiOptions(options.openapi);
|
|
1013
|
+
this.openApiGenerator = this.openApiOptions ? new OpenApiGeneratorService(this.outputDir) : null;
|
|
866
1014
|
this.validateConfiguration();
|
|
867
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
|
+
}
|
|
868
1027
|
/**
|
|
869
1028
|
* Validates the plugin configuration
|
|
870
1029
|
*/
|
|
@@ -876,7 +1035,7 @@ var RPCPlugin = class {
|
|
|
876
1035
|
if (!this.tsConfigPath?.trim()) {
|
|
877
1036
|
errors.push("TypeScript config path cannot be empty");
|
|
878
1037
|
} else {
|
|
879
|
-
if (!
|
|
1038
|
+
if (!import_fs2.default.existsSync(this.tsConfigPath)) {
|
|
880
1039
|
errors.push(`TypeScript config file not found at: ${this.tsConfigPath}`);
|
|
881
1040
|
}
|
|
882
1041
|
}
|
|
@@ -901,15 +1060,37 @@ var RPCPlugin = class {
|
|
|
901
1060
|
/**
|
|
902
1061
|
* Main analysis method that coordinates all three components
|
|
903
1062
|
*/
|
|
904
|
-
async analyzeEverything() {
|
|
1063
|
+
async analyzeEverything(force = false) {
|
|
905
1064
|
try {
|
|
906
1065
|
this.log("Starting comprehensive RPC analysis...");
|
|
907
1066
|
this.analyzedRoutes = [];
|
|
908
1067
|
this.analyzedSchemas = [];
|
|
909
1068
|
this.generatedInfo = null;
|
|
910
|
-
this.
|
|
911
|
-
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);
|
|
912
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 });
|
|
913
1094
|
this.log(
|
|
914
1095
|
`\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
|
|
915
1096
|
);
|
|
@@ -920,10 +1101,11 @@ var RPCPlugin = class {
|
|
|
920
1101
|
}
|
|
921
1102
|
}
|
|
922
1103
|
/**
|
|
923
|
-
* 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.
|
|
924
1106
|
*/
|
|
925
|
-
async analyze() {
|
|
926
|
-
await this.analyzeEverything();
|
|
1107
|
+
async analyze(force = true) {
|
|
1108
|
+
await this.analyzeEverything(force);
|
|
927
1109
|
}
|
|
928
1110
|
/**
|
|
929
1111
|
* Get the analyzed routes
|
|
@@ -943,13 +1125,24 @@ var RPCPlugin = class {
|
|
|
943
1125
|
getGenerationInfo() {
|
|
944
1126
|
return this.generatedInfo;
|
|
945
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
|
+
}
|
|
946
1138
|
/**
|
|
947
1139
|
* Cleanup resources to prevent memory leaks
|
|
948
1140
|
*/
|
|
949
1141
|
dispose() {
|
|
950
|
-
this.
|
|
951
|
-
|
|
952
|
-
|
|
1142
|
+
if (this.project) {
|
|
1143
|
+
this.project.getSourceFiles().forEach((file) => this.project.removeSourceFile(file));
|
|
1144
|
+
this.project = null;
|
|
1145
|
+
}
|
|
953
1146
|
}
|
|
954
1147
|
// ============================================================================
|
|
955
1148
|
// LOGGING UTILITIES
|
|
@@ -975,16 +1168,19 @@ var RPCPlugin = class {
|
|
|
975
1168
|
DEFAULT_OPTIONS,
|
|
976
1169
|
GENERIC_TYPES,
|
|
977
1170
|
LOG_PREFIX,
|
|
1171
|
+
OpenApiGeneratorService,
|
|
978
1172
|
RPCPlugin,
|
|
979
1173
|
RouteAnalyzerService,
|
|
980
1174
|
SchemaGeneratorService,
|
|
981
1175
|
buildFullApiPath,
|
|
982
1176
|
buildFullPath,
|
|
983
1177
|
camelCase,
|
|
1178
|
+
computeHash,
|
|
984
1179
|
extractNamedType,
|
|
985
|
-
generateTypeImports,
|
|
986
1180
|
generateTypeScriptInterface,
|
|
987
1181
|
mapJsonSchemaTypeToTypeScript,
|
|
988
|
-
|
|
1182
|
+
readChecksum,
|
|
1183
|
+
safeToString,
|
|
1184
|
+
writeChecksum
|
|
989
1185
|
});
|
|
990
1186
|
//# sourceMappingURL=index.js.map
|