Created
October 11, 2017 00:41
-
-
Save brad-jones/25bae9dbc0a55e1c7f764247e606ae24 to your computer and use it in GitHub Desktop.
ts-simple-ast script to add real reflection to typescript/javascript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as ts from 'typescript'; | |
import TsSimpleAst, { TypeGuards, GetAccessorDeclaration, SetAccessorDeclaration, PropertyDeclaration, Type, TypeNode, Node, TypedNode, Scope } from "ts-simple-ast"; | |
let ast = new TsSimpleAst | |
({ | |
tsConfigFilePath: __dirname + '/tsconfig.options.json', | |
compilerOptions: { outDir: __dirname + '/dist' } | |
}); | |
ast.addSourceFiles(__dirname + '/src/**/*{.d.ts,.ts}'); | |
let reflections = []; | |
for (let srcFileNode of ast.getSourceFiles().filter(_ => _.getFilePath().includes(__dirname + '/src'))) | |
{ | |
console.log(`Transpiling ${srcFileNode.getFilePath().replace(__dirname, '')}`); | |
let modulePath = srcFileNode.getFilePath().replace(__dirname + '/src/', '').replace('.ts', ''); | |
let resolveType = (node: Node<ts.Node>): { name: string, moduleSpecifier: string } => | |
{ | |
let resolvedType = 'any'; | |
if (TypeGuards.isTypedNode(node)) | |
{ | |
resolvedType = node.getType().getApparentType().getText(node); | |
if (resolvedType === 'any' && node.getTypeNode()) | |
{ | |
resolvedType = node.getTypeNode().getText() | |
} | |
} | |
else if (TypeGuards.isSetAccessorDeclaration(node)) | |
{ | |
resolvedType = node.getParameters()[0].getType().getApparentType().getText(node); | |
} | |
else if (TypeGuards.isReturnTypedNode(node)) | |
{ | |
resolvedType = node.getReturnType().getApparentType().getText(node); | |
} | |
return resolveTypeFromString(resolvedType); | |
}; | |
let resolveTypeFromString = (resolvedType: string): { name: string, moduleSpecifier: string } => | |
{ | |
let moduleSpecifier = ''; | |
let resolvedTypeStart = resolvedType.split('.')[0].trim(); | |
for (let importDec of srcFileNode.getImports()) | |
{ | |
let defaultImport = importDec.getDefaultImport(); | |
if (defaultImport && defaultImport.getText() === resolvedTypeStart) | |
{ | |
moduleSpecifier = importDec.getModuleSpecifier(); | |
resolvedType = resolvedType.replace(resolvedTypeStart, 'default'); | |
break; | |
} | |
let nsImport = importDec.getNamespaceImport(); | |
if (nsImport && nsImport.getText() === resolvedTypeStart) | |
{ | |
moduleSpecifier = importDec.getModuleSpecifier(); | |
resolvedType = resolvedType.replace(resolvedTypeStart, '*'); | |
break; | |
} | |
for (let namedImport of importDec.getNamedImports()) | |
{ | |
let alias = namedImport.getAlias(); | |
if (alias && alias.getText() === resolvedTypeStart) | |
{ | |
moduleSpecifier = importDec.getModuleSpecifier(); | |
resolvedType = resolvedType.replace(resolvedTypeStart, namedImport.getName().getText()); | |
break; | |
} | |
if (namedImport.getName().getText() === resolvedTypeStart) | |
{ | |
moduleSpecifier = importDec.getModuleSpecifier(); | |
break; | |
} | |
} | |
if (moduleSpecifier !== '') break; | |
} | |
return { name: resolvedType, moduleSpecifier: moduleSpecifier }; | |
}; | |
reflections.push(...srcFileNode.getClasses().filter(c => c.isDefaultExport() || c.isNamedExport()).map(c => | |
{ | |
return { | |
type: 'class', | |
target: `require('${modulePath}').${c.getName()}`, | |
name: c.getName(), | |
modulePath: modulePath, | |
isNamedExport: c.isNamedExport(), | |
isDefaultExport: c.isDefaultExport(), | |
abstract: c.isAbstract(), | |
implements: c.getImplements().map(i => resolveTypeFromString(i.getText())), | |
extends: c.getExtends() && resolveTypeFromString(c.getExtends().getText()) || null, | |
properties: c.getInstanceProperties().concat(c.getStaticProperties()).map(p => | |
{ | |
let newP = { | |
name: p.getName(), | |
scope: p.getScope(), | |
modifiers: p.getModifiers().filter(m => m.getText() !== p.getScope()).map(m => m.getText()), | |
getter: false, | |
setter: false, | |
static: false, | |
type: resolveType(p) | |
}; | |
if (TypeGuards.isStaticableNode(p)) | |
{ | |
newP.static = p.isStatic(); | |
} | |
if (TypeGuards.isGetAccessorDeclaration(p)) | |
{ | |
newP.getter = true; | |
} | |
else if (TypeGuards.isSetAccessorDeclaration(p)) | |
{ | |
newP.setter = true; | |
} | |
return newP; | |
}), | |
methods: c.getInstanceMethods().concat(c.getStaticMethods()).map(m => | |
{ | |
let args = m.getReturnType().getTypeArguments(); | |
return { | |
name: m.getName(), | |
abstract: m.isAbstract(), | |
static: m.isStatic(), | |
async: m.isAsync(), | |
overloaded: m.isOverload(), | |
parameters: m.getParameters().map(p => | |
{ | |
return { | |
name: p.getName(), | |
type: resolveType(p) | |
}; | |
}), | |
returnType: resolveType(m) | |
}; | |
}) | |
}; | |
})); | |
reflections.push(...srcFileNode.getInterfaces().filter(i => i.isDefaultExport() || i.isNamedExport()).map(i => | |
{ | |
return { | |
type: 'interface', | |
name: i.getName(), | |
modulePath: modulePath, | |
isNamedExport: i.isNamedExport(), | |
isDefaultExport: i.isDefaultExport(), | |
extends: i.getExtends().map(e => resolveTypeFromString(e.getText())), | |
properties: i.getProperties().map(p => | |
{ | |
return { | |
name: p.getName(), | |
modifiers: p.getModifiers().map(m => m.getText()), | |
type: resolveType(p) | |
}; | |
}), | |
methods: i.getMethods().map(m => | |
{ | |
let args = m.getReturnType().getTypeArguments(); | |
return { | |
name: m.getName(), | |
parameters: m.getParameters().map(p => | |
{ | |
return { | |
name: p.getName(), | |
type: resolveType(p) | |
}; | |
}), | |
returnType: resolveType(m) | |
}; | |
}) | |
}; | |
})); | |
reflections.push(...srcFileNode.getFunctions().filter(f => f.isDefaultExport() || f.isNamedExport()).map(f => | |
{ | |
return { | |
type: 'function', | |
name: f.getName(), | |
modulePath: modulePath, | |
isNamedExport: f.isNamedExport(), | |
isDefaultExport: f.isDefaultExport(), | |
}; | |
})); | |
} | |
ast.addSourceFileFromStructure(__dirname + '/src/app/Reflected.ts', | |
{ | |
imports: | |
[ | |
{ namedImports:[{name:'', alias: ''}], moduleSpecifier:'' } | |
], | |
functions: | |
[{ | |
name: 'tsReflect', | |
bodyText: `return ${JSON.stringify(reflections)};`, | |
isExported: true | |
}] | |
}); | |
let result = ast.emit(); | |
let errors = result.getDiagnostics(); | |
if (errors.length > 0) | |
{ | |
console.log(errors); | |
process.exit(1); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Very hacky proof of concept.