Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(openapi-generator): support types imported from node_modules #766

Merged
merged 19 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
31eea6b
feat: add support for basic imported types from node modules
ad-world May 10, 2024
17d919a
refactor(openapi-generator): support node modules (excluding express)
anshchaturvedi May 15, 2024
58a3429
refactor: cleanup node_modules generator code
ad-world May 15, 2024
4fb5588
fix broken `ref.test.ts` tests
anshchaturvedi May 15, 2024
9637dcb
fix(openapi-generator): broken resolve.test.ts tests
ad-world May 16, 2024
66be823
fix(openapi-generator): fix more tests
anshchaturvedi May 16, 2024
fbcb5e4
chore: ignore and log `swc` errors and write test
anshchaturvedi May 16, 2024
940ed49
chore(openapi-generator): add tests for openapi node_modules generation
ad-world May 16, 2024
8d508b3
refactor(openapi-generator): use unknown instead if any in project.js…
ad-world May 17, 2024
e208234
refactor: merge test node_modules into one
anshchaturvedi May 17, 2024
96c911e
refactor: assert error message when file cannot be parsed
anshchaturvedi May 17, 2024
c2b02b4
fix: remove prettier from `package.json`
anshchaturvedi May 17, 2024
6aea62a
fix: exclude `sample-types` type package from build
anshchaturvedi May 17, 2024
3ffdd12
test: add e2e tests for api spec generation from imported types
ad-world May 17, 2024
eae3632
test: add comments to ensure intentional errors in test node_modules …
ad-world May 17, 2024
8dc4164
fix: add `union` type test to `externalModuleApiSpec.test.ts`
anshchaturvedi May 17, 2024
de2c6c6
test: add error to array instead of exiting test process
ad-world May 17, 2024
c02b6e2
feat(openapi-generator): make node_module resolving more generic by r…
ad-world May 21, 2024
b97074e
refactor(openapi-generator): remove unneeded files and package bumps
anshchaturvedi May 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
coverage/
dist/
flake.lock
packages/openapi-generator/test/
ad-world marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"homepage": "https://github.com/BitGo/api-ts#readme",
"workspaces": [
"packages/**"
"packages/*"
],
"packageManager": "[email protected]",
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion packages/openapi-generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"clean": "rm -rf -- dist",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"test": "c8 --all --src src node --require @swc-node/register --test test/*.test.ts"
"test": "c8 --all --src src node --require @swc-node/register --test test/*.test.ts",
"test:target": "c8 --all --src src node --require @swc-node/register"
anshchaturvedi marked this conversation as resolved.
Show resolved Hide resolved
},
"dependencies": {
"@swc/core": "1.5.7",
Expand Down
4 changes: 3 additions & 1 deletion packages/openapi-generator/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ const app = command({
});
let schema: Schema | undefined;
while (((schema = queue.pop()), schema !== undefined)) {
const refs = getRefs(schema);
const refs = getRefs(schema, project.right.getTypes());
for (const ref of refs) {
if (components[ref.name] !== undefined) {
continue;
Expand All @@ -169,6 +169,7 @@ const app = command({
console.error(`Could not find '${ref.name}' from '${ref.location}'`);
process.exit(1);
}

const initE = findSymbolInitializer(project.right, sourceFile, ref.name);
if (E.isLeft(initE)) {
console.error(
Expand All @@ -177,6 +178,7 @@ const app = command({
process.exit(1);
}
const [newSourceFile, init] = initE.right;

const codecE = parseCodecInitializer(project.right, newSourceFile, init);
if (E.isLeft(codecE)) {
console.error(
Expand Down
18 changes: 10 additions & 8 deletions packages/openapi-generator/src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,6 @@ function codecIdentifier(
}
const name = id.property.value;

if (!objectImportSym.from.startsWith('.')) {
return E.left(
`Unimplemented named member reference '${objectImportSym.localName}.${name}' from '${objectImportSym.from}'`,
);
}

const newInitE = findSymbolInitializer(project, source, [
objectImportSym.localName,
name,
Expand Down Expand Up @@ -360,9 +354,17 @@ export function parseCodecInitializer(
if (schema.type !== 'ref') {
return E.right(schema);
} else {
const refSource = project.get(schema.location);
let refSource = project.get(schema.location);

if (refSource === undefined) {
return E.left(`Cannot find '${schema.name}' from '${schema.location}'`);
// schema.location might be a package name -> need to resolve the path from the project types
const path = project.getTypes()[schema.name];
if (path === undefined)
return E.left(`Cannot find module '${schema.location}' in the project`);
refSource = project.get(path);
if (refSource === undefined) {
return E.left(`Cannot find '${schema.name}' from '${schema.location}'`);
}
}
const initE = findSymbolInitializer(project, refSource, schema.name);
if (E.isLeft(initE)) {
Expand Down
78 changes: 65 additions & 13 deletions packages/openapi-generator/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ export class Project {
private readonly knownImports: Record<string, Record<string, KnownCodec>>;

private files: Record<string, SourceFile>;
private types: Record<string, string>;

constructor(files: Record<string, SourceFile> = {}, knownImports = KNOWN_IMPORTS) {
this.files = files;
this.knownImports = knownImports;
this.types = {};
}

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

if (sourceFile === undefined) continue;

// map types to their file path
for (const exp of sourceFile.symbols.exports) {
this.types[exp.exportedName] = path;
}

this.add(path, sourceFile);

for (const sym of Object.values(sourceFile.symbols.imports)) {
if (!sym.from.startsWith('.')) {
continue;
}

const filePath = p.dirname(path);
const absImportPathE = this.resolve(filePath, sym.from);
if (E.isLeft(absImportPathE)) {
return absImportPathE;
} else if (!this.has(absImportPathE.right)) {
queue.push(absImportPathE.right);
// If we are not resolving a relative path, we need to resolve the entry point
const baseDir = p.dirname(sourceFile.path);
let entryPoint = this.resolveEntryPoint(baseDir, sym.from);
if (E.isLeft(entryPoint)) {
continue;
} else if (!this.has(entryPoint.right)) {
queue.push(entryPoint.right);
}
} else {
const filePath = p.dirname(path);
const absImportPathE = this.resolve(filePath, sym.from);
if (E.isLeft(absImportPathE)) {
return absImportPathE;
} else if (!this.has(absImportPathE.right)) {
queue.push(absImportPathE.right);
}
}
}
for (const starExport of sourceFile.symbols.exportStarFiles) {
Expand All @@ -75,24 +91,60 @@ export class Project {
return await readFile(filename, 'utf8');
}

resolveEntryPoint(basedir: string, library: string): E.Either<string, string> {
try {
const packageJson = resolve.sync(`${library}/package.json`, {
basedir,
extensions: ['.json'],
});
const packageInfo = JSON.parse(fs.readFileSync(packageJson, 'utf8'));

let typesEntryPoint = '';

if (packageInfo['types']) {
typesEntryPoint = packageInfo['types'];
}

if (packageInfo['typings']) {
typesEntryPoint = packageInfo['typings'];
}

if (!typesEntryPoint) {
return E.left(`Could not find types entry point for ${library}`);
}

const entryPoint = resolve.sync(`${library}/${typesEntryPoint}`, {
basedir,
extensions: ['.ts', '.js'],
});
return E.right(entryPoint);
} catch (err) {
return E.left(`Could not resolve entry point for ${library}: ${err}`);
}
}

resolve(basedir: string, path: string): E.Either<string, string> {
try {
const result = resolve.sync(path, {
basedir,
extensions: ['.ts', '.js'],
});
return E.right(result);
} catch (e: any) {
if (typeof e === 'object' && e.hasOwnProperty('message')) {
} catch (e: unknown) {
if (e instanceof Error && e.message) {
return E.left(e.message);
} else {
return E.left(JSON.stringify(e));
}

return E.left(JSON.stringify(e));
}
}

resolveKnownImport(path: string, name: string): KnownCodec | undefined {
const baseKey = path.startsWith('.') ? '.' : path;
return this.knownImports[baseKey]?.[name];
}

getTypes() {
return this.types;
}
}
20 changes: 15 additions & 5 deletions packages/openapi-generator/src/ref.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import type { Schema, Reference } from './ir';
import fs from 'fs';

export function getRefs(schema: Schema): Reference[] {
export function getRefs(schema: Schema, typeMap: Record<string, string>): Reference[] {
if (schema.type === 'ref') {
if (!fs.existsSync(schema.location)) {
// The location is a node module - we need to populate the location here
const newPath = typeMap[schema.name];
if (!newPath) {
return [];
}

return [{ ...schema, location: newPath }];
}
return [schema];
} else if (schema.type === 'array') {
return getRefs(schema.items);
return getRefs(schema.items, typeMap);
} else if (
schema.type === 'intersection' ||
schema.type === 'union' ||
schema.type === 'tuple'
) {
return schema.schemas.reduce<Reference[]>((acc, member) => {
return [...acc, ...getRefs(member)];
return [...acc, ...getRefs(member, typeMap)];
}, []);
} else if (schema.type === 'object') {
return Object.values(schema.properties).reduce<Reference[]>((acc, member) => {
return [...acc, ...getRefs(member)];
return [...acc, ...getRefs(member, typeMap)];
}, []);
} else if (schema.type === 'record') {
return getRefs(schema.codomain);
return getRefs(schema.codomain, typeMap);
} else {
return [];
}
Expand Down
8 changes: 7 additions & 1 deletion packages/openapi-generator/src/resolveInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ function resolveImportPath(
sourceFile: SourceFile,
path: string,
): E.Either<string, SourceFile> {
const importPathE = project.resolve(dirname(sourceFile.path), path);
let importPathE;
if (path.startsWith('.')) {
importPathE = project.resolve(dirname(sourceFile.path), path);
} else {
importPathE = project.resolveEntryPoint(dirname(sourceFile.path), path);
}

if (E.isLeft(importPathE)) {
return importPathE;
}
Expand Down
11 changes: 7 additions & 4 deletions packages/openapi-generator/src/sourceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export type SourceFile = {
// increasing counter for this, so we also need to track it globally here
let lastSpanEnd = -1;

export async function parseSource(path: string, src: string): Promise<SourceFile> {
export async function parseSource(
path: string,
src: string,
): Promise<SourceFile | undefined> {
try {
const module = await swc.parse(src, {
syntax: 'typescript',
Expand All @@ -34,8 +37,8 @@ export async function parseSource(path: string, src: string): Promise<SourceFile
symbols,
span: module.span,
};
} catch (e) {
console.error(e);
process.exit(1);
} catch (e: unknown) {
console.error(`Error parsing source file: ${path}`, e);
return undefined;
}
}
5 changes: 3 additions & 2 deletions packages/openapi-generator/test/apiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { NestedDirectoryJSON } from 'memfs';

import { TestProject } from './testProject';
import { parseApiSpec, parseApiSpecComment, type Route } from '../src';
import { MOCK_NODE_MODULES_DIR } from './externalModules';

async function testCase(
description: string,
Expand All @@ -14,7 +15,7 @@ async function testCase(
expectedErrors: string[] = [],
) {
test(description, async () => {
const project = new TestProject(files);
const project = new TestProject({ ...files, ...MOCK_NODE_MODULES_DIR });

await project.parseEntryPoint(entryPoint);
const sourceFile = project.get(entryPoint);
Expand Down Expand Up @@ -288,5 +289,5 @@ const MISSING_REFERENCE = {
};

testCase('missing reference', MISSING_REFERENCE, '/index.ts', {}, [
"Cannot find 'Foo' from 'foo'",
"Cannot find module 'foo' in the project",
]);
Loading