@honestjs/rpc-plugin 1.5.0 → 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/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # RPC Plugin
2
2
 
3
- The RPC Plugin automatically analyzes your HonestJS controllers and, by default, generates a fully-typed TypeScript RPC
4
- client with proper parameter typing. You can also provide custom generators.
3
+ The RPC Plugin automatically analyzes your HonestJS controllers and, by default,
4
+ generates a fully-typed TypeScript RPC client with proper parameter typing. You
5
+ can also provide custom generators.
5
6
 
6
7
  ## Installation
7
8
 
@@ -16,47 +17,55 @@ pnpm add @honestjs/rpc-plugin
16
17
  ## Basic Setup
17
18
 
18
19
  ```typescript
19
- import { RPCPlugin } from '@honestjs/rpc-plugin'
20
- import { Application } from 'honestjs'
21
- import AppModule from './app.module'
20
+ import { RPCPlugin } from "@honestjs/rpc-plugin";
21
+ import { Application } from "honestjs";
22
+ import AppModule from "./app.module";
22
23
 
23
24
  const { hono } = await Application.create(AppModule, {
24
- plugins: [new RPCPlugin()]
25
- })
25
+ plugins: [new RPCPlugin()],
26
+ });
26
27
 
27
- export default hono
28
+ export default hono;
28
29
  ```
29
30
 
30
31
  ## Configuration Options
31
32
 
32
33
  ```typescript
33
34
  interface RPCPluginOptions {
34
- readonly controllerPattern?: string // Glob pattern for controller files (default: 'src/modules/*/*.controller.ts')
35
- readonly tsConfigPath?: string // Path to tsconfig.json (default: 'tsconfig.json')
36
- readonly outputDir?: string // Output directory for generated files (default: './generated/rpc')
37
- readonly generateOnInit?: boolean // Generate files on initialization (default: true)
38
- readonly generators?: readonly RPCGenerator[] // Optional list of generators to execute
35
+ readonly controllerPattern?: string; // Glob pattern for controller files (default: 'src/modules/*/*.controller.ts')
36
+ readonly tsConfigPath?: string; // Path to tsconfig.json (default: 'tsconfig.json')
37
+ readonly outputDir?: string; // Output directory for generated files (default: './generated/rpc')
38
+ readonly generateOnInit?: boolean; // Generate files on initialization (default: true)
39
+ readonly generators?: readonly RPCGenerator[]; // Optional list of generators to execute
40
+ readonly mode?: "strict" | "best-effort"; // strict fails on warnings/fallbacks
41
+ readonly logLevel?: "silent" | "error" | "warn" | "info" | "debug"; // default: info
42
+ readonly customClassMatcher?: (
43
+ classDeclaration: ClassDeclaration,
44
+ ) => boolean; // optional override; default discovery uses decorators
45
+ readonly failOnSchemaError?: boolean; // default true in strict mode
46
+ readonly failOnRouteAnalysisWarning?: boolean; // default true in strict mode
39
47
  readonly context?: {
40
- readonly namespace?: string // Default: 'rpc'
48
+ readonly namespace?: string; // Default: 'rpc'
41
49
  readonly keys?: {
42
- readonly artifact?: string // Default: 'artifact'
43
- }
44
- }
50
+ readonly artifact?: string; // Default: 'artifact'
51
+ };
52
+ };
45
53
  }
46
54
  ```
47
55
 
48
56
  ### Generator Behavior
49
57
 
50
- - If `generators` is omitted, the plugin uses the built-in `TypeScriptClientGenerator` by default.
58
+ - If `generators` is omitted, the plugin uses the built-in
59
+ `TypeScriptClientGenerator` by default.
51
60
  - If `generators` is provided, only those generators are executed.
52
61
  - You can still use the built-in TypeScript client generator explicitly:
53
62
 
