@honestjs/rpc-plugin 1.4.1 → 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 +203 -129
- package/dist/index.d.mts +112 -21
- package/dist/index.d.ts +112 -21
- package/dist/index.js +339 -123
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +333 -120
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,19 +32,22 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
BUILTIN_TYPES: () => BUILTIN_TYPES,
|
|
34
34
|
BUILTIN_UTILITY_TYPES: () => BUILTIN_UTILITY_TYPES,
|
|
35
|
-
ClientGeneratorService: () => ClientGeneratorService,
|
|
36
35
|
DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
|
|
37
36
|
GENERIC_TYPES: () => GENERIC_TYPES,
|
|
38
37
|
LOG_PREFIX: () => LOG_PREFIX,
|
|
39
38
|
RPCPlugin: () => RPCPlugin,
|
|
39
|
+
RPC_ARTIFACT_VERSION: () => RPC_ARTIFACT_VERSION,
|
|
40
40
|
RouteAnalyzerService: () => RouteAnalyzerService,
|
|
41
41
|
SchemaGeneratorService: () => SchemaGeneratorService,
|
|
42
|
+
TypeScriptClientGenerator: () => TypeScriptClientGenerator,
|
|
43
|
+
assertRpcArtifact: () => assertRpcArtifact,
|
|
42
44
|
buildFullApiPath: () => buildFullApiPath,
|
|
43
45
|
buildFullPath: () => buildFullPath,
|
|
44
46
|
camelCase: () => camelCase,
|
|
45
47
|
computeHash: () => computeHash,
|
|
46
48
|
extractNamedType: () => extractNamedType,
|
|
47
49
|
generateTypeScriptInterface: () => generateTypeScriptInterface,
|
|
50
|
+
isRpcArtifact: () => isRpcArtifact,
|
|
48
51
|
mapJsonSchemaTypeToTypeScript: () => mapJsonSchemaTypeToTypeScript,
|
|
49
52
|
readChecksum: () => readChecksum,
|
|
50
53
|
safeToString: () => safeToString,
|
|
@@ -63,6 +66,9 @@ var DEFAULT_OPTIONS = {
|
|
|
63
66
|
tsConfigPath: "tsconfig.json",
|
|
64
67
|
outputDir: "./generated/rpc",
|
|
65
68
|
generateOnInit: true,
|
|
69
|
+
mode: "best-effort",
|
|
70
|
+
logLevel: "info",
|
|
71
|
+
artifactVersion: "1",
|
|
66
72
|
context: {
|
|
67
73
|
namespace: "rpc",
|
|
68
74
|
keys: {
|
|
@@ -86,47 +92,20 @@ var BUILTIN_UTILITY_TYPES = /* @__PURE__ */ new Set([
|
|
|
86
92
|
var BUILTIN_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "any", "void", "unknown"]);
|
|
87
93
|
var GENERIC_TYPES = /* @__PURE__ */ new Set(["Array", "Promise", "Partial"]);
|
|
88
94
|
|
|
89
|
-
// src/
|
|
90
|
-
var
|
|
91
|
-
var import_fs = require("fs");
|
|
92
|
-
var import_promises = require("fs/promises");
|
|
95
|
+
// src/generators/typescript-client.generator.ts
|
|
96
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
93
97
|
var import_path = __toESM(require("path"));
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (const filePath of sorted) {
|
|
101
|
-
hasher.update((0, import_fs.readFileSync)(filePath, "utf-8"));
|
|
102
|
-
hasher.update("\0");
|
|
103
|
-
}
|
|
104
|
-
return hasher.digest("hex");
|
|
105
|
-
}
|
|
106
|
-
function readChecksum(outputDir) {
|
|
107
|
-
const checksumPath = import_path.default.join(outputDir, CHECKSUM_FILENAME);
|
|
108
|
-
if (!(0, import_fs.existsSync)(checksumPath)) return null;
|
|
109
|
-
try {
|
|
110
|
-
const raw = (0, import_fs.readFileSync)(checksumPath, "utf-8");
|
|
111
|
-
const data = JSON.parse(raw);
|
|
112
|
-
if (typeof data.hash !== "string" || !Array.isArray(data.files)) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
return data;
|
|
116
|
-
} catch {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
98
|
+
|
|
99
|
+
// src/utils/string-utils.ts
|
|
100
|
+
function safeToString(value) {
|
|
101
|
+
if (typeof value === "string") return value;
|
|
102
|
+
if (typeof value === "symbol") return value.description || "Symbol";
|
|
103
|
+
return String(value);
|
|
119
104
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const checksumPath = import_path.default.join(outputDir, CHECKSUM_FILENAME);
|
|
123
|
-
await (0, import_promises.writeFile)(checksumPath, JSON.stringify(data, null, 2), "utf-8");
|
|
105
|
+
function camelCase(str) {
|
|
106
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
124
107
|
}
|
|
125
108
|
|
|
126
|
-
// src/services/client-generator.service.ts
|
|
127
|
-
var import_promises2 = __toESM(require("fs/promises"));
|
|
128
|
-
var import_path2 = __toESM(require("path"));
|
|
129
|
-
|
|
130
109
|
// src/utils/path-utils.ts
|
|
131
110
|
function buildFullPath(basePath, parameters) {
|
|
132
111
|
if (!basePath || typeof basePath !== "string") return "/";
|
|
@@ -165,46 +144,67 @@ function buildFullApiPath(route) {
|
|
|
165
144
|
return fullPath || "/";
|
|
166
145
|
}
|
|
167
146
|
|
|
168
|
-
// src/
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
147
|
+
// src/generators/generator-utils.ts
|
|
148
|
+
function groupRoutesByController(routes) {
|
|
149
|
+
const groups = /* @__PURE__ */ new Map();
|
|
150
|
+
for (const route of routes) {
|
|
151
|
+
const controller = safeToString(route.controller);
|
|
152
|
+
if (!groups.has(controller)) {
|
|
153
|
+
groups.set(controller, []);
|
|
154
|
+
}
|
|
155
|
+
groups.get(controller).push(route);
|
|
156
|
+
}
|
|
157
|
+
return groups;
|
|
173
158
|
}
|
|
174
|
-
function
|
|
175
|
-
|
|
159
|
+
function buildNormalizedRequestPath(route) {
|
|
160
|
+
let requestPath = buildFullApiPath(route);
|
|
161
|
+
for (const parameter of route.parameters ?? []) {
|
|
162
|
+
if (parameter.decoratorType !== "param") continue;
|
|
163
|
+
const placeholder = `:${String(parameter.data ?? parameter.name)}`;
|
|
164
|
+
requestPath = requestPath.replace(placeholder, `:${parameter.name}`);
|
|
165
|
+
}
|
|
166
|
+
return requestPath;
|
|
176
167
|
}
|
|
177
168
|
|
|
178
|
-
// src/
|
|
179
|
-
var
|
|
169
|
+
// src/generators/typescript-client.generator.ts
|
|
170
|
+
var TypeScriptClientGenerator = class {
|
|
180
171
|
constructor(outputDir) {
|
|
181
172
|
this.outputDir = outputDir;
|
|
182
173
|
}
|
|
174
|
+
name = "typescript-client";
|
|
175
|
+
/**
|
|
176
|
+
* Generates the TypeScript RPC client.
|
|
177
|
+
*/
|
|
178
|
+
async generate(context) {
|
|
179
|
+
return this.generateClient(context.routes, context.schemas);
|
|
180
|
+
}
|
|
183
181
|
/**
|
|
184
|
-
* Generates the TypeScript RPC client
|
|
182
|
+
* Generates the TypeScript RPC client.
|
|
185
183
|
*/
|
|
186
184
|
async generateClient(routes, schemas) {
|
|
187
|
-
await
|
|
185
|
+
await import_promises.default.mkdir(this.outputDir, { recursive: true });
|
|
188
186
|
await this.generateClientFile(routes, schemas);
|
|
189
187
|
const generatedInfo = {
|
|
190
|
-
|
|
188
|
+
generator: this.name,
|
|
189
|
+
clientFile: import_path.default.join(this.outputDir, "client.ts"),
|
|
190
|
+
outputFiles: [import_path.default.join(this.outputDir, "client.ts")],
|
|
191
191
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
192
192
|
};
|
|
193
193
|
return generatedInfo;
|
|
194
194
|
}
|
|
195
195
|
/**
|
|
196
|
-
* Generates the main client file with types included
|
|
196
|
+
* Generates the main client file with types included.
|
|
197
197
|
*/
|
|
198
198
|
async generateClientFile(routes, schemas) {
|
|
199
199
|
const clientContent = this.generateClientContent(routes, schemas);
|
|
200
|
-
const clientPath =
|
|
201
|
-
await
|
|
200
|
+
const clientPath = import_path.default.join(this.outputDir, "client.ts");
|
|
201
|
+
await import_promises.default.writeFile(clientPath, clientContent, "utf-8");
|
|
202
202
|
}
|
|
203
203
|
/**
|
|
204
|
-
* Generates the client TypeScript content with types included
|
|
204
|
+
* Generates the client TypeScript content with types included.
|
|
205
205
|
*/
|
|
206
206
|
generateClientContent(routes, schemas) {
|
|
207
|
-
const controllerGroups =
|
|
207
|
+
const controllerGroups = groupRoutesByController(routes);
|
|
208
208
|
return `// ============================================================================
|
|
209
209
|
// TYPES SECTION
|
|
210
210
|
// ============================================================================
|
|
@@ -365,13 +365,21 @@ export class ApiClient {
|
|
|
365
365
|
return undefined as T
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
const
|
|
368
|
+
const contentType = response.headers.get('content-type') || ''
|
|
369
|
+
const isJson = contentType.includes('application/json') || contentType.includes('+json')
|
|
370
|
+
const responseData = isJson ? await response.json() : await response.text()
|
|
369
371
|
|
|
370
372
|
if (!response.ok) {
|
|
371
|
-
|
|
373
|
+
const message =
|
|
374
|
+
typeof responseData === 'object' && responseData && 'message' in (responseData as Record<string, unknown>)
|
|
375
|
+
? String((responseData as Record<string, unknown>).message)
|
|
376
|
+
: typeof responseData === 'string' && responseData.trim()
|
|
377
|
+
? responseData
|
|
378
|
+
: 'Request failed'
|
|
379
|
+
throw new ApiError(response.status, message)
|
|
372
380
|
}
|
|
373
381
|
|
|
374
|
-
return responseData
|
|
382
|
+
return responseData as T
|
|
375
383
|
} catch (error) {
|
|
376
384
|
if (error instanceof ApiError) {
|
|
377
385
|
throw error
|
|
@@ -385,7 +393,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
385
393
|
`;
|
|
386
394
|
}
|
|
387
395
|
/**
|
|
388
|
-
* Generates controller methods for the client
|
|
396
|
+
* Generates controller methods for the client.
|
|
389
397
|
*/
|
|
390
398
|
generateControllerMethods(controllerGroups) {
|
|
391
399
|
let methods = "";
|
|
@@ -440,14 +448,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
440
448
|
methods += "undefined";
|
|
441
449
|
methods += `>) => {
|
|
442
450
|
`;
|
|
443
|
-
|
|
444
|
-
if (pathParams.length > 0) {
|
|
445
|
-
for (const pathParam of pathParams) {
|
|
446
|
-
const paramName = pathParam.name;
|
|
447
|
-
const placeholder = `:${String(pathParam.data)}`;
|
|
448
|
-
requestPath = requestPath.replace(placeholder, `:${paramName}`);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
+
const requestPath = buildNormalizedRequestPath(route);
|
|
451
452
|
methods += ` return this.request<Result>('${httpMethod.toUpperCase()}', \`${requestPath}\`, options)
|
|
452
453
|
`;
|
|
453
454
|
methods += ` },
|
|
@@ -461,7 +462,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
461
462
|
return methods;
|
|
462
463
|
}
|
|
463
464
|
/**
|
|
464
|
-
* Extracts the proper return type from route analysis
|
|
465
|
+
* Extracts the proper return type from route analysis.
|
|
465
466
|
*/
|
|
466
467
|
extractReturnType(returns) {
|
|
467
468
|
if (!returns) return "any";
|
|
@@ -472,7 +473,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
472
473
|
return returns;
|
|
473
474
|
}
|
|
474
475
|
/**
|
|
475
|
-
* Generates schema types from integrated schema generation
|
|
476
|
+
* Generates schema types from integrated schema generation.
|
|
476
477
|
*/
|
|
477
478
|
generateSchemaTypes(schemas) {
|
|
478
479
|
if (schemas.length === 0) {
|
|
@@ -489,21 +490,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
489
490
|
return content;
|
|
490
491
|
}
|
|
491
492
|
/**
|
|
492
|
-
*
|
|
493
|
-
*/
|
|
494
|
-
groupRoutesByController(routes) {
|
|
495
|
-
const groups = /* @__PURE__ */ new Map();
|
|
496
|
-
for (const route of routes) {
|
|
497
|
-
const controller = safeToString(route.controller);
|
|
498
|
-
if (!groups.has(controller)) {
|
|
499
|
-
groups.set(controller, []);
|
|
500
|
-
}
|
|
501
|
-
groups.get(controller).push(route);
|
|
502
|
-
}
|
|
503
|
-
return groups;
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Analyzes route parameters to determine their types and usage
|
|
493
|
+
* Analyzes route parameters to determine their types and usage.
|
|
507
494
|
*/
|
|
508
495
|
analyzeRouteParameters(route) {
|
|
509
496
|
const parameters = route.parameters || [];
|
|
@@ -514,13 +501,79 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
514
501
|
}
|
|
515
502
|
};
|
|
516
503
|
|
|
504
|
+
// src/utils/hash-utils.ts
|
|
505
|
+
var import_crypto = require("crypto");
|
|
506
|
+
var import_fs = require("fs");
|
|
507
|
+
var import_promises2 = require("fs/promises");
|
|
508
|
+
var import_path2 = __toESM(require("path"));
|
|
509
|
+
var CHECKSUM_FILENAME = ".rpc-checksum";
|
|
510
|
+
function computeHash(filePaths) {
|
|
511
|
+
const sorted = [...filePaths].sort();
|
|
512
|
+
const hasher = (0, import_crypto.createHash)("sha256");
|
|
513
|
+
hasher.update(`files:${sorted.length}
|
|
514
|
+
`);
|
|
515
|
+
for (const filePath of sorted) {
|
|
516
|
+
hasher.update((0, import_fs.readFileSync)(filePath, "utf-8"));
|
|
517
|
+
hasher.update("\0");
|
|
518
|
+
}
|
|
519
|
+
return hasher.digest("hex");
|
|
520
|
+
}
|
|
521
|
+
function readChecksum(outputDir) {
|
|
522
|
+
const checksumPath = import_path2.default.join(outputDir, CHECKSUM_FILENAME);
|
|
523
|
+
if (!(0, import_fs.existsSync)(checksumPath)) return null;
|
|
524
|
+
try {
|
|
525
|
+
const raw = (0, import_fs.readFileSync)(checksumPath, "utf-8");
|
|
526
|
+
const data = JSON.parse(raw);
|
|
527
|
+
if (typeof data.hash !== "string" || !Array.isArray(data.files)) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
return data;
|
|
531
|
+
} catch {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async function writeChecksum(outputDir, data) {
|
|
536
|
+
await (0, import_promises2.mkdir)(outputDir, { recursive: true });
|
|
537
|
+
const checksumPath = import_path2.default.join(outputDir, CHECKSUM_FILENAME);
|
|
538
|
+
await (0, import_promises2.writeFile)(checksumPath, JSON.stringify(data, null, 2), "utf-8");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/utils/artifact-contract.ts
|
|
542
|
+
var RPC_ARTIFACT_VERSION = "1";
|
|
543
|
+
function isRpcArtifact(value) {
|
|
544
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
545
|
+
const obj = value;
|
|
546
|
+
return typeof obj.artifactVersion === "string" && Array.isArray(obj.routes) && Array.isArray(obj.schemas);
|
|
547
|
+
}
|
|
548
|
+
function assertRpcArtifact(value) {
|
|
549
|
+
if (!isRpcArtifact(value)) {
|
|
550
|
+
throw new Error("Invalid RPC artifact: expected { artifactVersion, routes, schemas }");
|
|
551
|
+
}
|
|
552
|
+
if (value.artifactVersion !== RPC_ARTIFACT_VERSION) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Unsupported RPC artifact version '${value.artifactVersion}'. Supported: ${RPC_ARTIFACT_VERSION}.`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
517
559
|
// src/services/route-analyzer.service.ts
|
|
518
560
|
var import_honestjs = require("honestjs");
|
|
519
561
|
var RouteAnalyzerService = class {
|
|
562
|
+
customClassMatcher;
|
|
563
|
+
onWarn;
|
|
564
|
+
warnings = [];
|
|
565
|
+
constructor(options = {}) {
|
|
566
|
+
this.customClassMatcher = options.customClassMatcher;
|
|
567
|
+
this.onWarn = options.onWarn;
|
|
568
|
+
}
|
|
569
|
+
getWarnings() {
|
|
570
|
+
return this.warnings;
|
|
571
|
+
}
|
|
520
572
|
/**
|
|
521
573
|
* Analyzes controller methods to extract type information
|
|
522
574
|
*/
|
|
523
575
|
async analyzeControllerMethods(project) {
|
|
576
|
+
this.warnings = [];
|
|
524
577
|
const routes = import_honestjs.RouteRegistry.getRoutes();
|
|
525
578
|
if (!routes?.length) {
|
|
526
579
|
return [];
|
|
@@ -541,13 +594,20 @@ var RouteAnalyzerService = class {
|
|
|
541
594
|
const classes = sourceFile.getClasses();
|
|
542
595
|
for (const classDeclaration of classes) {
|
|
543
596
|
const className = classDeclaration.getName();
|
|
544
|
-
if (className
|
|
597
|
+
if (className && this.isControllerClass(classDeclaration, className)) {
|
|
545
598
|
controllers.set(className, classDeclaration);
|
|
546
599
|
}
|
|
547
600
|
}
|
|
548
601
|
}
|
|
549
602
|
return controllers;
|
|
550
603
|
}
|
|
604
|
+
isControllerClass(classDeclaration, _className) {
|
|
605
|
+
if (this.customClassMatcher) {
|
|
606
|
+
return this.customClassMatcher(classDeclaration);
|
|
607
|
+
}
|
|
608
|
+
const decoratorNames = classDeclaration.getDecorators().map((decorator) => decorator.getName());
|
|
609
|
+
return decoratorNames.includes("Controller") || decoratorNames.includes("View");
|
|
610
|
+
}
|
|
551
611
|
/**
|
|
552
612
|
* Processes all routes and extracts type information
|
|
553
613
|
*/
|
|
@@ -558,10 +618,9 @@ var RouteAnalyzerService = class {
|
|
|
558
618
|
const extendedRoute = this.createExtendedRoute(route, controllers);
|
|
559
619
|
analyzedRoutes.push(extendedRoute);
|
|
560
620
|
} catch (routeError) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
);
|
|
621
|
+
const warning = `Skipping route ${safeToString(route.controller)}.${safeToString(route.handler)}`;
|
|
622
|
+
this.warnings.push(warning);
|
|
623
|
+
this.onWarn?.(warning, routeError);
|
|
565
624
|
}
|
|
566
625
|
}
|
|
567
626
|
return analyzedRoutes;
|
|
@@ -581,6 +640,10 @@ var RouteAnalyzerService = class {
|
|
|
581
640
|
returns = this.getReturnType(handlerMethod);
|
|
582
641
|
parameters = this.getParametersWithTypes(handlerMethod, route.parameters || []);
|
|
583
642
|
}
|
|
643
|
+
} else {
|
|
644
|
+
const warning = `Controller class not found in source files: ${controllerName} (handler: ${handlerName})`;
|
|
645
|
+
this.warnings.push(warning);
|
|
646
|
+
this.onWarn?.(warning);
|
|
584
647
|
}
|
|
585
648
|
return {
|
|
586
649
|
controller: controllerName,
|
|
@@ -719,14 +782,23 @@ function extractNamedType(type) {
|
|
|
719
782
|
|
|
720
783
|
// src/services/schema-generator.service.ts
|
|
721
784
|
var SchemaGeneratorService = class {
|
|
722
|
-
constructor(controllerPattern, tsConfigPath) {
|
|
785
|
+
constructor(controllerPattern, tsConfigPath, options = {}) {
|
|
723
786
|
this.controllerPattern = controllerPattern;
|
|
724
787
|
this.tsConfigPath = tsConfigPath;
|
|
788
|
+
this.failOnSchemaError = options.failOnSchemaError ?? false;
|
|
789
|
+
this.onWarn = options.onWarn;
|
|
790
|
+
}
|
|
791
|
+
failOnSchemaError;
|
|
792
|
+
onWarn;
|
|
793
|
+
warnings = [];
|
|
794
|
+
getWarnings() {
|
|
795
|
+
return this.warnings;
|
|
725
796
|
}
|
|
726
797
|
/**
|
|
727
798
|
* Generates JSON schemas from types used in controllers
|
|
728
799
|
*/
|
|
729
800
|
async generateSchemas(project) {
|
|
801
|
+
this.warnings = [];
|
|
730
802
|
const sourceFiles = project.getSourceFiles(this.controllerPattern);
|
|
731
803
|
const collectedTypes = this.collectTypesFromControllers(sourceFiles);
|
|
732
804
|
return this.processTypes(collectedTypes);
|
|
@@ -773,7 +845,12 @@ var SchemaGeneratorService = class {
|
|
|
773
845
|
typescriptType
|
|
774
846
|
});
|
|
775
847
|
} catch (err) {
|
|
776
|
-
|
|
848
|
+
if (this.failOnSchemaError) {
|
|
849
|
+
throw err;
|
|
850
|
+
}
|
|
851
|
+
const warning = `Failed to generate schema for ${typeName}`;
|
|
852
|
+
this.warnings.push(warning);
|
|
853
|
+
this.onWarn?.(warning, err);
|
|
777
854
|
}
|
|
778
855
|
}
|
|
779
856
|
return schemas;
|
|
@@ -792,7 +869,12 @@ var SchemaGeneratorService = class {
|
|
|
792
869
|
});
|
|
793
870
|
return generator.createSchema(typeName);
|
|
794
871
|
} catch (error) {
|
|
795
|
-
|
|
872
|
+
if (this.failOnSchemaError) {
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
const warning = `Failed to generate schema for type ${typeName}`;
|
|
876
|
+
this.warnings.push(warning);
|
|
877
|
+
this.onWarn?.(warning, error);
|
|
796
878
|
return {
|
|
797
879
|
type: "object",
|
|
798
880
|
properties: {},
|
|
@@ -810,27 +892,44 @@ var RPCPlugin = class {
|
|
|
810
892
|
generateOnInit;
|
|
811
893
|
contextNamespace;
|
|
812
894
|
contextArtifactKey;
|
|
895
|
+
mode;
|
|
896
|
+
logLevel;
|
|
897
|
+
failOnSchemaError;
|
|
898
|
+
failOnRouteAnalysisWarning;
|
|
899
|
+
customClassMatcher;
|
|
813
900
|
// Services
|
|
814
901
|
routeAnalyzer;
|
|
815
902
|
schemaGenerator;
|
|
816
|
-
|
|
903
|
+
generators;
|
|
817
904
|
// Shared ts-morph project
|
|
818
905
|
project = null;
|
|
819
906
|
// Internal state
|
|
820
907
|
analyzedRoutes = [];
|
|
821
908
|
analyzedSchemas = [];
|
|
822
|
-
|
|
909
|
+
generatedInfos = [];
|
|
910
|
+
diagnostics = null;
|
|
823
911
|
app = null;
|
|
824
912
|
constructor(options = {}) {
|
|
825
913
|
this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
|
|
826
914
|
this.tsConfigPath = options.tsConfigPath ?? import_path3.default.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
|
|
827
915
|
this.outputDir = options.outputDir ?? import_path3.default.resolve(process.cwd(), DEFAULT_OPTIONS.outputDir);
|
|
828
916
|
this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
|
|
917
|
+
this.mode = options.mode ?? DEFAULT_OPTIONS.mode;
|
|
918
|
+
this.logLevel = options.logLevel ?? DEFAULT_OPTIONS.logLevel;
|
|
829
919
|
this.contextNamespace = options.context?.namespace ?? DEFAULT_OPTIONS.context.namespace;
|
|
830
920
|
this.contextArtifactKey = options.context?.keys?.artifact ?? DEFAULT_OPTIONS.context.keys.artifact;
|
|
831
|
-
this.
|
|
832
|
-
this.
|
|
833
|
-
this.
|
|
921
|
+
this.customClassMatcher = options.customClassMatcher;
|
|
922
|
+
this.failOnSchemaError = options.failOnSchemaError ?? this.mode === "strict";
|
|
923
|
+
this.failOnRouteAnalysisWarning = options.failOnRouteAnalysisWarning ?? this.mode === "strict";
|
|
924
|
+
this.routeAnalyzer = new RouteAnalyzerService({
|
|
925
|
+
customClassMatcher: this.customClassMatcher,
|
|
926
|
+
onWarn: (message, details) => this.logWarn(message, details)
|
|
927
|
+
});
|
|
928
|
+
this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath, {
|
|
929
|
+
failOnSchemaError: this.failOnSchemaError,
|
|
930
|
+
onWarn: (message, details) => this.logWarn(message, details)
|
|
931
|
+
});
|
|
932
|
+
this.generators = options.generators ?? [new TypeScriptClientGenerator(this.outputDir)];
|
|
834
933
|
this.validateConfiguration();
|
|
835
934
|
}
|
|
836
935
|
/**
|
|
@@ -851,17 +950,31 @@ var RPCPlugin = class {
|
|
|
851
950
|
if (!this.outputDir?.trim()) {
|
|
852
951
|
errors.push("Output directory cannot be empty");
|
|
853
952
|
}
|
|
953
|
+
if (!["strict", "best-effort"].includes(this.mode)) {
|
|
954
|
+
errors.push('Mode must be "strict" or "best-effort"');
|
|
955
|
+
}
|
|
956
|
+
if (!["silent", "error", "warn", "info", "debug"].includes(this.logLevel)) {
|
|
957
|
+
errors.push("logLevel must be one of: silent, error, warn, info, debug");
|
|
958
|
+
}
|
|
854
959
|
if (!this.contextNamespace?.trim()) {
|
|
855
960
|
errors.push("Context namespace cannot be empty");
|
|
856
961
|
}
|
|
857
962
|
if (!this.contextArtifactKey?.trim()) {
|
|
858
963
|
errors.push("Context artifact key cannot be empty");
|
|
859
964
|
}
|
|
965
|
+
for (const generator of this.generators) {
|
|
966
|
+
if (!generator.name?.trim()) {
|
|
967
|
+
errors.push("Generator name cannot be empty");
|
|
968
|
+
}
|
|
969
|
+
if (typeof generator.generate !== "function") {
|
|
970
|
+
errors.push(`Generator "${generator.name || "unknown"}" must implement generate(context)`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
860
973
|
if (errors.length > 0) {
|
|
861
974
|
throw new Error(`Configuration validation failed: ${errors.join(", ")}`);
|
|
862
975
|
}
|
|
863
976
|
this.log(
|
|
864
|
-
`Configuration validated: controllerPattern=${this.controllerPattern}, tsConfigPath=${this.tsConfigPath}, outputDir=${this.outputDir}`
|
|
977
|
+
`Configuration validated: controllerPattern=${this.controllerPattern}, tsConfigPath=${this.tsConfigPath}, outputDir=${this.outputDir}, mode=${this.mode}`
|
|
865
978
|
);
|
|
866
979
|
}
|
|
867
980
|
/**
|
|
@@ -870,14 +983,17 @@ var RPCPlugin = class {
|
|
|
870
983
|
afterModulesRegistered = async (app, hono) => {
|
|
871
984
|
this.app = app;
|
|
872
985
|
if (this.generateOnInit) {
|
|
873
|
-
await this.analyzeEverything();
|
|
986
|
+
await this.analyzeEverything({ force: false, dryRun: false });
|
|
874
987
|
this.publishArtifact(app);
|
|
875
988
|
}
|
|
876
989
|
};
|
|
877
990
|
/**
|
|
878
991
|
* Main analysis method that coordinates all three components
|
|
879
992
|
*/
|
|
880
|
-
async analyzeEverything(
|
|
993
|
+
async analyzeEverything(options) {
|
|
994
|
+
const { force, dryRun } = options;
|
|
995
|
+
const warnings = [];
|
|
996
|
+
let cacheState = force ? "bypass" : "miss";
|
|
881
997
|
try {
|
|
882
998
|
this.log("Starting comprehensive RPC analysis...");
|
|
883
999
|
this.dispose();
|
|
@@ -889,21 +1005,50 @@ var RPCPlugin = class {
|
|
|
889
1005
|
const stored = readChecksum(this.outputDir);
|
|
890
1006
|
if (stored && stored.hash === currentHash && this.outputFilesExist()) {
|
|
891
1007
|
if (this.loadArtifactFromDisk()) {
|
|
892
|
-
|
|
1008
|
+
cacheState = "hit";
|
|
1009
|
+
this.logDebug("Source files unchanged - skipping regeneration");
|
|
1010
|
+
this.diagnostics = {
|
|
1011
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1012
|
+
mode: this.mode,
|
|
1013
|
+
dryRun,
|
|
1014
|
+
cache: cacheState,
|
|
1015
|
+
routesCount: this.analyzedRoutes.length,
|
|
1016
|
+
schemasCount: this.analyzedSchemas.length,
|
|
1017
|
+
warnings: []
|
|
1018
|
+
};
|
|
893
1019
|
this.dispose();
|
|
894
1020
|
return;
|
|
895
1021
|
}
|
|
896
|
-
this.
|
|
1022
|
+
this.logDebug("Source files unchanged but cached artifact missing/invalid - regenerating");
|
|
897
1023
|
}
|
|
898
1024
|
}
|
|
899
1025
|
this.analyzedRoutes = [];
|
|
900
1026
|
this.analyzedSchemas = [];
|
|
901
|
-
this.
|
|
1027
|
+
this.generatedInfos = [];
|
|
902
1028
|
this.analyzedRoutes = await this.routeAnalyzer.analyzeControllerMethods(this.project);
|
|
1029
|
+
warnings.push(...this.routeAnalyzer.getWarnings());
|
|
903
1030
|
this.analyzedSchemas = await this.schemaGenerator.generateSchemas(this.project);
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1031
|
+
warnings.push(...this.schemaGenerator.getWarnings());
|
|
1032
|
+
if (this.failOnRouteAnalysisWarning && this.routeAnalyzer.getWarnings().length > 0) {
|
|
1033
|
+
throw new Error(`Route analysis warnings encountered in strict mode: ${this.routeAnalyzer.getWarnings().join("; ")}`);
|
|
1034
|
+
}
|
|
1035
|
+
if (!dryRun) {
|
|
1036
|
+
this.generatedInfos = await this.runGenerators();
|
|
1037
|
+
}
|
|
1038
|
+
if (!dryRun) {
|
|
1039
|
+
await writeChecksum(this.outputDir, { hash: computeHash(filePaths), files: filePaths });
|
|
1040
|
+
this.writeArtifactToDisk();
|
|
1041
|
+
}
|
|
1042
|
+
this.diagnostics = {
|
|
1043
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1044
|
+
mode: this.mode,
|
|
1045
|
+
dryRun,
|
|
1046
|
+
cache: cacheState,
|
|
1047
|
+
routesCount: this.analyzedRoutes.length,
|
|
1048
|
+
schemasCount: this.analyzedSchemas.length,
|
|
1049
|
+
warnings
|
|
1050
|
+
};
|
|
1051
|
+
this.writeDiagnosticsToDisk();
|
|
907
1052
|
this.log(
|
|
908
1053
|
`\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
|
|
909
1054
|
);
|
|
@@ -913,13 +1058,10 @@ var RPCPlugin = class {
|
|
|
913
1058
|
throw error;
|
|
914
1059
|
}
|
|
915
1060
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
async analyze(force = true) {
|
|
921
|
-
await this.analyzeEverything(force);
|
|
922
|
-
if (this.app) {
|
|
1061
|
+
async analyze(forceOrOptions = true) {
|
|
1062
|
+
const options = typeof forceOrOptions === "boolean" ? { force: forceOrOptions, dryRun: false } : { force: forceOrOptions.force ?? true, dryRun: forceOrOptions.dryRun ?? false };
|
|
1063
|
+
await this.analyzeEverything(options);
|
|
1064
|
+
if (this.app && !options.dryRun) {
|
|
923
1065
|
this.publishArtifact(this.app);
|
|
924
1066
|
}
|
|
925
1067
|
}
|
|
@@ -939,42 +1081,89 @@ var RPCPlugin = class {
|
|
|
939
1081
|
* Get the generation info
|
|
940
1082
|
*/
|
|
941
1083
|
getGenerationInfo() {
|
|
942
|
-
return this.
|
|
1084
|
+
return this.generatedInfos[0] ?? null;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Get all generation infos
|
|
1088
|
+
*/
|
|
1089
|
+
getGenerationInfos() {
|
|
1090
|
+
return this.generatedInfos;
|
|
1091
|
+
}
|
|
1092
|
+
getDiagnostics() {
|
|
1093
|
+
return this.diagnostics;
|
|
943
1094
|
}
|
|
944
1095
|
/**
|
|
945
1096
|
* Checks whether expected output files exist on disk
|
|
946
1097
|
*/
|
|
947
1098
|
outputFilesExist() {
|
|
948
|
-
|
|
1099
|
+
if (!import_fs2.default.existsSync(import_path3.default.join(this.outputDir, "rpc-artifact.json"))) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
if (!this.hasTypeScriptGenerator()) {
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
return import_fs2.default.existsSync(import_path3.default.join(this.outputDir, "client.ts"));
|
|
949
1106
|
}
|
|
950
1107
|
getArtifactPath() {
|
|
951
1108
|
return import_path3.default.join(this.outputDir, "rpc-artifact.json");
|
|
952
1109
|
}
|
|
1110
|
+
getDiagnosticsPath() {
|
|
1111
|
+
return import_path3.default.join(this.outputDir, "rpc-diagnostics.json");
|
|
1112
|
+
}
|
|
953
1113
|
writeArtifactToDisk() {
|
|
954
1114
|
const artifact = {
|
|
1115
|
+
artifactVersion: RPC_ARTIFACT_VERSION,
|
|
955
1116
|
routes: this.analyzedRoutes,
|
|
956
1117
|
schemas: this.analyzedSchemas
|
|
957
1118
|
};
|
|
958
1119
|
import_fs2.default.mkdirSync(this.outputDir, { recursive: true });
|
|
959
1120
|
import_fs2.default.writeFileSync(this.getArtifactPath(), JSON.stringify(artifact));
|
|
960
1121
|
}
|
|
1122
|
+
writeDiagnosticsToDisk() {
|
|
1123
|
+
if (!this.diagnostics) return;
|
|
1124
|
+
import_fs2.default.mkdirSync(this.outputDir, { recursive: true });
|
|
1125
|
+
import_fs2.default.writeFileSync(this.getDiagnosticsPath(), JSON.stringify(this.diagnostics, null, 2));
|
|
1126
|
+
}
|
|
961
1127
|
loadArtifactFromDisk() {
|
|
962
1128
|
try {
|
|
963
1129
|
const raw = import_fs2.default.readFileSync(this.getArtifactPath(), "utf8");
|
|
964
1130
|
const parsed = JSON.parse(raw);
|
|
965
|
-
if (
|
|
966
|
-
|
|
1131
|
+
if (parsed.artifactVersion === void 0) {
|
|
1132
|
+
if (!Array.isArray(parsed.routes) || !Array.isArray(parsed.schemas)) {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
this.analyzedRoutes = parsed.routes;
|
|
1136
|
+
this.analyzedSchemas = parsed.schemas;
|
|
1137
|
+
} else {
|
|
1138
|
+
assertRpcArtifact(parsed);
|
|
1139
|
+
this.analyzedRoutes = parsed.routes;
|
|
1140
|
+
this.analyzedSchemas = parsed.schemas;
|
|
967
1141
|
}
|
|
968
|
-
this.
|
|
969
|
-
this.analyzedSchemas = parsed.schemas;
|
|
970
|
-
this.generatedInfo = null;
|
|
1142
|
+
this.generatedInfos = [];
|
|
971
1143
|
return true;
|
|
972
1144
|
} catch {
|
|
973
1145
|
return false;
|
|
974
1146
|
}
|
|
975
1147
|
}
|
|
1148
|
+
async runGenerators() {
|
|
1149
|
+
const results = [];
|
|
1150
|
+
for (const generator of this.generators) {
|
|
1151
|
+
this.log(`Running generator: ${generator.name}`);
|
|
1152
|
+
const result = await generator.generate({
|
|
1153
|
+
outputDir: this.outputDir,
|
|
1154
|
+
routes: this.analyzedRoutes,
|
|
1155
|
+
schemas: this.analyzedSchemas
|
|
1156
|
+
});
|
|
1157
|
+
results.push(result);
|
|
1158
|
+
}
|
|
1159
|
+
return results;
|
|
1160
|
+
}
|
|
1161
|
+
hasTypeScriptGenerator() {
|
|
1162
|
+
return this.generators.some((generator) => generator.name === "typescript-client");
|
|
1163
|
+
}
|
|
976
1164
|
publishArtifact(app) {
|
|
977
1165
|
app.getContext().set(this.getArtifactContextKey(), {
|
|
1166
|
+
artifactVersion: RPC_ARTIFACT_VERSION,
|
|
978
1167
|
routes: this.analyzedRoutes,
|
|
979
1168
|
schemas: this.analyzedSchemas
|
|
980
1169
|
});
|
|
@@ -998,32 +1187,59 @@ var RPCPlugin = class {
|
|
|
998
1187
|
* Logs a message with the plugin prefix
|
|
999
1188
|
*/
|
|
1000
1189
|
log(message) {
|
|
1001
|
-
|
|
1190
|
+
if (this.canLog("info")) {
|
|
1191
|
+
console.log(`${LOG_PREFIX} ${message}`);
|
|
1192
|
+
}
|
|
1002
1193
|
}
|
|
1003
1194
|
/**
|
|
1004
1195
|
* Logs an error with the plugin prefix
|
|
1005
1196
|
*/
|
|
1006
1197
|
logError(message, error) {
|
|
1007
|
-
|
|
1198
|
+
if (this.canLog("error")) {
|
|
1199
|
+
console.error(`${LOG_PREFIX} ${message}`, error || "");
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
logWarn(message, details) {
|
|
1203
|
+
if (this.canLog("warn")) {
|
|
1204
|
+
console.warn(`${LOG_PREFIX} ${message}`, details || "");
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
logDebug(message) {
|
|
1208
|
+
if (this.canLog("debug")) {
|
|
1209
|
+
console.log(`${LOG_PREFIX} ${message}`);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
canLog(level) {
|
|
1213
|
+
const order = {
|
|
1214
|
+
silent: 0,
|
|
1215
|
+
error: 1,
|
|
1216
|
+
warn: 2,
|
|
1217
|
+
info: 3,
|
|
1218
|
+
debug: 4
|
|
1219
|
+
};
|
|
1220
|
+
return order[this.logLevel] >= order[level];
|
|
1008
1221
|
}
|
|
1009
1222
|
};
|
|
1010
1223
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1011
1224
|
0 && (module.exports = {
|
|
1012
1225
|
BUILTIN_TYPES,
|
|
1013
1226
|
BUILTIN_UTILITY_TYPES,
|
|
1014
|
-
ClientGeneratorService,
|
|
1015
1227
|
DEFAULT_OPTIONS,
|
|
1016
1228
|
GENERIC_TYPES,
|
|
1017
1229
|
LOG_PREFIX,
|
|
1018
1230
|
RPCPlugin,
|
|
1231
|
+
RPC_ARTIFACT_VERSION,
|
|
1019
1232
|
RouteAnalyzerService,
|
|
1020
1233
|
SchemaGeneratorService,
|
|
1234
|
+
TypeScriptClientGenerator,
|
|
1235
|
+
assertRpcArtifact,
|
|
1021
1236
|
buildFullApiPath,
|
|
1022
1237
|
buildFullPath,
|
|
1023
1238
|
camelCase,
|
|
1024
1239
|
computeHash,
|
|
1025
1240
|
extractNamedType,
|
|
1026
1241
|
generateTypeScriptInterface,
|
|
1242
|
+
isRpcArtifact,
|
|
1027
1243
|
mapJsonSchemaTypeToTypeScript,
|
|
1028
1244
|
readChecksum,
|
|
1029
1245
|
safeToString,
|