Skip to content

Commit 2fbf33d

Browse files
msutkowskikahirokunnphryneas
authored
Support custom base query from CLI and add tests (#13)
* Add chalk, support named and default exports from a baseQuery CLI arg * Add jest, msw and CLI tests * Add test for --file option * Simple string snapshot serializer Co-authored-by: kahirokunn <[email protected]> Co-authored-by: Lenz Weber <[email protected]>
1 parent 9782a07 commit 2fbf33d

20 files changed

+1521
-95
lines changed

.vscode/settings.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"typescript.tsdk": "node_modules/typescript/lib",
3+
"jest.pathToJest": "npm test --",
4+
"jest.enableInlineErrorMessages": true,
5+
"jest.autoEnable": false,
6+
"editor.formatOnSave": true
7+
}

README.md

+13-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,18 @@ This is a utility library meant to be used with [RTK Query](https://rtk-query-do
1717

1818
### Usage
1919

20+
By default, running the CLI will only log the output to the terminal. You can either pipe this output to a new file, or you can specify an output file via CLI args.
21+
22+
#### Piping to a file (including react hooks generation)
23+
24+
```bash
25+
npx @rtk-incubator/rtk-query-codegen-openapi --hooks https://petstore3.swagger.io/api/v3/openapi.json > petstore-api.generated.ts
26+
```
27+
28+
#### Specifying an output file (including react hooks generation)
29+
2030
```bash
21-
curl -o petstore.json https://petstore3.swagger.io/api/v3/openapi.json
22-
npx @rtk-incubator/rtk-query-codegen-openapi petstore.json > petstore-api.generated.ts
31+
npx @rtk-incubator/rtk-query-codegen-openapi --file generated.api.ts --hooks https://petstore3.swagger.io/api/v3/openapi.json
2332
```
2433

2534
### CLI Options
@@ -30,6 +39,8 @@ npx @rtk-incubator/rtk-query-codegen-openapi petstore.json > petstore-api.genera
3039
- `--argSuffix <name>` - change the suffix of the args (default: `ApiArg`)
3140
- `--responseSuffix <name>` - change the suffix of the args (default: `ApiResponse`)
3241
- `--baseUrl <url>` - set the `baseUrl`
42+
- `--hooks` - include React Hooks in the output (ex: `export const { useGetModelQuery, useUpdateModelMutation } = api`)
43+
- `--file <filename>` - specify a filename to output to (ex: `./generated.api.ts`)
3344

3445
### Documentation
3546

babel.config.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// babel.config.js
2+
module.exports = {
3+
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
4+
};

jest.config.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const { resolve } = require('path');
2+
3+
const tsConfigPath = resolve('./test/tsconfig');
4+
5+
/** @typedef {import('ts-jest/dist/types')} */
6+
/** @type {import('@jest/types').Config.InitialOptions} */
7+
const config = {
8+
rootDir: './test',
9+
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
10+
globals: {
11+
'ts-jest': {
12+
tsconfig: tsConfigPath,
13+
},
14+
},
15+
};
16+
17+
module.exports = config;

package-lock.json

+13-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+13-1
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,29 @@
1212
"scripts": {
1313
"build": "tsc",
1414
"prepare": "npm run build && chmod +x ./lib/bin/cli.js",
15-
"format": "prettier --write \"src/**/*.ts\""
15+
"format": "prettier --write \"src/**/*.ts\"",
16+
"test:update": "lib/bin/cli.js test/fixtures/petstore.json --file test/fixtures/generated.ts -h",
17+
"test": "jest"
1618
},
1719
"devDependencies": {
20+
"@babel/core": "^7.12.10",
21+
"@babel/preset-env": "^7.12.11",
22+
"@babel/preset-typescript": "^7.12.7",
1823
"@rtk-incubator/rtk-query": "^0.2.0",
1924
"@types/commander": "^2.12.2",
25+
"@types/jest": "^26.0.20",
2026
"@types/lodash": "^4.14.165",
2127
"@types/node": "^14.14.12",
2228
"@types/prettier": "^2.1.6",
29+
"babel-jest": "^26.6.3",
30+
"chalk": "^4.1.0",
31+
"del": "^6.0.0",
2332
"husky": "^4.3.6",
33+
"jest": "^26.6.3",
34+
"msw": "^0.25.0",
2435
"prettier": "^2.2.1",
2536
"pretty-quick": "^3.1.0",
37+
"ts-jest": "^26.4.4",
2638
"ts-node": "^9.1.0",
2739
"yalc": "^1.0.0-pre.47"
2840
},

src/bin/cli.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
#!/usr/bin/env node
2-
3-
import path = require('path');
4-
import program = require('commander');
1+
import * as path from 'path';
52
import * as fs from 'fs';
3+
import program from 'commander';
4+
import chalk from 'chalk';
65

76
// tslint:disable-next-line
87
const meta = require('../../package.json');
@@ -51,12 +50,14 @@ if (program.args.length === 0) {
5150
: s,
5251
{} as GenerationOptions
5352
);
54-
generateApi(schemaAbsPath, generateApiOptions).then(async (sourceCode) => {
55-
const outputFile = program['file'];
56-
if (outputFile) {
57-
fs.writeFileSync(`${process.cwd()}/${outputFile}`, await prettify(outputFile, sourceCode));
58-
} else {
59-
console.log(await prettify(null, sourceCode));
60-
}
61-
});
53+
generateApi(schemaAbsPath, generateApiOptions)
54+
.then(async (sourceCode) => {
55+
const outputFile = program['file'];
56+
if (outputFile) {
57+
fs.writeFileSync(`${process.cwd()}/${outputFile}`, await prettify(outputFile, sourceCode));
58+
} else {
59+
console.log(await prettify(null, sourceCode));
60+
}
61+
})
62+
.catch((err) => console.error(err));
6263
}