54
63
  ```typescript
55
- import { RPCPlugin, TypeScriptClientGenerator } from '@honestjs/rpc-plugin'
64
+ import { RPCPlugin, TypeScriptClientGenerator } from "@honestjs/rpc-plugin";
56
65
 
57
66
  new RPCPlugin({
58
- generators: [new TypeScriptClientGenerator('./generated/rpc')]
59
- })
67
+ generators: [new TypeScriptClientGenerator("./generated/rpc")],
68
+ });
60
69
  ```
61
70
 
62
71
  ## Application Context Artifact
@@ -65,68 +74,78 @@ After analysis, RPC plugin publishes this artifact to the application context:
65
74
 
66
75
  ```typescript
67
76
  type RpcArtifact = {
68
- routes: ExtendedRouteInfo[]
69
- schemas: SchemaInfo[]
70
- }
77
+ artifactVersion: string;
78
+ routes: ExtendedRouteInfo[];
79
+ schemas: SchemaInfo[];
80
+ };
71
81
  ```
72
82
 
73
- Default key is `'rpc.artifact'` (from `context.namespace + '.' + context.keys.artifact`). This enables direct integration with API docs:
83
+ Default key is `'rpc.artifact'` (from
84
+ `context.namespace + '.' + context.keys.artifact`). This enables direct
85
+ integration with API docs:
74
86
 
75
87
  ```typescript
76
- import { ApiDocsPlugin } from '@honestjs/api-docs-plugin'
88
+ import { ApiDocsPlugin } from "@honestjs/api-docs-plugin";
77
89
 
78
90
  const { hono } = await Application.create(AppModule, {
79
- plugins: [new RPCPlugin(), new ApiDocsPlugin({ artifact: 'rpc.artifact' })]
80
- })
91
+ plugins: [new RPCPlugin(), new ApiDocsPlugin({ artifact: "rpc.artifact" })],
92
+ });
81
93
  ```
82
94
 
95
+ `artifactVersion` is currently `"1"` and is used for compatibility checks.
96
+
83
97
  ## What It Generates
84
98
 
85
99
  The plugin generates files in the output directory (default: `./generated/rpc`):
86
100
 
87
- | File | Description | When generated |
88
- | --------------- | -------------------------------------------- | --------------------- |
89
- | `client.ts` | Type-safe RPC client with all DTOs | When TypeScript generator runs |
90
- | `.rpc-checksum` | Hash of source files for incremental caching | Always |
91
- | `rpc-artifact.json` | Serialized routes/schemas artifact for cache-backed context publishing | Always |
101
+ | File | Description | When generated |
102
+ | ---------------------- | ---------------------------------------------------------------------- | ------------------------------ |
103
+ | `client.ts` | Type-safe RPC client with all DTOs | When TypeScript generator runs |
104
+ | `.rpc-checksum` | Hash of source files for incremental caching | Always |
105
+ | `rpc-artifact.json` | Serialized routes/schemas artifact for cache-backed context publishing | Always |
106
+ | `rpc-diagnostics.json` | Diagnostics report (mode, warnings, cache status) | Always |
92
107
 
93
108
  ### TypeScript RPC Client (`client.ts`)
94
109
 
95
- The plugin generates a single comprehensive file that includes both the client and all type definitions:
110
+ The plugin generates a single comprehensive file that includes both the client
111
+ and all type definitions:
96
112
 
97
113
  - **Controller-based organization**: Methods grouped by controller
98
114
  - **Type-safe parameters**: Path, query, and body parameters with proper typing
99
- - **Flexible request options**: Clean separation of params, query, body, and headers
115
+ - **Flexible request options**: Clean separation of params, query, body, and
116
+ headers
100
117
  - **Error handling**: Built-in error handling with custom ApiError class
101
118
  - **Header management**: Easy custom header management
102
- - **Custom fetch support**: Inject custom fetch implementations for testing, middleware, and compatibility
103
- - **Integrated types**: All DTOs, interfaces, and utility types included in the same file
119
+ - **Custom fetch support**: Inject custom fetch implementations for testing,
120
+ middleware, and compatibility
121
+ - **Integrated types**: All DTOs, interfaces, and utility types included in the
122
+ same file
104
123
 
105
124
  ```typescript
106
125
  // Generated client usage
107
- import { ApiClient } from './generated/rpc/client'
126
+ import { ApiClient } from "./generated/rpc/client";
108
127
 
109
128
  // Create client instance with base URL
110
- const apiClient = new ApiClient('http://localhost:3000')
129
+ const apiClient = new ApiClient("http://localhost:3000");
111
130
 
112
131
  // Type-safe API calls
113
132
  const user = await apiClient.users.create({
114
- body: { name: 'John', email: 'john@example.com' }
115
- })
133
+ body: { name: "John", email: "john@example.com" },
134
+ });
116
135
 
117
136
  const users = await apiClient.users.list({
118
- query: { page: 1, limit: 10 }
119
- })
137
+ query: { page: 1, limit: 10 },
138
+ });
120
139
 
121
140
  const user = await apiClient.users.getById({
122
- params: { id: '123' }
123
- })
141
+ params: { id: "123" },
142
+ });
124
143
 
125
144
  // Set custom headers
126
145
  apiClient.setDefaultHeaders({
127
- 'X-API-Key': 'your-api-key',
128
- Authorization: 'Bearer your-jwt-token'
129
- })
146
+ "X-API-Key": "your-api-key",
147
+ Authorization: "Bearer your-jwt-token",
148
+ });
130
149
  ```
131
150
 
132
151
  The generated `client.ts` file contains everything you need:
@@ -142,7 +161,8 @@ The RPC client supports custom fetch implementations, which is useful for:
142
161
 
143
162
  - **Testing**: Inject mock fetch functions for unit testing
144
163
  - **Custom Logic**: Add logging, retries, or other middleware
145
- - **Environment Compatibility**: Use different fetch implementations (node-fetch, undici, etc.)
164
+ - **Environment Compatibility**: Use different fetch implementations
165
+ (node-fetch, undici, etc.)
146
166
  - **Interceptors**: Wrap requests with custom logic before/after execution
147
167
 
148
168
  ### Basic Custom Fetch Example
@@ -150,13 +170,18 @@ The RPC client supports custom fetch implementations, which is useful for:
150
170
  ```typescript
151
171
  // Simple logging wrapper
152
172
  const loggingFetch = (input: RequestInfo | URL, init?: RequestInit) => {
153
- console.log(`[${new Date().toISOString()}] Making ${init?.method || 'GET'} request to:`, input)
154
- return fetch(input, init)
155
- }
156
-
157
- const apiClient = new ApiClient('http://localhost:3000', {
158
- fetchFn: loggingFetch
159
- })
173
+ console.log(
174
+ `[${new Date().toISOString()}] Making ${
175
+ init?.method || "GET"
176
+ } request to:`,
177
+ input,
178
+ );
179
+ return fetch(input, init);
180
+ };
181
+
182
+ const apiClient = new ApiClient("http://localhost:3000", {
183
+ fetchFn: loggingFetch,
184
+ });
160
185
  ```
161
186
 
162
187
  ### Advanced Custom Fetch Examples
@@ -167,24 +192,26 @@ const retryFetch = (maxRetries = 3) => {
167
192
  return async (input: RequestInfo | URL, init?: RequestInit) => {
168
193
  for (let i = 0; i <= maxRetries; i++) {
169
194
  try {
170
- const response = await fetch(input, init)
171
- if (response.ok) return response
195
+ const response = await fetch(input, init);
196
+ if (response.ok) return response;
172
197
 
173
- if (i === maxRetries) return response
198
+ if (i === maxRetries) return response;
174
199
 
175
200
  // Wait with exponential backoff
176
- await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000))
201
+ await new Promise((resolve) =>
202
+ setTimeout(resolve, Math.pow(2, i) * 1000)
203
+ );
177
204
  } catch (error) {
178
- if (i === maxRetries) throw error
205
+ if (i === maxRetries) throw error;
179
206
  }
180
207
  }
181
- throw new Error('Max retries exceeded')
182
- }
183
- }
208
+ throw new Error("Max retries exceeded");
209
+ };
210
+ };
184
211
 
185
- const apiClientWithRetry = new ApiClient('http://localhost:3000', {
186
- fetchFn: retryFetch(3)
187
- })
212
+ const apiClientWithRetry = new ApiClient("http://localhost:3000", {
213
+ fetchFn: retryFetch(3),
214
+ });
188
215
 
189
216
  // Request/response interceptor
190
217
  const interceptorFetch = (input: RequestInfo | URL, init?: RequestInit) => {
@@ -193,20 +220,20 @@ const interceptorFetch = (input: RequestInfo | URL, init?: RequestInit) => {
193
220
  ...init,
194
221
  headers: {
195
222
  ...init?.headers,
196
- 'X-Request-ID': crypto.randomUUID()
197
- }
198
- }
223
+ "X-Request-ID": crypto.randomUUID(),
224
+ },
225
+ };
199
226
 
200
227
  return fetch(input, enhancedInit).then((response) => {
201
228
  // Post-response logic
202
- console.log(`Response status: ${response.status}`)
203
- return response
204
- })
205
- }
206
-
207
- const apiClientWithInterceptor = new ApiClient('http://localhost:3000', {
208
- fetchFn: interceptorFetch
209
- })
229
+ console.log(`Response status: ${response.status}`);
230
+ return response;
231
+ });
232
+ };
233
+
234
+ const apiClientWithInterceptor = new ApiClient("http://localhost:3000", {
235
+ fetchFn: interceptorFetch,
236
+ });
210
237
  ```
211
238
 
212
239
  ### Testing with Custom Fetch
@@ -215,37 +242,45 @@ const apiClientWithInterceptor = new ApiClient('http://localhost:3000', {
215
242
  // Mock fetch for testing
216
243
  const mockFetch = jest.fn().mockResolvedValue({
217
244
  ok: true,
218
- json: () => Promise.resolve({ data: { id: '123', name: 'Test User' } })
219
- })
245
+ json: () => Promise.resolve({ data: { id: "123", name: "Test User" } }),
246
+ });
220
247
 
221
- const testApiClient = new ApiClient('http://test.com', {
222
- fetchFn: mockFetch
223
- })
248
+ const testApiClient = new ApiClient("http://test.com", {
249
+ fetchFn: mockFetch,
250
+ });
224
251
 
225
252
  // Your test can now verify the mock was called
226
- expect(mockFetch).toHaveBeenCalledWith('http://test.com/api/v1/users/123', expect.objectContaining({ method: 'GET' }))
253
+ expect(mockFetch).toHaveBeenCalledWith(
254
+ "http://test.com/api/v1/users/123",
255
+ expect.objectContaining({ method: "GET" }),
256
+ );
227
257
  ```
228
258
 
229
259
  ## Hash-based Caching
230
260
 
231
- 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.
261
+ On startup the plugin hashes all controller source files (SHA-256) and stores
262
+ the checksum in `.rpc-checksum` inside the output directory. On subsequent runs,
263
+ if the hash matches and the expected output files already exist, the expensive
264
+ analysis and generation pipeline is skipped entirely. This significantly reduces
265
+ startup time in large projects.
232
266
 
233
267
  Caching is automatic and requires no configuration. To force regeneration:
234
268
 
235
269
  ```typescript
236
- // Manual call — defaults to force=true, always regenerates
237
- await rpcPlugin.analyze()
238
-
239
270
  // Explicit cache bypass
240
- await rpcPlugin.analyze(true)
271
+ await rpcPlugin.analyze({ force: true });
241
272
 
242
273
  // Respect the cache (same behavior as automatic startup)
243
- await rpcPlugin.analyze(false)
274
+ await rpcPlugin.analyze({ force: false });
244
275
  ```
245
276
 
246
- You can also delete `.rpc-checksum` from the output directory to clear the cache.
277
+ You can also delete `.rpc-checksum` from the output directory to clear the
278
+ cache.
247
279
 
