Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f70fd02

Browse files
committedApr 22, 2024
chore(deps): update @hey-api/openapi-ts to 0.42.1
- Update to the latest `@hey-api/openapi-ts` version `0.42.1` - Fixed a bug where types in mutation hooks were imported from a (non-relative) direct file import. - This is the same fix implemented in #61, which was done for query hooks. - Fixed a bug where JSDocs were not being added to generated hooks. - Add more unit tests Fixes: #83 Enables work towards: #81
1 parent 18ac5fa commit f70fd02

22 files changed

+1121
-424
lines changed
 

‎README.md

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,16 @@ Options:
4545
-o, --output <value> Output directory (default: "openapi")
4646
-c, --client <value> HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch")
4747
--request <value> Path to custom request file
48-
--useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
49-
--enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript']
50-
--base <value> Manually set base in OpenAPI config instead of inferring from server value
51-
--serviceResponse <value> Define shape of returned value from service calls ['body', 'generics', 'response']
48+
--format <value> Process output folder with formatter? ['biome', 'prettier']
49+
--lint <value> Process output folder with linter? ['eslint', 'biome']
5250
--operationId Use operation ID to generate operation names?
53-
--lint Process output folder with linter?
54-
--format Process output folder with formatter?
51+
--serviceResponse <value> Define shape of returned value from service calls ['body', 'generics', 'response']
52+
--base <value> Manually set base in OpenAPI config instead of inferring from server value
53+
--enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript']
54+
--useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
55+
--debug Enable debug mode
56+
--noSchemas Disable generating schemas for request and response objects
57+
--schemaTypes <value> Define the type of schema generation ['form', 'json'] (default: "json")
5558
-h, --help display help for command
5659
```
5760

@@ -86,11 +89,7 @@ function App() {
8689
return (
8790
<div className="App">
8891
<h1>Pet List</h1>
89-
<ul>
90-
{data?.map((pet) => (
91-
<li key={pet.id}>{pet.name}</li>
92-
))}
93-
</ul>
92+
<ul>{data?.map((pet) => <li key={pet.id}>{pet.name}</li>)}</ul>
9493
</div>
9594
);
9695
}
@@ -129,13 +128,7 @@ import { useDefaultClientFindPetsSuspense } from "../openapi/queries/suspense";
129128
function ChildComponent() {
130129
const { data } = useDefaultClientFindPetsSuspense({ tags: [], limit: 10 });
131130

132-
return (
133-
<ul>
134-
{data?.map((pet, index) => (
135-
<li key={pet.id}>{pet.name}</li>
136-
))}
137-
</ul>
138-
);
131+
return <ul>{data?.map((pet, index) => <li key={pet.id}>{pet.name}</li>)}</ul>;
139132
}
140133

141134
function ParentComponent() {

‎examples/react-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"dev:mock": "prism mock ./petstore.yaml --dynamic",
1010
"build": "tsc && vite build",
1111
"preview": "vite preview",
12-
"generate:api": "node ../../dist/cli.mjs -i ./petstore.yaml -c axios --request ./request.ts",
12+
"generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ./petstore.yaml -c axios --request ./request.ts",
1313
"test:generated": "tsc -p ./tsconfig.openapi.json --noEmit"
1414
},
1515
"dependencies": {

‎package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"author": "Daiki Urata (@7nohe)",
3939
"license": "MIT",
4040
"devDependencies": {
41-
"@hey-api/openapi-ts": "0.36.0",
41+
"@hey-api/openapi-ts": "0.42.1",
4242
"@types/node": "^20.10.6",
4343
"@vitest/coverage-v8": "^1.5.0",
4444
"commander": "^12.0.0",
@@ -49,11 +49,11 @@
4949
"vitest": "^1.5.0"
5050
},
5151
"peerDependencies": {
52-
"@hey-api/openapi-ts": "0.36.0",
53-
"commander": ">= 11 < 13",
54-
"glob": ">= 10",
55-
"ts-morph": ">= 22 < 23",
56-
"typescript": ">= 4.8.3"
52+
"@hey-api/openapi-ts": "0.42.1",
53+
"commander": "12.x",
54+
"glob": "10.x",
55+
"ts-morph": "22.x",
56+
"typescript": "5.x"
5757
},
5858
"engines": {
5959
"node": ">=14"

‎pnpm-lock.yaml

Lines changed: 198 additions & 201 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/cli.mts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
11
#!/usr/bin/env node
22
import { generate } from "./generate.mjs";
33
import { Command, Option } from "commander";
4-
import { UserConfig } from "@hey-api/openapi-ts";
54
import { readFile } from "fs/promises";
65
import { dirname, join } from "path";
76
import { fileURLToPath } from "node:url";
7+
import { defaultOutputPath } from "./constants.mjs";
88

99
const program = new Command();
1010

11-
export type LimitedUserConfig = Omit<UserConfig, "useOptions">;
11+
export type LimitedUserConfig = {
12+
input: string;
13+
output: string;
14+
client?: "angular" | "axios" | "fetch" | "node" | "xhr";
15+
request?: string;
16+
format?: "biome" | "prettier";
17+
lint?: "biome" | "eslint";
18+
operationId?: boolean;
19+
serviceResponse?: "body" | "response";
20+
base?: string;
21+
enums?: "javascript" | "typescript";
22+
useDateType?: boolean;
23+
debug?: boolean;
24+
noSchemas?: boolean;
25+
schemaType?: "form" | "json";
26+
};
1227

1328
async function setupProgram() {
1429
const __filename = fileURLToPath(import.meta.url);
@@ -25,21 +40,31 @@ async function setupProgram() {
2540
"-i, --input <value>",
2641
"OpenAPI specification, can be a path, url or string content (required)"
2742
)
28-
.option("-o, --output <value>", "Output directory", "openapi")
43+
.option("-o, --output <value>", "Output directory", defaultOutputPath)
2944
.addOption(
3045
new Option("-c, --client <value>", "HTTP client to generate")
3146
.choices(["angular", "axios", "fetch", "node", "xhr"])
3247
.default("fetch")
3348
)
3449
.option("--request <value>", "Path to custom request file")
35-
.option("--format", "Process output folder with formatter?")
36-
.option("--lint", "Process output folder with linter?")
50+
.addOption(
51+
new Option(
52+
"--format <value>",
53+
"Process output folder with formatter?"
54+
).choices(["biome", "prettier"])
55+
)
56+
.addOption(
57+
new Option(
58+
"--lint <value>",
59+
"Process output folder with linter?"
60+
).choices(["biome", "eslint"])
61+
)
3762
.option("--operationId", "Use operation ID to generate operation names?")
3863
.addOption(
3964
new Option(
4065
"--serviceResponse <value>",
4166
"Define shape of returned value from service calls"
42-
).choices(["body", "generics", "response"])
67+
).choices(["body", "response"])
4368
)
4469
.option(
4570
"--base <value>",
@@ -55,6 +80,14 @@ async function setupProgram() {
5580
"--useDateType",
5681
"Use Date type instead of string for date types for models, this will not convert the data to a Date object"
5782
)
83+
.option("--debug", "Run in debug mode?")
84+
.option("--noSchemas", "Disable generating JSON schemas")
85+
.addOption(
86+
new Option(
87+
"--schemaType <value>",
88+
"Type of JSON schema [Default: 'json']"
89+
).choices(["form", "json"])
90+
)
5891
.parse();
5992

6093
const options = program.opts<LimitedUserConfig>();

‎src/common.mts

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { type PathLike } from "fs";
22
import { stat } from "fs/promises";
33
import ts from "typescript";
4+
import path from "path";
45
import {
56
MethodDeclaration,
6-
JSDoc,
77
SourceFile,
88
ParameterDeclaration,
9+
ClassDeclaration,
910
} from "ts-morph";
11+
import { LimitedUserConfig } from "./cli.mjs";
12+
import { requestsOutputPath } from "./constants.mjs";
1013

1114
export const TData = ts.factory.createIdentifier("TData");
1215
export const TError = ts.factory.createIdentifier("TError");
@@ -27,7 +30,11 @@ export const lowercaseFirstLetter = (str: string) => {
2730
};
2831

2932
export const getNameFromMethod = (method: MethodDeclaration) => {
30-
return method.getName();
33+
const methodName = method.getName();
34+
if (!methodName) {
35+
throw new Error("Method name not found");
36+
}
37+
return methodName;
3138
};
3239

3340
export type MethodDescription = {
@@ -36,7 +43,7 @@ export type MethodDescription = {
3643
method: MethodDeclaration;
3744
methodBlock: ts.Block;
3845
httpMethodName: string;
39-
jsDoc: JSDoc[];
46+
jsDoc: string;
4047
isDeprecated: boolean;
4148
};
4249

@@ -89,3 +96,73 @@ export function extractPropertiesFromObjectParam(param: ParameterDeclaration) {
8996
}));
9097
return paramNodes;
9198
}
99+
100+
/**
101+
* Replace the import("...") surrounding the type if there is one.
102+
* This can happen when the type is imported from another file, but
103+
* we are already importing all the types from that file.
104+
*
105+
* https://regex101.com/r/3DyHaQ/1
106+
*
107+
* TODO: Replace with a more robust solution.
108+
*/
109+
export function getShortType(type: string) {
110+
return type.replaceAll(/import\(".*"\)\./g, "");
111+
}
112+
113+
export function getClassesFromService(node: SourceFile) {
114+
const klasses = node.getClasses();
115+
116+
if (!klasses.length) {
117+
throw new Error("No classes found");
118+
}
119+
120+
return klasses.map((klass) => {
121+
const className = klass.getName();
122+
if (!className) {
123+
throw new Error("Class name not found");
124+
}
125+
return {
126+
className,
127+
klass,
128+
};
129+
});
130+
}
131+
132+
export function getClassNameFromClassNode(klass: ClassDeclaration) {
133+
const className = klass.getName();
134+
135+
if (!className) {
136+
throw new Error("Class name not found");
137+
}
138+
return className;
139+
}
140+
141+
export function formatOptions(options: LimitedUserConfig) {
142+
// loop through properties on the options object
143+
// if the property is a string of number then convert it to a number
144+
// if the property is a string of boolean then convert it to a boolean
145+
const formattedOptions = Object.entries(options).reduce(
146+
(acc, [key, value]) => {
147+
const typedKey = key as keyof LimitedUserConfig;
148+
const typedValue = value as (typeof options)[keyof LimitedUserConfig];
149+
const parsedNumber = safeParseNumber(typedValue);
150+
if (value === "true" || value === true) {
151+
(acc as any)[typedKey] = true;
152+
} else if (value === "false" || value === false) {
153+
(acc as any)[typedKey] = false;
154+
} else if (!isNaN(parsedNumber)) {
155+
(acc as any)[typedKey] = parsedNumber;
156+
} else {
157+
(acc as any)[typedKey] = typedValue;
158+
}
159+
return acc;
160+
},
161+
options
162+
);
163+
return formattedOptions;
164+
}
165+
166+
export function buildOutputPath(outputPath: string) {
167+
return path.join(outputPath, requestsOutputPath);
168+
}

‎src/constants.mts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
export const defaultOutputPath = "openapi";
22
export const queriesOutputPath = "queries";
33
export const requestsOutputPath = "requests";
4+
5+
export const serviceFileName = "services.gen";
6+
export const modalsFileName = "types.gen";

‎src/createImports.mts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ts from "typescript";
22
import { posix } from "path";
33
import { Project } from "ts-morph";
4+
import { modalsFileName, serviceFileName } from "./constants.mjs";
45

56
const { join } = posix;
67

@@ -13,11 +14,11 @@ export const createImports = ({
1314
}) => {
1415
const modelsFile = project
1516
.getSourceFiles()
16-
.find((sourceFile) => sourceFile.getFilePath().includes("models.ts"));
17+
.find((sourceFile) => sourceFile.getFilePath().includes(modalsFileName));
1718

1819
const serviceFile = project
1920
.getSourceFiles()
20-
.find((sourceFile) => sourceFile.getFilePath().includes("services.ts"));
21+
.find((sourceFile) => sourceFile.getFilePath().includes(serviceFileName));
2122

2223
if (!modelsFile) {
2324
console.warn(`
@@ -116,7 +117,7 @@ export const createImports = ({
116117
),
117118
])
118119
),
119-
ts.factory.createStringLiteral(join("../requests")),
120+
ts.factory.createStringLiteral(join("../requests", serviceFileName)),
120121
undefined
121122
),
122123
];
@@ -138,7 +139,7 @@ export const createImports = ({
138139
),
139140
])
140141
),
141-
ts.factory.createStringLiteral(join("../requests/models")),
142+
ts.factory.createStringLiteral(join("../requests/", modalsFileName)),
142143
undefined
143144
)
144145
);

