@honestjs/rpc-plugin 1.4.1 → 1.6.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.js CHANGED
@@ -32,19 +32,22 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  BUILTIN_TYPES: () => BUILTIN_TYPES,
34
34
  BUILTIN_UTILITY_TYPES: () => BUILTIN_UTILITY_TYPES,
35
- ClientGeneratorService: () => ClientGeneratorService,
36
35
  DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
37
36
  GENERIC_TYPES: () => GENERIC_TYPES,
38
37
  LOG_PREFIX: () => LOG_PREFIX,
39
38
  RPCPlugin: () => RPCPlugin,
39
+ RPC_ARTIFACT_VERSION: () => RPC_ARTIFACT_VERSION,
40
40
  RouteAnalyzerService: () => RouteAnalyzerService,
41
41
  SchemaGeneratorService: () => SchemaGeneratorService,
42
+ TypeScriptClientGenerator: () => TypeScriptClientGenerator,
43
+ assertRpcArtifact: () => assertRpcArtifact,
42
44
  buildFullApiPath: () => buildFullApiPath,
43
45
  buildFullPath: () => buildFullPath,
44
46
  camelCase: () => camelCase,
45
47
  computeHash: () => computeHash,
46
48
  extractNamedType: () => extractNamedType,
47
49
  generateTypeScriptInterface: () => generateTypeScriptInterface,
50
+ isRpcArtifact: () => isRpcArtifact,
48
51
  mapJsonSchemaTypeToTypeScript: () => mapJsonSchemaTypeToTypeScript,
49
52
  readChecksum: () => readChecksum,
50
53
  safeToString: () => safeToString,
