UNPKG

rbxts-transformer-t

Version:

TypeScript transformer which converts TypeScript types to t entities

374 lines (261 loc) 11 kB
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}` }