‎src/createUseMutation.mts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
capitalizeFirstLetter,
99
extractPropertiesFromObjectParam,
1010
getNameFromMethod,
11+
getShortType,
1112
} from "./common.mjs";
1213
import { addJSDocToNode } from "./util.mjs";
1314

@@ -41,11 +42,9 @@ function generateAwaitedReturnType({
4142
}
4243

4344
export const createUseMutation = ({
44-
node,
4545
className,
4646
method,
47-
jsDoc = [],
48-
isDeprecated = false,
47+
jsDoc,
4948
}: MethodDescription) => {
5049
const methodName = getNameFromMethod(method);
5150
const awaitedResponseDataType = generateAwaitedReturnType({
@@ -83,24 +82,13 @@ export const createUseMutation = ({
8382
refParam.optional
8483
? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
8584
: undefined,
86-
// refParam.questionToken ?? refParam.initializer
87-
// ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
88-
// : refParam.questionToken,
8985
ts.factory.createTypeReferenceNode(
90-
refParam.type.getText(param)
86+
getShortType(refParam.type.getText(param))
9187
)
9288
)
9389
);
9490
})
9591
.flat()
96-
// return ts.factory.createPropertySignature(
97-
// undefined,
98-
// ts.factory.createIdentifier(param.getName()),
99-
// param.compilerNode.questionToken ?? param.compilerNode.initializer
100-
// ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
101-
// : param.compilerNode.questionToken,
102-
// param.compilerNode.type
103-
// );
10492
)
10593
: ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
10694

@@ -262,7 +250,7 @@ export const createUseMutation = ({
262250
)
263251
);
264252

265-
const hookWithJsDoc = addJSDocToNode(exportHook, node, isDeprecated, jsDoc);
253+
const hookWithJsDoc = addJSDocToNode(exportHook, jsDoc);
266254

267255
return {
268256
mutationResult,

‎src/createUseQuery.mts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
capitalizeFirstLetter,
66
extractPropertiesFromObjectParam,
77
getNameFromMethod,
8+
getShortType,
89
queryKeyConstraint,
910
queryKeyGenericType,
1011
TData,
@@ -72,15 +73,6 @@ export const createApiResponseType = ({
7273
};
7374
};
7475

75-
/**
76-
* Replace the import("...") surrounding the type if there is one.
77-
* This can happen when the type is imported from another file, but
78-
* we are already importing all the types from that file.
79-
*/
80-
function getShortType(type: string) {
81-
return type.replaceAll(/import\("[a-zA-Z\/\.-]*"\)\./g, "");
82-
}
83-
8476
export function getRequestParamFromMethod(method: MethodDeclaration) {
8577
if (!method.getParameters().length) {
8678
return null;
@@ -122,9 +114,6 @@ export function getRequestParamFromMethod(method: MethodDeclaration) {
122114
refParam.optional
123115
? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
124116
: undefined,
125-
// param.hasQuestionToken() ?? param.getInitializer()?.compilerNode
126-
// ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
127-
// : param.getQuestionTokenNode()?.compilerNode,
128117
ts.factory.createTypeReferenceNode(refParam.typeName)
129118
);
130119
})
@@ -426,11 +415,9 @@ function createQueryHook({
426415
}
427416

428417
export const createUseQuery = ({
429-
node,
430418
className,
431419
method,
432-
jsDoc = [],
433-
isDeprecated: deprecated = false,
420+
jsDoc,
434421
}: MethodDescription) => {
435422
const methodName = getNameFromMethod(method);
436423
const queryKey = createQueryKeyFromMethod({ method, className });
@@ -461,13 +448,8 @@ export const createUseQuery = ({
461448
className,
462449
});
463450

464-
const hookWithJsDoc = addJSDocToNode(queryHook, node, deprecated, jsDoc);
465-
const suspenseHookWithJsDoc = addJSDocToNode(
466-
suspenseQueryHook,
467-
node,
468-
deprecated,
469-
jsDoc
470-
);
451+
const hookWithJsDoc = addJSDocToNode(queryHook, jsDoc);
452+
const suspenseHookWithJsDoc = addJSDocToNode(suspenseQueryHook, jsDoc);
471453

472454
const returnTypeExport = createReturnTypeExport({
473455
className,

‎src/generate.mts

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,38 @@
11
import { createClient, UserConfig } from "@hey-api/openapi-ts";
22
import { print } from "./print.mjs";
3-
import path from "path";
43
import { createSource } from "./createSource.mjs";
5-
import { defaultOutputPath, requestsOutputPath } from "./constants.mjs";
6-
import { safeParseNumber } from "./common.mjs";
4+
import { buildOutputPath, formatOptions } from "./common.mjs";
5+
import { LimitedUserConfig } from "./cli.mjs";
76

8-
export async function generate(options: UserConfig, version: string) {
9-
const openApiOutputPath = path.join(
10-
options.output ?? defaultOutputPath,
11-
requestsOutputPath
12-
);
7+
export async function generate(options: LimitedUserConfig, version: string) {
8+
const openApiOutputPath = buildOutputPath(options.output);
9+
const formattedOptions = formatOptions(options);
1310

14-
// loop through properties on the options object
15-
// if the property is a string of number then convert it to a number
16-
// if the property is a string of boolean then convert it to a boolean
17-
const formattedOptions = Object.entries(options).reduce(
18-
(acc, [key, value]) => {
19-
const typedKey = key as keyof UserConfig;
20-
const typedValue = value as (typeof options)[keyof UserConfig];
21-
const parsedNumber = safeParseNumber(typedValue);
22-
if (value === "true" || value === true) {
23-
(acc as any)[typedKey] = true;
24-
} else if (value === "false" || value === false) {
25-
(acc as any)[typedKey] = false;
26-
} else if (!isNaN(parsedNumber)) {
27-
(acc as any)[typedKey] = parsedNumber;
28-
} else {
29-
(acc as any)[typedKey] = typedValue;
30-
}
31-
return acc;
32-
},
33-
options
34-
);
3511
const config: UserConfig = {
36-
...formattedOptions,
12+
base: formattedOptions.base,
13+
client: formattedOptions.client,
14+
debug: formattedOptions.debug,
15+
dryRun: false,
16+
enums: formattedOptions.enums,
17+
exportCore: true,
18+
format: formattedOptions.format,
19+
input: formattedOptions.input,
20+
lint: formattedOptions.lint,
3721
output: openApiOutputPath,
22+
request: formattedOptions.request,
23+
schemas: {
24+
export: !formattedOptions.noSchemas,
25+
type: formattedOptions.schemaType,
26+
},
27+
services: {
28+
export: true,
29+
response: formattedOptions.serviceResponse,
30+
},
31+
types: {
32+
dates: formattedOptions.useDateType,
33+
export: true,
34+
},
3835
useOptions: true,
39-
exportCore: true,
40-
exportModels: true,
41-
exportServices: true,
42-
write: true,
4336
};
4437
await createClient(config);
4538
const source = await createSource({

‎src/print.mts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { mkdir, writeFile } from "fs/promises";
22
import path from "path";
3-
import { defaultOutputPath, queriesOutputPath } from "./constants.mjs";
3+
import { queriesOutputPath } from "./constants.mjs";
44
import { LimitedUserConfig } from "./cli.mjs";
55
import { exists } from "./common.mjs";
66

@@ -9,9 +9,9 @@ async function printGeneratedTS(
99
name: string;
1010
content: string;
1111
},
12-
options: LimitedUserConfig
12+
options: Pick<LimitedUserConfig, "output">
1313
) {
14-
const dir = path.join(options.output ?? defaultOutputPath, queriesOutputPath);
14+
const dir = path.join(options.output, queriesOutputPath);
1515
const dirExists = await exists(dir);
1616
if (!dirExists) {
1717
await mkdir(dir, { recursive: true });
@@ -24,9 +24,9 @@ export async function print(
2424
name: string;
2525
content: string;
2626
}[],
27-
options: LimitedUserConfig
27+
options: Pick<LimitedUserConfig, "output">
2828
) {
29-
const outputPath = options.output ?? defaultOutputPath;
29+
const outputPath = options.output;
3030
const dirExists = await exists(outputPath);
3131
if (!dirExists) {
3232
await mkdir(outputPath);

‎src/service.mts

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import ts from "typescript";
22
import { ClassDeclaration, Project, SourceFile } from "ts-morph";
3-
import { MethodDescription } from "./common.mjs";
3+
import {
4+
MethodDescription,
5+
getClassNameFromClassNode,
6+
getClassesFromService,
7+
} from "./common.mjs";
8+
import { serviceFileName } from "./constants.mjs";
49

510
export type Service = {
611
node: SourceFile;
@@ -14,7 +19,7 @@ export type Service = {
1419
export async function getServices(project: Project): Promise<Service> {
1520
const node = project
1621
.getSourceFiles()
17-
.find((sourceFile) => sourceFile.getFilePath().includes("services.ts"));
22+
.find((sourceFile) => sourceFile.getFilePath().includes(serviceFileName));
1823

1924
if (!node) {
2025
throw new Error("No service node found");
@@ -31,34 +36,6 @@ export async function getServices(project: Project): Promise<Service> {
3136
} satisfies Service;
3237
}
3338

34-
function getClassesFromService(node: SourceFile) {
35-
const klasses = node.getClasses();
36-
37-
if (!klasses.length) {
38-
throw new Error("No classes found");
39-
}
40-
41-
return klasses.map((klass) => {
42-
const className = klass.getName();
43-
if (!className) {
44-
throw new Error("Class name not found");
45-
}
46-
return {
47-
className,
48-
klass,
49-
};
50-
});
51-
}
52-
53-
function getClassNameFromClassNode(klass: ClassDeclaration) {
54-
const className = klass.getName();
55-
56-
if (!className) {
57-
throw new Error("Class name not found");
58-
}
59-
return className;
60-
}
61-
6239
function getMethodsFromService(node: SourceFile, klass: ClassDeclaration) {
6340
const methods = klass.getMethods();
6441
if (!methods.length) {
@@ -106,7 +83,10 @@ function getMethodsFromService(node: SourceFile, klass: ClassDeclaration) {
10683
};
10784

10885
const children = getAllChildren(method.compilerNode);
109-
const jsDoc = method.getJsDocs().map((jsDoc) => jsDoc);
86+
const jsDocs = children
87+
.filter((c) => c.kind === ts.SyntaxKind.JSDoc)
88+
.map((c) => c.getText(node.compilerNode));
89+
const jsDoc = jsDocs?.[0];
11090
const isDeprecated = children.some(
11191
(c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag
11292
);

‎src/util.mts

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,26 @@
11
import ts from "typescript";
2-
import { JSDoc, SourceFile } from "ts-morph";
32

43
export function addJSDocToNode<T extends ts.Node>(
54
node: T,
6-
sourceFile: SourceFile,
7-
deprecated: boolean,
8-
jsDoc: JSDoc[] = []
5+
jsDoc: string | undefined
96
): T {
10-
const deprecatedString = deprecated ? "@deprecated" : "";
7+
if (!jsDoc) {
8+
return node;
9+
}
10+
const removedFirstLine = jsDoc.trim().replace(/^\/\*\*/, "*");
11+
// replace last */ with ''
12+
const removedSecondLine = removedFirstLine.replace(/\*\/$/, "");
1113

12-
const jsDocString = [deprecatedString]
13-
.concat(
14-
jsDoc.map((comment) => {
15-
if (typeof comment === "string") {
16-
return comment;
17-
}
18-
if (Array.isArray(comment)) {
19-
return comment.map((c) => c.getText(sourceFile)).join("\n");
20-
}
21-
return "";
22-
})
23-
)
24-
// remove empty lines
25-
.filter(Boolean)
26-
// trim
27-
.map((comment) => comment.trim())
28-
// add * to each line
29-
.map((comment) => `* ${comment}`)
30-
// join lines
31-
.join("\n")
32-
// replace new lines with \n *
33-
.replace(/\n/g, "\n * ");
14+
const split = removedSecondLine.split("\n");
15+
const trimmed = split.map((line) => line.trim());
16+
const joined = trimmed.join("\n");
3417

35-
const nodeWithJSDoc = jsDocString
36-
? ts.addSyntheticLeadingComment(
37-
node,
38-
ts.SyntaxKind.MultiLineCommentTrivia,
39-
`*\n ${jsDocString}\n `,
40-
true
41-
)
42-
: node;
18+
const nodeWithJSDoc = ts.addSyntheticLeadingComment(
19+
node,
20+
ts.SyntaxKind.MultiLineCommentTrivia,
21+
joined,
22+
true
23+
);
4324

4425
return nodeWithJSDoc;
4526
}

‎tests/__snapshots__/createSource.test.ts.snap

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export * from "./queries";
1010
exports[`createSource > createSource 2`] = `
1111
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
1212
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
13-
import { DefaultService, DefaultData } from "../requests";
14-
import { Pet, NewPet, Error } from "../requests/models";
13+
import { DefaultService } from "../requests/services.gen";
14+
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
1515
export type DefaultServiceFindPetsDefaultResponse = Awaited<ReturnType<typeof DefaultService.findPets>>;
1616
export type DefaultServiceFindPetsQueryResult<TData = DefaultServiceFindPetsDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
1717
export const useDefaultServiceFindPetsKey = "DefaultServiceFindPets";
@@ -31,28 +31,71 @@ exports[`createSource > createSource 3`] = `
3131
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
3232
import * as Common from "./common";
3333
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
34-
import { DefaultService, DefaultData } from "../requests";
35-
import { Pet, NewPet, Error } from "../requests/models";
34+
import { DefaultService } from "../requests/services.gen";
35+
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
36+
/**
37+
* Returns all pets from the system that the user has access to
38+
* Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.
39+
*
40+
* Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.
41+
*
42+
* @param data The data for the request.
43+
* @param data.tags tags to filter by
44+
* @param data.limit maximum number of results to return
45+
* @returns Pet pet response
46+
* @returns Error unexpected error
47+
* @throws ApiError
48+
*/
3649
export const useDefaultServiceFindPets = <TData = Common.DefaultServiceFindPetsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ limit, tags }: {
3750
limit?: number;
3851
tags?: string[];
3952
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetsKey, ...(queryKey ?? [{ limit, tags }])], queryFn: () => DefaultService.findPets({ limit, tags }) as TData, ...options });
4053
/**
41-
* @deprecated
42-
*/
54+
* @deprecated
55+
* This path is not fully defined.
56+
* @returns unknown unexpected error
57+
* @throws ApiError
58+
*/
4359
export const useDefaultServiceGetNotDefined = <TData = Common.DefaultServiceGetNotDefinedDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>(queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceGetNotDefinedKey, ...(queryKey ?? [])], queryFn: () => DefaultService.getNotDefined() as TData, ...options });
60+
/**
61+
* Returns a user based on a single ID, if the user does not have access to the pet
62+
* @param data The data for the request.
63+
* @param data.id ID of pet to fetch
64+
* @returns Pet pet response
65+
* @returns Error unexpected error
66+
* @throws ApiError
67+
*/
4468
export const useDefaultServiceFindPetById = <TData = Common.DefaultServiceFindPetByIdDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ id }: {
4569
id: number;
4670
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])], queryFn: () => DefaultService.findPetById({ id }) as TData, ...options });
71+
/**
72+
* Creates a new pet in the store. Duplicates are allowed
73+
* @param data The data for the request.
74+
* @param data.requestBody Pet to add to the store
75+
* @returns Pet pet response
76+
* @returns Error unexpected error
77+
* @throws ApiError
78+
*/
4779
export const useDefaultServiceAddPet = <TData = Common.DefaultServiceAddPetMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, {
4880
requestBody: NewPet;
4981
}, TContext>, "mutationFn">) => useMutation<TData, TError, {
5082
requestBody: NewPet;
5183
}, TContext>({ mutationFn: ({ requestBody }) => DefaultService.addPet({ requestBody }) as unknown as Promise<TData>, ...options });
5284
/**
53-
* @deprecated
54-
*/
85+
* @deprecated
86+
* This path is not defined at all.
87+
* @returns unknown unexpected error
88+
* @throws ApiError
89+
*/
5590
export const useDefaultServicePostNotDefined = <TData = Common.DefaultServicePostNotDefinedMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, void, TContext>, "mutationFn">) => useMutation<TData, TError, void, TContext>({ mutationFn: () => DefaultService.postNotDefined() as unknown as Promise<TData>, ...options });
91+
/**
92+
* deletes a single pet based on the ID supplied
93+
* @param data The data for the request.
94+
* @param data.id ID of pet to delete
95+
* @returns Error unexpected error
96+
* @returns void pet deleted
97+
* @throws ApiError
98+
*/
5699
export const useDefaultServiceDeletePet = <TData = Common.DefaultServiceDeletePetMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, {
57100
id: number;
58101
}, TContext>, "mutationFn">) => useMutation<TData, TError, {
@@ -65,16 +108,40 @@ exports[`createSource > createSource 4`] = `
65108
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
66109
import * as Common from "./common";
67110
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
68-
import { DefaultService, DefaultData } from "../requests";
69-
import { Pet, NewPet, Error } from "../requests/models";
111+
import { DefaultService } from "../requests/services.gen";
112+
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
113+
/**
114+
* Returns all pets from the system that the user has access to
115+
* Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.
116+
*
117+
* Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.
118+
*
119+
* @param data The data for the request.
120+
* @param data.tags tags to filter by
121+
* @param data.limit maximum number of results to return
122+
* @returns Pet pet response
123+
* @returns Error unexpected error
124+
* @throws ApiError
125+
*/
70126
export const useDefaultServiceFindPetsSuspense = <TData = Common.DefaultServiceFindPetsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ limit, tags }: {
71127
limit?: number;
72128
tags?: string[];
73129
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetsKey, ...(queryKey ?? [{ limit, tags }])], queryFn: () => DefaultService.findPets({ limit, tags }) as TData, ...options });
74130
/**
75-
* @deprecated
76-
*/
131+
* @deprecated
132+
* This path is not fully defined.
133+
* @returns unknown unexpected error
134+
* @throws ApiError
135+
*/
77136
export const useDefaultServiceGetNotDefinedSuspense = <TData = Common.DefaultServiceGetNotDefinedDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>(queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceGetNotDefinedKey, ...(queryKey ?? [])], queryFn: () => DefaultService.getNotDefined() as TData, ...options });
137+
/**
138+
* Returns a user based on a single ID, if the user does not have access to the pet
139+
* @param data The data for the request.
140+
* @param data.id ID of pet to fetch
141+
* @returns Pet pet response
142+
* @returns Error unexpected error
143+
* @throws ApiError
144+
*/
78145
export const useDefaultServiceFindPetByIdSuspense = <TData = Common.DefaultServiceFindPetByIdDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ id }: {
79146
id: number;
80147
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])], queryFn: () => DefaultService.findPetById({ id }) as TData, ...options });

‎tests/__snapshots__/generate.test.ts.snap

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
exports[`generate > common.ts 1`] = `
44
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
55
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
6-
import { DefaultService, DefaultData } from "../requests";
7-
import { Pet, NewPet, Error } from "../requests/models";
6+
import { DefaultService } from "../requests/services.gen";
7+
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
88
export type DefaultServiceFindPetsDefaultResponse = Awaited<ReturnType<typeof DefaultService.findPets>>;
99
export type DefaultServiceFindPetsQueryResult<TData = DefaultServiceFindPetsDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
1010
export const useDefaultServiceFindPetsKey = "DefaultServiceFindPets";
@@ -31,28 +31,71 @@ exports[`generate > queries.ts 1`] = `
3131
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
3232
import * as Common from "./common";
3333
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
34-
import { DefaultService, DefaultData } from "../requests";
35-
import { Pet, NewPet, Error } from "../requests/models";
34+
import { DefaultService } from "../requests/services.gen";
35+
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
36+
/**
37+
* Returns all pets from the system that the user has access to
38+
* Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.
39+
*
40+
* Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.
41+
*
42+
* @param data The data for the request.
43+
* @param data.tags tags to filter by
44+
* @param data.limit maximum number of results to return
45+
* @returns Pet pet response
46+
* @returns Error unexpected error
47+
* @throws ApiError
48+
*/
3649
export const useDefaultServiceFindPets = <TData = Common.DefaultServiceFindPetsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ limit, tags }: {
3750
limit?: number;
3851
tags?: string[];
3952
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetsKey, ...(queryKey ?? [{ limit, tags }])], queryFn: () => DefaultService.findPets({ limit, tags }) as TData, ...options });
4053
/**
41-
* @deprecated
42-
*/
54+
* @deprecated
55+
* This path is not fully defined.
56+
* @returns unknown unexpected error
57+
* @throws ApiError
58+
*/
4359
export const useDefaultServiceGetNotDefined = <TData = Common.DefaultServiceGetNotDefinedDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>(queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceGetNotDefinedKey, ...(queryKey ?? [])], queryFn: () => DefaultService.getNotDefined() as TData, ...options });
60+
/**
61+
* Returns a user based on a single ID, if the user does not have access to the pet
62+
* @param data The data for the request.
63+
* @param data.id ID of pet to fetch
64+
* @returns Pet pet response
65+
* @returns Error unexpected error
66+
* @throws ApiError
67+
*/
4468
export const useDefaultServiceFindPetById = <TData = Common.DefaultServiceFindPetByIdDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ id }: {
4569
id: number;
4670
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])], queryFn: () => DefaultService.findPetById({ id }) as TData, ...options });
71+
/**
72+
* Creates a new pet in the store. Duplicates are allowed
73+
* @param data The data for the request.
74+
* @param data.requestBody Pet to add to the store
75+
* @returns Pet pet response
76+
* @returns Error unexpected error
77+
* @throws ApiError
78+
*/
4779
export const useDefaultServiceAddPet = <TData = Common.DefaultServiceAddPetMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, {
4880
requestBody: NewPet;
4981
}, TContext>, "mutationFn">) => useMutation<TData, TError, {
5082
requestBody: NewPet;
5183
}, TContext>({ mutationFn: ({ requestBody }) => DefaultService.addPet({ requestBody }) as unknown as Promise<TData>, ...options });
5284
/**
53-
* @deprecated
54-
*/
85+
* @deprecated
86+
* This path is not defined at all.
87+
* @returns unknown unexpected error
88+
* @throws ApiError
89+
*/
5590
export const useDefaultServicePostNotDefined = <TData = Common.DefaultServicePostNotDefinedMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, void, TContext>, "mutationFn">) => useMutation<TData, TError, void, TContext>({ mutationFn: () => DefaultService.postNotDefined() as unknown as Promise<TData>, ...options });
91+
/**
92+
* deletes a single pet based on the ID supplied
93+
* @param data The data for the request.
94+
* @param data.id ID of pet to delete
95+
* @returns Error unexpected error
96+
* @returns void pet deleted
97+
* @throws ApiError
98+
*/
5699
export const useDefaultServiceDeletePet = <TData = Common.DefaultServiceDeletePetMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, {
57100
id: number;
58101
}, TContext>, "mutationFn">) => useMutation<TData, TError, {
@@ -65,16 +108,40 @@ exports[`generate > suspense.ts 1`] = `
65108
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
66109
import * as Common from "./common";
67110
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
68-
import { DefaultService, DefaultData } from "../requests";
69-
import { Pet, NewPet, Error } from "../requests/models";
111+
import { DefaultService } from "../requests/services.gen";
112+
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
113+
/**
114+
* Returns all pets from the system that the user has access to
115+
* Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.
116+
*
117+
* Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.
118+
*
119+
* @param data The data for the request.
120+
* @param data.tags tags to filter by
121+
* @param data.limit maximum number of results to return
122+
* @returns Pet pet response
123+
* @returns Error unexpected error
124+
* @throws ApiError
125+
*/
70126
export const useDefaultServiceFindPetsSuspense = <TData = Common.DefaultServiceFindPetsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ limit, tags }: {
71127
limit?: number;
72128
tags?: string[];
73129
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetsKey, ...(queryKey ?? [{ limit, tags }])], queryFn: () => DefaultService.findPets({ limit, tags }) as TData, ...options });
74130
/**
75-
* @deprecated
76-
*/
131+
* @deprecated
132+
* This path is not fully defined.
133+
* @returns unknown unexpected error
134+
* @throws ApiError
135+
*/
77136
export const useDefaultServiceGetNotDefinedSuspense = <TData = Common.DefaultServiceGetNotDefinedDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>(queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceGetNotDefinedKey, ...(queryKey ?? [])], queryFn: () => DefaultService.getNotDefined() as TData, ...options });
137+
/**
138+
* Returns a user based on a single ID, if the user does not have access to the pet
139+
* @param data The data for the request.
140+
* @param data.id ID of pet to fetch
141+
* @returns Pet pet response
142+
* @returns Error unexpected error
143+
* @throws ApiError
144+
*/
78145
export const useDefaultServiceFindPetByIdSuspense = <TData = Common.DefaultServiceFindPetByIdDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ id }: {
79146
id: number;
80147
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])], queryFn: () => DefaultService.findPetById({ id }) as TData, ...options });

