@hyper-fetch/cli 7.2.1
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/.eslintrc.json +11 -0
- package/README.md +63 -0
- package/dist/cli.cjs.js +3087 -0
- package/dist/cli.cjs.js.map +7 -0
- package/dist/index.d.ts +100 -0
- package/package.json +77 -0
- package/src/cli/index.ts +54 -0
- package/src/codegen/index.ts +1 -0
- package/src/codegen/openapi/generator.ts +338 -0
- package/src/codegen/openapi/http-methods.enum.ts +10 -0
- package/src/codegen/openapi/index.ts +3 -0
- package/src/codegen/openapi/openapi.types.ts +14 -0
- package/src/codegen/openapi/operations.ts +22 -0
- package/src/codegen/openapi/utils.ts +34 -0
- package/src/commands/add.ts +90 -0
- package/src/commands/generate.ts +104 -0
- package/src/commands/init.ts +191 -0
- package/src/config/get-config.ts +59 -0
- package/src/config/get-ts-alias.ts +25 -0
- package/src/config/schema.ts +22 -0
- package/src/index.ts +1 -0
- package/src/preflights/preflight-add.ts +43 -0
- package/src/preflights/preflight-generate.ts +46 -0
- package/src/utils/errors.ts +13 -0
- package/src/utils/handle-error.ts +35 -0
- package/src/utils/highlighter.ts +8 -0
- package/src/utils/logger.ts +20 -0
- package/src/utils/resolve-import.ts +14 -0
- package/src/utils/show-help.ts +37 -0
- package/src/utils/spinner.ts +13 -0
- package/src/utils/zod-to-table.ts +16 -0
- package/tsconfig.json +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hyper-fetch/cli",
|
|
3
|
+
"version": "7.2.1",
|
|
4
|
+
"description": "Hyper Fetch cli for code generation and utilities",
|
|
5
|
+
"author": "Maciej Pyrc <maciekpyrc@gmail.com>, Kacper Skawina <kacper.skawina@gmail.com>",
|
|
6
|
+
"homepage": "https://hyperfetch.bettertyped.com/",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"module": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"source": "src/index.ts",
|
|
12
|
+
"cli": "src/cli/index.ts",
|
|
13
|
+
"climain": "dist/cli.cjs.js",
|
|
14
|
+
"bin": "dist/cli.cjs.js",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "npx rimraf dist",
|
|
17
|
+
"test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest --watchAll --maxWorkers=3 --forceExit",
|
|
18
|
+
"test:pipeline": "node --experimental-vm-modules ../../node_modules/jest/bin/jest --watchAll=false --maxWorkers=1 --no-cache --forceExit",
|
|
19
|
+
"dev": "npx tsx ./src/cli/index.ts",
|
|
20
|
+
"prebuild": "yarn update:version",
|
|
21
|
+
"build": "yarn clean && node ../../scripts/build.js",
|
|
22
|
+
"postbuild": "yarn rollup -c ../../rollup.config.js",
|
|
23
|
+
"update:version": "node ../../scripts/update-package-version.js --prefix hyper-fetch-cli-v",
|
|
24
|
+
"lint": "eslint . --ext .js,.jsx,.tsx,.ts --fix",
|
|
25
|
+
"format": "prettier --write .",
|
|
26
|
+
"typescheck": "tsc --noEmit --emitDeclarationOnly false",
|
|
27
|
+
"tests": "yarn lint-staged",
|
|
28
|
+
"release": "yarn semantic-release --extends ../../release.config.js -t 'hyper-fetch-cli-v${version}'"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"registry": "https://registry.npmjs.com/"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/BetterTyped/hyper-fetch.git",
|
|
37
|
+
"directory": "packages/cli"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/BetterTyped/hyper-fetch/issues"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@anttiviljami/dtsgenerator": "^3.12.2",
|
|
44
|
+
"@apidevtools/json-schema-ref-parser": "^11.9.3",
|
|
45
|
+
"@hyper-fetch/core": "*",
|
|
46
|
+
"@inquirer/prompts": "^7.6.0",
|
|
47
|
+
"chalk": "^5.4.1",
|
|
48
|
+
"cli-table3": "^0.6.5",
|
|
49
|
+
"commander": "^11.0.0",
|
|
50
|
+
"execa": "^8.0.1",
|
|
51
|
+
"fs-extra": "^11.3.0",
|
|
52
|
+
"lodash": "^4.17.21",
|
|
53
|
+
"openapi-types": "^12.1.3",
|
|
54
|
+
"ora": "^8.2.0",
|
|
55
|
+
"prettier": "^3.6.2",
|
|
56
|
+
"react": "^19.1.0",
|
|
57
|
+
"react-dom": "^19.1.0",
|
|
58
|
+
"zod": "^4.0.2"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/fs-extra": "^11.0.4",
|
|
62
|
+
"abortcontroller-polyfill": "^1.7.5"
|
|
63
|
+
},
|
|
64
|
+
"lint-staged": {
|
|
65
|
+
"*.{js,jsx,ts,tsx}": [
|
|
66
|
+
"yarn jest --passWithNoTests"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"keywords": [
|
|
70
|
+
"codegen",
|
|
71
|
+
"openapi",
|
|
72
|
+
"hyper fetch",
|
|
73
|
+
"swagger",
|
|
74
|
+
"openapi schema",
|
|
75
|
+
"code generator"
|
|
76
|
+
]
|
|
77
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { select } from "@inquirer/prompts";
|
|
4
|
+
|
|
5
|
+
import pkg from "../../package.json";
|
|
6
|
+
import { generate } from "commands/generate";
|
|
7
|
+
import { init } from "commands/init";
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program.name("hyper-fetch").description("CLI for Hyper Fetch").version(pkg.version);
|
|
12
|
+
|
|
13
|
+
const commands = {
|
|
14
|
+
init,
|
|
15
|
+
// add,
|
|
16
|
+
generate,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
Object.values(commands).forEach((command) => {
|
|
20
|
+
program.addCommand(command);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const main = async () => {
|
|
24
|
+
try {
|
|
25
|
+
let chosenCommand: string | undefined;
|
|
26
|
+
|
|
27
|
+
if (process.argv[2] && Object.keys(commands).includes(process.argv[2])) {
|
|
28
|
+
const command = commands[process.argv[2] as keyof typeof commands]!;
|
|
29
|
+
chosenCommand = command.name();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!chosenCommand) {
|
|
33
|
+
chosenCommand = await select({
|
|
34
|
+
message: "What do you want to do?",
|
|
35
|
+
choices: program.commands.map((cmd) => ({
|
|
36
|
+
name: cmd.name(),
|
|
37
|
+
value: cmd.name(),
|
|
38
|
+
description: cmd.description(),
|
|
39
|
+
})),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await program.parseAsync([process.argv[0], process.argv[1], chosenCommand]);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (e instanceof Error) {
|
|
46
|
+
if (e.message.includes("User force closed the prompt")) {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./openapi/generator";
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import DtsGenerator, { ExportedType } from "@anttiviljami/dtsgenerator/dist/core/dtsGenerator";
|
|
2
|
+
import RefParser from "@apidevtools/json-schema-ref-parser";
|
|
3
|
+
import { parseSchema } from "@anttiviljami/dtsgenerator/dist/core/type";
|
|
4
|
+
import { find, chain, isEmpty } from "lodash";
|
|
5
|
+
import * as prettier from "prettier";
|
|
6
|
+
import * as fs from "fs-extra";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { createClient } from "@hyper-fetch/core";
|
|
9
|
+
|
|
10
|
+
import { Document, Operation, GeneratedTypes } from "./openapi.types";
|
|
11
|
+
import { getAvailableOperations } from "./operations";
|
|
12
|
+
import { adjustPathParamsFormat, normalizeOperationId, createTypeBaseName, isUrl } from "./utils";
|
|
13
|
+
import { HttpMethod } from "./http-methods.enum";
|
|
14
|
+
import { Config } from "config/schema";
|
|
15
|
+
|
|
16
|
+
interface RefError {
|
|
17
|
+
path: string;
|
|
18
|
+
ref: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const formatSchema = (obj: any, indent = 1): string => {
|
|
23
|
+
const indentation = " ".repeat(indent);
|
|
24
|
+
const entries = Object.entries(obj)
|
|
25
|
+
.map(([key, value]) => {
|
|
26
|
+
const formattedKey = key.includes("-") ? `"${key}"` : key;
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
return `${indentation}${formattedKey}: ${value};`;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "object" && value !== null) {
|
|
31
|
+
return `${indentation}${formattedKey}: {\n${formatSchema(value, indent + 1)}\n${indentation}};`;
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
})
|
|
35
|
+
.join("\n");
|
|
36
|
+
return entries;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export class OpenapiRequestGenerator {
|
|
40
|
+
protected openapiDocument: Document;
|
|
41
|
+
constructor(openapiDocument: any) {
|
|
42
|
+
this.openapiDocument = openapiDocument as Document;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async generateFile({ config, fileName }: { config: Config; fileName: string }) {
|
|
46
|
+
const defaultFileName = "openapi.client";
|
|
47
|
+
const { schemaTypes, generatedTypes, sdkSchema, createSdkFn } = await this.generateRequestsFromSchema();
|
|
48
|
+
const contents = [
|
|
49
|
+
`import { createSdk as coreCreateSdk, ClientInstance, RequestInstance } from "@hyper-fetch/core";`,
|
|
50
|
+
"\n\n",
|
|
51
|
+
schemaTypes,
|
|
52
|
+
"\n\n",
|
|
53
|
+
generatedTypes.join("\n\n"),
|
|
54
|
+
"\n\n",
|
|
55
|
+
sdkSchema,
|
|
56
|
+
"\n\n",
|
|
57
|
+
createSdkFn,
|
|
58
|
+
].join("");
|
|
59
|
+
const prettierOpts = {
|
|
60
|
+
printWidth: 120,
|
|
61
|
+
tabWidth: 2,
|
|
62
|
+
useTabs: false,
|
|
63
|
+
semi: true,
|
|
64
|
+
singleQuote: false,
|
|
65
|
+
trailingComma: "all" as const,
|
|
66
|
+
bracketSpacing: true,
|
|
67
|
+
bracketSameLine: false,
|
|
68
|
+
proseWrap: "always" as const,
|
|
69
|
+
arrowParens: "always" as const,
|
|
70
|
+
parser: "typescript" as const,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const fName = fileName || defaultFileName;
|
|
74
|
+
const extension = config.tsx ? ".ts" : ".js";
|
|
75
|
+
const hasExtension = [".ts", ".js", ".tsx", ".jsx"].some((ext) => fName.endsWith(ext));
|
|
76
|
+
const generatedPath = path.join(config.resolvedPaths.api, `${fName}${hasExtension ? "" : extension}`);
|
|
77
|
+
|
|
78
|
+
const file = await prettier.format(contents, prettierOpts);
|
|
79
|
+
await fs.writeFile(generatedPath, file, {
|
|
80
|
+
flag: "w",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return generatedPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static getSchemaFromUrl = async ({ url, config }: { url: string; config: Config }) => {
|
|
87
|
+
if (isUrl(url)) {
|
|
88
|
+
const client = createClient({ url });
|
|
89
|
+
const getSchema = client.createRequest<{ response: Document }>()({ endpoint: "" });
|
|
90
|
+
const { data, error } = await getSchema.send();
|
|
91
|
+
if (data) {
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
throw error || new Error("Failed to fetch schema");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const schema = fs.readFileSync(path.join(config.resolvedPaths.cwd, url), "utf-8");
|
|
98
|
+
return JSON.parse(schema);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
generateRequestsFromSchema = async () => {
|
|
102
|
+
const { schemaTypes, exportedTypes } = await OpenapiRequestGenerator.prepareSchema(this.openapiDocument);
|
|
103
|
+
|
|
104
|
+
const generatedTypes: string[] = [];
|
|
105
|
+
const schemaTree: Record<string, any> = {};
|
|
106
|
+
|
|
107
|
+
getAvailableOperations(this.openapiDocument).forEach((operation) => {
|
|
108
|
+
const meta = OpenapiRequestGenerator.generateMethodMetadata(operation, exportedTypes);
|
|
109
|
+
const operationTypes = OpenapiRequestGenerator.generateTypes(meta);
|
|
110
|
+
const requestInstanceType = OpenapiRequestGenerator.generateRequestInstanceType(meta, operationTypes);
|
|
111
|
+
|
|
112
|
+
generatedTypes.push(Object.values(operationTypes).join("\n"));
|
|
113
|
+
|
|
114
|
+
const { path: relPath, method } = meta;
|
|
115
|
+
const segments = relPath.split("/").filter(Boolean);
|
|
116
|
+
|
|
117
|
+
let currentLevel = schemaTree;
|
|
118
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
119
|
+
for (const segment of segments) {
|
|
120
|
+
const key = segment.startsWith(":") ? `$${segment.slice(1)}` : segment;
|
|
121
|
+
if (!currentLevel[key]) {
|
|
122
|
+
currentLevel[key] = {};
|
|
123
|
+
}
|
|
124
|
+
currentLevel = currentLevel[key];
|
|
125
|
+
}
|
|
126
|
+
currentLevel[method.toLowerCase()] = requestInstanceType;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const sdkSchema = `export type SdkSchema<Client extends ClientInstance> = {\n${formatSchema(schemaTree)}\n}`;
|
|
130
|
+
|
|
131
|
+
const createSdkFn = `
|
|
132
|
+
export const createSdk = <Client extends ClientInstance>(client: Client) => {
|
|
133
|
+
return coreCreateSdk<Client, SdkSchema<Client>>(client);
|
|
134
|
+
};
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
return { schemaTypes, generatedTypes, sdkSchema, createSdkFn };
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
static generateRequestInstanceType(
|
|
141
|
+
{ id, path: relPath }: { id: string; path: string },
|
|
142
|
+
types: Record<string, string>,
|
|
143
|
+
) {
|
|
144
|
+
const Response = types[`${createTypeBaseName(id)}ResponseType`]
|
|
145
|
+
? `${createTypeBaseName(id)}ResponseType`
|
|
146
|
+
: undefined;
|
|
147
|
+
const Payload = types[`${createTypeBaseName(id)}RequestBody`] ? `${createTypeBaseName(id)}RequestBody` : undefined;
|
|
148
|
+
const LocalError = types[`${createTypeBaseName(id)}ErrorType`] ? `${createTypeBaseName(id)}ErrorType` : undefined;
|
|
149
|
+
const QueryParams = types[`${createTypeBaseName(id)}QueryParams`]
|
|
150
|
+
? `${createTypeBaseName(id)}QueryParams`
|
|
151
|
+
: undefined;
|
|
152
|
+
|
|
153
|
+
const genericTypes: string[] = [];
|
|
154
|
+
|
|
155
|
+
genericTypes.push(`client: Client`);
|
|
156
|
+
genericTypes.push(`endpoint: "${relPath}"`);
|
|
157
|
+
|
|
158
|
+
if (Response) {
|
|
159
|
+
genericTypes.push(`response: ${Response}`);
|
|
160
|
+
}
|
|
161
|
+
if (Payload) {
|
|
162
|
+
genericTypes.push(`payload: ${Payload}`);
|
|
163
|
+
}
|
|
164
|
+
if (LocalError) {
|
|
165
|
+
genericTypes.push(`error: ${LocalError}`);
|
|
166
|
+
}
|
|
167
|
+
if (QueryParams) {
|
|
168
|
+
genericTypes.push(`queryParams: ${QueryParams}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return `RequestInstance<{ ${genericTypes.join(", ")} }>`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static generateHyperFetchRequest(
|
|
175
|
+
{ id, path: relPath, method }: { id: string; path: string; method: string },
|
|
176
|
+
types: Record<string, string>,
|
|
177
|
+
) {
|
|
178
|
+
const Response = types[`${createTypeBaseName(id)}ResponseType`]
|
|
179
|
+
? `${createTypeBaseName(id)}ResponseType`
|
|
180
|
+
: undefined;
|
|
181
|
+
const Payload = types[`${createTypeBaseName(id)}RequestBody`] ? `${createTypeBaseName(id)}RequestBody` : undefined;
|
|
182
|
+
const LocalError = types[`${createTypeBaseName(id)}ErrorType`] ? `${createTypeBaseName(id)}ErrorType` : undefined;
|
|
183
|
+
const QueryParams = types[`${createTypeBaseName(id)}QueryParams`]
|
|
184
|
+
? `${createTypeBaseName(id)}QueryParams`
|
|
185
|
+
: undefined;
|
|
186
|
+
const getVariableName = (str: string) => str.charAt(0).toLowerCase() + str.slice(1);
|
|
187
|
+
|
|
188
|
+
let genericType = "";
|
|
189
|
+
|
|
190
|
+
const addToGenericType = (key: string, value: string) => {
|
|
191
|
+
if (genericType) {
|
|
192
|
+
genericType += ",";
|
|
193
|
+
}
|
|
194
|
+
genericType += `${key}:${value}`;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
if (Response) {
|
|
198
|
+
addToGenericType("response", Response);
|
|
199
|
+
}
|
|
200
|
+
if (Payload) {
|
|
201
|
+
addToGenericType("payload", Payload);
|
|
202
|
+
}
|
|
203
|
+
if (LocalError) {
|
|
204
|
+
addToGenericType("error", LocalError);
|
|
205
|
+
}
|
|
206
|
+
if (QueryParams) {
|
|
207
|
+
addToGenericType("query", QueryParams);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (genericType) {
|
|
211
|
+
genericType = `<{${genericType}}>`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return `export const ${getVariableName(
|
|
215
|
+
createTypeBaseName(id),
|
|
216
|
+
)} = client.createRequest${genericType}()({method: "${method}", endpoint: "${relPath}"})`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
static generateTypes({
|
|
220
|
+
id,
|
|
221
|
+
pathParametersType,
|
|
222
|
+
queryParametersType,
|
|
223
|
+
requestBodyType,
|
|
224
|
+
errorType,
|
|
225
|
+
responseType,
|
|
226
|
+
}: {
|
|
227
|
+
id: string;
|
|
228
|
+
pathParametersType: string | undefined;
|
|
229
|
+
queryParametersType: string | undefined;
|
|
230
|
+
requestBodyType: string | undefined;
|
|
231
|
+
errorType: string;
|
|
232
|
+
responseType: string;
|
|
233
|
+
}) {
|
|
234
|
+
const typeName = createTypeBaseName(id);
|
|
235
|
+
const types: GeneratedTypes<typeof typeName> = {};
|
|
236
|
+
if (pathParametersType) {
|
|
237
|
+
types[`${typeName}PathParams`] = `export type ${typeName}PathParams = ${pathParametersType}`;
|
|
238
|
+
}
|
|
239
|
+
if (queryParametersType) {
|
|
240
|
+
types[`${typeName}QueryParams`] = `export type ${typeName}QueryParams = ${queryParametersType}`;
|
|
241
|
+
}
|
|
242
|
+
if (requestBodyType) {
|
|
243
|
+
types[`${typeName}RequestBody`] = `export type ${typeName}RequestBody = ${requestBodyType}`;
|
|
244
|
+
}
|
|
245
|
+
if (errorType) {
|
|
246
|
+
types[`${typeName}ErrorType`] = `export type ${typeName}ErrorType = ${errorType}`;
|
|
247
|
+
}
|
|
248
|
+
if (responseType) {
|
|
249
|
+
types[`${typeName}ResponseType`] = `export type ${typeName}ResponseType = ${responseType}`;
|
|
250
|
+
}
|
|
251
|
+
return types;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
static generateMethodMetadata(
|
|
255
|
+
operation: { operationId: string; path: string; method: string } & Partial<Operation>,
|
|
256
|
+
exportTypes: ExportedType[],
|
|
257
|
+
) {
|
|
258
|
+
const { operationId, method, path: relPath } = operation;
|
|
259
|
+
const normalizedOperationId = normalizeOperationId(operationId);
|
|
260
|
+
const pathParametersType = find(exportTypes, {
|
|
261
|
+
schemaRef: `#/paths/${normalizedOperationId}/pathParameters`,
|
|
262
|
+
})?.path;
|
|
263
|
+
const queryParametersType = find(exportTypes, {
|
|
264
|
+
schemaRef: `#/paths/${normalizedOperationId}/queryParameters`,
|
|
265
|
+
})?.path;
|
|
266
|
+
const requestBodyType = find(exportTypes, { schemaRef: `#/paths/${normalizedOperationId}/requestBody` })?.path;
|
|
267
|
+
const responseTypePaths = chain(exportTypes)
|
|
268
|
+
.filter(({ schemaRef }) => schemaRef.startsWith(`#/paths/${normalizedOperationId}/responses/2`))
|
|
269
|
+
.map(({ path: responsePath }) => responsePath)
|
|
270
|
+
.value();
|
|
271
|
+
const errorTypePaths = chain(exportTypes)
|
|
272
|
+
.filter(
|
|
273
|
+
({ schemaRef }) =>
|
|
274
|
+
schemaRef.startsWith(`#/paths/${normalizedOperationId}/responses/4`) ||
|
|
275
|
+
schemaRef.startsWith(`#/paths/${normalizedOperationId}/responses/5`),
|
|
276
|
+
)
|
|
277
|
+
.map(({ path: errorPath }) => errorPath)
|
|
278
|
+
.value();
|
|
279
|
+
|
|
280
|
+
const responseType = !isEmpty(responseTypePaths) ? responseTypePaths.join(" | ") : "any";
|
|
281
|
+
const errorType = !isEmpty(errorTypePaths) ? errorTypePaths.join(" | ") : "undefined";
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
id: normalizedOperationId,
|
|
285
|
+
pathParametersType,
|
|
286
|
+
queryParametersType,
|
|
287
|
+
requestBodyType,
|
|
288
|
+
errorType,
|
|
289
|
+
responseType,
|
|
290
|
+
path: adjustPathParamsFormat(relPath),
|
|
291
|
+
method: method ? method.toUpperCase() : HttpMethod.GET,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
static validateSchema(openapiDocument: Document) {
|
|
296
|
+
// Validate refs before processing
|
|
297
|
+
const errors: RefError[] = [];
|
|
298
|
+
|
|
299
|
+
function validateRefs(obj: any, refPath = "") {
|
|
300
|
+
if (!obj || typeof obj !== "object") return;
|
|
301
|
+
|
|
302
|
+
// Check if current object has $ref
|
|
303
|
+
if (obj.$ref && typeof obj.$ref === "string" && obj.$ref.endsWith("/")) {
|
|
304
|
+
errors.push({
|
|
305
|
+
path: refPath,
|
|
306
|
+
ref: obj.$ref,
|
|
307
|
+
message: `Invalid reference "${obj.$ref}" - reference path cannot end with '/'`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Recursively check all object properties
|
|
312
|
+
Object.entries(obj).forEach(([key, value]) => {
|
|
313
|
+
const newPath = refPath ? `${refPath}.${key}` : key;
|
|
314
|
+
validateRefs(value, newPath);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
validateRefs(openapiDocument);
|
|
319
|
+
|
|
320
|
+
// If there are validation errors, throw them with details
|
|
321
|
+
if (errors.length > 0) {
|
|
322
|
+
const errorMessages = errors.map((err) => `Invalid reference at ${err.path}: ${err.message}`);
|
|
323
|
+
throw new Error(`Schema validation failed. The following errors were found:\n${errorMessages.join("\n")}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
static async prepareSchema(openapiDocument: Document) {
|
|
328
|
+
OpenapiRequestGenerator.validateSchema(openapiDocument);
|
|
329
|
+
|
|
330
|
+
const rootSchema = await RefParser.bundle(openapiDocument);
|
|
331
|
+
const schema = parseSchema(rootSchema as any);
|
|
332
|
+
const generator = new DtsGenerator([schema]);
|
|
333
|
+
const schemaTypes = await generator.generate();
|
|
334
|
+
const exportedTypes = generator.getExports();
|
|
335
|
+
|
|
336
|
+
return { schemaTypes, exportedTypes };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
|
|
2
|
+
|
|
3
|
+
import { HttpMethod } from "./http-methods.enum";
|
|
4
|
+
|
|
5
|
+
export interface Operation extends OpenAPIV3.OperationObject {
|
|
6
|
+
path: string;
|
|
7
|
+
method: HttpMethod;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Document = OpenAPIV3.Document | OpenAPIV3_1.Document;
|
|
11
|
+
|
|
12
|
+
export type GeneratedTypes<T extends string> = {
|
|
13
|
+
[K in `${T}PathParams` | `${T}QueryParams` | `${T}RequestBody` | `${T}ResponseType` | `${T}ErrorType`]: string;
|
|
14
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
|
|
2
|
+
|
|
3
|
+
import { HttpMethod } from "./http-methods.enum";
|
|
4
|
+
import { Operation } from "./openapi.types";
|
|
5
|
+
|
|
6
|
+
export function getAvailableOperations(openApiJson: OpenAPIV3.Document | OpenAPIV3_1.Document) {
|
|
7
|
+
const paths = openApiJson.paths || {};
|
|
8
|
+
return Object.entries(paths).flatMap(([path, pathObject]) => {
|
|
9
|
+
return Object.values(HttpMethod)
|
|
10
|
+
.map((method) => ({ path, method, operation: pathObject?.[method] }))
|
|
11
|
+
.filter(({ operation }) => operation?.operationId)
|
|
12
|
+
.map(({ operation, method }) => {
|
|
13
|
+
const op: { operationId: string; path: string; method: string } & Partial<Operation> = {
|
|
14
|
+
...operation,
|
|
15
|
+
operationId: operation!.operationId as string,
|
|
16
|
+
path,
|
|
17
|
+
method,
|
|
18
|
+
};
|
|
19
|
+
return op;
|
|
20
|
+
});
|
|
21
|
+
}) as ({ operationId: string } & Operation)[];
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
|
|
2
|
+
|
|
3
|
+
export function adjustPathParamsFormat(path: string) {
|
|
4
|
+
// Naive implementation for now:
|
|
5
|
+
return path.replace(/}/g, "").replace(/{/g, ":");
|
|
6
|
+
}
|
|
7
|
+
export function createTypeBaseName(str: string) {
|
|
8
|
+
const capitalizeFirstLetter = (s: string) => {
|
|
9
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
10
|
+
};
|
|
11
|
+
return str.split("_").map(capitalizeFirstLetter).join("");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function normalizeOperationId(key: string): string {
|
|
15
|
+
return key
|
|
16
|
+
.replace(/\/(.)/g, (_match: string, p1: string) => {
|
|
17
|
+
return p1.toUpperCase();
|
|
18
|
+
})
|
|
19
|
+
.replace(/}/g, "")
|
|
20
|
+
.replace(/{/g, "$")
|
|
21
|
+
.replace(/^\//, "")
|
|
22
|
+
.replace(/[^0-9A-Za-z_$]+/g, "_");
|
|
23
|
+
}
|
|
24
|
+
export const isUrl = (schemaPath: string) => {
|
|
25
|
+
try {
|
|
26
|
+
return Boolean(new URL(schemaPath));
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function getBaseUrl(openApiJson: OpenAPIV3.Document | OpenAPIV3_1.Document): string {
|
|
33
|
+
return openApiJson.servers?.[0]?.url ?? "";
|
|
34
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { checkbox, confirm } from "@inquirer/prompts";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
// import { Registry } from "registry/schema";
|
|
6
|
+
import { logger } from "../utils/logger";
|
|
7
|
+
import { handleError } from "../utils/handle-error";
|
|
8
|
+
import { preFlightAdd } from "preflights/preflight-add";
|
|
9
|
+
import { showHelp } from "utils/show-help";
|
|
10
|
+
|
|
11
|
+
// const registry: Registry[] = getRegistry();
|
|
12
|
+
|
|
13
|
+
export const addOptionsSchema = z.object({
|
|
14
|
+
sdks: z.array(z.string()).optional().describe("names, url or local path to the sdks"),
|
|
15
|
+
yes: z.boolean().optional().describe("skip confirmation prompt."),
|
|
16
|
+
overwrite: z.boolean().optional().describe("overwrite existing files."),
|
|
17
|
+
cwd: z.string().describe("the working directory. defaults to the current directory."),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
async function promptForSdks() {
|
|
21
|
+
const sdks = await checkbox({
|
|
22
|
+
message: "Which SDKs would you like to add?",
|
|
23
|
+
choices: [] as { name: string; value: string }[],
|
|
24
|
+
// choices: registry.map((sdk) => ({
|
|
25
|
+
// name: sdk.meta.name,
|
|
26
|
+
// value: sdk.meta.name,
|
|
27
|
+
// })),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return sdks;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// TODO: figure out a way to add sdks from registry?
|
|
34
|
+
export const add = new Command()
|
|
35
|
+
.name("Add")
|
|
36
|
+
.description("Add SDK from registry to your project")
|
|
37
|
+
.argument("[sdks...]", "names, url or local path to the sdks")
|
|
38
|
+
.option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd())
|
|
39
|
+
.option("-y, --yes", "skip confirmation prompt.", false)
|
|
40
|
+
.option("-o, --overwrite", "overwrite existing files.", false)
|
|
41
|
+
.option("-h, --help <help>", "display help for command")
|
|
42
|
+
.action(async (sdks: string[], opts: z.infer<typeof addOptionsSchema>) => {
|
|
43
|
+
try {
|
|
44
|
+
const help = process.argv.includes("--help") || process.argv.includes("-h");
|
|
45
|
+
|
|
46
|
+
if (help) {
|
|
47
|
+
return showHelp(addOptionsSchema);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await preFlightAdd({
|
|
51
|
+
...opts,
|
|
52
|
+
sdks,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const options = addOptionsSchema.parse({
|
|
56
|
+
sdks,
|
|
57
|
+
...opts,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let selectedSdks = options.sdks;
|
|
61
|
+
if (!selectedSdks?.length) {
|
|
62
|
+
selectedSdks = await promptForSdks();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!selectedSdks?.length) {
|
|
66
|
+
logger.warn("No SDKs selected. Exiting.");
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!options.yes) {
|
|
71
|
+
const proceed = await confirm({
|
|
72
|
+
message: `Ready to install the following SDKs?\n - ${selectedSdks.join("\n - ")}`,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!proceed) {
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logger.info("Installing SDKs...");
|
|
81
|
+
|
|
82
|
+
// for (const sdk of selectedSdks) {
|
|
83
|
+
// TODO: implement
|
|
84
|
+
// }
|
|
85
|
+
|
|
86
|
+
logger.success("SDKs installed successfully!");
|
|
87
|
+
} catch (error) {
|
|
88
|
+
handleError(error);
|
|
89
|
+
}
|
|
90
|
+
});
|