@honestjs/rpc-plugin 1.0.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 ADDED
@@ -0,0 +1,928 @@
1
+ // src/rpc.plugin.ts
2
+ import fs2 from "fs";
3
+ import path2 from "path";
4
+
5
+ // src/constants/defaults.ts
6
+ var DEFAULT_OPTIONS = {
7
+ controllerPattern: "src/modules/*/*.controller.ts",
8
+ tsConfigPath: "tsconfig.json",
9
+ outputDir: "./generated/rpc",
10
+ generateOnInit: true
11
+ };
12
+ var LOG_PREFIX = "[ RPCPlugin ]";
13
+ var BUILTIN_UTILITY_TYPES = /* @__PURE__ */ new Set([
14
+ "Partial",
15
+ "Required",
16
+ "Readonly",
17
+ "Pick",
18
+ "Omit",
19
+ "Record",
20
+ "Exclude",
21
+ "Extract",
22
+ "ReturnType",
23
+ "InstanceType"
24
+ ]);
25
+ var BUILTIN_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "any", "void", "unknown"]);
26
+ var GENERIC_TYPES = /* @__PURE__ */ new Set(["Array", "Promise", "Partial"]);
27
+
28
+ // src/services/client-generator.service.ts
29
+ import fs from "fs/promises";
30
+ import path from "path";
31
+
32
+ // src/utils/path-utils.ts
33
+ function buildFullPath(basePath, parameters) {
34
+ if (!basePath || typeof basePath !== "string") return "/";
35
+ let path3 = basePath;
36
+ if (parameters && Array.isArray(parameters)) {
37
+ for (const param of parameters) {
38
+ if (param.data && typeof param.data === "string" && param.data.startsWith(":")) {
39
+ const paramName = param.data.slice(1);
40
+ path3 = path3.replace(`:${paramName}`, `\${${paramName}}`);
41
+ }
42
+ }
43
+ }
44
+ return path3;
45
+ }
46
+ function buildFullApiPath(route) {
47
+ const prefix = route.prefix || "";
48
+ const version = route.version || "";
49
+ const routePath = route.route || "";
50
+ const path3 = route.path || "";
51
+ let fullPath = "";
52
+ if (prefix && prefix !== "/") {
53
+ fullPath += prefix.replace(/^\/+|\/+$/g, "");
54
+ }
55
+ if (version && version !== "/") {
56
+ fullPath += `/${version.replace(/^\/+|\/+$/g, "")}`;
57
+ }
58
+ if (routePath && routePath !== "/") {
59
+ fullPath += `/${routePath.replace(/^\/+|\/+$/g, "")}`;
60
+ }
61
+ if (path3 && path3 !== "/") {
62
+ fullPath += `/${path3.replace(/^\/+|\/+$/g, "")}`;
63
+ } else if (path3 === "/") {
64
+ fullPath += "/";
65
+ }
66
+ return fullPath || "/";
67
+ }
68
+
69
+ // src/utils/string-utils.ts
70
+ function safeToString(value) {
71
+ if (typeof value === "string") return value;
72
+ if (typeof value === "symbol") return value.description || "Symbol";
73
+ return String(value);
74
+ }
75
+ function camelCase(str) {
76
+ return str.charAt(0).toLowerCase() + str.slice(1);
77
+ }
78
+
79
+ // src/services/client-generator.service.ts
80
+ var ClientGeneratorService = class {
81
+ constructor(outputDir) {
82
+ this.outputDir = outputDir;
83
+ }
84
+ /**
85
+ * Generates the TypeScript RPC client
86
+ */
87
+ async generateClient(routes, schemas) {
88
+ await fs.mkdir(this.outputDir, { recursive: true });
89
+ await this.generateClientFile(routes, schemas);
90
+ const generatedInfo = {
91
+ clientFile: path.join(this.outputDir, "client.ts"),
92
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
93
+ };
94
+ return generatedInfo;
95
+ }
96
+ /**
97
+ * Generates the main client file with types included
98
+ */
99
+ async generateClientFile(routes, schemas) {
100
+ const clientContent = this.generateClientContent(routes, schemas);
101
+ const clientPath = path.join(this.outputDir, "client.ts");
102
+ await fs.writeFile(clientPath, clientContent, "utf-8");
103
+ }
104
+ /**
105
+ * Generates the client TypeScript content with types included
106
+ */
107
+ generateClientContent(routes, schemas) {
108
+ const controllerGroups = this.groupRoutesByController(routes);
109
+ return `// ============================================================================
110
+ // TYPES SECTION
111
+ // ============================================================================
112
+
113
+ /**
114
+ * API Response wrapper
115
+ */
116
+ export interface ApiResponse<T = any> {
117
+ data: T
118
+ message?: string
119
+ success: boolean
120
+ }
121
+
122
+ /**
123
+ * API Error class
124
+ */
125
+ export class ApiError extends Error {
126
+ constructor(
127
+ public statusCode: number,
128
+ message: string
129
+ ) {
130
+ super(message)
131
+ this.name = 'ApiError'
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Clean separation of concerns for request options
137
+ */
138
+ export type RequestOptions<
139
+ TParams = undefined,
140
+ TQuery = undefined,
141
+ TBody = undefined,
142
+ THeaders = undefined
143
+ > = (TParams extends undefined ? object : { params: TParams }) &
144
+ (TQuery extends undefined ? object : { query: TQuery }) &
145
+ (TBody extends undefined ? object : { body: TBody }) &
146
+ (THeaders extends undefined ? object : { headers: THeaders })
147
+
148
+ // Generated DTOs and types from integrated Schema Generation
149
+ ${this.generateSchemaTypes(schemas)}
150
+
151
+ // ============================================================================
152
+ // CLIENT SECTION
153
+ // ============================================================================
154
+
155
+ /**
156
+ * Generated RPC Client
157
+ *
158
+ * This class provides a type-safe HTTP client for interacting with your API endpoints.
159
+ * It's automatically generated by the RPCPlugin based on your controller definitions.
160
+ *
161
+ * @example
162
+ * \`\`\`typescript
163
+ * const apiClient = new ApiClient('http://localhost:3000')
164
+ *
165
+ * // Make a request to get users
166
+ * const response = await apiClient.users.getUsers()
167
+ *
168
+ * // Make a request with parameters
169
+ * const user = await apiClient.users.getUser({ params: { id: '123' } })
170
+ *
171
+ * // Make a request with body data
172
+ * const newUser = await apiClient.users.createUser({
173
+ * body: { name: 'John', email: 'john@example.com' }
174
+ * })
175
+ * \`\`\`
176
+ *
177
+ * @generated This class is auto-generated by RPCPlugin
178
+ */
179
+ export class ApiClient {
180
+ private baseUrl: string
181
+ private defaultHeaders: Record<string, string>
182
+
183
+ constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {
184
+ this.baseUrl = baseUrl.replace(/\\/$/, '')
185
+ this.defaultHeaders = {
186
+ 'Content-Type': 'application/json',
187
+ ...defaultHeaders
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Set default headers for all requests
193
+ */
194
+ setDefaultHeaders(headers: Record<string, string>): this {
195
+ this.defaultHeaders = { ...this.defaultHeaders, ...headers }
196
+ return this
197
+ }
198
+
199
+ /**
200
+ * Set default authorization header
201
+ */
202
+ setDefaultAuth(token: string): this {
203
+ this.defaultHeaders.Authorization = \`Bearer \${token}\`
204
+ return this
205
+ }
206
+
207
+ /**
208
+ * Make an HTTP request with flexible options
209
+ */
210
+ private async request<T>(
211
+ method: string,
212
+ path: string,
213
+ options: RequestOptions<any, any, any, any> = {}
214
+ ): Promise<ApiResponse<T>> {
215
+ const { params, query, body, headers = {} } = options as any
216
+
217
+ // Build the final URL with path parameters
218
+ let finalPath = path
219
+ if (params) {
220
+ Object.entries(params).forEach(([key, value]) => {
221
+ finalPath = finalPath.replace(\`:\${key}\`, String(value))
222
+ })
223
+ }
224
+
225
+ const url = new URL(finalPath, this.baseUrl)
226
+
227
+ // Add query parameters
228
+ if (query) {
229
+ Object.entries(query).forEach(([key, value]) => {
230
+ if (value !== undefined && value !== null) {
231
+ url.searchParams.append(key, String(value))
232
+ }
233
+ })
234
+ }
235
+
236
+ // Merge default headers with request-specific headers
237
+ const finalHeaders = { ...this.defaultHeaders, ...headers }
238
+
239
+ const requestOptions: RequestInit = {
240
+ method,
241
+ headers: finalHeaders,
242
+ }
243
+
244
+ if (body && method !== 'GET') {
245
+ requestOptions.body = JSON.stringify(body)
246
+ }
247
+
248
+ try {
249
+ const response = await fetch(url.toString(), requestOptions)
250
+ const responseData = await response.json()
251
+
252
+ if (!response.ok) {
253
+ throw new ApiError(response.status, responseData.message || 'Request failed')
254
+ }
255
+
256
+ return responseData
257
+ } catch (error) {
258
+ if (error instanceof ApiError) {
259
+ throw error
260
+ }
261
+ throw new ApiError(0, error instanceof Error ? error.message : 'Network error')
262
+ }
263
+ }
264
+
265
+ ${this.generateControllerMethods(controllerGroups)}
266
+ }
267
+ `;
268
+ }
269
+ /**
270
+ * Generates controller methods for the client
271
+ */
272
+ generateControllerMethods(controllerGroups) {
273
+ let methods = "";
274
+ for (const [controllerName, routes] of controllerGroups) {
275
+ const className = controllerName.replace(/Controller$/, "");
276
+ methods += `
277
+ // ${className} Controller
278
+ `;
279
+ methods += ` get ${camelCase(className)}() {
280
+ `;
281
+ methods += ` return {
282
+ `;
283
+ for (const route of routes) {
284
+ const methodName = camelCase(safeToString(route.handler));
285
+ const httpMethod = safeToString(route.method).toLowerCase();
286
+ const { pathParams, queryParams, bodyParams } = this.analyzeRouteParameters(route);
287
+ const hasRequiredParams = pathParams.length > 0 || queryParams.some((p) => p.required) || bodyParams.length > 0 && httpMethod !== "get";
288
+ methods += ` ${methodName}: async (options${hasRequiredParams ? "" : "?"}: RequestOptions<`;
289
+ if (pathParams.length > 0) {
290
+ const pathParamTypes = pathParams.map((p) => {
291
+ const paramName = p.name;
292
+ const paramType = p.type || "any";
293
+ return `${paramName}: ${paramType}`;
294
+ });
295
+ methods += `{ ${pathParamTypes.join(", ")} }`;
296
+ } else {
297
+ methods += "undefined";
298
+ }
299
+ methods += ", ";
300
+ if (queryParams.length > 0) {
301
+ const queryParamTypes = queryParams.map((p) => {
302
+ const paramName = p.name;
303
+ const paramType = p.type || "any";
304
+ return `${paramName}: ${paramType}`;
305
+ });
306
+ methods += `{ ${queryParamTypes.join(", ")} }`;
307
+ } else {
308
+ methods += "undefined";
309
+ }
310
+ methods += ", ";
311
+ if (bodyParams.length > 0) {
312
+ const bodyParamTypes = bodyParams.map((p) => {
313
+ const paramType = p.type || "any";
314
+ return paramType;
315
+ });
316
+ methods += bodyParamTypes[0] || "any";
317
+ } else {
318
+ methods += "undefined";
319
+ }
320
+ methods += ", ";
321
+ methods += "undefined";
322
+ const returnType = this.extractReturnType(route.returns);
323
+ methods += `>): Promise<ApiResponse<${returnType}>> => {
324
+ `;
325
+ let requestPath = buildFullApiPath(route);
326
+ if (pathParams.length > 0) {
327
+ for (const pathParam of pathParams) {
328
+ const paramName = pathParam.name;
329
+ const placeholder = `:${String(pathParam.data)}`;
330
+ requestPath = requestPath.replace(placeholder, `:${paramName}`);
331
+ }
332
+ }
333
+ methods += ` return this.request<${returnType}>('${httpMethod.toUpperCase()}', \`${requestPath}\`, options)
334
+ `;
335
+ methods += ` },
336
+ `;
337
+ }
338
+ methods += ` }
339
+ `;
340
+ methods += ` }
341
+ `;
342
+ }
343
+ return methods;
344
+ }
345
+ /**
346
+ * Extracts the proper return type from route analysis
347
+ */
348
+ extractReturnType(returns) {
349
+ if (!returns) return "any";
350
+ const promiseMatch = returns.match(/Promise<(.+)>/);
351
+ if (promiseMatch) {
352
+ return promiseMatch[1];
353
+ }
354
+ return returns;
355
+ }
356
+ /**
357
+ * Generates schema types from integrated schema generation
358
+ */
359
+ generateSchemaTypes(schemas) {
360
+ if (schemas.length === 0) {
361
+ return "// No schemas available from integrated Schema Generation\n";
362
+ }
363
+ let content = "// Schema types from integrated Schema Generation\n";
364
+ for (const schemaInfo of schemas) {
365
+ if (schemaInfo.typescriptType) {
366
+ content += `${schemaInfo.typescriptType}
367
+
368
+ `;
369
+ }
370
+ }
371
+ return content;
372
+ }
373
+ /**
374
+ * Groups routes by controller for better organization
375
+ */
376
+ groupRoutesByController(routes) {
377
+ const groups = /* @__PURE__ */ new Map();
378
+ for (const route of routes) {
379
+ const controller = safeToString(route.controller);
380
+ if (!groups.has(controller)) {
381
+ groups.set(controller, []);
382
+ }
383
+ groups.get(controller).push(route);
384
+ }
385
+ return groups;
386
+ }
387
+ /**
388
+ * Analyzes route parameters to determine their types and usage
389
+ */
390
+ analyzeRouteParameters(route) {
391
+ const parameters = route.parameters || [];
392
+ const method = String(route.method || "").toLowerCase();
393
+ const isInPath = (p) => {
394
+ const pathSegment = p.data;
395
+ return !!pathSegment && typeof pathSegment === "string" && route.path.includes(`:${pathSegment}`);
396
+ };
397
+ const pathParams = parameters.filter((p) => isInPath(p)).map((p) => ({ ...p, required: true }));
398
+ const rawBody = parameters.filter((p) => !isInPath(p) && method !== "get");
399
+ const bodyParams = rawBody.map((p) => ({
400
+ ...p,
401
+ required: true
402
+ }));
403
+ const queryParams = parameters.filter((p) => !isInPath(p) && method === "get").map((p) => ({
404
+ ...p,
405
+ required: p.required === true
406
+ // default false if not provided
407
+ }));
408
+ return { pathParams, queryParams, bodyParams };
409
+ }
410
+ };
411
+
412
+ // src/services/route-analyzer.service.ts
413
+ import { RouteRegistry } from "honestjs";
414
+ import { Project } from "ts-morph";
415
+ var RouteAnalyzerService = class {
416
+ constructor(controllerPattern, tsConfigPath) {
417
+ this.controllerPattern = controllerPattern;
418
+ this.tsConfigPath = tsConfigPath;
419
+ }
420
+ // Track projects for cleanup
421
+ projects = [];
422
+ /**
423
+ * Analyzes controller methods to extract type information
424
+ */
425
+ async analyzeControllerMethods() {
426
+ const routes = RouteRegistry.getRoutes();
427
+ if (!routes?.length) {
428
+ return [];
429
+ }
430
+ const project = this.createProject();
431
+ const controllers = this.findControllerClasses(project);
432
+ if (controllers.size === 0) {
433
+ return [];
434
+ }
435
+ return this.processRoutes(routes, controllers);
436
+ }
437
+ /**
438
+ * Creates a new ts-morph project
439
+ */
440
+ createProject() {
441
+ const project = new Project({
442
+ tsConfigFilePath: this.tsConfigPath
443
+ });
444
+ project.addSourceFilesAtPaths([this.controllerPattern]);
445
+ this.projects.push(project);
446
+ return project;
447
+ }
448
+ /**
449
+ * Cleanup resources to prevent memory leaks
450
+ */
451
+ dispose() {
452
+ this.projects.forEach((project) => {
453
+ project.getSourceFiles().forEach((file) => project.removeSourceFile(file));
454
+ });
455
+ this.projects = [];
456
+ }
457
+ /**
458
+ * Finds controller classes in the project
459
+ */
460
+ findControllerClasses(project) {
461
+ const controllers = /* @__PURE__ */ new Map();
462
+ const files = project.getSourceFiles();
463
+ for (const sourceFile of files) {
464
+ const classes = sourceFile.getClasses();
465
+ for (const classDeclaration of classes) {
466
+ const className = classDeclaration.getName();
467
+ if (className?.endsWith("Controller")) {
468
+ controllers.set(className, classDeclaration);
469
+ }
470
+ }
471
+ }
472
+ return controllers;
473
+ }
474
+ /**
475
+ * Processes all routes and extracts type information
476
+ */
477
+ processRoutes(routes, controllers) {
478
+ const analyzedRoutes = [];
479
+ const errors = [];
480
+ for (const route of routes) {
481
+ try {
482
+ const extendedRoute = this.createExtendedRoute(route, controllers);
483
+ analyzedRoutes.push(extendedRoute);
484
+ } catch (routeError) {
485
+ const error = routeError instanceof Error ? routeError : new Error(String(routeError));
486
+ errors.push(error);
487
+ console.error(
488
+ `Error processing route ${safeToString(route.controller)}.${safeToString(route.handler)}:`,
489
+ routeError
490
+ );
491
+ }
492
+ }
493
+ if (errors.length > 0) {
494
+ throw new Error(`Failed to process ${errors.length} routes: ${errors.map((e) => e.message).join(", ")}`);
495
+ }
496
+ return analyzedRoutes;
497
+ }
498
+ /**
499
+ * Creates an extended route with type information
500
+ */
501
+ createExtendedRoute(route, controllers) {
502
+ const controllerName = safeToString(route.controller);
503
+ const handlerName = safeToString(route.handler);
504
+ const controllerClass = controllers.get(controllerName);
505
+ let returns;
506
+ let parameters;
507
+ if (controllerClass) {
508
+ const handlerMethod = controllerClass.getMethods().find((method) => method.getName() === handlerName);
509
+ if (handlerMethod) {
510
+ returns = this.getReturnType(handlerMethod);
511
+ parameters = this.getParametersWithTypes(handlerMethod, route.parameters || []);
512
+ }
513
+ }
514
+ return {
515
+ controller: controllerName,
516
+ handler: handlerName,
517
+ method: safeToString(route.method).toUpperCase(),
518
+ prefix: route.prefix,
519
+ version: route.version,
520
+ route: route.route,
521
+ path: route.path,
522
+ fullPath: buildFullPath(route.path, route.parameters),
523
+ parameters,
524
+ returns
525
+ };
526
+ }
527
+ /**
528
+ * Gets the return type of a method
529
+ */
530
+ getReturnType(method) {
531
+ const type = method.getReturnType();
532
+ const typeText = type.getText(method);
533
+ const aliasSymbol = type.getAliasSymbol();
534
+ if (aliasSymbol) {
535
+ return aliasSymbol.getName();
536
+ }
537
+ return typeText.replace(/import\(".*?"\)\./g, "");
538
+ }
539
+ /**
540
+ * Gets parameters with their types
541
+ */
542
+ getParametersWithTypes(method, parameters) {
543
+ const result = [];
544
+ const declaredParams = method.getParameters();
545
+ const sortedParams = [...parameters].sort((a, b) => a.index - b.index);
546
+ for (const param of sortedParams) {
547
+ const index = param.index;
548
+ if (index < declaredParams.length) {
549
+ const declaredParam = declaredParams[index];
550
+ const paramName = declaredParam.getName();
551
+ const paramType = declaredParam.getType().getText().replace(/import\(".*?"\)\./g, "");
552
+ result.push({
553
+ index,
554
+ name: paramName,
555
+ type: paramType,
556
+ required: true,
557
+ data: param.data,
558
+ factory: param.factory,
559
+ metatype: param.metatype
560
+ });
561
+ } else {
562
+ result.push({
563
+ index,
564
+ name: `param${index}`,
565
+ type: param.metatype?.name || "unknown",
566
+ required: true,
567
+ data: param.data,
568
+ factory: param.factory,
569
+ metatype: param.metatype
570
+ });
571
+ }
572
+ }
573
+ return result;
574
+ }
575
+ };
576
+
577
+ // src/services/schema-generator.service.ts
578
+ import { createGenerator } from "ts-json-schema-generator";
579
+ import { Project as Project2 } from "ts-morph";
580
+
581
+ // src/utils/schema-utils.ts
582
+ function mapJsonSchemaTypeToTypeScript(schema) {
583
+ const type = schema.type;
584
+ switch (type) {
585
+ case "string":
586
+ if (schema.enum && Array.isArray(schema.enum)) {
587
+ return `'${schema.enum.join("' | '")}'`;
588
+ }
589
+ return "string";
590
+ case "number":
591
+ case "integer":
592
+ return "number";
593
+ case "boolean":
594
+ return "boolean";
595
+ case "array": {
596
+ const itemType = mapJsonSchemaTypeToTypeScript(schema.items || {});
597
+ return `${itemType}[]`;
598
+ }
599
+ case "object":
600
+ return "Record<string, any>";
601
+ default:
602
+ return "any";
603
+ }
604
+ }
605
+ function generateTypeScriptInterface(typeName, schema) {
606
+ try {
607
+ const typeDefinition = schema.definitions?.[typeName];
608
+ if (!typeDefinition) {
609
+ return `export interface ${typeName} {
610
+ // No schema definition found
611
+ }`;
612
+ }
613
+ const properties = typeDefinition.properties || {};
614
+ const required = typeDefinition.required || [];
615
+ let interfaceCode = `export interface ${typeName} {
616
+ `;
617
+ for (const [propName, propSchema] of Object.entries(properties)) {
618
+ const isRequired = required.includes(propName);
619
+ const type = mapJsonSchemaTypeToTypeScript(propSchema);
620
+ const optional = isRequired ? "" : "?";
621
+ interfaceCode += ` ${propName}${optional}: ${type}
622
+ `;
623
+ }
624
+ interfaceCode += "}";
625
+ return interfaceCode;
626
+ } catch (error) {
627
+ console.error(`Failed to generate TypeScript interface for ${typeName}:`, error);
628
+ return `export interface ${typeName} {
629
+ // Failed to generate interface
630
+ }`;
631
+ }
632
+ }
633
+
634
+ // src/utils/type-utils.ts
635
+ function extractNamedType(type) {
636
+ const symbol = type.getAliasSymbol() || type.getSymbol();
637
+ if (!symbol) return null;
638
+ const name = symbol.getName();
639
+ if (GENERIC_TYPES.has(name)) {
640
+ const inner = type.getAliasTypeArguments()?.[0] || type.getTypeArguments()?.[0];
641
+ return inner ? extractNamedType(inner) : null;
642
+ }
643
+ if (BUILTIN_TYPES.has(name)) return null;
644
+ return name;
645
+ }
646
+ function generateTypeImports(routes) {
647
+ const types = /* @__PURE__ */ new Set();
648
+ for (const route of routes) {
649
+ if (route.parameters) {
650
+ for (const param of route.parameters) {
651
+ if (param.type && !["string", "number", "boolean"].includes(param.type)) {
652
+ const typeMatch = param.type.match(/(\w+)(?:<.*>)?/);
653
+ if (typeMatch) {
654
+ const typeName = typeMatch[1];
655
+ if (!BUILTIN_UTILITY_TYPES.has(typeName)) {
656
+ types.add(typeName);
657
+ }
658
+ }
659
+ }
660
+ }
661
+ }
662
+ if (route.returns) {
663
+ const returnType = route.returns.replace(/Promise<(.+)>/, "$1");
664
+ const baseType = returnType.replace(/\[\]$/, "");
665
+ if (!["string", "number", "boolean", "any", "void", "unknown"].includes(baseType)) {
666
+ types.add(baseType);
667
+ }
668
+ }
669
+ }
670
+ return Array.from(types).join(", ");
671
+ }
672
+
673
+ // src/services/schema-generator.service.ts
674
+ var SchemaGeneratorService = class {
675
+ constructor(controllerPattern, tsConfigPath) {
676
+ this.controllerPattern = controllerPattern;
677
+ this.tsConfigPath = tsConfigPath;
678
+ }
679
+ // Track projects for cleanup
680
+ projects = [];
681
+ /**
682
+ * Generates JSON schemas from types used in controllers
683
+ */
684
+ async generateSchemas() {
685
+ const project = this.createProject();
686
+ const sourceFiles = project.getSourceFiles(this.controllerPattern);
687
+ const collectedTypes = this.collectTypesFromControllers(sourceFiles);
688
+ return this.processTypes(collectedTypes);
689
+ }
690
+ /**
691
+ * Creates a new ts-morph project
692
+ */
693
+ createProject() {
694
+ const project = new Project2({
695
+ tsConfigFilePath: this.tsConfigPath
696
+ });
697
+ this.projects.push(project);
698
+ return project;
699
+ }
700
+ /**
701
+ * Cleanup resources to prevent memory leaks
702
+ */
703
+ dispose() {
704
+ this.projects.forEach((project) => {
705
+ project.getSourceFiles().forEach((file) => project.removeSourceFile(file));
706
+ });
707
+ this.projects = [];
708
+ }
709
+ /**
710
+ * Collects types from controller files
711
+ */
712
+ collectTypesFromControllers(sourceFiles) {
713
+ const collectedTypes = /* @__PURE__ */ new Set();
714
+ for (const file of sourceFiles) {
715
+ for (const cls of file.getClasses()) {
716
+ for (const method of cls.getMethods()) {
717
+ this.collectTypesFromMethod(method, collectedTypes);
718
+ }
719
+ }
720
+ }
721
+ return collectedTypes;
722
+ }
723
+ /**
724
+ * Collects types from a single method
725
+ */
726
+ collectTypesFromMethod(method, collectedTypes) {
727
+ for (const param of method.getParameters()) {
728
+ const type2 = extractNamedType(param.getType());
729
+ if (type2) collectedTypes.add(type2);
730
+ }
731
+ const returnType = method.getReturnType();
732
+ const innerType = returnType.getTypeArguments()[0] ?? returnType;
733
+ const type = extractNamedType(innerType);
734
+ if (type) collectedTypes.add(type);
735
+ }
736
+ /**
737
+ * Processes collected types to generate schemas
738
+ */
739
+ async processTypes(collectedTypes) {
740
+ const schemas = [];
741
+ for (const typeName of collectedTypes) {
742
+ try {
743
+ const schema = await this.generateSchemaForType(typeName);
744
+ const typescriptType = generateTypeScriptInterface(typeName, schema);
745
+ schemas.push({
746
+ type: typeName,
747
+ schema,
748
+ typescriptType
749
+ });
750
+ } catch (err) {
751
+ console.error(`Failed to generate schema for ${typeName}:`, err);
752
+ }
753
+ }
754
+ return schemas;
755
+ }
756
+ /**
757
+ * Generates schema for a specific type
758
+ */
759
+ async generateSchemaForType(typeName) {
760
+ try {
761
+ const generator = createGenerator({
762
+ path: this.controllerPattern,
763
+ tsconfig: this.tsConfigPath,
764
+ type: typeName,
765
+ skipTypeCheck: false
766
+ // Enable type checking for better error detection
767
+ });
768
+ return generator.createSchema(typeName);
769
+ } catch (error) {
770
+ console.error(`Failed to generate schema for type ${typeName}:`, error);
771
+ return {
772
+ type: "object",
773
+ properties: {},
774
+ required: []
775
+ };
776
+ }
777
+ }
778
+ };
779
+
780
+ // src/rpc.plugin.ts
781
+ var RPCPlugin = class {
782
+ controllerPattern;
783
+ tsConfigPath;
784
+ outputDir;
785
+ generateOnInit;
786
+ // Services
787
+ routeAnalyzer;
788
+ schemaGenerator;
789
+ clientGenerator;
790
+ // Internal state
791
+ analyzedRoutes = [];
792
+ analyzedSchemas = [];
793
+ generatedInfo = null;
794
+ constructor(options = {}) {
795
+ this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
796
+ this.tsConfigPath = options.tsConfigPath ?? path2.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
797
+ this.outputDir = options.outputDir ?? path2.resolve(process.cwd(), DEFAULT_OPTIONS.outputDir);
798
+ this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
799
+ this.routeAnalyzer = new RouteAnalyzerService(this.controllerPattern, this.tsConfigPath);
800
+ this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath);
801
+ this.clientGenerator = new ClientGeneratorService(this.outputDir);
802
+ this.validateConfiguration();
803
+ }
804
+ /**
805
+ * Validates the plugin configuration
806
+ */
807
+ validateConfiguration() {
808
+ const errors = [];
809
+ if (!this.controllerPattern?.trim()) {
810
+ errors.push("Controller pattern cannot be empty");
811
+ }
812
+ if (!this.tsConfigPath?.trim()) {
813
+ errors.push("TypeScript config path cannot be empty");
814
+ } else {
815
+ if (!fs2.existsSync(this.tsConfigPath)) {
816
+ errors.push(`TypeScript config file not found at: ${this.tsConfigPath}`);
817
+ }
818
+ }
819
+ if (!this.outputDir?.trim()) {
820
+ errors.push("Output directory cannot be empty");
821
+ }
822
+ if (errors.length > 0) {
823
+ throw new Error(`Configuration validation failed: ${errors.join(", ")}`);
824
+ }
825
+ this.log(
826
+ `Configuration validated: controllerPattern=${this.controllerPattern}, tsConfigPath=${this.tsConfigPath}, outputDir=${this.outputDir}`
827
+ );
828
+ }
829
+ /**
830
+ * Called after all modules are registered
831
+ */
832
+ afterModulesRegistered = async (app, hono) => {
833
+ if (this.generateOnInit) {
834
+ await this.analyzeEverything();
835
+ }
836
+ };
837
+ /**
838
+ * Main analysis method that coordinates all three components
839
+ */
840
+ async analyzeEverything() {
841
+ try {
842
+ this.log("Starting comprehensive RPC analysis...");
843
+ this.analyzedRoutes.push(...await this.routeAnalyzer.analyzeControllerMethods());
844
+ this.analyzedSchemas.push(...await this.schemaGenerator.generateSchemas());
845
+ this.generatedInfo = await this.clientGenerator.generateClient(this.analyzedRoutes, this.analyzedSchemas);
846
+ this.log(
847
+ `\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
848
+ );
849
+ } catch (error) {
850
+ this.logError("Error during RPC analysis:", error);
851
+ this.dispose();
852
+ throw error;
853
+ }
854
+ }
855
+ /**
856
+ * Manually trigger analysis (useful for testing or re-generation)
857
+ */
858
+ async analyze() {
859
+ await this.analyzeEverything();
860
+ }
861
+ /**
862
+ * Get the analyzed routes
863
+ */
864
+ getRoutes() {
865
+ return this.analyzedRoutes;
866
+ }
867
+ /**
868
+ * Get the analyzed schemas
869
+ */
870
+ getSchemas() {
871
+ return this.analyzedSchemas;
872
+ }
873
+ /**
874
+ * Get the generation info
875
+ */
876
+ getGenerationInfo() {
877
+ return this.generatedInfo;
878
+ }
879
+ /**
880
+ * Cleanup resources to prevent memory leaks
881
+ */
882
+ dispose() {
883
+ this.routeAnalyzer.dispose();
884
+ this.schemaGenerator.dispose();
885
+ this.log("Resources cleaned up");
886
+ }
887
+ /**
888
+ * Plugin lifecycle cleanup
889
+ */
890
+ onDestroy() {
891
+ this.dispose();
892
+ }
893
+ // ============================================================================
894
+ // LOGGING UTILITIES
895
+ // ============================================================================
896
+ /**
897
+ * Logs a message with the plugin prefix
898
+ */
899
+ log(message) {
900
+ console.log(`${LOG_PREFIX} ${message}`);
901
+ }
902
+ /**
903
+ * Logs an error with the plugin prefix
904
+ */
905
+ logError(message, error) {
906
+ console.error(`${LOG_PREFIX} ${message}`, error || "");
907
+ }
908
+ };
909
+ export {
910
+ BUILTIN_TYPES,
911
+ BUILTIN_UTILITY_TYPES,
912
+ ClientGeneratorService,
913
+ DEFAULT_OPTIONS,
914
+ GENERIC_TYPES,
915
+ LOG_PREFIX,
916
+ RPCPlugin,
917
+ RouteAnalyzerService,
918
+ SchemaGeneratorService,
919
+ buildFullApiPath,
920
+ buildFullPath,
921
+ camelCase,
922
+ extractNamedType,
923
+ generateTypeImports,
924
+ generateTypeScriptInterface,
925
+ mapJsonSchemaTypeToTypeScript,
926
+ safeToString
927
+ };
928
+ //# sourceMappingURL=index.mjs.map