feat: Inject types as structures, not just as strings

This commit is contained in:
Glen Whitney 2024-10-15 05:30:32 -07:00
parent b9675c6d59
commit d5b4ccfc2a
3 changed files with 173 additions and 3 deletions

View File

@ -2,4 +2,4 @@ import {inspect} from 'node:util'
import * as specifications from './all' import * as specifications from './all'
console.log(inspect(specifications, {depth: 8, colors: true})) console.log(inspect(specifications, {depth: 10, colors: true}))

View File

@ -2,6 +2,8 @@ import { readFileSync, writeFileSync, readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { dirname, join, relative } from 'node:path' import { dirname, join, relative } from 'node:path'
import ts2json from './ts2json.mjs'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename) const __dirname = dirname(__filename)
@ -15,11 +17,13 @@ for (const file of files) {
} }
function reflectType5(srcFile, options = { debug: false }) { function reflectType5(srcFile, options = { debug: false }) {
log(`Reflecting file "${relative(__dirname, srcFile)}"`) log(`=========Reflecting file "${relative(__dirname, srcFile)}"`)
const defFile = srcFile.replace(/.js$/, '.d.ts') const defFile = srcFile.replace(/.js$/, '.d.ts')
const src = String(readFileSync(srcFile)) const src = String(readFileSync(srcFile))
const defs = String(readFileSync(defFile)) const defs = String(readFileSync(defFile))
const parsedDefs = ts2json(defFile)
console.log('Defs from', defFile, 'are', parsedDefs)
const typeDefMatches = defs.matchAll(/: ({(?:(?!\n}).)+\n}) & (?:(?!ReflectedTypeInfo).)+ReflectedTypeInfo/gs) const typeDefMatches = defs.matchAll(/: ({(?:(?!\n}).)+\n}) & (?:(?!ReflectedTypeInfo).)+ReflectedTypeInfo/gs)
if (!typeDefMatches) { if (!typeDefMatches) {
@ -31,7 +35,7 @@ function reflectType5(srcFile, options = { debug: false }) {
log(` ${typeDefs.length} ReflectedTypeInfo found`) log(` ${typeDefs.length} ReflectedTypeInfo found`)
let index = 0 let index = 0
const srcReflected = src.replaceAll(/(\s*)\.ship\(\)/g, () => { let srcReflected = src.replaceAll(/(\s*)\.ship\(\)/g, () => {
const def = typeDefs[index] const def = typeDefs[index]
index++ index++
return `.ship({ reflectedType5: \`${def}\` })` return `.ship({ reflectedType5: \`${def}\` })`
@ -42,6 +46,11 @@ function reflectType5(srcFile, options = { debug: false }) {
log(' WARNING: not all ReflectedTypeInfo occurrences could be injected') log(' WARNING: not all ReflectedTypeInfo occurrences could be injected')
} }
for (const id in parsedDefs) {
if (id.includes('interface')) continue
srcReflected +=
`\n${id}.reflectedType6 = ${JSON.stringify(parsedDefs[id])}\n`
}
writeFileSync(srcFile, srcReflected) writeFileSync(srcFile, srcReflected)
function log(...args) { function log(...args) {

161
tools/ts2json.mjs Normal file
View File

@ -0,0 +1,161 @@
import * as ts from 'typescript'
const PROPERTY_TYPES = {
any: ts.SyntaxKind.AnyKeyword,
boolean: ts.SyntaxKind.BooleanKeyword,
number: ts.SyntaxKind.NumberKeyword,
string: ts.SyntaxKind.StringKeyword,
}
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 typeStructure(typeNode, checker) {
switch (typeNode.kind) {
case ts.SyntaxKind.IntersectionType:
return {
_intersection: typeNode.types.map(t => typeStructure(t, checker))
}
case ts.SyntaxKind.ImportType:
return {
_importedFrom: typeNode.argument.literal.text,
_name: typeNode.qualifier.text
}
case ts.SyntaxKind.TypeLiteral:
return Object.fromEntries(typeNode.members.map(
mem => [mem.name.text, typeStructure(mem.type, checker)]))
case ts.SyntaxKind.FunctionType:
return {
_parameters: typeNode.parameters.map(
p => typeStructure(p.type, checker)),
_returns: typeStructure(typeNode.type, checker)
}
case ts.SyntaxKind.LiteralType:
return checker.typeToString(checker.getTypeFromTypeNode(typeNode))
default:
console.log('I See', ts.SyntaxKind[typeNode.kind])
}
return checker.getTypeFromTypeNode(typeNode).intrinsicName
}
const visit = (parent, checker) => node => {
switch (node.kind) {
case ts.SyntaxKind.ImportDeclaration:
console.log('Skipping import from', node.moduleSpecifier.text)
break
case ts.SyntaxKind.VariableStatement:
console.log('Processing variable',
ts.SyntaxKind[node.declarationList.kind])
node.declarationList.declarations.forEach(visit(parent, checker))
console.log('Done with variable')
break
case ts.SyntaxKind.VariableDeclaration:
console.log('Processing var dec', node.name.text, 'in', parent)
const typeStruc = typeStructure(node.type, checker)
console.log('Type is', typeStructure(node.type, checker))
if ('_intersection' in typeStruc) {
console.log('First type is', typeStruc._intersection[0])
}
parent.addChild(node.name.text, typeStruc)
break
case ts.SyntaxKind.ModuleDeclaration:
let moduleName = node.name.text
console.log('Declaring', moduleName)
ts.forEachChild(node, visit(parent.addChild(moduleName), checker))
break
case ts.SyntaxKind.ModuleBlock:
console.log('Block')
ts.forEachChild(node, visit(parent, checker));
break
case ts.SyntaxKind.InterfaceDeclaration:
let interfaceName = node.name.text
console.log('Skipping Interface', interfaceName);
break
parent[interfaceName] = {}
ts.forEachChild(node, visit(parent.addChild(interfaceName), 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 {
for (let type in PROPERTY_TYPES) {
if (propertyType.kind === PROPERTY_TYPES[type]) {
parent.addChild(realPropertyName, type)
break
}
}
}
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]
}