@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.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
|
/**
|
|
@@ -258,6 +297,14 @@ export class ApiClient {
|
|
|
258
297
|
|
|
259
298
|
try {
|
|
260
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
|
+
|
|
261
308
|
const responseData = await response.json()
|
|
262
309
|
|
|
263
310
|
if (!response.ok) {
|
|
@@ -400,71 +447,216 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
400
447
|
*/
|
|
401
448
|
analyzeRouteParameters(route) {
|
|
402
449
|
const parameters = route.parameters || [];
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
return !!pathSegment && typeof pathSegment === "string" && route.path.includes(`:${pathSegment}`);
|
|
407
|
-
};
|
|
408
|
-
const pathParams = parameters.filter((p) => isInPath(p)).map((p) => ({ ...p, required: true }));
|
|
409
|
-
const rawBody = parameters.filter((p) => !isInPath(p) && method !== "get");
|
|
410
|
-
const bodyParams = rawBody.map((p) => ({
|
|
411
|
-
...p,
|
|
412
|
-
required: true
|
|
413
|
-
}));
|
|
414
|
-
const queryParams = parameters.filter((p) => !isInPath(p) && method === "get").map((p) => ({
|
|
415
|
-
...p,
|
|
416
|
-
required: p.required === true
|
|
417
|
-
// default false if not provided
|
|
418
|
-
}));
|
|
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 }));
|
|
419
453
|
return { pathParams, queryParams, bodyParams };
|
|
420
454
|
}
|
|
421
455
|
};
|
|
422
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
|
+
|
|
423
643
|
// src/services/route-analyzer.service.ts
|
|
424
644
|
import { RouteRegistry } from "honestjs";
|
|
425
|
-
import { Project } from "ts-morph";
|
|
426
645
|
var RouteAnalyzerService = class {
|
|
427
|
-
constructor(controllerPattern, tsConfigPath) {
|
|
428
|
-
this.controllerPattern = controllerPattern;
|
|
429
|
-
this.tsConfigPath = tsConfigPath;
|
|
430
|
-
}
|
|
431
|
-
// Track projects for cleanup
|
|
432
|
-
projects = [];
|
|
433
646
|
/**
|
|
434
647
|
* Analyzes controller methods to extract type information
|
|
435
648
|
*/
|
|
436
|
-
async analyzeControllerMethods() {
|
|
649
|
+
async analyzeControllerMethods(project) {
|
|
437
650
|
const routes = RouteRegistry.getRoutes();
|
|
438
651
|
if (!routes?.length) {
|
|
439
652
|
return [];
|
|
440
653
|
}
|
|
441
|
-
const project = this.createProject();
|
|
442
654
|
const controllers = this.findControllerClasses(project);
|
|
443
655
|
if (controllers.size === 0) {
|
|
444
656
|
return [];
|
|
445
657
|
}
|
|
446
658
|
return this.processRoutes(routes, controllers);
|
|
447
659
|
}
|
|
448
|
-
/**
|
|
449
|
-
* Creates a new ts-morph project
|
|
450
|
-
*/
|
|
451
|
-
createProject() {
|
|
452
|
-
const project = new Project({
|
|
453
|
-
tsConfigFilePath: this.tsConfigPath
|
|
454
|
-
});
|
|
455
|
-
project.addSourceFilesAtPaths([this.controllerPattern]);
|
|
456
|
-
this.projects.push(project);
|
|
457
|
-
return project;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Cleanup resources to prevent memory leaks
|
|
461
|
-
*/
|
|
462
|
-
dispose() {
|
|
463
|
-
this.projects.forEach((project) => {
|
|
464
|
-
project.getSourceFiles().forEach((file) => project.removeSourceFile(file));
|
|
465
|
-
});
|
|
466
|
-
this.projects = [];
|
|
467
|
-
}
|
|
468
660
|
/**
|
|
469
661
|
* Finds controller classes in the project
|
|
470
662
|
*/
|
|
@@ -487,23 +679,17 @@ var RouteAnalyzerService = class {
|
|
|
487
679
|
*/
|
|
488
680
|
processRoutes(routes, controllers) {
|
|
489
681
|
const analyzedRoutes = [];
|
|
490
|
-
const errors = [];
|
|
491
682
|
for (const route of routes) {
|
|
492
683
|
try {
|
|
493
684
|
const extendedRoute = this.createExtendedRoute(route, controllers);
|
|
494
685
|
analyzedRoutes.push(extendedRoute);
|
|
495
686
|
} catch (routeError) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
console.error(
|
|
499
|
-
`Error processing route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
|
|
687
|
+
console.warn(
|
|
688
|
+
`${LOG_PREFIX} Skipping route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
|
|
500
689
|
routeError
|
|
501
690
|
);
|
|
502
691
|
}
|
|
503
692
|
}
|
|
504
|
-
if (errors.length > 0) {
|
|
505
|
-
throw new Error(`Failed to process ${errors.length} routes: ${errors.map((e) => e.message).join(", ")}`);
|
|
506
|
-
}
|
|
507
693
|
return analyzedRoutes;
|
|
508
694
|
}
|
|
509
695
|
/**
|
|
@@ -556,6 +742,7 @@ var RouteAnalyzerService = class {
|
|
|
556
742
|
const sortedParams = [...parameters].sort((a, b) => a.index - b.index);
|
|
557
743
|
for (const param of sortedParams) {
|
|
558
744
|
const index = param.index;
|
|
745
|
+
const decoratorType = param.name;
|
|
559
746
|
if (index < declaredParams.length) {
|
|
560
747
|
const declaredParam = declaredParams[index];
|
|
561
748
|
const paramName = declaredParam.getName();
|
|
@@ -563,6 +750,7 @@ var RouteAnalyzerService = class {
|
|
|
563
750
|
result.push({
|
|
564
751
|
index,
|
|
565
752
|
name: paramName,
|
|
753
|
+
decoratorType,
|
|
566
754
|
type: paramType,
|
|
567
755
|
required: true,
|
|
568
756
|
data: param.data,
|
|
@@ -573,6 +761,7 @@ var RouteAnalyzerService = class {
|
|
|
573
761
|
result.push({
|
|
574
762
|
index,
|
|
575
763
|
name: `param${index}`,
|
|
764
|
+
decoratorType,
|
|
576
765
|
type: param.metatype?.name || "unknown",
|
|
577
766
|
required: true,
|
|
578
767
|
data: param.data,
|
|
@@ -587,7 +776,6 @@ var RouteAnalyzerService = class {
|
|
|
587
776
|
|
|
588
777
|
// src/services/schema-generator.service.ts
|
|
589
778
|
import { createGenerator } from "ts-json-schema-generator";
|
|
590
|
-
import { Project as Project2 } from "ts-morph";
|
|
591
779
|
|
|
592
780
|
// src/utils/schema-utils.ts
|
|
593
781
|
function mapJsonSchemaTypeToTypeScript(schema) {
|
|
@@ -654,32 +842,6 @@ function extractNamedType(type) {
|
|
|
654
842
|
if (BUILTIN_TYPES.has(name)) return null;
|
|
655
843
|
return name;
|
|
656
844
|
}
|
|
657
|
-
function generateTypeImports(routes) {
|
|
658
|
-
const types = /* @__PURE__ */ new Set();
|
|
659
|
-
for (const route of routes) {
|
|
660
|
-
if (route.parameters) {
|
|
661
|
-
for (const param of route.parameters) {
|
|
662
|
-
if (param.type && !["string", "number", "boolean"].includes(param.type)) {
|
|
663
|
-
const typeMatch = param.type.match(/(\w+)(?:<.*>)?/);
|
|
664
|
-
if (typeMatch) {
|
|
665
|
-
const typeName = typeMatch[1];
|
|
666
|
-
if (!BUILTIN_UTILITY_TYPES.has(typeName)) {
|
|
667
|
-
types.add(typeName);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
if (route.returns) {
|
|
674
|
-
const returnType = route.returns.replace(/Promise<(.+)>/, "$1");
|
|
675
|
-
const baseType = returnType.replace(/\[\]$/, "");
|
|
676
|
-
if (!["string", "number", "boolean", "any", "void", "unknown"].includes(baseType)) {
|
|
677
|
-
types.add(baseType);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
return Array.from(types).join(", ");
|
|
682
|
-
}
|
|
683
845
|
|
|
684
846
|
// src/services/schema-generator.service.ts
|
|
685
847
|
var SchemaGeneratorService = class {
|
|
@@ -687,37 +849,14 @@ var SchemaGeneratorService = class {
|
|
|
687
849
|
this.controllerPattern = controllerPattern;
|
|
688
850
|
this.tsConfigPath = tsConfigPath;
|
|
689
851
|
}
|
|
690
|
-
// Track projects for cleanup
|
|
691
|
-
projects = [];
|
|
692
852
|
/**
|
|
693
853
|
* Generates JSON schemas from types used in controllers
|
|
694
854
|
*/
|
|
695
|
-
async generateSchemas() {
|
|
696
|
-
const project = this.createProject();
|
|
855
|
+
async generateSchemas(project) {
|
|
697
856
|
const sourceFiles = project.getSourceFiles(this.controllerPattern);
|
|
698
857
|
const collectedTypes = this.collectTypesFromControllers(sourceFiles);
|
|
699
858
|
return this.processTypes(collectedTypes);
|
|
700
859
|
}
|
|
701
|
-
/**
|
|
702
|
-
* Creates a new ts-morph project
|
|
703
|
-
*/
|
|
704
|
-
createProject() {
|
|
705
|
-
const project = new Project2({
|
|
706
|
-
tsConfigFilePath: this.tsConfigPath
|
|
707
|
-
});
|
|
708
|
-
project.addSourceFilesAtPaths([this.controllerPattern]);
|
|
709
|
-
this.projects.push(project);
|
|
710
|
-
return project;
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Cleanup resources to prevent memory leaks
|
|
714
|
-
*/
|
|
715
|
-
dispose() {
|
|
716
|
-
this.projects.forEach((project) => {
|
|
717
|
-
project.getSourceFiles().forEach((file) => project.removeSourceFile(file));
|
|
718
|
-
});
|
|
719
|
-
this.projects = [];
|
|
720
|
-
}
|
|
721
860
|
/**
|
|
722
861
|
* Collects types from controller files
|
|
723
862
|
*/
|
|
@@ -799,20 +938,37 @@ var RPCPlugin = class {
|
|
|
799
938
|
routeAnalyzer;
|
|
800
939
|
schemaGenerator;
|
|
801
940
|
clientGenerator;
|
|
941
|
+
openApiGenerator;
|
|
942
|
+
openApiOptions;
|
|
943
|
+
// Shared ts-morph project
|
|
944
|
+
project = null;
|
|
802
945
|
// Internal state
|
|
803
946
|
analyzedRoutes = [];
|
|
804
947
|
analyzedSchemas = [];
|
|
805
948
|
generatedInfo = null;
|
|
806
949
|
constructor(options = {}) {
|
|
807
950
|
this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
|
|
808
|
-
this.tsConfigPath = options.tsConfigPath ??
|
|
809
|
-
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);
|
|
810
953
|
this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
|
|
811
|
-
this.routeAnalyzer = new RouteAnalyzerService(
|
|
954
|
+
this.routeAnalyzer = new RouteAnalyzerService();
|
|
812
955
|
this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath);
|
|
813
956
|
this.clientGenerator = new ClientGeneratorService(this.outputDir);
|
|
957
|
+
this.openApiOptions = this.resolveOpenApiOptions(options.openapi);
|
|
958
|
+
this.openApiGenerator = this.openApiOptions ? new OpenApiGeneratorService(this.outputDir) : null;
|
|
814
959
|
this.validateConfiguration();
|
|
815
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
|
+
}
|
|
816
972
|
/**
|
|
817
973
|
* Validates the plugin configuration
|
|
818
974
|
*/
|
|
@@ -824,7 +980,7 @@ var RPCPlugin = class {
|
|
|
824
980
|
if (!this.tsConfigPath?.trim()) {
|
|
825
981
|
errors.push("TypeScript config path cannot be empty");
|
|
826
982
|
} else {
|
|
827
|
-
if (!
|
|
983
|
+
if (!fs3.existsSync(this.tsConfigPath)) {
|
|
828
984
|
errors.push(`TypeScript config file not found at: ${this.tsConfigPath}`);
|
|
829
985
|
}
|
|
830
986
|
}
|
|
@@ -849,15 +1005,37 @@ var RPCPlugin = class {
|
|
|
849
1005
|
/**
|
|
850
1006
|
* Main analysis method that coordinates all three components
|
|
851
1007
|
*/
|
|
852
|
-
async analyzeEverything() {
|
|
1008
|
+
async analyzeEverything(force = false) {
|
|
853
1009
|
try {
|
|
854
1010
|
this.log("Starting comprehensive RPC analysis...");
|
|
855
1011
|
this.analyzedRoutes = [];
|
|
856
1012
|
this.analyzedSchemas = [];
|
|
857
1013
|
this.generatedInfo = null;
|
|
858
|
-
this.
|
|
859
|
-
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);
|
|
860
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 });
|
|
861
1039
|
this.log(
|
|
862
1040
|
`\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
|
|
863
1041
|
);
|
|
@@ -868,10 +1046,11 @@ var RPCPlugin = class {
|
|
|
868
1046
|
}
|
|
869
1047
|
}
|
|
870
1048
|
/**
|
|
871
|
-
* 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.
|
|
872
1051
|
*/
|
|
873
|
-
async analyze() {
|
|
874
|
-
await this.analyzeEverything();
|
|
1052
|
+
async analyze(force = true) {
|
|
1053
|
+
await this.analyzeEverything(force);
|
|
875
1054
|
}
|
|
876
1055
|
/**
|
|
877
1056
|
* Get the analyzed routes
|
|
@@ -891,13 +1070,24 @@ var RPCPlugin = class {
|
|
|
891
1070
|
getGenerationInfo() {
|
|
892
1071
|
return this.generatedInfo;
|
|
893
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
|
+
}
|
|
894
1083
|
/**
|
|
895
1084
|
* Cleanup resources to prevent memory leaks
|
|
896
1085
|
*/
|
|
897
1086
|
dispose() {
|
|
898
|
-
this.
|
|
899
|
-
|
|
900
|
-
|
|
1087
|
+
if (this.project) {
|
|
1088
|
+
this.project.getSourceFiles().forEach((file) => this.project.removeSourceFile(file));
|
|
1089
|
+
this.project = null;
|
|
1090
|
+
}
|
|
901
1091
|
}
|
|
902
1092
|
// ============================================================================
|
|
903
1093
|
// LOGGING UTILITIES
|
|
@@ -922,16 +1112,19 @@ export {
|
|
|
922
1112
|
DEFAULT_OPTIONS,
|
|
923
1113
|
GENERIC_TYPES,
|
|
924
1114
|
LOG_PREFIX,
|
|
1115
|
+
OpenApiGeneratorService,
|
|
925
1116
|
RPCPlugin,
|
|
926
1117
|
RouteAnalyzerService,
|
|
927
1118
|
SchemaGeneratorService,
|
|
928
1119
|
buildFullApiPath,
|
|
929
1120
|
buildFullPath,
|
|
930
1121
|
camelCase,
|
|
1122
|
+
computeHash,
|
|
931
1123
|
extractNamedType,
|
|
932
|
-
generateTypeImports,
|
|
933
1124
|
generateTypeScriptInterface,
|
|
934
1125
|
mapJsonSchemaTypeToTypeScript,
|
|
935
|
-
|
|
1126
|
+
readChecksum,
|
|
1127
|
+
safeToString,
|
|
1128
|
+
writeChecksum
|
|
936
1129
|
};
|
|
937
1130
|
//# sourceMappingURL=index.mjs.map
|