@rexeus/typeweaver 0.0.3 → 0.0.4
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 +403 -70
- package/dist/LICENSE +202 -0
- package/dist/NOTICE +4 -0
- package/dist/{cli-CteKWdCY.js → cli-Cz6q9I7F.js} +941 -296
- package/dist/{flow--vV0j3Y-.js → flow-q2wMXrDa.js} +8 -8
- package/dist/{glimmer-B-ODUU1A.js → glimmer-wgjvri6H.js} +34 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -7
- package/dist/{markdown-Xi16tYTk.js → markdown-Nz6Lc3gB.js} +4 -4
- package/dist/{postcss-DdgOJBTx.js → postcss-yEOijaXJ.js} +42 -42
- package/dist/run-cli-with-tsx.js +8 -7
- package/dist/templates/Index.ejs +8 -0
- package/dist/{typescript-C4gnKzhB.js → typescript-Cv79a1Qz.js} +2 -2
- package/dist/{yaml-B0tq6Ttj.js → yaml-DT3qlFoE.js} +2 -2
- package/package.json +29 -25
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import path__default, { dirname } from 'path';
|
|
3
|
+
import url, { fileURLToPath, pathToFileURL, URL as URL$1 } from 'url';
|
|
4
|
+
import { Command } from 'commander';
|
|
3
5
|
import fs, { realpathSync, statSync } from 'fs';
|
|
4
|
-
import {
|
|
6
|
+
import { PluginRegistry, PluginContextBuilder } from '@rexeus/typeweaver-gen';
|
|
7
|
+
import TypesPlugin from '@rexeus/typeweaver-types';
|
|
8
|
+
import { render } from 'ejs';
|
|
5
9
|
import { createRequire, builtinModules } from 'module';
|
|
6
|
-
import url, { fileURLToPath, pathToFileURL, URL as URL$1 } from 'url';
|
|
7
10
|
import process3 from 'process';
|
|
8
11
|
import os from 'os';
|
|
9
12
|
import tty from 'tty';
|
|
@@ -11,203 +14,140 @@ import fs$1 from 'fs/promises';
|
|
|
11
14
|
import assert2 from 'assert';
|
|
12
15
|
import v8 from 'v8';
|
|
13
16
|
import { format, inspect } from 'util';
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import TypesPlugin from '@rexeus/typeweaver-types';
|
|
17
|
-
import { Command } from 'commander';
|
|
17
|
+
import { HttpMethod, HttpStatusCode, HttpResponseDefinition, HttpOperationDefinition, HttpStatusCodeNameMap } from '@rexeus/typeweaver-core';
|
|
18
|
+
import { z } from 'zod/v4';
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
var version = "0.0.4";
|
|
21
|
+
var packageJson = {
|
|
22
|
+
version: version};
|
|
23
|
+
|
|
24
|
+
class IndexFileGenerator {
|
|
25
|
+
constructor(templateDir) {
|
|
26
|
+
this.templateDir = templateDir;
|
|
27
|
+
}
|
|
28
|
+
generate(context) {
|
|
29
|
+
const templateFilePath = path__default.join(this.templateDir, "Index.ejs");
|
|
30
|
+
const template = fs.readFileSync(templateFilePath, "utf8");
|
|
31
|
+
const indexPaths = /* @__PURE__ */ new Set();
|
|
32
|
+
for (const generatedFile of context.getGeneratedFiles()) {
|
|
33
|
+
indexPaths.add(`./${generatedFile.replace(/\.ts$/, "")}`);
|
|
34
|
+
}
|
|
35
|
+
const content = render(template, {
|
|
36
|
+
indexPaths: Array.from(indexPaths).sort()
|
|
37
|
+
});
|
|
38
|
+
fs.writeFileSync(path__default.join(context.outputDir, "index.ts"), content);
|
|
23
39
|
}
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
class
|
|
27
|
-
constructor(
|
|
28
|
-
super(
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
31
|
-
this.
|
|
32
|
-
this.explanation = explanation;
|
|
42
|
+
class PluginLoadingFailure extends Error {
|
|
43
|
+
constructor(pluginName, attempts) {
|
|
44
|
+
super(`Failed to load plugin '${pluginName}'`);
|
|
45
|
+
this.pluginName = pluginName;
|
|
46
|
+
this.attempts = attempts;
|
|
47
|
+
Object.setPrototypeOf(this, PluginLoadingFailure.prototype);
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
50
|
|
|
36
|
-
class
|
|
37
|
-
constructor(
|
|
38
|
-
this.
|
|
51
|
+
class PluginLoader {
|
|
52
|
+
constructor(registry, requiredPlugins, strategies = ["npm", "local"]) {
|
|
53
|
+
this.registry = registry;
|
|
54
|
+
this.requiredPlugins = requiredPlugins;
|
|
55
|
+
this.strategies = strategies;
|
|
39
56
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
sharedResponseResources: []
|
|
47
|
-
};
|
|
48
|
-
const sharedDefinitions = contents.find(
|
|
49
|
-
(content) => content.name === "shared"
|
|
50
|
-
);
|
|
51
|
-
if (sharedDefinitions) {
|
|
52
|
-
if (!sharedDefinitions.isDirectory()) {
|
|
53
|
-
throw new InvalidSharedDirError("'shared' is a file, not a directory");
|
|
54
|
-
}
|
|
55
|
-
result.sharedResponseResources = await this.getSharedResponseResources();
|
|
56
|
-
console.info(
|
|
57
|
-
`Found '${result.sharedResponseResources.length}' shared responses`
|
|
58
|
-
);
|
|
59
|
-
} else {
|
|
60
|
-
console.info("No 'shared' directory found");
|
|
57
|
+
/**
|
|
58
|
+
* Load all plugins from configuration
|
|
59
|
+
*/
|
|
60
|
+
async loadPlugins(config) {
|
|
61
|
+
for (const requiredPlugin of this.requiredPlugins) {
|
|
62
|
+
this.registry.register(requiredPlugin);
|
|
61
63
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.info(`Skipping '${content.name}' as it is not a directory`);
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (content.name === "shared") {
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
const entityName = content.name;
|
|
71
|
-
const entitySourceDir = path__default.join(this.config.sourceDir, entityName);
|
|
72
|
-
const operationResources = await this.getEntityOperationResources(
|
|
73
|
-
entitySourceDir,
|
|
74
|
-
entityName
|
|
75
|
-
);
|
|
76
|
-
result.entityResources[entityName] = operationResources;
|
|
77
|
-
console.info(
|
|
78
|
-
`Found '${operationResources.length}' operation definitions for entity '${entityName}'`
|
|
79
|
-
);
|
|
64
|
+
if (!config?.plugins) {
|
|
65
|
+
return;
|
|
80
66
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
for (const content of sharedContents) {
|
|
89
|
-
if (!content.isFile()) {
|
|
90
|
-
console.info(`Skipping '${content.name}' as it is not a file`);
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const sourceFileName = content.name;
|
|
94
|
-
const sourceFile = path__default.join(this.config.sharedSourceDir, sourceFileName);
|
|
95
|
-
const definition = await import(sourceFile);
|
|
96
|
-
if (!definition.default) {
|
|
97
|
-
console.info(
|
|
98
|
-
`Skipping '${sourceFile}' as it does not have a default export`
|
|
99
|
-
);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (!(definition.default instanceof HttpResponseDefinition)) {
|
|
103
|
-
console.info(
|
|
104
|
-
`Skipping '${sourceFile}' as it is not an instance of HttpResponseDefinition`
|
|
105
|
-
);
|
|
106
|
-
continue;
|
|
67
|
+
const successful = [];
|
|
68
|
+
for (const plugin of config.plugins) {
|
|
69
|
+
let result;
|
|
70
|
+
if (typeof plugin === "string") {
|
|
71
|
+
result = await this.loadPlugin(plugin);
|
|
72
|
+
} else {
|
|
73
|
+
result = await this.loadPlugin(plugin[0]);
|
|
107
74
|
}
|
|
108
|
-
if (
|
|
109
|
-
throw new
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
sourceFile,
|
|
113
|
-
"'isShared' property is not set to 'true'"
|
|
75
|
+
if (result.success === false) {
|
|
76
|
+
throw new PluginLoadingFailure(
|
|
77
|
+
result.error.pluginName,
|
|
78
|
+
result.error.attempts
|
|
114
79
|
);
|
|
115
80
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const outputFile = path__default.join(outputDir, outputFileName);
|
|
119
|
-
sharedResponseResources.push({
|
|
120
|
-
...definition.default,
|
|
121
|
-
isShared: true,
|
|
122
|
-
sourceDir: this.config.sharedSourceDir,
|
|
123
|
-
sourceFile,
|
|
124
|
-
sourceFileName,
|
|
125
|
-
outputFile,
|
|
126
|
-
outputFileName,
|
|
127
|
-
outputDir
|
|
128
|
-
});
|
|
81
|
+
successful.push(result.value);
|
|
82
|
+
this.registry.register(result.value.plugin);
|
|
129
83
|
}
|
|
130
|
-
|
|
84
|
+
this.reportSuccessfulLoads(successful);
|
|
131
85
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Load a plugin from a string identifier
|
|
88
|
+
*/
|
|
89
|
+
async loadPlugin(pluginName) {
|
|
90
|
+
const possiblePaths = this.generatePluginPaths(pluginName);
|
|
91
|
+
const attempts = [];
|
|
92
|
+
for (const possiblePath of possiblePaths) {
|
|
93
|
+
try {
|
|
94
|
+
const pluginPackage = await import(possiblePath);
|
|
95
|
+
if (pluginPackage.default) {
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
value: {
|
|
99
|
+
plugin: new pluginPackage.default(),
|
|
100
|
+
source: possiblePath
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
attempts.push({
|
|
105
|
+
path: possiblePath,
|
|
106
|
+
error: "No default export found"
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
attempts.push({
|
|
110
|
+
path: possiblePath,
|
|
111
|
+
error: error instanceof Error ? error.message : String(error)
|
|
112
|
+
});
|
|
150
113
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
error: {
|
|
118
|
+
pluginName,
|
|
119
|
+
attempts
|
|
156
120
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const outputClientFileName = `${operationId}Client.ts`;
|
|
174
|
-
const outputClientFile = path__default.join(outputDir, outputClientFileName);
|
|
175
|
-
const operationResource = {
|
|
176
|
-
sourceDir,
|
|
177
|
-
sourceFile,
|
|
178
|
-
sourceFileName,
|
|
179
|
-
definition: {
|
|
180
|
-
...definition.default,
|
|
181
|
-
responses: []
|
|
182
|
-
},
|
|
183
|
-
outputDir,
|
|
184
|
-
entityName,
|
|
185
|
-
outputRequestFile,
|
|
186
|
-
outputResponseFile,
|
|
187
|
-
outputResponseValidationFile,
|
|
188
|
-
outputRequestValidationFile,
|
|
189
|
-
outputRequestFileName,
|
|
190
|
-
outputRequestValidationFileName,
|
|
191
|
-
outputResponseFileName,
|
|
192
|
-
outputResponseValidationFileName,
|
|
193
|
-
outputClientFile,
|
|
194
|
-
outputClientFileName
|
|
195
|
-
};
|
|
196
|
-
if (!definition.default.responses) {
|
|
197
|
-
throw new Error(
|
|
198
|
-
`Operation '${operationId}' does not have any responses`
|
|
199
|
-
);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Generate possible plugin paths based on configured strategies
|
|
125
|
+
*/
|
|
126
|
+
generatePluginPaths(pluginName) {
|
|
127
|
+
const paths = [];
|
|
128
|
+
for (const strategy of this.strategies) {
|
|
129
|
+
switch (strategy) {
|
|
130
|
+
case "npm":
|
|
131
|
+
paths.push(`@rexeus/typeweaver-${pluginName}`);
|
|
132
|
+
paths.push(`@rexeus/${pluginName}`);
|
|
133
|
+
break;
|
|
134
|
+
case "local":
|
|
135
|
+
paths.push(pluginName);
|
|
136
|
+
break;
|
|
200
137
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
138
|
+
}
|
|
139
|
+
return paths;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Report successful plugin loads
|
|
143
|
+
*/
|
|
144
|
+
reportSuccessfulLoads(successful) {
|
|
145
|
+
if (successful.length > 0) {
|
|
146
|
+
console.info(`Successfully loaded ${successful.length} plugin(s):`);
|
|
147
|
+
for (const result of successful) {
|
|
148
|
+
console.info(` - ${result.plugin.name} (from ${result.source})`);
|
|
207
149
|
}
|
|
208
|
-
definitions.push(operationResource);
|
|
209
150
|
}
|
|
210
|
-
return definitions;
|
|
211
151
|
}
|
|
212
152
|
}
|
|
213
153
|
|
|
@@ -22453,11 +22393,11 @@ var { parsers, printers } = createParsersAndPrinters([
|
|
|
22453
22393
|
printers: ["estree", "estree-json"]
|
|
22454
22394
|
},
|
|
22455
22395
|
{
|
|
22456
|
-
importPlugin: () => import('./flow
|
|
22396
|
+
importPlugin: () => import('./flow-q2wMXrDa.js'),
|
|
22457
22397
|
parsers: ["flow"]
|
|
22458
22398
|
},
|
|
22459
22399
|
{
|
|
22460
|
-
importPlugin: () => import('./glimmer-
|
|
22400
|
+
importPlugin: () => import('./glimmer-wgjvri6H.js'),
|
|
22461
22401
|
parsers: ["glimmer"],
|
|
22462
22402
|
printers: ["glimmer"]
|
|
22463
22403
|
},
|
|
@@ -22472,7 +22412,7 @@ var { parsers, printers } = createParsersAndPrinters([
|
|
|
22472
22412
|
printers: ["html"]
|
|
22473
22413
|
},
|
|
22474
22414
|
{
|
|
22475
|
-
importPlugin: () => import('./markdown-
|
|
22415
|
+
importPlugin: () => import('./markdown-Nz6Lc3gB.js'),
|
|
22476
22416
|
parsers: ["markdown", "mdx", "remark"],
|
|
22477
22417
|
printers: ["mdast"]
|
|
22478
22418
|
},
|
|
@@ -22481,16 +22421,16 @@ var { parsers, printers } = createParsersAndPrinters([
|
|
|
22481
22421
|
parsers: ["meriyah"]
|
|
22482
22422
|
},
|
|
22483
22423
|
{
|
|
22484
|
-
importPlugin: () => import('./postcss-
|
|
22424
|
+
importPlugin: () => import('./postcss-yEOijaXJ.js'),
|
|
22485
22425
|
parsers: ["css", "less", "scss"],
|
|
22486
22426
|
printers: ["postcss"]
|
|
22487
22427
|
},
|
|
22488
22428
|
{
|
|
22489
|
-
importPlugin: () => import('./typescript-
|
|
22429
|
+
importPlugin: () => import('./typescript-Cv79a1Qz.js'),
|
|
22490
22430
|
parsers: ["typescript"]
|
|
22491
22431
|
},
|
|
22492
22432
|
{
|
|
22493
|
-
importPlugin: () => import('./yaml-
|
|
22433
|
+
importPlugin: () => import('./yaml-DT3qlFoE.js'),
|
|
22494
22434
|
parsers: ["yaml"],
|
|
22495
22435
|
printers: ["yaml"]
|
|
22496
22436
|
}
|
|
@@ -22810,7 +22750,6 @@ var debugApis = {
|
|
|
22810
22750
|
printDocToString: withPlugins(printDocToString2),
|
|
22811
22751
|
mockable: mockable_default
|
|
22812
22752
|
};
|
|
22813
|
-
var src_default = index_exports;
|
|
22814
22753
|
|
|
22815
22754
|
class Prettier {
|
|
22816
22755
|
constructor(outputDir) {
|
|
@@ -22825,7 +22764,7 @@ class Prettier {
|
|
|
22825
22764
|
if (content.isFile()) {
|
|
22826
22765
|
const filePath = path__default.join(targetDir, content.name);
|
|
22827
22766
|
const unformatted = fs.readFileSync(filePath, "utf8");
|
|
22828
|
-
const formatted = await
|
|
22767
|
+
const formatted = await format2(unformatted, {
|
|
22829
22768
|
parser: "typescript"
|
|
22830
22769
|
});
|
|
22831
22770
|
fs.writeFileSync(filePath, formatted);
|
|
@@ -22836,133 +22775,823 @@ class Prettier {
|
|
|
22836
22775
|
}
|
|
22837
22776
|
}
|
|
22838
22777
|
|
|
22839
|
-
class
|
|
22840
|
-
constructor(
|
|
22841
|
-
|
|
22778
|
+
class DuplicateOperationIdError extends Error {
|
|
22779
|
+
constructor(operationId, firstFile, duplicateFile) {
|
|
22780
|
+
super(
|
|
22781
|
+
`Duplicate operation ID '${operationId}' found.
|
|
22782
|
+
First defined in: \`${firstFile}\`
|
|
22783
|
+
Duplicate found in: \`${duplicateFile}\``
|
|
22784
|
+
);
|
|
22785
|
+
this.operationId = operationId;
|
|
22786
|
+
this.firstFile = firstFile;
|
|
22787
|
+
this.duplicateFile = duplicateFile;
|
|
22842
22788
|
}
|
|
22843
|
-
|
|
22844
|
-
|
|
22845
|
-
|
|
22846
|
-
|
|
22847
|
-
|
|
22848
|
-
|
|
22789
|
+
}
|
|
22790
|
+
|
|
22791
|
+
class DuplicateResponseNameError extends Error {
|
|
22792
|
+
constructor(responseName, firstFile, duplicateFile) {
|
|
22793
|
+
super(
|
|
22794
|
+
`Duplicate response name '${responseName}' found.
|
|
22795
|
+
First defined in: \`${firstFile}\`
|
|
22796
|
+
Duplicate found in: \`${duplicateFile}\``
|
|
22797
|
+
);
|
|
22798
|
+
this.responseName = responseName;
|
|
22799
|
+
this.firstFile = firstFile;
|
|
22800
|
+
this.duplicateFile = duplicateFile;
|
|
22801
|
+
}
|
|
22802
|
+
}
|
|
22803
|
+
|
|
22804
|
+
class DuplicateRouteError extends Error {
|
|
22805
|
+
constructor(path, method, firstOperationId, firstFile, duplicateOperationId, duplicateFile) {
|
|
22806
|
+
super(
|
|
22807
|
+
`Duplicate route '${method} ${path}' found.
|
|
22808
|
+
First defined by operation '${firstOperationId}' in: \`${firstFile}\`
|
|
22809
|
+
Duplicate defined by operation '${duplicateOperationId}' in: \`${duplicateFile}\``
|
|
22810
|
+
);
|
|
22811
|
+
this.path = path;
|
|
22812
|
+
this.method = method;
|
|
22813
|
+
this.firstOperationId = firstOperationId;
|
|
22814
|
+
this.firstFile = firstFile;
|
|
22815
|
+
this.duplicateOperationId = duplicateOperationId;
|
|
22816
|
+
this.duplicateFile = duplicateFile;
|
|
22817
|
+
}
|
|
22818
|
+
}
|
|
22819
|
+
|
|
22820
|
+
class DefinitionRegistry {
|
|
22821
|
+
operationIds = /* @__PURE__ */ new Map();
|
|
22822
|
+
responseNames = /* @__PURE__ */ new Map();
|
|
22823
|
+
routes = /* @__PURE__ */ new Map();
|
|
22824
|
+
registerOperation(operation, sourceFile) {
|
|
22825
|
+
const existingFile = this.operationIds.get(operation.operationId);
|
|
22826
|
+
if (existingFile) {
|
|
22827
|
+
throw new DuplicateOperationIdError(
|
|
22828
|
+
operation.operationId,
|
|
22829
|
+
existingFile,
|
|
22830
|
+
sourceFile
|
|
22831
|
+
);
|
|
22849
22832
|
}
|
|
22850
|
-
|
|
22851
|
-
|
|
22833
|
+
this.operationIds.set(operation.operationId, sourceFile);
|
|
22834
|
+
const normalizedPath = this.normalizePath(operation.path);
|
|
22835
|
+
const routeKey = `${operation.method} ${normalizedPath}`;
|
|
22836
|
+
const existingRoute = this.routes.get(routeKey);
|
|
22837
|
+
if (existingRoute) {
|
|
22838
|
+
throw new DuplicateRouteError(
|
|
22839
|
+
operation.path,
|
|
22840
|
+
operation.method,
|
|
22841
|
+
existingRoute.operationId,
|
|
22842
|
+
existingRoute.file,
|
|
22843
|
+
operation.operationId,
|
|
22844
|
+
sourceFile
|
|
22845
|
+
);
|
|
22846
|
+
}
|
|
22847
|
+
this.routes.set(routeKey, {
|
|
22848
|
+
operationId: operation.operationId,
|
|
22849
|
+
file: sourceFile
|
|
22850
|
+
});
|
|
22851
|
+
}
|
|
22852
|
+
registerResponse(response, sourceFile) {
|
|
22853
|
+
const existingFile = this.responseNames.get(response.name);
|
|
22854
|
+
if (existingFile) {
|
|
22855
|
+
throw new DuplicateResponseNameError(
|
|
22856
|
+
response.name,
|
|
22857
|
+
existingFile,
|
|
22858
|
+
sourceFile
|
|
22859
|
+
);
|
|
22860
|
+
}
|
|
22861
|
+
this.responseNames.set(response.name, sourceFile);
|
|
22862
|
+
}
|
|
22863
|
+
hasOperationId(operationId) {
|
|
22864
|
+
return this.operationIds.has(operationId);
|
|
22865
|
+
}
|
|
22866
|
+
hasResponseName(name) {
|
|
22867
|
+
return this.responseNames.has(name);
|
|
22868
|
+
}
|
|
22869
|
+
hasRoute(method, path) {
|
|
22870
|
+
const normalizedPath = this.normalizePath(path);
|
|
22871
|
+
const routeKey = `${method} ${normalizedPath}`;
|
|
22872
|
+
return this.routes.has(routeKey);
|
|
22873
|
+
}
|
|
22874
|
+
getOperationFile(operationId) {
|
|
22875
|
+
return this.operationIds.get(operationId);
|
|
22876
|
+
}
|
|
22877
|
+
getResponseFile(name) {
|
|
22878
|
+
return this.responseNames.get(name);
|
|
22879
|
+
}
|
|
22880
|
+
getRouteInfo(method, path) {
|
|
22881
|
+
const normalizedPath = this.normalizePath(path);
|
|
22882
|
+
const routeKey = `${method} ${normalizedPath}`;
|
|
22883
|
+
return this.routes.get(routeKey);
|
|
22884
|
+
}
|
|
22885
|
+
normalizePath(path) {
|
|
22886
|
+
let paramIndex = 1;
|
|
22887
|
+
return path.replace(/:([a-zA-Z0-9_]+)/g, () => {
|
|
22888
|
+
return `:param${paramIndex++}`;
|
|
22852
22889
|
});
|
|
22853
|
-
fs.writeFileSync(path__default.join(context.outputDir, "index.ts"), content);
|
|
22854
22890
|
}
|
|
22855
22891
|
}
|
|
22856
22892
|
|
|
22857
|
-
class
|
|
22858
|
-
constructor(
|
|
22859
|
-
super(
|
|
22860
|
-
|
|
22861
|
-
|
|
22862
|
-
|
|
22893
|
+
class EmptyResponseArrayError extends Error {
|
|
22894
|
+
constructor(operationId, file) {
|
|
22895
|
+
super(
|
|
22896
|
+
`Operation '${operationId}' has no responses defined at \`${file}\`
|
|
22897
|
+
Operations must have at least one response definition`
|
|
22898
|
+
);
|
|
22899
|
+
this.operationId = operationId;
|
|
22900
|
+
this.file = file;
|
|
22863
22901
|
}
|
|
22864
22902
|
}
|
|
22865
22903
|
|
|
22866
|
-
class
|
|
22867
|
-
constructor(
|
|
22868
|
-
|
|
22869
|
-
|
|
22870
|
-
|
|
22904
|
+
class InvalidHttpMethodError extends Error {
|
|
22905
|
+
constructor(operationId, method, file) {
|
|
22906
|
+
const validMethods = Object.values(HttpMethod).join(", ");
|
|
22907
|
+
super(
|
|
22908
|
+
`Invalid HTTP method '${method}' in operation '${operationId}' at \`${file}\`
|
|
22909
|
+
Valid methods: ${validMethods}`
|
|
22910
|
+
);
|
|
22911
|
+
this.operationId = operationId;
|
|
22912
|
+
this.method = method;
|
|
22913
|
+
this.file = file;
|
|
22871
22914
|
}
|
|
22872
|
-
|
|
22873
|
-
|
|
22874
|
-
|
|
22875
|
-
|
|
22876
|
-
|
|
22877
|
-
|
|
22915
|
+
}
|
|
22916
|
+
|
|
22917
|
+
class InvalidPathParameterError extends Error {
|
|
22918
|
+
constructor(operationId, path, issue, file) {
|
|
22919
|
+
super(
|
|
22920
|
+
`Invalid path parameters in operation '${operationId}' at \`${file}\`
|
|
22921
|
+
Path: ${path}
|
|
22922
|
+
Issue: ${issue}`
|
|
22923
|
+
);
|
|
22924
|
+
this.operationId = operationId;
|
|
22925
|
+
this.path = path;
|
|
22926
|
+
this.issue = issue;
|
|
22927
|
+
this.file = file;
|
|
22928
|
+
}
|
|
22929
|
+
}
|
|
22930
|
+
|
|
22931
|
+
class InvalidSchemaError extends Error {
|
|
22932
|
+
constructor(schemaType, definitionName, context, file) {
|
|
22933
|
+
const schemaRequirement = schemaType === "body" ? "Must be a Zod schema" : "Must be a Zod object schema";
|
|
22934
|
+
super(
|
|
22935
|
+
`Invalid ${schemaType} schema in ${context} '${definitionName}' at \`${file}\`. ${schemaRequirement}.`
|
|
22936
|
+
);
|
|
22937
|
+
this.schemaType = schemaType;
|
|
22938
|
+
this.definitionName = definitionName;
|
|
22939
|
+
this.context = context;
|
|
22940
|
+
this.file = file;
|
|
22941
|
+
}
|
|
22942
|
+
}
|
|
22943
|
+
|
|
22944
|
+
class InvalidSchemaShapeError extends Error {
|
|
22945
|
+
constructor(schemaType, definitionName, context, propertyName, invalidType, file) {
|
|
22946
|
+
const allowedTypes = schemaType === "param" ? "string-based types (ZodString, ZodLiteral<string>, ZodEnum) or ZodOptional of these types" : "string-based types (ZodString, ZodLiteral<string>, ZodEnum), ZodOptional, or ZodArray of these types";
|
|
22947
|
+
super(
|
|
22948
|
+
`Invalid ${schemaType} schema shape in ${context} '${definitionName}' at \`${file}\`
|
|
22949
|
+
Property '${propertyName}' has invalid type: ${invalidType}
|
|
22950
|
+
Allowed types: ${allowedTypes}`
|
|
22951
|
+
);
|
|
22952
|
+
this.schemaType = schemaType;
|
|
22953
|
+
this.definitionName = definitionName;
|
|
22954
|
+
this.context = context;
|
|
22955
|
+
this.propertyName = propertyName;
|
|
22956
|
+
this.invalidType = invalidType;
|
|
22957
|
+
this.file = file;
|
|
22958
|
+
}
|
|
22959
|
+
}
|
|
22960
|
+
|
|
22961
|
+
class InvalidStatusCodeError extends Error {
|
|
22962
|
+
constructor(statusCode, responseName, file) {
|
|
22963
|
+
const validStatusCodes = Object.values(HttpStatusCode).join(", ");
|
|
22964
|
+
super(
|
|
22965
|
+
`Invalid status code '${statusCode}' in response '${responseName}' at \`${file}\`
|
|
22966
|
+
Valid status codes: ${validStatusCodes}`
|
|
22967
|
+
);
|
|
22968
|
+
this.statusCode = statusCode;
|
|
22969
|
+
this.responseName = responseName;
|
|
22970
|
+
this.file = file;
|
|
22971
|
+
}
|
|
22972
|
+
}
|
|
22973
|
+
|
|
22974
|
+
class MissingRequiredFieldError extends Error {
|
|
22975
|
+
constructor(entityType, entityName, missingField, file) {
|
|
22976
|
+
super(
|
|
22977
|
+
`Missing required field '${missingField}' in ${entityType} '${entityName}' at \`${file}\``
|
|
22978
|
+
);
|
|
22979
|
+
this.entityType = entityType;
|
|
22980
|
+
this.entityName = entityName;
|
|
22981
|
+
this.missingField = missingField;
|
|
22982
|
+
this.file = file;
|
|
22983
|
+
}
|
|
22984
|
+
}
|
|
22985
|
+
|
|
22986
|
+
class DefinitionValidator {
|
|
22987
|
+
registry;
|
|
22988
|
+
constructor(registry) {
|
|
22989
|
+
this.registry = registry ?? new DefinitionRegistry();
|
|
22990
|
+
}
|
|
22991
|
+
validateOperation(operation, sourceFile) {
|
|
22992
|
+
this.registry.registerOperation(operation, sourceFile);
|
|
22993
|
+
this.validateOperationRequiredFields(operation, sourceFile);
|
|
22994
|
+
this.validateHttpMethod(operation, sourceFile);
|
|
22995
|
+
if (operation.request) {
|
|
22996
|
+
this.validateRequestSchemas(operation, sourceFile);
|
|
22997
|
+
}
|
|
22998
|
+
this.validateOperationResponses(operation, sourceFile);
|
|
22999
|
+
this.validatePathParameters(operation, sourceFile);
|
|
23000
|
+
}
|
|
23001
|
+
validateResponse(response, sourceFile) {
|
|
23002
|
+
this.registry.registerResponse(response, sourceFile);
|
|
23003
|
+
this.validateResponseRequiredFields(response, sourceFile);
|
|
23004
|
+
this.validateStatusCode(response, sourceFile);
|
|
23005
|
+
this.validateResponseSchemas(response, sourceFile);
|
|
23006
|
+
}
|
|
23007
|
+
validateOperationRequiredFields(operation, sourceFile) {
|
|
23008
|
+
if (!operation.operationId) {
|
|
23009
|
+
throw new MissingRequiredFieldError(
|
|
23010
|
+
"operation",
|
|
23011
|
+
"unknown",
|
|
23012
|
+
"operationId",
|
|
23013
|
+
sourceFile
|
|
23014
|
+
);
|
|
22878
23015
|
}
|
|
22879
|
-
if (!
|
|
22880
|
-
|
|
23016
|
+
if (!operation.path) {
|
|
23017
|
+
throw new MissingRequiredFieldError(
|
|
23018
|
+
"operation",
|
|
23019
|
+
operation.operationId,
|
|
23020
|
+
"path",
|
|
23021
|
+
sourceFile
|
|
23022
|
+
);
|
|
22881
23023
|
}
|
|
22882
|
-
|
|
22883
|
-
|
|
22884
|
-
|
|
22885
|
-
|
|
22886
|
-
|
|
22887
|
-
|
|
22888
|
-
|
|
23024
|
+
if (!operation.method) {
|
|
23025
|
+
throw new MissingRequiredFieldError(
|
|
23026
|
+
"operation",
|
|
23027
|
+
operation.operationId,
|
|
23028
|
+
"method",
|
|
23029
|
+
sourceFile
|
|
23030
|
+
);
|
|
23031
|
+
}
|
|
23032
|
+
if (!operation.summary) {
|
|
23033
|
+
throw new MissingRequiredFieldError(
|
|
23034
|
+
"operation",
|
|
23035
|
+
operation.operationId,
|
|
23036
|
+
"summary",
|
|
23037
|
+
sourceFile
|
|
23038
|
+
);
|
|
23039
|
+
}
|
|
23040
|
+
}
|
|
23041
|
+
validateHttpMethod(operation, sourceFile) {
|
|
23042
|
+
const validMethods = Object.values(HttpMethod);
|
|
23043
|
+
if (!validMethods.includes(operation.method)) {
|
|
23044
|
+
throw new InvalidHttpMethodError(
|
|
23045
|
+
operation.operationId,
|
|
23046
|
+
operation.method,
|
|
23047
|
+
sourceFile
|
|
23048
|
+
);
|
|
23049
|
+
}
|
|
23050
|
+
}
|
|
23051
|
+
validateRequestSchemas(operation, sourceFile) {
|
|
23052
|
+
const request = operation.request;
|
|
23053
|
+
if (request.header) {
|
|
23054
|
+
this.validateSchema(
|
|
23055
|
+
request.header,
|
|
23056
|
+
"header",
|
|
23057
|
+
operation.operationId,
|
|
23058
|
+
"request",
|
|
23059
|
+
sourceFile
|
|
23060
|
+
);
|
|
23061
|
+
}
|
|
23062
|
+
if (request.query) {
|
|
23063
|
+
this.validateSchema(
|
|
23064
|
+
request.query,
|
|
23065
|
+
"query",
|
|
23066
|
+
operation.operationId,
|
|
23067
|
+
"request",
|
|
23068
|
+
sourceFile
|
|
23069
|
+
);
|
|
23070
|
+
}
|
|
23071
|
+
if (request.body) {
|
|
23072
|
+
this.validateSchema(
|
|
23073
|
+
request.body,
|
|
23074
|
+
"body",
|
|
23075
|
+
operation.operationId,
|
|
23076
|
+
"request",
|
|
23077
|
+
sourceFile
|
|
23078
|
+
);
|
|
23079
|
+
}
|
|
23080
|
+
if (request.param) {
|
|
23081
|
+
this.validateSchema(
|
|
23082
|
+
request.param,
|
|
23083
|
+
"param",
|
|
23084
|
+
operation.operationId,
|
|
23085
|
+
"request",
|
|
23086
|
+
sourceFile
|
|
23087
|
+
);
|
|
23088
|
+
}
|
|
23089
|
+
}
|
|
23090
|
+
validateOperationResponses(operation, sourceFile) {
|
|
23091
|
+
if (!operation.responses || operation.responses.length === 0) {
|
|
23092
|
+
throw new EmptyResponseArrayError(operation.operationId, sourceFile);
|
|
23093
|
+
}
|
|
23094
|
+
for (const response of operation.responses) {
|
|
23095
|
+
if (response instanceof HttpResponseDefinition) {
|
|
23096
|
+
continue;
|
|
22889
23097
|
}
|
|
22890
|
-
|
|
22891
|
-
|
|
22892
|
-
|
|
22893
|
-
|
|
23098
|
+
this.validateResponse(response, sourceFile);
|
|
23099
|
+
}
|
|
23100
|
+
}
|
|
23101
|
+
validateResponseRequiredFields(response, sourceFile) {
|
|
23102
|
+
if (!response.name) {
|
|
23103
|
+
throw new MissingRequiredFieldError(
|
|
23104
|
+
"response",
|
|
23105
|
+
"unknown",
|
|
23106
|
+
"name",
|
|
23107
|
+
sourceFile
|
|
23108
|
+
);
|
|
23109
|
+
}
|
|
23110
|
+
if (response.statusCode === void 0 || response.statusCode === null) {
|
|
23111
|
+
throw new MissingRequiredFieldError(
|
|
23112
|
+
"response",
|
|
23113
|
+
response.name,
|
|
23114
|
+
"statusCode",
|
|
23115
|
+
sourceFile
|
|
23116
|
+
);
|
|
23117
|
+
}
|
|
23118
|
+
if (!response.description) {
|
|
23119
|
+
throw new MissingRequiredFieldError(
|
|
23120
|
+
"response",
|
|
23121
|
+
response.name,
|
|
23122
|
+
"description",
|
|
23123
|
+
sourceFile
|
|
23124
|
+
);
|
|
23125
|
+
}
|
|
23126
|
+
}
|
|
23127
|
+
validateStatusCode(response, sourceFile) {
|
|
23128
|
+
const validStatusCodes = Object.values(HttpStatusCode);
|
|
23129
|
+
if (!validStatusCodes.includes(response.statusCode)) {
|
|
23130
|
+
throw new InvalidStatusCodeError(
|
|
23131
|
+
response.statusCode,
|
|
23132
|
+
response.name,
|
|
23133
|
+
sourceFile
|
|
23134
|
+
);
|
|
23135
|
+
}
|
|
23136
|
+
}
|
|
23137
|
+
validateResponseSchemas(response, sourceFile) {
|
|
23138
|
+
if (response.header) {
|
|
23139
|
+
this.validateSchema(
|
|
23140
|
+
response.header,
|
|
23141
|
+
"header",
|
|
23142
|
+
response.name,
|
|
23143
|
+
"response",
|
|
23144
|
+
sourceFile
|
|
23145
|
+
);
|
|
23146
|
+
}
|
|
23147
|
+
if (response.body) {
|
|
23148
|
+
this.validateSchema(
|
|
23149
|
+
response.body,
|
|
23150
|
+
"body",
|
|
23151
|
+
response.name,
|
|
23152
|
+
"response",
|
|
23153
|
+
sourceFile
|
|
23154
|
+
);
|
|
23155
|
+
}
|
|
23156
|
+
}
|
|
23157
|
+
validateSchema(schema, schemaType, definitionName, context, sourceFile) {
|
|
23158
|
+
if (schemaType === "body") {
|
|
23159
|
+
if (!(schema instanceof z.ZodType)) {
|
|
23160
|
+
throw new InvalidSchemaError(
|
|
23161
|
+
schemaType,
|
|
23162
|
+
definitionName,
|
|
23163
|
+
context,
|
|
23164
|
+
sourceFile
|
|
22894
23165
|
);
|
|
22895
23166
|
}
|
|
22896
|
-
|
|
22897
|
-
|
|
23167
|
+
return;
|
|
23168
|
+
}
|
|
23169
|
+
if (!(schema instanceof z.ZodObject)) {
|
|
23170
|
+
throw new InvalidSchemaError(
|
|
23171
|
+
schemaType,
|
|
23172
|
+
definitionName,
|
|
23173
|
+
context,
|
|
23174
|
+
sourceFile
|
|
23175
|
+
);
|
|
23176
|
+
}
|
|
23177
|
+
if (schemaType === "param") {
|
|
23178
|
+
this.validateParamShape(schema, definitionName, sourceFile);
|
|
23179
|
+
} else {
|
|
23180
|
+
this.validateHeaderOrQueryShape(
|
|
23181
|
+
schema,
|
|
23182
|
+
schemaType,
|
|
23183
|
+
definitionName,
|
|
23184
|
+
context,
|
|
23185
|
+
sourceFile
|
|
23186
|
+
);
|
|
22898
23187
|
}
|
|
22899
|
-
this.reportSuccessfulLoads(successful);
|
|
22900
23188
|
}
|
|
22901
|
-
|
|
22902
|
-
|
|
22903
|
-
|
|
22904
|
-
|
|
22905
|
-
|
|
22906
|
-
|
|
22907
|
-
|
|
22908
|
-
|
|
22909
|
-
|
|
22910
|
-
|
|
22911
|
-
|
|
22912
|
-
|
|
22913
|
-
|
|
22914
|
-
|
|
22915
|
-
|
|
22916
|
-
|
|
22917
|
-
|
|
23189
|
+
validatePathParameters(operation, sourceFile) {
|
|
23190
|
+
const pathParamMatches = operation.path.matchAll(/:([a-zA-Z0-9_]+)/g);
|
|
23191
|
+
const pathParamsSet = /* @__PURE__ */ new Set();
|
|
23192
|
+
for (const match of pathParamMatches) {
|
|
23193
|
+
const paramName = match[1];
|
|
23194
|
+
if (pathParamsSet.has(paramName)) {
|
|
23195
|
+
throw new InvalidPathParameterError(
|
|
23196
|
+
operation.operationId,
|
|
23197
|
+
operation.path,
|
|
23198
|
+
`Duplicate parameter name '${paramName}' in path`,
|
|
23199
|
+
sourceFile
|
|
23200
|
+
);
|
|
23201
|
+
}
|
|
23202
|
+
pathParamsSet.add(paramName);
|
|
23203
|
+
}
|
|
23204
|
+
const paramSchema = operation.request?.param;
|
|
23205
|
+
if (pathParamsSet.size > 0 && !paramSchema) {
|
|
23206
|
+
throw new InvalidPathParameterError(
|
|
23207
|
+
operation.operationId,
|
|
23208
|
+
operation.path,
|
|
23209
|
+
`Path contains parameters [${Array.from(pathParamsSet).join(", ")}] but request.param is not defined`,
|
|
23210
|
+
sourceFile
|
|
23211
|
+
);
|
|
23212
|
+
}
|
|
23213
|
+
if (paramSchema && paramSchema instanceof z.ZodObject) {
|
|
23214
|
+
const paramShape = paramSchema.shape;
|
|
23215
|
+
const paramKeys = new Set(Object.keys(paramShape));
|
|
23216
|
+
for (const pathParam of pathParamsSet) {
|
|
23217
|
+
if (!paramKeys.has(pathParam)) {
|
|
23218
|
+
throw new InvalidPathParameterError(
|
|
23219
|
+
operation.operationId,
|
|
23220
|
+
operation.path,
|
|
23221
|
+
`Path parameter ':${pathParam}' is not defined in request.param`,
|
|
23222
|
+
sourceFile
|
|
23223
|
+
);
|
|
23224
|
+
}
|
|
23225
|
+
}
|
|
23226
|
+
for (const paramKey of paramKeys) {
|
|
23227
|
+
if (!pathParamsSet.has(paramKey)) {
|
|
23228
|
+
throw new InvalidPathParameterError(
|
|
23229
|
+
operation.operationId,
|
|
23230
|
+
operation.path,
|
|
23231
|
+
`Parameter '${paramKey}' is defined in request.param but not used in the path`,
|
|
23232
|
+
sourceFile
|
|
23233
|
+
);
|
|
22918
23234
|
}
|
|
22919
|
-
attempts.push({
|
|
22920
|
-
path: possiblePath,
|
|
22921
|
-
error: "No default export found"
|
|
22922
|
-
});
|
|
22923
|
-
} catch (error) {
|
|
22924
|
-
attempts.push({
|
|
22925
|
-
path: possiblePath,
|
|
22926
|
-
error: error instanceof Error ? error.message : String(error)
|
|
22927
|
-
});
|
|
22928
23235
|
}
|
|
22929
23236
|
}
|
|
22930
|
-
|
|
22931
|
-
|
|
22932
|
-
|
|
22933
|
-
|
|
22934
|
-
|
|
23237
|
+
}
|
|
23238
|
+
validateHeaderOrQueryShape(schema, schemaType, definitionName, context, sourceFile) {
|
|
23239
|
+
const shape = schema.shape;
|
|
23240
|
+
for (const [propName, propSchema] of Object.entries(shape)) {
|
|
23241
|
+
if (!this.isValidHeaderOrQueryValue(propSchema)) {
|
|
23242
|
+
const typeName = this.getZodTypeName(propSchema);
|
|
23243
|
+
throw new InvalidSchemaShapeError(
|
|
23244
|
+
schemaType,
|
|
23245
|
+
definitionName,
|
|
23246
|
+
context,
|
|
23247
|
+
propName,
|
|
23248
|
+
typeName,
|
|
23249
|
+
sourceFile
|
|
23250
|
+
);
|
|
23251
|
+
}
|
|
23252
|
+
}
|
|
23253
|
+
}
|
|
23254
|
+
validateParamShape(schema, operationId, sourceFile) {
|
|
23255
|
+
const shape = schema.shape;
|
|
23256
|
+
for (const [propName, propSchema] of Object.entries(shape)) {
|
|
23257
|
+
if (!this.isValidParamValue(propSchema)) {
|
|
23258
|
+
const typeName = this.getZodTypeName(propSchema);
|
|
23259
|
+
throw new InvalidSchemaShapeError(
|
|
23260
|
+
"param",
|
|
23261
|
+
operationId,
|
|
23262
|
+
"request",
|
|
23263
|
+
propName,
|
|
23264
|
+
typeName,
|
|
23265
|
+
sourceFile
|
|
23266
|
+
);
|
|
22935
23267
|
}
|
|
23268
|
+
}
|
|
23269
|
+
}
|
|
23270
|
+
isValidHeaderOrQueryValue(schema) {
|
|
23271
|
+
if (this.isStringBasedType(schema)) {
|
|
23272
|
+
return true;
|
|
23273
|
+
}
|
|
23274
|
+
if (schema instanceof z.ZodOptional) {
|
|
23275
|
+
return this.isValidHeaderOrQueryValue(schema.unwrap());
|
|
23276
|
+
}
|
|
23277
|
+
if (schema instanceof z.ZodArray) {
|
|
23278
|
+
return this.isStringBasedType(schema.element);
|
|
23279
|
+
}
|
|
23280
|
+
return false;
|
|
23281
|
+
}
|
|
23282
|
+
isValidParamValue(schema) {
|
|
23283
|
+
if (this.isStringBasedType(schema)) {
|
|
23284
|
+
return true;
|
|
23285
|
+
}
|
|
23286
|
+
if (schema instanceof z.ZodOptional) {
|
|
23287
|
+
return this.isStringBasedType(schema.unwrap());
|
|
23288
|
+
}
|
|
23289
|
+
return false;
|
|
23290
|
+
}
|
|
23291
|
+
isStringBasedType(schema) {
|
|
23292
|
+
if (schema instanceof z.ZodString) {
|
|
23293
|
+
return true;
|
|
23294
|
+
}
|
|
23295
|
+
if (schema instanceof z.ZodLiteral && typeof schema.value === "string") {
|
|
23296
|
+
return true;
|
|
23297
|
+
}
|
|
23298
|
+
if (schema instanceof z.ZodEnum) {
|
|
23299
|
+
return true;
|
|
23300
|
+
}
|
|
23301
|
+
const typeName = schema.constructor.name;
|
|
23302
|
+
const stringFormatTypes = [
|
|
23303
|
+
"ZodULID",
|
|
23304
|
+
"ZodUUID",
|
|
23305
|
+
"ZodUUIDv4",
|
|
23306
|
+
"ZodUUIDv7",
|
|
23307
|
+
"ZodUUIDv8",
|
|
23308
|
+
"ZodEmail",
|
|
23309
|
+
"ZodURL",
|
|
23310
|
+
"ZodCUID",
|
|
23311
|
+
"ZodCUID2",
|
|
23312
|
+
"ZodNanoID",
|
|
23313
|
+
"ZodBase64",
|
|
23314
|
+
"ZodBase64URL",
|
|
23315
|
+
"ZodEmoji",
|
|
23316
|
+
"ZodIPv4",
|
|
23317
|
+
"ZodIPv6",
|
|
23318
|
+
"ZodCIDRv4",
|
|
23319
|
+
"ZodCIDRv6",
|
|
23320
|
+
"ZodE164",
|
|
23321
|
+
"ZodJWT",
|
|
23322
|
+
"ZodASCII",
|
|
23323
|
+
"ZodUTF8",
|
|
23324
|
+
"ZodLowercase",
|
|
23325
|
+
"ZodGUID",
|
|
23326
|
+
"ZodISODate",
|
|
23327
|
+
"ZodISOTime",
|
|
23328
|
+
"ZodISODateTime",
|
|
23329
|
+
"ZodISODuration"
|
|
23330
|
+
];
|
|
23331
|
+
if (stringFormatTypes.includes(typeName)) {
|
|
23332
|
+
return true;
|
|
23333
|
+
}
|
|
23334
|
+
if (schema._def && schema._def.typeName && schema._def.typeName.includes("String")) {
|
|
23335
|
+
return true;
|
|
23336
|
+
}
|
|
23337
|
+
return false;
|
|
23338
|
+
}
|
|
23339
|
+
getZodTypeName(schema) {
|
|
23340
|
+
const typeName = schema.constructor.name;
|
|
23341
|
+
if (schema instanceof z.ZodOptional) {
|
|
23342
|
+
return `ZodOptional<${this.getZodTypeName(schema.unwrap())}>`;
|
|
23343
|
+
}
|
|
23344
|
+
if (schema instanceof z.ZodArray) {
|
|
23345
|
+
return `ZodArray<${this.getZodTypeName(schema.element)}>`;
|
|
23346
|
+
}
|
|
23347
|
+
if (schema instanceof z.ZodLiteral) {
|
|
23348
|
+
return `ZodLiteral<${typeof schema.value}>`;
|
|
23349
|
+
}
|
|
23350
|
+
return typeName;
|
|
23351
|
+
}
|
|
23352
|
+
getRegistry() {
|
|
23353
|
+
return this.registry;
|
|
23354
|
+
}
|
|
23355
|
+
}
|
|
23356
|
+
|
|
23357
|
+
class InvalidSharedDirError extends Error {
|
|
23358
|
+
constructor(explanation) {
|
|
23359
|
+
super("Invalid shared dir");
|
|
23360
|
+
this.explanation = explanation;
|
|
23361
|
+
}
|
|
23362
|
+
}
|
|
23363
|
+
|
|
23364
|
+
class ResourceReader {
|
|
23365
|
+
constructor(config) {
|
|
23366
|
+
this.config = config;
|
|
23367
|
+
}
|
|
23368
|
+
async getResources() {
|
|
23369
|
+
const contents = fs.readdirSync(this.config.sourceDir, {
|
|
23370
|
+
withFileTypes: true
|
|
23371
|
+
});
|
|
23372
|
+
const result = {
|
|
23373
|
+
entityResources: {},
|
|
23374
|
+
sharedResponseResources: []
|
|
22936
23375
|
};
|
|
23376
|
+
const validator = new DefinitionValidator();
|
|
23377
|
+
if (fs.existsSync(this.config.sharedSourceDir)) {
|
|
23378
|
+
const sharedStats = fs.statSync(this.config.sharedSourceDir);
|
|
23379
|
+
if (!sharedStats.isDirectory()) {
|
|
23380
|
+
throw new InvalidSharedDirError(
|
|
23381
|
+
`'${this.config.sharedSourceDir}' is a file, not a directory`
|
|
23382
|
+
);
|
|
23383
|
+
}
|
|
23384
|
+
result.sharedResponseResources = await this.getSharedResponseResources(validator);
|
|
23385
|
+
console.info(
|
|
23386
|
+
`Found '${result.sharedResponseResources.length}' shared responses in '${this.config.sharedSourceDir}'`
|
|
23387
|
+
);
|
|
23388
|
+
} else {
|
|
23389
|
+
console.info(
|
|
23390
|
+
`No shared directory found at '${this.config.sharedSourceDir}'`
|
|
23391
|
+
);
|
|
23392
|
+
}
|
|
23393
|
+
const normalizedSharedPath = path__default.resolve(this.config.sharedSourceDir);
|
|
23394
|
+
for (const content of contents) {
|
|
23395
|
+
if (!content.isDirectory()) {
|
|
23396
|
+
console.info(`Skipping '${content.name}' as it is not a directory`);
|
|
23397
|
+
continue;
|
|
23398
|
+
}
|
|
23399
|
+
const entityName = content.name;
|
|
23400
|
+
const entitySourceDir = path__default.resolve(this.config.sourceDir, entityName);
|
|
23401
|
+
if (entitySourceDir === normalizedSharedPath || normalizedSharedPath.startsWith(entitySourceDir + path__default.sep)) {
|
|
23402
|
+
console.info(
|
|
23403
|
+
`Skipping '${content.name}' as it is or contains the shared directory`
|
|
23404
|
+
);
|
|
23405
|
+
continue;
|
|
23406
|
+
}
|
|
23407
|
+
const responseResources = await this.getEntityResponseResources(
|
|
23408
|
+
entitySourceDir,
|
|
23409
|
+
entityName,
|
|
23410
|
+
validator
|
|
23411
|
+
);
|
|
23412
|
+
const operationResources = await this.getEntityOperationResources(
|
|
23413
|
+
entitySourceDir,
|
|
23414
|
+
entityName,
|
|
23415
|
+
validator,
|
|
23416
|
+
[...result.sharedResponseResources, ...responseResources]
|
|
23417
|
+
);
|
|
23418
|
+
result.entityResources[entityName] = {
|
|
23419
|
+
operations: operationResources,
|
|
23420
|
+
responses: responseResources
|
|
23421
|
+
};
|
|
23422
|
+
console.info(
|
|
23423
|
+
`Found '${operationResources.length}' operation definitions for entity '${entityName}'`
|
|
23424
|
+
);
|
|
23425
|
+
if (responseResources.length > 0) {
|
|
23426
|
+
console.info(
|
|
23427
|
+
`Found '${responseResources.length}' response definitions for entity '${entityName}'`
|
|
23428
|
+
);
|
|
23429
|
+
}
|
|
23430
|
+
}
|
|
23431
|
+
return result;
|
|
22937
23432
|
}
|
|
22938
|
-
|
|
22939
|
-
|
|
22940
|
-
|
|
22941
|
-
|
|
22942
|
-
|
|
22943
|
-
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
|
|
22947
|
-
paths.push(`@rexeus/${pluginName}`);
|
|
22948
|
-
break;
|
|
22949
|
-
case "local":
|
|
22950
|
-
paths.push(pluginName);
|
|
22951
|
-
break;
|
|
23433
|
+
scanDirectoryRecursively(dir) {
|
|
23434
|
+
const files = [];
|
|
23435
|
+
const contents = fs.readdirSync(dir, { withFileTypes: true });
|
|
23436
|
+
for (const content of contents) {
|
|
23437
|
+
const fullPath = path__default.join(dir, content.name);
|
|
23438
|
+
if (content.isDirectory()) {
|
|
23439
|
+
files.push(...this.scanDirectoryRecursively(fullPath));
|
|
23440
|
+
} else if (content.isFile() && content.name.endsWith(".ts")) {
|
|
23441
|
+
files.push(fullPath);
|
|
22952
23442
|
}
|
|
22953
23443
|
}
|
|
22954
|
-
return
|
|
23444
|
+
return files;
|
|
22955
23445
|
}
|
|
22956
|
-
|
|
22957
|
-
|
|
22958
|
-
|
|
22959
|
-
|
|
22960
|
-
|
|
22961
|
-
|
|
22962
|
-
|
|
22963
|
-
console.info(
|
|
23446
|
+
async getSharedResponseResources(validator) {
|
|
23447
|
+
const files = this.scanDirectoryRecursively(this.config.sharedSourceDir);
|
|
23448
|
+
const sharedResponseResources = [];
|
|
23449
|
+
for (const sourceFile of files) {
|
|
23450
|
+
const sourceFileName = path__default.basename(sourceFile);
|
|
23451
|
+
const definition = await import(sourceFile);
|
|
23452
|
+
if (!definition.default) {
|
|
23453
|
+
console.info(
|
|
23454
|
+
`Skipping '${sourceFile}' as it does not have a default export`
|
|
23455
|
+
);
|
|
23456
|
+
continue;
|
|
23457
|
+
}
|
|
23458
|
+
if (!(definition.default instanceof HttpResponseDefinition)) {
|
|
23459
|
+
console.info(
|
|
23460
|
+
`Skipping '${sourceFile}' as it is not an instance of HttpResponseDefinition`
|
|
23461
|
+
);
|
|
23462
|
+
continue;
|
|
23463
|
+
}
|
|
23464
|
+
validator.validateResponse(definition.default, sourceFile);
|
|
23465
|
+
const outputDir = this.config.sharedOutputDir;
|
|
23466
|
+
const outputFileName = `${definition.default.name}Response.ts`;
|
|
23467
|
+
const outputFile = path__default.join(outputDir, outputFileName);
|
|
23468
|
+
sharedResponseResources.push({
|
|
23469
|
+
...definition.default,
|
|
23470
|
+
sourceDir: this.config.sharedSourceDir,
|
|
23471
|
+
sourceFile,
|
|
23472
|
+
sourceFileName,
|
|
23473
|
+
outputFile,
|
|
23474
|
+
outputFileName,
|
|
23475
|
+
outputDir
|
|
23476
|
+
});
|
|
23477
|
+
}
|
|
23478
|
+
return sharedResponseResources;
|
|
23479
|
+
}
|
|
23480
|
+
async getEntityOperationResources(sourceDir, entityName, validator, referencedResponses) {
|
|
23481
|
+
const files = this.scanDirectoryRecursively(sourceDir);
|
|
23482
|
+
const definitions = [];
|
|
23483
|
+
for (const sourceFile of files) {
|
|
23484
|
+
const sourceFileName = path__default.basename(sourceFile);
|
|
23485
|
+
const definition = await import(sourceFile);
|
|
23486
|
+
if (!definition.default) {
|
|
23487
|
+
console.info(
|
|
23488
|
+
`Skipping '${sourceFile}' as it does not have a default export`
|
|
23489
|
+
);
|
|
23490
|
+
continue;
|
|
23491
|
+
}
|
|
23492
|
+
if (!(definition.default instanceof HttpOperationDefinition)) {
|
|
23493
|
+
console.info(
|
|
23494
|
+
`Skipping '${sourceFile}' as it is not an instance of HttpOperationDefinition`
|
|
23495
|
+
);
|
|
23496
|
+
continue;
|
|
23497
|
+
}
|
|
23498
|
+
validator.validateOperation(definition.default, sourceFile);
|
|
23499
|
+
const { operationId } = definition.default;
|
|
23500
|
+
const outputDir = path__default.join(this.config.outputDir, entityName);
|
|
23501
|
+
const outputRequestFileName = `${operationId}Request.ts`;
|
|
23502
|
+
const outputRequestFile = path__default.join(outputDir, outputRequestFileName);
|
|
23503
|
+
const outputResponseFileName = `${operationId}Response.ts`;
|
|
23504
|
+
const outputResponseFile = path__default.join(outputDir, outputResponseFileName);
|
|
23505
|
+
const outputRequestValidationFileName = `${operationId}RequestValidator.ts`;
|
|
23506
|
+
const outputRequestValidationFile = path__default.join(
|
|
23507
|
+
outputDir,
|
|
23508
|
+
outputRequestValidationFileName
|
|
23509
|
+
);
|
|
23510
|
+
const outputResponseValidationFileName = `${operationId}ResponseValidator.ts`;
|
|
23511
|
+
const outputResponseValidationFile = path__default.join(
|
|
23512
|
+
outputDir,
|
|
23513
|
+
outputResponseValidationFileName
|
|
23514
|
+
);
|
|
23515
|
+
const outputClientFileName = `${operationId}Client.ts`;
|
|
23516
|
+
const outputClientFile = path__default.join(outputDir, outputClientFileName);
|
|
23517
|
+
const operationResource = {
|
|
23518
|
+
sourceDir,
|
|
23519
|
+
sourceFile,
|
|
23520
|
+
sourceFileName,
|
|
23521
|
+
definition: {
|
|
23522
|
+
...definition.default,
|
|
23523
|
+
responses: []
|
|
23524
|
+
},
|
|
23525
|
+
outputDir,
|
|
23526
|
+
entityName,
|
|
23527
|
+
outputRequestFile,
|
|
23528
|
+
outputResponseFile,
|
|
23529
|
+
outputResponseValidationFile,
|
|
23530
|
+
outputRequestValidationFile,
|
|
23531
|
+
outputRequestFileName,
|
|
23532
|
+
outputRequestValidationFileName,
|
|
23533
|
+
outputResponseFileName,
|
|
23534
|
+
outputResponseValidationFileName,
|
|
23535
|
+
outputClientFile,
|
|
23536
|
+
outputClientFileName
|
|
23537
|
+
};
|
|
23538
|
+
if (!definition.default.responses) {
|
|
23539
|
+
throw new Error(
|
|
23540
|
+
`Operation '${operationId}' does not have any responses`
|
|
23541
|
+
);
|
|
23542
|
+
}
|
|
23543
|
+
for (const response of definition.default.responses) {
|
|
23544
|
+
if (referencedResponses.some((ref) => ref.name === response.name)) {
|
|
23545
|
+
const referencedResponse = {
|
|
23546
|
+
...response,
|
|
23547
|
+
statusCodeName: HttpStatusCodeNameMap[response.statusCode],
|
|
23548
|
+
isReference: true
|
|
23549
|
+
};
|
|
23550
|
+
operationResource.definition.responses.push(referencedResponse);
|
|
23551
|
+
} else {
|
|
23552
|
+
const extendedResponse = {
|
|
23553
|
+
...response,
|
|
23554
|
+
statusCodeName: HttpStatusCodeNameMap[response.statusCode],
|
|
23555
|
+
isReference: false
|
|
23556
|
+
};
|
|
23557
|
+
operationResource.definition.responses.push(extendedResponse);
|
|
23558
|
+
}
|
|
23559
|
+
}
|
|
23560
|
+
definitions.push(operationResource);
|
|
23561
|
+
}
|
|
23562
|
+
return definitions;
|
|
23563
|
+
}
|
|
23564
|
+
async getEntityResponseResources(sourceDir, entityName, validator) {
|
|
23565
|
+
const files = this.scanDirectoryRecursively(sourceDir);
|
|
23566
|
+
const responseResources = [];
|
|
23567
|
+
for (const sourceFile of files) {
|
|
23568
|
+
const sourceFileName = path__default.basename(sourceFile);
|
|
23569
|
+
const definition = await import(sourceFile);
|
|
23570
|
+
if (!definition.default) {
|
|
23571
|
+
continue;
|
|
23572
|
+
}
|
|
23573
|
+
if (!(definition.default instanceof HttpResponseDefinition)) {
|
|
23574
|
+
continue;
|
|
22964
23575
|
}
|
|
23576
|
+
validator.validateResponse(definition.default, sourceFile);
|
|
23577
|
+
const outputFileName = `${definition.default.name}Response.ts`;
|
|
23578
|
+
const outputFile = path__default.join(
|
|
23579
|
+
this.config.outputDir,
|
|
23580
|
+
entityName,
|
|
23581
|
+
outputFileName
|
|
23582
|
+
);
|
|
23583
|
+
responseResources.push({
|
|
23584
|
+
...definition.default,
|
|
23585
|
+
sourceDir,
|
|
23586
|
+
sourceFile,
|
|
23587
|
+
sourceFileName,
|
|
23588
|
+
outputFile,
|
|
23589
|
+
outputFileName,
|
|
23590
|
+
outputDir: path__default.join(this.config.outputDir, entityName),
|
|
23591
|
+
entityName
|
|
23592
|
+
});
|
|
22965
23593
|
}
|
|
23594
|
+
return responseResources;
|
|
22966
23595
|
}
|
|
22967
23596
|
}
|
|
22968
23597
|
|
|
@@ -22976,6 +23605,8 @@ class Generator {
|
|
|
22976
23605
|
indexFileGenerator;
|
|
22977
23606
|
resourceReader = null;
|
|
22978
23607
|
prettier = null;
|
|
23608
|
+
inputDir = "";
|
|
23609
|
+
sharedInputDir = "";
|
|
22979
23610
|
outputDir = "";
|
|
22980
23611
|
sourceDir = "";
|
|
22981
23612
|
sharedSourceDir = "";
|
|
@@ -22991,13 +23622,22 @@ class Generator {
|
|
|
22991
23622
|
*/
|
|
22992
23623
|
async generate(definitionDir, outputDir, config) {
|
|
22993
23624
|
console.info("Starting generation...");
|
|
22994
|
-
this.initializeDirectories(definitionDir, outputDir);
|
|
23625
|
+
this.initializeDirectories(definitionDir, outputDir, config?.shared);
|
|
22995
23626
|
if (config?.clean ?? true) {
|
|
22996
23627
|
console.info("Cleaning output directory...");
|
|
22997
23628
|
fs.rmSync(this.outputDir, { recursive: true, force: true });
|
|
22998
23629
|
}
|
|
22999
23630
|
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
23000
23631
|
fs.mkdirSync(this.sharedOutputDir, { recursive: true });
|
|
23632
|
+
console.info(
|
|
23633
|
+
`Copying definitions from '${this.inputDir}' to '${this.sourceDir}'...`
|
|
23634
|
+
);
|
|
23635
|
+
fs.cpSync(this.inputDir, this.sourceDir, {
|
|
23636
|
+
recursive: true,
|
|
23637
|
+
filter: (src) => {
|
|
23638
|
+
return src.endsWith(".ts") || src.endsWith(".js") || src.endsWith(".json") || src.endsWith(".mjs") || src.endsWith(".cjs") || fs.statSync(src).isDirectory();
|
|
23639
|
+
}
|
|
23640
|
+
});
|
|
23001
23641
|
await this.pluginLoader.loadPlugins(config);
|
|
23002
23642
|
this.resourceReader = new ResourceReader({
|
|
23003
23643
|
sourceDir: this.sourceDir,
|
|
@@ -23055,22 +23695,24 @@ class Generator {
|
|
|
23055
23695
|
`Generated files: ${this.contextBuilder.getGeneratedFiles().length}`
|
|
23056
23696
|
);
|
|
23057
23697
|
}
|
|
23058
|
-
initializeDirectories(definitionDir, outputDir) {
|
|
23059
|
-
this.
|
|
23698
|
+
initializeDirectories(definitionDir, outputDir, sharedDir) {
|
|
23699
|
+
this.inputDir = definitionDir;
|
|
23700
|
+
this.sharedInputDir = sharedDir ?? path__default.join(definitionDir, "shared");
|
|
23060
23701
|
this.outputDir = outputDir;
|
|
23061
|
-
this.sharedSourceDir = path__default.join(definitionDir, "shared");
|
|
23062
23702
|
this.sharedOutputDir = path__default.join(outputDir, "shared");
|
|
23703
|
+
this.sourceDir = path__default.join(this.outputDir, "definition");
|
|
23704
|
+
const inputToSharedDirRelative = path__default.relative(
|
|
23705
|
+
this.inputDir,
|
|
23706
|
+
this.sharedInputDir
|
|
23707
|
+
);
|
|
23708
|
+
this.sharedSourceDir = path__default.join(this.sourceDir, inputToSharedDirRelative);
|
|
23063
23709
|
}
|
|
23064
23710
|
}
|
|
23065
23711
|
|
|
23066
|
-
var version = "0.0.3";
|
|
23067
|
-
var packageJson = {
|
|
23068
|
-
version: version};
|
|
23069
|
-
|
|
23070
23712
|
const program = new Command();
|
|
23071
23713
|
const execDir = process.cwd();
|
|
23072
23714
|
program.name("@rexeus/typeweaver").description("Type-safe API framework with code generation for TypeScript").version(packageJson.version);
|
|
23073
|
-
program.command("generate").description("Generate types, validators, and clients from API definitions").option("-i, --input <inputDir>", "path to definition directory").option("-o, --output <outputDir>", "output directory for generated files").option("-c, --config <configFile>", "path to configuration file").option("-p, --plugins <plugins>", "comma-separated list of plugins to use").option("--prettier", "format generated code with Prettier (default: true)").option("--no-prettier", "disable Prettier formatting").option("--clean", "clean output directory before generation (default: true)").option("--no-clean", "disable cleaning output directory").action(async (options) => {
|
|
23715
|
+
program.command("generate").description("Generate types, validators, and clients from API definitions").option("-i, --input <inputDir>", "path to definition directory").option("-o, --output <outputDir>", "output directory for generated files").option("-s, --shared <path>", "path to shared definitions directory").option("-c, --config <configFile>", "path to configuration file").option("-p, --plugins <plugins>", "comma-separated list of plugins to use").option("--prettier", "format generated code with Prettier (default: true)").option("--no-prettier", "disable Prettier formatting").option("--clean", "clean output directory before generation (default: true)").option("--no-clean", "disable cleaning output directory").action(async (options) => {
|
|
23074
23716
|
let config = {};
|
|
23075
23717
|
if (options.config) {
|
|
23076
23718
|
const configPath = path__default.isAbsolute(options.config) ? options.config : path__default.join(execDir, options.config);
|
|
@@ -23087,6 +23729,7 @@ program.command("generate").description("Generate types, validators, and clients
|
|
|
23087
23729
|
}
|
|
23088
23730
|
const inputDir = options.input ?? config.input;
|
|
23089
23731
|
const outputDir = options.output ?? config.output;
|
|
23732
|
+
const sharedDir = options.shared ?? config.shared;
|
|
23090
23733
|
if (!inputDir) {
|
|
23091
23734
|
throw new Error(
|
|
23092
23735
|
"No input directory provided. Use --input or specify in config file."
|
|
@@ -23099,9 +23742,11 @@ program.command("generate").description("Generate types, validators, and clients
|
|
|
23099
23742
|
}
|
|
23100
23743
|
const resolvedInputDir = path__default.isAbsolute(inputDir) ? inputDir : path__default.join(execDir, inputDir);
|
|
23101
23744
|
const resolvedOutputDir = path__default.isAbsolute(outputDir) ? outputDir : path__default.join(execDir, outputDir);
|
|
23745
|
+
const resolvedSharedDir = sharedDir ? path__default.isAbsolute(sharedDir) ? sharedDir : path__default.join(execDir, sharedDir) : void 0;
|
|
23102
23746
|
const finalConfig = {
|
|
23103
23747
|
input: resolvedInputDir,
|
|
23104
23748
|
output: resolvedOutputDir,
|
|
23749
|
+
shared: resolvedSharedDir,
|
|
23105
23750
|
prettier: options.prettier ?? config.prettier ?? true,
|
|
23106
23751
|
clean: options.clean ?? config.clean ?? true
|
|
23107
23752
|
};
|
|
@@ -23113,7 +23758,7 @@ program.command("generate").description("Generate types, validators, and clients
|
|
|
23113
23758
|
const generator = new Generator();
|
|
23114
23759
|
return generator.generate(resolvedInputDir, resolvedOutputDir, finalConfig);
|
|
23115
23760
|
});
|
|
23116
|
-
program.command("init").description("Initialize a new
|
|
23761
|
+
program.command("init").description("Initialize a new typeweaver project (coming soon)").action(() => {
|
|
23117
23762
|
console.log("The init command is coming soon!");
|
|
23118
23763
|
});
|
|
23119
23764
|
program.parse(process.argv);
|