diff --git a/packages/scripts/src/commands/intergen.ts b/packages/scripts/src/commands/intergen.ts index 2506e08..1095118 100644 --- a/packages/scripts/src/commands/intergen.ts +++ b/packages/scripts/src/commands/intergen.ts @@ -1,275 +1,365 @@ -import * as path from 'path' +import * as fs from 'fs' import * as ts from 'typescript' -import {readFileSync} from 'fs' +import {argparse, arg} from '@rondo/argparse' +import {error, info} from '../log' -// function processLiteral(type: ts.TypeLiteralNode): string { -// switch (type -// } - -function processTypeArguments( - typeArguments?: ts.NodeArray, -): string { - if (!typeArguments) { - return '' - } - - return '<' + typeArguments.map(processTypes).join(', ') + '>' +function isObjectType(type: ts.Type): type is ts.ObjectType { + return !!(type.flags & ts.TypeFlags.Object) } -function processTypeParameters( - typeParameters?: ts.NodeArray, -): string { - if (!typeParameters) { - return '' +function isTypeReference(type: ts.ObjectType): type is ts.TypeReference { + return !!(type.objectFlags & ts.ObjectFlags.Reference) +} + +function filterInvisibleProperties(type: ts.Symbol): boolean { + const flags = ts.getCombinedModifierFlags(type.valueDeclaration) + return !(flags & ts.ModifierFlags.NonPublicAccessibilityModifier) +} + +interface IClassProperty { + name: string + type: ts.Type + relevantTypes: ts.Type[] + typeString: string + optional: boolean +} + +interface IClassDefinition { + name: string + type: ts.Type + typeParameters: ts.TypeParameter[] + relevantTypeParameters: ts.Type[] + allRelevantTypes: ts.Type[] + properties: IClassProperty[] +} + +/* + * TODO + * + * Interfaces generated from exported class delcarations will be prefixed with + * "I". A few cases need to be differentiated: + * + * a) Private (non-exported) types / interfaces / classes defined and used in + * same module. In case of non-exported classes, an error can be thrown. + * These can be copied and perhaps indexed to prevent collisions. + * b) Referenced exported classes from the same file + * c) Referenced exported classes from a neighbouring file + * d) Referenced imported classes from external modules. Real world example: + * entities in @rondo/comments-server import and use entities from + * @rondo/comments. These types will have to be processed by this module. + * e) Referenced interfaces should be re-imported in the output file. + * + */ + +export function typecheck(...argv: string[]) { + const args = argparse({ + input: arg('string', {alias: 'i', required: true}), + debug: arg('boolean'), + help: arg('boolean', {alias: 'h'}), + output: arg('string', {default: '-'}), + }).parse(argv) + + function debug(m: string, ...meta: any[]) { + if (args.debug) { + error(m, ...meta) + } } - return '<' + typeParameters - .map(tp => ({ - name: tp.name.text, - constraint: tp.constraint - ? processTypes(tp.constraint) - : undefined, - default: tp.default - ? processTypes(tp.default) - : undefined, - })) - .map(tp => { - if (tp.constraint && tp.default) { - return `${tp.name} extends ${tp.constraint} = ${tp.default}` + + /** Generate interfaces for all exported classes in a set of .ts files */ + function classesToInterfaces( + fileNames: string[], + options: ts.CompilerOptions, + ): string[] { + // Build a program using the set of root file names in fileNames + const program = ts.createProgram(fileNames, options) + + // Get the checker, we will use it to find more about classes + const checker = program.getTypeChecker() + + const classDefs: IClassDefinition[] = [] + + function typeToString(type: ts.Type): string { + return checker.typeToString(type) } - if (tp.constraint) { - return `${tp.name} extends ${tp.constraint}` + + /** + * Can be used to filters out global types like Array or string from a list + * of types. For example: types.filter(filterGlobalTypes) + */ + function filterGlobalTypes(type: ts.Type): boolean { + debug('filterGlobalTypes: %s', typeToString(type)) + if (type.aliasSymbol) { + // keep type aliases + return true + } + const symbol = type.getSymbol() + if (!symbol) { + debug(' no symbol') + // e.g. string or number types have no symbol + return false + } + if (symbol && symbol.flags & ts.SymbolFlags.Transient) { + debug(' is transient') + // Array is transient. not sure if this is the best way to figure this + return false + } + // if (symbol && !((symbol as any).parent)) { + // // debug(' no parent', symbol) + // // e.g. Array symbol has no parent + // return false + // } + if (type.isLiteral()) { + debug(' is literal') + return false + } + if (type.isUnionOrIntersection()) { + debug(' is u or i') + return false + } + if (isObjectType(type) && isTypeReference(type)) { + debug(' is object type') + if (isObjectType(type.target) && + type.target.objectFlags & ts.ObjectFlags.Tuple) { + debug(' is tuple') + return false + } + } + + debug(' keep!') + return true } - if (tp.default) { - return `${tp.name} = ${tp.default}` + + /** + * Converts a generic type to the target of the type reference. + */ + function mapGenericTypes(type: ts.Type): ts.Type { + if (type.aliasSymbol) { + return checker.getDeclaredTypeOfSymbol(type.aliasSymbol) + } + if (isObjectType(type) && isTypeReference(type)) { + return type.target + } + return type } - return tp.name + + /** + * Removes duplicates from an array of types + */ + function filterDuplicates(type: ts.Type, i: number, arr: ts.Type[]) { + // TODO improve performance of this method + return i === arr.indexOf(type) + } + + /** + * Recursively retrieves a list of all type parameters. + */ + function getAllTypeParameters(type: ts.Type): ts.Type[] { + function collectTypeParams( + type2: ts.Type, params?: readonly ts.Type[], + ): ts.Type[] { + const types: ts.Type[] = [type2] + if (params) { + params.forEach(t => { + const atp = getAllTypeParameters(t) + types.push(...atp) + }) + } + return types + } + + if (type.aliasSymbol) { + return collectTypeParams(type, type.aliasTypeArguments) + } + if (isObjectType(type) && isTypeReference(type)) { + return collectTypeParams(type, type.typeArguments) + } + + if (type.isUnionOrIntersection()) { + return collectTypeParams(type, type.types) + } + + if (type.isClassOrInterface()) { + return collectTypeParams(type, type.typeParameters) + } + + return [type] + } + + /** + * True if this is visible outside this file, false otherwise + */ + function isNodeExported(node: ts.Node): boolean { + return ( + (ts.getCombinedModifierFlags(node as any) & + ts.ModifierFlags.Export) !== 0 || + (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) + ) + } + + function handleClassDeclaration(node: ts.ClassDeclaration) { + if (!node.name) { + return + } + // This is a top level class, get its symbol + const symbol = checker.getSymbolAtLocation(node.name) + if (!symbol) { + return + } + + const type = checker.getDeclaredTypeOfSymbol(symbol) + handleType(type) + } + + const typeDefinitions: Map = new Map() + function handleType(type: ts.Type) { + if (typeDefinitions.has(type)) { + return + } + if (type.aliasSymbol) { + throw new Error('Type aliases are not supported') + } + const typeParameters: ts.TypeParameter[] = [] + const expandedTypeParameters: ts.Type[] = [] + const allRelevantTypes: ts.Type[] = [] + + function handleTypeParameters(typeParams: readonly ts.Type[]) { + typeParams.forEach(tp => { + const constraint = tp.getConstraint() + if (constraint) { + expandedTypeParameters.push(...getAllTypeParameters(tp)) + } + const def = tp.getDefault() + if (def) { + expandedTypeParameters.push(...getAllTypeParameters(tp)) + } + + typeParameters.push(tp) + }) + } + + if (type.isClassOrInterface() && type.typeParameters) { + handleTypeParameters(type.typeParameters) + } + + if (type.aliasSymbol && type.aliasTypeArguments) { + handleTypeParameters(type.aliasTypeArguments) + } + + const properties = type.getApparentProperties() + + const filterClassTypeParameters = + (t: ts.Type) => typeParameters.every(tp => tp !== t) + + const classProperties: IClassProperty[] = properties + .filter(filterInvisibleProperties) + .map(p => { + const vd = p.valueDeclaration + const optional = ts.isPropertyDeclaration(vd) && !!vd.questionToken + + const propType = checker.getTypeOfSymbolAtLocation(p, vd) + + const typeParams = getAllTypeParameters(propType) + + const relevantTypes = typeParams + .filter(filterGlobalTypes) + .filter(filterClassTypeParameters) + .map(mapGenericTypes) + .filter(filterDuplicates) + + allRelevantTypes.push(...relevantTypes) + + return { + name: p.getName(), + type: propType, + relevantTypes, + typeString: typeToString(propType), + optional, + } + }) + + const relevantTypeParameters = expandedTypeParameters + .filter(filterGlobalTypes) + .filter(mapGenericTypes) + .filter(filterDuplicates) + + allRelevantTypes.push(...relevantTypeParameters) + + const classDef: IClassDefinition = { + name: typeToString(type), + type, + // name: symbol.getName(), + typeParameters, + allRelevantTypes: allRelevantTypes + .filter(filterClassTypeParameters) + .filter(filterDuplicates), + relevantTypeParameters, + properties: classProperties, + } + + classDefs.push(classDef) + typeDefinitions.set(type, classDef) + + classDef.allRelevantTypes.forEach(handleType) + } + + /** + * Visit nodes finding exported classes + */ + function visit(node: ts.Node) { + // Only consider exported nodes + if (!isNodeExported(node)) { + return + } + + if (ts.isClassDeclaration(node)) { + handleClassDeclaration(node) + } + } + + // Visit every sourceFile in the program + for (const sourceFile of program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile) { + // Walk the tree to search for classes + ts.forEachChild(sourceFile, visit) + } + } + + function setTypeName(type: ts.Type, mappings: Map) { + const name = typeToString(type) + mappings.set(type, `I${name}`) + } + + const nameMappings = new Map() + for (const classDef of classDefs) { + setTypeName(classDef.type, nameMappings) + for (const t of classDef.allRelevantTypes) { + setTypeName(classDef.type, nameMappings) + } + } + + function createInterface(classDef: IClassDefinition): string { + const name = nameMappings.get(classDef.type)! + const start = `interface ${name} {` + const properties = classDef.properties.map(p => { + return ` ${p.name}: ${nameMappings.get(p.type) || p.typeString}` + }) + .join('\n') + const end = '}' + return `${start}\n${properties}\n${end}` + } + + return classDefs.map(createInterface) + } + + const interfaces = classesToInterfaces([args.input], { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.CommonJS, }) - .join(', ') + '>' -} -function processLiteral( - literal: ts.BooleanLiteral | ts.LiteralExpression | ts.PrefixUnaryExpression, -) { - switch (literal.kind) { - case ts.SyntaxKind.TrueKeyword: - return '\'true\'' - case ts.SyntaxKind.FalseKeyword: - return '\'false\'' - default: - if (ts.isLiteralExpression(literal)) { - return `'${literal.text}'` - } - throw new Error('Unsupported literal type: ' + literal.kind) + const value = interfaces.join('\n\n') + if (args.output === '-') { + info(value) + } else { + fs.writeFileSync(args.output, value) } } - -function processTypes(type: ts.TypeNode): string { - switch (type.kind) { - case ts.SyntaxKind.StringKeyword: - return 'string' - case ts.SyntaxKind.BooleanKeyword: - return 'boolean' - case ts.SyntaxKind.NumberKeyword: - return 'number' - case ts.SyntaxKind.NullKeyword: - return 'null' - case ts.SyntaxKind.TrueKeyword: - return '\'true\'' - case ts.SyntaxKind.FalseKeyword: - return '\'false\'' - case ts.SyntaxKind.LiteralType: - const literalType = type as ts.LiteralTypeNode - return processLiteral(literalType.literal) - // return literalType.literal.text - case ts.SyntaxKind.TypeReference: - const typeRef = type as ts.TypeReferenceNode - typeRef.typeArguments - // FIXME do not use any - return (typeRef.typeName as any).escapedText + processTypeArguments(typeRef.typeArguments) - case ts.SyntaxKind.TypeLiteral: - const typeLiteral = type as ts.TypeLiteralNode - return '{' + processInterfaceMembers(typeLiteral.members).join('\n') + '}' - // typeLiteral.members.map(processTypes) - // console.log('aaa', JSON.stringify(typeLiteral, null, ' ')) - // return 'type literal...' - // console.log(' ', 'type literal...') - // TODO recursively iterate through typeLiteral.members - break - case ts.SyntaxKind.UnionType: - const unionType = type as ts.UnionTypeNode - const unionTypes = unionType.types.map(processTypes).join(' | ') - return unionTypes - case ts.SyntaxKind.TupleType: - const tupleTypeNode = type as ts.TupleTypeNode - const tupleTypes = tupleTypeNode.elementTypes.map(processTypes).join(', ') - return `[${tupleTypes}]` - // TODO recursively iterate through tupleTypeNode.elementTypes - break - case ts.SyntaxKind.ArrayType: - const arrayType = type as ts.ArrayTypeNode - return `Array<${processTypes(arrayType.elementType)}>` - default: - throw new Error('unhandled type: ' + type.kind) - } -} - -function processInterfaceMembers( - members: ts.NodeArray, -): string[] { - const results: string[] = [] - - for (const m of members) { - if (m.kind !== ts.SyntaxKind.PropertySignature) { - throw new Error('not implemented support for node.kind: ' + m.kind) - } - const member = m as ts.PropertySignature - const name = (m.name as ts.Identifier).escapedText - - let result = '' - let readonly = false - if (m.modifiers) { - for (const modifier of m.modifiers) { - if (modifier.kind === ts.SyntaxKind.ReadonlyKeyword) { - readonly = true - continue - } - - console.log(' modifier.kind:', modifier.kind) - } - } - - if (readonly) { - result += 'readonly ' + name - } else { - result += name - } - - if (!member.type) { - throw new Error('No member type!') - } - - if (member.questionToken) { - result += '?: ' - } else { - result += ': ' - } - - result += processTypes(member.type) - results.push(result) - } - - return results -} - -function processClassMembers( - members: ts.NodeArray, -): string[] { - const results: string[] = [] - - for (const m of members) { - if (m.kind !== ts.SyntaxKind.PropertyDeclaration) { - throw new Error('not implemented support for node.kind: ' + m.kind) - } - const member = m as ts.PropertyDeclaration - const name = (m.name as ts.Identifier).escapedText - - let result = '' - let readonly = false - let skip = false - if (m.modifiers) { - for (const modifier of m.modifiers) { - if (modifier.kind === ts.SyntaxKind.PrivateKeyword) { - skip = true - continue - } - if (modifier.kind === ts.SyntaxKind.ProtectedKeyword) { - skip = true - continue - } - if (modifier.kind === ts.SyntaxKind.ReadonlyKeyword) { - readonly = true - continue - } - - console.log(' modifier.kind:', modifier.kind) - } - } - - if (skip) { - continue - } - if (readonly) { - result += 'readonly ' + name - } else { - result += name - } - - if (!member.type) { - throw new Error('No member type!') - } - - if (member.questionToken) { - result += '?: ' - } else { - result += ': ' - } - - result += processTypes(member.type) - results.push(result) - } - - return results -} - -function delint(sourceFile: ts.SourceFile) { - delintNode(sourceFile) - - function delintNode(node: ts.Node) { - // TODO check which classes are exported - switch (node.kind) { - case ts.SyntaxKind.InterfaceDeclaration: - const interfaceDeclaration = node as ts.InterfaceDeclaration - let ifaceName = interfaceDeclaration.name.text - if (interfaceDeclaration.typeParameters) { - ifaceName += processTypeParameters(interfaceDeclaration.typeParameters) - } - console.log( - ifaceName, - processInterfaceMembers(interfaceDeclaration.members)) - break - case ts.SyntaxKind.TypeAliasDeclaration: - const typeAlias = node as ts.TypeAliasDeclaration - let taName = typeAlias.name.text - if (typeAlias.typeParameters) { - taName += processTypeParameters(typeAlias.typeParameters) - } - console.log(taName) - break - case ts.SyntaxKind.ClassDeclaration: - const cls = node as ts.ClassDeclaration - if (!cls.name) { - // TODO warn - throw new Error('no class name: ' + cls.pos) - break - } - let clsName = cls.name.text - if (cls.typeParameters) { - clsName += processTypeParameters(cls.typeParameters) - } - console.log(clsName, processClassMembers(cls.members)) - } - ts.forEachChild(node, delintNode) - } -} - -export async function intergen(argv: string[]) { - const filename = path.join(__dirname, 'intergen.sample.ts') - const sourceFile = ts.createSourceFile( - filename, - readFileSync(filename).toString(), - ts.ScriptTarget.ES2015, - false, - ) - - delint(sourceFile) -} diff --git a/packages/scripts/src/commands/typecheck.ts b/packages/scripts/src/commands/typecheck.ts deleted file mode 100644 index 1095118..0000000 --- a/packages/scripts/src/commands/typecheck.ts +++ /dev/null @@ -1,365 +0,0 @@ -import * as fs from 'fs' -import * as ts from 'typescript' -import {argparse, arg} from '@rondo/argparse' -import {error, info} from '../log' - -function isObjectType(type: ts.Type): type is ts.ObjectType { - return !!(type.flags & ts.TypeFlags.Object) -} - -function isTypeReference(type: ts.ObjectType): type is ts.TypeReference { - return !!(type.objectFlags & ts.ObjectFlags.Reference) -} - -function filterInvisibleProperties(type: ts.Symbol): boolean { - const flags = ts.getCombinedModifierFlags(type.valueDeclaration) - return !(flags & ts.ModifierFlags.NonPublicAccessibilityModifier) -} - -interface IClassProperty { - name: string - type: ts.Type - relevantTypes: ts.Type[] - typeString: string - optional: boolean -} - -interface IClassDefinition { - name: string - type: ts.Type - typeParameters: ts.TypeParameter[] - relevantTypeParameters: ts.Type[] - allRelevantTypes: ts.Type[] - properties: IClassProperty[] -} - -/* - * TODO - * - * Interfaces generated from exported class delcarations will be prefixed with - * "I". A few cases need to be differentiated: - * - * a) Private (non-exported) types / interfaces / classes defined and used in - * same module. In case of non-exported classes, an error can be thrown. - * These can be copied and perhaps indexed to prevent collisions. - * b) Referenced exported classes from the same file - * c) Referenced exported classes from a neighbouring file - * d) Referenced imported classes from external modules. Real world example: - * entities in @rondo/comments-server import and use entities from - * @rondo/comments. These types will have to be processed by this module. - * e) Referenced interfaces should be re-imported in the output file. - * - */ - -export function typecheck(...argv: string[]) { - const args = argparse({ - input: arg('string', {alias: 'i', required: true}), - debug: arg('boolean'), - help: arg('boolean', {alias: 'h'}), - output: arg('string', {default: '-'}), - }).parse(argv) - - function debug(m: string, ...meta: any[]) { - if (args.debug) { - error(m, ...meta) - } - } - - /** Generate interfaces for all exported classes in a set of .ts files */ - function classesToInterfaces( - fileNames: string[], - options: ts.CompilerOptions, - ): string[] { - // Build a program using the set of root file names in fileNames - const program = ts.createProgram(fileNames, options) - - // Get the checker, we will use it to find more about classes - const checker = program.getTypeChecker() - - const classDefs: IClassDefinition[] = [] - - function typeToString(type: ts.Type): string { - return checker.typeToString(type) - } - - /** - * Can be used to filters out global types like Array or string from a list - * of types. For example: types.filter(filterGlobalTypes) - */ - function filterGlobalTypes(type: ts.Type): boolean { - debug('filterGlobalTypes: %s', typeToString(type)) - if (type.aliasSymbol) { - // keep type aliases - return true - } - const symbol = type.getSymbol() - if (!symbol) { - debug(' no symbol') - // e.g. string or number types have no symbol - return false - } - if (symbol && symbol.flags & ts.SymbolFlags.Transient) { - debug(' is transient') - // Array is transient. not sure if this is the best way to figure this - return false - } - // if (symbol && !((symbol as any).parent)) { - // // debug(' no parent', symbol) - // // e.g. Array symbol has no parent - // return false - // } - if (type.isLiteral()) { - debug(' is literal') - return false - } - if (type.isUnionOrIntersection()) { - debug(' is u or i') - return false - } - if (isObjectType(type) && isTypeReference(type)) { - debug(' is object type') - if (isObjectType(type.target) && - type.target.objectFlags & ts.ObjectFlags.Tuple) { - debug(' is tuple') - return false - } - } - - debug(' keep!') - return true - } - - /** - * Converts a generic type to the target of the type reference. - */ - function mapGenericTypes(type: ts.Type): ts.Type { - if (type.aliasSymbol) { - return checker.getDeclaredTypeOfSymbol(type.aliasSymbol) - } - if (isObjectType(type) && isTypeReference(type)) { - return type.target - } - return type - } - - /** - * Removes duplicates from an array of types - */ - function filterDuplicates(type: ts.Type, i: number, arr: ts.Type[]) { - // TODO improve performance of this method - return i === arr.indexOf(type) - } - - /** - * Recursively retrieves a list of all type parameters. - */ - function getAllTypeParameters(type: ts.Type): ts.Type[] { - function collectTypeParams( - type2: ts.Type, params?: readonly ts.Type[], - ): ts.Type[] { - const types: ts.Type[] = [type2] - if (params) { - params.forEach(t => { - const atp = getAllTypeParameters(t) - types.push(...atp) - }) - } - return types - } - - if (type.aliasSymbol) { - return collectTypeParams(type, type.aliasTypeArguments) - } - if (isObjectType(type) && isTypeReference(type)) { - return collectTypeParams(type, type.typeArguments) - } - - if (type.isUnionOrIntersection()) { - return collectTypeParams(type, type.types) - } - - if (type.isClassOrInterface()) { - return collectTypeParams(type, type.typeParameters) - } - - return [type] - } - - /** - * True if this is visible outside this file, false otherwise - */ - function isNodeExported(node: ts.Node): boolean { - return ( - (ts.getCombinedModifierFlags(node as any) & - ts.ModifierFlags.Export) !== 0 || - (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) - ) - } - - function handleClassDeclaration(node: ts.ClassDeclaration) { - if (!node.name) { - return - } - // This is a top level class, get its symbol - const symbol = checker.getSymbolAtLocation(node.name) - if (!symbol) { - return - } - - const type = checker.getDeclaredTypeOfSymbol(symbol) - handleType(type) - } - - const typeDefinitions: Map = new Map() - function handleType(type: ts.Type) { - if (typeDefinitions.has(type)) { - return - } - if (type.aliasSymbol) { - throw new Error('Type aliases are not supported') - } - const typeParameters: ts.TypeParameter[] = [] - const expandedTypeParameters: ts.Type[] = [] - const allRelevantTypes: ts.Type[] = [] - - function handleTypeParameters(typeParams: readonly ts.Type[]) { - typeParams.forEach(tp => { - const constraint = tp.getConstraint() - if (constraint) { - expandedTypeParameters.push(...getAllTypeParameters(tp)) - } - const def = tp.getDefault() - if (def) { - expandedTypeParameters.push(...getAllTypeParameters(tp)) - } - - typeParameters.push(tp) - }) - } - - if (type.isClassOrInterface() && type.typeParameters) { - handleTypeParameters(type.typeParameters) - } - - if (type.aliasSymbol && type.aliasTypeArguments) { - handleTypeParameters(type.aliasTypeArguments) - } - - const properties = type.getApparentProperties() - - const filterClassTypeParameters = - (t: ts.Type) => typeParameters.every(tp => tp !== t) - - const classProperties: IClassProperty[] = properties - .filter(filterInvisibleProperties) - .map(p => { - const vd = p.valueDeclaration - const optional = ts.isPropertyDeclaration(vd) && !!vd.questionToken - - const propType = checker.getTypeOfSymbolAtLocation(p, vd) - - const typeParams = getAllTypeParameters(propType) - - const relevantTypes = typeParams - .filter(filterGlobalTypes) - .filter(filterClassTypeParameters) - .map(mapGenericTypes) - .filter(filterDuplicates) - - allRelevantTypes.push(...relevantTypes) - - return { - name: p.getName(), - type: propType, - relevantTypes, - typeString: typeToString(propType), - optional, - } - }) - - const relevantTypeParameters = expandedTypeParameters - .filter(filterGlobalTypes) - .filter(mapGenericTypes) - .filter(filterDuplicates) - - allRelevantTypes.push(...relevantTypeParameters) - - const classDef: IClassDefinition = { - name: typeToString(type), - type, - // name: symbol.getName(), - typeParameters, - allRelevantTypes: allRelevantTypes - .filter(filterClassTypeParameters) - .filter(filterDuplicates), - relevantTypeParameters, - properties: classProperties, - } - - classDefs.push(classDef) - typeDefinitions.set(type, classDef) - - classDef.allRelevantTypes.forEach(handleType) - } - - /** - * Visit nodes finding exported classes - */ - function visit(node: ts.Node) { - // Only consider exported nodes - if (!isNodeExported(node)) { - return - } - - if (ts.isClassDeclaration(node)) { - handleClassDeclaration(node) - } - } - - // Visit every sourceFile in the program - for (const sourceFile of program.getSourceFiles()) { - if (!sourceFile.isDeclarationFile) { - // Walk the tree to search for classes - ts.forEachChild(sourceFile, visit) - } - } - - function setTypeName(type: ts.Type, mappings: Map) { - const name = typeToString(type) - mappings.set(type, `I${name}`) - } - - const nameMappings = new Map() - for (const classDef of classDefs) { - setTypeName(classDef.type, nameMappings) - for (const t of classDef.allRelevantTypes) { - setTypeName(classDef.type, nameMappings) - } - } - - function createInterface(classDef: IClassDefinition): string { - const name = nameMappings.get(classDef.type)! - const start = `interface ${name} {` - const properties = classDef.properties.map(p => { - return ` ${p.name}: ${nameMappings.get(p.type) || p.typeString}` - }) - .join('\n') - const end = '}' - return `${start}\n${properties}\n${end}` - } - - return classDefs.map(createInterface) - } - - const interfaces = classesToInterfaces([args.input], { - target: ts.ScriptTarget.ES5, - module: ts.ModuleKind.CommonJS, - }) - - const value = interfaces.join('\n\n') - if (args.output === '-') { - info(value) - } else { - fs.writeFileSync(args.output, value) - } -}