248
- > **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.
280
+ > **Note:** The hash covers controller files matched by the `controllerPattern`
281
+ > glob. If you only change a DTO/model file that lives outside that pattern, the
282
+ > cache won't invalidate automatically. Use `analyze()` or delete
283
+ > `.rpc-checksum` in that case.
249
284
 
250
285
  ## How It Works
251
286
 
@@ -285,21 +320,40 @@ export class ApiClient {
285
320
  get users() {
286
321
  return {
287
322
  create: async <Result = User>(
288
- options: RequestOptions<{ name: string; email: string }, undefined, undefined, undefined>
323
+ options: RequestOptions<
324
+ { name: string; email: string },
325
+ undefined,
326
+ undefined,
327
+ undefined
328
+ >,
289
329
  ) => {
290
- return this.request<Result>('POST', `/api/v1/users/`, options)
330
+ return this.request<Result>("POST", `/api/v1/users/`, options);
291
331
  },
292
332
  list: async <Result = User[]>(
293
- options?: RequestOptions<undefined, { page: number; limit: number }, undefined, undefined>
333
+ options?: RequestOptions<
334
+ undefined,
335
+ { page: number; limit: number },
336
+ undefined,
337
+ undefined
338
+ >,
294
339
  ) => {
295
- return this.request<Result>('GET', `/api/v1/users/`, options)
340
+ return this.request<Result>("GET", `/api/v1/users/`, options);
296
341
  },
297
342
  getById: async <Result = User>(
298
- options: RequestOptions<undefined, { id: string }, undefined, undefined>
343
+ options: RequestOptions<
344
+ undefined,
345
+ { id: string },
346
+ undefined,
347
+ undefined
348
+ >,
299
349
  ) => {
300
- return this.request<Result>('GET', `/api/v1/users/:id`, options)
301
- }
302
- }
350
+ return this.request<Result>(
351
+ "GET",
352
+ `/api/v1/users/:id`,
353
+ options,
354
+ );
355
+ },
356
+ };
303
357
  }
304
358
  }
305
359
 
@@ -308,24 +362,29 @@ export type RequestOptions<
308
362
  TParams = undefined,
309
363
  TQuery = undefined,
310
364
  TBody = undefined,
311
- THeaders = undefined
312
- > = (TParams extends undefined ? object : { params: TParams }) &
313
- (TQuery extends undefined ? object : { query: TQuery }) &
314
- (TBody extends undefined ? object : { body: TBody }) &
315
- (THeaders extends undefined ? object : { headers: THeaders })
365
+ THeaders = undefined,
366
+ > =
367
+ & (TParams extends undefined ? object : { params: TParams })
368
+ & (TQuery extends undefined ? object : { query: TQuery })
369
+ & (TBody extends undefined ? object : { body: TBody })
370
+ & (THeaders extends undefined ? object : { headers: THeaders });
316
371
  ```
317
372
 
318
373
  ## Plugin Lifecycle
319
374
 
320
- The plugin automatically generates files when your HonestJS application starts up (if `generateOnInit` is true). On
321
- subsequent startups, the hash-based cache will skip regeneration if controller files haven't changed.
375
+ The plugin automatically generates files when your HonestJS application starts
376
+ up (if `generateOnInit` is true). On subsequent startups, the hash-based cache
377
+ will skip regeneration if controller files haven't changed.
322
378
 
323
379
  You can also manually trigger generation:
324
380
 
325
381
  ```typescript
326
- const rpcPlugin = new RPCPlugin()
327
- await rpcPlugin.analyze() // Force regeneration (bypasses cache)
328
- await rpcPlugin.analyze(false) // Respect cache
382
+ const rpcPlugin = new RPCPlugin();
383
+ await rpcPlugin.analyze({ force: true }); // Force regeneration (bypasses cache)
384
+ await rpcPlugin.analyze({ force: false }); // Respect cache
385
+
386
+ // Analyze-only mode (no files generated, diagnostics still emitted)
387
+ await rpcPlugin.analyze({ force: true, dryRun: true });
329
388
  ```
330
389
 
331
390
  ## Advanced Usage
@@ -336,9 +395,9 @@ If your controllers follow a different file structure:
336
395
 
337
396
  ```typescript
