Skip to content

Instantly share code, notes, and snippets.

@brad-jones
Created October 11, 2017 00:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brad-jones/25bae9dbc0a55e1c7f764247e606ae24 to your computer and use it in GitHub Desktop.
Save brad-jones/25bae9dbc0a55e1c7f764247e606ae24 to your computer and use it in GitHub Desktop.
ts-simple-ast script to add real reflection to typescript/javascript
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);
}
@brad-jones
Copy link
Author

Very hacky proof of concept.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment