@honestjs/rpc-plugin 1.3.0 → 1.4.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,6 @@
1
1
  // src/rpc.plugin.ts
2
- import fs3 from "fs";
3
- import path4 from "path";
2
+ import fs2 from "fs";
3
+ import path3 from "path";
4
4
  import { Project } from "ts-morph";
5
5
 
6
6
  // src/constants/defaults.ts
@@ -8,7 +8,13 @@ var DEFAULT_OPTIONS = {
8
8
  controllerPattern: "src/modules/*/*.controller.ts",
9
9
  tsConfigPath: "tsconfig.json",
10
10
  outputDir: "./generated/rpc",
11
- generateOnInit: true
11
+ generateOnInit: true,
12
+ context: {
13
+ namespace: "rpc",
14
+ keys: {
15
+ artifact: "artifact"
16
+ }
17
+ }
12
18
  };
13
19
  var LOG_PREFIX = "[ RPCPlugin ]";
14
20
  var BUILTIN_UTILITY_TYPES = /* @__PURE__ */ new Set([
@@ -70,22 +76,22 @@ import path2 from "path";
70
76
  // src/utils/path-utils.ts
71
77
  function buildFullPath(basePath, parameters) {
72
78
  if (!basePath || typeof basePath !== "string") return "/";
73
- let path5 = basePath;
79
+ let path4 = basePath;
74
80
  if (parameters && Array.isArray(parameters)) {
75
81
  for (const param of parameters) {
76
82
  if (param.data && typeof param.data === "string" && param.data.startsWith(":")) {
77
83
  const paramName = param.data.slice(1);
78
- path5 = path5.replace(`:${paramName}`, `\${${paramName}}`);
84
+ path4 = path4.replace(`:${paramName}`, `\${${paramName}}`);
79
85
  }
80
86
  }
81
87
  }
82
- return path5;
88
+ return path4;
83
89
  }
84
90
  function buildFullApiPath(route) {
85
91
  const prefix = route.prefix || "";
86
92
  const version = route.version || "";
87
93
  const routePath = route.route || "";
88
- const path5 = route.path || "";
94
+ const path4 = route.path || "";
89
95
  let fullPath = "";
90
96
  if (prefix && prefix !== "/") {
91
97
  fullPath += prefix.replace(/^\/+|\/+$/g, "");
@@ -96,9 +102,9 @@ function buildFullApiPath(route) {
96
102
  if (routePath && routePath !== "/") {
97
103
  fullPath += `/${routePath.replace(/^\/+|\/+$/g, "")}`;
98
104
  }
99
- if (path5 && path5 !== "/") {
100
- fullPath += `/${path5.replace(/^\/+|\/+$/g, "")}`;
101
- } else if (path5 === "/") {
105
+ if (path4 && path4 !== "/") {
106
+ fullPath += `/${path4.replace(/^\/+|\/+$/g, "")}`;
107
+ } else if (path4 === "/") {
102
108
  fullPath += "/";
103
109
  }
104
110
  if (fullPath && !fullPath.startsWith("/")) fullPath = "/" + fullPath;
@@ -454,192 +460,6 @@ ${this.generateControllerMethods(controllerGroups)}
454
460
  }
455
461
  };
456
462
 
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
-
643
463
  // src/services/route-analyzer.service.ts
644
464
  import { RouteRegistry } from "honestjs";
645
465
  var RouteAnalyzerService = class {
@@ -934,41 +754,31 @@ var RPCPlugin = class {
934
754
  tsConfigPath;
935
755
  outputDir;
936
756
  generateOnInit;
757
+ contextNamespace;
758
+ contextArtifactKey;
937
759
  // Services
938
760
  routeAnalyzer;
939
761
  schemaGenerator;
940
762
  clientGenerator;
941
- openApiGenerator;
942
- openApiOptions;
943
763
  // Shared ts-morph project
944
764
  project = null;
945
765
  // Internal state
946
766
  analyzedRoutes = [];
947
767
  analyzedSchemas = [];
948
768
  generatedInfo = null;
769
+ app = null;
949
770
  constructor(options = {}) {
950
771
  this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
951
- this.tsConfigPath = options.tsConfigPath ?? path4.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
952
- this.outputDir = options.outputDir ?? path4.resolve(process.cwd(), DEFAULT_OPTIONS.outputDir);
772
+ this.tsConfigPath = options.tsConfigPath ?? path3.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
773
+ this.outputDir = options.outputDir ?? path3.resolve(process.cwd(), DEFAULT_OPTIONS.outputDir);
953
774
  this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
775
+ this.contextNamespace = options.context?.namespace ?? DEFAULT_OPTIONS.context.namespace;
776
+ this.contextArtifactKey = options.context?.keys?.artifact ?? DEFAULT_OPTIONS.context.keys.artifact;
954
777
  this.routeAnalyzer = new RouteAnalyzerService();
955
778
  this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath);
956
779
  this.clientGenerator = new ClientGeneratorService(this.outputDir);
957
- this.openApiOptions = this.resolveOpenApiOptions(options.openapi);
958
- this.openApiGenerator = this.openApiOptions ? new OpenApiGeneratorService(this.outputDir) : null;
959
780
  this.validateConfiguration();
960
781
  }
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
- }
972
782
  /**
973
783
  * Validates the plugin configuration
974
784
  */
@@ -980,13 +790,19 @@ var RPCPlugin = class {
980
790
  if (!this.tsConfigPath?.trim()) {
981
791
  errors.push("TypeScript config path cannot be empty");
982
792
  } else {
983
- if (!fs3.existsSync(this.tsConfigPath)) {
793
+ if (!fs2.existsSync(this.tsConfigPath)) {
984
794
  errors.push(`TypeScript config file not found at: ${this.tsConfigPath}`);
985
795
  }
986
796
  }
987
797
  if (!this.outputDir?.trim()) {
988
798
  errors.push("Output directory cannot be empty");
989
799
  }
800
+ if (!this.contextNamespace?.trim()) {
801
+ errors.push("Context namespace cannot be empty");
802
+ }
803
+ if (!this.contextArtifactKey?.trim()) {
804
+ errors.push("Context artifact key cannot be empty");
805
+ }
990
806
  if (errors.length > 0) {
991
807
  throw new Error(`Configuration validation failed: ${errors.join(", ")}`);
992
808
  }
@@ -998,8 +814,10 @@ var RPCPlugin = class {
998
814
  * Called after all modules are registered
999
815
  */
1000
816
  afterModulesRegistered = async (app, hono) => {
817
+ this.app = app;
1001
818
  if (this.generateOnInit) {
1002
819
  await this.analyzeEverything();
820
+ this.publishArtifact(app);
1003
821
  }
1004
822
  };
1005
823
  /**
@@ -1008,9 +826,6 @@ var RPCPlugin = class {
1008
826
  async analyzeEverything(force = false) {
1009
827
  try {
1010
828
  this.log("Starting comprehensive RPC analysis...");
1011
- this.analyzedRoutes = [];
1012
- this.analyzedSchemas = [];
1013
- this.generatedInfo = null;
1014
829
  this.dispose();
1015
830
  this.project = new Project({ tsConfigFilePath: this.tsConfigPath });
1016
831
  this.project.addSourceFilesAtPaths([this.controllerPattern]);
@@ -1019,23 +834,22 @@ var RPCPlugin = class {
1019
834
  const currentHash = computeHash(filePaths);
1020
835
  const stored = readChecksum(this.outputDir);
1021
836
  if (stored && stored.hash === currentHash && this.outputFilesExist()) {
1022
- this.log("Source files unchanged \u2014 skipping regeneration");
1023
- this.dispose();
1024
- return;
837
+ if (this.loadArtifactFromDisk()) {
838
+ this.log("Source files unchanged \u2014 skipping regeneration");
839
+ this.dispose();
840
+ return;
841
+ }
842
+ this.log("Source files unchanged but cached artifact missing/invalid \u2014 regenerating");
1025
843
  }
1026
844
  }
845
+ this.analyzedRoutes = [];
846
+ this.analyzedSchemas = [];
847
+ this.generatedInfo = null;
1027
848
  this.analyzedRoutes = await this.routeAnalyzer.analyzeControllerMethods(this.project);
1028
849
  this.analyzedSchemas = await this.schemaGenerator.generateSchemas(this.project);
1029
850
  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
851
  await writeChecksum(this.outputDir, { hash: computeHash(filePaths), files: filePaths });
852
+ this.writeArtifactToDisk();
1039
853
  this.log(
1040
854
  `\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
1041
855
  );
@@ -1051,6 +865,9 @@ var RPCPlugin = class {
1051
865
  */
1052
866
  async analyze(force = true) {
1053
867
  await this.analyzeEverything(force);
868
+ if (this.app) {
869
+ this.publishArtifact(this.app);
870
+ }
1054
871
  }
1055
872
  /**
1056
873
  * Get the analyzed routes
@@ -1074,11 +891,42 @@ var RPCPlugin = class {
1074
891
  * Checks whether expected output files exist on disk
1075
892
  */
1076
893
  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));
894
+ return fs2.existsSync(path3.join(this.outputDir, "client.ts")) && fs2.existsSync(path3.join(this.outputDir, "rpc-artifact.json"));
895
+ }
896
+ getArtifactPath() {
897
+ return path3.join(this.outputDir, "rpc-artifact.json");
898
+ }
899
+ writeArtifactToDisk() {
900
+ const artifact = {
901
+ routes: this.analyzedRoutes,
902
+ schemas: this.analyzedSchemas
903
+ };
904
+ fs2.mkdirSync(this.outputDir, { recursive: true });
905
+ fs2.writeFileSync(this.getArtifactPath(), JSON.stringify(artifact));
906
+ }
907
+ loadArtifactFromDisk() {
908
+ try {
909
+ const raw = fs2.readFileSync(this.getArtifactPath(), "utf8");
910
+ const parsed = JSON.parse(raw);
911
+ if (!Array.isArray(parsed.routes) || !Array.isArray(parsed.schemas)) {
912
+ return false;
913
+ }
914
+ this.analyzedRoutes = parsed.routes;
915
+ this.analyzedSchemas = parsed.schemas;
916
+ this.generatedInfo = null;
917
+ return true;
918
+ } catch {
919
+ return false;
1080
920
  }
1081
- return true;
921
+ }
922
+ publishArtifact(app) {
923
+ app.getContext().set(this.getArtifactContextKey(), {
924
+ routes: this.analyzedRoutes,
925
+ schemas: this.analyzedSchemas
926
+ });
927
+ }
928
+ getArtifactContextKey() {
929
+ return `${this.contextNamespace}.${this.contextArtifactKey}`;
1082
930
  }
1083
931
  /**
1084
932
  * Cleanup resources to prevent memory leaks
@@ -1112,7 +960,6 @@ export {
1112
960
  DEFAULT_OPTIONS,
1113
961
  GENERIC_TYPES,
1114
962
  LOG_PREFIX,
1115
- OpenApiGeneratorService,
1116
963
  RPCPlugin,
1117
964
  RouteAnalyzerService,
1118
965
  SchemaGeneratorService,