Skip to content

Commit 065e8c2

Browse files
authored
Merge pull request #1965 from vega/next
Release
2 parents 054d8d7 + 4ac21b2 commit 065e8c2

27 files changed

+931
-475
lines changed

.github/workflows/test.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@ jobs:
1212
test:
1313
name: Test
1414

15-
runs-on: ubuntu-latest
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
os: [ubuntu-latest, windows-latest]
19+
20+
runs-on: ${{ matrix.os }}
1621

1722
steps:
1823
- uses: actions/checkout@v4
1924

2025
- uses: actions/setup-node@v4
2126
with:
27+
node-version: 22
2228
cache: "yarn"
2329

2430
- name: Install Node dependencies

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
/*.ts
22
coverage/
33
dist/
4+
cjs/
45
node_modules/
56
!auto.config.ts
67
/.idea/
78

89
# local config for auto
910
.env
1011

12+
# Other package managers
13+
pnpm-lock.yaml
14+
package-lock.json

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ fs.writeFile(outputPath, schemaString, (err) => {
231231
- `keyof`
232232
- conditional types
233233
- functions
234+
- `Promise<T>` unwraps to `T`
234235

235236
## Run locally
236237

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
66
/** @type {import('@types/eslint').Linter.FlatConfig[]} */
77
export default tseslint.config(
88
{
9-
ignores: ["dist"],
9+
ignores: ["dist", "cjs", "build"],
1010
},
1111
eslint.configs.recommended,
1212
{

factory/parser.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import ts from "typescript";
1+
import type ts from "typescript";
22
import { BasicAnnotationsReader } from "../src/AnnotationsReader/BasicAnnotationsReader.js";
33
import { ExtendedAnnotationsReader } from "../src/AnnotationsReader/ExtendedAnnotationsReader.js";
44
import { ChainNodeParser } from "../src/ChainNodeParser.js";
55
import { CircularReferenceNodeParser } from "../src/CircularReferenceNodeParser.js";
6-
import { CompletedConfig } from "../src/Config.js";
6+
import type { CompletedConfig } from "../src/Config.js";
77
import { ExposeNodeParser } from "../src/ExposeNodeParser.js";
8-
import { MutableParser } from "../src/MutableParser.js";
9-
import { NodeParser } from "../src/NodeParser.js";
8+
import type { MutableParser } from "../src/MutableParser.js";
9+
import type { NodeParser } from "../src/NodeParser.js";
1010
import { AnnotatedNodeParser } from "../src/NodeParser/AnnotatedNodeParser.js";
1111
import { AnyTypeNodeParser } from "../src/NodeParser/AnyTypeNodeParser.js";
1212
import { ArrayLiteralExpressionNodeParser } from "../src/NodeParser/ArrayLiteralExpressionNodeParser.js";
@@ -55,9 +55,10 @@ import { UndefinedTypeNodeParser } from "../src/NodeParser/UndefinedTypeNodePars
5555
import { UnionNodeParser } from "../src/NodeParser/UnionNodeParser.js";
5656
import { UnknownTypeNodeParser } from "../src/NodeParser/UnknownTypeNodeParser.js";
5757
import { VoidTypeNodeParser } from "../src/NodeParser/VoidTypeNodeParser.js";
58-
import { SubNodeParser } from "../src/SubNodeParser.js";
58+
import type { SubNodeParser } from "../src/SubNodeParser.js";
5959
import { TopRefNodeParser } from "../src/TopRefNodeParser.js";
6060
import { SatisfiesNodeParser } from "../src/NodeParser/SatisfiesNodeParser.js";
61+
import { PromiseNodeParser } from "../src/NodeParser/PromiseNodeParser.js";
6162

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

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

125+
.addNodeParser(new PromiseNodeParser(typeChecker, chainNodeParser))
124126
.addNodeParser(new TypeReferenceNodeParser(typeChecker, chainNodeParser))
125127
.addNodeParser(new ExpressionWithTypeArgumentsNodeParser(typeChecker, chainNodeParser))
126128
.addNodeParser(new IndexedAccessTypeNodeParser(typeChecker, chainNodeParser))

factory/program.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ function getTsConfig(config: Config) {
6060
}
6161

6262
export function createProgram(config: CompletedConfig): ts.Program {
63-
const rootNamesFromPath = config.path ? glob.sync(normalize(path.resolve(config.path))) : [];
63+
const rootNamesFromPath = config.path
64+
? glob.sync(normalize(path.resolve(config.path))).map((rootName) => normalize(rootName))
65+
: [];
6466
const tsconfig = getTsConfig(config);
6567
const rootNames = rootNamesFromPath.length ? rootNamesFromPath : tsconfig.fileNames;
6668

package.json

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
"name": "ts-json-schema-generator",
33
"version": "2.1.1",
44
"description": "Generate JSON schema from your Typescript sources",
5-
"main": "dist/index.js",
5+
"module": "dist/index.js",
66
"types": "dist/index.d.ts",
77
"type": "module",
88
"bin": {
99
"ts-json-schema-generator": "./bin/ts-json-schema-generator.js"
1010
},
1111
"files": [
1212
"dist",
13+
"cjs",
1314
"src",
1415
"factory",
1516
"index.*",
@@ -44,13 +45,18 @@
4445
"engines": {
4546
"node": ">=18.0.0"
4647
},
48+
"exports": {
49+
"import": "./dist/index.js",
50+
"require": "./cjs/index.js"
51+
},
4752
"dependencies": {
4853
"@types/json-schema": "^7.0.15",
4954
"commander": "^12.0.0",
5055
"glob": "^10.3.12",
5156
"json5": "^2.2.3",
5257
"normalize-path": "^3.0.0",
5358
"safe-stable-stringify": "^2.4.3",
59+
"tslib": "^2.6.2",
5460
"typescript": "^5.4.5"
5561
},
5662
"devDependencies": {
@@ -65,6 +71,7 @@
6571
"@types/jest": "^29.5.12",
6672
"@types/node": "^20.12.7",
6773
"@types/normalize-path": "^3.0.2",
74+
"@types/ts-expose-internals": "npm:ts-expose-internals@^5.4.5",
6875
"ajv": "^8.12.0",
6976
"ajv-formats": "^3.0.1",
7077
"auto": "^11.1.6",
@@ -83,7 +90,9 @@
8390
},
8491
"scripts": {
8592
"prepublishOnly": "yarn build",
86-
"build": "tsc",
93+
"build": "npm run build:cjs && npm run build:esm",
94+
"build:cjs": "tsc -p tsconfig.cjs.json",
95+
"build:esm": "tsc -p tsconfig.json",
8796
"watch": "tsc -w",
8897
"lint": "eslint",
8998
"format": "eslint --fix",
@@ -94,6 +103,5 @@
94103
"debug": "tsx --inspect-brk ts-json-schema-generator.ts",
95104
"run": "tsx ts-json-schema-generator.ts",
96105
"release": "yarn build && auto shipit"
97-
},
98-
"packageManager": "[email protected]"
106+
}
99107
}

src/NodeParser/CallExpressionParser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class CallExpressionParser implements SubNodeParser {
2020
const type = this.typeChecker.getTypeAtLocation(node);
2121

2222
// FIXME: remove special case
23-
if ((type as any)?.typeArguments) {
23+
if (Array.isArray((type as any)?.typeArguments?.[0]?.types)) {
2424
return new TupleType([
2525
new UnionType((type as any).typeArguments[0].types.map((t: any) => new LiteralType(t.value))),
2626
]);

src/NodeParser/FunctionNodeParser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ export class FunctionNodeParser implements SubNodeParser {
1818
public supportsNode(node: ts.TypeNode): boolean {
1919
return (
2020
node.kind === ts.SyntaxKind.FunctionType ||
21+
// @ts-expect-error internals type bug
2122
node.kind === ts.SyntaxKind.FunctionExpression ||
23+
// @ts-expect-error internals type bug
2224
node.kind === ts.SyntaxKind.ArrowFunction ||
25+
// @ts-expect-error internals type bug
2326
node.kind === ts.SyntaxKind.FunctionDeclaration
2427
);
2528
}

src/NodeParser/PromiseNodeParser.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import ts from "typescript";
2+
import { Context, type NodeParser } from "../NodeParser.js";
3+
import type { SubNodeParser } from "../SubNodeParser.js";
4+
import { AliasType } from "../Type/AliasType.js";
5+
import type { BaseType } from "../Type/BaseType.js";
6+
import { DefinitionType } from "../Type/DefinitionType.js";
7+
import { getKey } from "../Utils/nodeKey.js";
8+
9+
/**
10+
* Needs to be registered before 261, 260, 230, 262 node kinds
11+
*/
12+
export class PromiseNodeParser implements SubNodeParser {
13+
public constructor(
14+
protected typeChecker: ts.TypeChecker,
15+
protected childNodeParser: NodeParser,
16+
) {}
17+
18+
public supportsNode(node: ts.Node): boolean {
19+
if (
20+
// 261 interface PromiseInterface extends Promise<T>
21+
!ts.isInterfaceDeclaration(node) &&
22+
// 260 class PromiseClass implements Promise<T>
23+
!ts.isClassDeclaration(node) &&
24+
// 230 Promise<T>
25+
!ts.isExpressionWithTypeArguments(node) &&
26+
// 262 type PromiseAlias = Promise<T>;
27+
!ts.isTypeAliasDeclaration(node)
28+
) {
29+
return false;
30+
}
31+
32+
const type = this.typeChecker.getTypeAtLocation(node);
33+
34+
const awaitedType = this.typeChecker.getAwaitedType(type);
35+
36+
// ignores non awaitable types
37+
if (!awaitedType) {
38+
return false;
39+
}
40+
41+
// If the awaited type differs from the original type, the type extends promise
42+
// Awaited<Promise<T>> -> T (Promise<T> !== T)
43+
// Awaited<Y> -> Y (Y === Y)
44+
if (awaitedType === type) {
45+
return false;
46+
}
47+
48+
// In types like: A<T> = T, type C = A<1>, C has the same type as A<1> and 1,
49+
// the awaitedType is NOT the same reference as the type, so a assignability
50+
// check is needed
51+
return (
52+
!this.typeChecker.isTypeAssignableTo(type, awaitedType) &&
53+
!this.typeChecker.isTypeAssignableTo(awaitedType, type)
54+
);
55+
}
56+
57+
public createType(
58+
node: ts.InterfaceDeclaration | ts.ClassDeclaration | ts.ExpressionWithTypeArguments | ts.TypeAliasDeclaration,
59+
context: Context,
60+
): BaseType {
61+
const type = this.typeChecker.getTypeAtLocation(node);
62+
const awaitedType = this.typeChecker.getAwaitedType(type)!; // supportsNode ensures this
63+
const awaitedNode = this.typeChecker.typeToTypeNode(awaitedType, undefined, ts.NodeBuilderFlags.IgnoreErrors);
64+
65+
if (!awaitedNode) {
66+
throw new Error(
67+
`Could not find awaited node for type ${node.pos === -1 ? "<unresolved>" : node.getText()}`,
68+
);
69+
}
70+
71+
const baseNode = this.childNodeParser.createType(awaitedNode, new Context(node));
72+
73+
const name = this.getNodeName(node);
74+
75+
// Nodes without name should just be their awaited type
76+
// export class extends Promise<T> {} -> T
77+
// export class A extends Promise<T> {} -> A (ref to T)
78+
if (!name) {
79+
return baseNode;
80+
}
81+
82+
return new DefinitionType(name, new AliasType(`promise-${getKey(node, context)}`, baseNode));
83+
}
84+
85+
private getNodeName(
86+
node: ts.InterfaceDeclaration | ts.ClassDeclaration | ts.ExpressionWithTypeArguments | ts.TypeAliasDeclaration,
87+
) {
88+
if (ts.isExpressionWithTypeArguments(node)) {
89+
if (!ts.isHeritageClause(node.parent)) {
90+
throw new Error("Expected ExpressionWithTypeArguments to have a HeritageClause parent");
91+
}
92+
93+
return node.parent.parent.name?.getText();
94+
}
95+
96+
return node.name?.getText();
97+
}
98+
}

0 commit comments

Comments
 (0)