Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/next' into arthur/better-errors
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurfiorette committed May 23, 2024
2 parents be22ec4 + 4ac21b2 commit 964546a
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 83 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*.ts
coverage/
dist/
cjs/
node_modules/
!auto.config.ts
/.idea/
Expand All @@ -10,4 +11,4 @@ node_modules/

# Other package managers
pnpm-lock.yaml
package-lock.json
package-lock.json
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: 10 additions & 2 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.0.0",
"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 Down Expand Up @@ -84,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 Down
198 changes: 130 additions & 68 deletions src/SchemaGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import ts from "typescript";
import { Context, NodeParser } from "./NodeParser.js";
import { Definition } from "./Schema/Definition.js";
import { Schema } from "./Schema/Schema.js";
import { BaseType } from "./Type/BaseType.js";
import type { Config } from "./Config.js";
import { MultipleDefinitionsTJSGError, RootlessTJSGError } from "./Error/Errors.js";
import { Context, type NodeParser } from "./NodeParser.js";
import type { Definition } from "./Schema/Definition.js";
import type { Schema } from "./Schema/Schema.js";
import type { BaseType } from "./Type/BaseType.js";
import { DefinitionType } from "./Type/DefinitionType.js";
import { TypeFormatter } from "./TypeFormatter.js";
import { StringMap } from "./Utils/StringMap.js";
import { localSymbolAtNode, symbolAtNode } from "./Utils/symbolAtNode.js";
import { removeUnreachable } from "./Utils/removeUnreachable.js";
import { Config } from "./Config.js";
import type { TypeFormatter } from "./TypeFormatter.js";
import type { StringMap } from "./Utils/StringMap.js";
import { hasJsDocTag } from "./Utils/hasJsDocTag.js";
import { MultipleDefinitionsTJSGError, RootlessTJSGError } from "./Error/Errors.js";
import { removeUnreachable } from "./Utils/removeUnreachable.js";