@@ -63,6 +66,9 @@ var DEFAULT_OPTIONS = {
63
66
  tsConfigPath: "tsconfig.json",
64
67
  outputDir: "./generated/rpc",
65
68
  generateOnInit: true,
69
+ mode: "best-effort",
70
+ logLevel: "info",
71
+ artifactVersion: "1",
66
72
  context: {
67
73
  namespace: "rpc",
68
74
  keys: {
@@ -86,47 +92,20 @@ var BUILTIN_UTILITY_TYPES = /* @__PURE__ */ new Set([
86
92
  var BUILTIN_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "any", "void", "unknown"]);
87
93
  var GENERIC_TYPES = /* @__PURE__ */ new Set(["Array", "Promise", "Partial"]);
88
94
 
89
- // src/utils/hash-utils.ts
90
- var import_crypto = require("crypto");
91
- var import_fs = require("fs");
92
- var import_promises = require("fs/promises");
95
+ // src/generators/typescript-client.generator.ts
96
+ var import_promises = __toESM(require("fs/promises"));
93
97
  var import_path = __toESM(require("path"));
94
- var CHECKSUM_FILENAME = ".rpc-checksum";
95
- function computeHash(filePaths) {
96
- const sorted = [...filePaths].sort();
97
- const hasher = (0, import_crypto.createHash)("sha256");
98
- hasher.update(`files:${sorted.length}
99
- `);
100
- for (const filePath of sorted) {
101
- hasher.update((0, import_fs.readFileSync)(filePath, "utf-8"));
102
- hasher.update("\0");
103
- }
104
- return hasher.digest("hex");
105
- }
106
- function readChecksum(outputDir) {
107
- const checksumPath = import_path.default.join(outputDir, CHECKSUM_FILENAME);
108
- if (!(0, import_fs.existsSync)(checksumPath)) return null;
109
- try {
110
- const raw = (0, import_fs.readFileSync)(checksumPath, "utf-8");
111
- const data = JSON.parse(raw);
112
- if (typeof data.hash !== "string" || !Array.isArray(data.files)) {
113
- return null;
114
- }
115
- return data;
116
- } catch {
117
- return null;
118
- }
98
+
99
+ // src/utils/string-utils.ts
100
+ function safeToString(value) {
101
+ if (typeof value === "string") return value;
102
+ if (typeof value === "symbol") return value.description || "Symbol";
103
+ return String(value);
119
104
  }
120
- async function writeChecksum(outputDir, data) {
121
- await (0, import_promises.mkdir)(outputDir, { recursive: true });
122
- const checksumPath = import_path.default.join(outputDir, CHECKSUM_FILENAME);
123
- await (0, import_promises.writeFile)(checksumPath, JSON.stringify(data, null, 2), "utf-8");
105
+ function camelCase(str) {
106
+ return str.charAt(0).toLowerCase() + str.slice(1);
124
107
  }
125
108
 
126
- // src/services/client-generator.service.ts
127
- var import_promises2 = __toESM(require("fs/promises"));
128
- var import_path2 = __toESM(require("path"));
129
-
130
109
  // src/utils/path-utils.ts
131
110
  function buildFullPath(basePath, parameters) {
132
111
  if (!basePath || typeof basePath !== "string") return "/";
@@ -165,46 +144,67 @@ function buildFullApiPath(route) {
165
144
  return fullPath || "/";
166
145
  }
167
146
 
168
- // src/utils/string-utils.ts
169
- function safeToString(value) {
170
- if (typeof value === "string") return value;
171
- if (typeof value === "symbol") return value.description || "Symbol";
172
- return String(value);
147
+ // src/generators/generator-utils.ts
148
+ function groupRoutesByController(routes) {
149
+ const groups = /* @__PURE__ */ new Map();
150
+ for (const route of routes) {
151
+ const controller = safeToString(route.controller);
152
+ if (!groups.has(controller)) {
153
+ groups.set(controller, []);
154
+ }
155
+ groups.get(controller).push(route);
156
+ }
157
+ return groups;
173
158
  }
174
- function camelCase(str) {
175
- return str.charAt(0).toLowerCase() + str.slice(1);
159
+ function buildNormalizedRequestPath(route) {
160
+ let requestPath = buildFullApiPath(route);
161
+ for (const parameter of route.parameters ?? []) {
162
+ if (parameter.decoratorType !== "param") continue;
163
+ const placeholder = `:${String(parameter.data ?? parameter.name)}`;
164
+ requestPath = requestPath.replace(placeholder, `:${parameter.name}`);
165
+ }
166
+ return requestPath;
176
167
  }
177
168
 
178
- // src/services/client-generator.service.ts
179
- var ClientGeneratorService = class {
169
+ // src/generators/typescript-client.generator.ts
170
+ var TypeScriptClientGenerator = class {
180
171
  constructor(outputDir) {
181
172
  this.outputDir = outputDir;
182
173
  }
174
+ name = "typescript-client";
175
+ /**
176
+ * Generates the TypeScript RPC client.
177
+ */
178
+ async generate(context) {
179
+ return this.generateClient(context.routes, context.schemas);
180
+ }
183
181
  /**
184
- * Generates the TypeScript RPC client
182
+ * Generates the TypeScript RPC client.
185
183
  */
186
184
  async generateClient(routes, schemas) {
187
- await import_promises2.default.mkdir(this.outputDir, { recursive: true });
185
+ await import_promises.default.mkdir(this.outputDir, { recursive: true });
188
186
  await this.generateClientFile(routes, schemas);
189
187
  const generatedInfo = {
190
- clientFile: import_path2.default.join(this.outputDir, "client.ts"),
188
+ generator: this.name,
189
+ clientFile: import_path.default.join(this.outputDir, "client.ts"),
190
+ outputFiles: [import_path.default.join(this.outputDir, "client.ts")],
191
191
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
192
192
  };
193
193
  return generatedInfo;
194
194
  }
195
195
  /**
196
- * Generates the main client file with types included
196
+ * Generates the main client file with types included.
197
197
  */
198
198
  async generateClientFile(routes, schemas) {
199
199
  const clientContent = this.generateClientContent(routes, schemas);
200
- const clientPath = import_path2.default.join(this.outputDir, "client.ts");
201
- await import_promises2.default.writeFile(clientPath, clientContent, "utf-8");
200
+ const clientPath = import_path.default.join(this.outputDir, "client.ts");
201
+ await import_promises.default.writeFile(clientPath, clientContent, "utf-8");
202
202
  }
203
203
  /**
204
- * Generates the client TypeScript content with types included
204
+ * Generates the client TypeScript content with types included.
205
205
  */
206
206
  generateClientContent(routes, schemas) {
207
- const controllerGroups = this.groupRoutesByController(routes);
207
+ const controllerGroups = groupRoutesByController(routes);
208
208
  return `// ============================================================================
209
209
  // TYPES SECTION
210
210
  // ============================================================================
@@ -365,13 +365,21 @@ export class ApiClient {
365
365
  return undefined as T
366
366
  }
367
367
 
368
- const responseData = await response.json()
368
+ const contentType = response.headers.get('content-type') || ''
369
+ const isJson = contentType.includes('application/json') || contentType.includes('+json')
370
+ const responseData = isJson ? await response.json() : await response.text()
369
371
 
370
372
  if (!response.ok) {
371
- throw new ApiError(response.status, responseData.message || 'Request failed')
373
+ const message =
374
+ typeof responseData === 'object' && responseData && 'message' in (responseData as Record<string, unknown>)
375
+ ? String((responseData as Record<string, unknown>).message)
376
+ : typeof responseData === 'string' && responseData.trim()
377
+ ? responseData
378
+ : 'Request failed'
379
+ throw new ApiError(response.status, message)
372
380
  }
373
381
 
374
- return responseData
382
+ return responseData as T
375
383
  } catch (error) {
376
384
  if (error instanceof ApiError) {
377
385
  throw error
@@ -385,7 +393,7 @@ ${this.generateControllerMethods(controllerGroups)}
385
393
  `;
386
394
  }
387
395
  /**
388
- * Generates controller methods for the client
396
+ * Generates controller methods for the client.
389
397
  */
390
398
  generateControllerMethods(controllerGroups) {
391
399
  let methods = "";
@@ -440,14 +448,7 @@ ${this.generateControllerMethods(controllerGroups)}
440
448
  methods += "undefined";
441
449
  methods += `>) => {
442
450
  `;
443
- let requestPath = buildFullApiPath(route);
444
- if (pathParams.length > 0) {
445
- for (const pathParam of pathParams) {
446
- const paramName = pathParam.name;
447
- const placeholder = `:${String(pathParam.data)}`;
448
- requestPath = requestPath.replace(placeholder, `:${paramName}`);
449
- }
450
- }
451
+ const requestPath = buildNormalizedRequestPath(route);
451
452
  methods += ` return this.request<Result>('${httpMethod.toUpperCase()}', \`${requestPath}\`, options)
452
453
  `;
453
454
  methods += ` },
@@ -461,7 +462,7 @@ ${this.generateControllerMethods(controllerGroups)}
461
462
  return methods;
462
463
  }
463
464
  /**
464
- * Extracts the proper return type from route analysis
465
+ * Extracts the proper return type from route analysis.
465
466
  */
466
467
  extractReturnType(returns) {
467
468
  if (!returns) return "any";
@@ -472,7 +473,7 @@ ${this.generateControllerMethods(controllerGroups)}
472
473
  return returns;
473
474
  }
474
475
  /**
475
- * Generates schema types from integrated schema generation
476
+ * Generates schema types from integrated schema generation.
476
477
  */
477
478
  generateSchemaTypes(schemas) {
478
479
  if (schemas.length === 0) {
@@ -489,21 +490,7 @@ ${this.generateControllerMethods(controllerGroups)}
489
490
  return content;
490
491
  }
491
492
  /**
492
- * Groups routes by controller for better organization
493
- */
494
- groupRoutesByController(routes) {
495
- const groups = /* @__PURE__ */ new Map();
496
- for (const route of routes) {
497
- const controller = safeToString(route.controller);
498
- if (!groups.has(controller)) {
499
- groups.set(controller, []);
500
- }
501
- groups.get(controller).push(route);
502
- }
503
- return groups;
504
- }
505
- /**
506
- * Analyzes route parameters to determine their types and usage
493
+ * Analyzes route parameters to determine their types and usage.
507
494
  */
508
495
  analyzeRouteParameters(route) {
509
496
  const parameters = route.parameters || [];
@@ -514,13 +501,79 @@ ${this.generateControllerMethods(controllerGroups)}
514
501
  }
515
502
  };
516
503
 
504
+ // src/utils/hash-utils.ts
505
+ var import_crypto = require("crypto");
506
+ var import_fs = require("fs");
507
+ var import_promises2 = require("fs/promises");
508
+ var import_path2 = __toESM(require("path"));
509
+ var CHECKSUM_FILENAME = ".rpc-checksum";
510
+ function computeHash(filePaths) {
511
+ const sorted = [...filePaths].sort();
512
+ const hasher = (0, import_crypto.createHash)("sha256");
513
+ hasher.update(`files:${sorted.length}
514
+ `);
515
+ for (const filePath of sorted) {
516
+ hasher.update((0, import_fs.readFileSync)(filePath, "utf-8"));
517
+ hasher.update("\0");
518
+ }
519
+ return hasher.digest("hex");
520
+ }
521
+ function readChecksum(outputDir) {
522
+ const checksumPath = import_path2.default.join(outputDir, CHECKSUM_FILENAME);
523
+ if (!(0, import_fs.existsSync)(checksumPath)) return null;
524
+ try {
525
+ const raw = (0, import_fs.readFileSync)(checksumPath, "utf-8");
526
+ const data = JSON.parse(raw);
527
+ if (typeof data.hash !== "string" || !Array.isArray(data.files)) {
528
+ return null;
529
+ }
530
+ return data;
531
+ } catch {
532
+ return null;
533
+ }
534
+ }
535
+ async function writeChecksum(outputDir, data) {
536
+ await (0, import_promises2.mkdir)(outputDir, { recursive: true });
537
+ const checksumPath = import_path2.default.join(outputDir, CHECKSUM_FILENAME);
538
+ await (0, import_promises2.writeFile)(checksumPath, JSON.stringify(data, null, 2), "utf-8");
539
+ }
540
+
541
+ // src/utils/artifact-contract.ts
542
+ var RPC_ARTIFACT_VERSION = "1";
543
+ function isRpcArtifact(value) {
544
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
545
+ const obj = value;
546
+ return typeof obj.artifactVersion === "string" && Array.isArray(obj.routes) && Array.isArray(obj.schemas);
547
+ }
548
+ function assertRpcArtifact(value) {
549
+ if (!isRpcArtifact(value)) {
550
+ throw new Error("Invalid RPC artifact: expected { artifactVersion, routes, schemas }");
551
+ }
552
+ if (value.artifactVersion !== RPC_ARTIFACT_VERSION) {
553
+ throw new Error(
554
+ `Unsupported RPC artifact version '${value.artifactVersion}'. Supported: ${RPC_ARTIFACT_VERSION}.`
555
+ );
556
+ }
557
+ }
558
+
517
559
  // src/services/route-analyzer.service.ts
518
560
  var import_honestjs = require("honestjs");
519
561
  var RouteAnalyzerService = class {
562
+ customClassMatcher;
563
+ onWarn;
564
+ warnings = [];
565
+ constructor(options = {}) {
566
+ this.customClassMatcher = options.customClassMatcher;
567
+ this.onWarn = options.onWarn;
568
+ }
569
+ getWarnings() {
570
+ return this.warnings;
571
+ }
520
572
  /**
521
573
  * Analyzes controller methods to extract type information
522
574
  */
523
575
  async analyzeControllerMethods(project) {
576
+ this.warnings = [];
524
577
  const routes = import_honestjs.RouteRegistry.getRoutes();
525
578
  if (!routes?.length) {
526
579
  return [];
@@ -541,13 +594,20 @@ var RouteAnalyzerService = class {
541
594
  const classes = sourceFile.getClasses();
542
595
  for (const classDeclaration of classes) {
543
596
  const className = classDeclaration.getName();
544
- if (className?.endsWith("Controller")) {
597
+ if (className && this.isControllerClass(classDeclaration, className)) {
545
598
  controllers.set(className, classDeclaration);
546
599
  }
547
600
  }
548
601
  }
549
602
  return controllers;
550
603
  }
604
+ isControllerClass(classDeclaration, _className) {
605
+ if (this.customClassMatcher) {
606
+ return this.customClassMatcher(classDeclaration);
607
+ }
608
+ const decoratorNames = classDeclaration.getDecorators().map((decorator) => decorator.getName());
609
+ return decoratorNames.includes("Controller") || decoratorNames.includes("View");
610
+ }
551
611
  /**
552
612
  * Processes all routes and extracts type information
553
613
  */
@@ -558,10 +618,9 @@ var RouteAnalyzerService = class {
558
618
  const extendedRoute = this.createExtendedRoute(route, controllers);
559
619
  analyzedRoutes.push(extendedRoute);
560
620
  } catch (routeError) {
561
- console.warn(
562
- `${LOG_PREFIX} Skipping route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
563
- routeError
564
- );
621
+ const warning = `Skipping route ${safeToString(route.controller)}.${safeToString(route.handler)}`;
622
+ this.warnings.push(warning);
623
+ this.onWarn?.(warning, routeError);
565
624
  }
566
625
  }
567
626
  return analyzedRoutes;
@@ -581,6 +640,10 @@ var RouteAnalyzerService = class {
581
640
  returns = this.getReturnType(handlerMethod);
582
641
  parameters = this.getParametersWithTypes(handlerMethod, route.parameters || []);
583
642
  }
643
+ } else {
644
+ const warning = `Controller class not found in source files: ${controllerName} (handler: ${handlerName})`;
645
+ this.warnings.push(warning);
646
+ this.onWarn?.(warning);
584
647
  }
585
648
  return {
586
649
  controller: controllerName,
@@ -719,14 +782,23 @@ function extractNamedType(type) {
719
782
 
720
783
  // src/services/schema-generator.service.ts
721
784
  var SchemaGeneratorService = class {
722
- constructor(controllerPattern, tsConfigPath) {
785
+ constructor(controllerPattern, tsConfigPath, options = {}) {
723
786
  this.controllerPattern = controllerPattern;
724
787
  this.tsConfigPath = tsConfigPath;
788
+ this.failOnSchemaError = options.failOnSchemaError ?? false;
789
+ this.onWarn = options.onWarn;
790
+ }
791
+ failOnSchemaError;
792
+ onWarn;
793
+ warnings = [];
794
+ getWarnings() {
795
+ return this.warnings;
725
796
  }
726
797
  /**
727
798
  * Generates JSON schemas from types used in controllers
728
799
  */
729
800
  async generateSchemas(project) {
801
+ this.warnings = [];
730
802
  const sourceFiles = project.getSourceFiles(this.controllerPattern);
731
803
  const collectedTypes = this.collectTypesFromControllers(sourceFiles);
732
804
  return this.processTypes(collectedTypes);
@@ -773,7 +845,12 @@ var SchemaGeneratorService = class {
773
845
  typescriptType
774
846
  });
775
847
  } catch (err) {
776
- console.error(`Failed to generate schema for ${typeName}:`, err);
848
+ if (this.failOnSchemaError) {
849
+ throw err;
850
+ }
851
+ const warning = `Failed to generate schema for ${typeName}`;
852
+ this.warnings.push(warning);
853
+ this.onWarn?.(warning, err);
777
854
  }
778
855
  }
779
856
  return schemas;
@@ -792,7 +869,12 @@ var SchemaGeneratorService = class {
792
869
  });
793
870
  return generator.createSchema(typeName);
794
871
  } catch (error) {
795
- console.error(`Failed to generate schema for type ${typeName}:`, error);
872
+ if (this.failOnSchemaError) {
873
+ throw error;
874
+ }
875
+ const warning = `Failed to generate schema for type ${typeName}`;
876
+ this.warnings.push(warning);
877
+ this.onWarn?.(warning, error);
796
878
  return {
797
879
  type: "object",
798
880
  properties: {},
@@ -810,27 +892,44 @@ var RPCPlugin = class {
810
892
  generateOnInit;
811
893
  contextNamespace;
812
894
  contextArtifactKey;
895
+ mode;
896
+ logLevel;
897
+ failOnSchemaError;
898
+ failOnRouteAnalysisWarning;
899
+ customClassMatcher;
813
900
  // Services
814
901
  routeAnalyzer;
815
902
  schemaGenerator;
816
- clientGenerator;
903
+ generators;
817
904
  // Shared ts-morph project
818
905
  project = null;
819
906
  // Internal state
820
907
  analyzedRoutes = [];
821
908
  analyzedSchemas = [];
822
- generatedInfo = null;
909
+ generatedInfos = [];
910
+ diagnostics = null;
823
911
  app = null;
824
912
  constructor(options = {}) {
825
913
  this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
826
914
  this.tsConfigPath = options.tsConfigPath ?? import_path3.default.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
827
915
  this.outputDir = options.outputDir ?? import_path3.default.resolve(process.cwd(), DEFAULT_OPTIONS.outputDir);
828
916
  this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
917
+ this.mode = options.mode ?? DEFAULT_OPTIONS.mode;
918
+ this.logLevel = options.logLevel ?? DEFAULT_OPTIONS.logLevel;
829
919
  this.contextNamespace = options.context?.namespace ?? DEFAULT_OPTIONS.context.namespace;
830
920
  this.contextArtifactKey = options.context?.keys?.artifact ?? DEFAULT_OPTIONS.context.keys.artifact;
831
- this.routeAnalyzer = new RouteAnalyzerService();
832
- this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath);
833
- this.clientGenerator = new ClientGeneratorService(this.outputDir);
921
+ this.customClassMatcher = options.customClassMatcher;
922
+ this.failOnSchemaError = options.failOnSchemaError ?? this.mode === "strict";
923
+ this.failOnRouteAnalysisWarning = options.failOnRouteAnalysisWarning ?? this.mode === "strict";
924
+ this.routeAnalyzer = new RouteAnalyzerService({
925
+ customClassMatcher: this.customClassMatcher,
926
+ onWarn: (message, details) => this.logWarn(message, details)
927
+ });
928
+ this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath, {
929
+ failOnSchemaError: this.failOnSchemaError,
930
+ onWarn: (message, details) => this.logWarn(message, details)
931
+ });
932
+ this.generators = options.generators ?? [new TypeScriptClientGenerator(this.outputDir)];
834
933
  this.validateConfiguration();
835
934
  }
836
935
  /**
@@ -851,17 +950,31 @@ var RPCPlugin = class {
851
950
  if (!this.outputDir?.trim()) {
852
951
  errors.push("Output directory cannot be empty");
853
952
  }
953
+ if (!["strict", "best-effort"].includes(this.mode)) {
954
+ errors.push('Mode must be "strict" or "best-effort"');
955
+ }
956
+ if (!["silent", "error", "warn", "info", "debug"].includes(this.logLevel)) {
957
+ errors.push("logLevel must be one of: silent, error, warn, info, debug");
958
+ }
854
959
  if (!this.contextNamespace?.trim()) {
855
960
  errors.push("Context namespace cannot be empty");
856
961
  }
857
962
  if (!this.contextArtifactKey?.trim()) {
858
963
  errors.push("Context artifact key cannot be empty");
859
964
  }
965
+ for (const generator of this.generators) {
966
+ if (!generator.name?.trim()) {
967
+ errors.push("Generator name cannot be empty");
968
+ }
969
+ if (typeof generator.generate !== "function") {
970
+ errors.push(`Generator "${generator.name || "unknown"}" must implement generate(context)`);
971
+ }
972
+ }
860
973
  if (errors.length > 0) {
861
974
  throw new Error(`Configuration validation failed: ${errors.join(", ")}`);
862
975
  }
863
976
  this.log(
864
- `Configuration validated: controllerPattern=${this.controllerPattern}, tsConfigPath=${this.tsConfigPath}, outputDir=${this.outputDir}`
977
+ `Configuration validated: controllerPattern=${this.controllerPattern}, tsConfigPath=${this.tsConfigPath}, outputDir=${this.outputDir}, mode=${this.mode}`
865
978
  );
866
979
  }
867
980
  /**
@@ -870,14 +983,17 @@ var RPCPlugin = class {
870
983
  afterModulesRegistered = async (app, hono) => {
871
984
  this.app = app;
872
985
  if (this.generateOnInit) {
873
- await this.analyzeEverything();
986
+ await this.analyzeEverything({ force: false, dryRun: false });
874
987
  this.publishArtifact(app);
875
988
  }
876
989
  };
877
990
  /**
878
991
  * Main analysis method that coordinates all three components
879
992
  */
880
- async analyzeEverything(force = false) {
993
+ async analyzeEverything(options) {
994
+ const { force, dryRun } = options;
995
+ const warnings = [];
996
+ let cacheState = force ? "bypass" : "miss";
881
997
  try {
882
998
  this.log("Starting comprehensive RPC analysis...");
883
999
  this.dispose();
@@ -889,21 +1005,50 @@ var RPCPlugin = class {
889
1005
  const stored = readChecksum(this.outputDir);
890
1006
  if (stored && stored.hash === currentHash && this.outputFilesExist()) {
891
1007
  if (this.loadArtifactFromDisk()) {
892
- this.log("Source files unchanged \u2014 skipping regeneration");
1008
+ cacheState = "hit";
1009
+ this.logDebug("Source files unchanged - skipping regeneration");
1010
+ this.diagnostics = {
1011
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1012
+ mode: this.mode,
1013
+ dryRun,
1014
+ cache: cacheState,
1015
+ routesCount: this.analyzedRoutes.length,
1016
+ schemasCount: this.analyzedSchemas.length,
1017
+ warnings: []
1018
+ };
893
1019
  this.dispose();
894
1020
  return;
895
1021
  }
896
- this.log("Source files unchanged but cached artifact missing/invalid \u2014 regenerating");
1022
+ this.logDebug("Source files unchanged but cached artifact missing/invalid - regenerating");
897
1023
  }
898
1024
  }
899
1025
  this.analyzedRoutes = [];
900
1026
  this.analyzedSchemas = [];
901
- this.generatedInfo = null;
1027
+ this.generatedInfos = [];
902
1028
  this.analyzedRoutes = await this.routeAnalyzer.analyzeControllerMethods(this.project);
1029
+ warnings.push(...this.routeAnalyzer.getWarnings());
903
1030
  this.analyzedSchemas = await this.schemaGenerator.generateSchemas(this.project);
904
- this.generatedInfo = await this.clientGenerator.generateClient(this.analyzedRoutes, this.analyzedSchemas);
905
- await writeChecksum(this.outputDir, { hash: computeHash(filePaths), files: filePaths });
906
- this.writeArtifactToDisk();
1031
+ warnings.push(...this.schemaGenerator.getWarnings());
1032
+ if (this.failOnRouteAnalysisWarning && this.routeAnalyzer.getWarnings().length > 0) {
1033
+ throw new Error(`Route analysis warnings encountered in strict mode: ${this.routeAnalyzer.getWarnings().join("; ")}`);
1034
+ }
1035
+ if (!dryRun) {
1036
+ this.generatedInfos = await this.runGenerators();
1037
+ }
1038
+ if (!dryRun) {
1039
+ await writeChecksum(this.outputDir, { hash: computeHash(filePaths), files: filePaths });
1040
+ this.writeArtifactToDisk();
1041
+ }
1042
+ this.diagnostics = {
1043
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1044
+ mode: this.mode,
1045
+ dryRun,
1046
+ cache: cacheState,
1047
+ routesCount: this.analyzedRoutes.length,
1048
+ schemasCount: this.analyzedSchemas.length,
1049
+ warnings
1050
+ };
1051
+ this.writeDiagnosticsToDisk();
907
1052
  this.log(
908
1053
  `\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
909
1054
  );
@@ -913,13 +1058,10 @@ var RPCPlugin = class {
913
1058
  throw error;
914
1059
  }
915
1060
  }
916
- /**
917
- * Manually trigger analysis (useful for testing or re-generation).
918
- * Defaults to force=true to bypass cache; pass false to use caching.
919
- */
920
- async analyze(force = true) {
921
- await this.analyzeEverything(force);
922
- if (this.app) {
1061
+ async analyze(forceOrOptions = true) {
1062
+ const options = typeof forceOrOptions === "boolean" ? { force: forceOrOptions, dryRun: false } : { force: forceOrOptions.force ?? true, dryRun: forceOrOptions.dryRun ?? false };
1063
+ await this.analyzeEverything(options);
1064
+ if (this.app && !options.dryRun) {
923
1065
  this.publishArtifact(this.app);
924
1066
  }
925
1067
  }
@@ -939,42 +1081,89 @@ var RPCPlugin = class {
939
1081
  * Get the generation info
940
1082
  */
941
1083
  getGenerationInfo() {
942
- return this.generatedInfo;
1084
+ return this.generatedInfos[0] ?? null;
1085
+ }
1086
+ /**
1087
+ * Get all generation infos
1088
+ */
1089
+ getGenerationInfos() {
1090
+ return this.generatedInfos;
1091
+ }
1092
+ getDiagnostics() {
1093
+ return this.diagnostics;
943
1094
  }
944
1095
  /**
945
1096
  * Checks whether expected output files exist on disk
946
1097
  */
947
1098
  outputFilesExist() {
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"));
1099
+ if (!import_fs2.default.existsSync(import_path3.default.join(this.outputDir, "rpc-artifact.json"))) {
1100
+ return false;
1101
+ }
1102
+ if (!this.hasTypeScriptGenerator()) {
1103
+ return true;
1104
+ }
1105
+ return import_fs2.default.existsSync(import_path3.default.join(this.outputDir, "client.ts"));
949
1106
  }
950
1107
  getArtifactPath() {
951
1108
  return import_path3.default.join(this.outputDir, "rpc-artifact.json");
952
1109
  }
1110
+ getDiagnosticsPath() {
1111
+ return import_path3.default.join(this.outputDir, "rpc-diagnostics.json");
1112
+ }
953
1113
  writeArtifactToDisk() {
954
1114
  const artifact = {
1115
+ artifactVersion: RPC_ARTIFACT_VERSION,
955
1116
  routes: this.analyzedRoutes,
956
1117
  schemas: this.analyzedSchemas
957
1118
  };
958
1119
  import_fs2.default.mkdirSync(this.outputDir, { recursive: true });
959
1120
  import_fs2.default.writeFileSync(this.getArtifactPath(), JSON.stringify(artifact));
960
1121
  }
1122
+ writeDiagnosticsToDisk() {
1123
+ if (!this.diagnostics) return;
1124
+ import_fs2.default.mkdirSync(this.outputDir, { recursive: true });
1125
+ import_fs2.default.writeFileSync(this.getDiagnosticsPath(), JSON.stringify(this.diagnostics, null, 2));
1126
+ }
961
1127
  loadArtifactFromDisk() {
962
1128
  try {
963
1129
  const raw = import_fs2.default.readFileSync(this.getArtifactPath(), "utf8");
964
1130
  const parsed = JSON.parse(raw);
965
- if (!Array.isArray(parsed.routes) || !Array.isArray(parsed.schemas)) {
966
- return false;
1131
+ if (parsed.artifactVersion === void 0) {
1132
+ if (!Array.isArray(parsed.routes) || !Array.isArray(parsed.schemas)) {
1133
+ return false;
1134
+ }
1135
+ this.analyzedRoutes = parsed.routes;
1136
+ this.analyzedSchemas = parsed.schemas;
1137
+ } else {
1138
+ assertRpcArtifact(parsed);
1139
+ this.analyzedRoutes = parsed.routes;
1140
+ this.analyzedSchemas = parsed.schemas;
967
1141
  }
968
- this.analyzedRoutes = parsed.routes;
969
- this.analyzedSchemas = parsed.schemas;
970
- this.generatedInfo = null;
1142
+ this.generatedInfos = [];
971
1143
  return true;
972
1144
  } catch {
973
1145
  return false;
974
1146
  }
975
1147
  }
1148
+ async runGenerators() {
1149
+ const results = [];
1150
+ for (const generator of this.generators) {
1151
+ this.log(`Running generator: ${generator.name}`);
1152
+ const result = await generator.generate({
1153
+ outputDir: this.outputDir,
1154
+ routes: this.analyzedRoutes,
1155
+ schemas: this.analyzedSchemas
1156
+ });
1157
+ results.push(result);
1158
+ }
1159
+ return results;
1160
+ }
1161
+ hasTypeScriptGenerator() {
1162
+ return this.generators.some((generator) => generator.name === "typescript-client");
1163
+ }
976
1164
  publishArtifact(app) {
977
1165
  app.getContext().set(this.getArtifactContextKey(), {
1166
+ artifactVersion: RPC_ARTIFACT_VERSION,
978
1167
  routes: this.analyzedRoutes,
979
1168
  schemas: this.analyzedSchemas
980
1169
  });
@@ -998,32 +1187,59 @@ var RPCPlugin = class {
998
1187
  * Logs a message with the plugin prefix
999
1188
  */
1000
1189
  log(message) {
1001
- console.log(`${LOG_PREFIX} ${message}`);
1190
+ if (this.canLog("info")) {
1191
+ console.log(`${LOG_PREFIX} ${message}`);
1192
+ }
1002
1193
  }
1003
1194
  /**
1004
1195
  * Logs an error with the plugin prefix
1005
1196
  */
1006
1197
  logError(message, error) {
1007
- console.error(`${LOG_PREFIX} ${message}`, error || "");
1198
+ if (this.canLog("error")) {
1199
+ console.error(`${LOG_PREFIX} ${message}`, error || "");
1200
+ }
1201
+ }
1202
+ logWarn(message, details) {
1203
+ if (this.canLog("warn")) {
1204
+ console.warn(`${LOG_PREFIX} ${message}`, details || "");
1205
+ }
1206
+ }
1207
+ logDebug(message) {
1208
+ if (this.canLog("debug")) {
1209
+ console.log(`${LOG_PREFIX} ${message}`);
1210
+ }
1211
+ }
1212
+ canLog(level) {
1213
+ const order = {
1214
+ silent: 0,
1215
+ error: 1,
1216
+ warn: 2,
1217
+ info: 3,
1218
+ debug: 4
1219
+ };
1220
+ return order[this.logLevel] >= order[level];
1008
1221
  }
1009
1222
  };
1010
1223
  // Annotate the CommonJS export names for ESM import in node:
1011
1224
  0 && (module.exports = {
1012
1225
  BUILTIN_TYPES,
1013
1226
  BUILTIN_UTILITY_TYPES,
1014
- ClientGeneratorService,
1015
1227
  DEFAULT_OPTIONS,
1016
1228
  GENERIC_TYPES,
1017
1229
  LOG_PREFIX,
1018
1230
  RPCPlugin,
1231
+ RPC_ARTIFACT_VERSION,
1019
1232
  RouteAnalyzerService,
1020
1233
  SchemaGeneratorService,
1234
+ TypeScriptClientGenerator,
1235
+ assertRpcArtifact,
1021
1236
  buildFullApiPath,
1022
1237
  buildFullPath,
1023
1238
  camelCase,
1024
1239
  computeHash,
1025
1240
  extractNamedType,
1026
1241
  generateTypeScriptInterface,
1242
+ isRpcArtifact,
1027
1243
  mapJsonSchemaTypeToTypeScript,
1028
1244
  readChecksum,
1029
1245
  safeToString,