@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/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/rpc.plugin.ts
2
- import fs2 from "fs";
3
- import path2 from "path";
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 path from "path";
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 path3 = basePath;
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
- path3 = path3.replace(`:${paramName}`, `\${${paramName}}`);
78
+ path5 = path5.replace(`:${paramName}`, `\${${paramName}}`);
41
79
  }
42
80
  }
43
81
  }
44
- return path3;
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 path3 = route.path || "";
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 (path3 && path3 !== "/") {
62
- fullPath += `/${path3.replace(/^\/+|\/+$/g, "")}`;
63
- } else if (path3 === "/") {
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: path.join(this.outputDir, "client.ts"),
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 = path.join(this.outputDir, "client.ts");
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 method = String(route.method || "").toLowerCase();
404
- const isInPath = (p) => {
405
- const pathSegment = p.data;
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
- const error = routeError instanceof Error ? routeError : new Error(String(routeError));
497
- errors.push(error);
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 ?? path2.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
809
- this.outputDir = options.outputDir ?? path2.resolve(process.cwd(), DEFAULT_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(this.controllerPattern, this.tsConfigPath);
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 (!fs2.existsSync(this.tsConfigPath)) {
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.analyzedRoutes = await this.routeAnalyzer.analyzeControllerMethods();
859
- this.analyzedSchemas = await this.schemaGenerator.generateSchemas();
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.routeAnalyzer.dispose();
899
- this.schemaGenerator.dispose();
900
- this.log("Resources cleaned up");
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
- safeToString
1126
+ readChecksum,
1127
+ safeToString,
1128
+ writeChecksum
936
1129
  };
937
1130
  //# sourceMappingURL=index.mjs.map