rbxts-transformer-t
Version:
TypeScript transformer which converts TypeScript types to t entities
374 lines (261 loc) • 11 kB
text/typescript
import ts, { factory } from "typescript";
import { getInstalledPathSync } from "get-installed-path";
import path from "path";
import fs from "fs";
import * as utility from "./utility";
export const OBJECT_NAME = "t";
export const MARCO_NAME = "$terrify";
const typePath = getInstalledPathSync("@rbxts/types", { local: true })
const instanceDefType = path.normalize(path.join(typePath, "include" ,"generated" ,"None.d.ts"))
function get_t_Path(): string {
try {
return getInstalledPathSync("@rbxts/t", { local: true })
} catch {
throw "[rbxts-transformer-t ERROR]: @rbxts/t must be installed for rbxts-transformer-t to work."
}
}
const ROBLOX_TYPES = [
"Axes",
"BrickColor",
"CFrame",
"Color3",
"ColorSequence",
"ColorSequenceKeypoint",
"DockWidgetPluginGuiInfo",
"Faces",
"Instance",
"NumberRange",
"NumberSequence",
"NumberSequenceKeypoint",
"PathWaypoint",
"PhysicalProperties",
"Random",
"Ray",
"Rect",
"Region3",
"Region3int16",
"TweenInfo",
"UDim",
"UDim2",
"Vector2",
"Vector3",
"Vector3int16",
"RBXScriptSignal",
"RBXScriptConnection",
"Enum",
"EnumItem",
]
function createPropertyAccess(propertyName: string): ts.PropertyAccessExpression {
return factory.createPropertyAccessExpression(factory.createIdentifier(OBJECT_NAME), factory.createIdentifier(propertyName))
}
function createMethodCall(methodName: string, params: ts.Expression[]): ts.CallExpression {
return factory.createCallExpression(createPropertyAccess(methodName), undefined, params)
}
function createLiteral(literal: ts.Expression): ts.Expression {
return createMethodCall("literal", [literal])
}
function createLiteralExpression(type: ts.Type, typeChecker: ts.TypeChecker) {
const stringType = typeChecker.typeToString(type)
if (stringType === "true" || stringType === "false") {
return stringType === "true" ? factory.createTrue() : factory.createFalse()
}
if (type.isStringLiteral())
return factory.createStringLiteral(type.value)
if (type.isNumberLiteral())
return factory.createNumericLiteral(type.value.toString())
}
function convertTypesArray(types: readonly ts.Type[], typeChecker: ts.TypeChecker): ts.Expression[] {
const transformNextType = (types: readonly ts.Type[], result: ts.Expression[]): ts.Expression[] => {
if (types.length === 0)
return result
const [head, ...tail] = types
const res = buildType(head, typeChecker)
return transformNextType(tail, [...result, res])
}
return transformNextType(types, [])
}
/**
* Converts ts.UnionType to a ts.Expression
*/
function convertUnionType(type: ts.UnionType, typeChecker: ts.TypeChecker): ts.Expression {
const nodes = type.aliasSymbol?.declarations
const types: ts.Type[] = nodes !== undefined && nodes.length !== 0
? (<any>nodes[0]).type.types.map(typeChecker.getTypeFromTypeNode)
: type.types
const [literalTypes, notLiteralTypes] = utility.separateArray(types, utility.isLiteral(typeChecker));
const literalExpression = literalTypes.length === 0 ?
undefined :
factory.createCallExpression(createPropertyAccess("literal"), undefined,
literalTypes.map((type) => createLiteralExpression(type, typeChecker) as ts.Expression))
const result = convertTypesArray(notLiteralTypes, typeChecker)
if (result.length === 0 && literalExpression)
return literalExpression
if (literalExpression)
result.push(literalExpression)
return createMethodCall("union", result)
}
/**
* Converts ts.TupleType to a ts.Expression
*/
function convertTupleType(type: ts.TupleType, typeChecker: ts.TypeChecker): ts.Expression {
const result = convertTypesArray((<any>type).resolvedTypeArguments, typeChecker)
return createMethodCall("strictArray", result)
}
/**
* Converts Array type to a ts.Expression
*/
function convertArrayType(type: ts.GenericType, typeChecker: ts.TypeChecker): ts.Expression {
const args = type.typeArguments
if (args === undefined || args.length === 0)
throw new Error("Array must have type arguments")
const result = buildType(args[0], typeChecker)
return createMethodCall("array", [result])
}
/**
* Converts Map type to a ts.Expression
*/
function convertMapType(type: ts.GenericType, typeChecker: ts.TypeChecker): ts.Expression {
const args = type.typeArguments
if (args === undefined)
throw new Error("Map must have type arguments")
const result = convertTypesArray(args, typeChecker)
return createMethodCall("map", result)
}
/**
* Converts Array of object properties to t.interface Expression. If there are
* optional properties, they will be built as separate objects
* and mixed to main object using t.intersection function
*/
function convertObjectType(props: ts.Symbol[], typeChecker: ts.TypeChecker): ts.Expression {
if (props.length === 0)
return createMethodCall("interface", [factory.createObjectLiteralExpression([])])
const preparedProps = props.map(prop => {
const origin = (prop as any).syntheticOrigin
return origin !== undefined ? origin : prop
})
// separate properties by weither
// property is optional or not
// and get 2 property lists
const [optionalProps, nonOptionalProps] = utility.separateArray(preparedProps, utility.isOptionalPropertyDeclaration)
// Builds t.interface entity by property list
const handlePropList = (props: ts.Symbol[], isOptional?: boolean): ts.Expression => {
const handledProps = props.map(prop => utility.extractProperty(prop, typeChecker))
const types = handledProps.map(prop => prop.type)
const result = convertTypesArray(types, typeChecker)
const properties = result.map(
(p, i) => factory.createPropertyAssignment(utility.buildPropertyName(handledProps[i].name), isOptional ? createMethodCall("optional", [p]) : p)
)
return createMethodCall("interface", [factory.createObjectLiteralExpression(properties)])
}
// Build 2 or 1 objects for each optional props object and add them to the result array
const result: ts.Expression[] = []
if (optionalProps.length !== 0)
result.push(handlePropList(optionalProps, true))
if (nonOptionalProps.length !== 0)
result.push(handlePropList(nonOptionalProps))
return result.length === 1
? result[0]
: createMethodCall("intersection", result)
}
/**
* Converts interface type to to ts.Expression
*/
function convertInterfaceType(type: ts.InterfaceType, typeChecker: ts.TypeChecker): ts.Expression {
const props = (type as any).declaredProperties ?? []
const object = convertObjectType(props, typeChecker)
const parents = type.symbol.declarations! // TODO
.map((d: any) => d.heritageClauses)
.filter(Boolean)
.reduce(utility.mergeArrays, [])
.map((clause: any) => clause.types)
.reduce(utility.mergeArrays, [])
.map(typeChecker.getTypeFromTypeNode)
if (parents.length === 0)
return object
const parentsTransformed = convertTypesArray(parents, typeChecker)
const nodesArray = [object, ...parentsTransformed]
return createMethodCall("intersection", nodesArray)
}
function convertEnumType(type: ts.Type, typeChecker: ts.TypeChecker): ts.Expression {
const enumDeclaration = type.symbol.valueDeclaration as ts.EnumDeclaration;
const result: ts.Expression[] = [];
for (const member of enumDeclaration.members) {
const value = typeChecker.getConstantValue(member);
if (typeof value === "string")
result.push(factory.createStringLiteral(value));
else if (typeof value === "number")
result.push(factory.createNumericLiteral(value));
else
throw `Unsupported!`;
}
return createMethodCall("literal", result);
}
let tTypeDefinitions = fs.readFileSync(path.join(get_t_Path(), "lib", "t.d.ts"), "utf8")
export function is_t_ImportDeclaration(program: ts.Program) {
return (node: ts.Node) => {
if (!ts.isImportDeclaration(node))
return false
if (!node.importClause)
return false
const namedBindings = node.importClause.namedBindings;
if (!node.importClause.name && !namedBindings)
return false
const importSymbol = program.getTypeChecker().getSymbolAtLocation(node.moduleSpecifier);
if (!importSymbol || importSymbol.valueDeclaration!.getSourceFile().text !== tTypeDefinitions) // TODO
return false
return true;
}
}
export function buildType(type: ts.Type, typeChecker: ts.TypeChecker): ts.Expression {
const stringType = typeChecker.typeToString(type)
// Checking for error cases
if (stringType === "never")
throw new Error("Never type transformation is not supported")
if (type.isClass())
throw new Error("Transformation of classes is not supported")
// Basic types transformation
if (["null", "undefined", "void", "unknown"].includes(stringType))
return createPropertyAccess("none")
if (ROBLOX_TYPES.includes(stringType))
return createPropertyAccess(stringType)
const fileName = type.symbol?.declarations?.[0]?.getSourceFile()?.fileName;
if (fileName && path.normalize(fileName) === instanceDefType) {
return createMethodCall("instanceIsA", [factory.createStringLiteral(stringType)])
}
if (utility.isBrickColorType(type))
return createPropertyAccess("BrickColor")
if (utility.isEnum(type))
return createMethodCall("enum", [factory.createPropertyAccessExpression(factory.createIdentifier("Enum"), stringType)])
if (stringType === "true" || stringType === "false") {
const literal = stringType === "true" ? factory.createTrue() : factory.createFalse()
return createLiteral(literal)
}
if (type.isStringLiteral())
return createLiteral(factory.createStringLiteral(type.value))
if (type.isNumberLiteral())
return createLiteral(factory.createNumericLiteral(type.value.toString()))
if (["string", "number", "boolean", "any", "thread"].includes(stringType))
return createPropertyAccess(stringType)
if (utility.isFunctionType(type))
return createPropertyAccess("callback")
// Complex types transformation
try {
if (utility.isCustomEnum(type))
return convertEnumType(type, typeChecker)
if (utility.isMapType(type))
return convertMapType(type, typeChecker)
if (type.isUnion())
return convertUnionType(type, typeChecker)
else if (utility.isTupleType(type, typeChecker))
return convertTupleType(type, typeChecker)
else if (utility.isArrayType(type, typeChecker))
return convertArrayType(type, typeChecker)
else if (utility.isObjectType(type) || type.isIntersection())
return convertObjectType(type.getProperties(), typeChecker)
else if (type.isClassOrInterface())
return convertInterfaceType(type, typeChecker)
} catch (err) {
throw `[t-ts-transformer ERROR]: Failed to build type ${stringType}\n${err}`
}
throw `Cannot build type ${stringType}`
}