src/generate.ts

+110-22
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import * as ts from 'typescript';
2+
import * as fs from 'fs';
3+
import chalk from 'chalk';
24
import { camelCase } from 'lodash';
35
import ApiGenerator, {
46
getOperationName,
5-
supportDeepObjects,
67
getReferenceName,
78
isReference,
9+
supportDeepObjects,
810
} from 'oazapfts/lib/codegen/generate';
9-
import { OpenAPIV3 } from 'openapi-types';
1011
import { createQuestionToken, keywordType } from 'oazapfts/lib/codegen/tscodegen';
12+
import { OpenAPIV3 } from 'openapi-types';
1113
import { generateReactHooks } from './generators/react-hooks';
12-
import { capitalize, getOperationDefinitions, getV3Doc, isQuery } from './utils';
1314
import { GenerationOptions, OperationDefinition } from './types';
15+
import { capitalize, getOperationDefinitions, getV3Doc, isQuery, MESSAGES } from './utils';
1416

1517
const { factory } = ts;
1618

@@ -19,6 +21,9 @@ function defaultIsDataResponse(code: string) {
1921
return !Number.isNaN(parsedCode) && parsedCode >= 200 && parsedCode < 300;
2022
}
2123

24+
let customBaseQueryNode: ts.ImportDeclaration | undefined;
25+
let baseQueryFn: string, filePath: string;
26+
2227
export async function generateApi(
2328
spec: string,
2429
{
@@ -60,14 +65,91 @@ export async function generateApi(
6065
return declaration;
6166
}
6267

63-
return printer.printNode(
68+
/**
69+
* --baseQuery handling
70+
* 1. If baseQuery is specified, we confirm that the file exists
71+
* 2. If there is a seperator in the path, file presence + named function existence is verified.
72+
* 3. If there is a not a seperator, file presence + default export existence is verified.
73+
*/
74+
75+
function fnExportExists(path: string, fnName: string) {
76+
const fileName = `${process.cwd()}/${path}`;
77+
78+
const sourceFile = ts.createSourceFile(
79+
fileName,
80+
fs.readFileSync(fileName).toString(),
81+
ts.ScriptTarget.ES2015,
82+
/*setParentNodes */ true
83+
);
84+
85+
let found = false;
86+
87+
ts.forEachChild(sourceFile, (node) => {
88+
const text = node.getText();
89+
if (ts.isExportAssignment(node)) {
90+
if (text.includes(fnName)) {
91+
found = true;
92+
}
93+
} else if (ts.isVariableStatement(node) || ts.isFunctionDeclaration(node) || ts.isExportDeclaration(node)) {
94+
if (text.includes(fnName) && text.includes('export')) {
95+
found = true;
96+
}
97+
} else if (ts.isExportAssignment(node)) {
98+
if (text.includes(`export ${fnName}`)) {
99+
found = true;
100+
}
101+
}
102+
});
103+
104+
return found;
105+
}
106+
107+
// If a baseQuery was specified as an arg, we try to parse and resolve it. If not, fallback to `fetchBaseQuery` or throw when appropriate.
108+
if (baseQuery !== 'fetchBaseQuery') {
109+
if (baseQuery.includes(':')) {
110+
// User specified a named function
111+
[filePath, baseQueryFn] = baseQuery.split(':');
112+
113+
if (!baseQueryFn || !fnExportExists(filePath, baseQueryFn)) {
114+
throw new Error(MESSAGES.NAMED_EXPORT_MISSING);
115+
} else if (!fs.existsSync(filePath)) {
116+
throw new Error(MESSAGES.FILE_NOT_FOUND);
117+
}
118+
119+
customBaseQueryNode = generateImportNode(filePath, {
120+
[baseQueryFn]: baseQueryFn,
121+
});
122+
} else {
123+
filePath = baseQuery;
124+
baseQueryFn = 'fetchBaseQuery';
125+
126+
if (!fs.existsSync(filePath)) {
127+
throw new Error(MESSAGES.FILE_NOT_FOUND);
128+
} else if (!fnExportExists(filePath, 'default')) {
129+
throw new Error(MESSAGES.DEFAULT_EXPORT_MISSING);
130+
}
131+
132+
console.warn(chalk`
133+
{yellow.bold A custom baseQuery was specified without a named function. We're going to import the default as {underline customBaseQuery}}
134+
`);
135+
136+
baseQueryFn = 'customBaseQuery';
137+
138+
customBaseQueryNode = generateImportNode(filePath, {
139+
default: baseQueryFn,
140+
});
141+
}
142+
}
143+
144+
const sourceCode = printer.printNode(
64145
ts.EmitHint.Unspecified,
65146
factory.createSourceFile(
66147
[
67148
generateImportNode('@rtk-incubator/rtk-query', {
68149
createApi: 'createApi',
69-
fetchBaseQuery: 'fetchBaseQuery',
150+
...(baseQuery === 'fetchBaseQuery' ? { fetchBaseQuery: 'fetchBaseQuery' } : {}),
70151
}),
152+
...(customBaseQueryNode ? [customBaseQueryNode] : []),
71153
generateCreateApiCall(),
72154
...Object.values(interfaces),
73155
...apiGen['aliases'],
@@ -79,6 +161,8 @@ export async function generateApi(
79161
resultFile
80162
);
81163

164+
return sourceCode;
165+
82166
function generateImportNode(pkg: string, namedImports: Record<string, string>, defaultImportName?: string) {
83167
return factory.createImportDeclaration(
84168
undefined,
@@ -87,12 +171,14 @@ export async function generateApi(
87171
false,
88172
defaultImportName !== undefined ? factory.createIdentifier(defaultImportName) : undefined,
89173
factory.createNamedImports(
90-
Object.entries(namedImports).map(([propertyName, name]) =>
91-
factory.createImportSpecifier(
92-
name === propertyName ? undefined : factory.createIdentifier(propertyName),
93-
factory.createIdentifier(name)
174+
Object.entries(namedImports)
175+
.filter((args) => args[1])
176+
.map(([propertyName, name]) =>
177+
factory.createImportSpecifier(
178+
name === propertyName ? undefined : factory.createIdentifier(propertyName),
179+
factory.createIdentifier(name as string)
180+
)
94181
)
95-
)
96182
)
97183
),
98184
factory.createStringLiteral(pkg)
@@ -119,7 +205,7 @@ export async function generateApi(
119205
),
120206
factory.createPropertyAssignment(
121207
factory.createIdentifier('baseQuery'),
122-
factory.createCallExpression(factory.createIdentifier(baseQuery), undefined, [
208+
factory.createCallExpression(factory.createIdentifier(baseQueryFn || baseQuery), undefined, [
123209
factory.createObjectLiteralExpression(
124210
[
125211
factory.createPropertyAssignment(
@@ -355,17 +441,19 @@ export async function generateApi(
355441
return factory.createArrowFunction(
356442
undefined,
357443
undefined,
358-
[
359-
factory.createParameterDeclaration(
360-
undefined,
361-
undefined,
362-
undefined,
363-
rootObject,
364-
undefined,
365-
undefined,
366-
undefined
367-
),
368-
],
444+
Object.keys(queryArg).length
445+
? [
446+
factory.createParameterDeclaration(
447+
undefined,
448+
undefined,
449+
undefined,
450+
rootObject,
451+
undefined,
452+
undefined,
453+
undefined
454+
),
455+
]
456+
: [],
369457
undefined,
370458
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
371459
factory.createParenthesizedExpression(

src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from './getV3Doc';
44
export * from './isQuery';
55
export * from './isValidUrl';
66
export * from './prettier';
7+
export * from './messages';

src/utils/messages.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const MESSAGES = {
2+
NAMED_EXPORT_MISSING: `You specified a named export that does not exist or was empty.`,
3+
DEFAULT_EXPORT_MISSING: `Specified file exists, but no default export was found for the --baseQuery`,
4+
FILE_NOT_FOUND: `Unable to locate the specified file provided to --baseQuery`,
5+
};

0 commit comments

Comments
 (0)