Rename typecheck.ts to intergen.ts

This commit is contained in:
Jerko Steiner 2019-08-14 14:17:02 +07:00
parent cca888641a
commit 25593dd994
2 changed files with 353 additions and 628 deletions

View File

@ -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<ts.TypeNode>,
): 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<ts.TypeParameterDeclaration>,
): 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<ts.Type, IClassDefinition> = 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<ts.Type, string>) {
const name = typeToString(type)
mappings.set(type, `I${name}`)
}
const nameMappings = new Map<ts.Type, string>()
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<ts.TypeElement>,
): 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<ts.ClassElement>,
): 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)
}

View File

@ -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<ts.Type, IClassDefinition> = 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<ts.Type, string>) {
const name = typeToString(type)
mappings.set(type, `I${name}`)
}
const nameMappings = new Map<ts.Type, string>()
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)
}
}