@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.mjs CHANGED
@@ -9,6 +9,9 @@ var DEFAULT_OPTIONS = {
9
9
  tsConfigPath: "tsconfig.json",
10
10
  outputDir: "./generated/rpc",
11
11
  generateOnInit: true,
12
+ mode: "best-effort",
13
+ logLevel: "info",
14
+ artifactVersion: "1",
12
15
  context: {
13
16
  namespace: "rpc",
14
17
  keys: {
@@ -32,47 +35,20 @@ var BUILTIN_UTILITY_TYPES = /* @__PURE__ */ new Set([
32
35
  var BUILTIN_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "any", "void", "unknown"]);
33
36
  var GENERIC_TYPES = /* @__PURE__ */ new Set(["Array", "Promise", "Partial"]);
34
37
 
35
- // src/utils/hash-utils.ts
36
- import { createHash } from "crypto";
37
- import { existsSync, readFileSync } from "fs";
38
- import { mkdir, writeFile } from "fs/promises";
38
+ // src/generators/typescript-client.generator.ts
39
+ import fs from "fs/promises";
39
40
  import path from "path";
40
- var CHECKSUM_FILENAME = ".rpc-checksum";
41
- function computeHash(filePaths) {
42
- const sorted = [...filePaths].sort();
43
- const hasher = createHash("sha256");
44
- hasher.update(`files:${sorted.length}
45
- `);
46
- for (const filePath of sorted) {
47
- hasher.update(readFileSync(filePath, "utf-8"));
48
- hasher.update("\0");
49
- }
50
- return hasher.digest("hex");
51
- }
52
- function readChecksum(outputDir) {
53
- const checksumPath = path.join(outputDir, CHECKSUM_FILENAME);
54
- if (!existsSync(checksumPath)) return null;
55
- try {
56
- const raw = readFileSync(checksumPath, "utf-8");
57
- const data = JSON.parse(raw);
58
- if (typeof data.hash !== "string" || !Array.isArray(data.files)) {
59
- return null;
60
- }
61
- return data;
62
- } catch {
63
- return null;
64
- }
41
+
42
+ // src/utils/string-utils.ts
43
+ function safeToString(value) {
44
+ if (typeof value === "string") return value;
45
+ if (typeof value === "symbol") return value.description || "Symbol";
46
+ return String(value);
65
47
  }
66
- async function writeChecksum(outputDir, data) {
67
- await mkdir(outputDir, { recursive: true });
68
- const checksumPath = path.join(outputDir, CHECKSUM_FILENAME);
69
- await writeFile(checksumPath, JSON.stringify(data, null, 2), "utf-8");
48
+ function camelCase(str) {
49
+ return str.charAt(0).toLowerCase() + str.slice(1);
70
50
  }
71
51
 
72
- // src/services/client-generator.service.ts
73
- import fs from "fs/promises";
74
- import path2 from "path";
75
-
76
52
  // src/utils/path-utils.ts
77
53
  function buildFullPath(basePath, parameters) {
78
54
  if (!basePath || typeof basePath !== "string") return "/";
@@ -111,46 +87,67 @@ function buildFullApiPath(route) {
111
87
  return fullPath || "/";
112
88
  }
113
89
 
114
- // src/utils/string-utils.ts
115
- function safeToString(value) {
116
- if (typeof value === "string") return value;
117
- if (typeof value === "symbol") return value.description || "Symbol";
118
- return String(value);
90
+ // src/generators/generator-utils.ts
91
+ function groupRoutesByController(routes) {
92
+ const groups = /* @__PURE__ */ new Map();
93
+ for (const route of routes) {
94
+ const controller = safeToString(route.controller);
95
+ if (!groups.has(controller)) {
96
+ groups.set(controller, []);
97
+ }
98
+ groups.get(controller).push(route);
99
+ }
100
+ return groups;
119
101
  }
120
- function camelCase(str) {
121
- return str.charAt(0).toLowerCase() + str.slice(1);
102
+ function buildNormalizedRequestPath(route) {
103
+ let requestPath = buildFullApiPath(route);
104
+ for (const parameter of route.parameters ?? []) {
105
+ if (parameter.decoratorType !== "param") continue;
106
+ const placeholder = `:${String(parameter.data ?? parameter.name)}`;
107
+ requestPath = requestPath.replace(placeholder, `:${parameter.name}`);
108
+ }
109
+ return requestPath;
122
110
  }
123
111
 
124
- // src/services/client-generator.service.ts
125
- var ClientGeneratorService = class {
112
+ // src/generators/typescript-client.generator.ts
113
+ var TypeScriptClientGenerator = class {
126
114
  constructor(outputDir) {
127
115
  this.outputDir = outputDir;
128
116
  }
117
+ name = "typescript-client";
129
118
  /**
130
- * Generates the TypeScript RPC client
119
+ * Generates the TypeScript RPC client.
120
+ */
121
+ async generate(context) {
122
+ return this.generateClient(context.routes, context.schemas);
123
+ }
124
+ /**
125
+ * Generates the TypeScript RPC client.
131
126
  */
132
127
  async generateClient(routes, schemas) {
133
128
  await fs.mkdir(this.outputDir, { recursive: true });
134
129
  await this.generateClientFile(routes, schemas);
135
130
  const generatedInfo = {
136
- clientFile: path2.join(this.outputDir, "client.ts"),
131
+ generator: this.name,
132
+ clientFile: path.join(this.outputDir, "client.ts"),
133
+ outputFiles: [path.join(this.outputDir, "client.ts")],
137
134
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
138
135
  };
139
136
  return generatedInfo;
140
137
  }
141
138
  /**
142
- * Generates the main client file with types included
139
+ * Generates the main client file with types included.
143
140
  */
144
141
  async generateClientFile(routes, schemas) {
145
142
  const clientContent = this.generateClientContent(routes, schemas);
146
- const clientPath = path2.join(this.outputDir, "client.ts");
143
+ const clientPath = path.join(this.outputDir, "client.ts");
147
144
  await fs.writeFile(clientPath, clientContent, "utf-8");
148
145
  }
149
146
  /**
150
- * Generates the client TypeScript content with types included
147
+ * Generates the client TypeScript content with types included.
151
148
  */
152
149
  generateClientContent(routes, schemas) {
153
- const controllerGroups = this.groupRoutesByController(routes);
150
+ const controllerGroups = groupRoutesByController(routes);
154
151
  return `// ============================================================================
155
152
  // TYPES SECTION
156
153
  // ============================================================================
@@ -311,13 +308,21 @@ export class ApiClient {
311
308
  return undefined as T
312
309
  }
313
310
 
314
- const responseData = await response.json()
311
+ const contentType = response.headers.get('content-type') || ''
312
+ const isJson = contentType.includes('application/json') || contentType.includes('+json')
313
+ const responseData = isJson ? await response.json() : await response.text()
315
314
 
316
315
  if (!response.ok) {
317
- throw new ApiError(response.status, responseData.message || 'Request failed')
316
+ const message =
317
+ typeof responseData === 'object' && responseData && 'message' in (responseData as Record<string, unknown>)
318
+ ? String((responseData as Record<string, unknown>).message)
319
+ : typeof responseData === 'string' && responseData.trim()
320
+ ? responseData
321
+ : 'Request failed'
322
+ throw new ApiError(response.status, message)
318
323
  }
319
324
 
320
- return responseData
325
+ return responseData as T
321
326
  } catch (error) {
322
327
  if (error instanceof ApiError) {
323
328
  throw error
@@ -331,7 +336,7 @@ ${this.generateControllerMethods(controllerGroups)}
331
336
  `;
332
337
  }
333
338
  /**
334
- * Generates controller methods for the client
339
+ * Generates controller methods for the client.
335
340
  */
336
341
  generateControllerMethods(controllerGroups) {
337
342
  let methods = "";
@@ -386,14 +391,7 @@ ${this.generateControllerMethods(controllerGroups)}
386
391
  methods += "undefined";
387
392
  methods += `>) => {
388
393
  `;
389
- let requestPath = buildFullApiPath(route);
390
- if (pathParams.length > 0) {
391
- for (const pathParam of pathParams) {
392
- const paramName = pathParam.name;
393
- const placeholder = `:${String(pathParam.data)}`;
394
- requestPath = requestPath.replace(placeholder, `:${paramName}`);
395
- }
396
- }
394
+ const requestPath = buildNormalizedRequestPath(route);
397
395
  methods += ` return this.request<Result>('${httpMethod.toUpperCase()}', \`${requestPath}\`, options)
398
396
  `;
399
397
  methods += ` },
@@ -407,7 +405,7 @@ ${this.generateControllerMethods(controllerGroups)}
407
405
  return methods;
408
406
  }
409
407
  /**
410
- * Extracts the proper return type from route analysis
408
+ * Extracts the proper return type from route analysis.
411
409
  */
412
410
  extractReturnType(returns) {
413
411
  if (!returns) return "any";
@@ -418,7 +416,7 @@ ${this.generateControllerMethods(controllerGroups)}
418
416
  return returns;
419
417
  }
420
418
  /**
421
- * Generates schema types from integrated schema generation
419
+ * Generates schema types from integrated schema generation.
422
420
  */
423
421
  generateSchemaTypes(schemas) {
424
422
  if (schemas.length === 0) {
@@ -435,21 +433,7 @@ ${this.generateControllerMethods(controllerGroups)}
435
433
  return content;
436
434
  }
437
435
  /**
438
- * Groups routes by controller for better organization
439
- */
440
- groupRoutesByController(routes) {
441
- const groups = /* @__PURE__ */ new Map();
442
- for (const route of routes) {
443
- const controller = safeToString(route.controller);
444
- if (!groups.has(controller)) {
445
- groups.set(controller, []);
446
- }
447
- groups.get(controller).push(route);
448
- }
449
- return groups;
450
- }
451
- /**
452
- * Analyzes route parameters to determine their types and usage
436
+ * Analyzes route parameters to determine their types and usage.
453
437
  */
454
438
  analyzeRouteParameters(route) {
455
439
  const parameters = route.parameters || [];
@@ -460,13 +444,79 @@ ${this.generateControllerMethods(controllerGroups)}
460
444
  }
461
445
  };
462
446
 
447
+ // src/utils/hash-utils.ts
448
+ import { createHash } from "crypto";
449
+ import { existsSync, readFileSync } from "fs";
450
+ import { mkdir, writeFile } from "fs/promises";
451
+ import path2 from "path";
452
+ var CHECKSUM_FILENAME = ".rpc-checksum";
453
+ function computeHash(filePaths) {
454
+ const sorted = [...filePaths].sort();
455
+ const hasher = createHash("sha256");
456
+ hasher.update(`files:${sorted.length}
457
+ `);
458
+ for (const filePath of sorted) {
459
+ hasher.update(readFileSync(filePath, "utf-8"));
460
+ hasher.update("\0");
461
+ }
462
+ return hasher.digest("hex");
463
+ }
464
+ function readChecksum(outputDir) {
465
+ const checksumPath = path2.join(outputDir, CHECKSUM_FILENAME);
466
+ if (!existsSync(checksumPath)) return null;
467
+ try {
468
+ const raw = readFileSync(checksumPath, "utf-8");
469
+ const data = JSON.parse(raw);
470
+ if (typeof data.hash !== "string" || !Array.isArray(data.files)) {
471
+ return null;
472
+ }
473
+ return data;
474
+ } catch {
475
+ return null;
476
+ }
477
+ }
478
+ async function writeChecksum(outputDir, data) {
479
+ await mkdir(outputDir, { recursive: true });
480
+ const checksumPath = path2.join(outputDir, CHECKSUM_FILENAME);
481
+ await writeFile(checksumPath, JSON.stringify(data, null, 2), "utf-8");
482
+ }
483
+
484
+ // src/utils/artifact-contract.ts
485
+ var RPC_ARTIFACT_VERSION = "1";
486
+ function isRpcArtifact(value) {
487
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
488
+ const obj = value;
489
+ return typeof obj.artifactVersion === "string" && Array.isArray(obj.routes) && Array.isArray(obj.schemas);
490
+ }
491
+ function assertRpcArtifact(value) {
492
+ if (!isRpcArtifact(value)) {
493
+ throw new Error("Invalid RPC artifact: expected { artifactVersion, routes, schemas }");
494
+ }
495
+ if (value.artifactVersion !== RPC_ARTIFACT_VERSION) {
496
+ throw new Error(
497
+ `Unsupported RPC artifact version '${value.artifactVersion}'. Supported: ${RPC_ARTIFACT_VERSION}.`
498
+ );
499
+ }
500
+ }
501
+
463
502
  // src/services/route-analyzer.service.ts
464
503
  import { RouteRegistry } from "honestjs";
465
504
  var RouteAnalyzerService = class {
505
+ customClassMatcher;
506
+ onWarn;
507
+ warnings = [];
508
+ constructor(options = {}) {
509
+ this.customClassMatcher = options.customClassMatcher;
510
+ this.onWarn = options.onWarn;
511
+ }
512
+ getWarnings() {
513
+ return this.warnings;
514
+ }
466
515
  /**
467
516
  * Analyzes controller methods to extract type information
468
517
  */
469
518
  async analyzeControllerMethods(project) {
519
+ this.warnings = [];
470
520
  const routes = RouteRegistry.getRoutes();
471
521
  if (!routes?.length) {
472
522
  return [];
@@ -487,13 +537,20 @@ var RouteAnalyzerService = class {
487
537
  const classes = sourceFile.getClasses();
488
538
  for (const classDeclaration of classes) {
489
539
  const className = classDeclaration.getName();
490
- if (className?.endsWith("Controller")) {
540
+ if (className && this.isControllerClass(classDeclaration, className)) {
491
541
  controllers.set(className, classDeclaration);
492
542
  }
493
543
  }
494
544
  }
495
545
  return controllers;
496
546
  }
547
+ isControllerClass(classDeclaration, _className) {
548
+ if (this.customClassMatcher) {
549
+ return this.customClassMatcher(classDeclaration);
550
+ }
551
+ const decoratorNames = classDeclaration.getDecorators().map((decorator) => decorator.getName());
552
+ return decoratorNames.includes("Controller") || decoratorNames.includes("View");
553
+ }
497
554
  /**
498
555
  * Processes all routes and extracts type information
499
556
  */
@@ -504,10 +561,9 @@ var RouteAnalyzerService = class {
504
561
  const extendedRoute = this.createExtendedRoute(route, controllers);
505
562
  analyzedRoutes.push(extendedRoute);
506
563
  } catch (routeError) {
507
- console.warn(
508
- `${LOG_PREFIX} Skipping route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
509
- routeError
510
- );
564
+ const warning = `Skipping route ${safeToString(route.controller)}.${safeToString(route.handler)}`;
565
+ this.warnings.push(warning);
566
+ this.onWarn?.(warning, routeError);
511
567
  }
512
568
  }
513
569
  return analyzedRoutes;
@@ -527,6 +583,10 @@ var RouteAnalyzerService = class {
527
583
  returns = this.getReturnType(handlerMethod);
528
584
  parameters = this.getParametersWithTypes(handlerMethod, route.parameters || []);
529
585
  }
586
+ } else {
587
+ const warning = `Controller class not found in source files: ${controllerName} (handler: ${handlerName})`;
588
+ this.warnings.push(warning);
589
+ this.onWarn?.(warning);
530
590
  }
531
591
  return {
532
592
  controller: controllerName,
@@ -665,14 +725,23 @@ function extractNamedType(type) {
665
725
 
666
726
  // src/services/schema-generator.service.ts
667
727
  var SchemaGeneratorService = class {
668
- constructor(controllerPattern, tsConfigPath) {
728
+ constructor(controllerPattern, tsConfigPath, options = {}) {
669
729
  this.controllerPattern = controllerPattern;
670
730
  this.tsConfigPath = tsConfigPath;
731
+ this.failOnSchemaError = options.failOnSchemaError ?? false;
732
+ this.onWarn = options.onWarn;
733
+ }
734
+ failOnSchemaError;
735
+ onWarn;
736
+ warnings = [];
737
+ getWarnings() {
738
+ return this.warnings;
671
739
  }
672
740
  /**
673
741
  * Generates JSON schemas from types used in controllers
674
742
  */
675
743
  async generateSchemas(project) {
744
+ this.warnings = [];
676
745
  const sourceFiles = project.getSourceFiles(this.controllerPattern);
677
746
  const collectedTypes = this.collectTypesFromControllers(sourceFiles);
678
747
  return this.processTypes(collectedTypes);
@@ -719,7 +788,12 @@ var SchemaGeneratorService = class {
719
788
  typescriptType
720
789
  });
721
790
  } catch (err) {
722
- console.error(`Failed to generate schema for ${typeName}:`, err);
791
+ if (this.failOnSchemaError) {
792
+ throw err;
793
+ }
794
+ const warning = `Failed to generate schema for ${typeName}`;
795
+ this.warnings.push(warning);
796
+ this.onWarn?.(warning, err);
723
797
  }
724
798
  }
725
799
  return schemas;
@@ -738,7 +812,12 @@ var SchemaGeneratorService = class {
738
812
  });
739
813
  return generator.createSchema(typeName);
740
814
  } catch (error) {
741
- console.error(`Failed to generate schema for type ${typeName}:`, error);
815
+ if (this.failOnSchemaError) {
816
+ throw error;
817
+ }
818
+ const warning = `Failed to generate schema for type ${typeName}`;
819
+ this.warnings.push(warning);
820
+ this.onWarn?.(warning, error);
742
821
  return {
743
822
  type: "object",
744
823
  properties: {},
@@ -756,27 +835,44 @@ var RPCPlugin = class {
756
835
  generateOnInit;
757
836
  contextNamespace;
758
837
  contextArtifactKey;
838
+ mode;
839
+ logLevel;
840
+ failOnSchemaError;
841
+ failOnRouteAnalysisWarning;
842
+ customClassMatcher;
759
843
  // Services
760
844
  routeAnalyzer;
761
845
  schemaGenerator;
762
- clientGenerator;
846
+ generators;
763
847
  // Shared ts-morph project
764
848
  project = null;
765
849
  // Internal state
766
850
  analyzedRoutes = [];
767
851
  analyzedSchemas = [];
768
- generatedInfo = null;
852
+ generatedInfos = [];
853
+ diagnostics = null;
769
854
  app = null;
770
855
  constructor(options = {}) {
771
856
  this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
772
857
  this.tsConfigPath = options.tsConfigPath ?? path3.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
773
858
  this.outputDir = options.outputDir ?? path3.resolve(process.cwd(), DEFAULT_OPTIONS.outputDir);
774
859
  this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
860
+ this.mode = options.mode ?? DEFAULT_OPTIONS.mode;
861
+ this.logLevel = options.logLevel ?? DEFAULT_OPTIONS.logLevel;
775
862
  this.contextNamespace = options.context?.namespace ?? DEFAULT_OPTIONS.context.namespace;
776
863
  this.contextArtifactKey = options.context?.keys?.artifact ?? DEFAULT_OPTIONS.context.keys.artifact;
777
- this.routeAnalyzer = new RouteAnalyzerService();
778
- this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath);
779
- this.clientGenerator = new ClientGeneratorService(this.outputDir);
864
+ this.customClassMatcher = options.customClassMatcher;
865
+ this.failOnSchemaError = options.failOnSchemaError ?? this.mode === "strict";
866
+ this.failOnRouteAnalysisWarning = options.failOnRouteAnalysisWarning ?? this.mode === "strict";
867
+ this.routeAnalyzer = new RouteAnalyzerService({
868
+ customClassMatcher: this.customClassMatcher,
869
+ onWarn: (message, details) => this.logWarn(message, details)
870
+ });
871
+ this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath, {
872
+ failOnSchemaError: this.failOnSchemaError,
873
+ onWarn: (message, details) => this.logWarn(message, details)
874
+ });
875
+ this.generators = options.generators ?? [new TypeScriptClientGenerator(this.outputDir)];
780
876
  this.validateConfiguration();
781
877
  }
782
878
  /**
@@ -797,17 +893,31 @@ var RPCPlugin = class {
797
893
  if (!this.outputDir?.trim()) {
798
894
  errors.push("Output directory cannot be empty");
799
895
  }
896
+ if (!["strict", "best-effort"].includes(this.mode)) {
897
+ errors.push('Mode must be "strict" or "best-effort"');
898
+ }
899
+ if (!["silent", "error", "warn", "info", "debug"].includes(this.logLevel)) {
900
+ errors.push("logLevel must be one of: silent, error, warn, info, debug");
901
+ }
800
902
  if (!this.contextNamespace?.trim()) {
801
903
  errors.push("Context namespace cannot be empty");
802
904
  }
803
905
  if (!this.contextArtifactKey?.trim()) {
804
906
  errors.push("Context artifact key cannot be empty");
805
907
  }
908
+ for (const generator of this.generators) {
909
+ if (!generator.name?.trim()) {
910
+ errors.push("Generator name cannot be empty");
911
+ }
912
+ if (typeof generator.generate !== "function") {
913
+ errors.push(`Generator "${generator.name || "unknown"}" must implement generate(context)`);
914
+ }
915
+ }
806
916
  if (errors.length > 0) {
807
917
  throw new Error(`Configuration validation failed: ${errors.join(", ")}`);
808
918
  }
809
919
  this.log(
810
- `Configuration validated: controllerPattern=${this.controllerPattern}, tsConfigPath=${this.tsConfigPath}, outputDir=${this.outputDir}`
920
+ `Configuration validated: controllerPattern=${this.controllerPattern}, tsConfigPath=${this.tsConfigPath}, outputDir=${this.outputDir}, mode=${this.mode}`
811
921
  );
812
922
  }
813
923
  /**
@@ -816,14 +926,17 @@ var RPCPlugin = class {
816
926
  afterModulesRegistered = async (app, hono) => {
817
927
  this.app = app;
818
928
  if (this.generateOnInit) {
819
- await this.analyzeEverything();
929
+ await this.analyzeEverything({ force: false, dryRun: false });
820
930
  this.publishArtifact(app);
821
931
  }
822
932
  };
823
933
  /**
824
934
  * Main analysis method that coordinates all three components
825
935
  */
826
- async analyzeEverything(force = false) {
936
+ async analyzeEverything(options) {
937
+ const { force, dryRun } = options;
938
+ const warnings = [];
939
+ let cacheState = force ? "bypass" : "miss";
827
940
  try {
828
941
  this.log("Starting comprehensive RPC analysis...");
829
942
  this.dispose();
@@ -835,21 +948,50 @@ var RPCPlugin = class {
835
948
  const stored = readChecksum(this.outputDir);
836
949
  if (stored && stored.hash === currentHash && this.outputFilesExist()) {
837
950
  if (this.loadArtifactFromDisk()) {
838
- this.log("Source files unchanged \u2014 skipping regeneration");
951
+ cacheState = "hit";
952
+ this.logDebug("Source files unchanged - skipping regeneration");
953
+ this.diagnostics = {
954
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
955
+ mode: this.mode,
956
+ dryRun,
957
+ cache: cacheState,
958
+ routesCount: this.analyzedRoutes.length,
959
+ schemasCount: this.analyzedSchemas.length,
960
+ warnings: []
961
+ };
839
962
  this.dispose();
840
963
  return;
841
964
  }
842
- this.log("Source files unchanged but cached artifact missing/invalid \u2014 regenerating");
965
+ this.logDebug("Source files unchanged but cached artifact missing/invalid - regenerating");
843
966
  }
844
967
  }
845
968
  this.analyzedRoutes = [];
846
969
  this.analyzedSchemas = [];
847
- this.generatedInfo = null;
970
+ this.generatedInfos = [];
848
971
  this.analyzedRoutes = await this.routeAnalyzer.analyzeControllerMethods(this.project);
972
+ warnings.push(...this.routeAnalyzer.getWarnings());
849
973
  this.analyzedSchemas = await this.schemaGenerator.generateSchemas(this.project);
850
- this.generatedInfo = await this.clientGenerator.generateClient(this.analyzedRoutes, this.analyzedSchemas);
851
- await writeChecksum(this.outputDir, { hash: computeHash(filePaths), files: filePaths });
852
- this.writeArtifactToDisk();
974
+ warnings.push(...this.schemaGenerator.getWarnings());
975
+ if (this.failOnRouteAnalysisWarning && this.routeAnalyzer.getWarnings().length > 0) {
976
+ throw new Error(`Route analysis warnings encountered in strict mode: ${this.routeAnalyzer.getWarnings().join("; ")}`);
977
+ }
978
+ if (!dryRun) {
979
+ this.generatedInfos = await this.runGenerators();
980
+ }
981
+ if (!dryRun) {
982
+ await writeChecksum(this.outputDir, { hash: computeHash(filePaths), files: filePaths });
983
+ this.writeArtifactToDisk();
984
+ }
985
+ this.diagnostics = {
986
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
987
+ mode: this.mode,
988
+ dryRun,
989
+ cache: cacheState,
990
+ routesCount: this.analyzedRoutes.length,
991
+ schemasCount: this.analyzedSchemas.length,
992
+ warnings
993
+ };
994
+ this.writeDiagnosticsToDisk();
853
995
  this.log(
854
996
  `\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
855
997
  );
@@ -859,13 +1001,10 @@ var RPCPlugin = class {
859
1001
  throw error;
860
1002
  }
861
1003
  }
862
- /**
863
- * Manually trigger analysis (useful for testing or re-generation).
864
- * Defaults to force=true to bypass cache; pass false to use caching.
865
- */
866
- async analyze(force = true) {
867
- await this.analyzeEverything(force);
868
- if (this.app) {
1004
+ async analyze(forceOrOptions = true) {
1005
+ const options = typeof forceOrOptions === "boolean" ? { force: forceOrOptions, dryRun: false } : { force: forceOrOptions.force ?? true, dryRun: forceOrOptions.dryRun ?? false };
1006
+ await this.analyzeEverything(options);
1007
+ if (this.app && !options.dryRun) {
869
1008
  this.publishArtifact(this.app);
870
1009
  }
871
1010
  }
@@ -885,42 +1024,89 @@ var RPCPlugin = class {
885
1024
  * Get the generation info
886
1025
  */
887
1026
  getGenerationInfo() {
888
- return this.generatedInfo;
1027
+ return this.generatedInfos[0] ?? null;
1028
+ }
1029
+ /**
1030
+ * Get all generation infos
1031
+ */
1032
+ getGenerationInfos() {
1033
+ return this.generatedInfos;
1034
+ }
1035
+ getDiagnostics() {
1036
+ return this.diagnostics;
889
1037
  }
890
1038
  /**
891
1039
  * Checks whether expected output files exist on disk
892
1040
  */
893
1041
  outputFilesExist() {
894
- return fs2.existsSync(path3.join(this.outputDir, "client.ts")) && fs2.existsSync(path3.join(this.outputDir, "rpc-artifact.json"));
1042
+ if (!fs2.existsSync(path3.join(this.outputDir, "rpc-artifact.json"))) {
1043
+ return false;
1044
+ }
1045
+ if (!this.hasTypeScriptGenerator()) {
1046
+ return true;
1047
+ }
1048
+ return fs2.existsSync(path3.join(this.outputDir, "client.ts"));
895
1049
  }
896
1050
  getArtifactPath() {
897
1051
  return path3.join(this.outputDir, "rpc-artifact.json");
898
1052
  }
1053
+ getDiagnosticsPath() {
1054
+ return path3.join(this.outputDir, "rpc-diagnostics.json");
1055
+ }
899
1056
  writeArtifactToDisk() {
900
1057
  const artifact = {
1058
+ artifactVersion: RPC_ARTIFACT_VERSION,
901
1059
  routes: this.analyzedRoutes,
902
1060
  schemas: this.analyzedSchemas
903
1061
  };
904
1062
  fs2.mkdirSync(this.outputDir, { recursive: true });
905
1063
  fs2.writeFileSync(this.getArtifactPath(), JSON.stringify(artifact));
906
1064
  }
1065
+ writeDiagnosticsToDisk() {
1066
+ if (!this.diagnostics) return;
1067
+ fs2.mkdirSync(this.outputDir, { recursive: true });
1068
+ fs2.writeFileSync(this.getDiagnosticsPath(), JSON.stringify(this.diagnostics, null, 2));
1069
+ }
907
1070
  loadArtifactFromDisk() {
908
1071
  try {
909
1072
  const raw = fs2.readFileSync(this.getArtifactPath(), "utf8");
910
1073
  const parsed = JSON.parse(raw);
911
- if (!Array.isArray(parsed.routes) || !Array.isArray(parsed.schemas)) {
912
- return false;
1074
+ if (parsed.artifactVersion === void 0) {
1075
+ if (!Array.isArray(parsed.routes) || !Array.isArray(parsed.schemas)) {
1076
+ return false;
1077
+ }
1078
+ this.analyzedRoutes = parsed.routes;
1079
+ this.analyzedSchemas = parsed.schemas;
1080
+ } else {
1081
+ assertRpcArtifact(parsed);
1082
+ this.analyzedRoutes = parsed.routes;
1083
+ this.analyzedSchemas = parsed.schemas;
913
1084
  }
914
- this.analyzedRoutes = parsed.routes;
915
- this.analyzedSchemas = parsed.schemas;
916
- this.generatedInfo = null;
1085
+ this.generatedInfos = [];
917
1086
  return true;
918
1087
  } catch {
919
1088
  return false;
920
1089
  }
921
1090
  }
1091
+ async runGenerators() {
1092
+ const results = [];
1093
+ for (const generator of this.generators) {
1094
+ this.log(`Running generator: ${generator.name}`);
1095
+ const result = await generator.generate({
1096
+ outputDir: this.outputDir,
1097
+ routes: this.analyzedRoutes,
1098
+ schemas: this.analyzedSchemas
1099
+ });
1100
+ results.push(result);
1101
+ }
1102
+ return results;
1103
+ }
1104
+ hasTypeScriptGenerator() {
1105
+ return this.generators.some((generator) => generator.name === "typescript-client");
1106
+ }
922
1107
  publishArtifact(app) {
923
1108
  app.getContext().set(this.getArtifactContextKey(), {
1109
+ artifactVersion: RPC_ARTIFACT_VERSION,
924
1110
  routes: this.analyzedRoutes,
925
1111
  schemas: this.analyzedSchemas
926
1112
  });
@@ -944,31 +1130,58 @@ var RPCPlugin = class {
944
1130
  * Logs a message with the plugin prefix
945
1131
  */
946
1132
  log(message) {
947
- console.log(`${LOG_PREFIX} ${message}`);
1133
+ if (this.canLog("info")) {
1134
+ console.log(`${LOG_PREFIX} ${message}`);
1135
+ }
948
1136
  }
949
1137
  /**
950
1138
  * Logs an error with the plugin prefix
951
1139
  */
952
1140
  logError(message, error) {
953
- console.error(`${LOG_PREFIX} ${message}`, error || "");
1141
+ if (this.canLog("error")) {
1142
+ console.error(`${LOG_PREFIX} ${message}`, error || "");
1143
+ }
1144
+ }
1145
+ logWarn(message, details) {
1146
+ if (this.canLog("warn")) {
1147
+ console.warn(`${LOG_PREFIX} ${message}`, details || "");
1148
+ }
1149
+ }
1150
+ logDebug(message) {
1151
+ if (this.canLog("debug")) {
1152
+ console.log(`${LOG_PREFIX} ${message}`);
1153
+ }
1154
+ }
1155
+ canLog(level) {
1156
+ const order = {
1157
+ silent: 0,
1158
+ error: 1,
1159
+ warn: 2,
1160
+ info: 3,
1161
+ debug: 4
1162
+ };
1163
+ return order[this.logLevel] >= order[level];
954
1164
  }
955
1165
  };
956
1166
  export {
957
1167
  BUILTIN_TYPES,
958
1168
  BUILTIN_UTILITY_TYPES,
959
- ClientGeneratorService,
960
1169
  DEFAULT_OPTIONS,
961
1170
  GENERIC_TYPES,
962
1171
  LOG_PREFIX,
963
1172
  RPCPlugin,
1173
+ RPC_ARTIFACT_VERSION,
964
1174
  RouteAnalyzerService,
965
1175
  SchemaGeneratorService,
1176
+ TypeScriptClientGenerator,
1177
+ assertRpcArtifact,
966
1178
  buildFullApiPath,
967
1179
  buildFullPath,
968
1180
  camelCase,
969
1181
  computeHash,
970
1182
  extractNamedType,
971
1183
  generateTypeScriptInterface,
1184
+ isRpcArtifact,
972
1185
  mapJsonSchemaTypeToTypeScript,
973
1186
  readChecksum,
974
1187
  safeToString,