rbxts-transformer-t-new-new
Version:
TypeScript transformer which converts TypeScript types to t entities
433 lines (338 loc) • 10.9 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-new-new ERROR]: @rbxts/t must be installed for rbxts-transformer-t-new-new 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",
"Vector2int16",
"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}`;
}