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