Skip to content

Commit 75c4280

Browse files
committed
feat: JSONをinterfaceに変換する
1 parent a5444b5 commit 75c4280

16 files changed

+311
-21
lines changed

README.md

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
1-
# @himenon/node-lib-template
1+
# @himenon/typescript-codegen
2+
3+
```bash
4+
AST --> TypeScript
5+
```
6+
7+
## References
8+
9+
TypeScript
10+
11+
- https://ts-ast-viewer.com
12+
- http://akito0107.hatenablog.com/entry/2018/12/23/020323
13+
14+
JavaScript
15+
16+
- https://astexplorer.net/
17+
18+
Flow
19+
20+
- https://talks.leko.jp/transform-flow-to-typescript-using-ast/#0
21+
22+
Babel
23+
24+
- https://blog.ikeryo1182.com/typescript-transpiler/
225

326
## Usage
427

28+
## Development
29+
530
| scripts | description |
631
| :------------------------ | :------------------------------------------ |
732
| `build` | typescript build and create proxy directory |
@@ -26,4 +51,4 @@
2651

2752
## LICENCE
2853

29-
[@himenon-node-lib-template](https://github.com/Himenon/node-lib-template)・MIT
54+
[@himenon-typescript-codegen](https://github.com/Himenon/typescript-codegen)・MIT

jest.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ module.exports = {
6060
// A set of global variables that need to be available in all test environments
6161
globals: {
6262
"ts-jest": {
63-
tsConfig: "tsconfig.json",
63+
tsconfig: "tsconfig.json",
6464
diagnostics: false,
6565
},
6666
},

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@
6161
"kind-of": "6.0.3",
6262
"node-fetch": "2.6.1"
6363
},
64+
"dependencies": {
65+
"@babel/generator": "^7.12.11",
66+
"@babel/parser": "^7.12.11",
67+
"@types/json-schema": "^7.0.6"
68+
},
6469
"devDependencies": {
6570
"@commitlint/cli": "11.0.0",
6671
"@commitlint/config-conventional": "11.0.0",
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { generateInterface } from "../generateInterface";
2+
3+
describe("Interface Generator", () => {
4+
it("Change Interface Name", () => {
5+
const name = "MyTestInterface";
6+
const expectResult = `interface ${name} {\n}\n`;
7+
const result = generateInterface({ name, schemas: {} });
8+
expect(result).toBe(expectResult);
9+
});
10+
});

src/__tests__/index.test.ts

-10
This file was deleted.
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { JSONSchema4 } from "json-schema";
2+
import { generateInterface } from "../generateInterface";
3+
4+
const schemas: JSONSchema4 = {
5+
type: "object",
6+
properties: {
7+
name: {
8+
type: "string",
9+
required: false,
10+
},
11+
age: {
12+
type: "number",
13+
},
14+
body: {
15+
type: "object",
16+
properties: {
17+
child: {
18+
type: "string",
19+
},
20+
},
21+
},
22+
},
23+
};
24+
25+
describe("Interface Generator", () => {
26+
it("Change Interface Name", () => {
27+
const name = "MyTestInterface";
28+
const expectResult = `interface ${name} {\n}\n`;
29+
const result = generateInterface({ name, schemas });
30+
console.log(JSON.stringify(result));
31+
expect(result).toBe(expectResult);
32+
});
33+
});

src/convertAstToTypeScriptCode.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as ts from "typescript";
2+
3+
export const convertAstToTypeScriptCode = (sourceFile: ts.SourceFile): string => {
4+
const printer = ts.createPrinter(); // AST -> TypeScriptに変換
5+
return printer.printFile(sourceFile);
6+
};

src/generateInterface.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { transform } from "./transform";
2+
import * as Traverse from "./traverse";
3+
import { JSONSchema4 } from "json-schema";
4+
5+
interface Params {
6+
name: string;
7+
schemas: JSONSchema4;
8+
}
9+
10+
const code = `
11+
interface DummyInterface {
12+
13+
}
14+
`;
15+
16+
export const generateInterface = (params: Params): string => {
17+
const result = transform(code, [
18+
Traverse.InterfaceName.traverse({ name: params.name }),
19+
Traverse.InterfaceAppendMembers.traverse({
20+
schemas: params.schemas,
21+
}),
22+
]);
23+
console.log({ result });
24+
return result;
25+
};

src/index.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1-
export const hello = (name: string): string => {
2-
const params = {
3-
hoge: 1,
4-
fuga: 2,
5-
};
6-
return `Hello ${name} ${JSON.stringify(params)}`;
1+
import * as ts from "typescript";
2+
3+
const code = `
4+
interface Hoge {
5+
name: string;
6+
}
7+
8+
const hoge: Hoge = {
9+
name: "hoge",
710
};
11+
`;
12+
13+
const source = ts.createSourceFile("", code, ts.ScriptTarget.ESNext);
14+
15+
const result = ts.transform(source, []);
16+
result.dispose();
17+
18+
const printer = ts.createPrinter();
819

9-
console.log(hello("Your name"));
20+
console.log(printer.printFile(result.transformed[0] as ts.SourceFile));

src/transform.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as ts from "typescript";
2+
import { convertAstToTypeScriptCode } from "./convertAstToTypeScriptCode";
3+
import * as Types from "./types";
4+
5+
export const transform = (code: string, transformers: Types.TransformerFactory<ts.SourceFile>[] = []) => {
6+
const source = ts.createSourceFile("", code, ts.ScriptTarget.ESNext);
7+
const result = ts.transform(source, transformers);
8+
result.dispose();
9+
if (result.transformed.length > 1) {
10+
console.error(result.transformed);
11+
throw new Error("1個以上あるよ");
12+
}
13+
return convertAstToTypeScriptCode(result.transformed[0]);
14+
};

src/traverse/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * as InterfaceName from "./interfaceName";
2+
export * as InterfaceAppendMembers from "./interfaceAppendMembers";
+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import * as ts from "typescript";
2+
import { JSONSchema4 } from "json-schema";
3+
4+
export interface Params {
5+
schemas: JSONSchema4;
6+
}
7+
8+
const createStringType = ({ factory }: ts.TransformationContext): ts.TypeNode => {
9+
return factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
10+
};
11+
12+
const createAnyType = ({ factory }: ts.TransformationContext): ts.TypeNode => {
13+
return factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
14+
};
15+
16+
const createNumberType = ({ factory }: ts.TransformationContext): ts.TypeNode => {
17+
return factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
18+
};
19+
20+
const createNullType = ({ factory }: ts.TransformationContext): ts.TypeNode => {
21+
return factory.createLiteralTypeNode(factory.createNull());
22+
};
23+
24+
const createBooleanType = ({ factory }: ts.TransformationContext): ts.TypeNode => {
25+
return factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
26+
};
27+
28+
const createArrayType = ({ factory }: ts.TransformationContext, typeNode: ts.TypeNode): ts.TypeNode => {
29+
return factory.createArrayTypeNode(typeNode);
30+
};
31+
32+
const createObjectMemberType = ({ factory }: ts.TransformationContext, members: readonly ts.TypeElement[] | undefined) => {
33+
return factory.createTypeLiteralNode(members);
34+
};
35+
36+
/**
37+
* Don't use root schema root
38+
*/
39+
const createTypeNode = (context: ts.TransformationContext, schema: JSONSchema4): ts.TypeNode => {
40+
if (schema.type === "object") {
41+
if (!schema.properties) {
42+
return createObjectMemberType(context, []);
43+
}
44+
const members = Object.entries(schema.properties).map(([key, childSchema]) => {
45+
return context.factory.createPropertySignature(
46+
undefined,
47+
context.factory.createIdentifier(key),
48+
undefined,
49+
createTypeNode(context, childSchema),
50+
);
51+
});
52+
return createObjectMemberType(context, members);
53+
} else if (schema.type === "string") {
54+
return createStringType(context);
55+
} else if (schema.type === "array") {
56+
if (!schema.items) {
57+
return createArrayType(context, createAnyType(context));
58+
}
59+
if (!Array.isArray(schema.items)) {
60+
return createArrayType(context, createTypeNode(context, schema.items));
61+
}
62+
throw new Error("TODO");
63+
} else if (schema.type === "any") {
64+
return createAnyType(context);
65+
} else if (schema.type === "boolean") {
66+
return createBooleanType(context);
67+
} else if (schema.type === "number") {
68+
return createNumberType(context);
69+
} else if (schema.type === "integer") {
70+
return createNumberType(context);
71+
} else if (schema.type === "null") {
72+
return createNullType(context);
73+
}
74+
return createAnyType(context);
75+
};
76+
77+
const createInterfaceFromRootObjectSchema = (context: ts.TransformationContext, schema: JSONSchema4): ts.PropertySignature[] => {
78+
if (schema.type !== "object" || !schema.properties) {
79+
return [];
80+
}
81+
return Object.entries(schema.properties).map(([key, childSchema]) => {
82+
return context.factory.createPropertySignature(
83+
undefined,
84+
context.factory.createIdentifier(key),
85+
undefined,
86+
createTypeNode(context, childSchema),
87+
);
88+
});
89+
};
90+
91+
/**
92+
* interface名をrenameする
93+
*/
94+
export const traverse = (params: Params) => <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
95+
const visit = (node: ts.Node): ts.Node => {
96+
node = ts.visitEachChild(node, visit, context);
97+
if (!ts.isInterfaceDeclaration(node)) {
98+
return node;
99+
}
100+
return context.factory.createInterfaceDeclaration(
101+
undefined,
102+
undefined,
103+
node.name,
104+
undefined,
105+
undefined,
106+
createInterfaceFromRootObjectSchema(context, params.schemas),
107+
);
108+
};
109+
return ts.visitNode(rootNode, visit);
110+
};

src/traverse/interfaceName.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as ts from "typescript";
2+
3+
export interface Params {
4+
name: string;
5+
}
6+
7+
/**
8+
* interface名をrenameする
9+
*/
10+
export const traverse = (params: Params) => <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
11+
const visit = (node: ts.Node): ts.Node => {
12+
node = ts.visitEachChild(node, visit, context);
13+
if (ts.isSourceFile(node) && node.statements.length !== 1) {
14+
throw new Error("only one interface traverse");
15+
}
16+
if (!ts.isInterfaceDeclaration(node)) {
17+
return node;
18+
}
19+
const name = context.factory.createIdentifier(params.name);
20+
return context.factory.updateInterfaceDeclaration(node, undefined, undefined, name, undefined, undefined, []);
21+
};
22+
return ts.visitNode(rootNode, visit);
23+
};

src/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import * as ts from "typescript";
2+
3+
export type TransformerFactory<T extends ts.Node> = ts.TransformerFactory<T>;

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"skipLibCheck": true,
1313
"sourceMap": false,
1414
"target": "esnext",
15-
"lib": ["dom", "es2020"],
15+
"lib": ["dom", "es2019"],
1616
"rootDir": ".",
1717
"outDir": "lib",
1818
"strict": true

0 commit comments

Comments
 (0)