@honestjs/rpc-plugin 1.2.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/README.md CHANGED
@@ -18,10 +18,13 @@ pnpm add @honestjs/rpc-plugin
18
18
  ```typescript
19
19
  import { RPCPlugin } from '@honestjs/rpc-plugin'
20
20
  import { Application } from 'honestjs'
21
+ import AppModule from './app.module'
21
22
 
22
- const app = new Application({
23
- plugins: [RPCPlugin]
23
+ const { hono } = await Application.create(AppModule, {
24
+ plugins: [new RPCPlugin()]
24
25
  })
26
+
27
+ export default hono
25
28
  ```
26
29
 
27
30
  ## Configuration Options
@@ -32,11 +35,46 @@ interface RPCPluginOptions {
32
35
  readonly tsConfigPath?: string // Path to tsconfig.json (default: 'tsconfig.json')
33
36
  readonly outputDir?: string // Output directory for generated files (default: './generated/rpc')
34
37
  readonly generateOnInit?: boolean // Generate files on initialization (default: true)
38
+ readonly context?: {
39
+ readonly namespace?: string // Default: 'rpc'
40
+ readonly keys?: {
41
+ readonly artifact?: string // Default: 'artifact'
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Application Context Artifact
48
+
49
+ After analysis, RPC plugin publishes this artifact to the application context:
50
+
51
+ ```typescript
52
+ type RpcArtifact = {
53
+ routes: ExtendedRouteInfo[]
54
+ schemas: SchemaInfo[]
35
55
  }
36
56
  ```
37
57
 
58
+ Default key is `'rpc.artifact'` (from `context.namespace + '.' + context.keys.artifact`). This enables direct integration with API docs:
59
+
60
+ ```typescript
61
+ import { ApiDocsPlugin } from '@honestjs/api-docs-plugin'
62
+
63
+ const { hono } = await Application.create(AppModule, {
64
+ plugins: [new RPCPlugin(), new ApiDocsPlugin({ artifact: 'rpc.artifact' })]
65
+ })
66
+ ```
67
+
38
68
  ## What It Generates
39
69
 
70
+ The plugin generates files in the output directory (default: `./generated/rpc`):
71
+
72
+ | File | Description | When generated |
73
+ | --------------- | -------------------------------------------- | --------------------- |
74
+ | `client.ts` | Type-safe RPC client with all DTOs | Always |
75
+ | `.rpc-checksum` | Hash of source files for incremental caching | Always |
76
+ | `rpc-artifact.json` | Serialized routes/schemas artifact for cache-backed context publishing | Always |
77
+
40
78
  ### TypeScript RPC Client (`client.ts`)
41
79
 
42
80
  The plugin generates a single comprehensive file that includes both the client and all type definitions:
@@ -173,6 +211,27 @@ const testApiClient = new ApiClient('http://test.com', {
173
211
  expect(mockFetch).toHaveBeenCalledWith('http://test.com/api/v1/users/123', expect.objectContaining({ method: 'GET' }))
174
212
  ```
175
213
 
214
+ ## Hash-based Caching
215
+
216
+ On startup the plugin hashes all controller source files (SHA-256) and stores the checksum in `.rpc-checksum` inside the output directory. On subsequent runs, if the hash matches and the expected output files already exist, the expensive analysis and generation pipeline is skipped entirely. This significantly reduces startup time in large projects.
217
+
218
+ Caching is automatic and requires no configuration. To force regeneration:
219
+
220
+ ```typescript
221
+ // Manual call — defaults to force=true, always regenerates
222
+ await rpcPlugin.analyze()
223
+
224
+ // Explicit cache bypass
225
+ await rpcPlugin.analyze(true)
226
+
227
+ // Respect the cache (same behavior as automatic startup)
228
+ await rpcPlugin.analyze(false)
229
+ ```
230
+
231
+ You can also delete `.rpc-checksum` from the output directory to clear the cache.
232
+
233
+ > **Note:** The hash covers controller files matched by the `controllerPattern` glob. If you only change a DTO/model file that lives outside that pattern, the cache won't invalidate automatically. Use `analyze()` or delete `.rpc-checksum` in that case.
234
+
176
235
  ## How It Works
177
236
 
178
237
  ### 1. Route Analysis
@@ -196,6 +255,12 @@ expect(mockFetch).toHaveBeenCalledWith('http://test.com/api/v1/users/123', expec
196
255
  - Creates parameter validation and typing
197
256
  - Builds the complete RPC client with proper error handling
198
257
 
258
+ ### 4. Incremental Caching
259
+
260
+ - Hashes all matched controller files after glob resolution
261
+ - Compares against the stored `.rpc-checksum`
262
+ - Skips steps 1–3 when files are unchanged and output already exists
263
+
199
264
  ## Example Generated Output
200
265
 
201
266
  ### Generated Client
@@ -237,12 +302,15 @@ export type RequestOptions<
237
302
 
238
303
  ## Plugin Lifecycle
239
304
 
240
- The plugin automatically generates files when your HonestJS application starts up (if `generateOnInit` is true). You can
241
- also manually trigger generation:
305
+ The plugin automatically generates files when your HonestJS application starts up (if `generateOnInit` is true). On
306
+ subsequent startups, the hash-based cache will skip regeneration if controller files haven't changed.
307
+
308
+ You can also manually trigger generation:
242
309
 
243
310
  ```typescript
244
311
  const rpcPlugin = new RPCPlugin()
245
- await rpcPlugin.analyze() // Manually trigger analysis and generation
312
+ await rpcPlugin.analyze() // Force regeneration (bypasses cache)
313
+ await rpcPlugin.analyze(false) // Respect cache
246
314
  ```
247
315
 
248
316
  ## Advanced Usage
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { RouteInfo, ParameterMetadata, IPlugin, Application } from 'honestjs';
2
2
  import { Hono } from 'hono';
3
- import { Type } from 'ts-morph';
3
+ import { Project, Type } from 'ts-morph';
4
4
 
5
5
  /**
6
6
  * Parameter metadata with enhanced type information
@@ -9,6 +9,8 @@ interface ParameterMetadataWithType extends ParameterMetadata {
9
9
  readonly type: string;
10
10
  readonly required: boolean;
11
11
  readonly name: string;
12
+ /** Original decorator kind: 'body', 'param', 'query', 'header', etc. */
13
+ readonly decoratorType: string;
12
14
  }
13
15
  /**
14
16
  * Extended route information with comprehensive type data
@@ -49,21 +51,13 @@ interface GeneratedClientInfo {
49
51
  /**
50
52
  * Clean separation of concerns for request options
51
53
  */
52
- type RequestOptions<TParams = never, TQuery = never, TBody = never, THeaders = never> = (TParams extends never ? {
53
- params?: never;
54
- } : {
54
+ type RequestOptions<TParams = undefined, TQuery = undefined, TBody = undefined, THeaders = undefined> = (TParams extends undefined ? object : {
55
55
  params: TParams;
56
- }) & (TQuery extends never ? {
57
- query?: never;
58
- } : {
59
- query?: TQuery;
60
- }) & (TBody extends never ? {
61
- body?: never;
62
- } : {
56
+ }) & (TQuery extends undefined ? object : {
57
+ query: TQuery;
58
+ }) & (TBody extends undefined ? object : {
63
59
  body: TBody;
64
- }) & (THeaders extends never ? {
65
- headers?: never;
66
- } : {
60
+ }) & (THeaders extends undefined ? object : {
67
61
  headers: THeaders;
68
62
  });
69
63
  /**
@@ -86,6 +80,12 @@ interface RPCPluginOptions {
86
80
  readonly tsConfigPath?: string;
87
81
  readonly outputDir?: string;
88
82
  readonly generateOnInit?: boolean;
83
+ readonly context?: {
84
+ readonly namespace?: string;
85
+ readonly keys?: {
86
+ readonly artifact?: string;
87
+ };
88
+ };
89
89
  }
90
90
  /**
91
91
  * Comprehensive RPC plugin that combines route analysis, schema generation, and client generation
@@ -95,12 +95,16 @@ declare class RPCPlugin implements IPlugin {
95
95
  private readonly tsConfigPath;
96
96
  private readonly outputDir;
97
97
  private readonly generateOnInit;
98
+ private readonly contextNamespace;
99
+ private readonly contextArtifactKey;
98
100
  private readonly routeAnalyzer;
99
101
  private readonly schemaGenerator;
100
102
  private readonly clientGenerator;
103
+ private project;
101
104
  private analyzedRoutes;
102
105
  private analyzedSchemas;
103
106
  private generatedInfo;
107
+ private app;
104
108
  constructor(options?: RPCPluginOptions);
105
109
  /**
106
110
  * Validates the plugin configuration
@@ -115,9 +119,10 @@ declare class RPCPlugin implements IPlugin {
115
119
  */
116
120
  private analyzeEverything;
117
121
  /**
118
- * Manually trigger analysis (useful for testing or re-generation)
122
+ * Manually trigger analysis (useful for testing or re-generation).
123
+ * Defaults to force=true to bypass cache; pass false to use caching.
119
124
  */
120
- analyze(): Promise<void>;
125
+ analyze(force?: boolean): Promise<void>;
121
126
  /**
122
127
  * Get the analyzed routes
123
128
  */
@@ -130,6 +135,15 @@ declare class RPCPlugin implements IPlugin {
130
135
  * Get the generation info
131
136
  */
132
137
  getGenerationInfo(): GeneratedClientInfo | null;
138
+ /**
139
+ * Checks whether expected output files exist on disk
140
+ */
141
+ private outputFilesExist;
142
+ private getArtifactPath;
143
+ private writeArtifactToDisk;
144
+ private loadArtifactFromDisk;
145
+ private publishArtifact;
146
+ private getArtifactContextKey;
133
147
  /**
134
148
  * Cleanup resources to prevent memory leaks
135
149
  */
@@ -188,22 +202,10 @@ declare class ClientGeneratorService {
188
202
  * Service for analyzing controller methods and extracting type information
189
203
  */
190
204
  declare class RouteAnalyzerService {
191
- private readonly controllerPattern;
192
- private readonly tsConfigPath;
193
- constructor(controllerPattern: string, tsConfigPath: string);
194
- private projects;
195
205
  /**
196
206
  * Analyzes controller methods to extract type information
197
207
  */
198
- analyzeControllerMethods(): Promise<ExtendedRouteInfo[]>;
199
- /**
200
- * Creates a new ts-morph project
201
- */
202
- private createProject;
203
- /**
204
- * Cleanup resources to prevent memory leaks
205
- */
206
- dispose(): void;
208
+ analyzeControllerMethods(project: Project): Promise<ExtendedRouteInfo[]>;
207
209
  /**
208
210
  * Finds controller classes in the project
209
211
  */
@@ -233,19 +235,10 @@ declare class SchemaGeneratorService {
233
235
  private readonly controllerPattern;
234
236
  private readonly tsConfigPath;
235
237
  constructor(controllerPattern: string, tsConfigPath: string);
236
- private projects;
237
238
  /**
238
239
  * Generates JSON schemas from types used in controllers
239
240
  */
240
- generateSchemas(): Promise<SchemaInfo[]>;
241
- /**
242
- * Creates a new ts-morph project
243
- */
244
- private createProject;
245
- /**
246
- * Cleanup resources to prevent memory leaks
247
- */
248
- dispose(): void;
241
+ generateSchemas(project: Project): Promise<SchemaInfo[]>;
249
242
  /**
250
243
  * Collects types from controller files
251
244
  */
@@ -264,6 +257,26 @@ declare class SchemaGeneratorService {
264
257
  private generateSchemaForType;
265
258
  }
266
259
 
260
+ interface ChecksumData {
261
+ hash: string;
262
+ files: string[];
263
+ }
264
+ /**
265
+ * Computes a deterministic SHA-256 hash from file contents.
266
+ * Sorts paths before reading to ensure consistent ordering.
267
+ * Includes the file count in the hash so adding/removing files changes it.
268
+ */
269
+ declare function computeHash(filePaths: string[]): string;
270
+ /**
271
+ * Reads the stored checksum from the output directory.
272
+ * Returns null if the file is missing or corrupt.
273
+ */
274
+ declare function readChecksum(outputDir: string): ChecksumData | null;
275
+ /**
276
+ * Writes the checksum data to the output directory.
277
+ */
278
+ declare function writeChecksum(outputDir: string, data: ChecksumData): Promise<void>;
279
+
267
280
  /**
268
281
  * Builds the full path with parameter placeholders
269
282
  */
@@ -295,10 +308,6 @@ declare function camelCase(str: string): string;
295
308
  * Extracts a named type from a TypeScript type
296
309
  */
297
310
  declare function extractNamedType(type: Type): string | null;
298
- /**
299
- * Generates type imports for the client
300
- */
301
- declare function generateTypeImports(routes: readonly any[]): string;
302
311
 
303
312
  /**
304
313
  * Default configuration options for the RPCPlugin
@@ -308,6 +317,12 @@ declare const DEFAULT_OPTIONS: {
308
317
  readonly tsConfigPath: "tsconfig.json";
309
318
  readonly outputDir: "./generated/rpc";
310
319
  readonly generateOnInit: true;
320
+ readonly context: {
321
+ readonly namespace: "rpc";
322
+ readonly keys: {
323
+ readonly artifact: "artifact";
324
+ };
325
+ };
311
326
  };
312
327
  /**
313
328
  * Log prefix for the RPC plugin
@@ -326,4 +341,4 @@ declare const BUILTIN_TYPES: Set<string>;
326
341
  */
327
342
  declare const GENERIC_TYPES: Set<string>;
328
343
 
329
- export { ApiError, BUILTIN_TYPES, BUILTIN_UTILITY_TYPES, ClientGeneratorService, type ControllerGroups, DEFAULT_OPTIONS, type ExtendedRouteInfo, type FetchFunction, GENERIC_TYPES, type GeneratedClientInfo, LOG_PREFIX, type ParameterMetadataWithType, RPCPlugin, type RPCPluginOptions, type RequestOptions, RouteAnalyzerService, type RouteParameter, SchemaGeneratorService, type SchemaInfo, buildFullApiPath, buildFullPath, camelCase, extractNamedType, generateTypeImports, generateTypeScriptInterface, mapJsonSchemaTypeToTypeScript, safeToString };
344
+ export { ApiError, BUILTIN_TYPES, BUILTIN_UTILITY_TYPES, type ChecksumData, ClientGeneratorService, type ControllerGroups, DEFAULT_OPTIONS, type ExtendedRouteInfo, type FetchFunction, GENERIC_TYPES, type GeneratedClientInfo, LOG_PREFIX, type ParameterMetadataWithType, RPCPlugin, type RPCPluginOptions, type RequestOptions, RouteAnalyzerService, type RouteParameter, SchemaGeneratorService, type SchemaInfo, buildFullApiPath, buildFullPath, camelCase, computeHash, extractNamedType, generateTypeScriptInterface, mapJsonSchemaTypeToTypeScript, readChecksum, safeToString, writeChecksum };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { RouteInfo, ParameterMetadata, IPlugin, Application } from 'honestjs';
2
2
  import { Hono } from 'hono';
3
- import { Type } from 'ts-morph';
3
+ import { Project, Type } from 'ts-morph';
4
4
 
5
5
  /**
6
6
  * Parameter metadata with enhanced type information
@@ -9,6 +9,8 @@ interface ParameterMetadataWithType extends ParameterMetadata {
9
9
  readonly type: string;
10
10
  readonly required: boolean;
11
11
  readonly name: string;
12
+ /** Original decorator kind: 'body', 'param', 'query', 'header', etc. */
13
+ readonly decoratorType: string;
12
14
  }
13
15
  /**
14
16
  * Extended route information with comprehensive type data
@@ -49,21 +51,13 @@ interface GeneratedClientInfo {
49
51
  /**
50
52
  * Clean separation of concerns for request options
51
53
  */
52
- type RequestOptions<TParams = never, TQuery = never, TBody = never, THeaders = never> = (TParams extends never ? {
53
- params?: never;
54
- } : {
54
+ type RequestOptions<TParams = undefined, TQuery = undefined, TBody = undefined, THeaders = undefined> = (TParams extends undefined ? object : {
55
55
  params: TParams;
56
- }) & (TQuery extends never ? {
57
- query?: never;
58
- } : {
59
- query?: TQuery;
60
- }) & (TBody extends never ? {
61
- body?: never;
62
- } : {
56
+ }) & (TQuery extends undefined ? object : {
57
+ query: TQuery;
58
+ }) & (TBody extends undefined ? object : {
63
59
  body: TBody;
64
- }) & (THeaders extends never ? {
65
- headers?: never;
66
- } : {
60
+ }) & (THeaders extends undefined ? object : {
67
61
  headers: THeaders;
68
62
  });
69
63
  /**
@@ -86,6 +80,12 @@ interface RPCPluginOptions {
86
80
  readonly tsConfigPath?: string;
87
81
  readonly outputDir?: string;
88
82
  readonly generateOnInit?: boolean;
83
+ readonly context?: {
84
+ readonly namespace?: string;
85
+ readonly keys?: {
86
+ readonly artifact?: string;
87
+ };
88
+ };
89
89
  }
90
90
  /**
91
91
  * Comprehensive RPC plugin that combines route analysis, schema generation, and client generation
@@ -95,12 +95,16 @@ declare class RPCPlugin implements IPlugin {
95
95
  private readonly tsConfigPath;
96
96
  private readonly outputDir;
97
97
  private readonly generateOnInit;
98
+ private readonly contextNamespace;
99
+ private readonly contextArtifactKey;
98
100
  private readonly routeAnalyzer;
99
101
  private readonly schemaGenerator;
100
102
  private readonly clientGenerator;
103
+ private project;
101
104
  private analyzedRoutes;
102
105
  private analyzedSchemas;
103
106
  private generatedInfo;
107
+ private app;
104
108
  constructor(options?: RPCPluginOptions);
105
109
  /**
106
110
  * Validates the plugin configuration
@@ -115,9 +119,10 @@ declare class RPCPlugin implements IPlugin {
115
119
  */
116
120
  private analyzeEverything;
117
121
  /**
118
- * Manually trigger analysis (useful for testing or re-generation)
122
+ * Manually trigger analysis (useful for testing or re-generation).
123
+ * Defaults to force=true to bypass cache; pass false to use caching.
119
124
  */
120
- analyze(): Promise<void>;
125
+ analyze(force?: boolean): Promise<void>;
121
126
  /**
122
127
  * Get the analyzed routes
123
128
  */
@@ -130,6 +135,15 @@ declare class RPCPlugin implements IPlugin {
130
135
  * Get the generation info
131
136
  */
132
137
  getGenerationInfo(): GeneratedClientInfo | null;
138
+ /**
139
+ * Checks whether expected output files exist on disk
140
+ */
141
+ private outputFilesExist;
142
+ private getArtifactPath;
143
+ private writeArtifactToDisk;
144
+ private loadArtifactFromDisk;
145
+ private publishArtifact;
146
+ private getArtifactContextKey;
133
147
  /**
134
148
  * Cleanup resources to prevent memory leaks
135
149
  */
@@ -188,22 +202,10 @@ declare class ClientGeneratorService {
188
202
  * Service for analyzing controller methods and extracting type information
189
203
  */
190
204
  declare class RouteAnalyzerService {
191
- private readonly controllerPattern;
192
- private readonly tsConfigPath;
193
- constructor(controllerPattern: string, tsConfigPath: string);
194
- private projects;
195
205
  /**
196
206
  * Analyzes controller methods to extract type information
197
207
  */
198
- analyzeControllerMethods(): Promise<ExtendedRouteInfo[]>;
199
- /**
200
- * Creates a new ts-morph project
201
- */
202
- private createProject;
203
- /**
204
- * Cleanup resources to prevent memory leaks
205
- */
206
- dispose(): void;
208
+ analyzeControllerMethods(project: Project): Promise<ExtendedRouteInfo[]>;
207
209
  /**
208
210
  * Finds controller classes in the project
209
211
  */
@@ -233,19 +235,10 @@ declare class SchemaGeneratorService {
233
235
  private readonly controllerPattern;
234
236
  private readonly tsConfigPath;
235
237
  constructor(controllerPattern: string, tsConfigPath: string);
236
- private projects;
237
238
  /**
238
239
  * Generates JSON schemas from types used in controllers
239
240
  */
240
- generateSchemas(): Promise<SchemaInfo[]>;
241
- /**
242
- * Creates a new ts-morph project
243
- */
244
- private createProject;
245
- /**
246
- * Cleanup resources to prevent memory leaks
247
- */
248
- dispose(): void;
241
+ generateSchemas(project: Project): Promise<SchemaInfo[]>;
249
242
  /**
250
243
  * Collects types from controller files
251
244
  */
@@ -264,6 +257,26 @@ declare class SchemaGeneratorService {
264
257
  private generateSchemaForType;
265
258
  }
266
259
 
260
+ interface ChecksumData {
261
+ hash: string;
262
+ files: string[];
263
+ }
264
+ /**
265
+ * Computes a deterministic SHA-256 hash from file contents.
266
+ * Sorts paths before reading to ensure consistent ordering.
267
+ * Includes the file count in the hash so adding/removing files changes it.
268
+ */
269
+ declare function computeHash(filePaths: string[]): string;
270
+ /**
271
+ * Reads the stored checksum from the output directory.
272
+ * Returns null if the file is missing or corrupt.
273
+ */
274
+ declare function readChecksum(outputDir: string): ChecksumData | null;
275
+ /**
276
+ * Writes the checksum data to the output directory.
277
+ */
278
+ declare function writeChecksum(outputDir: string, data: ChecksumData): Promise<void>;
279
+
267
280
  /**
268
281
  * Builds the full path with parameter placeholders
269
282
  */
@@ -295,10 +308,6 @@ declare function camelCase(str: string): string;
295
308
  * Extracts a named type from a TypeScript type
296
309
  */
297
310
  declare function extractNamedType(type: Type): string | null;
298
- /**
299
- * Generates type imports for the client
300
- */
301
- declare function generateTypeImports(routes: readonly any[]): string;
302
311
 
303
312
  /**
304
313
  * Default configuration options for the RPCPlugin
@@ -308,6 +317,12 @@ declare const DEFAULT_OPTIONS: {
308
317
  readonly tsConfigPath: "tsconfig.json";
309
318
  readonly outputDir: "./generated/rpc";
310
319
  readonly generateOnInit: true;
320
+ readonly context: {
321
+ readonly namespace: "rpc";
322
+ readonly keys: {
323
+ readonly artifact: "artifact";
324
+ };
325
+ };
311
326
  };
312
327
  /**
313
328
  * Log prefix for the RPC plugin
@@ -326,4 +341,4 @@ declare const BUILTIN_TYPES: Set<string>;
326
341
  */
327
342
  declare const GENERIC_TYPES: Set<string>;
328
343
 
329
- export { ApiError, BUILTIN_TYPES, BUILTIN_UTILITY_TYPES, ClientGeneratorService, type ControllerGroups, DEFAULT_OPTIONS, type ExtendedRouteInfo, type FetchFunction, GENERIC_TYPES, type GeneratedClientInfo, LOG_PREFIX, type ParameterMetadataWithType, RPCPlugin, type RPCPluginOptions, type RequestOptions, RouteAnalyzerService, type RouteParameter, SchemaGeneratorService, type SchemaInfo, buildFullApiPath, buildFullPath, camelCase, extractNamedType, generateTypeImports, generateTypeScriptInterface, mapJsonSchemaTypeToTypeScript, safeToString };
344
+ export { ApiError, BUILTIN_TYPES, BUILTIN_UTILITY_TYPES, type ChecksumData, ClientGeneratorService, type ControllerGroups, DEFAULT_OPTIONS, type ExtendedRouteInfo, type FetchFunction, GENERIC_TYPES, type GeneratedClientInfo, LOG_PREFIX, type ParameterMetadataWithType, RPCPlugin, type RPCPluginOptions, type RequestOptions, RouteAnalyzerService, type RouteParameter, SchemaGeneratorService, type SchemaInfo, buildFullApiPath, buildFullPath, camelCase, computeHash, extractNamedType, generateTypeScriptInterface, mapJsonSchemaTypeToTypeScript, readChecksum, safeToString, writeChecksum };