338
397
  new RPCPlugin({
339
- controllerPattern: 'src/controllers/**/*.controller.ts',
340
- outputDir: './src/generated/api'
341
- })
398
+ controllerPattern: "src/controllers/**/*.controller.ts",
399
+ outputDir: "./src/generated/api",
400
+ });
342
401
  ```
343
402
 
344
403
  ### Manual Generation Control
@@ -347,11 +406,11 @@ Disable automatic generation and control when files are generated:
347
406
 
348
407
  ```typescript
349
408
  const rpcPlugin = new RPCPlugin({
350
- generateOnInit: false
351
- })
409
+ generateOnInit: false,
410
+ });
352
411
 
353
412
  // Later in your code
354
- await rpcPlugin.analyze()
413
+ await rpcPlugin.analyze();
355
414
  ```
356
415
 
357
416
  ## Integration with HonestJS
@@ -361,32 +420,32 @@ await rpcPlugin.analyze()
361
420
  Here's how your controllers should be structured for optimal RPC generation:
362
421
 
363
422
  ```typescript
364
- import { Controller, Post, Get, Body, Param, Query } from 'honestjs'
423
+ import { Body, Controller, Get, Param, Post, Query } from "honestjs";
365
424
 
366
425
  interface CreateUserDto {
367
- name: string
368
- email: string
426
+ name: string;
427
+ email: string;
369
428
  }
370
429
 
371
430
  interface ListUsersQuery {
372
- page?: number
373
- limit?: number
431
+ page?: number;
432
+ limit?: number;
374
433
  }
375
434
 
376
- @Controller('/users')
435
+ @Controller("/users")
377
436
  export class UsersController {
378
- @Post('/')
437
+ @Post("/")
379
438
  async create(@Body() createUserDto: CreateUserDto): Promise<User> {
380
439
  // Implementation
381
440
  }
382
441
 
383
- @Get('/')
442
+ @Get("/")
384
443
  async list(@Query() query: ListUsersQuery): Promise<User[]> {
385
444
  // Implementation
386
445
  }
387
446
 
388
- @Get('/:id')
389
- async getById(@Param('id') id: string): Promise<User> {
447
+ @Get("/:id")
448
+ async getById(@Param("id") id: string): Promise<User> {
390
449
  // Implementation
391
450
  }
392
451
  }
@@ -397,13 +456,13 @@ export class UsersController {
397
456
  Ensure your controllers are properly registered in modules:
398
457
 
399
458
  ```typescript
400
- import { Module } from 'honestjs'
401
- import { UsersController } from './users.controller'
402
- import { UsersService } from './users.service'
459
+ import { Module } from "honestjs";
460
+ import { UsersController } from "./users.controller";
461
+ import { UsersService } from "./users.service";
403
462
 
404
463
  @Module({
405
464
  controllers: [UsersController],
406
- providers: [UsersService]
465
+ services: [UsersService],
407
466
  })
408
467
  export class UsersModule {}
409
468
  ```
@@ -415,13 +474,13 @@ The generated client includes comprehensive error handling:
415
474
  ```typescript
416
475
  try {
417
476
  const user = await apiClient.users.create({
418
- body: { name: 'John', email: 'john@example.com' }
419
- })
477
+ body: { name: "John", email: "john@example.com" },
478
+ });
420
479
  } catch (error) {
421
480
  if (error instanceof ApiError) {
422
- console.error(`API Error ${error.statusCode}: ${error.message}`)
481
+ console.error(`API Error ${error.statusCode}: ${error.message}`);
423
482
  } else {
424
- console.error('Unexpected error:', error)
483
+ console.error("Unexpected error:", error);
425
484
  }
426
485
  }
427
486
  ```