Skip to content

Commit 0976308

Browse files
Support module aliases with --baseQuery (#29)
* smart import path * Add test to assert TSCONFIG_FILE_NOT_FOUND is thrown Co-authored-by: Matt Sutkowski <[email protected]>
1 parent 2f13bb2 commit 0976308

14 files changed

+414
-110
lines changed

package-lock.json

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@babel/preset-typescript": "^7.12.7",
2323
"@rtk-incubator/rtk-query": "^0.2.0",
2424
"@types/commander": "^2.12.2",
25+
"@types/glob-to-regexp": "^0.4.0",
2526
"@types/jest": "^26.0.20",
2627
"@types/lodash": "^4.14.165",
2728
"@types/node": "^14.14.12",
@@ -41,6 +42,7 @@
4142
"dependencies": {
4243
"@apidevtools/swagger-parser": "^10.0.2",
4344
"commander": "^6.2.0",
45+
"glob-to-regexp": "^0.4.1",
4446
"oazapfts": "npm:@phryneas/experimental-oazapfts@^0.1.1-0",
4547
"swagger2openapi": "^7.0.4",
4648
"ts-morph": "^9.1.0",

src/bin/cli.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import program from 'commander';
88
const meta = require('../../package.json');
99
import { generateApi } from '../generate';
1010
import { GenerationOptions } from '../types';
11-
import { isValidUrl, prettify } from '../utils';
11+
import { isValidUrl, MESSAGES, prettify } from '../utils';
12+
import { getCompilerOptions } from '../utils/getTsConfig';
1213

1314
program
1415
.version(meta.version)
@@ -20,7 +21,8 @@ program
2021
.option('--responseSuffix <name>', 'pass response suffix')
2122
.option('--baseUrl <url>', 'pass baseUrl')
2223
.option('-h, --hooks', 'generate React Hooks')
23-
.option('--file <filename>', 'output file name (ex: generated.api.ts)')
24+
.option('-c, --config <path>', 'pass tsconfig path for resolve path alias')
25+
.option('-f, --file <filename>', 'output file name (ex: generated.api.ts)')
2426
.parse(process.argv);
2527

2628
if (program.args.length === 0) {
@@ -39,26 +41,46 @@ if (program.args.length === 0) {
3941
'baseUrl',
4042
'hooks',
4143
'file',
44+
'config',
4245
] as const;
4346

44-
const generateApiOptions = options.reduce(
45-
(s, key) =>
46-
program[key]
47-
? {
48-
...s,
49-
[key]: program[key],
50-
}
51-
: s,
52-
{} as GenerationOptions
53-
);
47+
const outputFile = program['file'];
48+
let tsConfigFilePath = program['config'];
49+
50+
if (tsConfigFilePath) {
51+
tsConfigFilePath = path.resolve(tsConfigFilePath);
52+
if (!fs.existsSync(tsConfigFilePath)) {
53+
throw Error(MESSAGES.TSCONFIG_FILE_NOT_FOUND);
54+
}
55+
}
56+
57+
const compilerOptions = getCompilerOptions(tsConfigFilePath);
58+
59+
const generateApiOptions = {
60+
...options.reduce(
61+
(s, key) =>
62+
program[key]
63+
? {
64+
...s,
65+
[key]: program[key],
66+
}
67+
: s,
68+
{} as GenerationOptions
69+
),
70+
outputFile,
71+
compilerOptions,
72+
};
5473
generateApi(schemaAbsPath, generateApiOptions)
5574
.then(async (sourceCode) => {
5675
const outputFile = program['file'];
5776
if (outputFile) {
58-
fs.writeFileSync(`${process.cwd()}/${outputFile}`, await prettify(outputFile, sourceCode));
77+
fs.writeFileSync(path.resolve(process.cwd(), outputFile), await prettify(outputFile, sourceCode));
5978
} else {
6079
console.log(await prettify(null, sourceCode));
6180
}
6281
})
63-
.catch((err) => console.error(err));
82+
.catch((err) => {
83+
console.error(err);
84+
process.exit(1);
85+
});
6486
}

src/generate.ts

Lines changed: 25 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as ts from 'typescript';
2-
import * as fs from 'fs';
3-
import chalk from 'chalk';
2+
import * as path from 'path';
43
import { camelCase } from 'lodash';
54
import ApiGenerator, {
65
getOperationName,
@@ -12,15 +11,16 @@ import { createQuestionToken, keywordType } from 'oazapfts/lib/codegen/tscodegen
1211
import { OpenAPIV3 } from 'openapi-types';
1312
import { generateReactHooks } from './generators/react-hooks';
1413
import { GenerationOptions, OperationDefinition } from './types';
15-
import { capitalize, getOperationDefinitions, getV3Doc, isQuery, MESSAGES, stripFileExtension } from './utils';
14+
import { capitalize, getOperationDefinitions, getV3Doc, isQuery, MESSAGES } from './utils';
1615
import { removeUndefined } from './utils/removeUndefined';
1716
import {
1817
generateCreateApiCall,
19-
generateImportNode,
2018
generateEndpointDefinition,
2119
generateStringLiteralArray,
2220
ObjectPropertyDefinitions,
2321
} from './codegen';
22+
import { generateSmartImportNode } from './generators/smart-import-node';
23+
import { generateImportNode } from './generators/import-node';
2424

2525
const { factory } = ts;
2626

@@ -30,7 +30,7 @@ function defaultIsDataResponse(code: string) {
3030
}
3131

3232
let customBaseQueryNode: ts.ImportDeclaration | undefined;
33-
let baseQueryFn: string, filePath: string;
33+
let moduleName: string;
3434

3535
export async function generateApi(
3636
spec: string,
@@ -42,7 +42,9 @@ export async function generateApi(
4242
responseSuffix = 'ApiResponse',
4343
baseUrl,
4444
hooks,
45+
outputFile,
4546
isDataResponse = defaultIsDataResponse,
47+
compilerOptions,
4648
}: GenerationOptions
4749
) {
4850
const v3Doc = await getV3Doc(spec);
@@ -80,76 +82,37 @@ export async function generateApi(
8082
* 3. If there is a not a seperator, file presence + default export existence is verified.
8183
*/
8284

83-
function fnExportExists(path: string, fnName: string) {
84-
const fileName = `${process.cwd()}/${path}`;
85-
86-
const sourceFile = ts.createSourceFile(
87-
fileName,
88-
fs.readFileSync(fileName).toString(),
89-
ts.ScriptTarget.ES2015,
90-
/*setParentNodes */ true
91-
);
92-
93-
let found = false;
94-
95-
ts.forEachChild(sourceFile, (node) => {
96-
const text = node.getText();
97-
if (ts.isExportAssignment(node)) {
98-
if (text.includes(fnName)) {
99-
found = true;
100-
}
101-
} else if (ts.isVariableStatement(node) || ts.isFunctionDeclaration(node) || ts.isExportDeclaration(node)) {
102-
if (text.includes(fnName) && text.includes('export')) {
103-
found = true;
104-
}
105-
} else if (ts.isExportAssignment(node)) {
106-
if (text.includes(`export ${fnName}`)) {
107-
found = true;
108-
}
109-
}
110-
});
111-
112-
return found;
85+
if (outputFile) {
86+
outputFile = path.resolve(process.cwd(), outputFile);
11387
}
11488

11589
// If a baseQuery was specified as an arg, we try to parse and resolve it. If not, fallback to `fetchBaseQuery` or throw when appropriate.
90+
91+
let targetName = 'default';
11692
if (baseQuery !== 'fetchBaseQuery') {
11793
if (baseQuery.includes(':')) {
11894
// User specified a named function
119-
[filePath, baseQueryFn] = baseQuery.split(':');
95+
[moduleName, baseQuery] = baseQuery.split(':');
12096

121-
if (!baseQueryFn || !fnExportExists(filePath, baseQueryFn)) {
97+
if (!baseQuery) {
12298
throw new Error(MESSAGES.NAMED_EXPORT_MISSING);
123-
} else if (!fs.existsSync(filePath)) {
124-
throw new Error(MESSAGES.FILE_NOT_FOUND);
12599
}
126-
127-
customBaseQueryNode = generateImportNode(stripFileExtension(filePath), {
128-
[baseQueryFn]: baseQueryFn,
129-
});
100+
targetName = baseQuery;
130101
} else {
131-
filePath = baseQuery;
132-
baseQueryFn = 'fetchBaseQuery';
133-
134-
if (!fs.existsSync(filePath)) {
135-
throw new Error(MESSAGES.FILE_NOT_FOUND);
136-
} else if (!fnExportExists(filePath, 'default')) {
137-
throw new Error(MESSAGES.DEFAULT_EXPORT_MISSING);
138-
}
139-
140-
console.warn(chalk`
141-
{yellow.bold A custom baseQuery was specified without a named function. We're going to import the default as {underline customBaseQuery}}
142-
`);
143-
144-
baseQueryFn = 'customBaseQuery';
145-
146-
customBaseQueryNode = generateImportNode(stripFileExtension(filePath), {
147-
default: baseQueryFn,
148-
});
102+
moduleName = baseQuery;
103+
baseQuery = 'customBaseQuery';
149104
}
105+
106+
customBaseQueryNode = generateSmartImportNode({
107+
moduleName,
108+
containingFile: outputFile,
109+
targetName,
110+
targetAlias: baseQuery,
111+
compilerOptions,
112+
});
150113
}
151114

152-
const baseQueryCall = factory.createCallExpression(factory.createIdentifier(baseQueryFn || baseQuery), undefined, [
115+
const baseQueryCall = factory.createCallExpression(factory.createIdentifier(baseQuery), undefined, [
153116
factory.createObjectLiteralExpression(
154117
[
155118
factory.createPropertyAssignment(
@@ -197,28 +160,6 @@ export async function generateApi(
197160

198161
return sourceCode;
199162

200-
function generateImportNode(pkg: string, namedImports: Record<string, string>, defaultImportName?: string) {
201-
return factory.createImportDeclaration(
202-
undefined,
203-
undefined,
204-
factory.createImportClause(
205-
false,
206-
defaultImportName !== undefined ? factory.createIdentifier(defaultImportName) : undefined,
207-
factory.createNamedImports(
208-
Object.entries(namedImports)
209-
.filter((args) => args[1])
210-
.map(([propertyName, name]) =>
211-
factory.createImportSpecifier(
212-
name === propertyName ? undefined : factory.createIdentifier(propertyName),
213-
factory.createIdentifier(name as string)
214-
)
215-
)
216-
)
217-
),
218-
factory.createStringLiteral(pkg)
219-
);
220-
}
221-
222163
function generateEntityTypes(_: { operationDefinitions: OperationDefinition[]; v3Doc: OpenAPIV3.Document }) {
223164
return generateStringLiteralArray([]); // TODO
224165
}

src/generators/import-node.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as ts from 'typescript';
2+
3+
const { factory } = ts;
4+
5+
export function generateImportNode(pkg: string, namedImports: Record<string, string>, defaultImportName?: string) {
6+
return factory.createImportDeclaration(
7+
undefined,
8+
undefined,
9+
factory.createImportClause(
10+
false,
11+
defaultImportName !== undefined ? factory.createIdentifier(defaultImportName) : undefined,
12+
factory.createNamedImports(
13+
Object.entries(namedImports)
14+
.filter((args) => args[1])
15+
.map(([propertyName, name]) =>
16+
factory.createImportSpecifier(
17+
name === propertyName ? undefined : factory.createIdentifier(propertyName),
18+
factory.createIdentifier(name as string)
19+
)
20+
)
21+
)
22+
),
23+
factory.createStringLiteral(pkg)
24+
);
25+
}

src/generators/smart-import-node.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as ts from 'typescript';
2+
import * as fs from 'fs';
3+
4+
import { MESSAGES, stripFileExtension } from '../utils';
5+
import { isModuleInsidePathAlias } from '../utils/isModuleInsidePathAlias';
6+
import { generateImportNode } from './import-node';
7+
import { fnExportExists } from '../utils/fnExportExists';
8+
import { resolveImportPath } from '../utils/resolveImportPath';
9+
10+
type SmartGenerateImportNode = {
11+
moduleName: string;
12+
containingFile?: string;
13+
targetName: string;
14+
targetAlias: string;
15+
compilerOptions?: ts.CompilerOptions;
16+
};
17+
export const generateSmartImportNode = ({
18+
moduleName,
19+
containingFile,
20+
targetName,
21+
targetAlias,
22+
compilerOptions,
23+
}: SmartGenerateImportNode): ts.ImportDeclaration => {
24+
if (fs.existsSync(moduleName)) {
25+
if (fnExportExists(moduleName, targetName)) {
26+
return generateImportNode(
27+
stripFileExtension(containingFile ? resolveImportPath(moduleName, containingFile) : moduleName),
28+
{
29+
[targetName]: targetAlias,
30+
}
31+
);
32+
}
33+
34+
if (targetName === 'default') {
35+
throw new Error(MESSAGES.DEFAULT_EXPORT_MISSING);
36+
}
37+
throw new Error(MESSAGES.NAMED_EXPORT_MISSING);
38+
}
39+
40+
if (!compilerOptions) {
41+
throw new Error(MESSAGES.FILE_NOT_FOUND);
42+
}
43+
44+
// maybe moduleName is path alias
45+
if (isModuleInsidePathAlias(compilerOptions, moduleName)) {
46+
return generateImportNode(stripFileExtension(moduleName), {
47+
[targetName]: targetAlias,
48+
});
49+
}
50+
51+
throw new Error(MESSAGES.FILE_NOT_FOUND);
52+
};

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as ts from 'typescript';
12
import { OpenAPIV3 } from 'openapi-types';
23

34
export type OperationDefinition = {
@@ -17,5 +18,7 @@ export type GenerationOptions = {
1718
responseSuffix?: string;
1819
baseUrl?: string;
1920
hooks?: boolean;
21+
outputFile?: string;
22+
compilerOptions?: ts.CompilerOptions;
2023
isDataResponse?(code: string, response: OpenAPIV3.ResponseObject, allResponses: OpenAPIV3.ResponsesObject): boolean;
2124
};

0 commit comments

Comments
 (0)