export class SchemaGenerator {
public constructor(
Expand All @@ -32,7 +31,10 @@ export class SchemaGenerator {

const rootTypeDefinition = rootTypes.length === 1 ? this.getRootTypeDefinition(rootTypes[0]) : undefined;
const definitions: StringMap<Definition> = {};
rootTypes.forEach((rootType) => this.appendRootChildDefinitions(rootType, definitions));

for (const rootType of rootTypes) {
this.appendRootChildDefinitions(rootType, definitions);
}

const reachableDefinitions = removeUnreachable(rootTypeDefinition, definitions);

Expand All @@ -47,15 +49,15 @@ export class SchemaGenerator {
protected getRootNodes(fullName: string | undefined): ts.Node[] {
if (fullName && fullName !== "*") {
return [this.findNamedNode(fullName)];
} else {
const rootFileNames = this.program.getRootFileNames();
const rootSourceFiles = this.program
.getSourceFiles()
.filter((sourceFile) => rootFileNames.includes(sourceFile.fileName));
const rootNodes = new Map<string, ts.Node>();
this.appendTypes(rootSourceFiles, this.program.getTypeChecker(), rootNodes);
return [...rootNodes.values()];
}

const rootFileNames = this.program.getRootFileNames();
const rootSourceFiles = this.program
.getSourceFiles()
.filter((sourceFile) => rootFileNames.includes(sourceFile.fileName));
const rootNodes = new Map<string, ts.Node>();
this.appendTypes(rootSourceFiles, this.program.getTypeChecker(), rootNodes);
return [...rootNodes.values()];
}
protected findNamedNode(fullName: string): ts.Node {
const typeChecker = this.program.getTypeChecker();
Expand Down Expand Up @@ -133,6 +135,7 @@ export class SchemaGenerator {

return { projectFiles, externalFiles };
}

protected appendTypes(
sourceFiles: readonly ts.SourceFile[],
typeChecker: ts.TypeChecker,
Expand All @@ -142,72 +145,131 @@ export class SchemaGenerator {
this.inspectNode(sourceFile, typeChecker, types);
}
}

protected inspectNode(node: ts.Node, typeChecker: ts.TypeChecker, allTypes: Map<string, ts.Node>): void {
switch (node.kind) {
case ts.SyntaxKind.VariableDeclaration: {
const variableDeclarationNode = node as ts.VariableDeclaration;
if (
variableDeclarationNode.initializer?.kind === ts.SyntaxKind.ArrowFunction ||
variableDeclarationNode.initializer?.kind === ts.SyntaxKind.FunctionExpression
) {
this.inspectNode(variableDeclarationNode.initializer, typeChecker, allTypes);
}
return;
if (ts.isVariableDeclaration(node)) {
if (
node.initializer?.kind === ts.SyntaxKind.ArrowFunction ||
node.initializer?.kind === ts.SyntaxKind.FunctionExpression
) {
this.inspectNode(node.initializer, typeChecker, allTypes);
}
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.EnumDeclaration:
case ts.SyntaxKind.TypeAliasDeclaration:
if (
this.config?.expose === "all" ||
(this.isExportType(node) && !this.isGenericType(node as ts.TypeAliasDeclaration))
) {
allTypes.set(this.getFullName(node, typeChecker), node);
return;
}
return;
case ts.SyntaxKind.ConstructorType:
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.FunctionExpression:
case ts.SyntaxKind.ArrowFunction:

return;
}

if (
ts.isInterfaceDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isEnumDeclaration(node) ||
ts.isTypeAliasDeclaration(node)
) {
if (
this.config?.expose === "all" ||
(this.isExportType(node) && !this.isGenericType(node as ts.TypeAliasDeclaration))
) {
allTypes.set(this.getFullName(node, typeChecker), node);
return;
case ts.SyntaxKind.ExportSpecifier: {
const exportSpecifierNode = node as ts.ExportSpecifier;
const symbol = typeChecker.getExportSpecifierLocalTargetSymbol(exportSpecifierNode);
if (symbol?.declarations?.length === 1) {
const declaration = symbol.declarations[0];
if (declaration.kind === ts.SyntaxKind.ImportSpecifier) {
// Handling the `Foo` in `import { Foo } from "./lib"; export { Foo };`
const importSpecifierNode = declaration as ts.ImportSpecifier;
const type = typeChecker.getTypeAtLocation(importSpecifierNode);
if (type.symbol?.declarations?.length === 1) {
this.inspectNode(type.symbol.declarations[0], typeChecker, allTypes);
}
} else {
// Handling the `Bar` in `export { Bar } from './lib';`
this.inspectNode(declaration, typeChecker, allTypes);
}
return;
}

if (
ts.isFunctionDeclaration(node) ||
ts.isFunctionExpression(node) ||
ts.isArrowFunction(node) ||
ts.isConstructorTypeNode(node)
) {
allTypes.set(this.getFullName(node, typeChecker), node);
return;
}

if (ts.isExportSpecifier(node)) {
const symbol = typeChecker.getExportSpecifierLocalTargetSymbol(node);

if (symbol?.declarations?.length === 1) {
const declaration = symbol.declarations[0];

if (ts.isImportSpecifier(declaration)) {
// Handling the `Foo` in `import { Foo } from "./lib"; export { Foo };`
const type = typeChecker.getTypeAtLocation(declaration);

if (type.symbol?.declarations?.length === 1) {
this.inspectNode(type.symbol.declarations[0], typeChecker, allTypes);
}
} else {
// Handling the `Bar` in `export { Bar } from './lib';`
this.inspectNode(declaration, typeChecker, allTypes);
}
}

return;
}

if (ts.isExportDeclaration(node)) {
if (!ts.isExportDeclaration(node)) {
return;
}
default:
ts.forEachChild(node, (subnode) => this.inspectNode(subnode, typeChecker, allTypes));

// export { variable } clauses
if (!node.moduleSpecifier) {
return;
}

const symbol = typeChecker.getSymbolAtLocation(node.moduleSpecifier);

// should never hit this (maybe type error in user's code)
if (!symbol || !symbol.declarations) {
return;
}

// module augmentation can result in more than one source file
for (const source of symbol.declarations) {
const sourceSymbol = typeChecker.getSymbolAtLocation(source);

if (!sourceSymbol) {
return;
}

const moduleExports = typeChecker.getExportsOfModule(sourceSymbol);

for (const moduleExport of moduleExports) {
const nodes =
moduleExport.declarations ||
(!!moduleExport.valueDeclaration && [moduleExport.valueDeclaration]);

// should never hit this (maybe type error in user's code)
if (!nodes) {
return;
}

for (const subnodes of nodes) {
this.inspectNode(subnodes, typeChecker, allTypes);
}
}
}

return;
}

ts.forEachChild(node, (subnode) => this.inspectNode(subnode, typeChecker, allTypes));
}
protected isExportType(node: ts.Node): boolean {

protected isExportType(
node: ts.InterfaceDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.TypeAliasDeclaration,
): boolean {
if (this.config?.jsDoc !== "none" && hasJsDocTag(node, "internal")) {
return false;
}
const localSymbol = localSymbolAtNode(node);
return localSymbol ? "exportSymbol" in localSymbol : false;

return !!node.localSymbol?.exportSymbol;
}

protected isGenericType(node: ts.TypeAliasDeclaration): boolean {
return !!(node.typeParameters && node.typeParameters.length > 0);
}
protected getFullName(node: ts.Node, typeChecker: ts.TypeChecker): string {
const symbol = symbolAtNode(node)!;
return typeChecker.getFullyQualifiedName(symbol).replace(/".*"\./, "");

protected getFullName(node: ts.Declaration, typeChecker: ts.TypeChecker): string {
return typeChecker.getFullyQualifiedName(node.symbol).replace(/".*"\./, "");
}
}
7 changes: 2 additions & 5 deletions src/Utils/symbolAtNode.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import ts from "typescript";
import type ts from "typescript";

export function symbolAtNode(node: ts.Node): ts.Symbol | undefined {
return (node as any).symbol;
}
export function localSymbolAtNode(node: ts.Node): ts.Symbol | undefined {
return (node as any).localSymbol;
return (node as ts.Declaration).symbol;
}
2 changes: 1 addition & 1 deletion test/sourceless-nodes/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe("sourceless-nodes", () => {
});
});

// From github.com/arthurfiorette/kita/blob/main/packages/generator/src/util/type-resolver.ts
// From https://github.com/kitajs/kitajs/blob/ebf23297de07887c78becff52120f941e69386ec/packages/parser/src/util/nodes.ts#L64
function getReturnType(node: ts.SignatureDeclaration, typeChecker: ts.TypeChecker) {
const signature = typeChecker.getSignatureFromDeclaration(node);
const implicitType = typeChecker.getReturnTypeOfSignature(signature!);
Expand Down
2 changes: 2 additions & 0 deletions test/valid-data-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,6 @@ describe("valid-data-type", () => {
it("lowercase", assertValidSchema("lowercase", "MyType"));

it("promise-extensions", assertValidSchema("promise-extensions", "*"));

it("export-star", assertValidSchema("export-star", "*", undefined, { mainTsOnly: true }));
});
5 changes: 5 additions & 0 deletions test/valid-data/export-star/literal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type A = 1;

export type B = "string";

type C = "internal";
6 changes: 6 additions & 0 deletions test/valid-data/export-star/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from "./literal";
export * from "./object";

export type External = 1;

type Internal = 2;
11 changes: 11 additions & 0 deletions test/valid-data/export-star/object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface D {
a: 1;
}

export class E {
b: 2;
}

interface F {
internal: true;
}
Loading

0 comments on commit 964546a

Please sign in to comment.