Skip to content

Commit

Permalink
Merge pull request #1513 from vega/next
Browse files Browse the repository at this point in the history
  • Loading branch information
domoritz committed Dec 22, 2022
2 parents 9babda4 + 0ac5df1 commit 3c07347
Show file tree
Hide file tree
Showing 42 changed files with 1,412 additions and 978 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,23 @@ This project is made possible by a [community of contributors](https://github.co

## CLI Usage

Run the schema generator with npx:

```bash
npx ts-json-schema-generator --path 'my/project/**/*.ts' --type 'My.Type.Name'
```

Or install the package and then run it

```bash
npm install --save ts-json-schema-generator
./node_modules/.bin/ts-json-schema-generator --path 'my/project/**/*.ts' --type 'My.Type.Name'
```

Note that different platforms (e.g. Windows) may use different path separators so you may have to adjust the command above.

Also note that you need to quote paths with `*` as otherwise the shell will expand the paths and therefore only pass the first path to the generator.

## Programmatic Usage

```js
Expand Down Expand Up @@ -252,7 +262,7 @@ fs.writeFile(output_path, schemaString, (err) => {
- `interface` types
- `enum` types
- `union`, `tuple`, `type[]` types
- `Date`, `RegExp` types
- `Date`, `RegExp`, `URL` types
- `string`, `boolean`, `number` types
- `"value"`, `123`, `true`, `false`, `null`, `undefined` literals
- type aliases
Expand Down
10 changes: 9 additions & 1 deletion factory/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { BooleanLiteralNodeParser } from "../src/NodeParser/BooleanLiteralNodePa
import { BooleanTypeNodeParser } from "../src/NodeParser/BooleanTypeNodeParser";
import { CallExpressionParser } from "../src/NodeParser/CallExpressionParser";
import { ConditionalTypeNodeParser } from "../src/NodeParser/ConditionalTypeNodeParser";
import { ConstructorNodeParser } from "../src/NodeParser/ConstructorNodeParser";
import { EnumNodeParser } from "../src/NodeParser/EnumNodeParser";
import { ExpressionWithTypeArgumentsNodeParser } from "../src/NodeParser/ExpressionWithTypeArgumentsNodeParser";
import { FunctionNodeParser } from "../src/NodeParser/FunctionNodeParser";
Expand Down Expand Up @@ -112,6 +113,7 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa
.addNodeParser(new BooleanLiteralNodeParser())
.addNodeParser(new NullLiteralNodeParser())
.addNodeParser(new FunctionNodeParser())
.addNodeParser(new ConstructorNodeParser())
.addNodeParser(new ObjectLiteralExpressionNodeParser(chainNodeParser))
.addNodeParser(new ArrayLiteralExpressionNodeParser(chainNodeParser))

Expand Down Expand Up @@ -157,7 +159,13 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa
.addNodeParser(
withCircular(
withExpose(
withJsDoc(new TypeLiteralNodeParser(withJsDoc(chainNodeParser), mergedConfig.additionalProperties))
withJsDoc(
new TypeLiteralNodeParser(
typeChecker,
withJsDoc(chainNodeParser),
mergedConfig.additionalProperties
)
)
)
)
)
Expand Down
2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export * from "./src/Type/BooleanType";
export * from "./src/Type/DefinitionType";
export * from "./src/Type/EnumType";
export * from "./src/Type/FunctionType";
export * from "./src/Type/HiddenType";
export * from "./src/Type/IntersectionType";
export * from "./src/Type/LiteralType";
export * from "./src/Type/NeverType";
Expand Down Expand Up @@ -82,6 +83,7 @@ export * from "./src/TypeFormatter/ArrayTypeFormatter";
export * from "./src/TypeFormatter/BooleanTypeFormatter";
export * from "./src/TypeFormatter/DefinitionTypeFormatter";
export * from "./src/TypeFormatter/EnumTypeFormatter";
export * from "./src/TypeFormatter/HiddenTypeFormatter";
export * from "./src/TypeFormatter/IntersectionTypeFormatter";
export * from "./src/TypeFormatter/LiteralTypeFormatter";
export * from "./src/TypeFormatter/LiteralUnionTypeFormatter";
Expand Down
32 changes: 16 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,39 +45,39 @@
},
"dependencies": {
"@types/json-schema": "^7.0.11",
"commander": "^9.4.0",
"commander": "^9.4.1",
"glob": "^8.0.3",
"json5": "^2.2.1",
"normalize-path": "^3.0.0",
"safe-stable-stringify": "^2.4.0",
"typescript": "~4.8.3"
"safe-stable-stringify": "^2.4.1",
"typescript": "~4.9.3"
},
"devDependencies": {
"@auto-it/conventional-commits": "^10.37.6",
"@auto-it/first-time-contributor": "^10.37.6",
"@babel/core": "^7.19.1",
"@babel/preset-env": "^7.19.1",
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.18.6",
"@types/glob": "^8.0.0",
"@types/jest": "^29.0.3",
"@types/node": "^18.7.18",
"@types/jest": "^29.2.3",
"@types/node": "^18.11.10",
"@types/normalize-path": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0",
"ajv": "^8.11.0",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"ajv": "^8.11.2",
"ajv-formats": "^2.1.1",
"auto": "^10.37.6",
"chai": "^4.3.6",
"chai": "^4.3.7",
"cross-env": "^7.0.3",
"eslint": "^8.23.1",
"eslint": "^8.29.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.0.3",
"jest-junit": "^14.0.1",
"prettier": "^2.7.1",
"jest": "^29.3.1",
"jest-junit": "^15.0.0",
"prettier": "^2.8.0",
"ts-node": "^10.9.1",
"vega": "^5.22.1",
"vega-lite": "^5.5.0"
"vega-lite": "^5.6.0"
},
"scripts": {
"prepublishOnly": "yarn build",
Expand Down
3 changes: 2 additions & 1 deletion src/NodeParser/AnnotatedNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export class AnnotatedNodeParser implements SubNodeParser {
const baseType = this.childNodeParser.createType(node, context, reference);

// Don't return annotations for lib types such as Exclude.
if (node.getSourceFile().fileName.match(/[/\\]typescript[/\\]lib[/\\]lib\.[^/\\]+\.d\.ts$/i)) {
// Sourceless nodes may not have a fileName, just ignore them.
if (node.getSourceFile()?.fileName.match(/[/\\]typescript[/\\]lib[/\\]lib\.[^/\\]+\.d\.ts$/i)) {
let specialCase = false;

// Special case for Exclude<T, U>: use the annotation of T.
Expand Down
18 changes: 18 additions & 0 deletions src/NodeParser/ConstructorNodeParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import ts from "typescript";
import { SubNodeParser } from "../SubNodeParser";
import { BaseType } from "../Type/BaseType";
import { ConstructorType } from "../Type/ConstructorType";

/**
* A constructor node parser that creates a constructor type so that mapped
* types can use constructors as values. There is no formatter for constructor
* types.
*/
export class ConstructorNodeParser implements SubNodeParser {
public supportsNode(node: ts.ConstructorTypeNode): boolean {
return node.kind === ts.SyntaxKind.ConstructorType;
}
public createType(): BaseType {
return new ConstructorType();
}
}
21 changes: 18 additions & 3 deletions src/NodeParser/InterfaceAndClassNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,27 @@ export class InterfaceAndClassNodeParser implements SubNodeParser {
}
return members;
}, [] as (ts.PropertyDeclaration | ts.PropertySignature | ts.ParameterPropertyDeclaration)[])
.filter((member) => isPublic(member) && !isStatic(member) && member.type && !isNodeHidden(member))
.filter((member) => isPublic(member) && !isStatic(member) && !isNodeHidden(member))
.reduce((entries, member) => {
let memberType: ts.Node | undefined = member.type;

// Use the type checker if the member has no explicit type
// Ignore members without an initializer. They have no useful type.
if (memberType === undefined && member.initializer !== undefined) {
const type = this.typeChecker.getTypeAtLocation(member);
memberType = this.typeChecker.typeToTypeNode(type, node, ts.NodeBuilderFlags.NoTruncation);
}

if (memberType !== undefined) {
return [...entries, { member, memberType }];
}
return entries;
}, [])
.map(
(member) =>
({ member, memberType }) =>
new ObjectProperty(
this.getPropertyName(member.name),
this.childNodeParser.createType(member.type!, context),
this.childNodeParser.createType(memberType, context),
!member.questionToken
)
)
Expand Down
12 changes: 6 additions & 6 deletions src/NodeParser/MappedTypeNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { ObjectProperty, ObjectType } from "../Type/ObjectType";
import { StringType } from "../Type/StringType";
import { SymbolType } from "../Type/SymbolType";
import { UnionType } from "../Type/UnionType";
import assert from "../Utils/assert";
import { derefAnnotatedType, derefType } from "../Utils/derefType";
import { getKey } from "../Utils/nodeKey";
import { preserveAnnotation } from "../Utils/preserveAnnotation";
Expand Down Expand Up @@ -79,23 +78,24 @@ export class MappedTypeNodeParser implements SubNodeParser {
}
}

protected mapKey(node: ts.MappedTypeNode, rawKey: LiteralType, context: Context): LiteralType {
protected mapKey(node: ts.MappedTypeNode, rawKey: LiteralType, context: Context): BaseType {
if (!node.nameType) {
return rawKey;
}
const key = derefType(
this.childNodeParser.createType(node.nameType, this.createSubContext(node, rawKey, context))
);
assert(key instanceof LiteralType, "Must resolve to Literal");

return key;
}

protected getProperties(node: ts.MappedTypeNode, keyListType: UnionType, context: Context): ObjectProperty[] {
return keyListType
.getTypes()
.filter((type): type is LiteralType => type instanceof LiteralType)
.reduce((result: ObjectProperty[], key: LiteralType) => {
const namedKey = this.mapKey(node, key, context);
.map((type) => [type, this.mapKey(node, type, context)])
.filter((value): value is [LiteralType, LiteralType] => value[1] instanceof LiteralType)
.reduce((result: ObjectProperty[], [key, mappedKey]: [LiteralType, LiteralType]) => {
const propertyType = this.childNodeParser.createType(
node.type!,
this.createSubContext(node, key, context)
Expand All @@ -110,7 +110,7 @@ export class MappedTypeNodeParser implements SubNodeParser {
}

const objectProperty = new ObjectProperty(
namedKey.getValue().toString(),
mappedKey.getValue().toString(),
preserveAnnotation(propertyType, newType),
!node.questionToken && !hasUndefined
);
Expand Down
39 changes: 25 additions & 14 deletions src/NodeParser/StringTemplateLiteralNodeParser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import ts from "typescript";
import { UnknownTypeError } from "../Error/UnknownTypeError";
import { Context, NodeParser } from "../NodeParser";
import { SubNodeParser } from "../SubNodeParser";
import { BaseType } from "../Type/BaseType";
import { LiteralType } from "../Type/LiteralType";
import { StringType } from "../Type/StringType";
import { UnionType } from "../Type/UnionType";
import { extractLiterals } from "../Utils/extractLiterals";

Expand All @@ -18,24 +20,33 @@ export class StringTemplateLiteralNodeParser implements SubNodeParser {
if (node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
return new LiteralType(node.text);
}
const prefix = node.head.text;
const matrix: string[][] = [[prefix]].concat(
node.templateSpans.map((span) => {
const suffix = span.literal.text;
const type = this.childNodeParser.createType(span.type, context);
return extractLiterals(type).map((value) => value + suffix);
})
);

const expandedLiterals = expand(matrix);
try {
const prefix = node.head.text;
const matrix: string[][] = [[prefix]].concat(
node.templateSpans.map((span) => {
const suffix = span.literal.text;
const type = this.childNodeParser.createType(span.type, context);
return extractLiterals(type).map((value) => value + suffix);
})
);

const expandedTypes = expandedLiterals.map((literal) => new LiteralType(literal));
const expandedLiterals = expand(matrix);

if (expandedTypes.length === 1) {
return expandedTypes[0];
}
const expandedTypes = expandedLiterals.map((literal) => new LiteralType(literal));

if (expandedTypes.length === 1) {
return expandedTypes[0];
}

return new UnionType(expandedTypes);
return new UnionType(expandedTypes);
} catch (error) {
if (error instanceof UnknownTypeError) {
return new StringType();
}

throw error;
}
}
}

Expand Down
50 changes: 40 additions & 10 deletions src/NodeParser/TypeLiteralNodeParser.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import ts from "typescript";
import ts, { isPropertySignature, MethodSignature, PropertySignature } from "typescript";
import { Context, NodeParser } from "../NodeParser";
import { SubNodeParser } from "../SubNodeParser";
import { BaseType } from "../Type/BaseType";
import { FunctionType } from "../Type/FunctionType";
import { NeverType } from "../Type/NeverType";
import { ObjectProperty, ObjectType } from "../Type/ObjectType";
import { ReferenceType } from "../Type/ReferenceType";
import { isNodeHidden } from "../Utils/isHidden";
import { getKey } from "../Utils/nodeKey";

export class TypeLiteralNodeParser implements SubNodeParser {
public constructor(protected childNodeParser: NodeParser, protected readonly additionalProperties: boolean) {}
public constructor(
protected typeChecker: ts.TypeChecker,
protected childNodeParser: NodeParser,
protected readonly additionalProperties: boolean
) {}

public supportsNode(node: ts.TypeLiteralNode): boolean {
return node.kind === ts.SyntaxKind.TypeLiteral;
Expand All @@ -34,15 +39,21 @@ export class TypeLiteralNodeParser implements SubNodeParser {
let hasRequiredNever = false;

const properties = node.members
.filter(ts.isPropertySignature)
.filter(
(element): element is PropertySignature | MethodSignature =>
ts.isPropertySignature(element) || ts.isMethodSignature(element)
)
.filter((propertyNode) => !isNodeHidden(propertyNode))
.map((propertyNode) => {
const propertySymbol: ts.Symbol = (propertyNode as any).symbol;
const type = this.childNodeParser.createType(propertyNode.type!, context);
const objectProperty = new ObjectProperty(propertySymbol.getName(), type, !propertyNode.questionToken);

return objectProperty;
})
.map(
(propertyNode) =>
new ObjectProperty(
this.getPropertyName(propertyNode.name),
isPropertySignature(propertyNode)
? this.childNodeParser.createType(propertyNode.type!, context)
: new FunctionType(),
!propertyNode.questionToken
)
)
.filter((prop) => {
if (prop.isRequired() && prop.getType() instanceof NeverType) {
hasRequiredNever = true;
Expand All @@ -69,4 +80,23 @@ export class TypeLiteralNodeParser implements SubNodeParser {
protected getTypeId(node: ts.Node, context: Context): string {
return `structure-${getKey(node, context)}`;
}

protected getPropertyName(propertyName: ts.PropertyName): string {
if (propertyName.kind === ts.SyntaxKind.ComputedPropertyName) {
const symbol = this.typeChecker.getSymbolAtLocation(propertyName);

if (symbol) {
return symbol.getName();
}
}

try {
return propertyName.getText();
} catch {
// When propertyName was programmatically created, it doesn't have a source file.
// Then, getText() will throw an error. But, for programmatically created nodes,`
// `escapedText` is available.
return (propertyName as ts.Identifier).escapedText as string;
}
}
}

0 comments on commit 3c07347

Please sign in to comment.