@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.mjs
CHANGED
|
@@ -9,6 +9,9 @@ var DEFAULT_OPTIONS = {
|
|
|
9
9
|
tsConfigPath: "tsconfig.json",
|
|
10
10
|
outputDir: "./generated/rpc",
|
|
11
11
|
generateOnInit: true,
|
|
12
|
+
mode: "best-effort",
|
|
13
|
+
logLevel: "info",
|
|
14
|
+
artifactVersion: "1",
|
|
12
15
|
context: {
|
|
13
16
|
namespace: "rpc",
|
|
14
17
|
keys: {
|
|
@@ -32,47 +35,20 @@ var BUILTIN_UTILITY_TYPES = /* @__PURE__ */ new Set([
|
|
|
32
35
|
var BUILTIN_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "any", "void", "unknown"]);
|
|
33
36
|
var GENERIC_TYPES = /* @__PURE__ */ new Set(["Array", "Promise", "Partial"]);
|
|
34
37
|
|
|
35
|
-
// src/
|
|
36
|
-
import
|
|
37
|
-
import { existsSync, readFileSync } from "fs";
|
|
38
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
38
|
+
// src/generators/typescript-client.generator.ts
|
|
39
|
+
import fs from "fs/promises";
|
|
39
40
|
import path from "path";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
for (const filePath of sorted) {
|
|
47
|
-
hasher.update(readFileSync(filePath, "utf-8"));
|
|
48
|
-
hasher.update("\0");
|
|
49
|
-
}
|
|
50
|
-
return hasher.digest("hex");
|
|
51
|
-
}
|
|
52
|
-
function readChecksum(outputDir) {
|
|
53
|
-
const checksumPath = path.join(outputDir, CHECKSUM_FILENAME);
|
|
54
|
-
if (!existsSync(checksumPath)) return null;
|
|
55
|
-
try {
|
|
56
|
-
const raw = readFileSync(checksumPath, "utf-8");
|
|
57
|
-
const data = JSON.parse(raw);
|
|
58
|
-
if (typeof data.hash !== "string" || !Array.isArray(data.files)) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
return data;
|
|
62
|
-
} catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
41
|
+
|
|
42
|
+
// src/utils/string-utils.ts
|
|
43
|
+
function safeToString(value) {
|
|
44
|
+
if (typeof value === "string") return value;
|
|
45
|
+
if (typeof value === "symbol") return value.description || "Symbol";
|
|
46
|
+
return String(value);
|
|
65
47
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const checksumPath = path.join(outputDir, CHECKSUM_FILENAME);
|
|
69
|
-
await writeFile(checksumPath, JSON.stringify(data, null, 2), "utf-8");
|
|
48
|
+
function camelCase(str) {
|
|
49
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
70
50
|
}
|
|
71
51
|
|
|
72
|
-
// src/services/client-generator.service.ts
|
|
73
|
-
import fs from "fs/promises";
|
|
74
|
-
import path2 from "path";
|
|
75
|
-
|
|
76
52
|
// src/utils/path-utils.ts
|
|
77
53
|
function buildFullPath(basePath, parameters) {
|
|
78
54
|
if (!basePath || typeof basePath !== "string") return "/";
|
|
@@ -111,46 +87,67 @@ function buildFullApiPath(route) {
|
|
|
111
87
|
return fullPath || "/";
|
|
112
88
|
}
|
|
113
89
|
|
|
114
|
-
// src/
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
90
|
+
// src/generators/generator-utils.ts
|
|
91
|
+
function groupRoutesByController(routes) {
|
|
92
|
+
const groups = /* @__PURE__ */ new Map();
|
|
93
|
+
for (const route of routes) {
|
|
94
|
+
const controller = safeToString(route.controller);
|
|
95
|
+
if (!groups.has(controller)) {
|
|
96
|
+
groups.set(controller, []);
|
|
97
|
+
}
|
|
98
|
+
groups.get(controller).push(route);
|
|
99
|
+
}
|
|
100
|
+
return groups;
|
|
119
101
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
102
|
+
function buildNormalizedRequestPath(route) {
|
|
103
|
+
let requestPath = buildFullApiPath(route);
|
|
104
|
+
for (const parameter of route.parameters ?? []) {
|
|
105
|
+
if (parameter.decoratorType !== "param") continue;
|
|
106
|
+
const placeholder = `:${String(parameter.data ?? parameter.name)}`;
|
|
107
|
+
requestPath = requestPath.replace(placeholder, `:${parameter.name}`);
|
|
108
|
+
}
|
|
109
|
+
return requestPath;
|
|
122
110
|
}
|
|
123
111
|
|
|
124
|
-
// src/
|
|
125
|
-
var
|
|
112
|
+
// src/generators/typescript-client.generator.ts
|
|
113
|
+
var TypeScriptClientGenerator = class {
|
|
126
114
|
constructor(outputDir) {
|
|
127
115
|
this.outputDir = outputDir;
|
|
128
116
|
}
|
|
117
|
+
name = "typescript-client";
|
|
129
118
|
/**
|
|
130
|
-
* Generates the TypeScript RPC client
|
|
119
|
+
* Generates the TypeScript RPC client.
|
|
120
|
+
*/
|
|
121
|
+
async generate(context) {
|
|
122
|
+
return this.generateClient(context.routes, context.schemas);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Generates the TypeScript RPC client.
|
|
131
126
|
*/
|
|
132
127
|
async generateClient(routes, schemas) {
|
|
133
128
|
await fs.mkdir(this.outputDir, { recursive: true });
|
|
134
129
|
await this.generateClientFile(routes, schemas);
|
|
135
130
|
const generatedInfo = {
|
|
136
|
-
|
|
131
|
+
generator: this.name,
|
|
132
|
+
clientFile: path.join(this.outputDir, "client.ts"),
|
|
133
|
+
outputFiles: [path.join(this.outputDir, "client.ts")],
|
|
137
134
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
138
135
|
};
|
|
139
136
|
return generatedInfo;
|
|
140
137
|
}
|
|
141
138
|
/**
|
|
142
|
-
* Generates the main client file with types included
|
|
139
|
+
* Generates the main client file with types included.
|
|
143
140
|
*/
|
|
144
141
|
async generateClientFile(routes, schemas) {
|
|
145
142
|
const clientContent = this.generateClientContent(routes, schemas);
|
|
146
|
-
const clientPath =
|
|
143
|
+
const clientPath = path.join(this.outputDir, "client.ts");
|
|
147
144
|
await fs.writeFile(clientPath, clientContent, "utf-8");
|
|
148
145
|
}
|
|
149
146
|
/**
|
|
150
|
-
* Generates the client TypeScript content with types included
|
|
147
|
+
* Generates the client TypeScript content with types included.
|
|
151
148
|
*/
|
|
152
149
|
generateClientContent(routes, schemas) {
|
|
153
|
-
const controllerGroups =
|
|
150
|
+
const controllerGroups = groupRoutesByController(routes);
|
|
154
151
|
return `// ============================================================================
|
|
155
152
|
// TYPES SECTION
|
|
156
153
|
// ============================================================================
|
|
@@ -311,13 +308,21 @@ export class ApiClient {
|
|
|
311
308
|
return undefined as T
|
|
312
309
|
}
|
|
313
310
|
|
|
314
|
-
const
|
|
311
|
+
const contentType = response.headers.get('content-type') || ''
|
|
312
|
+
const isJson = contentType.includes('application/json') || contentType.includes('+json')
|
|
313
|
+
const responseData = isJson ? await response.json() : await response.text()
|
|
315
314
|
|
|
316
315
|
if (!response.ok) {
|
|
317
|
-
|
|
316
|
+
const message =
|
|
317
|
+
typeof responseData === 'object' && responseData && 'message' in (responseData as Record<string, unknown>)
|
|
318
|
+
? String((responseData as Record<string, unknown>).message)
|
|
319
|
+
: typeof responseData === 'string' && responseData.trim()
|
|
320
|
+
? responseData
|
|
321
|
+
: 'Request failed'
|
|
322
|
+
throw new ApiError(response.status, message)
|
|
318
323
|
}
|
|
319
324
|
|
|
320
|
-
return responseData
|
|
325
|
+
return responseData as T
|
|
321
326
|
} catch (error) {
|
|
322
327
|
if (error instanceof ApiError) {
|
|
323
328
|
throw error
|
|
@@ -331,7 +336,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
331
336
|
`;
|
|
332
337
|
}
|
|
333
338
|
/**
|
|
334
|
-
* Generates controller methods for the client
|
|
339
|
+
* Generates controller methods for the client.
|
|
335
340
|
*/
|
|
336
341
|
generateControllerMethods(controllerGroups) {
|
|
337
342
|
let methods = "";
|
|
@@ -386,14 +391,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
386
391
|
methods += "undefined";
|
|
387
392
|
methods += `>) => {
|
|
388
393
|
`;
|
|
389
|
-
|
|
390
|
-
if (pathParams.length > 0) {
|
|
391
|
-
for (const pathParam of pathParams) {
|
|
392
|
-
const paramName = pathParam.name;
|
|
393
|
-
const placeholder = `:${String(pathParam.data)}`;
|
|
394
|
-
requestPath = requestPath.replace(placeholder, `:${paramName}`);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
394
|
+
const requestPath = buildNormalizedRequestPath(route);
|
|
397
395
|
methods += ` return this.request<Result>('${httpMethod.toUpperCase()}', \`${requestPath}\`, options)
|
|
398
396
|
`;
|
|
399
397
|
methods += ` },
|
|
@@ -407,7 +405,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
407
405
|
return methods;
|
|
408
406
|
}
|
|
409
407
|
/**
|
|
410
|
-
* Extracts the proper return type from route analysis
|
|
408
|
+
* Extracts the proper return type from route analysis.
|
|
411
409
|
*/
|
|
412
410
|
extractReturnType(returns) {
|
|
413
411
|
if (!returns) return "any";
|
|
@@ -418,7 +416,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
418
416
|
return returns;
|
|
419
417
|
}
|
|
420
418
|
/**
|
|
421
|
-
* Generates schema types from integrated schema generation
|
|
419
|
+
* Generates schema types from integrated schema generation.
|
|
422
420
|
*/
|
|
423
421
|
generateSchemaTypes(schemas) {
|
|
424
422
|
if (schemas.length === 0) {
|
|
@@ -435,21 +433,7 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
435
433
|
return content;
|
|
436
434
|
}
|
|
437
435
|
/**
|
|
438
|
-
*
|
|
439
|
-
*/
|
|
440
|
-
groupRoutesByController(routes) {
|
|
441
|
-
const groups = /* @__PURE__ */ new Map();
|
|
442
|
-
for (const route of routes) {
|
|
443
|
-
const controller = safeToString(route.controller);
|
|
444
|
-
if (!groups.has(controller)) {
|
|
445
|
-
groups.set(controller, []);
|
|
446
|
-
}
|
|
447
|
-
groups.get(controller).push(route);
|
|
448
|
-
}
|
|
449
|
-
return groups;
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Analyzes route parameters to determine their types and usage
|
|
436
|
+
* Analyzes route parameters to determine their types and usage.
|
|
453
437
|
*/
|
|
454
438
|
analyzeRouteParameters(route) {
|
|
455
439
|
const parameters = route.parameters || [];
|
|
@@ -460,13 +444,79 @@ ${this.generateControllerMethods(controllerGroups)}
|
|
|
460
444
|
}
|
|
461
445
|
};
|
|
462
446
|
|
|
447
|
+
// src/utils/hash-utils.ts
|
|
448
|
+
import { createHash } from "crypto";
|
|
449
|
+
import { existsSync, readFileSync } from "fs";
|
|
450
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
451
|
+
import path2 from "path";
|
|
452
|
+
var CHECKSUM_FILENAME = ".rpc-checksum";
|
|
453
|
+
function computeHash(filePaths) {
|
|
454
|
+
const sorted = [...filePaths].sort();
|
|
455
|
+
const hasher = createHash("sha256");
|
|
456
|
+
hasher.update(`files:${sorted.length}
|
|
457
|
+
`);
|
|
458
|
+
for (const filePath of sorted) {
|
|
459
|
+
hasher.update(readFileSync(filePath, "utf-8"));
|
|
460
|
+
hasher.update("\0");
|
|
461
|
+
}
|
|
462
|
+
return hasher.digest("hex");
|
|
463
|
+
}
|
|
464
|
+
function readChecksum(outputDir) {
|
|
465
|
+
const checksumPath = path2.join(outputDir, CHECKSUM_FILENAME);
|
|
466
|
+
if (!existsSync(checksumPath)) return null;
|
|
467
|
+
try {
|
|
468
|
+
const raw = readFileSync(checksumPath, "utf-8");
|
|
469
|
+
const data = JSON.parse(raw);
|
|
470
|
+
if (typeof data.hash !== "string" || !Array.isArray(data.files)) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
return data;
|
|
474
|
+
} catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function writeChecksum(outputDir, data) {
|
|
479
|
+
await mkdir(outputDir, { recursive: true });
|
|
480
|
+
const checksumPath = path2.join(outputDir, CHECKSUM_FILENAME);
|
|
481
|
+
await writeFile(checksumPath, JSON.stringify(data, null, 2), "utf-8");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/utils/artifact-contract.ts
|
|
485
|
+
var RPC_ARTIFACT_VERSION = "1";
|
|
486
|
+
function isRpcArtifact(value) {
|
|
487
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
488
|
+
const obj = value;
|
|
489
|
+
return typeof obj.artifactVersion === "string" && Array.isArray(obj.routes) && Array.isArray(obj.schemas);
|
|
490
|
+
}
|
|
491
|
+
function assertRpcArtifact(value) {
|
|
492
|
+
if (!isRpcArtifact(value)) {
|
|
493
|
+
throw new Error("Invalid RPC artifact: expected { artifactVersion, routes, schemas }");
|
|
494
|
+
}
|
|
495
|
+
if (value.artifactVersion !== RPC_ARTIFACT_VERSION) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`Unsupported RPC artifact version '${value.artifactVersion}'. Supported: ${RPC_ARTIFACT_VERSION}.`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
463
502
|
// src/services/route-analyzer.service.ts
|
|
464
503
|
import { RouteRegistry } from "honestjs";
|
|
465
504
|
var RouteAnalyzerService = class {
|
|
505
|
+
customClassMatcher;
|
|
506
|
+
onWarn;
|
|
507
|
+
warnings = [];
|
|
508
|
+
constructor(options = {}) {
|
|
509
|
+
this.customClassMatcher = options.customClassMatcher;
|
|
510
|
+
this.onWarn = options.onWarn;
|
|
511
|
+
}
|
|
512
|
+
getWarnings() {
|
|
513
|
+
return this.warnings;
|
|
514
|
+
}
|
|
466
515
|
/**
|
|
467
516
|
* Analyzes controller methods to extract type information
|
|
468
517
|
*/
|
|
469
518
|
async analyzeControllerMethods(project) {
|
|
519
|
+
this.warnings = [];
|
|
470
520
|
const routes = RouteRegistry.getRoutes();
|
|
471
521
|
if (!routes?.length) {
|
|
472
522
|
return [];
|
|
@@ -487,13 +537,20 @@ var RouteAnalyzerService = class {
|
|
|
487
537
|
const classes = sourceFile.getClasses();
|
|
488
538
|
for (const classDeclaration of classes) {
|
|
489
539
|
const className = classDeclaration.getName();
|
|
490
|
-
if (className
|
|
540
|
+
if (className && this.isControllerClass(classDeclaration, className)) {
|
|
491
541
|
controllers.set(className, classDeclaration);
|
|
492
542
|
}
|
|
493
543
|
}
|
|
494
544
|
}
|
|
495
545
|
return controllers;
|
|
496
546
|
}
|
|
547
|
+
isControllerClass(classDeclaration, _className) {
|
|
548
|
+
if (this.customClassMatcher) {
|
|
549
|
+
return this.customClassMatcher(classDeclaration);
|
|
550
|
+
}
|
|
551
|
+
const decoratorNames = classDeclaration.getDecorators().map((decorator) => decorator.getName());
|
|
552
|
+
return decoratorNames.includes("Controller") || decoratorNames.includes("View");
|
|
553
|
+
}
|
|
497
554
|
/**
|
|
498
555
|
* Processes all routes and extracts type information
|
|
499
556
|
*/
|
|
@@ -504,10 +561,9 @@ var RouteAnalyzerService = class {
|
|
|
504
561
|
const extendedRoute = this.createExtendedRoute(route, controllers);
|
|
505
562
|
analyzedRoutes.push(extendedRoute);
|
|
506
563
|
} catch (routeError) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
);
|
|
564
|
+
const warning = `Skipping route ${safeToString(route.controller)}.${safeToString(route.handler)}`;
|
|
565
|
+
this.warnings.push(warning);
|
|
566
|
+
this.onWarn?.(warning, routeError);
|
|
511
567
|
}
|
|
512
568
|
}
|
|
513
569
|
return analyzedRoutes;
|
|
@@ -527,6 +583,10 @@ var RouteAnalyzerService = class {
|
|
|
527
583
|
returns = this.getReturnType(handlerMethod);
|
|
528
584
|
parameters = this.getParametersWithTypes(handlerMethod, route.parameters || []);
|
|
529
585
|
}
|
|
586
|
+
} else {
|
|
587
|
+
const warning = `Controller class not found in source files: ${controllerName} (handler: ${handlerName})`;
|
|
588
|
+
this.warnings.push(warning);
|
|
589
|
+
this.onWarn?.(warning);
|
|
530
590
|
}
|
|
531
591
|
return {
|
|
532
592
|
controller: controllerName,
|
|
@@ -665,14 +725,23 @@ function extractNamedType(type) {
|
|
|
665
725
|
|
|
666
726
|
// src/services/schema-generator.service.ts
|
|
667
727
|
var SchemaGeneratorService = class {
|
|
668
|
-
constructor(controllerPattern, tsConfigPath) {
|
|
728
|
+
constructor(controllerPattern, tsConfigPath, options = {}) {
|
|
669
729
|
this.controllerPattern = controllerPattern;
|
|
670
730
|
this.tsConfigPath = tsConfigPath;
|
|
731
|
+
this.failOnSchemaError = options.failOnSchemaError ?? false;
|
|
732
|
+
this.onWarn = options.onWarn;
|
|
733
|
+
}
|
|
734
|
+
failOnSchemaError;
|
|
735
|
+
onWarn;
|
|
736
|
+
warnings = [];
|
|
737
|
+
getWarnings() {
|
|
738
|
+
return this.warnings;
|
|
671
739
|
}
|
|
672
740
|
/**
|
|
673
741
|
* Generates JSON schemas from types used in controllers
|
|
674
742
|
*/
|
|
675
743
|
async generateSchemas(project) {
|
|
744
|
+
this.warnings = [];
|
|
676
745
|
const sourceFiles = project.getSourceFiles(this.controllerPattern);
|
|
677
746
|
const collectedTypes = this.collectTypesFromControllers(sourceFiles);
|
|
678
747
|
return this.processTypes(collectedTypes);
|
|
@@ -719,7 +788,12 @@ var SchemaGeneratorService = class {
|
|
|
719
788
|
typescriptType
|
|
720
789
|
});
|
|
721
790
|
} catch (err) {
|
|
722
|
-
|
|
791
|
+
if (this.failOnSchemaError) {
|
|
792
|
+
throw err;
|
|
793
|
+
}
|
|
794
|
+
const warning = `Failed to generate schema for ${typeName}`;
|
|
795
|
+
this.warnings.push(warning);
|
|
796
|
+
this.onWarn?.(warning, err);
|
|
723
797
|
}
|
|
724
798
|
}
|
|
725
799
|
return schemas;
|
|
@@ -738,7 +812,12 @@ var SchemaGeneratorService = class {
|
|
|
738
812
|
});
|
|
739
813
|
return generator.createSchema(typeName);
|
|
740
814
|
} catch (error) {
|
|
741
|
-
|
|
815
|
+
if (this.failOnSchemaError) {
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
818
|
+
const warning = `Failed to generate schema for type ${typeName}`;
|
|
819
|
+
this.warnings.push(warning);
|
|
820
|
+
this.onWarn?.(warning, error);
|
|
742
821
|
return {
|
|
743
822
|
type: "object",
|
|
744
823
|
properties: {},
|
|
@@ -756,27 +835,44 @@ var RPCPlugin = class {
|
|
|
756
835
|
generateOnInit;
|
|
757
836
|
contextNamespace;
|
|
758
837
|
contextArtifactKey;
|
|
838
|
+
mode;
|
|
839
|
+
logLevel;
|
|
840
|
+
failOnSchemaError;
|
|
841
|
+
failOnRouteAnalysisWarning;
|
|
842
|
+
customClassMatcher;
|
|
759
843
|
// Services
|
|
760
844
|
routeAnalyzer;
|
|
761
845
|
schemaGenerator;
|
|
762
|
-
|
|
846
|
+
generators;
|
|
763
847
|
// Shared ts-morph project
|
|
764
848
|
project = null;
|
|
765
849
|
// Internal state
|
|
766
850
|
analyzedRoutes = [];
|
|
767
851
|
analyzedSchemas = [];
|
|
768
|
-
|
|
852
|
+
generatedInfos = [];
|
|
853
|
+
diagnostics = null;
|
|
769
854
|
app = null;
|
|
770
855
|
constructor(options = {}) {
|
|
771
856
|
this.controllerPattern = options.controllerPattern ?? DEFAULT_OPTIONS.controllerPattern;
|
|
772
857
|
this.tsConfigPath = options.tsConfigPath ?? path3.resolve(process.cwd(), DEFAULT_OPTIONS.tsConfigPath);
|
|
773
858
|
this.outputDir = options.outputDir ?? path3.resolve(process.cwd(), DEFAULT_OPTIONS.outputDir);
|
|
774
859
|
this.generateOnInit = options.generateOnInit ?? DEFAULT_OPTIONS.generateOnInit;
|
|
860
|
+
this.mode = options.mode ?? DEFAULT_OPTIONS.mode;
|
|
861
|
+
this.logLevel = options.logLevel ?? DEFAULT_OPTIONS.logLevel;
|
|
775
862
|
this.contextNamespace = options.context?.namespace ?? DEFAULT_OPTIONS.context.namespace;
|
|
776
863
|
this.contextArtifactKey = options.context?.keys?.artifact ?? DEFAULT_OPTIONS.context.keys.artifact;
|
|
777
|
-
this.
|
|
778
|
-
this.
|
|
779
|
-
this.
|
|
864
|
+
this.customClassMatcher = options.customClassMatcher;
|
|
865
|
+
this.failOnSchemaError = options.failOnSchemaError ?? this.mode === "strict";
|
|
866
|
+
this.failOnRouteAnalysisWarning = options.failOnRouteAnalysisWarning ?? this.mode === "strict";
|
|
867
|
+
this.routeAnalyzer = new RouteAnalyzerService({
|
|
868
|
+
customClassMatcher: this.customClassMatcher,
|
|
869
|
+
onWarn: (message, details) => this.logWarn(message, details)
|
|
870
|
+
});
|
|
871
|
+
this.schemaGenerator = new SchemaGeneratorService(this.controllerPattern, this.tsConfigPath, {
|
|
872
|
+
failOnSchemaError: this.failOnSchemaError,
|
|
873
|
+
onWarn: (message, details) => this.logWarn(message, details)
|
|
874
|
+
});
|
|
875
|
+
this.generators = options.generators ?? [new TypeScriptClientGenerator(this.outputDir)];
|
|
780
876
|
this.validateConfiguration();
|
|
781
877
|
}
|
|
782
878
|
/**
|
|
@@ -797,17 +893,31 @@ var RPCPlugin = class {
|
|
|
797
893
|
if (!this.outputDir?.trim()) {
|
|
798
894
|
errors.push("Output directory cannot be empty");
|
|
799
895
|
}
|
|
896
|
+
if (!["strict", "best-effort"].includes(this.mode)) {
|
|
897
|
+
errors.push('Mode must be "strict" or "best-effort"');
|
|
898
|
+
}
|
|
899
|
+
if (!["silent", "error", "warn", "info", "debug"].includes(this.logLevel)) {
|
|
900
|
+
errors.push("logLevel must be one of: silent, error, warn, info, debug");
|
|
901
|
+
}
|
|
800
902
|
if (!this.contextNamespace?.trim()) {
|
|
801
903
|
errors.push("Context namespace cannot be empty");
|
|
802
904
|
}
|
|
803
905
|
if (!this.contextArtifactKey?.trim()) {
|
|
804
906
|
errors.push("Context artifact key cannot be empty");
|
|
805
907
|
}
|
|
908
|
+
for (const generator of this.generators) {
|
|
909
|
+
if (!generator.name?.trim()) {
|
|
910
|
+
errors.push("Generator name cannot be empty");
|
|
911
|
+
}
|
|
912
|
+
if (typeof generator.generate !== "function") {
|
|
913
|
+
errors.push(`Generator "${generator.name || "unknown"}" must implement generate(context)`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
806
916
|
if (errors.length > 0) {
|
|
807
917
|
throw new Error(`Configuration validation failed: ${errors.join(", ")}`);
|
|
808
918
|
}
|
|
809
919
|
this.log(
|
|
810
|
-
`Configuration validated: controllerPattern=${this.controllerPattern}, tsConfigPath=${this.tsConfigPath}, outputDir=${this.outputDir}`
|
|
920
|
+
`Configuration validated: controllerPattern=${this.controllerPattern}, tsConfigPath=${this.tsConfigPath}, outputDir=${this.outputDir}, mode=${this.mode}`
|
|
811
921
|
);
|
|
812
922
|
}
|
|
813
923
|
/**
|
|
@@ -816,14 +926,17 @@ var RPCPlugin = class {
|
|
|
816
926
|
afterModulesRegistered = async (app, hono) => {
|
|
817
927
|
this.app = app;
|
|
818
928
|
if (this.generateOnInit) {
|
|
819
|
-
await this.analyzeEverything();
|
|
929
|
+
await this.analyzeEverything({ force: false, dryRun: false });
|
|
820
930
|
this.publishArtifact(app);
|
|
821
931
|
}
|
|
822
932
|
};
|
|
823
933
|
/**
|
|
824
934
|
* Main analysis method that coordinates all three components
|
|
825
935
|
*/
|
|
826
|
-
async analyzeEverything(
|
|
936
|
+
async analyzeEverything(options) {
|
|
937
|
+
const { force, dryRun } = options;
|
|
938
|
+
const warnings = [];
|
|
939
|
+
let cacheState = force ? "bypass" : "miss";
|
|
827
940
|
try {
|
|
828
941
|
this.log("Starting comprehensive RPC analysis...");
|
|
829
942
|
this.dispose();
|
|
@@ -835,21 +948,50 @@ var RPCPlugin = class {
|
|
|
835
948
|
const stored = readChecksum(this.outputDir);
|
|
836
949
|
if (stored && stored.hash === currentHash && this.outputFilesExist()) {
|
|
837
950
|
if (this.loadArtifactFromDisk()) {
|
|
838
|
-
|
|
951
|
+
cacheState = "hit";
|
|
952
|
+
this.logDebug("Source files unchanged - skipping regeneration");
|
|
953
|
+
this.diagnostics = {
|
|
954
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
955
|
+
mode: this.mode,
|
|
956
|
+
dryRun,
|
|
957
|
+
cache: cacheState,
|
|
958
|
+
routesCount: this.analyzedRoutes.length,
|
|
959
|
+
schemasCount: this.analyzedSchemas.length,
|
|
960
|
+
warnings: []
|
|
961
|
+
};
|
|
839
962
|
this.dispose();
|
|
840
963
|
return;
|
|
841
964
|
}
|
|
842
|
-
this.
|
|
965
|
+
this.logDebug("Source files unchanged but cached artifact missing/invalid - regenerating");
|
|
843
966
|
}
|
|
844
967
|
}
|
|
845
968
|
this.analyzedRoutes = [];
|
|
846
969
|
this.analyzedSchemas = [];
|
|
847
|
-
this.
|
|
970
|
+
this.generatedInfos = [];
|
|
848
971
|
this.analyzedRoutes = await this.routeAnalyzer.analyzeControllerMethods(this.project);
|
|
972
|
+
warnings.push(...this.routeAnalyzer.getWarnings());
|
|
849
973
|
this.analyzedSchemas = await this.schemaGenerator.generateSchemas(this.project);
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
974
|
+
warnings.push(...this.schemaGenerator.getWarnings());
|
|
975
|
+
if (this.failOnRouteAnalysisWarning && this.routeAnalyzer.getWarnings().length > 0) {
|
|
976
|
+
throw new Error(`Route analysis warnings encountered in strict mode: ${this.routeAnalyzer.getWarnings().join("; ")}`);
|
|
977
|
+
}
|
|
978
|
+
if (!dryRun) {
|
|
979
|
+
this.generatedInfos = await this.runGenerators();
|
|
980
|
+
}
|
|
981
|
+
if (!dryRun) {
|
|
982
|
+
await writeChecksum(this.outputDir, { hash: computeHash(filePaths), files: filePaths });
|
|
983
|
+
this.writeArtifactToDisk();
|
|
984
|
+
}
|
|
985
|
+
this.diagnostics = {
|
|
986
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
987
|
+
mode: this.mode,
|
|
988
|
+
dryRun,
|
|
989
|
+
cache: cacheState,
|
|
990
|
+
routesCount: this.analyzedRoutes.length,
|
|
991
|
+
schemasCount: this.analyzedSchemas.length,
|
|
992
|
+
warnings
|
|
993
|
+
};
|
|
994
|
+
this.writeDiagnosticsToDisk();
|
|
853
995
|
this.log(
|
|
854
996
|
`\u2705 RPC analysis complete: ${this.analyzedRoutes.length} routes, ${this.analyzedSchemas.length} schemas`
|
|
855
997
|
);
|
|
@@ -859,13 +1001,10 @@ var RPCPlugin = class {
|
|
|
859
1001
|
throw error;
|
|
860
1002
|
}
|
|
861
1003
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
async analyze(force = true) {
|
|
867
|
-
await this.analyzeEverything(force);
|
|
868
|
-
if (this.app) {
|
|
1004
|
+
async analyze(forceOrOptions = true) {
|
|
1005
|
+
const options = typeof forceOrOptions === "boolean" ? { force: forceOrOptions, dryRun: false } : { force: forceOrOptions.force ?? true, dryRun: forceOrOptions.dryRun ?? false };
|
|
1006
|
+
await this.analyzeEverything(options);
|
|
1007
|
+
if (this.app && !options.dryRun) {
|
|
869
1008
|
this.publishArtifact(this.app);
|
|
870
1009
|
}
|
|
871
1010
|
}
|
|
@@ -885,42 +1024,89 @@ var RPCPlugin = class {
|
|
|
885
1024
|
* Get the generation info
|
|
886
1025
|
*/
|
|
887
1026
|
getGenerationInfo() {
|
|
888
|
-
return this.
|
|
1027
|
+
return this.generatedInfos[0] ?? null;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Get all generation infos
|
|
1031
|
+
*/
|
|
1032
|
+
getGenerationInfos() {
|
|
1033
|
+
return this.generatedInfos;
|
|
1034
|
+
}
|
|
1035
|
+
getDiagnostics() {
|
|
1036
|
+
return this.diagnostics;
|
|
889
1037
|
}
|
|
890
1038
|
/**
|
|
891
1039
|
* Checks whether expected output files exist on disk
|
|
892
1040
|
*/
|
|
893
1041
|
outputFilesExist() {
|
|
894
|
-
|
|
1042
|
+
if (!fs2.existsSync(path3.join(this.outputDir, "rpc-artifact.json"))) {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
if (!this.hasTypeScriptGenerator()) {
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
return fs2.existsSync(path3.join(this.outputDir, "client.ts"));
|
|
895
1049
|
}
|
|
896
1050
|
getArtifactPath() {
|
|
897
1051
|
return path3.join(this.outputDir, "rpc-artifact.json");
|
|
898
1052
|
}
|
|
1053
|
+
getDiagnosticsPath() {
|
|
1054
|
+
return path3.join(this.outputDir, "rpc-diagnostics.json");
|
|
1055
|
+
}
|
|
899
1056
|
writeArtifactToDisk() {
|
|
900
1057
|
const artifact = {
|
|
1058
|
+
artifactVersion: RPC_ARTIFACT_VERSION,
|
|
901
1059
|
routes: this.analyzedRoutes,
|
|
902
1060
|
schemas: this.analyzedSchemas
|
|
903
1061
|
};
|
|
904
1062
|
fs2.mkdirSync(this.outputDir, { recursive: true });
|
|
905
1063
|
fs2.writeFileSync(this.getArtifactPath(), JSON.stringify(artifact));
|
|
906
1064
|
}
|
|
1065
|
+
writeDiagnosticsToDisk() {
|
|
1066
|
+
if (!this.diagnostics) return;
|
|
1067
|
+
fs2.mkdirSync(this.outputDir, { recursive: true });
|
|
1068
|
+
fs2.writeFileSync(this.getDiagnosticsPath(), JSON.stringify(this.diagnostics, null, 2));
|
|
1069
|
+
}
|
|
907
1070
|
loadArtifactFromDisk() {
|
|
908
1071
|
try {
|
|
909
1072
|
const raw = fs2.readFileSync(this.getArtifactPath(), "utf8");
|
|
910
1073
|
const parsed = JSON.parse(raw);
|
|
911
|
-
if (
|
|
912
|
-
|
|
1074
|
+
if (parsed.artifactVersion === void 0) {
|
|
1075
|
+
if (!Array.isArray(parsed.routes) || !Array.isArray(parsed.schemas)) {
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
this.analyzedRoutes = parsed.routes;
|
|
1079
|
+
this.analyzedSchemas = parsed.schemas;
|
|
1080
|
+
} else {
|
|
1081
|
+
assertRpcArtifact(parsed);
|
|
1082
|
+
this.analyzedRoutes = parsed.routes;
|
|
1083
|
+
this.analyzedSchemas = parsed.schemas;
|
|
913
1084
|
}
|
|
914
|
-
this.
|
|
915
|
-
this.analyzedSchemas = parsed.schemas;
|
|
916
|
-
this.generatedInfo = null;
|
|
1085
|
+
this.generatedInfos = [];
|
|
917
1086
|
return true;
|
|
918
1087
|
} catch {
|
|
919
1088
|
return false;
|
|
920
1089
|
}
|
|
921
1090
|
}
|
|
1091
|
+
async runGenerators() {
|
|
1092
|
+
const results = [];
|
|
1093
|
+
for (const generator of this.generators) {
|
|
1094
|
+
this.log(`Running generator: ${generator.name}`);
|
|
1095
|
+
const result = await generator.generate({
|
|
1096
|
+
outputDir: this.outputDir,
|
|
1097
|
+
routes: this.analyzedRoutes,
|
|
1098
|
+
schemas: this.analyzedSchemas
|
|
1099
|
+
});
|
|
1100
|
+
results.push(result);
|
|
1101
|
+
}
|
|
1102
|
+
return results;
|
|
1103
|
+
}
|
|
1104
|
+
hasTypeScriptGenerator() {
|
|
1105
|
+
return this.generators.some((generator) => generator.name === "typescript-client");
|
|
1106
|
+
}
|
|
922
1107
|
publishArtifact(app) {
|
|
923
1108
|
app.getContext().set(this.getArtifactContextKey(), {
|
|
1109
|
+
artifactVersion: RPC_ARTIFACT_VERSION,
|
|
924
1110
|
routes: this.analyzedRoutes,
|
|
925
1111
|
schemas: this.analyzedSchemas
|
|
926
1112
|
});
|
|
@@ -944,31 +1130,58 @@ var RPCPlugin = class {
|
|
|
944
1130
|
* Logs a message with the plugin prefix
|
|
945
1131
|
*/
|
|
946
1132
|
log(message) {
|
|
947
|
-
|
|
1133
|
+
if (this.canLog("info")) {
|
|
1134
|
+
console.log(`${LOG_PREFIX} ${message}`);
|
|
1135
|
+
}
|
|
948
1136
|
}
|
|
949
1137
|
/**
|
|
950
1138
|
* Logs an error with the plugin prefix
|
|
951
1139
|
*/
|
|
952
1140
|
logError(message, error) {
|
|
953
|
-
|
|
1141
|
+
if (this.canLog("error")) {
|
|
1142
|
+
console.error(`${LOG_PREFIX} ${message}`, error || "");
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
logWarn(message, details) {
|
|
1146
|
+
if (this.canLog("warn")) {
|
|
1147
|
+
console.warn(`${LOG_PREFIX} ${message}`, details || "");
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
logDebug(message) {
|
|
1151
|
+
if (this.canLog("debug")) {
|
|
1152
|
+
console.log(`${LOG_PREFIX} ${message}`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
canLog(level) {
|
|
1156
|
+
const order = {
|
|
1157
|
+
silent: 0,
|
|
1158
|
+
error: 1,
|
|
1159
|
+
warn: 2,
|
|
1160
|
+
info: 3,
|
|
1161
|
+
debug: 4
|
|
1162
|
+
};
|
|
1163
|
+
return order[this.logLevel] >= order[level];
|
|
954
1164
|
}
|
|
955
1165
|
};
|
|
956
1166
|
export {
|
|
957
1167
|
BUILTIN_TYPES,
|
|
958
1168
|
BUILTIN_UTILITY_TYPES,
|
|
959
|
-
ClientGeneratorService,
|
|
960
1169
|
DEFAULT_OPTIONS,
|
|
961
1170
|
GENERIC_TYPES,
|
|
962
1171
|
LOG_PREFIX,
|
|
963
1172
|
RPCPlugin,
|
|
1173
|
+
RPC_ARTIFACT_VERSION,
|
|
964
1174
|
RouteAnalyzerService,
|
|
965
1175
|
SchemaGeneratorService,
|
|
1176
|
+
TypeScriptClientGenerator,
|
|
1177
|
+
assertRpcArtifact,
|
|
966
1178
|
buildFullApiPath,
|
|
967
1179
|
buildFullPath,
|
|
968
1180
|
camelCase,
|
|
969
1181
|
computeHash,
|
|
970
1182
|
extractNamedType,
|
|
971
1183
|
generateTypeScriptInterface,
|
|
1184
|
+
isRpcArtifact,
|
|
972
1185
|
mapJsonSchemaTypeToTypeScript,
|
|
973
1186
|
readChecksum,
|
|
974
1187
|
safeToString,
|