math5/tools/ts2json.mjs

235 lines
8.4 KiB
JavaScript

import * as ts from 'typescript'
const intrinsicTypeKeywords = new Set([
ts.SyntaxKind.AnyKeyword,
ts.SyntaxKind.BooleanKeyword,
ts.SyntaxKind.NeverKeyword,
ts.SyntaxKind.NumberKeyword,
ts.SyntaxKind.StringKeyword,
ts.SyntaxKind.UndefinedKeyword,
ts.SyntaxKind.UnknownKeyword,
ts.SyntaxKind.VoidKeyword
])
const typeOperatorKeywords = new Map([
[ts.SyntaxKind.KeyofKeyword, '_keyof'],
[ts.SyntaxKind.ReadonlyKeyword, '_readonly'],
[ts.SyntaxKind.UniqueKeyword, '_unique'],
])
class TSNode {
constructor(name, type) {
this.children = []
this.addChild = (name, type) => {
let node = new TSNode(name, type)
this.children.push(node)
return node
}
this.getType = () => this.type
this.getObject = () => {
let map = {}
map[this.name] = this.children.length
? this.children
.map(child => child.getObject())
.reduce((pv, child) => {
for (let key in child) {
if (pv.hasOwnProperty(key) || key in pv) {
if (child[key]) {
Object.assign(pv[key], child[key])
}
} else {
pv[key] = child[key]
}
}
return pv
}, {})
: this.type
return map
};
this.name = name
this.type = type
}
}
function parameterTypeStructure(paramNode, checker) {
let typeStruc = typeStructure(paramNode.type, checker)
if (paramNode.questionToken) typeStruc = {_optional: typeStruc}
return typeStruc
}
function typeStructure(typeNode, checker) {
switch (typeNode.kind) {
case ts.SyntaxKind.UnionType:
return {
_union: typeNode.types.map(t => typeStructure(t, checker))
}
case ts.SyntaxKind.IntersectionType:
return {
_intersection: typeNode.types.map(t => typeStructure(t, checker))
}
case ts.SyntaxKind.ImportType: {
const typeStruc = {
_importedFrom: typeNode.argument.literal.text,
_name: typeNode.qualifier.text
}
if (typeNode.typeArguments) {
typeStruc._typeArguments = typeNode.typeArguments.map(
p => typeStructure(p, checker))
}
return typeStruc
}
case ts.SyntaxKind.ArrayType:
return {_array: typeStructure(typeNode.elementType, checker)}
case ts.SyntaxKind.TypeLiteral: // Seems to be plain object types
return Object.fromEntries(typeNode.members.map(
mem => [mem.name.text, typeStructure(mem.type, checker)]))
case ts.SyntaxKind.FunctionType: {
const typeStruc = {
_parameters: typeNode.parameters.map(
p => parameterTypeStructure(p, checker)),
_returns: typeStructure(typeNode.type, checker)
}
if (typeNode.typeParameters) {
typeStruc._typeParameters = typeNode.typeParameters.map(
p => p.name.text)
}
return typeStruc
}
case ts.SyntaxKind.IndexedAccessType:
return {
_ofType: typeStructure(typeNode.objectType, checker),
_index: typeStructure(typeNode.indexType, checker)
}
case ts.SyntaxKind.TypeOperator:
const key = typeOperatorKeywords.get(
typeNode.operator,
'_unidentified_operator'
)
return {
[key]:
typeStructure(typeNode.type, checker)
}
case ts.SyntaxKind.ConditionalType:
return {
_subtype: typeStructure(typeNode.checkType, checker),
_basetype: typeStructure(typeNode.extendsType, checker),
_truetype: typeStructure(typeNode.trueType, checker),
_falsetype: typeStructure(typeNode.falseType, checker),
}
case ts.SyntaxKind.TypeReference: {
const typeStruc = {_typeParameter: typeNode.typeName.text}
if (typeNode.typeArguments) {
typeStruc._typeArguments = typeNode.typeArguments.map(
arg => typeStructure(arg, checker))
}
return typeStruc
}
case ts.SyntaxKind.TypePredicate:
return {_is: typeStructure(typeNode.type, checker)}
case ts.SyntaxKind.LiteralType:
return checker.typeToString(checker.getTypeFromTypeNode(typeNode))
default:
if (intrinsicTypeKeywords.has(typeNode.kind)) {
return checker.getTypeFromTypeNode(typeNode).intrinsicName
}
}
throw new Error(`Unhandled type node ${ts.SyntaxKind[typeNode.kind]}`)
}
const visit = (parent, checker) => node => {
switch (node.kind) {
// Currently, we are ignoring the following sorts of statements
// that may appear in .d.ts files. We may need to revisit these,
// especially the InterfaceDeclaration and TypeAliasDeclaration,
// if we want to generate runtime information on pure type
// declarations. I think this may be necessary for example to compute
// the "RealType" of a type at runtime.
case ts.SyntaxKind.EndOfFileToken:
case ts.SyntaxKind.ExportDeclaration:
case ts.SyntaxKind.ImportDeclaration:
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.TypeAliasDeclaration:
break
case ts.SyntaxKind.VariableStatement:
node.declarationList.declarations.forEach(visit(parent, checker))
break
case ts.SyntaxKind.VariableDeclaration: {
const typeStruc = typeStructure(node.type, checker)
parent.addChild(node.name.text, typeStruc)
break
}
case ts.SyntaxKind.FunctionDeclaration: {
const typeStruc = {
_parameters: node.parameters.map(
p => parameterTypeStructure(p, checker)),
_returns: typeStructure(node.type, checker)
}
if (node.typeParameters) {
typeStruc._typeParameters = node.typeParameters.map(
p => p.name.text)
}
parent.addChild(node.name.text, typeStruc)
break
}
case ts.SyntaxKind.ModuleDeclaration:
let moduleName = node.name.text
visit(parent.addChild(moduleName), checker)(node.body)
break
case ts.SyntaxKind.ModuleBlock:
ts.forEachChild(node, visit(parent, checker));
break
case ts.SyntaxKind.PropertySignature:
let propertyName = node.name
let propertyType = node.type
let arrayDeep = 0
let realPropertyName =
'string' !== typeof propertyName && 'text' in propertyName
? propertyName.text
: propertyName
console.log('Property', realPropertyName)
while (propertyType.kind === ts.SyntaxKind.ArrayType) {
arrayDeep++
propertyType = propertyType.elementType
}
if (propertyType.kind === ts.SyntaxKind.TypeReference) {
let realPropertyType = propertyType.typeName
parent.addChild(
realPropertyName,
'Array<'.repeat(arrayDeep) +
(realPropertyType.kind === ts.SyntaxKind.QualifiedName
? realPropertyType.getText()
: 'text' in realPropertyType
? realPropertyType.text
: realPropertyType) +
'>'.repeat(arrayDeep)
)
} else {
if (intrinsicTypeKeywords.has(propertyType.kind)) {
parent.addChild(
realPropertyName,
checker.getTypeFromTypeNode(propertyType).intrinsicName)
}
}
break
default:
console.warn(
'Unhandled node kind',
node.kind, ts.SyntaxKind[node.kind])
}
}
export default function(filename, options = {}) {
const ROOT_NAME = 'root'
const node = new TSNode(ROOT_NAME)
let program = ts.createProgram([filename], options)
let checker = program.getTypeChecker()
const sourceFiles = program.getSourceFiles()
const filenameUnix = filename.replaceAll('\\', '/')
let sourceFile = sourceFiles.find(file => file.fileName === filenameUnix)
ts.forEachChild(sourceFile, visit(node, checker))
return node.getObject()[ROOT_NAME]
}