Skip to content

Commit 1163bd9

Browse files
authored
Merge pull request #766 from BitGo/DX-389-node-modules-generator
feat(openapi-generator): support types imported from `node_modules`
2 parents 2b22c3a + b97074e commit 1163bd9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+1188
-50
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
coverage/
22
dist/
33
flake.lock
4+
packages/openapi-generator/test/

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"homepage": "https://github.com/BitGo/api-ts#readme",
1616
"workspaces": [
17-
"packages/**"
17+
"packages/*"
1818
],
1919
"packageManager": "[email protected]",
2020
"scripts": {

packages/openapi-generator/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"clean": "rm -rf -- dist",
1818
"format": "prettier --check .",
1919
"format:fix": "prettier --write .",
20-
"test": "c8 --all --src src node --require @swc-node/register --test test/*.test.ts"
20+
"test": "c8 --all --src src node --require @swc-node/register --test test/*.test.ts",
21+
"test:target": "c8 --all --src src node --require @swc-node/register"
2122
},
2223
"dependencies": {
2324
"@swc/core": "1.5.7",

packages/openapi-generator/src/cli.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ const app = command({
159159
});
160160
let schema: Schema | undefined;
161161
while (((schema = queue.pop()), schema !== undefined)) {
162-
const refs = getRefs(schema);
162+
const refs = getRefs(schema, project.right.getTypes());
163163
for (const ref of refs) {
164164
if (components[ref.name] !== undefined) {
165165
continue;
@@ -169,6 +169,7 @@ const app = command({
169169
console.error(`Could not find '${ref.name}' from '${ref.location}'`);
170170
process.exit(1);
171171
}
172+
172173
const initE = findSymbolInitializer(project.right, sourceFile, ref.name);
173174
if (E.isLeft(initE)) {
174175
console.error(
@@ -177,6 +178,7 @@ const app = command({
177178
process.exit(1);
178179
}
179180
const [newSourceFile, init] = initE.right;
181+
180182
const codecE = parseCodecInitializer(project.right, newSourceFile, init);
181183
if (E.isLeft(codecE)) {
182184
console.error(

packages/openapi-generator/src/codec.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,6 @@ function codecIdentifier(
9696
}
9797
const name = id.property.value;
9898

99-
if (!objectImportSym.from.startsWith('.')) {
100-
return E.left(
101-
`Unimplemented named member reference '${objectImportSym.localName}.${name}' from '${objectImportSym.from}'`,
102-
);
103-
}
104-
10599
const newInitE = findSymbolInitializer(project, source, [
106100
objectImportSym.localName,
107101
name,
@@ -360,9 +354,17 @@ export function parseCodecInitializer(
360354
if (schema.type !== 'ref') {
361355
return E.right(schema);
362356
} else {
363-
const refSource = project.get(schema.location);
357+
let refSource = project.get(schema.location);
358+
364359
if (refSource === undefined) {
365-
return E.left(`Cannot find '${schema.name}' from '${schema.location}'`);
360+
// schema.location might be a package name -> need to resolve the path from the project types
361+
const path = project.getTypes()[schema.name];
362+
if (path === undefined)
363+
return E.left(`Cannot find module '${schema.location}' in the project`);
364+
refSource = project.get(path);
365+
if (refSource === undefined) {
366+
return E.left(`Cannot find '${schema.name}' from '${schema.location}'`);
367+
}
366368
}
367369
const initE = findSymbolInitializer(project, refSource, schema.name);
368370
if (E.isLeft(initE)) {

packages/openapi-generator/src/project.ts

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ export class Project {
1313
private readonly knownImports: Record<string, Record<string, KnownCodec>>;
1414

1515
private files: Record<string, SourceFile>;
16+
private types: Record<string, string>;
1617

1718
constructor(files: Record<string, SourceFile> = {}, knownImports = KNOWN_IMPORTS) {
1819
this.files = files;
1920
this.knownImports = knownImports;
21+
this.types = {};
2022
}
2123

2224
add(path: string, sourceFile: SourceFile): void {
@@ -42,19 +44,33 @@ export class Project {
4244
const src = await this.readFile(path);
4345
const sourceFile = await parseSource(path, src);
4446

47+
if (sourceFile === undefined) continue;
48+
49+
// map types to their file path
50+
for (const exp of sourceFile.symbols.exports) {
51+
this.types[exp.exportedName] = path;
52+
}
53+
4554
this.add(path, sourceFile);
4655

4756
for (const sym of Object.values(sourceFile.symbols.imports)) {
4857
if (!sym.from.startsWith('.')) {
49-
continue;
50-
}
51-
52-
const filePath = p.dirname(path);
53-
const absImportPathE = this.resolve(filePath, sym.from);
54-
if (E.isLeft(absImportPathE)) {
55-
return absImportPathE;
56-
} else if (!this.has(absImportPathE.right)) {
57-
queue.push(absImportPathE.right);
58+
// If we are not resolving a relative path, we need to resolve the entry point
59+
const baseDir = p.dirname(sourceFile.path);
60+
let entryPoint = this.resolveEntryPoint(baseDir, sym.from);
61+
if (E.isLeft(entryPoint)) {
62+
continue;
63+
} else if (!this.has(entryPoint.right)) {
64+
queue.push(entryPoint.right);
65+
}
66+
} else {
67+
const filePath = p.dirname(path);
68+
const absImportPathE = this.resolve(filePath, sym.from);
69+
if (E.isLeft(absImportPathE)) {
70+
return absImportPathE;
71+
} else if (!this.has(absImportPathE.right)) {
72+
queue.push(absImportPathE.right);
73+
}
5874
}
5975
}
6076
for (const starExport of sourceFile.symbols.exportStarFiles) {
@@ -75,24 +91,60 @@ export class Project {
7591
return await readFile(filename, 'utf8');
7692
}
7793

94+
resolveEntryPoint(basedir: string, library: string): E.Either<string, string> {
95+
try {
96+
const packageJson = resolve.sync(`${library}/package.json`, {
97+
basedir,
98+
extensions: ['.json'],
99+
});
100+
const packageInfo = JSON.parse(fs.readFileSync(packageJson, 'utf8'));
101+
102+
let typesEntryPoint = '';
103+
104+
if (packageInfo['types']) {
105+
typesEntryPoint = packageInfo['types'];
106+
}
107+
108+
if (packageInfo['typings']) {
109+
typesEntryPoint = packageInfo['typings'];
110+
}
111+
112+
if (!typesEntryPoint) {
113+
return E.left(`Could not find types entry point for ${library}`);
114+
}
115+
116+
const entryPoint = resolve.sync(`${library}/${typesEntryPoint}`, {
117+
basedir,
118+
extensions: ['.ts', '.js'],
119+
});
120+
return E.right(entryPoint);
121+
} catch (err) {
122+
return E.left(`Could not resolve entry point for ${library}: ${err}`);
123+
}
124+
}
125+
78126
resolve(basedir: string, path: string): E.Either<string, string> {
79127
try {
80128
const result = resolve.sync(path, {
81129
basedir,
82130
extensions: ['.ts', '.js'],
83131
});
84132
return E.right(result);
85-
} catch (e: any) {
86-
if (typeof e === 'object' && e.hasOwnProperty('message')) {
133+
} catch (e: unknown) {
134+
if (e instanceof Error && e.message) {
87135
return E.left(e.message);
88-
} else {
89-
return E.left(JSON.stringify(e));
90136
}
137+
138+
return E.left(JSON.stringify(e));
91139
}
92140
}
93141

94142
resolveKnownImport(path: string, name: string): KnownCodec | undefined {
95143
const baseKey = path.startsWith('.') ? '.' : path;
96144
return this.knownImports[baseKey]?.[name];
97145
}
146+
147+
getTypes() {
148+
return this.types;
149+
}
98150
}

packages/openapi-generator/src/ref.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,34 @@
11
import type { Schema, Reference } from './ir';
2+
import fs from 'fs';
23

3-
export function getRefs(schema: Schema): Reference[] {
4+
export function getRefs(schema: Schema, typeMap: Record<string, string>): Reference[] {
45
if (schema.type === 'ref') {
6+
if (!fs.existsSync(schema.location)) {
7+
// The location is a node module - we need to populate the location here
8+
const newPath = typeMap[schema.name];
9+
if (!newPath) {
10+
return [];
11+
}
12+
13+
return [{ ...schema, location: newPath }];
14+
}
515
return [schema];
616
} else if (schema.type === 'array') {
7-
return getRefs(schema.items);
17+
return getRefs(schema.items, typeMap);
818
} else if (
919
schema.type === 'intersection' ||
1020
schema.type === 'union' ||
1121
schema.type === 'tuple'
1222
) {
1323
return schema.schemas.reduce<Reference[]>((acc, member) => {
14-
return [...acc, ...getRefs(member)];
24+
return [...acc, ...getRefs(member, typeMap)];
1525
}, []);
1626
} else if (schema.type === 'object') {
1727
return Object.values(schema.properties).reduce<Reference[]>((acc, member) => {
18-
return [...acc, ...getRefs(member)];
28+
return [...acc, ...getRefs(member, typeMap)];
1929
}, []);
2030
} else if (schema.type === 'record') {
21-
return getRefs(schema.codomain);
31+
return getRefs(schema.codomain, typeMap);
2232
} else {
2333
return [];
2434
}

packages/openapi-generator/src/resolveInit.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ function resolveImportPath(
1313
sourceFile: SourceFile,
1414
path: string,
1515
): E.Either<string, SourceFile> {
16-
const importPathE = project.resolve(dirname(sourceFile.path), path);
16+
let importPathE;
17+
if (path.startsWith('.')) {
18+
importPathE = project.resolve(dirname(sourceFile.path), path);
19+
} else {
20+
importPathE = project.resolveEntryPoint(dirname(sourceFile.path), path);
21+
}
22+
1723
if (E.isLeft(importPathE)) {
1824
return importPathE;
1925
}

packages/openapi-generator/src/sourceFile.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ export type SourceFile = {
1414
// increasing counter for this, so we also need to track it globally here
1515
let lastSpanEnd = -1;
1616

17-
export async function parseSource(path: string, src: string): Promise<SourceFile> {
17+
export async function parseSource(
18+
path: string,
19+
src: string,
20+
): Promise<SourceFile | undefined> {
1821
try {
1922
const module = await swc.parse(src, {
2023
syntax: 'typescript',
@@ -34,8 +37,8 @@ export async function parseSource(path: string, src: string): Promise<SourceFile
3437
symbols,
3538
span: module.span,
3639
};
37-
} catch (e) {
38-
console.error(e);
39-
process.exit(1);
40+
} catch (e: unknown) {
41+
console.error(`Error parsing source file: ${path}`, e);
42+
return undefined;
4043
}
4144
}

0 commit comments

Comments
 (0)