@honestjs/rpc-plugin 1.3.0 → 1.4.1

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.js CHANGED
@@ -36,7 +36,6 @@ __export(index_exports, {
36
36
  DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
37
37
  GENERIC_TYPES: () => GENERIC_TYPES,
38
38
  LOG_PREFIX: () => LOG_PREFIX,
39
- OpenApiGeneratorService: () => OpenApiGeneratorService,
40
39
  RPCPlugin: () => RPCPlugin,
41
40
  RouteAnalyzerService: () => RouteAnalyzerService,
42
41
  SchemaGeneratorService: () => SchemaGeneratorService,
@@ -55,7 +54,7 @@ module.exports = __toCommonJS(index_exports);
55
54
 
56
55
  // src/rpc.plugin.ts
57
56
  var import_fs2 = __toESM(require("fs"));
58
- var import_path4 = __toESM(require("path"));
57
+ var import_path3 = __toESM(require("path"));
59
58
  var import_ts_morph = require("ts-morph");
60
59
 
61
60
  // src/constants/defaults.ts
@@ -63,7 +62,13 @@ var DEFAULT_OPTIONS = {
63
62
  controllerPattern: "src/modules/*/*.controller.ts",
64
63
  tsConfigPath: "tsconfig.json",
65
64
  outputDir: "./generated/rpc",
66
- generateOnInit: true
65
+ generateOnInit: true,
66
+ context: {
67
+ namespace: "rpc",
68
+ keys: {
69
+ artifact: "artifact"
70
+ }
71
+ }
67
72
  };
68
73
  var LOG_PREFIX = "[ RPCPlugin ]";
69
74
  var BUILTIN_UTILITY_TYPES = /* @__PURE__ */ new Set([
@@ -125,22 +130,22 @@ var import_path2 = __toESM(require("path"));
125
130
  // src/utils/path-utils.ts
126
131
  function buildFullPath(basePath, parameters) {
127
132
  if (!basePath || typeof basePath !== "string") return "/";
128
- let path5 = basePath;
133
+ let path4 = basePath;
129
134
  if (parameters && Array.isArray(parameters)) {
130
135
  for (const param of parameters) {
131
136
  if (param.data && typeof param.data === "string" && param.data.startsWith(":")) {
132
137
  const paramName = param.data.slice(1);
133
- path5 = path5.replace(`:${paramName}`, `\${${paramName}}`);
138
+ path4 = path4.replace(`:${paramName}`, `\${${paramName}}`);
134
139
  }
135
140
  }
136
141
  }
137
- return path5;
142
+ return path4;
138
143
  }
139
144
  function buildFullApiPath(route) {
140
145
  const prefix = route.prefix || "";
141
146
  const version = route.version || "";
142
147
  const routePath = route.route || "";
143
- const path5 = route.path || "";
148
+ const path4 = route.path || "";
144
149
  let fullPath = "";
145
150
  if (prefix && prefix !== "/") {
146
151
  fullPath += prefix.replace(/^\/+|\/+$/g, "");
@@ -151,9 +156,9 @@ function buildFullApiPath(route) {
151
156
  if (routePath && routePath !== "/") {
152
157
  fullPath += `/${routePath.replace(/^\/+|\/+$/g, "")}`;
153
158
  }
154
- if (path5 && path5 !== "/") {
155
- fullPath += `/${path5.replace(/^\/+|\/+$/g, "")}`;
156
- } else if (path5 === "/") {
159
+ if (path4 && path4 !== "/") {
160
+ fullPath += `/${path4.replace(/^\/+|\/+$/g, "")}`;
161
+ } else if (path4 === "/") {
157
162
  fullPath += "/";
158
163
  }
159
164
  if (fullPath && !fullPath.startsWith("/")) fullPath = "/" + fullPath;
@@ -509,192 +514,6 @@ ${this.generateControllerMethods(controllerGroups)}
509
514
  }
510
515
  };
511
516
 
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
-
698
517
  // src/services/route-analyzer.service.ts
699
518
  var import_honestjs = require("honestjs");
700
519
  var RouteAnalyzerService = class {
@@ -771,7 +590,7 @@ var RouteAnalyzerService = class {
771
590
  version: route.version,
772
591
  route: route.route,
773
592
  path: route.path,
774
- fullPath: buildFullPath(route.path, route.parameters),
593
+ fullPath: buildFullApiPath(route),
775
594
  parameters,
776
595
  returns
777
596
  };
@@ -989,41 +808,31 @@ var RPCPlugin = class {
989
808
  tsConfigPath;
990
809
  outputDir;
991
810
  generateOnInit;
811
+ contextNamespace;
812
+ contextArtifactKey;
992
813
  // Services
993
814
  routeAnalyzer;
994
815
  schemaGenerator;
995
816
  clientGenerator;
996
- openApiGenerator;
997
- openApiOptions;
998
817
  // Shared ts-morph project
999
818
  project = null;
1000
819
  // Internal state
1001
820
  analyzedRoutes = [];
1002
821
  analyzedSchemas = [];
1003
822
  generatedInfo = null;
823
+ app = null;
1004
824
  constructor(options = {}) {
1005
825
  this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
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);
826
+ this.tsConfigPath = options.tsConfigPath ?? import_path3.default.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
827
+ this.outputDir = options.outputDir ?? import_path3.default.resolve(process.cwd(), DEFAULT_OPTIONS.outputDir);
1008
828
  this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
829
+ this.contextNamespace = options.context?.namespace ?? DEFAULT_OPTIONS.context.namespace;
830
+ this.contextArtifactKey = options.context?.keys?.artifact ?? DEFAULT_OPTIONS.context.keys.artifact;
1009
831
  this.routeAnalyzer = new RouteAnalyzerService();
1010
832
  this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath);
1011
833
  this.clientGenerator = new ClientGeneratorService(this.outputDir);
1012
- this.openApiOptions = this.resolveOpenApiOptions(options.openapi);
1013
- this.openApiGenerator = this.openApiOptions ? new OpenApiGeneratorService(this.outputDir) : null;
1014
834
  this.validateConfiguration();
1015
835
  }
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
- }
1027
836
  /**
1028
837
  * Validates the plugin configuration
1029
838
  */
@@ -1042,6 +851,12 @@ var RPCPlugin = class {
1042
851
  if (!this.outputDir?.trim()) {
1043
852
  errors.push("Output directory cannot be empty");
1044
853
  }
854
+ if (!this.contextNamespace?.trim()) {
855
+ errors.push("Context namespace cannot be empty");
856
+ }
857
+ if (!this.contextArtifactKey?.trim()) {
858
+ errors.push("Context artifact key cannot be empty");
859
+ }
1045
860
  if (errors.length > 0) {
1046
861
  throw new Error(`Configuration validation failed: ${errors.join(", ")}`);
1047
862
  }
@@ -1053,8 +868,10 @@ var RPCPlugin = class {
1053
868
  * Called after all modules are registered
1054
869
  */
1055
870
  afterModulesRegistered = async (app, hono) => {
871
+ this.app = app;
1056
872
  if (this.generateOnInit) {
1057
873
  await this.analyzeEverything();
874
+ this.publishArtifact(app);
1058
875
  }
1059
876
  };
1060
877
  /**
@@ -1063,9 +880,6 @@ var RPCPlugin = class {
1063
880
  async analyzeEverything(force = false) {
1064
881
  try {
1065
882
  this.log("Starting comprehensive RPC analysis...");
1066
- this.analyzedRoutes = [];
1067
- this.analyzedSchemas = [];
1068
- this.generatedInfo = null;
1069
883
  this.dispose();
1070
884
  this.project = new import_ts_morph.Project({ tsConfigFilePath: this.tsConfigPath });
1071
885
  this.project.addSourceFilesAtPaths([this.controllerPattern]);
@@ -1074,23 +888,22 @@ var RPCPlugin = class {
1074
888
  const currentHash = computeHash(filePaths);
1075
889
  const stored = readChecksum(this.outputDir);
1076
890
  if (stored && stored.hash === currentHash && this.outputFilesExist()) {
1077
- this.log("Source files unchanged \u2014 skipping regeneration");
1078
- this.dispose();
1079
- return;
891
+ if (this.loadArtifactFromDisk()) {
892
+ this.log("Source files unchanged \u2014 skipping regeneration");
893
+ this.dispose();
894
+ return;
895
+ }
896
+ this.log("Source files unchanged but cached artifact missing/invalid \u2014 regenerating");
1080
897
  }
1081
898
  }
899
+ this.analyzedRoutes = [];
900
+ this.analyzedSchemas = [];
901
+ this.generatedInfo = null;
1082
902
  this.analyzedRoutes = await this.routeAnalyzer.analyzeControllerMethods(this.project);
1083
903
  this.analyzedSchemas = await this.schemaGenerator.generateSchemas(this.project);
1084
904
  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
905
  await writeChecksum(this.outputDir, { hash: computeHash(filePaths), files: filePaths });
906
+ this.writeArtifactToDisk();
1094
907
  this.log(
1095
908
  `\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
1096
909
  );
@@ -1106,6 +919,9 @@ var RPCPlugin = class {
1106
919
  */
1107
920
  async analyze(force = true) {
1108
921
  await this.analyzeEverything(force);
922
+ if (this.app) {
923
+ this.publishArtifact(this.app);
924
+ }
1109
925
  }
1110
926
  /**
1111
927
  * Get the analyzed routes
@@ -1129,11 +945,42 @@ var RPCPlugin = class {
1129
945
  * Checks whether expected output files exist on disk
1130
946
  */
1131
947
  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));
948
+ return import_fs2.default.existsSync(import_path3.default.join(this.outputDir, "client.ts")) && import_fs2.default.existsSync(import_path3.default.join(this.outputDir, "rpc-artifact.json"));
949
+ }
950
+ getArtifactPath() {
951
+ return import_path3.default.join(this.outputDir, "rpc-artifact.json");
952
+ }
953
+ writeArtifactToDisk() {
954
+ const artifact = {
955
+ routes: this.analyzedRoutes,
956
+ schemas: this.analyzedSchemas
957
+ };
958
+ import_fs2.default.mkdirSync(this.outputDir, { recursive: true });
959
+ import_fs2.default.writeFileSync(this.getArtifactPath(), JSON.stringify(artifact));
960
+ }
961
+ loadArtifactFromDisk() {
962
+ try {
963
+ const raw = import_fs2.default.readFileSync(this.getArtifactPath(), "utf8");
964
+ const parsed = JSON.parse(raw);
965
+ if (!Array.isArray(parsed.routes) || !Array.isArray(parsed.schemas)) {
966
+ return false;
967
+ }
968
+ this.analyzedRoutes = parsed.routes;
969
+ this.analyzedSchemas = parsed.schemas;
970
+ this.generatedInfo = null;
971
+ return true;
972
+ } catch {
973
+ return false;
1135
974
  }
1136
- return true;
975
+ }
976
+ publishArtifact(app) {
977
+ app.getContext().set(this.getArtifactContextKey(), {
978
+ routes: this.analyzedRoutes,
979
+ schemas: this.analyzedSchemas
980
+ });
981
+ }
982
+ getArtifactContextKey() {
983
+ return `${this.contextNamespace}.${this.contextArtifactKey}`;
1137
984
  }
1138
985
  /**
1139
986
  * Cleanup resources to prevent memory leaks
@@ -1168,7 +1015,6 @@ var RPCPlugin = class {
1168
1015
  DEFAULT_OPTIONS,
1169
1016
  GENERIC_TYPES,
1170
1017
  LOG_PREFIX,
1171
- OpenApiGeneratorService,
1172
1018
  RPCPlugin,
1173
1019
  RouteAnalyzerService,
1174
1020
  SchemaGeneratorService,