Skip to content

Commit 1b27258

Browse files
feat: support export * statements (#1962)
1 parent 183b426 commit 1b27258

File tree

8 files changed

+199
-73
lines changed

8 files changed

+199
-73
lines changed

src/SchemaGenerator.ts

Lines changed: 129 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import ts from "typescript";
2+
import type { Config } from "./Config.js";
23
import { NoRootTypeError } from "./Error/NoRootTypeError.js";
3-
import { Context, NodeParser } from "./NodeParser.js";
4-
import { Definition } from "./Schema/Definition.js";
5-
import { Schema } from "./Schema/Schema.js";
6-
import { BaseType } from "./Type/BaseType.js";
4+
import { Context, type NodeParser } from "./NodeParser.js";
5+
import type { Definition } from "./Schema/Definition.js";
6+
import type { Schema } from "./Schema/Schema.js";
7+
import type { BaseType } from "./Type/BaseType.js";
78
import { DefinitionType } from "./Type/DefinitionType.js";
8-
import { TypeFormatter } from "./TypeFormatter.js";
9-
import { StringMap } from "./Utils/StringMap.js";
10-
import { localSymbolAtNode, symbolAtNode } from "./Utils/symbolAtNode.js";
11-
import { removeUnreachable } from "./Utils/removeUnreachable.js";
12-
import { Config } from "./Config.js";
9+
import type { TypeFormatter } from "./TypeFormatter.js";
10+
import type { StringMap } from "./Utils/StringMap.js";
1311
import { hasJsDocTag } from "./Utils/hasJsDocTag.js";
12+
import { removeUnreachable } from "./Utils/removeUnreachable.js";
1413

1514
export class SchemaGenerator {
1615
public constructor(
@@ -32,7 +31,10 @@ export class SchemaGenerator {
3231

3332
const rootTypeDefinition = rootTypes.length === 1 ? this.getRootTypeDefinition(rootTypes[0]) : undefined;
3433
const definitions: StringMap<Definition> = {};
35-
rootTypes.forEach((rootType) => this.appendRootChildDefinitions(rootType, definitions));
34+
35+
for (const rootType of rootTypes) {
36+
this.appendRootChildDefinitions(rootType, definitions);
37+
}
3638

3739
const reachableDefinitions = removeUnreachable(rootTypeDefinition, definitions);
3840

@@ -47,15 +49,15 @@ export class SchemaGenerator {
4749
protected getRootNodes(fullName: string | undefined): ts.Node[] {
4850
if (fullName && fullName !== "*") {
4951
return [this.findNamedNode(fullName)];
50-
} else {
51-
const rootFileNames = this.program.getRootFileNames();
52-
const rootSourceFiles = this.program
53-
.getSourceFiles()
54-
.filter((sourceFile) => rootFileNames.includes(sourceFile.fileName));
55-
const rootNodes = new Map<string, ts.Node>();
56-
this.appendTypes(rootSourceFiles, this.program.getTypeChecker(), rootNodes);
57-
return [...rootNodes.values()];
5852
}
53+
54+
const rootFileNames = this.program.getRootFileNames();
55+
const rootSourceFiles = this.program
56+
.getSourceFiles()
57+
.filter((sourceFile) => rootFileNames.includes(sourceFile.fileName));
58+
const rootNodes = new Map<string, ts.Node>();
59+
this.appendTypes(rootSourceFiles, this.program.getTypeChecker(), rootNodes);
60+
return [...rootNodes.values()];
5961
}
6062
protected findNamedNode(fullName: string): ts.Node {
6163
const typeChecker = this.program.getTypeChecker();
@@ -129,6 +131,7 @@ export class SchemaGenerator {
129131

130132
return { projectFiles, externalFiles };
131133
}
134+
132135
protected appendTypes(
133136
sourceFiles: readonly ts.SourceFile[],
134137
typeChecker: ts.TypeChecker,
@@ -138,72 +141,131 @@ export class SchemaGenerator {
138141
this.inspectNode(sourceFile, typeChecker, types);
139142
}
140143
}
144+
141145
protected inspectNode(node: ts.Node, typeChecker: ts.TypeChecker, allTypes: Map<string, ts.Node>): void {
142-
switch (node.kind) {
143-
case ts.SyntaxKind.VariableDeclaration: {
144-
const variableDeclarationNode = node as ts.VariableDeclaration;
145-
if (
146-
variableDeclarationNode.initializer?.kind === ts.SyntaxKind.ArrowFunction ||
147-
variableDeclarationNode.initializer?.kind === ts.SyntaxKind.FunctionExpression
148-
) {
149-
this.inspectNode(variableDeclarationNode.initializer, typeChecker, allTypes);
150-
}
151-
return;
146+
if (ts.isVariableDeclaration(node)) {
147+
if (
148+
node.initializer?.kind === ts.SyntaxKind.ArrowFunction ||
149+
node.initializer?.kind === ts.SyntaxKind.FunctionExpression
150+
) {
151+
this.inspectNode(node.initializer, typeChecker, allTypes);
152152
}
153-
case ts.SyntaxKind.InterfaceDeclaration:
154-
case ts.SyntaxKind.ClassDeclaration:
155-
case ts.SyntaxKind.EnumDeclaration:
156-
case ts.SyntaxKind.TypeAliasDeclaration:
157-
if (
158-
this.config?.expose === "all" ||
159-
(this.isExportType(node) && !this.isGenericType(node as ts.TypeAliasDeclaration))
160-
) {
161-
allTypes.set(this.getFullName(node, typeChecker), node);
162-
return;
163-
}
164-
return;
165-
case ts.SyntaxKind.ConstructorType:
166-
case ts.SyntaxKind.FunctionDeclaration:
167-
case ts.SyntaxKind.FunctionExpression:
168-
case ts.SyntaxKind.ArrowFunction:
153+
154+
return;
155+
}
156+
157+
if (
158+
ts.isInterfaceDeclaration(node) ||
159+
ts.isClassDeclaration(node) ||
160+
ts.isEnumDeclaration(node) ||
161+
ts.isTypeAliasDeclaration(node)
162+
) {
163+
if (
164+
this.config?.expose === "all" ||
165+
(this.isExportType(node) && !this.isGenericType(node as ts.TypeAliasDeclaration))
166+
) {
169167
allTypes.set(this.getFullName(node, typeChecker), node);
170168
return;
171-
case ts.SyntaxKind.ExportSpecifier: {
172-
const exportSpecifierNode = node as ts.ExportSpecifier;
173-
const symbol = typeChecker.getExportSpecifierLocalTargetSymbol(exportSpecifierNode);
174-
if (symbol?.declarations?.length === 1) {
175-
const declaration = symbol.declarations[0];
176-
if (declaration.kind === ts.SyntaxKind.ImportSpecifier) {
177-
// Handling the `Foo` in `import { Foo } from "./lib"; export { Foo };`
178-
const importSpecifierNode = declaration as ts.ImportSpecifier;
179-
const type = typeChecker.getTypeAtLocation(importSpecifierNode);
180-
if (type.symbol?.declarations?.length === 1) {
181-
this.inspectNode(type.symbol.declarations[0], typeChecker, allTypes);
182-
}
183-
} else {
184-
// Handling the `Bar` in `export { Bar } from './lib';`
185-
this.inspectNode(declaration, typeChecker, allTypes);
169+
}
170+
return;
171+
}
172+
173+
if (
174+
ts.isFunctionDeclaration(node) ||
175+
ts.isFunctionExpression(node) ||
176+
ts.isArrowFunction(node) ||
177+
ts.isConstructorTypeNode(node)
178+
) {
179+
allTypes.set(this.getFullName(node, typeChecker), node);
180+
return;
181+
}
182+
183+
if (ts.isExportSpecifier(node)) {
184+
const symbol = typeChecker.getExportSpecifierLocalTargetSymbol(node);
185+
186+
if (symbol?.declarations?.length === 1) {
187+
const declaration = symbol.declarations[0];
188+
189+
if (ts.isImportSpecifier(declaration)) {
190+
// Handling the `Foo` in `import { Foo } from "./lib"; export { Foo };`
191+
const type = typeChecker.getTypeAtLocation(declaration);
192+
193+
if (type.symbol?.declarations?.length === 1) {
194+
this.inspectNode(type.symbol.declarations[0], typeChecker, allTypes);
186195
}
196+
} else {
197+
// Handling the `Bar` in `export { Bar } from './lib';`
198+
this.inspectNode(declaration, typeChecker, allTypes);
187199
}
200+
}
201+
202+
return;
203+
}
204+
205+
if (ts.isExportDeclaration(node)) {
206+
if (!ts.isExportDeclaration(node)) {
188207
return;
189208
}
190-
default:
191-
ts.forEachChild(node, (subnode) => this.inspectNode(subnode, typeChecker, allTypes));
209+
210+
// export { variable } clauses
211+
if (!node.moduleSpecifier) {
192212
return;
213+
}
214+
215+
const symbol = typeChecker.getSymbolAtLocation(node.moduleSpecifier);
216+
217+
// should never hit this (maybe type error in user's code)
218+
if (!symbol || !symbol.declarations) {
219+
return;
220+
}
221+
222+
// module augmentation can result in more than one source file
223+
for (const source of symbol.declarations) {
224+
const sourceSymbol = typeChecker.getSymbolAtLocation(source);
225+
226+
if (!sourceSymbol) {
227+
return;
228+
}
229+
230+
const moduleExports = typeChecker.getExportsOfModule(sourceSymbol);
231+
232+
for (const moduleExport of moduleExports) {
233+
const nodes =
234+
moduleExport.declarations ||
235+
(!!moduleExport.valueDeclaration && [moduleExport.valueDeclaration]);
236+
237+
// should never hit this (maybe type error in user's code)
238+
if (!nodes) {
239+
return;
240+
}
241+
242+
for (const subnodes of nodes) {
243+
this.inspectNode(subnodes, typeChecker, allTypes);
244+
}
245+
}
246+
}
247+
248+
return;
193249
}
250+
251+
ts.forEachChild(node, (subnode) => this.inspectNode(subnode, typeChecker, allTypes));
194252
}
195-
protected isExportType(node: ts.Node): boolean {
253+
254+
protected isExportType(
255+
node: ts.InterfaceDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.TypeAliasDeclaration,
256+
): boolean {
196257
if (this.config?.jsDoc !== "none" && hasJsDocTag(node, "internal")) {
197258
return false;
198259
}
199-
const localSymbol = localSymbolAtNode(node);
200-
return localSymbol ? "exportSymbol" in localSymbol : false;
260+
261+
return !!node.localSymbol?.exportSymbol;
201262
}
263+
202264
protected isGenericType(node: ts.TypeAliasDeclaration): boolean {
203265
return !!(node.typeParameters && node.typeParameters.length > 0);
204266
}
205-
protected getFullName(node: ts.Node, typeChecker: ts.TypeChecker): string {
206-
const symbol = symbolAtNode(node)!;
207-
return typeChecker.getFullyQualifiedName(symbol).replace(/".*"\./, "");
267+
268+
protected getFullName(node: ts.Declaration, typeChecker: ts.TypeChecker): string {
269+
return typeChecker.getFullyQualifiedName(node.symbol).replace(/".*"\./, "");
208270
}
209271
}

src/Utils/symbolAtNode.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import ts from "typescript";
1+
import type ts from "typescript";
22

33
export function symbolAtNode(node: ts.Node): ts.Symbol | undefined {
4-
return (node as any).symbol;
5-
}
6-
export function localSymbolAtNode(node: ts.Node): ts.Symbol | undefined {
7-
return (node as any).localSymbol;
4+
return (node as ts.Declaration).symbol;
85
}

test/sourceless-nodes/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe("sourceless-nodes", () => {
4141
});
4242
});
4343

44-
// From github.com/arthurfiorette/kita/blob/main/packages/generator/src/util/type-resolver.ts
44+
// From https://github.com/kitajs/kitajs/blob/ebf23297de07887c78becff52120f941e69386ec/packages/parser/src/util/nodes.ts#L64
4545
function getReturnType(node: ts.SignatureDeclaration, typeChecker: ts.TypeChecker) {
4646
const signature = typeChecker.getSignatureFromDeclaration(node);
4747
const implicitType = typeChecker.getReturnTypeOfSignature(signature!);

test/valid-data-type.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,6 @@ describe("valid-data-type", () => {
145145
it("lowercase", assertValidSchema("lowercase", "MyType"));
146146

147147
it("promise-extensions", assertValidSchema("promise-extensions", "*"));
148+
149+
it("export-star", assertValidSchema("export-star", "*", undefined, { mainTsOnly: true }));
148150
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type A = 1;
2+
3+
export type B = "string";
4+
5+
type C = "internal";
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export * from "./literal";
2+
export * from "./object";
3+
4+
export type External = 1;
5+
6+
type Internal = 2;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface D {
2+
a: 1;
3+
}
4+
5+
export class E {
6+
b: 2;
7+
}
8+
9+
interface F {
10+
internal: true;
11+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"definitions": {
4+
"A": {
5+
"const": 1,
6+
"type": "number"
7+
},
8+
"B": {
9+
"const": "string",
10+
"type": "string"
11+
},
12+
"D": {
13+
"additionalProperties": false,
14+
"properties": {
15+
"a": {
16+
"const": 1,
17+
"type": "number"
18+
}
19+
},
20+
"required": [
21+
"a"
22+
],
23+
"type": "object"
24+
},
25+
"E": {
26+
"additionalProperties": false,
27+
"properties": {
28+
"b": {
29+
"const": 2,
30+
"type": "number"
31+
}
32+
},
33+
"required": [
34+
"b"
35+
],
36+
"type": "object"
37+
},
38+
"External": {
39+
"const": 1,
40+
"type": "number"
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)