‎tests/common.test.ts

Lines changed: 211 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
import { describe, expect, test } from "vitest";
2-
import { BuildCommonTypeName, capitalizeFirstLetter, lowercaseFirstLetter, safeParseNumber } from "../src/common.mts";
1+
import { describe, expect, test, vi } from "vitest";
2+
import {
3+
BuildCommonTypeName,
4+
capitalizeFirstLetter,
5+
lowercaseFirstLetter,
6+
safeParseNumber,
7+
getShortType,
8+
formatOptions,
9+
getClassNameFromClassNode,
10+
getClassesFromService,
11+
getNameFromMethod,
12+
} from "../src/common.mts";
13+
import { LimitedUserConfig } from "../src/cli.mts";
314

415
describe("common", () => {
516
test("safeParseNumber", () => {
@@ -26,9 +37,204 @@ describe("common", () => {
2637
expect(lowercased).toBe("testTestTest");
2738
});
2839

29-
test('buildCommonTypeName', () => {
30-
const name = 'Name';
40+
test("buildCommonTypeName", () => {
41+
const name = "Name";
3142
const result = BuildCommonTypeName(name);
32-
expect(result.escapedText).toBe('Common.Name');
43+
expect(result.escapedText).toBe("Common.Name");
44+
});
45+
46+
describe("getShortType", () => {
47+
test("linux", () => {
48+
const type = 'import("/my/absolute/path").MyType';
49+
const result = getShortType(type);
50+
expect(result).toBe("MyType");
51+
});
52+
53+
test("no import", () => {
54+
const type = "MyType";
55+
const result = getShortType(type);
56+
expect(result).toBe("MyType");
57+
});
58+
59+
test("windows", () => {
60+
const type = 'import("D:/types.gen").MyType';
61+
const result = getShortType(type);
62+
expect(result).toBe("MyType");
63+
});
64+
65+
test("number", () => {
66+
const type = 'import("C:/Projekt/test_3.0/path").MyType';
67+
const result = getShortType(type);
68+
expect(result).toBe("MyType");
69+
});
70+
71+
test("underscore", () => {
72+
const type = 'import("C:/Projekt/test_one/path").MyType';
73+
const result = getShortType(type);
74+
expect(result).toBe("MyType");
75+
});
76+
77+
test("dash", () => {
78+
const type = 'import("C:/Projekt/test-one/path").MyType';
79+
const result = getShortType(type);
80+
expect(result).toBe("MyType");
81+
});
82+
});
83+
84+
test("formatOptions - converts string boolean to boolean (false)", () => {
85+
const options: LimitedUserConfig = {
86+
input: "input",
87+
output: "output",
88+
debug: "false" as any,
89+
};
90+
const formatted = formatOptions(options);
91+
92+
expect(formatted.debug).toStrictEqual(false);
93+
});
94+
95+
test("formatOptions - converts string boolean to boolean (true)", () => {
96+
const options: LimitedUserConfig = {
97+
input: "input",
98+
output: "output",
99+
debug: "true" as any,
100+
};
101+
const formatted = formatOptions(options);
102+
103+
expect(formatted.debug).toStrictEqual(true);
104+
});
105+
106+
test("formatOptions - converts string boolean to boolean (undefined)", () => {
107+
const options: LimitedUserConfig = {
108+
input: "input",
109+
output: "output",
110+
};
111+
const formatted = formatOptions(options);
112+
113+
expect(formatted.debug).toStrictEqual(undefined);
114+
});
115+
116+
test("formatOptions - converts string number to number", () => {
117+
const options: LimitedUserConfig = {
118+
input: "input",
119+
output: "output",
120+
debug: "123" as any,
121+
};
122+
const formatted = formatOptions(options);
123+
124+
expect(formatted.debug).toStrictEqual(123);
125+
});
126+
127+
test("formatOptions - leaves other values unchanged", () => {
128+
const options: LimitedUserConfig = {
129+
input: "input",
130+
output: "output",
131+
debug: "123" as any,
132+
lint: "eslint",
133+
};
134+
const formatted = formatOptions(options);
135+
136+
expect(formatted.lint).toStrictEqual("eslint");
137+
});
138+
139+
test("formatOptions - converts string number to number", () => {
140+
const options: LimitedUserConfig = {
141+
input: "input",
142+
output: "output",
143+
debug: Number.NaN as any,
144+
};
145+
const formatted = formatOptions(options);
146+
147+
expect(formatted.debug).toStrictEqual(Number.NaN);
148+
});
149+
150+
test("formatOptions - handle boolean true", () => {
151+
const options: LimitedUserConfig = {
152+
input: "input",
153+
output: "output",
154+
debug: true,
155+
};
156+
const formatted = formatOptions(options);
157+
158+
expect(formatted.debug).toStrictEqual(true);
159+
});
160+
161+
test("formatOptions - handle boolean false", () => {
162+
const options: LimitedUserConfig = {
163+
input: "input",
164+
output: "output",
165+
debug: false,
166+
};
167+
const formatted = formatOptions(options);
168+
169+
expect(formatted.debug).toStrictEqual(false);
170+
});
171+
172+
test("getClassNameFromClassNode - get's name", () => {
173+
const klass = {
174+
getName: () => "Test",
175+
} as any;
176+
const result = getClassNameFromClassNode(klass);
177+
expect(result).toBe("Test");
178+
});
179+
180+
test("getClassNameFromClassNode - no name", () => {
181+
const klass = {
182+
getName: () => undefined,
183+
} as any;
184+
expect(() => getClassNameFromClassNode(klass)).toThrowError(
185+
"Class name not found"
186+
);
187+
});
188+
189+
test("getClassesFromService - returns class name and class", () => {
190+
const klass = {
191+
getName: vi.fn(() => "Test"),
192+
};
193+
const node = {
194+
getClasses: vi.fn(() => [klass]),
195+
} as any;
196+
const result = getClassesFromService(node);
197+
expect(result).toStrictEqual([
198+
{
199+
className: "Test",
200+
klass,
201+
},
202+
]);
203+
});
204+
205+
test("getClassesFromService - no classes", () => {
206+
const node = {
207+
getClasses: vi.fn(() => []),
208+
} as any;
209+
expect(() => getClassesFromService(node)).toThrowError("No classes found");
210+
});
211+
212+
test("getClassesFromService - no name", () => {
213+
const klass = {
214+
getName: vi.fn(() => undefined),
215+
};
216+
const node = {
217+
getClasses: vi.fn(() => [klass]),
218+
} as any;
219+
expect(() => getClassesFromService(node)).toThrowError(
220+
"Class name not found"
221+
);
222+
});
223+
224+
test("getNameFromMethod - get method name", () => {
225+
const method = {
226+
getName: vi.fn(() => "test"),
227+
} as any;
228+
const result = getNameFromMethod(method);
229+
expect(result).toBe("test");
230+
});
231+
232+
test("getNameFromMethod - no method name", () => {
233+
const method = {
234+
getName: vi.fn(() => undefined),
235+
} as any;
236+
expect(() => getNameFromMethod(method)).toThrowError(
237+
"Method name not found"
238+
);
33239
});
34240
});

‎tests/createImports.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ describe(fileName, () => {
2222
const moduleNames = imports.map((i) => i.moduleSpecifier.text);
2323
expect(moduleNames).toStrictEqual([
2424
"@tanstack/react-query",
25-
"../requests",
26-
"../requests/models",
25+
"../requests/services.gen",
26+
"../requests/types.gen",
2727
]);
2828
await cleanOutputs(fileName);
2929
});
@@ -42,7 +42,11 @@ describe(fileName, () => {
4242

4343
// @ts-ignore
4444
const moduleNames = imports.map((i) => i.moduleSpecifier.text);
45-
expect(moduleNames).toStrictEqual(["@tanstack/react-query", "../requests"]);
45+
expect(moduleNames).toStrictEqual([
46+
"@tanstack/react-query",
47+
"../requests/services.gen",
48+
"../requests/types.gen",
49+
]);
4650
await cleanOutputs(fileName);
4751
});
4852
});

‎tests/generate.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { existsSync, readFileSync } from "node:fs";
22
import path from "node:path";
3-
import type { UserConfig } from "@hey-api/openapi-ts";
43
import { afterAll, beforeAll, describe, expect, test } from "vitest";
54
import { generate } from "../src/generate.mjs";
6-
import { rmdir } from "node:fs/promises";
5+
import { rm } from "node:fs/promises";
6+
import { LimitedUserConfig } from "../src/cli.mts";
77

88
const readOutput = (fileName: string) => {
99
return readFileSync(
@@ -14,18 +14,19 @@ const readOutput = (fileName: string) => {
1414

1515
describe("generate", () => {
1616
beforeAll(async () => {
17-
const options: UserConfig = {
17+
const options: LimitedUserConfig = {
1818
input: path.join(__dirname, "inputs", "petstore.yaml"),
1919
output: path.join("tests", "outputs"),
20-
lint: true,
21-
format: false,
20+
lint: "eslint",
2221
};
2322
await generate(options, "1.0.0");
2423
});
2524

2625
afterAll(async () => {
2726
if (existsSync(path.join(__dirname, "outputs"))) {
28-
await rmdir(path.join(__dirname, "outputs"), { recursive: true });
27+
await rm(path.join(__dirname, "outputs"), {
28+
recursive: true,
29+
});
2930
}
3031
});
3132

‎tests/print.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, test, expect, vi, beforeEach } from "vitest";
2+
import { print } from "../src/print.mjs";
3+
import * as common from "../src/common.mjs";
4+
import { mkdir, writeFile } from "fs/promises";
5+
6+
vi.mock("fs/promises", () => {
7+
return {
8+
mkdir: vi.fn(() => Promise.resolve()),
9+
writeFile: vi.fn(() => Promise.resolve()),
10+
};
11+
});
12+
13+
describe("print", () => {
14+
beforeEach(() => {
15+
vi.resetAllMocks();
16+
});
17+
test("print - doesn't create folders if folders exist", async () => {
18+
const exists = vi.spyOn(common, "exists");
19+
exists.mockImplementation(() => Promise.resolve(true));
20+
const result = await print(
21+
[
22+
{
23+
name: "test.ts",
24+
content: 'console.log("test")',
25+
},
26+
],
27+
{
28+
output: "dist",
29+
}
30+
);
31+
expect(exists).toBeCalledTimes(2);
32+
expect(result).toBeUndefined();
33+
expect(mkdir).toBeCalledTimes(0);
34+
expect(writeFile).toBeCalledTimes(1);
35+
});
36+
37+
test("print - creates folders if folders don't exist", async () => {
38+
const exists = vi.spyOn(common, "exists");
39+
exists.mockImplementation(() => Promise.resolve(false));
40+
const result = await print(
41+
[
42+
{
43+
name: "test.ts",
44+
content: 'console.log("test")',
45+
},
46+
],
47+
{
48+
output: "dist",
49+
}
50+
);
51+
expect(exists).toBeCalledTimes(2);
52+
expect(result).toBeUndefined();
53+
expect(mkdir).toBeCalledTimes(2);
54+
expect(writeFile).toBeCalledTimes(1);
55+
});
56+
});

‎tests/utils.test.ts

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import ts from "typescript";
2+
import { describe, expect, test } from "vitest";
3+
import { addJSDocToNode } from "../src/util.mts";
4+
import { Project } from "ts-morph";
5+
6+
describe("utils", () => {
7+
test("addJSDocToNode - deprecated", () => {
8+
const project = new Project({
9+
skipAddingFilesFromTsConfig: true,
10+
});
11+
12+
// create class
13+
const node = ts.factory.createClassDeclaration(
14+
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
15+
"TestClass",
16+
undefined,
17+
undefined,
18+
[]
19+
);
20+
// create file source
21+
const tsFile = ts.createSourceFile(
22+
"test.ts",
23+
"",
24+
ts.ScriptTarget.Latest,
25+
false,
26+
ts.ScriptKind.TS
27+
);
28+
29+
// create source file
30+
const tsSource = ts.factory.createSourceFile(
31+
[node],
32+
ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
33+
ts.NodeFlags.None
34+
);
35+
36+
// print source file
37+
const fileString = ts
38+
.createPrinter()
39+
.printNode(ts.EmitHint.Unspecified, tsSource, tsFile);
40+
41+
// create ts-morph source file
42+
const sourceFile = project.createSourceFile("test.ts", fileString);
43+
44+
if (!sourceFile) {
45+
throw new Error("Source file not found");
46+
}
47+
48+
// add jsdoc to node
49+
const jsDoc = `/**
50+
* @deprecated
51+
* This is a test
52+
* This is a test 2
53+
*/`;
54+
55+
const deprecated = true;
56+
57+
// find class node
58+
const foundNode = sourceFile.getClasses()[0]!;
59+
60+
// add jsdoc to node
61+
const nodeWithJSDoc = addJSDocToNode(foundNode.compilerNode, jsDoc);
62+
63+
// print node
64+
const nodetext = ts
65+
.createPrinter()
66+
.printNode(ts.EmitHint.Unspecified, nodeWithJSDoc, tsFile);
67+
68+
expect(nodetext).toMatchInlineSnapshot(`
69+
"/**
70+
* @deprecated
71+
* This is a test
72+
* This is a test 2
73+
*/
74+
export class TestClass {
75+
}"
76+
`);
77+
});
78+
79+
test("addJSDocToNode - not deprecated", () => {
80+
const project = new Project({
81+
skipAddingFilesFromTsConfig: true,
82+
});
83+
84+
// create class
85+
const node = ts.factory.createClassDeclaration(
86+
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
87+
"TestClass",
88+
undefined,
89+
undefined,
90+
[]
91+
);
92+
// create file source
93+
const tsFile = ts.createSourceFile(
94+
"test.ts",
95+
"",
96+
ts.ScriptTarget.Latest,
97+
false,
98+
ts.ScriptKind.TS
99+
);
100+
101+
// create source file
102+
const tsSource = ts.factory.createSourceFile(
103+
[node],
104+
ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
105+
ts.NodeFlags.None
106+
);
107+
108+
// print source file
109+
const fileString = ts
110+
.createPrinter()
111+
.printNode(ts.EmitHint.Unspecified, tsSource, tsFile);
112+
113+
// create ts-morph source file
114+
const sourceFile = project.createSourceFile("test.ts", fileString);
115+
116+
if (!sourceFile) {
117+
throw new Error("Source file not found");
118+
}
119+
120+
// add jsdoc to node
121+
const jsDoc = `/**
122+
* This is a test
123+
* This is a test 2
124+
*/`;
125+
126+
// find class node
127+
const foundNode = sourceFile.getClasses()[0]!;
128+
129+
// add jsdoc to node
130+
const nodeWithJSDoc = addJSDocToNode(foundNode.compilerNode, jsDoc);
131+
132+
// print node
133+
const nodetext = ts
134+
.createPrinter()
135+
.printNode(ts.EmitHint.Unspecified, nodeWithJSDoc, tsFile);
136+
137+
expect(nodetext).toMatchInlineSnapshot(`
138+
"/**
139+
* This is a test
140+
* This is a test 2
141+
*/
142+
export class TestClass {
143+
}"
144+
`);
145+
});
146+
147+
test("addJSDocToNode - does not add comment if no jsdoc", () => {
148+
const project = new Project({
149+
skipAddingFilesFromTsConfig: true,
150+
});
151+
152+
// create class
153+
const node = ts.factory.createClassDeclaration(
154+
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
155+
"TestClass",
156+
undefined,
157+
undefined,
158+
[]
159+
);
160+
// create file source
161+
const tsFile = ts.createSourceFile(
162+
"test.ts",
163+
"",
164+
ts.ScriptTarget.Latest,
165+
false,
166+
ts.ScriptKind.TS
167+
);
168+
169+
// create source file
170+
const tsSource = ts.factory.createSourceFile(
171+
[node],
172+
ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
173+
ts.NodeFlags.None
174+
);
175+
176+
// print source file
177+
const fileString = ts
178+
.createPrinter()
179+
.printNode(ts.EmitHint.Unspecified, tsSource, tsFile);
180+
181+
// create ts-morph source file
182+
const sourceFile = project.createSourceFile("test.ts", fileString);
183+
184+
if (!sourceFile) {
185+
throw new Error("Source file not found");
186+
}
187+
188+
// add jsdoc to node
189+
const jsDoc = undefined;
190+
191+
// find class node
192+
const foundNode = sourceFile.getClasses()[0]!;
193+
194+
// add jsdoc to node
195+
const nodeWithJSDoc = addJSDocToNode(foundNode.compilerNode, jsDoc);
196+
197+
// print node
198+
const nodetext = ts
199+
.createPrinter()
200+
.printNode(ts.EmitHint.Unspecified, nodeWithJSDoc, tsFile);
201+
202+
expect(nodetext).toMatchInlineSnapshot(`
203+
"export class TestClass {
204+
}"
205+
`);
206+
});
207+
208+
test("addJSDocToNode - adds comment if no jsdoc and deprecated true", () => {
209+
const project = new Project({
210+
skipAddingFilesFromTsConfig: true,
211+
});
212+
213+
// create class
214+
const node = ts.factory.createClassDeclaration(
215+
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
216+
"TestClass",
217+
undefined,
218+
undefined,
219+
[]
220+
);
221+
// create file source
222+
const tsFile = ts.createSourceFile(
223+
"test.ts",
224+
"",
225+
ts.ScriptTarget.Latest,
226+
false,
227+
ts.ScriptKind.TS
228+
);
229+
230+
// create source file
231+
const tsSource = ts.factory.createSourceFile(
232+
[node],
233+
ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
234+
ts.NodeFlags.None
235+
);
236+
237+
// print source file
238+
const fileString = ts
239+
.createPrinter()
240+
.printNode(ts.EmitHint.Unspecified, tsSource, tsFile);
241+
242+
// create ts-morph source file
243+
const sourceFile = project.createSourceFile("test.ts", fileString);
244+
245+
if (!sourceFile) {
246+
throw new Error("Source file not found");
247+
}
248+
249+
// add jsdoc to node
250+
const jsDoc = undefined;
251+
252+
// find class node
253+
const foundNode = sourceFile.getClasses()[0]!;
254+
255+
// add jsdoc to node
256+
const nodeWithJSDoc = addJSDocToNode(foundNode.compilerNode, jsDoc);
257+
258+
// print node
259+
const nodetext = ts
260+
.createPrinter()
261+
.printNode(ts.EmitHint.Unspecified, nodeWithJSDoc, tsFile);
262+
263+
expect(nodetext).toMatchInlineSnapshot(`
264+
"export class TestClass {
265+
}"
266+
`);
267+
});
268+
});

‎vitest.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import { defineConfig } from "vitest/config";
33
export default defineConfig({
44
test: {
55
coverage: {
6-
reporter: ['text', 'json-summary', 'json', 'html'],
7-
exclude: ['src/cli.mts', 'examples/**'],
6+
reporter: ["text", "json-summary", "json", "html"],
7+
exclude: ["src/cli.mts", "examples/**", "tests/**"],
88
reportOnFailure: true,
99
thresholds: {
1010
lines: 95,
1111
functions: 95,
1212
statements: 95,
1313
branches: 85,
14-
}
15-
}
14+
},
15+
},
1616
},
1717
});

0 commit comments

Comments
 (0)
Please sign in to comment.