Skip to content

Commit

Permalink
Merge pull request #1965 from vega/next
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
domoritz authored May 23, 2024
2 parents 054d8d7 + 4ac21b2 commit 065e8c2
Show file tree
Hide file tree
Showing 27 changed files with 931 additions and 475 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ jobs:
test:
name: Test

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: "yarn"

- name: Install Node dependencies
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
/*.ts
coverage/
dist/
cjs/
node_modules/
!auto.config.ts
/.idea/

# local config for auto
.env

# Other package managers
pnpm-lock.yaml
package-lock.json
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ fs.writeFile(outputPath, schemaString, (err) => {
- `keyof`
- conditional types
- functions
- `Promise<T>` unwraps to `T`

## Run locally

Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
/** @type {import('@types/eslint').Linter.FlatConfig[]} */
export default tseslint.config(
{
ignores: ["dist"],
ignores: ["dist", "cjs", "build"],
},
eslint.configs.recommended,
{
Expand Down
12 changes: 7 additions & 5 deletions factory/parser.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import ts from "typescript";
import type ts from "typescript";
import { BasicAnnotationsReader } from "../src/AnnotationsReader/BasicAnnotationsReader.js";
import { ExtendedAnnotationsReader } from "../src/AnnotationsReader/ExtendedAnnotationsReader.js";
import { ChainNodeParser } from "../src/ChainNodeParser.js";
import { CircularReferenceNodeParser } from "../src/CircularReferenceNodeParser.js";
import { CompletedConfig } from "../src/Config.js";
import type { CompletedConfig } from "../src/Config.js";
import { ExposeNodeParser } from "../src/ExposeNodeParser.js";
import { MutableParser } from "../src/MutableParser.js";
import { NodeParser } from "../src/NodeParser.js";
import type { MutableParser } from "../src/MutableParser.js";
import type { NodeParser } from "../src/NodeParser.js";
import { AnnotatedNodeParser } from "../src/NodeParser/AnnotatedNodeParser.js";
import { AnyTypeNodeParser } from "../src/NodeParser/AnyTypeNodeParser.js";
import { ArrayLiteralExpressionNodeParser } from "../src/NodeParser/ArrayLiteralExpressionNodeParser.js";
Expand Down Expand Up @@ -55,9 +55,10 @@ import { UndefinedTypeNodeParser } from "../src/NodeParser/UndefinedTypeNodePars
import { UnionNodeParser } from "../src/NodeParser/UnionNodeParser.js";
import { UnknownTypeNodeParser } from "../src/NodeParser/UnknownTypeNodeParser.js";
import { VoidTypeNodeParser } from "../src/NodeParser/VoidTypeNodeParser.js";
import { SubNodeParser } from "../src/SubNodeParser.js";
import type { SubNodeParser } from "../src/SubNodeParser.js";
import { TopRefNodeParser } from "../src/TopRefNodeParser.js";
import { SatisfiesNodeParser } from "../src/NodeParser/SatisfiesNodeParser.js";
import { PromiseNodeParser } from "../src/NodeParser/PromiseNodeParser.js";

export type ParserAugmentor = (parser: MutableParser) => void;

Expand Down Expand Up @@ -121,6 +122,7 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme
.addNodeParser(new LiteralNodeParser(chainNodeParser))
.addNodeParser(new ParenthesizedNodeParser(chainNodeParser))

.addNodeParser(new PromiseNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new TypeReferenceNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new ExpressionWithTypeArgumentsNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new IndexedAccessTypeNodeParser(typeChecker, chainNodeParser))
Expand Down
4 changes: 3 additions & 1 deletion factory/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ function getTsConfig(config: Config) {
}

export function createProgram(config: CompletedConfig): ts.Program {
const rootNamesFromPath = config.path ? glob.sync(normalize(path.resolve(config.path))) : [];
const rootNamesFromPath = config.path
? glob.sync(normalize(path.resolve(config.path))).map((rootName) => normalize(rootName))
: [];
const tsconfig = getTsConfig(config);
const rootNames = rootNamesFromPath.length ? rootNamesFromPath : tsconfig.fileNames;

Expand Down
16 changes: 12 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
"name": "ts-json-schema-generator",
"version": "2.1.1",
"description": "Generate JSON schema from your Typescript sources",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"bin": {
"ts-json-schema-generator": "./bin/ts-json-schema-generator.js"
},
"files": [
"dist",
"cjs",
"src",
"factory",
"index.*",
Expand Down Expand Up @@ -44,13 +45,18 @@
"engines": {
"node": ">=18.0.0"
},
"exports": {
"import": "./dist/index.js",
"require": "./cjs/index.js"
},
"dependencies": {
"@types/json-schema": "^7.0.15",
"commander": "^12.0.0",
"glob": "^10.3.12",
"json5": "^2.2.3",
"normalize-path": "^3.0.0",
"safe-stable-stringify": "^2.4.3",
"tslib": "^2.6.2",
"typescript": "^5.4.5"
},
"devDependencies": {
Expand All @@ -65,6 +71,7 @@
"@types/jest": "^29.5.12",
"@types/node": "^20.12.7",
"@types/normalize-path": "^3.0.2",
"@types/ts-expose-internals": "npm:ts-expose-internals@^5.4.5",
"ajv": "^8.12.0",
"ajv-formats": "^3.0.1",
"auto": "^11.1.6",
Expand All @@ -83,7 +90,9 @@
},
"scripts": {
"prepublishOnly": "yarn build",
"build": "tsc",
"build": "npm run build:cjs && npm run build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.json",
"watch": "tsc -w",
"lint": "eslint",
"format": "eslint --fix",
Expand All @@ -94,6 +103,5 @@
"debug": "tsx --inspect-brk ts-json-schema-generator.ts",
"run": "tsx ts-json-schema-generator.ts",
"release": "yarn build && auto shipit"
},
"packageManager": "yarn@1.22.19"
}
}
2 changes: 1 addition & 1 deletion src/NodeParser/CallExpressionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class CallExpressionParser implements SubNodeParser {
const type = this.typeChecker.getTypeAtLocation(node);

// FIXME: remove special case
if ((type as any)?.typeArguments) {
if (Array.isArray((type as any)?.typeArguments?.[0]?.types)) {
return new TupleType([
new UnionType((type as any).typeArguments[0].types.map((t: any) => new LiteralType(t.value))),
]);
Expand Down
3 changes: 3 additions & 0 deletions src/NodeParser/FunctionNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ export class FunctionNodeParser implements SubNodeParser {
public supportsNode(node: ts.TypeNode): boolean {
return (
node.kind === ts.SyntaxKind.FunctionType ||
// @ts-expect-error internals type bug
node.kind === ts.SyntaxKind.FunctionExpression ||
// @ts-expect-error internals type bug
node.kind === ts.SyntaxKind.ArrowFunction ||
// @ts-expect-error internals type bug
node.kind === ts.SyntaxKind.FunctionDeclaration
);
}
Expand Down
98 changes: 98 additions & 0 deletions src/NodeParser/PromiseNodeParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import ts from "typescript";
import { Context, type NodeParser } from "../NodeParser.js";
import type { SubNodeParser } from "../SubNodeParser.js";
import { AliasType } from "../Type/AliasType.js";
import type { BaseType } from "../Type/BaseType.js";
import { DefinitionType } from "../Type/DefinitionType.js";
import { getKey } from "../Utils/nodeKey.js";

/**
* Needs to be registered before 261, 260, 230, 262 node kinds
*/
export class PromiseNodeParser implements SubNodeParser {
public constructor(
protected typeChecker: ts.TypeChecker,
protected childNodeParser: NodeParser,
) {}

public supportsNode(node: ts.Node): boolean {
if (
// 261 interface PromiseInterface extends Promise<T>
!ts.isInterfaceDeclaration(node) &&
// 260 class PromiseClass implements Promise<T>
!ts.isClassDeclaration(node) &&
// 230 Promise<T>
!ts.isExpressionWithTypeArguments(node) &&
// 262 type PromiseAlias = Promise<T>;
!ts.isTypeAliasDeclaration(node)
) {
return false;
}

const type = this.typeChecker.getTypeAtLocation(node);

const awaitedType = this.typeChecker.getAwaitedType(type);

// ignores non awaitable types
if (!awaitedType) {
return false;
}

// If the awaited type differs from the original type, the type extends promise
// Awaited<Promise<T>> -> T (Promise<T> !== T)
// Awaited<Y> -> Y (Y === Y)
if (awaitedType === type) {
return false;
}

// In types like: A<T> = T, type C = A<1>, C has the same type as A<1> and 1,
// the awaitedType is NOT the same reference as the type, so a assignability
// check is needed
return (
!this.typeChecker.isTypeAssignableTo(type, awaitedType) &&
!this.typeChecker.isTypeAssignableTo(awaitedType, type)
);
}

public createType(
node: ts.InterfaceDeclaration | ts.ClassDeclaration | ts.ExpressionWithTypeArguments | ts.TypeAliasDeclaration,
context: Context,
): BaseType {
const type = this.typeChecker.getTypeAtLocation(node);
const awaitedType = this.typeChecker.getAwaitedType(type)!; // supportsNode ensures this
const awaitedNode = this.typeChecker.typeToTypeNode(awaitedType, undefined, ts.NodeBuilderFlags.IgnoreErrors);

if (!awaitedNode) {
throw new Error(
`Could not find awaited node for type ${node.pos === -1 ? "<unresolved>" : node.getText()}`,
);
}

const baseNode = this.childNodeParser.createType(awaitedNode, new Context(node));

const name = this.getNodeName(node);

// Nodes without name should just be their awaited type
// export class extends Promise<T> {} -> T
// export class A extends Promise<T> {} -> A (ref to T)
if (!name) {
return baseNode;
}

return new DefinitionType(name, new AliasType(`promise-${getKey(node, context)}`, baseNode));
}

private getNodeName(
node: ts.InterfaceDeclaration | ts.ClassDeclaration | ts.ExpressionWithTypeArguments | ts.TypeAliasDeclaration,
) {
if (ts.isExpressionWithTypeArguments(node)) {
if (!ts.isHeritageClause(node.parent)) {
throw new Error("Expected ExpressionWithTypeArguments to have a HeritageClause parent");
}

return node.parent.parent.name?.getText();
}

return node.name?.getText();
}
}
18 changes: 11 additions & 7 deletions src/NodeParser/TypeReferenceNodeParser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ts from "typescript";

import { Context, NodeParser } from "../NodeParser.js";
import { Context, type NodeParser } from "../NodeParser.js";
import type { SubNodeParser } from "../SubNodeParser.js";
import { AnnotatedType } from "../Type/AnnotatedType.js";
import { AnyType } from "../Type/AnyType.js";
Expand Down Expand Up @@ -31,11 +30,6 @@ export class TypeReferenceNodeParser implements SubNodeParser {
// property on the node itself.
(node.typeName as unknown as ts.Type).symbol;

// Wraps promise type to avoid resolving to a empty Object type.
if (typeSymbol.name === "Promise") {
return this.childNodeParser.createType(node.typeArguments![0], this.createSubContext(node, context));
}

if (typeSymbol.flags & ts.SymbolFlags.Alias) {
const aliasedSymbol = this.typeChecker.getAliasedSymbol(typeSymbol);

Expand All @@ -53,6 +47,16 @@ export class TypeReferenceNodeParser implements SubNodeParser {
return context.getArgument(typeSymbol.name);
}

// Wraps promise type to avoid resolving to a empty Object type.
if (typeSymbol.name === "Promise" || typeSymbol.name === "PromiseLike") {
// Promise without type resolves to Promise<any>
if (!node.typeArguments || node.typeArguments.length === 0) {
return new AnyType();
}

return this.childNodeParser.createType(node.typeArguments[0], this.createSubContext(node, context));
}

if (typeSymbol.name === "Array" || typeSymbol.name === "ReadonlyArray") {
const type = this.createSubContext(node, context).getArguments()[0];

Expand Down
Loading

0 comments on commit 065e8c2

Please sign in to comment.