2018-08-05 16:34:36 +02:00
import * as ts from "typescript"
import * as fs from "fs"
import * as commandLineArgs from "command-line-args"
2018-09-22 12:34:26 +02:00
import * as AllTypes from "./types/AllTypes"
import * as AllMetadata from "./metadata/AllMetadata"
export function toSimpleType ( typeNode ) {
switch ( typeNode . kind ) {
case ts . SyntaxKind . VoidKeyword :
return new AllTypes . VoidType ( )
2018-11-20 19:51:46 +01:00
// IndexedAccessTypes are things like `fn<T keyof Class>(x: T, y: Class[T])`
// This doesn't seem to be easy to deal with so let's kludge it for now
case ts . SyntaxKind . IndexedAccessType :
2019-05-30 15:35:50 +02:00
// Unknown is just like any, but slightly stricter
case ts . SyntaxKind . UnknownKeyword :
2018-09-22 12:34:26 +02:00
case ts . SyntaxKind . AnyKeyword :
return new AllTypes . AnyType ( )
case ts . SyntaxKind . BooleanKeyword :
return new AllTypes . BooleanType ( )
case ts . SyntaxKind . NumberKeyword :
return new AllTypes . NumberType ( )
case ts . SyntaxKind . ObjectKeyword :
return new AllTypes . ObjectType ( )
case ts . SyntaxKind . StringKeyword :
return new AllTypes . StringType ( )
case ts . SyntaxKind . Parameter :
let n = toSimpleType ( typeNode . type )
n . name = typeNode . name . original . escapedText
2019-05-31 13:25:51 +02:00
n . isDotDotDot = ! ! typeNode . dotDotDotToken
n . isQuestion = ! ! typeNode . questionToken
2018-09-22 12:34:26 +02:00
return n
case ts . SyntaxKind . TypeReference :
2018-11-04 17:20:01 +01:00
if ( ! typeNode . typeArguments ) {
// If there are no typeArguments, this is not a parametric type and we can return the type directly
try {
2018-11-20 19:51:46 +01:00
return toSimpleType (
typeNode . typeName . symbol . declarations [ 0 ] . type ,
)
2018-11-04 17:20:01 +01:00
} catch ( e ) {
// Fall back to what you'd do with typeArguments
}
}
2018-09-22 12:34:26 +02:00
let args = typeNode . typeArguments
? typeNode . typeArguments . map ( t = >
toSimpleType ( typeNode . typeArguments [ 0 ] ) ,
)
: [ ]
return new AllTypes . TypeReferenceType (
typeNode . typeName . escapedText ,
args ,
)
case ts . SyntaxKind . FunctionType :
// generics = (typeNode.typeParameters || []).map(p => new AllTypes.SimpleType(p))
return new AllTypes . FunctionType (
typeNode . parameters . map ( p = > toSimpleType ( p ) ) ,
toSimpleType ( typeNode . type ) ,
)
case ts . SyntaxKind . TypeLiteral :
2018-11-20 19:51:46 +01:00
let members = typeNode . members
. map ( member = > {
if ( member . kind == ts . SyntaxKind . IndexSignature ) {
// Something like this: { [str: string]: string[] }
return [ "" , toSimpleType ( member . type ) ]
}
// Very fun feature: when you have an object literal with >20 members, typescript will decide to replace some of them with a "... X more ..." node that obviously doesn't have a corresponding symbol, hence this check and the filter after the map
if ( member . name . symbol )
return [
member . name . symbol . escapedName ,
toSimpleType ( member . type ) ,
]
} )
. filter ( m = > m )
return new AllTypes . ObjectType ( new Map ( members ) )
2018-09-22 12:34:26 +02:00
case ts . SyntaxKind . ArrayType :
return new AllTypes . ArrayType ( toSimpleType ( typeNode . elementType ) )
case ts . SyntaxKind . TupleType :
return new AllTypes . TupleType (
typeNode . elementTypes . map ( t = > toSimpleType ( t ) ) ,
)
case ts . SyntaxKind . UnionType :
return new AllTypes . UnionType (
typeNode . types . map ( t = > toSimpleType ( t ) ) ,
)
break
case ts . SyntaxKind . LiteralType :
return new AllTypes . LiteralTypeType ( typeNode . literal . text )
break
default :
console . log ( typeNode )
throw new Error ( ` Unhandled kind ( ${ typeNode . kind } ) for ${ typeNode } ` )
2018-08-20 17:31:23 +02:00
}
}
2018-09-22 12:34:26 +02:00
/** True if node is visible outside its file, false otherwise */
2018-08-05 16:34:36 +02:00
function isNodeExported ( node : ts.Node ) : boolean {
return (
2018-08-19 12:08:49 +00:00
( ts . getCombinedModifierFlags ( < ts.Declaration > node ) &
ts . ModifierFlags . Export ) !==
0 ||
2018-08-05 16:34:36 +02:00
( ! ! node . parent && node . parent . kind === ts . SyntaxKind . SourceFile )
)
}
2018-11-07 08:12:53 +01:00
/** True if node is marked as @hidden in its documentation */
function isNodeHidden ( sourceFile , node ) : boolean {
2018-11-20 19:51:46 +01:00
return (
sourceFile &&
node . jsDoc &&
! ! node . jsDoc . find (
doc = >
sourceFile . text . slice ( doc . pos , doc . end ) . search ( "@hidden" ) != - 1 ,
)
)
2018-11-07 08:12:53 +01:00
}
2018-11-20 19:51:46 +01:00
function visit (
checker : any ,
sourceFile : any ,
file : AllMetadata.FileMetadata ,
node : any ,
) {
2018-09-01 21:51:08 +02:00
let symbol = checker . getSymbolAtLocation ( node . name )
if ( symbol && isNodeExported ( node ) ) {
let nodeName = symbol . escapedName
2018-08-05 16:34:36 +02:00
2018-09-01 21:51:08 +02:00
switch ( node . kind ) {
case ts . SyntaxKind . FunctionDeclaration :
2018-09-22 12:34:26 +02:00
// Grab the doc, default to empty string
let doc =
ts . displayPartsToString ( symbol . getDocumentationComment ( ) ) ||
""
// Grab the type
let ttype = checker . getTypeOfSymbolAtLocation (
symbol ,
symbol . valueDeclaration ! ,
)
// If the function has a type, try to convert it, if it doesn't, default to any
let t = ttype
? toSimpleType ( checker . typeToTypeNode ( ttype ) )
: new AllTypes . AnyType ( )
file . setFunction (
nodeName ,
2018-11-20 19:51:46 +01:00
new AllMetadata . SymbolMetadata (
doc ,
t ,
isNodeHidden ( sourceFile , node ) ,
) ,
2018-09-22 12:34:26 +02:00
)
return
2018-09-01 21:51:08 +02:00
case ts . SyntaxKind . ClassDeclaration :
2018-09-22 12:34:26 +02:00
let clazz = file . getClass ( nodeName )
if ( ! clazz ) {
clazz = new AllMetadata . ClassMetadata ( )
file . setClass ( nodeName , clazz )
}
2018-09-01 21:51:08 +02:00
symbol . members . forEach ( ( sym , name , map ) = > {
// Can't get doc/type from these special functions
// Or at least, it requires work that might not be needed for now
if ( [ "__constructor" , "get" , "set" ] . includes ( name ) ) return
2018-09-22 12:34:26 +02:00
// Grab the doc, default to empty string
let doc =
ts . displayPartsToString (
sym . getDocumentationComment ( ) ,
) || ""
// Grab the type
let ttype = checker . getTypeOfSymbolAtLocation (
sym ,
sym . valueDeclaration ! ,
)
// If the function has a type, try to convert it, if it doesn't, default to any
let t = ttype
? toSimpleType ( checker . typeToTypeNode ( ttype ) )
: new AllTypes . AnyType ( )
clazz . setMember (
name ,
2018-11-20 19:51:46 +01:00
new AllMetadata . SymbolMetadata (
doc ,
t ,
isNodeHidden ( sourceFile , node ) ,
) ,
2018-09-22 12:34:26 +02:00
)
2018-09-01 21:51:08 +02:00
} )
2018-09-22 12:34:26 +02:00
return
2018-09-01 21:51:08 +02:00
// Other declaration syntaxkinds:
// case ts.SyntaxKind.VariableDeclaration:
// case ts.SyntaxKind.VariableDeclarationList:
// case ts.SyntaxKind.PropertyDeclaration:
// case ts.SyntaxKind.MethodDeclaration:
// case ts.SyntaxKind.EndOfDeclarationMarker:
// case ts.SyntaxKind.MergeDeclarationMarker:
// case ts.SyntaxKind.MissingDeclaration:
// case ts.SyntaxKind.ClassExpression:
// case ts.SyntaxKind.InterfaceDeclaration:
// case ts.SyntaxKind.TypeAliasDeclaration:
// case ts.SyntaxKind.EnumDeclaration:
// case ts.SyntaxKind.ModuleDeclaration:
// case ts.SyntaxKind.ImportEqualsDeclaration:
// case ts.SyntaxKind.ImportDeclaration:
// case ts.SyntaxKind.NamespaceExportDeclaration:
// case ts.SyntaxKind.ExportDeclaration:
// case ts.SyntaxKind.Constructor:
}
2018-08-05 16:34:36 +02:00
}
2018-11-07 08:12:53 +01:00
ts . forEachChild ( node , node = > visit ( checker , sourceFile , file , node ) )
2018-08-05 16:34:36 +02:00
}
2018-08-16 20:53:10 +02:00
function generateMetadata (
out : string ,
themedir : string ,
fileNames : string [ ] ,
) : void {
/* Parse Tridactyl */
2018-08-05 16:34:36 +02:00
let program = ts . createProgram ( fileNames , {
target : ts.ScriptTarget.ES5 ,
module : ts.ModuleKind.CommonJS ,
} )
2018-09-22 12:34:26 +02:00
let metadata = new AllMetadata . ProgramMetadata ( )
2018-08-05 16:34:36 +02:00
for ( const sourceFile of program . getSourceFiles ( ) ) {
2018-09-22 12:34:26 +02:00
let name = ( fileNames as any ) . find ( name = >
sourceFile . fileName . match ( name ) ,
)
if ( name ) {
let file = metadata . getFile ( name )
if ( ! file ) {
file = new AllMetadata . FileMetadata ( )
metadata . setFile ( name , file )
}
2018-11-07 08:12:53 +01:00
visit ( program . getTypeChecker ( ) , sourceFile , file , sourceFile )
2018-09-22 12:34:26 +02:00
}
2018-08-05 16:34:36 +02:00
}
2018-11-04 17:20:01 +01:00
// We need to specify Type itself because it won't exist in AllTypes.js since it's an interface
2018-11-20 19:51:46 +01:00
let imports =
` import { Type } from "../compiler/types/AllTypes" \ n ` +
2018-09-22 12:34:26 +02:00
` import { ${ Object . keys ( AllTypes ) . join (
", " ,
) } } from "../compiler/types/AllTypes" \ n ` +
` import { ${ Object . keys ( AllMetadata ) . join (
", " ,
) } } from "../compiler/metadata/AllMetadata" \ n `
let metadataString =
imports + ` \ nexport let everything = ${ metadata . toConstructor ( ) } \ n `
2018-08-16 20:53:10 +02:00
if ( themedir ) {
metadataString += ` \ nexport let staticThemes = ${ JSON . stringify (
fs . readdirSync ( themedir ) ,
) } \ n `
}
2018-08-05 16:34:36 +02:00
// print out the doc
2018-08-16 20:53:10 +02:00
fs . writeFileSync ( out , metadataString )
2018-08-05 16:34:36 +02:00
return
}
let opts = commandLineArgs ( [
{ name : "out" , type : String } ,
2018-08-16 20:53:10 +02:00
{ name : "themeDir" , type : String } ,
2018-08-05 16:34:36 +02:00
{ name : "src" , type : String , multiple : true , defaultOption : true } ,
] )
2018-08-16 20:53:10 +02:00
if ( ! opts . out || opts . src . length < 1 )
2018-08-05 16:34:36 +02:00
throw new Error (
"Argument syntax: --out outfile [--src] file1.ts [file2.ts ...]" ,
)
2018-08-16 20:53:10 +02:00
generateMetadata ( opts . out , opts . themeDir , opts . src )