feat: Try two different kinds of type reflection (#5)
Co-authored-by: Jos de Jong <wjosdejong@gmail.com> Reviewed-on: #5 Co-authored-by: Glen Whitney <glen@studioinfinity.org> Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
parent
239215c234
commit
59c325ff6c
7 changed files with 618 additions and 14 deletions
62
tools/reflectTypes.mjs
Normal file
62
tools/reflectTypes.mjs
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { readFileSync, writeFileSync, readdirSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join, relative } from 'node:path'
|
||||
|
||||
import ts2json from './ts2json.mjs'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const buildDir = join(__dirname, '..', 'build')
|
||||
const files = (await readdirSync(buildDir, { recursive: true }))
|
||||
.filter(file => file.endsWith('.js'))
|
||||
.map(file => join(buildDir, file))
|
||||
|
||||
for (const file of files) {
|
||||
reflectType5(file, { debug: true })
|
||||
}
|
||||
|
||||
function reflectType5(srcFile, options = { debug: false }) {
|
||||
log(`=========Reflecting file "${relative(__dirname, srcFile)}"`)
|
||||
|
||||
const defFile = srcFile.replace(/.js$/, '.d.ts')
|
||||
const src = String(readFileSync(srcFile))
|
||||
const defs = String(readFileSync(defFile))
|
||||
const parsedDefs = ts2json(defFile)
|
||||
|
||||
const typeDefMatches = defs.matchAll(/: ({(?:(?!\n}).)+\n}) & (?:(?!ReflectedTypeInfo).)+ReflectedTypeInfo/gs)
|
||||
if (!typeDefMatches) {
|
||||
log('No ReflectedTypeInfo found.')
|
||||
return
|
||||
}
|
||||
|
||||
const typeDefs = Array.from(typeDefMatches).map(def => def[1])
|
||||
log(` ${typeDefs.length} ReflectedTypeInfo found`)
|
||||
|
||||
let index = 0
|
||||
let srcReflected = src.replaceAll(/(\s*)\.ship\(\)/g, () => {
|
||||
const def = typeDefs[index]
|
||||
index++
|
||||
return `.ship({ reflectedType5: \`${def}\` })`
|
||||
})
|
||||
log(` ReflectedTypeInfo injected in ${index} occurrences of .ship()`)
|
||||
|
||||
if (index !== typeDefs.length) {
|
||||
log(' WARNING: not all ReflectedTypeInfo occurrences could be injected')
|
||||
}
|
||||
|
||||
for (const id in parsedDefs) {
|
||||
if (id.includes('interface')) continue
|
||||
if (parsedDefs[id] === undefined) continue
|
||||
log(` Tagging ${id} with type data`, parsedDefs[id])
|
||||
srcReflected +=
|
||||
`\n${id}._reflectedType5 = ${JSON.stringify(parsedDefs[id])}\n`
|
||||
}
|
||||
writeFileSync(srcFile, srcReflected)
|
||||
|
||||
function log(...args) {
|
||||
if (options.debug) {
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
}
|
233
tools/ts2json.mjs
Normal file
233
tools/ts2json.mjs
Normal file
|
@ -0,0 +1,233 @@
|
|||
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()
|
||||
let sourceFile = sourceFiles.find(file => file.fileName === filename)
|
||||
|
||||
ts.forEachChild(sourceFile, visit(node, checker))
|
||||
|
||||
return node.getObject()[ROOT_NAME]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue