@honestjs/rpc-plugin 1.6.0 → 1.6.1

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