├── .gitignore ├── src ├── information │ ├── expressions │ │ └── .gitkeep │ ├── viewNode.ts │ ├── viewFile.ts │ ├── viewExpression.ts │ ├── viewStatement.ts │ └── statements │ │ └── viewClassDeclaration.ts ├── util │ ├── functions │ │ ├── getDeclarationOfType.ts │ │ ├── getNodeList.ts │ │ ├── replaceValue.ts │ │ ├── arePathsEqual.ts │ │ ├── wrapToBlock.ts │ │ ├── isCleanBuildDirectory.ts │ │ ├── shuffle.ts │ │ ├── addLeadingComment.ts │ │ ├── isTupleType.ts │ │ ├── getIndexExpression.ts │ │ ├── isDefinedType.ts │ │ ├── assert.ts │ │ ├── tryResolve.ts │ │ ├── getDeclarationName.ts │ │ ├── isPathDescendantOf.ts │ │ ├── isAttributesAccess.ts │ │ ├── getSuperClasses.ts │ │ ├── getPrettyName.ts │ │ ├── parseCommandLine.ts │ │ ├── createPathTranslator.ts │ │ ├── getPackageJson.ts │ │ ├── getInstanceTypeFromType.ts │ │ ├── emitTypescriptMismatch.ts │ │ ├── getUniversalTypeNode.ts │ │ └── buildGuardFromType.ts │ ├── cache.ts │ ├── schema.ts │ ├── diagnosticsUtils.ts │ ├── uid.ts │ └── factory.ts ├── types │ ├── classes.d.ts │ └── decorators.d.ts ├── transformations │ ├── macros │ │ ├── intrinsics │ │ │ ├── inlining.ts │ │ │ ├── symbol.ts │ │ │ ├── paths.ts │ │ │ ├── guards.ts │ │ │ ├── parameters.ts │ │ │ └── networking.ts │ │ └── updateComponentConfig.ts │ ├── expressions │ │ ├── transformNewExpression.ts │ │ ├── transformCallExpression.ts │ │ ├── transformDeleteExpression.ts │ │ ├── transformUnaryExpression.ts │ │ ├── transformAccessExpression.ts │ │ └── transformBinaryExpression.ts │ ├── transformStatementList.ts │ ├── transformNode.ts │ ├── transformStatement.ts │ ├── transformFile.ts │ ├── transformExpression.ts │ ├── statements │ │ └── transformClassDeclaration.ts │ └── transformUserMacro.ts ├── classes │ ├── pathTranslator │ │ ├── constants.ts │ │ └── index.ts │ ├── diagnostics.ts │ ├── logger.ts │ ├── nodeMetadata.ts │ ├── buildInfo.ts │ └── transformState.ts ├── transformer.ts └── index.ts ├── tsconfig.json ├── rojo-schema.json ├── LICENSE ├── .eslintrc.json ├── package.json └── flamework-schema.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | -------------------------------------------------------------------------------- /src/information/expressions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/functions/getDeclarationOfType.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | export function getDeclarationOfType(type: ts.Type) { 4 | return type.symbol?.declarations?.[0]; 5 | } 6 | -------------------------------------------------------------------------------- /src/util/functions/getNodeList.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | export function getNodeList(statements: T | T[]): T[] { 4 | return Array.isArray(statements) ? statements : [statements]; 5 | } 6 | -------------------------------------------------------------------------------- /src/util/functions/replaceValue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Replace a value (in-place) in an array. 3 | */ 4 | export function replaceValue(arr: Array, needle: T, value: T) { 5 | const index = arr.lastIndexOf(needle); 6 | if (index === -1) return; 7 | arr[index] = value; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/classes.d.ts: -------------------------------------------------------------------------------- 1 | import { DecoratorInfo } from "./decorators"; 2 | 3 | export interface ClassInfo { 4 | symbol: ts.Symbol; 5 | internalId: string; 6 | node: ts.Node; 7 | name: string; 8 | decorators: DecoratorInfo[]; 9 | containsLegacyDecorator: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /src/util/functions/arePathsEqual.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | export function arePathsEqual(path1: string, path2: string) { 4 | if (process.platform === "win32") { 5 | path1 = path1.toLowerCase(); 6 | path2 = path2.toLowerCase(); 7 | } 8 | return path.normalize(path1) === path.normalize(path2); 9 | } 10 | -------------------------------------------------------------------------------- /src/util/functions/wrapToBlock.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | export function wrapToBlock(nodes: ts.Statement | ts.Statement[]): ts.Statement { 4 | if (Array.isArray(nodes)) { 5 | if (nodes.length === 1) { 6 | return nodes[0]; 7 | } else { 8 | return ts.factory.createBlock(nodes); 9 | } 10 | } 11 | return nodes; 12 | } 13 | -------------------------------------------------------------------------------- /src/util/functions/isCleanBuildDirectory.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import ts from "typescript"; 3 | 4 | export function isCleanBuildDirectory(compilerOptions: ts.CompilerOptions) { 5 | if (compilerOptions.incremental && compilerOptions.tsBuildInfoFile) { 6 | return !fs.existsSync(compilerOptions.tsBuildInfoFile); 7 | } 8 | 9 | return true; 10 | } 11 | -------------------------------------------------------------------------------- /src/util/functions/shuffle.ts: -------------------------------------------------------------------------------- 1 | export function shuffle(array: ReadonlyArray): Array { 2 | return shuffleInPlace([...array]); 3 | } 4 | 5 | function shuffleInPlace(array: Array) { 6 | for (let i = array.length - 1; i >= 0; i--) { 7 | const randomIndex = Math.floor(Math.random() * (i + 1)); 8 | [array[i], array[randomIndex]] = [array[randomIndex], array[i]]; 9 | } 10 | 11 | return array; 12 | } 13 | -------------------------------------------------------------------------------- /src/util/functions/addLeadingComment.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | export function addLeadingComment(node: T, text: string, multiline = false) { 4 | if (node === undefined) return node; 5 | return ts.addSyntheticLeadingComment( 6 | node, 7 | multiline ? ts.SyntaxKind.MultiLineCommentTrivia : ts.SyntaxKind.SingleLineCommentTrivia, 8 | text, 9 | true, 10 | ) as T; 11 | } 12 | -------------------------------------------------------------------------------- /src/util/functions/isTupleType.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../../classes/transformState"; 3 | 4 | export function isTupleType(state: TransformState, type: ts.Type): type is ts.TupleTypeReference { 5 | return state.typeChecker.isTupleType(type); 6 | } 7 | 8 | export function isArrayType(state: TransformState, type: ts.Type): type is ts.TypeReference { 9 | return state.typeChecker.isArrayType(type); 10 | } 11 | -------------------------------------------------------------------------------- /src/information/viewNode.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../classes/transformState"; 3 | import { viewExpression } from "./viewExpression"; 4 | import { viewStatement } from "./viewStatement"; 5 | 6 | export function viewNode(state: TransformState, node: ts.Node) { 7 | if (ts.isExpression(node)) { 8 | viewExpression(state, node); 9 | } else if (ts.isStatement(node)) { 10 | viewStatement(state, node); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/util/functions/getIndexExpression.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { f } from "../factory"; 3 | 4 | /** 5 | * Gets the expression used to index the AccessExpression. 6 | * Converts properties to strings. 7 | */ 8 | export function getIndexExpression(expression: ts.AccessExpression) { 9 | if (f.is.propertyAccessExpression(expression)) { 10 | return f.string(expression.name.text); 11 | } else { 12 | return expression.argumentExpression; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "out", 6 | "rootDir": "src", 7 | "typeRoots": ["node_modules/@types"], 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "downlevelIteration": true, 11 | 12 | "strict": true, 13 | "lib": ["ES2015", "DOM"], 14 | 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/transformations/macros/intrinsics/inlining.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { f } from "../../../util/factory"; 3 | 4 | /** 5 | * An inlining intrinsic for basic return types. 6 | */ 7 | export function inlineMacroIntrinsic(signature: ts.Signature, args: ts.Expression[], parameter: ts.Symbol) { 8 | const parameterIndex = signature.parameters.findIndex((v) => v.valueDeclaration?.symbol === parameter); 9 | const argument = args[parameterIndex]; 10 | return f.as(argument, signature.getDeclaration().type!); 11 | } 12 | -------------------------------------------------------------------------------- /src/information/viewFile.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { Diagnostics } from "../classes/diagnostics"; 3 | import { TransformState } from "../classes/transformState"; 4 | import { viewNode } from "./viewNode"; 5 | 6 | export function viewFile(state: TransformState, file: ts.SourceFile) { 7 | function visitor(node: ts.Node) { 8 | viewNode(state, node); 9 | ts.forEachChild(node, visitor); 10 | } 11 | ts.forEachChild(file, visitor); 12 | 13 | for (const diag of Diagnostics.flush()) { 14 | state.addDiagnostic(diag); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/util/functions/isDefinedType.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | // https://github.com/roblox-ts/roblox-ts/blob/dc74f34fdab3caf20d65db080cf2dbf5c4f38fdc/src/TSTransformer/util/types.ts#L70 4 | export function isDefinedType(type: ts.Type) { 5 | return ( 6 | type.flags === ts.TypeFlags.Object && 7 | type.getProperties().length === 0 && 8 | type.getCallSignatures().length === 0 && 9 | type.getConstructSignatures().length === 0 && 10 | type.getNumberIndexType() === undefined && 11 | type.getStringIndexType() === undefined 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/util/functions/assert.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Asserts the truthiness of `value`, stops the debugger on failure. 3 | * @param value The value to check the truthiness of 4 | * @param message Optional. The message of the error 5 | */ 6 | export function assert(value: unknown, message?: string): asserts value { 7 | /* istanbul ignore if */ 8 | if (!value) { 9 | debugger; 10 | throw new Error( 11 | `Assertion Failed! ${message ?? ""}` + 12 | "\nPlease submit a bug report here:" + 13 | "\nhttps://github.com/rbxts-flamework/core/issues", 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/information/viewExpression.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../classes/transformState"; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | const VIEWERS = new Map void>([ 6 | // [ts.SyntaxKind.IfStatement, transformIfStatement], 7 | ]); 8 | 9 | export function viewExpression(state: TransformState, expression: ts.Expression) { 10 | // do stuff 11 | const viewer = VIEWERS.get(expression.kind); 12 | if (viewer) { 13 | viewer(state, expression); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/util/functions/tryResolve.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import type { TransformState } from "../../classes/transformState"; 3 | 4 | export function tryResolve(moduleName: string, path: string): string | undefined { 5 | try { 6 | return require.resolve(moduleName, { paths: [path] }); 7 | } catch (e) {} 8 | } 9 | 10 | export function tryResolveTS(state: TransformState, moduleName: string, path: string): string | undefined { 11 | const module = ts.resolveModuleName(moduleName, path, state.options, ts.sys); 12 | return module.resolvedModule?.resolvedFileName; 13 | } 14 | -------------------------------------------------------------------------------- /src/util/functions/getDeclarationName.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { f } from "../factory"; 3 | 4 | /** 5 | * Calculates a name, including all named ancestors, such as Enum.Material.Air 6 | * @param node The node to retrieve the name of 7 | */ 8 | export function getDeclarationName(node: ts.NamedDeclaration): string { 9 | if (!f.is.identifier(node.name)) return "$p:error"; 10 | 11 | let name = node.name.text; 12 | for (let parent = node.parent; parent !== undefined; parent = parent.parent) { 13 | if (ts.isNamedDeclaration(parent)) { 14 | name = parent.name.getText() + "." + name; 15 | } 16 | } 17 | return name; 18 | } 19 | -------------------------------------------------------------------------------- /src/information/viewStatement.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../classes/transformState"; 3 | import { viewClassDeclaration } from "./statements/viewClassDeclaration"; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | const VIEWERS = new Map void>([ 7 | [ts.SyntaxKind.ClassDeclaration, viewClassDeclaration], 8 | ]); 9 | 10 | export function viewStatement(state: TransformState, expression: ts.Statement) { 11 | // do stuff 12 | const viewer = VIEWERS.get(expression.kind); 13 | if (viewer) { 14 | viewer(state, expression); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/transformations/expressions/transformNewExpression.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../../classes/transformState"; 3 | import { transformUserMacro } from "../transformUserMacro"; 4 | 5 | export function transformNewExpression(state: TransformState, node: ts.NewExpression) { 6 | const symbol = state.getSymbol(node.expression); 7 | 8 | if (symbol) { 9 | if (state.isUserMacro(symbol)) { 10 | const signature = state.typeChecker.getResolvedSignature(node); 11 | if (signature) { 12 | return transformUserMacro(state, node, signature) ?? state.transform(node); 13 | } 14 | } 15 | } 16 | 17 | return state.transform(node); 18 | } 19 | -------------------------------------------------------------------------------- /src/transformations/transformStatementList.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../classes/transformState"; 3 | import { getNodeList } from "../util/functions/getNodeList"; 4 | import { transformStatement } from "./transformStatement"; 5 | 6 | export function transformStatementList(state: TransformState, statements: ReadonlyArray) { 7 | const result = new Array(); 8 | 9 | for (const statement of statements) { 10 | const [newStatements, prereqs] = state.capture(() => transformStatement(state, statement)); 11 | 12 | result.push(...prereqs); 13 | result.push(...getNodeList(newStatements)); 14 | } 15 | 16 | return result; 17 | } 18 | -------------------------------------------------------------------------------- /rojo-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "required": ["name", "tree"], 4 | "type": "object", 5 | "properties": { 6 | "name": { 7 | "type": "string" 8 | }, 9 | "servePort": { 10 | "type": "integer" 11 | }, 12 | "tree": { 13 | "$id": "tree", 14 | "type": "object", 15 | "properties": { 16 | "$className": { 17 | "type": "string" 18 | }, 19 | "$ignoreUnknownInstances": { 20 | "type": "boolean" 21 | }, 22 | "$path": { 23 | "type": "string" 24 | }, 25 | "$properties": { 26 | "type": "object" 27 | } 28 | }, 29 | "patternProperties": { 30 | "^[^\\$].*$": { "$ref": "tree" } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/types/decorators.d.ts: -------------------------------------------------------------------------------- 1 | import { ts } from "typescript"; 2 | 3 | export type BaseDecoratorInfo = ServiceDecorator | ControllerDecorator | CustomDecorator; 4 | export type DecoratorInfo = BaseDecoratorInfo | DecoratorWithNodes; 5 | 6 | interface BaseDecorator { 7 | type: "Base"; 8 | name: string; 9 | internalId: string; 10 | } 11 | 12 | interface DecoratorWithNodes extends BaseDecorator { 13 | type: "WithNodes"; 14 | symbol: ts.Symbol; 15 | declaration: ts.Node; 16 | arguments: ts.Node[]; 17 | } 18 | 19 | interface ServiceDecorator extends BaseDecorator { 20 | name: "Service"; 21 | } 22 | 23 | interface ControllerDecorator extends BaseDecorator { 24 | name: "Controller"; 25 | } 26 | 27 | type CustomDecorator = BaseDecorator; 28 | -------------------------------------------------------------------------------- /src/classes/pathTranslator/constants.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | export const PACKAGE_ROOT = path.join(__dirname, "..", "..", ".."); 4 | 5 | // intentionally not using PACKAGE_ROOT because playground has webpack issues 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports 7 | export const PKG_VERSION: string = require("../../../package.json").version; 8 | 9 | export const TS_EXT = ".ts"; 10 | export const TSX_EXT = ".tsx"; 11 | export const D_EXT = ".d"; 12 | export const LUA_EXT = ".lua"; 13 | 14 | export const INDEX_NAME = "index"; 15 | export const INIT_NAME = "init"; 16 | 17 | export enum ProjectType { 18 | Game = "game", 19 | Model = "model", 20 | Package = "package", 21 | } 22 | -------------------------------------------------------------------------------- /src/util/cache.ts: -------------------------------------------------------------------------------- 1 | import { RojoResolver } from "@roblox-ts/rojo-resolver"; 2 | import { PackageJsonResult } from "./functions/getPackageJson"; 3 | 4 | export interface Cache { 5 | rojoSum?: string; 6 | rojoResolver?: RojoResolver; 7 | buildInfoCandidates?: string[]; 8 | isInitialCompile: boolean; 9 | shouldView: Map; 10 | realPath: Map; 11 | moduleResolution: Map; 12 | pkgJsonCache: Map; 13 | } 14 | 15 | /** 16 | * Global cache that is only reset when rbxtsc is restarted. 17 | */ 18 | export const Cache: Cache = { 19 | isInitialCompile: true, 20 | shouldView: new Map(), 21 | realPath: new Map(), 22 | moduleResolution: new Map(), 23 | pkgJsonCache: new Map(), 24 | }; 25 | -------------------------------------------------------------------------------- /src/util/functions/isPathDescendantOf.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | /** 4 | * Checks if the `filePath` path is a descendant of the `dirPath` path. 5 | * @param filePath A path to a file. 6 | * @param dirPath A path to a directory. 7 | */ 8 | export function isPathDescendantOf(filePath: string, dirPath: string) { 9 | return dirPath === filePath || !path.relative(dirPath, filePath).startsWith(".."); 10 | } 11 | 12 | /** 13 | * Checks if the `filePath` is a descendant of any of the specified `dirPaths` paths. 14 | * @param filePath A path to a file. 15 | * @param dirPaths The directories to check. 16 | */ 17 | export function isPathDescendantOfAny(filePath: string, dirPaths: string[]) { 18 | return dirPaths.some((dirPath) => isPathDescendantOf(filePath, dirPath)); 19 | } 20 | -------------------------------------------------------------------------------- /src/util/functions/isAttributesAccess.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../../classes/transformState"; 3 | import { f } from "../factory"; 4 | import { NodeMetadata } from "../../classes/nodeMetadata"; 5 | 6 | /** 7 | * Checks if an expression is attempting to access component attributes. 8 | * E.g `this.attributes.myProp` 9 | */ 10 | export function isAttributesAccess(state: TransformState, expression: ts.Node): expression is ts.AccessExpression { 11 | if (!f.is.accessExpression(expression)) { 12 | return false; 13 | } 14 | 15 | const lhs = state.getSymbol(expression.expression); 16 | if (!lhs) { 17 | return false; 18 | } 19 | 20 | const metadata = NodeMetadata.fromSymbol(state, lhs); 21 | if (!metadata) { 22 | return false; 23 | } 24 | 25 | return metadata.isRequested("intrinsic-component-attributes"); 26 | } 27 | -------------------------------------------------------------------------------- /src/util/functions/getSuperClasses.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | export function getSuperClasses(typeChecker: ts.TypeChecker, node: ts.ClassDeclaration) { 4 | const superClasses = new Array(); 5 | const superClass = node.heritageClauses?.find((x) => x.token === ts.SyntaxKind.ExtendsKeyword)?.types?.[0]; 6 | if (superClass) { 7 | const aliasSymbol = typeChecker.getSymbolAtLocation(superClass.expression); 8 | if (aliasSymbol) { 9 | const symbol = ts.skipAlias(aliasSymbol, typeChecker); 10 | const classDeclaration = symbol?.declarations?.find((x): x is ts.ClassDeclaration => 11 | ts.isClassDeclaration(x), 12 | ); 13 | if (classDeclaration) { 14 | superClasses.push(classDeclaration as never); 15 | superClasses.push(...getSuperClasses(typeChecker, classDeclaration)); 16 | } 17 | } 18 | } 19 | return superClasses; 20 | } 21 | -------------------------------------------------------------------------------- /src/util/functions/getPrettyName.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../../classes/transformState"; 3 | import { f } from "../factory"; 4 | 5 | export function getPrettyName(state: TransformState, node: ts.Node | undefined, fallback: string, prefix = "_") { 6 | if (!node) return `${prefix}${fallback}`; 7 | 8 | return `${prefix}${getPrettyNameInner(state, node, fallback)}`; 9 | } 10 | 11 | function getPrettyNameInner(state: TransformState, node: ts.Node, fallback: string) { 12 | if (f.is.referenceType(node)) { 13 | const symbol = state.getSymbol(node.typeName); 14 | if (symbol) { 15 | return camelCase(symbol.name); 16 | } 17 | } else if (f.is.identifier(node)) { 18 | return camelCase(node.text); 19 | } 20 | 21 | return fallback; 22 | } 23 | 24 | function camelCase(name: string) { 25 | return name.substr(0, 1).toLowerCase() + name.substr(1); 26 | } 27 | -------------------------------------------------------------------------------- /src/transformations/transformNode.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { Diagnostics } from "../classes/diagnostics"; 3 | import { TransformState } from "../classes/transformState"; 4 | import { transformExpression } from "./transformExpression"; 5 | import { transformStatement } from "./transformStatement"; 6 | 7 | export function transformNode(state: TransformState, node: ts.Node): ts.Node | ts.Statement[] { 8 | try { 9 | if (ts.isExpression(node)) { 10 | return transformExpression(state, node); 11 | } else if (ts.isStatement(node)) { 12 | return transformStatement(state, node); 13 | } 14 | } catch (e) { 15 | if (e instanceof Error && !("diagnostic" in e)) { 16 | Diagnostics.error(node, `Flamework failure occurred here\n${e.stack}`); 17 | } 18 | 19 | throw e; 20 | } 21 | 22 | return ts.visitEachChild(node, (newNode) => transformNode(state, newNode), state.context); 23 | } 24 | -------------------------------------------------------------------------------- /src/util/schema.ts: -------------------------------------------------------------------------------- 1 | import Ajv from "ajv"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import { PACKAGE_ROOT } from "../classes/pathTranslator/constants"; 5 | import { FlameworkConfig } from "../classes/transformState"; 6 | import { FlameworkBuildInfo } from "../classes/buildInfo"; 7 | 8 | const SCHEMA = createSchema(); 9 | 10 | interface Schemas { 11 | config: FlameworkConfig; 12 | buildInfo: FlameworkBuildInfo; 13 | } 14 | 15 | function createSchema() { 16 | const schemaPath = path.join(PACKAGE_ROOT, "flamework-schema.json"); 17 | const schema = new Ajv(); 18 | schema.addSchema(JSON.parse(fs.readFileSync(schemaPath, { encoding: "ascii" })), "root"); 19 | 20 | return schema; 21 | } 22 | 23 | export function getSchemaErrors() { 24 | return SCHEMA.errors ?? []; 25 | } 26 | 27 | export function validateSchema(key: K, value: unknown): value is Schemas[K] { 28 | return SCHEMA.validate(`root#/properties/${key}`, value); 29 | } 30 | -------------------------------------------------------------------------------- /src/transformations/macros/intrinsics/symbol.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../../../classes/transformState"; 3 | import { getNodeUid, getTypeUid } from "../../../util/uid"; 4 | import { f } from "../../../util/factory"; 5 | 6 | /** 7 | * This function differs from `Modding.Generic` in that it will first try to preserve the symbol information 8 | * provided in the type argument. If there is no type argument (e.g it was inferred), it will be equivalent to `Modding.Generic` 9 | * 10 | * This is unfortunately necessary for certain APIs as things like `typeof Decorator` will lose symbol information, 11 | * which can be problematic for the Modding APIs. 12 | */ 13 | export function buildSymbolIdIntrinsic(state: TransformState, node: ts.CallExpression, type: ts.Type) { 14 | const typeArgument = node.typeArguments?.[0]; 15 | if (typeArgument) { 16 | return f.string(getNodeUid(state, typeArgument)); 17 | } 18 | 19 | return f.string(getTypeUid(state, type, node)); 20 | } 21 | -------------------------------------------------------------------------------- /src/transformations/expressions/transformCallExpression.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../../classes/transformState"; 3 | import { transformNode } from "../transformNode"; 4 | import { transformUserMacro } from "../transformUserMacro"; 5 | import { f } from "../../util/factory"; 6 | 7 | export function transformCallExpression(state: TransformState, node: ts.CallExpression) { 8 | const symbol = state.getSymbol(node.expression); 9 | 10 | if (symbol) { 11 | if (state.isUserMacro(symbol)) { 12 | // We skip `super()` expressions as we likely do not have enough information to evaluate it. 13 | if (f.is.superExpression(node.expression)) { 14 | return state.transform(node); 15 | } 16 | 17 | const signature = state.typeChecker.getResolvedSignature(node); 18 | if (signature) { 19 | return transformUserMacro(state, node, signature) ?? state.transform(node); 20 | } 21 | } 22 | } 23 | 24 | return ts.visitEachChild(node, (node) => transformNode(state, node), state.context); 25 | } 26 | -------------------------------------------------------------------------------- /src/util/functions/parseCommandLine.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | 5 | interface CommandLine { 6 | tsconfigPath: string; 7 | project: string; 8 | } 9 | 10 | function findTsConfigPath(projectPath: string) { 11 | let tsConfigPath: string | undefined = path.resolve(projectPath); 12 | if (!fs.existsSync(tsConfigPath) || !fs.statSync(tsConfigPath).isFile()) { 13 | tsConfigPath = ts.findConfigFile(tsConfigPath, ts.sys.fileExists); 14 | if (tsConfigPath === undefined) { 15 | throw new Error("Unable to find tsconfig.json!"); 16 | } 17 | } 18 | return path.resolve(process.cwd(), tsConfigPath); 19 | } 20 | 21 | export function parseCommandLine(): CommandLine { 22 | const options = {} as CommandLine; 23 | 24 | const projectIndex = process.argv.findIndex((x) => x === "-p" || x === "--project"); 25 | if (projectIndex !== -1) { 26 | options.tsconfigPath = findTsConfigPath(process.argv[projectIndex + 1]); 27 | } else { 28 | options.tsconfigPath = findTsConfigPath("."); 29 | } 30 | 31 | options.project = path.dirname(options.tsconfigPath); 32 | return options; 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flamework 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "jsx": true, 5 | "useJSXTextNode": true, 6 | "ecmaVersion": 2018, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "plugins": [ 11 | "@typescript-eslint", 12 | "prettier" 13 | ], 14 | "extends": [ 15 | "plugin:@typescript-eslint/recommended", 16 | "prettier", 17 | "plugin:prettier/recommended" 18 | ], 19 | "rules": { 20 | "prettier/prettier": [ 21 | "warn", 22 | { 23 | "semi": true, 24 | "trailingComma": "all", 25 | "singleQuote": false, 26 | "printWidth": 120, 27 | "tabWidth": 4, 28 | "useTabs": true, 29 | "endOfLine": "auto" 30 | } 31 | ], 32 | "@typescript-eslint/no-explicit-any": [ 33 | "warn", 34 | { 35 | "ignoreRestArgs": true 36 | } 37 | ], 38 | "@typescript-eslint/no-non-null-assertion": ["off"], 39 | "@typescript-eslint/explicit-module-boundary-types": ["off"], 40 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "file" }], 41 | "@typescript-eslint/no-namespace": ["off"], 42 | "prefer-const": ["error", { 43 | "destructuring": "all" 44 | }] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/util/functions/createPathTranslator.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import ts from "typescript"; 3 | import { PathTranslator } from "../../classes/pathTranslator"; 4 | import { assert } from "./assert"; 5 | 6 | function findAncestorDir(dirs: Array) { 7 | dirs = dirs.map(path.normalize).map((v) => (v.endsWith(path.sep) ? v : v + path.sep)); 8 | let currentDir = dirs[0]; 9 | while (!dirs.every((v) => v.startsWith(currentDir))) { 10 | currentDir = path.join(currentDir, ".."); 11 | } 12 | return currentDir; 13 | } 14 | 15 | function getRootDirs(compilerOptions: ts.CompilerOptions) { 16 | const rootDirs = compilerOptions.rootDir ? [compilerOptions.rootDir] : compilerOptions.rootDirs; 17 | if (!rootDirs) assert(false, "rootDir or rootDirs must be specified"); 18 | 19 | return rootDirs; 20 | } 21 | 22 | export function createPathTranslator(program: ts.Program) { 23 | const compilerOptions = program.getCompilerOptions(); 24 | const rootDir = findAncestorDir([program.getCommonSourceDirectory(), ...getRootDirs(compilerOptions)]); 25 | const outDir = compilerOptions.outDir!; 26 | return new PathTranslator(rootDir, outDir, undefined, compilerOptions.declaration || false); 27 | } 28 | -------------------------------------------------------------------------------- /src/transformations/expressions/transformDeleteExpression.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { Diagnostics } from "../../classes/diagnostics"; 3 | import { TransformState } from "../../classes/transformState"; 4 | import { f } from "../../util/factory"; 5 | import { getIndexExpression } from "../../util/functions/getIndexExpression"; 6 | import { isAttributesAccess } from "../../util/functions/isAttributesAccess"; 7 | 8 | export function transformDeleteExpression(state: TransformState, node: ts.DeleteExpression) { 9 | if (isAttributesAccess(state, node.expression)) { 10 | const name = getIndexExpression(node.expression); 11 | if (!name) Diagnostics.error(node.expression, "could not get index expression"); 12 | 13 | if (!f.is.accessExpression(node.expression.expression)) 14 | Diagnostics.error(node.expression, "assignments not supported with direct access"); 15 | 16 | const attributeSetter = state.addFileImport( 17 | node.getSourceFile(), 18 | "@flamework/components/out/baseComponent", 19 | "SYMBOL_ATTRIBUTE_SETTER", 20 | ); 21 | const thisAccess = node.expression.expression.expression; 22 | return f.call(f.field(thisAccess, attributeSetter, true), [name, f.nil()]); 23 | } 24 | 25 | return node; 26 | } 27 | -------------------------------------------------------------------------------- /src/transformations/transformStatement.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../classes/transformState"; 3 | import { catchDiagnostic } from "../util/diagnosticsUtils"; 4 | import { getNodeList } from "../util/functions/getNodeList"; 5 | import { transformClassDeclaration } from "./statements/transformClassDeclaration"; 6 | import { transformNode } from "./transformNode"; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | const TRANSFORMERS = new Map ts.Statement | ts.Statement[]>([ 10 | [ts.SyntaxKind.ClassDeclaration, transformClassDeclaration], 11 | ]); 12 | 13 | export function transformStatement(state: TransformState, statement: ts.Statement): ts.Statement | ts.Statement[] { 14 | return catchDiagnostic(statement, () => { 15 | const [node, prereqs] = state.capture(() => { 16 | const transformer = TRANSFORMERS.get(statement.kind); 17 | if (transformer) { 18 | return transformer(state, statement); 19 | } 20 | 21 | return ts.visitEachChild(statement, (newNode) => transformNode(state, newNode), state.context); 22 | }); 23 | 24 | return [...prereqs, ...getNodeList(node)]; 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/transformations/transformFile.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { Diagnostics } from "../classes/diagnostics"; 3 | import { TransformState } from "../classes/transformState"; 4 | import { f } from "../util/factory"; 5 | import { transformStatementList } from "./transformStatementList"; 6 | 7 | export function transformFile(state: TransformState, file: ts.SourceFile): ts.SourceFile { 8 | state.buildInfo.invalidateGlobs(state.getFileId(file)); 9 | 10 | const statements = transformStatementList(state, file.statements); 11 | 12 | const imports = state.fileImports.get(file.fileName); 13 | if (imports) { 14 | const firstStatement = statements[0]; 15 | 16 | statements.unshift( 17 | ...imports.map((info) => 18 | f.importDeclaration( 19 | info.path, 20 | info.entries.map((x) => [x.name, x.identifier]), 21 | ), 22 | ), 23 | ); 24 | 25 | // steal comments from original first statement so that comment directives work properly 26 | if (firstStatement && statements[0]) { 27 | const original = ts.getParseTreeNode(firstStatement); 28 | 29 | ts.moveSyntheticComments(statements[0], firstStatement); 30 | 31 | if (original) { 32 | ts.copyComments(original, statements[0]); 33 | ts.removeAllComments(original); 34 | } 35 | } 36 | } 37 | 38 | for (const diag of Diagnostics.flush()) { 39 | state.addDiagnostic(diag); 40 | } 41 | 42 | const sourceFile = f.update.sourceFile(file, statements); 43 | 44 | return sourceFile; 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rbxts-transformer-flamework", 3 | "version": "1.3.2", 4 | "description": "An opinionated game framework for Roblox.", 5 | "main": "out/index.js", 6 | "author": "Fireboltofdeath", 7 | "license": "MIT", 8 | "homepage": "https://github.com/FireTS/rbxts-transformer-flamework", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/FireTS/rbxts-transformer-flamework.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/FireTS/rbxts-transformer-flamework/issues" 15 | }, 16 | "scripts": { 17 | "build": "tsc", 18 | "watch": "tsc -w", 19 | "prepare": "tsc" 20 | }, 21 | "dependencies": { 22 | "@roblox-ts/rojo-resolver": "^1.0.2", 23 | "ajv": "^8.6.3", 24 | "chalk": "^4.1.2", 25 | "fs-extra": "^10.0.0", 26 | "glob": "9.3", 27 | "hashids": "^2.2.8", 28 | "normalize-package-data": "^3.0.3", 29 | "uuid": "^8.3.2" 30 | }, 31 | "devDependencies": { 32 | "@types/fs-extra": "^9.0.13", 33 | "@types/node": "^16.11.1", 34 | "@types/normalize-package-data": "^2.4.1", 35 | "@types/uuid": "^8.3.1", 36 | "@typescript-eslint/eslint-plugin": "^6.7.4", 37 | "@typescript-eslint/parser": "^6.7.4", 38 | "eslint": "^8.0.1", 39 | "eslint-config-prettier": "^8.3.0", 40 | "eslint-plugin-prettier": "^4.0.0", 41 | "prettier": "^2.4.1", 42 | "ts-expose-internals": "^5.2.2", 43 | "typescript": "^5.5.3" 44 | }, 45 | "peerDependencies": { 46 | "typescript": "*" 47 | }, 48 | "files": [ 49 | "out", 50 | "index.d.ts", 51 | "rojo-schema.json", 52 | "flamework-schema.json" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /src/util/functions/getPackageJson.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import path from "path"; 3 | import normalize from "normalize-package-data"; 4 | import { isPathDescendantOf } from "./isPathDescendantOf"; 5 | import { Cache } from "../cache"; 6 | 7 | export type PackageJsonResult = ReturnType; 8 | 9 | /** 10 | * Looks recursively at ancestors until a package.json is found 11 | * @param directory The directory to start under. 12 | */ 13 | export function getPackageJson(directory: string) { 14 | const existing = Cache.pkgJsonCache.get(path.normalize(directory)); 15 | if (existing) return existing; 16 | 17 | const result = getPackageJsonInner(directory); 18 | 19 | Cache.pkgJsonCache.set(path.normalize(directory), result); 20 | ts.forEachAncestorDirectory(directory, (dir) => { 21 | if (isPathDescendantOf(dir, result.directory)) { 22 | Cache.pkgJsonCache.set(path.normalize(dir), result); 23 | } else { 24 | return true; 25 | } 26 | }); 27 | 28 | return result; 29 | } 30 | 31 | function getPackageJsonInner(directory: string) { 32 | const packageJsonPath = ts.findPackageJson(directory, ts.sys as never); 33 | if (!packageJsonPath) throw new Error(`package.json not found in ${directory}`); 34 | 35 | const text = packageJsonPath ? ts.sys.readFile(packageJsonPath) : undefined; 36 | const packageJson = text ? JSON.parse(text) : {}; 37 | normalize(packageJson); 38 | 39 | return { 40 | directory: path.dirname(packageJsonPath), 41 | path: packageJsonPath, 42 | result: packageJson as normalize.Package, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/transformations/expressions/transformUnaryExpression.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { Diagnostics } from "../../classes/diagnostics"; 3 | import { TransformState } from "../../classes/transformState"; 4 | import { f } from "../../util/factory"; 5 | import { getIndexExpression } from "../../util/functions/getIndexExpression"; 6 | import { isAttributesAccess } from "../../util/functions/isAttributesAccess"; 7 | 8 | const MUTATING_OPERATORS = new Map([ 9 | [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.PlusToken], 10 | [ts.SyntaxKind.MinusMinusToken, ts.SyntaxKind.MinusToken], 11 | ]); 12 | 13 | export function transformUnaryExpression( 14 | state: TransformState, 15 | node: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression, 16 | ) { 17 | const nonAssignmentOperator = MUTATING_OPERATORS.get(node.operator); 18 | if (nonAssignmentOperator) { 19 | if (isAttributesAccess(state, node.operand)) { 20 | const name = getIndexExpression(node.operand); 21 | if (!name) Diagnostics.error(node.operand, "could not get index expression"); 22 | 23 | if (!f.is.accessExpression(node.operand.expression)) 24 | Diagnostics.error(node.operand, "assignments not supported with direct access"); 25 | 26 | const attributeSetter = state.addFileImport( 27 | node.getSourceFile(), 28 | "@flamework/components/out/baseComponent", 29 | "SYMBOL_ATTRIBUTE_SETTER", 30 | ); 31 | const thisAccess = node.operand.expression.expression; 32 | const args = [name, f.binary(node.operand, nonAssignmentOperator, 1)]; 33 | 34 | return f.call(f.field(thisAccess, attributeSetter, true), f.is.postfixUnary(node) ? [...args, true] : args); 35 | } 36 | } 37 | return state.transform(node); 38 | } 39 | -------------------------------------------------------------------------------- /src/classes/diagnostics.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | function createDiagnosticAtLocation( 4 | node: ts.Node, 5 | messageText: string, 6 | category: ts.DiagnosticCategory, 7 | file = ts.getSourceFileOfNode(node), 8 | ): ts.DiagnosticWithLocation { 9 | return { 10 | category, 11 | file, 12 | messageText, 13 | start: node.getStart(), 14 | length: node.getWidth(), 15 | code: " @flamework/core" as never, 16 | }; 17 | } 18 | 19 | export class DiagnosticError extends Error { 20 | constructor(public diagnostic: ts.DiagnosticWithLocation) { 21 | super(diagnostic.messageText as string); 22 | } 23 | } 24 | 25 | export class Diagnostics { 26 | static diagnostics = new Array(); 27 | 28 | static addDiagnostic(diag: ts.DiagnosticWithLocation) { 29 | this.diagnostics.push(diag); 30 | } 31 | 32 | static createDiagnostic(node: ts.Node, category: ts.DiagnosticCategory, ...messages: string[]) { 33 | return createDiagnosticAtLocation(node, messages.join("\n"), category); 34 | } 35 | 36 | static relocate(diagnostic: ts.DiagnosticWithLocation, node: ts.Node): never { 37 | diagnostic.file = ts.getSourceFileOfNode(node); 38 | diagnostic.start = node.getStart(); 39 | diagnostic.length = node.getWidth(); 40 | throw new DiagnosticError(diagnostic); 41 | } 42 | 43 | static error(node: ts.Node, ...messages: string[]): never { 44 | throw new DiagnosticError(this.createDiagnostic(node, ts.DiagnosticCategory.Error, ...messages)); 45 | } 46 | 47 | static warning(node: ts.Node, ...messages: string[]) { 48 | this.addDiagnostic(this.createDiagnostic(node, ts.DiagnosticCategory.Warning, ...messages)); 49 | } 50 | 51 | static flush() { 52 | const diagnostics = this.diagnostics; 53 | this.diagnostics = []; 54 | 55 | return diagnostics; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/transformations/macros/intrinsics/paths.ts: -------------------------------------------------------------------------------- 1 | import { TransformState } from "../../../classes/transformState"; 2 | import path from "path"; 3 | import { f } from "../../../util/factory"; 4 | import ts from "typescript"; 5 | import { Diagnostics } from "../../../classes/diagnostics"; 6 | 7 | /** 8 | * Generates a path glob. 9 | * 10 | * This generates a string as a reference to the runtime metadata exposed in core. 11 | */ 12 | export function buildPathGlobIntrinsic(state: TransformState, node: ts.Node, pathType: ts.Type) { 13 | if (!pathType.isStringLiteral()) { 14 | Diagnostics.error( 15 | node, 16 | `Path is invalid, expected string literal and got: ${state.typeChecker.typeToString(pathType)}`, 17 | ); 18 | } 19 | 20 | const file = state.getSourceFile(node); 21 | const glob = pathType.value; 22 | const absoluteGlob = glob.startsWith(".") 23 | ? path.relative(state.rootDirectory, path.resolve(path.dirname(file.fileName), glob)).replace(/\\/g, "/") 24 | : glob; 25 | 26 | state.buildInfo.addGlob(absoluteGlob, state.getFileId(file)); 27 | return f.string(state.obfuscateText(absoluteGlob, "addPaths")); 28 | } 29 | 30 | /** 31 | * Generates a path as an array. 32 | */ 33 | export function buildPathIntrinsic(state: TransformState, node: ts.Node, pathType: ts.Type) { 34 | if (!pathType.isStringLiteral()) { 35 | Diagnostics.error( 36 | node, 37 | `Path is invalid, expected string literal and got: ${state.typeChecker.typeToString(pathType)}`, 38 | ); 39 | } 40 | 41 | const outputPath = state.pathTranslator.getOutputPath(pathType.value); 42 | const rbxPath = state.rojoResolver?.getRbxPathFromFilePath(outputPath); 43 | if (!rbxPath) { 44 | Diagnostics.error(node, `Could not find Rojo data for '${pathType.value}'`); 45 | } 46 | 47 | return f.array([f.array(rbxPath.map(f.string))]); 48 | } 49 | -------------------------------------------------------------------------------- /src/util/functions/getInstanceTypeFromType.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { Diagnostics } from "../../classes/diagnostics"; 3 | import { assert } from "./assert"; 4 | import { getDeclarationOfType } from "./getDeclarationOfType"; 5 | 6 | export function getInstanceTypeFromType(file: ts.SourceFile, type: ts.Type) { 7 | assert(type.getProperty("_nominal_Instance"), "non instance type was passed into getInstanceTypeFromType"); 8 | 9 | const diagnosticsLocation = getDeclarationOfType(type) ?? file; 10 | const nominalProperties = getNominalProperties(type); 11 | 12 | let specificType = type, 13 | specificTypeCount = 0; 14 | for (const property of nominalProperties) { 15 | const noNominalName = /_nominal_(.*)/.exec(property.name)?.[1]; 16 | assert(noNominalName); 17 | 18 | const instanceSymbol = type.checker.resolveName(noNominalName, undefined, ts.SymbolFlags.Type, false); 19 | if (!instanceSymbol) continue; 20 | 21 | const instanceDeclaration = instanceSymbol.declarations?.[0]; 22 | if (!instanceDeclaration) continue; 23 | 24 | const instanceType = type.checker.getTypeAtLocation(instanceDeclaration); 25 | const subNominalProperties = getNominalProperties(instanceType); 26 | 27 | if (subNominalProperties.length > specificTypeCount) { 28 | specificType = instanceType; 29 | specificTypeCount = subNominalProperties.length; 30 | } 31 | } 32 | 33 | // intersection between two nominal types? 34 | for (const property of nominalProperties) { 35 | if (!specificType.getProperty(property.name)) { 36 | Diagnostics.error(diagnosticsLocation, `Intersection between nominal types is forbidden.`); 37 | } 38 | } 39 | 40 | return specificType; 41 | } 42 | 43 | function getNominalProperties(type: ts.Type) { 44 | return type.getProperties().filter((x) => x.name.startsWith("_nominal_")); 45 | } 46 | -------------------------------------------------------------------------------- /src/transformations/macros/intrinsics/guards.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../../../classes/transformState"; 3 | import { Diagnostics } from "../../../classes/diagnostics"; 4 | import { f } from "../../../util/factory"; 5 | import { buildGuardFromType } from "../../../util/functions/buildGuardFromType"; 6 | import { isArrayType, isTupleType } from "../../../util/functions/isTupleType"; 7 | 8 | /** 9 | * This intrinsic generates an array of element guards along with a rest guard for a tuple type. 10 | * 11 | * Whilst this is possible in TypeScript, it requires either slightly complex types or additional metadata. 12 | * This serves as a simple fast path. 13 | */ 14 | export function buildTupleGuardsIntrinsic(state: TransformState, node: ts.Node, tupleType: ts.Type) { 15 | const file = state.getSourceFile(node); 16 | 17 | // Tuples with only a rest element will get turned into an array 18 | if (isArrayType(state, tupleType)) { 19 | const guard = buildGuardFromType(state, node, tupleType.typeArguments![0], file); 20 | return f.array([f.array([]), guard]); 21 | } 22 | 23 | if (!isTupleType(state, tupleType) || !tupleType.typeArguments) { 24 | Diagnostics.error(node, `Intrinsic encountered non-tuple type: ${state.typeChecker.typeToString(tupleType)}`); 25 | } 26 | 27 | const guards = new Array(); 28 | let restGuard: ts.Expression = f.nil(); 29 | for (let i = 0; i < tupleType.typeArguments.length; i++) { 30 | const element = tupleType.typeArguments[i]; 31 | const declaration = tupleType.target.labeledElementDeclarations?.[i]; 32 | const guard = buildGuardFromType(state, declaration ?? node, element, file); 33 | 34 | if (tupleType.target.elementFlags[i] & ts.ElementFlags.Rest) { 35 | restGuard = guard; 36 | } else { 37 | guards.push(guard); 38 | } 39 | } 40 | 41 | return f.array([f.array(guards), restGuard]); 42 | } 43 | -------------------------------------------------------------------------------- /src/transformations/transformExpression.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../classes/transformState"; 3 | import { catchDiagnostic } from "../util/diagnosticsUtils"; 4 | import { transformAccessExpression } from "./expressions/transformAccessExpression"; 5 | import { transformBinaryExpression } from "./expressions/transformBinaryExpression"; 6 | import { transformCallExpression } from "./expressions/transformCallExpression"; 7 | import { transformDeleteExpression } from "./expressions/transformDeleteExpression"; 8 | import { transformNewExpression } from "./expressions/transformNewExpression"; 9 | import { transformUnaryExpression } from "./expressions/transformUnaryExpression"; 10 | import { transformNode } from "./transformNode"; 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | const TRANSFORMERS = new Map ts.Expression>([ 14 | [ts.SyntaxKind.CallExpression, transformCallExpression], 15 | [ts.SyntaxKind.NewExpression, transformNewExpression], 16 | [ts.SyntaxKind.PrefixUnaryExpression, transformUnaryExpression], 17 | [ts.SyntaxKind.PostfixUnaryExpression, transformUnaryExpression], 18 | [ts.SyntaxKind.BinaryExpression, transformBinaryExpression], 19 | [ts.SyntaxKind.ElementAccessExpression, transformAccessExpression], 20 | [ts.SyntaxKind.PropertyAccessExpression, transformAccessExpression], 21 | [ts.SyntaxKind.DeleteExpression, transformDeleteExpression], 22 | ]); 23 | 24 | export function transformExpression(state: TransformState, expression: ts.Expression): ts.Expression { 25 | return catchDiagnostic(expression, () => { 26 | const transformer = TRANSFORMERS.get(expression.kind); 27 | if (transformer) { 28 | return transformer(state, expression); 29 | } 30 | return ts.visitEachChild(expression, (newNode) => transformNode(state, newNode), state.context); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/transformations/expressions/transformAccessExpression.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../../classes/transformState"; 3 | import { f } from "../../util/factory"; 4 | import { Diagnostics } from "../../classes/diagnostics"; 5 | 6 | export function transformAccessExpression( 7 | state: TransformState, 8 | node: ts.PropertyAccessExpression | ts.ElementAccessExpression, 9 | ) { 10 | return transformNetworkEvent(state, node) ?? state.transform(node); 11 | } 12 | 13 | function transformNetworkEvent(state: TransformState, node: ts.PropertyAccessExpression | ts.ElementAccessExpression) { 14 | const type = state.typeChecker.getTypeAtLocation(node.expression); 15 | const hashType = state.typeChecker.getTypeOfPropertyOfType(type, "_flamework_key_obfuscation"); 16 | if (!hashType || !hashType.isStringLiteral()) return; 17 | 18 | // If the access expression doesn't have a name known at compile-time, we must throw an error. 19 | const name = getAccessName(node); 20 | if (name === undefined) { 21 | // This is prevents compiler errors when we're defining obfuscated objects, or accessing them internally. 22 | if (f.is.elementAccessExpression(node) && f.is.asExpression(node.argumentExpression)) { 23 | return; 24 | } 25 | 26 | Diagnostics.error(node, "This object has key obfuscation enabled and must be accessed directly."); 27 | } 28 | 29 | return f.elementAccessExpression( 30 | state.transformNode(node.expression), 31 | f.as(f.string(state.obfuscateText(name, hashType.value)), f.literalType(f.string(name))), 32 | node.questionDotToken, 33 | ); 34 | } 35 | 36 | function getAccessName(node: ts.PropertyAccessExpression | ts.ElementAccessExpression) { 37 | if (f.is.propertyAccessExpression(node)) { 38 | return node.name.text; 39 | } else { 40 | if (f.is.string(node.argumentExpression)) { 41 | return node.argumentExpression.text; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/util/diagnosticsUtils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import ts from "typescript"; 3 | import { DiagnosticError, Diagnostics } from "../classes/diagnostics"; 4 | 5 | type ValueOrDiagnostic = 6 | | { success: true; diagnostic?: ts.DiagnosticWithLocation; value: T } 7 | | { success: false; diagnostic: ts.DiagnosticWithLocation; value?: T }; 8 | 9 | export function captureDiagnostic(cb: (...args: A) => T, ...args: A): ValueOrDiagnostic { 10 | try { 11 | return { success: true, value: cb(...args) }; 12 | } catch (e: any) { 13 | if ("diagnostic" in e) { 14 | /// Temporary workaround for 1.1.1 15 | if ( 16 | ts.version.startsWith("1.1.1") && 17 | !ts.version.startsWith("1.1.1-dev") && 18 | !(globalThis as { RBXTSC_DEV?: boolean }).RBXTSC_DEV 19 | ) { 20 | e.diagnostic = undefined; 21 | throw e; 22 | } 23 | 24 | return { success: false, diagnostic: e.diagnostic }; 25 | } 26 | throw e; 27 | } 28 | } 29 | 30 | export function relocateDiagnostic(node: ts.Node, cb: (...args: A) => T, ...params: A): T { 31 | const result = captureDiagnostic(cb, ...params); 32 | 33 | if (result.success) { 34 | return result.value; 35 | } 36 | 37 | Diagnostics.relocate(result.diagnostic, node); 38 | } 39 | 40 | export function catchDiagnostic(fallback: T, cb: () => T): T { 41 | const result = captureDiagnostic(cb); 42 | 43 | if (!result.success) { 44 | Diagnostics.addDiagnostic(result.diagnostic); 45 | } 46 | 47 | return result.value ?? fallback; 48 | } 49 | 50 | export function withDiagnosticContext(node: ts.Node, message: (() => string) | string, callback: () => T) { 51 | const result = captureDiagnostic(callback); 52 | if (!result.success) { 53 | const newDiagnostic = Diagnostics.createDiagnostic( 54 | node, 55 | ts.DiagnosticCategory.Error, 56 | typeof message === "string" ? message : message(), 57 | ); 58 | 59 | ts.addRelatedInfo(newDiagnostic, result.diagnostic); 60 | for (const relatedInfo of result.diagnostic.relatedInformation ?? []) { 61 | ts.addRelatedInfo(newDiagnostic, relatedInfo); 62 | } 63 | 64 | throw new DiagnosticError(newDiagnostic); 65 | } 66 | 67 | return result.value; 68 | } 69 | -------------------------------------------------------------------------------- /src/transformations/macros/intrinsics/parameters.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { DiagnosticError, Diagnostics } from "../../../classes/diagnostics"; 3 | 4 | /** 5 | * Validates that the specified parameters can be inspected at compile-time (up to a depth of 1) 6 | */ 7 | export function validateParameterConstIntrinsic( 8 | node: ts.NewExpression | ts.CallExpression, 9 | signature: ts.Signature, 10 | parameters: ts.Symbol[], 11 | ) { 12 | for (const parameter of parameters) { 13 | const parameterIndex = signature.parameters.findIndex((v) => v.valueDeclaration?.symbol === parameter); 14 | const argument = node.arguments?.[parameterIndex]; 15 | if (!argument) { 16 | continue; 17 | } 18 | 19 | // Check if the argument is a literal (string, number, etc) 20 | if (ts.isLiteralExpression(argument)) { 21 | continue; 22 | } 23 | 24 | const elements = ts.isObjectLiteralExpression(argument) 25 | ? argument.properties 26 | : ts.isArrayLiteralExpression(argument) 27 | ? argument.elements 28 | : undefined; 29 | 30 | const parameterDiagnostic = Diagnostics.createDiagnostic( 31 | parameter.valueDeclaration ?? argument, 32 | ts.DiagnosticCategory.Message, 33 | "Required because this parameter must be known at compile-time.", 34 | ); 35 | 36 | // This argument is not an object or array literal. 37 | if (!elements) { 38 | const baseDiagnostic = Diagnostics.createDiagnostic( 39 | argument, 40 | ts.DiagnosticCategory.Error, 41 | "Flamework expected this argument to be a literal expression.", 42 | ); 43 | 44 | ts.addRelatedInfo(baseDiagnostic, parameterDiagnostic); 45 | 46 | throw new DiagnosticError(baseDiagnostic); 47 | } 48 | 49 | // We also want to validate that there are no spread operations inside the literal. 50 | for (const element of elements) { 51 | if (ts.isSpreadElement(element) || ts.isSpreadAssignment(element)) { 52 | const baseDiagnostic = Diagnostics.createDiagnostic( 53 | element, 54 | ts.DiagnosticCategory.Error, 55 | "Flamework does not support spread expressions in this location.", 56 | ); 57 | 58 | ts.addRelatedInfo(baseDiagnostic, parameterDiagnostic); 59 | 60 | throw new DiagnosticError(baseDiagnostic); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/util/functions/emitTypescriptMismatch.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import path from "path"; 3 | import ts from "typescript"; 4 | import { Logger } from "../../classes/logger"; 5 | import { TransformState } from "../../classes/transformState"; 6 | import { getPackageJson } from "./getPackageJson"; 7 | import { isPathDescendantOf } from "./isPathDescendantOf"; 8 | 9 | function tryResolve(name: string, path: string) { 10 | try { 11 | return require.resolve(name, { paths: [path] }); 12 | } catch (e) {} 13 | } 14 | 15 | function emitMessages(messages: string[]): never { 16 | Logger.writeLine(...messages); 17 | process.exit(1); 18 | } 19 | 20 | /** 21 | * Spits out information about the mismatch. 22 | * This should only be called after a mismatch is detected. 23 | */ 24 | export function emitTypescriptMismatch(state: TransformState, baseMessage: string): never { 25 | const messages = [baseMessage]; 26 | 27 | // Check if they have a local install. 28 | const robloxTsPath = tryResolve("roblox-ts", state.rootDirectory); 29 | if (!robloxTsPath) { 30 | messages.push( 31 | "It is recommended that you use a local install of roblox-ts.", 32 | `You can install a local version using ${chalk.green("npm install -D roblox-ts")}`, 33 | ); 34 | emitMessages(messages); 35 | } 36 | 37 | // Check if they've used a global install. 38 | if (require.main) { 39 | if (!isPathDescendantOf(require.main.filename, path.join(state.rootDirectory, "node_modules"))) { 40 | messages.push( 41 | "It appears you've run the transformer using a global install.", 42 | `You can run using the locally installed version using ${chalk.green("npx rbxtsc")}`, 43 | ); 44 | emitMessages(messages); 45 | } 46 | } 47 | 48 | // They're using a local install 49 | // but they're using the wrong TypeScript version. 50 | const robloxTsTypeScript = tryResolve("typescript", robloxTsPath); 51 | if (robloxTsTypeScript) { 52 | const typescriptPackage = getPackageJson(robloxTsTypeScript); 53 | if (typescriptPackage) { 54 | const requiredVersion = typescriptPackage.result.version; 55 | if (ts.version !== requiredVersion) { 56 | messages.push( 57 | `Flamework is using TypeScript version ${ts.version}`, 58 | `roblox-ts requires TypeScript version ${requiredVersion}`, 59 | `You can fix this by setting your TypeScript version: ${chalk.green( 60 | `npm install -D typescript@=${requiredVersion}`, 61 | )}`, 62 | ); 63 | } 64 | } 65 | } 66 | 67 | emitMessages(messages); 68 | } 69 | -------------------------------------------------------------------------------- /flamework-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "#root", 4 | "type": "object", 5 | "properties": { 6 | "config": { 7 | "$id": "config", 8 | "type": "object", 9 | "properties": { 10 | "logLevel": { 11 | "enum": ["none", "verbose"] 12 | }, 13 | "profiling": { 14 | "type": "boolean" 15 | }, 16 | "disableDependencyWarnings": { 17 | "type": "boolean" 18 | } 19 | } 20 | }, 21 | "globs": { 22 | "$id": "globs", 23 | "type": "object", 24 | "properties": { 25 | "paths": { 26 | "type": "object", 27 | "additionalProperties": { 28 | "type": "array", 29 | "items": { 30 | "type": "string" 31 | } 32 | } 33 | }, 34 | "origins": { 35 | "type": "object", 36 | "additionalProperties": { 37 | "type": "array", 38 | "items": { 39 | "type": "string" 40 | } 41 | } 42 | } 43 | } 44 | }, 45 | "buildInfo": { 46 | "$id": "buildInfo", 47 | "required": ["version", "flameworkVersion", "identifiers"], 48 | "type": "object", 49 | "properties": { 50 | "version": { 51 | "type": "number" 52 | }, 53 | "flameworkVersion": { 54 | "type": "string" 55 | }, 56 | "identifierPrefix": { 57 | "type": "string" 58 | }, 59 | "identifiers": { 60 | "type": "object", 61 | "additionalProperties": { 62 | "type": "string" 63 | } 64 | }, 65 | "salt": { 66 | "type": "string" 67 | }, 68 | "stringHashes": { 69 | "type": "object", 70 | "additionalProperties": { 71 | "type": "string" 72 | } 73 | }, 74 | "metadata": { 75 | "type": "object", 76 | "properties": { 77 | "config": { 78 | "$ref": "config" 79 | }, 80 | "globs": { 81 | "$ref": "globs" 82 | } 83 | } 84 | }, 85 | "classes": { 86 | "type": "array", 87 | "items": { 88 | "type": "object", 89 | "properties": { 90 | "filePath": { 91 | "type": "string" 92 | }, 93 | "internalId": { 94 | "type": "string" 95 | }, 96 | "decorators": { 97 | "type": "array", 98 | "items": { 99 | "type": "object", 100 | "properties": { 101 | "name": { 102 | "type": "string" 103 | }, 104 | "internalId": { 105 | "type": "string" 106 | } 107 | } 108 | } 109 | } 110 | 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/transformer.ts: -------------------------------------------------------------------------------- 1 | import {} from "ts-expose-internals"; 2 | import ts from "typescript"; 3 | import path from "path"; 4 | import { transformFile } from "./transformations/transformFile"; 5 | import { TransformerConfig, TransformState } from "./classes/transformState"; 6 | import { Logger } from "./classes/logger"; 7 | import { viewFile } from "./information/viewFile"; 8 | import { f } from "./util/factory"; 9 | import chalk from "chalk"; 10 | import { PKG_VERSION } from "./classes/pathTranslator/constants"; 11 | import { emitTypescriptMismatch } from "./util/functions/emitTypescriptMismatch"; 12 | 13 | export default function (program: ts.Program, config?: TransformerConfig) { 14 | return (context: ts.TransformationContext): ((file: ts.SourceFile) => ts.Node) => { 15 | if (Logger.verbose) Logger.write("\n"); 16 | f.setFactory(context.factory); 17 | 18 | const state = new TransformState(program, context, config ?? {}); 19 | let hasCollectedInformation = false; 20 | 21 | const projectFlameworkVersion = state.buildInfo.getFlameworkVersion(); 22 | if (projectFlameworkVersion !== PKG_VERSION) { 23 | Logger.writeLine( 24 | `${chalk.red("Project was compiled on different version of Flamework.")}`, 25 | `Please recompile by deleting the ${path.relative(state.currentDirectory, state.outDir)} directory`, 26 | `Current Flamework Version: ${chalk.yellow(PKG_VERSION)}`, 27 | `Previous Flamework Version: ${chalk.yellow(projectFlameworkVersion)}`, 28 | ); 29 | process.exit(1); 30 | } 31 | 32 | setTimeout(() => state.saveArtifacts()); 33 | return (file: ts.SourceFile) => { 34 | if (!ts.isSourceFile(file)) { 35 | emitTypescriptMismatch(state, chalk.red("Failed to load! TS version mismatch detected")); 36 | } 37 | 38 | if (state.config.noSemanticDiagnostics !== true) { 39 | const originalFile = ts.getParseTreeNode(file, ts.isSourceFile); 40 | if (originalFile) { 41 | const preEmitDiagnostics = ts.getPreEmitDiagnostics(program, originalFile); 42 | if (preEmitDiagnostics.some((x) => x.category === ts.DiagnosticCategory.Error)) { 43 | preEmitDiagnostics 44 | .filter(ts.isDiagnosticWithLocation) 45 | .forEach((diag) => context.addDiagnostic(diag)); 46 | return file; 47 | } 48 | } else { 49 | const relativeName = path.relative(state.srcDir, file.fileName); 50 | Logger.warn(`Failed to validate '${relativeName}' due to lack of parse tree node.`); 51 | } 52 | } 53 | 54 | if (!hasCollectedInformation) { 55 | hasCollectedInformation = true; 56 | 57 | program.getSourceFiles().forEach((file) => { 58 | if (file.isDeclarationFile && !state.shouldViewFile(file)) return; 59 | 60 | viewFile(state, file); 61 | }); 62 | } 63 | 64 | const result = transformFile(state, file); 65 | return result; 66 | }; 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/transformations/expressions/transformBinaryExpression.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { Diagnostics } from "../../classes/diagnostics"; 3 | import { TransformState } from "../../classes/transformState"; 4 | import { f } from "../../util/factory"; 5 | import { getIndexExpression } from "../../util/functions/getIndexExpression"; 6 | import { isAttributesAccess } from "../../util/functions/isAttributesAccess"; 7 | 8 | const MUTATING_OPERATORS = new Map([ 9 | [ts.SyntaxKind.EqualsToken, ts.SyntaxKind.EqualsToken], 10 | [ts.SyntaxKind.BarEqualsToken, ts.SyntaxKind.BarToken], 11 | [ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.PlusToken], 12 | [ts.SyntaxKind.MinusEqualsToken, ts.SyntaxKind.MinusToken], 13 | [ts.SyntaxKind.CaretEqualsToken, ts.SyntaxKind.CaretToken], 14 | [ts.SyntaxKind.SlashEqualsToken, ts.SyntaxKind.SlashToken], 15 | [ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.BarBarToken], 16 | [ts.SyntaxKind.PercentEqualsToken, ts.SyntaxKind.PercentToken], 17 | [ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskToken], 18 | [ts.SyntaxKind.AmpersandEqualsToken, ts.SyntaxKind.AmpersandToken], 19 | [ts.SyntaxKind.QuestionQuestionEqualsToken, ts.SyntaxKind.QuestionQuestionToken], 20 | [ts.SyntaxKind.AsteriskAsteriskEqualsToken, ts.SyntaxKind.AsteriskAsteriskToken], 21 | [ts.SyntaxKind.LessThanLessThanEqualsToken, ts.SyntaxKind.LessThanLessThanToken], 22 | [ts.SyntaxKind.AmpersandAmpersandEqualsToken, ts.SyntaxKind.AmpersandAmpersandToken], 23 | [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, ts.SyntaxKind.GreaterThanGreaterThanToken], 24 | [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken], 25 | ]); 26 | 27 | export function transformBinaryExpression(state: TransformState, node: ts.BinaryExpression) { 28 | const nonAssignmentOperator = MUTATING_OPERATORS.get(node.operatorToken.kind); 29 | if (nonAssignmentOperator) { 30 | if (isAttributesAccess(state, node.left)) { 31 | const name = getIndexExpression(node.left); 32 | if (!name) Diagnostics.error(node.left, "could not get index expression"); 33 | 34 | if (!f.is.accessExpression(node.left.expression)) 35 | Diagnostics.error(node.left, "assignments not supported with direct access"); 36 | 37 | const attributeSetter = state.addFileImport( 38 | node.getSourceFile(), 39 | "@flamework/components/out/baseComponent", 40 | "SYMBOL_ATTRIBUTE_SETTER", 41 | ); 42 | const thisAccess = node.left.expression.expression; 43 | const valueExpr = 44 | nonAssignmentOperator === ts.SyntaxKind.EqualsToken 45 | ? node.right 46 | : f.binary(node.left, nonAssignmentOperator, node.right); 47 | 48 | return f.call(f.field(thisAccess, attributeSetter, true), [name, valueExpr]); 49 | } 50 | } 51 | return state.transform(node); 52 | } 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import { existsSync } from "fs"; 3 | import { Module } from "module"; 4 | import path from "path"; 5 | import { isPathDescendantOf } from "./util/functions/isPathDescendantOf"; 6 | import { Logger } from "./classes/logger"; 7 | import { tryResolve } from "./util/functions/tryResolve"; 8 | 9 | const cwd = process.cwd(); 10 | const originalRequire = Module.prototype.require; 11 | 12 | function shouldTryHooking() { 13 | if (process.argv.includes("--no-flamework-hook")) { 14 | return false; 15 | } 16 | 17 | if (process.argv.includes("--force-flamework-hook")) { 18 | return true; 19 | } 20 | 21 | // Ensure we're running in the context of a project and not a multiplace repository or something, 22 | // as we don't have access to the project directory until roblox-ts invokes the transformer. 23 | if ( 24 | !existsSync(path.join(cwd, "tsconfig.json")) || 25 | !existsSync(path.join(cwd, "package.json")) || 26 | !existsSync(path.join(cwd, "node_modules")) 27 | ) { 28 | return false; 29 | } 30 | 31 | return true; 32 | } 33 | 34 | function hook() { 35 | const robloxTsPath = tryResolve("roblox-ts", cwd); 36 | if (!robloxTsPath) { 37 | return; 38 | } 39 | 40 | const robloxTsTypeScriptPath = tryResolve("typescript", robloxTsPath); 41 | if (!robloxTsTypeScriptPath) { 42 | return; 43 | } 44 | 45 | const flameworkTypeScript = require("typescript"); 46 | const robloxTsTypeScript = require(robloxTsTypeScriptPath); 47 | 48 | // Flamework and roblox-ts are referencing the same TypeScript module. 49 | if (flameworkTypeScript === robloxTsTypeScript) { 50 | return; 51 | } 52 | 53 | if (flameworkTypeScript.versionMajorMinor !== robloxTsTypeScript.versionMajorMinor) { 54 | if (Logger.verbose) { 55 | Logger.write("\n"); 56 | } 57 | 58 | Logger.warn( 59 | "TypeScript version differs", 60 | `Flamework: v${flameworkTypeScript.version}, roblox-ts: v${robloxTsTypeScript.version}`, 61 | `Flamework will switch to v${robloxTsTypeScript.version}, ` + 62 | `but you can get rid of this warning by running: npm i -D typescript@${robloxTsTypeScript.version}`, 63 | ); 64 | } 65 | 66 | Module.prototype.require = function flameworkHook(this: NodeJS.Module, id) { 67 | // Overwrite any Flamework TypeScript imports to roblox-ts' version. 68 | // To be on the safe side, this won't hook it in packages. 69 | if (id === "typescript" && isPathDescendantOf(this.filename, __dirname)) { 70 | return robloxTsTypeScript; 71 | } 72 | 73 | return originalRequire.call(this, id); 74 | } as NodeJS.Require; 75 | } 76 | 77 | if (shouldTryHooking()) { 78 | hook(); 79 | } 80 | 81 | const transformer = require("./transformer"); 82 | 83 | // After loading Flamework, we can unhook require. 84 | Module.prototype.require = originalRequire; 85 | 86 | export = transformer; 87 | -------------------------------------------------------------------------------- /src/classes/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | interface Label { 4 | label: string; 5 | start: number; 6 | } 7 | 8 | function getTime() { 9 | const hrTime = process.hrtime(); 10 | return hrTime[0] * 1000 + hrTime[1] / 1e6; 11 | } 12 | 13 | export class Logger { 14 | public static debug = true; 15 | public static verbose = process.argv.includes("--verbose"); 16 | 17 | private static timerTotals = new Map(); 18 | private static timers = new Array<[string, number]>(); 19 | private static timerHandle?: NodeJS.Timeout; 20 | 21 | static timer(name: string) { 22 | this.queueTimer(); 23 | this.timers.push([name, getTime()]); 24 | } 25 | 26 | static timerEnd() { 27 | const timer = this.timers.pop(); 28 | if (!timer) return; 29 | 30 | let currentValue = this.timerTotals.get(timer[0]); 31 | if (!currentValue) this.timerTotals.set(timer[0], (currentValue = [0, 0])); 32 | 33 | currentValue[0] += getTime() - timer[1]; 34 | currentValue[1]++; 35 | } 36 | 37 | static queueTimer() { 38 | if (this.timerHandle !== undefined) { 39 | this.timerHandle.refresh(); 40 | return; 41 | } 42 | 43 | this.timerHandle = setTimeout(() => { 44 | const totals = this.timerTotals; 45 | this.timerTotals = new Map(); 46 | 47 | for (const [name, [total, count]] of totals) { 48 | console.log(`Timer '${name}' took ${total.toFixed(2)}ms (${count})`); 49 | } 50 | }); 51 | } 52 | 53 | static write(message: string) { 54 | process.stdout.write(message); 55 | } 56 | 57 | static writeLine(...messages: Array) { 58 | if (!this.debug) return; 59 | 60 | for (const message of messages) { 61 | const text = typeof message === "string" ? `${message}` : `${JSON.stringify(message, undefined, "\t")}`; 62 | 63 | const flameworkPrefix = `[${chalk.gray("Flamework")}]: `; 64 | this.write(`${flameworkPrefix}${text.replace(/\n/g, `\n${flameworkPrefix}`)}\n`); 65 | } 66 | } 67 | 68 | static writeLineIfVerbose(...messages: Array) { 69 | if (this.verbose) return this.writeLine(...messages); 70 | } 71 | 72 | static info(...messages: Array) { 73 | this.writeLine(...messages.map((x) => chalk.blue(x))); 74 | } 75 | 76 | static infoIfVerbose(...messages: Array) { 77 | if (this.verbose) return this.info(...messages); 78 | } 79 | 80 | static warn(...messages: Array) { 81 | this.writeLine(...messages.map((x) => chalk.yellow(x))); 82 | } 83 | 84 | static warnIfVerbose(...messages: Array) { 85 | if (this.verbose) return this.warn(...messages); 86 | } 87 | 88 | static error(...messages: Array) { 89 | this.writeLine(...messages.map((x) => chalk.red(x))); 90 | } 91 | 92 | private static benchmarkLabels: Label[] = []; 93 | private static benchmarkOutput = ""; 94 | static benchmark(label: string) { 95 | if (!this.debug) return; 96 | 97 | const depth = this.benchmarkLabels.length; 98 | this.benchmarkLabels.push({ 99 | start: new Date().getTime(), 100 | label, 101 | }); 102 | this.benchmarkOutput += `${"\t".repeat(depth)}Begin ${label}\n`; 103 | } 104 | 105 | static benchmarkEnd() { 106 | if (!this.debug) return; 107 | 108 | const label = this.benchmarkLabels.pop(); 109 | const depth = this.benchmarkLabels.length; 110 | if (!label) throw new Error(`Unexpected benchmarkEnd()`); 111 | 112 | const timeDifference = new Date().getTime() - label.start; 113 | this.benchmarkOutput += `${"\t".repeat(depth)}End ${label.label} (${timeDifference}ms)\n`; 114 | 115 | if (depth === 0) { 116 | this.info(this.benchmarkOutput); 117 | this.benchmarkOutput = ""; 118 | } 119 | } 120 | 121 | static { 122 | // Workaround for vscode PTY not having color highlighting. 123 | if (process.env.VSCODE_CWD !== undefined) { 124 | // ANSI 256 125 | chalk.level = 2; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/information/statements/viewClassDeclaration.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import path from "path"; 3 | import { TransformState } from "../../classes/transformState"; 4 | import { ClassInfo } from "../../types/classes"; 5 | import { DecoratorInfo } from "../../types/decorators"; 6 | import { f } from "../../util/factory"; 7 | import { getNodeUid, getSymbolUid } from "../../util/uid"; 8 | import { NodeMetadata } from "../../classes/nodeMetadata"; 9 | 10 | export function viewClassDeclaration(state: TransformState, node: ts.ClassDeclaration) { 11 | const symbol = state.getSymbol(node); 12 | const internalId = getNodeUid(state, node); 13 | 14 | if (!node.name || !symbol) return; 15 | 16 | const nodeDecorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined; 17 | const decorators: DecoratorInfo[] = []; 18 | 19 | if (nodeDecorators) { 20 | for (const decorator of nodeDecorators) { 21 | if (!f.is.call(decorator.expression)) continue; 22 | 23 | const symbol = state.getSymbol(decorator.expression.expression); 24 | if (!symbol) continue; 25 | if (!symbol.declarations?.[0]) continue; 26 | if (!f.is.identifier(decorator.expression.expression)) continue; 27 | 28 | const name = decorator.expression.expression.text; 29 | 30 | decorators.push({ 31 | type: "WithNodes", 32 | declaration: symbol.declarations[0], 33 | arguments: decorator.expression.arguments.map((x) => x), 34 | internalId: getSymbolUid(state, symbol, decorator.expression.expression), 35 | name, 36 | symbol, 37 | }); 38 | } 39 | } 40 | 41 | const flameworkDecorators = hasFlameworkDecorators(state, node); 42 | const isFlameworkClass = flameworkDecorators || hasReflectMetadata(state, node); 43 | if (isFlameworkClass) { 44 | const classInfo: ClassInfo = { 45 | name: node.name.text, 46 | containsLegacyDecorator: flameworkDecorators, 47 | internalId, 48 | node, 49 | decorators, 50 | symbol, 51 | }; 52 | 53 | state.classes.set(symbol, classInfo); 54 | 55 | if (!state.isGame && !state.buildInfo.getBuildClass(internalId)) { 56 | const filePath = state.pathTranslator.getOutputPath(state.getSourceFile(node).fileName); 57 | const relativePath = path.relative(state.currentDirectory, filePath); 58 | state.buildInfo.addBuildClass({ 59 | filePath: relativePath, 60 | internalId, 61 | decorators: decorators.map((x) => ({ 62 | internalId: x.internalId, 63 | name: x.name, 64 | })), 65 | }); 66 | } 67 | } else { 68 | const buildClass = state.buildInfo.getBuildClass(internalId); 69 | if (buildClass) { 70 | state.classes.set(symbol, { 71 | internalId, 72 | node, 73 | symbol, 74 | name: node.name.text, 75 | decorators: buildClass.decorators.map((x) => ({ 76 | type: "Base", 77 | internalId: x.internalId, 78 | name: x.name, 79 | })), 80 | containsLegacyDecorator: false, 81 | }); 82 | } 83 | } 84 | } 85 | 86 | function hasReflectMetadata(state: TransformState, declaration: ts.ClassDeclaration) { 87 | const metadata = new NodeMetadata(state, declaration); 88 | if (metadata.isRequested("reflect")) { 89 | return true; 90 | } 91 | 92 | return false; 93 | } 94 | 95 | function hasFlameworkDecorators(state: TransformState, declaration: ts.ClassDeclaration) { 96 | const nodeDecorators = ts.canHaveDecorators(declaration) ? ts.getDecorators(declaration) : undefined; 97 | if (nodeDecorators && nodeDecorators.some((v) => isFlameworkDecorator(state, v))) { 98 | return true; 99 | } 100 | 101 | for (const member of declaration.members) { 102 | const nodeDecorators = ts.canHaveDecorators(member) ? ts.getDecorators(member) : undefined; 103 | if (nodeDecorators && nodeDecorators.some((v) => isFlameworkDecorator(state, v))) { 104 | return true; 105 | } 106 | } 107 | 108 | return false; 109 | } 110 | 111 | function isFlameworkDecorator(state: TransformState, decorator: ts.Decorator) { 112 | const decoratorType = state.typeChecker.getTypeAtLocation(decorator.expression); 113 | if (decoratorType.getProperty("_flamework_Decorator")) { 114 | return true; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/transformations/macros/intrinsics/networking.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../../../classes/transformState"; 3 | import assert from "assert"; 4 | import { f } from "../../../util/factory"; 5 | import { Diagnostics } from "../../../classes/diagnostics"; 6 | import { UserMacro } from "../../transformUserMacro"; 7 | import { getNodeUid } from "../../../util/uid"; 8 | 9 | /** 10 | * Obfuscates the names of events provided in networking middleware. 11 | * 12 | * This should eventually be replaced with a generic object obfuscation API. 13 | */ 14 | export function transformNetworkingMiddlewareIntrinsic( 15 | state: TransformState, 16 | signature: ts.Signature, 17 | args: ts.Expression[], 18 | parameters: ts.Symbol[], 19 | ) { 20 | for (const parameter of parameters) { 21 | const parameterIndex = signature.parameters.findIndex((v) => v.valueDeclaration?.symbol === parameter); 22 | const argument = args[parameterIndex]; 23 | if (!argument || !ts.isObjectLiteralExpression(argument)) { 24 | continue; 25 | } 26 | 27 | const obfuscateMiddleware = (middlewareObject: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression => { 28 | return f.update.object( 29 | middlewareObject, 30 | state.obfuscateArray(middlewareObject.properties).map((prop) => { 31 | if (f.is.propertyAssignmentDeclaration(prop) && "text" in prop.name) { 32 | return f.update.propertyAssignmentDeclaration( 33 | prop, 34 | f.is.object(prop.initializer) ? obfuscateMiddleware(prop.initializer) : prop.initializer, 35 | f.computedPropertyName( 36 | f.as( 37 | f.string(state.obfuscateText(prop.name.text, "remotes")), 38 | f.literalType(f.string(prop.name.text)), 39 | ), 40 | ), 41 | ); 42 | } 43 | return prop; 44 | }), 45 | ); 46 | }; 47 | 48 | const transformedElements = argument.properties.map((element) => { 49 | const name = element.name && ts.getPropertyNameForPropertyNameNode(element.name); 50 | if (name !== "middleware") { 51 | return element; 52 | } 53 | 54 | assert(f.is.propertyAssignmentDeclaration(element)); 55 | 56 | const value = element.initializer; 57 | if (!f.is.object(value)) { 58 | Diagnostics.error(value, "Networking middleware must be an object."); 59 | } 60 | 61 | return f.update.propertyAssignmentDeclaration(element, obfuscateMiddleware(value)); 62 | }); 63 | 64 | args[parameterIndex] = f.object(transformedElements, true); 65 | } 66 | } 67 | 68 | /** 69 | * Obfuscates the keys of user macro metadata using the specified context. 70 | * 71 | * This should eventually be replaced with a generic object obfuscation API. 72 | */ 73 | export function transformObfuscatedObjectIntrinsic(state: TransformState, macro: UserMacro, hashType: ts.Type) { 74 | const hashContext = hashType.isStringLiteral() ? hashType.value : undefined; 75 | 76 | if (macro.kind === "many" && macro.members instanceof Map) { 77 | // Maps are order-preserving, so we can shuffle the map directly. 78 | for (const [key, inner] of state.obfuscateArray([...macro.members])) { 79 | macro.members.delete(key); 80 | macro.members.set(state.obfuscateText(key, hashContext), inner); 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * Shuffles the order of an array to prevent const-matching. 87 | */ 88 | export function transformShuffleArrayIntrinsic(state: TransformState, macro: UserMacro) { 89 | if (macro.kind === "many" && Array.isArray(macro.members)) { 90 | macro.members = state.obfuscateArray(macro.members) as UserMacro[]; 91 | } 92 | } 93 | 94 | /** 95 | * Gets the ID of the macro's containing statement (e.g its variable.) 96 | * 97 | * This should eventually be replaced with a field in `Modding.Caller` 98 | */ 99 | export function buildDeclarationUidIntrinsic(state: TransformState, node: ts.Node) { 100 | const parentDeclaration = ts.findAncestor(node, f.is.namedDeclaration); 101 | if (!parentDeclaration) { 102 | Diagnostics.error(node, "This function must be under a variable declaration."); 103 | } 104 | 105 | return f.string(getNodeUid(state, parentDeclaration)); 106 | } 107 | -------------------------------------------------------------------------------- /src/classes/nodeMetadata.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { f } from "../util/factory"; 3 | import { TransformState } from "./transformState"; 4 | 5 | export class NodeMetadata { 6 | public static fromSymbol(state: TransformState, symbol: ts.Symbol) { 7 | if (symbol.valueDeclaration) { 8 | return new NodeMetadata(state, symbol.valueDeclaration); 9 | } 10 | } 11 | 12 | private set = new Set(); 13 | private symbols = new Map>(); 14 | private types = new Map>(); 15 | private trace = new Map(); 16 | 17 | private parseText(text: string, node: ts.Node) { 18 | for (const name of text.trim().replace(/\s+/, " ").split(" ")) { 19 | this.set.add(name); 20 | this.trace.set(name, node); 21 | } 22 | } 23 | 24 | private parseMetadata(state: TransformState, tag: ts.JSDocTag) { 25 | if (typeof tag.comment === "string") { 26 | this.parseText(tag.comment, tag); 27 | } else if (tag.comment) { 28 | for (const comment of tag.comment) { 29 | if (ts.isJSDocLinkLike(comment)) { 30 | if (!comment.name) continue; 31 | 32 | const symbol = state.getSymbol(comment.name); 33 | if (!symbol) continue; 34 | 35 | const type = 36 | symbol.flags & ts.SymbolFlags.TypeAlias 37 | ? state.typeChecker.getDeclaredTypeOfSymbol(symbol) 38 | : state.typeChecker.getTypeAtLocation(comment.name); 39 | 40 | let symbols = this.symbols.get(comment.text); 41 | let types = this.types.get(comment.text); 42 | if (!types) this.types.set(comment.text, (types = [])); 43 | if (!symbols) this.symbols.set(comment.text, (symbols = [])); 44 | 45 | symbols.push(symbol); 46 | types.push(type); 47 | this.trace.set(symbol, comment); 48 | this.trace.set(type, comment); 49 | } else { 50 | this.parseText(comment.text, comment); 51 | } 52 | } 53 | } 54 | } 55 | 56 | private parse(state: TransformState, node: ts.Node) { 57 | const tags = ts.getJSDocTags(node); 58 | for (const tag of tags) { 59 | if (tag.tagName.text === "metadata") { 60 | this.parseMetadata(state, tag); 61 | } 62 | } 63 | 64 | const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined; 65 | if (decorators) { 66 | for (const decorator of decorators) { 67 | const expression = decorator.expression; 68 | const symbol = state.getSymbol(f.is.call(expression) ? expression.expression : expression); 69 | if (!symbol || !symbol.declarations) continue; 70 | 71 | for (const declaration of symbol.declarations) { 72 | this.parse(state, declaration); 73 | } 74 | } 75 | } 76 | 77 | if (ts.isClassElement(node) && node.name) { 78 | // Interfaces are able to request metadata for their own property/methods. 79 | const name = ts.getNameFromPropertyName(node.name); 80 | if (name && ts.isClassLike(node.parent)) { 81 | const implementNodes = ts.getEffectiveImplementsTypeNodes(node.parent); 82 | if (implementNodes) { 83 | for (const implement of implementNodes) { 84 | const symbol = state.getSymbol(implement.expression); 85 | const member = symbol?.members?.get(ts.escapeLeadingUnderscores(name)); 86 | if (member && member.declarations) { 87 | for (const declaration of member.declarations) { 88 | this.parse(state, declaration); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } else if (ts.isClassLike(node)) { 95 | // Interfaces are able to request metadata for the object it is implemented on. 96 | const implementNodes = ts.getEffectiveImplementsTypeNodes(node); 97 | if (implementNodes) { 98 | for (const implement of implementNodes) { 99 | const symbol = state.getSymbol(implement.expression); 100 | if (symbol && symbol.declarations?.[0]) { 101 | this.parse(state, symbol.declarations[0]); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | constructor(state: TransformState, node: ts.Node) { 109 | this.parse(state, node); 110 | } 111 | 112 | isRequested(metadata: string) { 113 | if (this.set.has(`~${metadata}`)) { 114 | return false; 115 | } 116 | 117 | return this.set.has(metadata) || this.set.has("*"); 118 | } 119 | 120 | getSymbol(key: string) { 121 | return this.symbols.get(key); 122 | } 123 | 124 | getType(key: string) { 125 | return this.types.get(key); 126 | } 127 | 128 | getTrace(name: string | ts.Symbol | ts.Type) { 129 | return this.trace.get(name); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/classes/pathTranslator/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { D_EXT, INDEX_NAME, INIT_NAME, LUA_EXT, TSX_EXT, TS_EXT } from "./constants"; 3 | import { assert } from "../../util/functions/assert"; 4 | 5 | export class PathInfo { 6 | private constructor(public dirName: string, public fileName: string, public exts: Array) {} 7 | 8 | public static from(filePath: string) { 9 | const dirName = path.dirname(filePath); 10 | const parts = filePath.slice(dirName.length + path.sep.length).split("."); 11 | const fileName = parts.shift(); 12 | const exts = parts.map((v) => "." + v); 13 | assert(fileName !== undefined); 14 | return new PathInfo(dirName, fileName, exts); 15 | } 16 | 17 | public extsPeek(depth = 0): string | undefined { 18 | return this.exts[this.exts.length - (depth + 1)]; 19 | } 20 | 21 | public join(): string { 22 | return path.join(this.dirName, [this.fileName, ...this.exts].join("")); 23 | } 24 | } 25 | 26 | export class PathTranslator { 27 | constructor( 28 | public readonly rootDir: string, 29 | public readonly outDir: string, 30 | public readonly buildInfoOutputPath: string | undefined, 31 | public readonly declaration: boolean, 32 | ) {} 33 | 34 | private makeRelativeFactory(from = this.rootDir, to = this.outDir) { 35 | return (pathInfo: PathInfo) => path.join(to, path.relative(from, pathInfo.join())); 36 | } 37 | 38 | /** 39 | * Maps an input path to an output path 40 | * - `.tsx?` && !`.d.tsx?` -> `.lua` 41 | * - `index` -> `init` 42 | * - `src/*` -> `out/*` 43 | */ 44 | public getOutputPath(filePath: string) { 45 | const makeRelative = this.makeRelativeFactory(); 46 | const pathInfo = PathInfo.from(filePath); 47 | 48 | if ((pathInfo.extsPeek() === TS_EXT || pathInfo.extsPeek() === TSX_EXT) && pathInfo.extsPeek(1) !== D_EXT) { 49 | pathInfo.exts.pop(); // pop .tsx? 50 | 51 | // index -> init 52 | if (pathInfo.fileName === INDEX_NAME) { 53 | pathInfo.fileName = INIT_NAME; 54 | } 55 | 56 | pathInfo.exts.push(LUA_EXT); 57 | } 58 | 59 | return makeRelative(pathInfo); 60 | } 61 | 62 | /** 63 | * Maps an output path to possible import paths 64 | * - `.lua` -> `.tsx?` 65 | * - `init` -> `index` 66 | * - `out/*` -> `src/*` 67 | */ 68 | public getInputPaths(filePath: string) { 69 | const makeRelative = this.makeRelativeFactory(this.outDir, this.rootDir); 70 | const possiblePaths = new Array(); 71 | const pathInfo = PathInfo.from(filePath); 72 | 73 | // index.*.lua cannot come from a .ts file 74 | if (pathInfo.extsPeek() === LUA_EXT && pathInfo.fileName !== INDEX_NAME) { 75 | pathInfo.exts.pop(); 76 | const originalFileName = pathInfo.fileName; 77 | 78 | // init -> index 79 | if (pathInfo.fileName === INIT_NAME) { 80 | pathInfo.fileName = INDEX_NAME; 81 | } 82 | 83 | // .ts 84 | pathInfo.exts.push(TS_EXT); 85 | possiblePaths.push(makeRelative(pathInfo)); 86 | pathInfo.exts.pop(); 87 | 88 | // .tsx 89 | pathInfo.exts.push(TSX_EXT); 90 | possiblePaths.push(makeRelative(pathInfo)); 91 | pathInfo.exts.pop(); 92 | 93 | pathInfo.fileName = originalFileName; 94 | pathInfo.exts.push(LUA_EXT); 95 | } 96 | 97 | if (this.declaration) { 98 | if ((pathInfo.extsPeek() === TS_EXT || pathInfo.extsPeek() === TSX_EXT) && pathInfo.extsPeek(1) === D_EXT) { 99 | const tsExt = pathInfo.exts.pop(); // pop .tsx? 100 | assert(tsExt); 101 | pathInfo.exts.pop(); // pop .d 102 | 103 | // .ts 104 | pathInfo.exts.push(TS_EXT); 105 | possiblePaths.push(makeRelative(pathInfo)); 106 | pathInfo.exts.pop(); 107 | 108 | // .tsx 109 | pathInfo.exts.push(TSX_EXT); 110 | possiblePaths.push(makeRelative(pathInfo)); 111 | pathInfo.exts.pop(); 112 | 113 | pathInfo.exts.push(D_EXT); 114 | pathInfo.exts.push(tsExt); 115 | } 116 | } 117 | 118 | possiblePaths.push(makeRelative(pathInfo)); 119 | return possiblePaths; 120 | } 121 | 122 | /** 123 | * Maps a src path to an import path 124 | * - `.d.tsx?` -> `.tsx?` -> `.lua` 125 | * - `index` -> `init` 126 | */ 127 | public getImportPath(filePath: string, isNodeModule = false) { 128 | const makeRelative = this.makeRelativeFactory(); 129 | const pathInfo = PathInfo.from(filePath); 130 | 131 | if (pathInfo.extsPeek() === TS_EXT || pathInfo.extsPeek() === TSX_EXT) { 132 | pathInfo.exts.pop(); // pop .tsx? 133 | if (pathInfo.extsPeek() === D_EXT) { 134 | pathInfo.exts.pop(); // pop .d 135 | } 136 | 137 | // index -> init 138 | if (pathInfo.fileName === INDEX_NAME) { 139 | pathInfo.fileName = INIT_NAME; 140 | } 141 | 142 | pathInfo.exts.push(LUA_EXT); // push .lua 143 | } 144 | 145 | return isNodeModule ? pathInfo.join() : makeRelative(pathInfo); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/transformations/macros/updateComponentConfig.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { TransformState } from "../../classes/transformState"; 3 | import { buildGuardFromType, buildGuardsFromType } from "../../util/functions/buildGuardFromType"; 4 | import { f } from "../../util/factory"; 5 | import { getSuperClasses } from "../../util/functions/getSuperClasses"; 6 | import { NodeMetadata } from "../../classes/nodeMetadata"; 7 | import { withDiagnosticContext } from "../../util/diagnosticsUtils"; 8 | 9 | function calculateOmittedGuards( 10 | state: TransformState, 11 | classDeclaration: ts.ClassDeclaration, 12 | customAttributes?: ts.ObjectLiteralElementLike, 13 | ) { 14 | const omittedNames = new Set(); 15 | if (f.is.propertyAssignmentDeclaration(customAttributes) && f.is.object(customAttributes.initializer)) { 16 | for (const prop of customAttributes.initializer.properties) { 17 | if (f.is.string(prop.name) || f.is.identifier(prop.name)) { 18 | omittedNames.add(prop.name.text); 19 | } 20 | } 21 | } 22 | 23 | const type = state.typeChecker.getTypeAtLocation(classDeclaration); 24 | const property = type.getProperty("attributes"); 25 | if (!property) return omittedNames; 26 | 27 | const superClass = getSuperClasses(state.typeChecker, classDeclaration)[0]; 28 | if (!superClass) return omittedNames; 29 | 30 | const superType = state.typeChecker.getTypeAtLocation(superClass); 31 | const superProperty = superType.getProperty("attributes"); 32 | if (!superProperty) return omittedNames; 33 | 34 | const attributes = state.typeChecker.getTypeOfSymbolAtLocation(property, classDeclaration); 35 | const superAttributes = state.typeChecker.getTypeOfSymbolAtLocation(superProperty, superClass); 36 | for (const { name } of superAttributes.getProperties()) { 37 | const prop = state.typeChecker.getTypeOfPropertyOfType(attributes, name); 38 | const superProp = state.typeChecker.getTypeOfPropertyOfType(superAttributes, name); 39 | 40 | if (prop && superProp && superProp === prop) { 41 | omittedNames.add(name); 42 | } 43 | } 44 | 45 | return omittedNames; 46 | } 47 | 48 | function updateAttributeGuards( 49 | state: TransformState, 50 | node: ts.ClassDeclaration, 51 | properties: ts.ObjectLiteralElementLike[], 52 | ) { 53 | const type = state.typeChecker.getTypeAtLocation(node); 54 | 55 | const property = type.getProperty("attributes"); 56 | if (!property) return; 57 | 58 | const attributesMeta = NodeMetadata.fromSymbol(state, property); 59 | if (!attributesMeta || !attributesMeta.isRequested("intrinsic-component-attributes")) return; 60 | 61 | const attributesType = state.typeChecker.getTypeOfSymbolAtLocation(property, node); 62 | if (!attributesType) return; 63 | 64 | const attributes = properties.find((x) => x.name && "text" in x.name && x.name.text === "attributes"); 65 | const attributeGuards = withDiagnosticContext( 66 | node.name ?? node, 67 | () => `Failed to generate component attributes: ${state.typeChecker.typeToString(attributesType)}`, 68 | () => buildGuardsFromType(state, node.name ?? node, attributesType), 69 | ); 70 | 71 | const omittedGuards = calculateOmittedGuards(state, node, attributes); 72 | const filteredGuards = attributeGuards.filter((x) => !omittedGuards.has((x.name as ts.StringLiteral).text)); 73 | properties = properties.filter((x) => x !== attributes); 74 | 75 | if (f.is.propertyAssignmentDeclaration(attributes) && f.is.object(attributes.initializer)) { 76 | properties.push( 77 | f.update.propertyAssignmentDeclaration( 78 | attributes, 79 | f.update.object(attributes.initializer, [ 80 | ...attributes.initializer.properties.map((v) => state.transformNode(v)), 81 | ...filteredGuards, 82 | ]), 83 | attributes.name, 84 | ), 85 | ); 86 | } else { 87 | properties.push(f.propertyAssignmentDeclaration("attributes", f.object(filteredGuards))); 88 | } 89 | 90 | return properties; 91 | } 92 | 93 | function updateInstanceGuard( 94 | state: TransformState, 95 | node: ts.ClassDeclaration, 96 | properties: ts.ObjectLiteralElementLike[], 97 | ) { 98 | const type = state.typeChecker.getTypeAtLocation(node); 99 | 100 | const property = type.getProperty("instance"); 101 | if (!property) return; 102 | 103 | const attributesMeta = NodeMetadata.fromSymbol(state, property); 104 | if (!attributesMeta || !attributesMeta.isRequested("intrinsic-component-instance")) return; 105 | 106 | const superClass = getSuperClasses(state.typeChecker, node)[0]; 107 | if (!superClass) return; 108 | 109 | const customGuard = properties.find((x) => x.name && "text" in x.name && x.name.text === "instanceGuard"); 110 | if (customGuard) return; 111 | 112 | const instanceType = state.typeChecker.getTypeOfSymbolAtLocation(property, node); 113 | if (!instanceType) return; 114 | 115 | const superType = state.typeChecker.getTypeAtLocation(superClass); 116 | const superProperty = superType.getProperty("instance"); 117 | if (!superProperty) return; 118 | 119 | const superInstanceType = state.typeChecker.getTypeOfSymbolAtLocation(superProperty, superClass); 120 | if (!superInstanceType) return; 121 | 122 | if (!type.checker.isTypeAssignableTo(superInstanceType, instanceType)) { 123 | const guard = buildGuardFromType(state, node, instanceType); 124 | properties.push(f.propertyAssignmentDeclaration("instanceGuard", guard)); 125 | } 126 | 127 | return properties; 128 | } 129 | 130 | export function updateComponentConfig( 131 | state: TransformState, 132 | node: ts.ClassDeclaration, 133 | properties: ts.ObjectLiteralElementLike[], 134 | ): ts.ObjectLiteralElementLike[] { 135 | properties = updateAttributeGuards(state, node, properties) ?? properties; 136 | properties = updateInstanceGuard(state, node, properties) ?? properties; 137 | return properties; 138 | } 139 | -------------------------------------------------------------------------------- /src/util/uid.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import ts from "typescript"; 3 | import { Diagnostics } from "../classes/diagnostics"; 4 | import { TransformState } from "../classes/transformState"; 5 | import { f } from "./factory"; 6 | import { getDeclarationName } from "./functions/getDeclarationName"; 7 | import { getPackageJson } from "./functions/getPackageJson"; 8 | import { isDefinedType } from "./functions/isDefinedType"; 9 | import { isPathDescendantOfAny } from "./functions/isPathDescendantOf"; 10 | 11 | /** 12 | * Format the internal id to be shorter, remove `out` part of path, and use hashPrefix. 13 | */ 14 | function formatInternalid(state: TransformState, internalId: string, hashPrefix = state.config.hashPrefix) { 15 | const match = new RegExp(`^.*:(.*)@(.+)$`).exec(internalId); 16 | if (!match) return internalId; 17 | 18 | const [, path, name] = match; 19 | const revisedPath = path.replace(/^(.*?)[\/\\]/, ""); 20 | return hashPrefix ? `${hashPrefix}:${revisedPath}@${name}` : `${revisedPath}@${name}`; 21 | } 22 | 23 | /** 24 | * Gets the short ID for a node and includes the hash for uniqueness. 25 | */ 26 | function getShortId(state: TransformState, node: ts.Declaration, hashPrefix = state.config.hashPrefix) { 27 | const hash = state.hash(state.buildInfo.getLatestId(), true); 28 | const fullName = getDeclarationName(node); 29 | const fileName = path.parse(node.getSourceFile().fileName).name; 30 | const luaFileName = fileName === "index" ? "init" : fileName; 31 | const isShort = state.config.idGenerationMode === "short"; 32 | const shortId = `${isShort ? luaFileName + "@" : ""}${fullName}{${hash}}`; 33 | return hashPrefix ? `${state.config.hashPrefix}:${shortId}` : shortId; 34 | } 35 | 36 | export function getInternalId(state: TransformState, node: ts.NamedDeclaration) { 37 | const filePath = state.getSourceFile(node).fileName; 38 | const fullName = getDeclarationName(node); 39 | const { directory, result } = getPackageJson(path.dirname(filePath)); 40 | 41 | if (isPathDescendantOfAny(filePath, state.rootDirs)) { 42 | const outputPath = state.pathTranslator.getOutputPath(filePath).replace(/(\.lua|\.d\.ts)$/, ""); 43 | const relativePath = path.relative(state.currentDirectory, outputPath); 44 | const internalId = `${result.name}:${relativePath.replace(/\\/g, "/")}@${fullName}`; 45 | return { 46 | isPackage: false, 47 | internalId, 48 | }; 49 | } 50 | 51 | const relativePath = path.relative(directory, filePath.replace(/(\.d)?.ts$/, "").replace(/index$/, "init")); 52 | const internalId = `${result.name}:${relativePath.replace(/\\/g, "/")}@${fullName}`; 53 | return { 54 | isPackage: true, 55 | internalId, 56 | }; 57 | } 58 | 59 | export function getDeclarationUid(state: TransformState, node: ts.NamedDeclaration) { 60 | const { isPackage, internalId } = getInternalId(state, node); 61 | const id = state.buildInfo.getIdentifierFromInternal(internalId); 62 | if (id) return id; 63 | 64 | // this is a package, and the package itself did not generate an id 65 | // use the internal ID to prevent breakage between packages and games. 66 | if (isPackage) { 67 | const buildInfo = state.buildInfo.getBuildInfoFromFile(state.getSourceFile(node).fileName); 68 | if (buildInfo) { 69 | const prefix = buildInfo.getIdentifierPrefix(); 70 | if (prefix) { 71 | return formatInternalid(state, internalId, prefix); 72 | } 73 | } 74 | return internalId; 75 | } 76 | 77 | let newId: string; 78 | if (state.config.idGenerationMode === "obfuscated") { 79 | newId = state.hash(state.buildInfo.getLatestId()); 80 | } else if (state.config.idGenerationMode === "short" || state.config.idGenerationMode === "tiny") { 81 | newId = getShortId(state, node); 82 | } else { 83 | newId = formatInternalid(state, internalId); 84 | } 85 | 86 | state.buildInfo.addIdentifier(internalId, newId); 87 | return newId; 88 | } 89 | 90 | export function getSymbolUid(state: TransformState, symbol: ts.Symbol, trace: ts.Node): string; 91 | export function getSymbolUid(state: TransformState, symbol: ts.Symbol, trace?: ts.Node): string | undefined; 92 | export function getSymbolUid(state: TransformState, symbol: ts.Symbol, trace?: ts.Node) { 93 | if (symbol.valueDeclaration) { 94 | return getDeclarationUid(state, symbol.valueDeclaration); 95 | } else if (symbol.declarations?.[0]) { 96 | return getDeclarationUid(state, symbol.declarations[0]); 97 | } else if (trace) { 98 | Diagnostics.error(trace, `Could not find UID for symbol "${symbol.name}"`); 99 | } 100 | } 101 | 102 | export function getTypeUid(state: TransformState, type: ts.Type, trace: ts.Node): string; 103 | export function getTypeUid(state: TransformState, type: ts.Type, trace?: ts.Node): string | undefined; 104 | export function getTypeUid(state: TransformState, type: ts.Type, trace?: ts.Node) { 105 | if (type.symbol) { 106 | return getSymbolUid(state, type.symbol, trace); 107 | } else if (isDefinedType(type)) { 108 | return `$p:defined`; 109 | } else if (type.flags & ts.TypeFlags.Intrinsic) { 110 | return `$p:${(type as ts.IntrinsicType).intrinsicName}`; 111 | } else if (type.flags & ts.TypeFlags.NumberLiteral) { 112 | return `$pn:${(type as ts.NumberLiteralType).value}`; 113 | } else if (type.flags & ts.TypeFlags.StringLiteral) { 114 | return `$ps:${(type as ts.StringLiteralType).value}`; 115 | } else if (trace) { 116 | Diagnostics.error(trace, `Could not find UID for type "${type.checker.typeToString(type)}"`); 117 | } 118 | } 119 | 120 | export function getNodeUid(state: TransformState, node: ts.Node): string { 121 | if (f.is.namedDeclaration(node)) { 122 | return getDeclarationUid(state, node); 123 | } 124 | 125 | // resolve type aliases to the alias declaration 126 | if (f.is.referenceType(node)) { 127 | return getNodeUid(state, node.typeName); 128 | } else if (f.is.queryType(node)) { 129 | return getNodeUid(state, node.exprName); 130 | } 131 | 132 | const symbol = state.getSymbol(node); 133 | if (symbol) { 134 | return getSymbolUid(state, symbol, node); 135 | } 136 | 137 | const type = state.typeChecker.getTypeAtLocation(node); 138 | return getTypeUid(state, type, node); 139 | } 140 | -------------------------------------------------------------------------------- /src/util/functions/getUniversalTypeNode.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { f } from "../factory"; 3 | 4 | const FORMAT_FLAGS = 5 | (ts.TypeFormatFlags.UseFullyQualifiedType | ts.TypeFormatFlags.WriteClassExpressionAsTypeLiteral) & 6 | ts.TypeFormatFlags.NodeBuilderFlagsMask; 7 | 8 | /** 9 | * Returns a TypeNode generator that will attempt to create a TypeNode accessible from location. 10 | * Otherwise, returns undefined. 11 | */ 12 | export function getUniversalTypeNodeGenerator(location: ts.Node) { 13 | const visitingTypes = new Set(); 14 | const prereqs = new Array(); 15 | const prereq = new Map(); 16 | return { generate, prereqs }; 17 | 18 | function generate(type: ts.Type): ts.TypeNode | undefined { 19 | const prereqId = prereq.get(type); 20 | if (prereqId) { 21 | return f.referenceType(prereqId); 22 | } 23 | 24 | if (visitingTypes.has(type)) { 25 | // recursive type 26 | return f.referenceType(getPrereq(type)); 27 | } 28 | 29 | visitingTypes.add(type); 30 | const generatedType = generateInner(type); 31 | visitingTypes.delete(type); 32 | 33 | if (generatedType) { 34 | const prereqId = prereq.get(type); 35 | if (prereqId) { 36 | prereqs.push(f.typeAliasDeclaration(prereqId, generatedType)); 37 | return f.referenceType(prereqId); 38 | } 39 | 40 | return generatedType; 41 | } 42 | } 43 | 44 | function generateInner(type: ts.Type) { 45 | if (type.isUnionOrIntersection()) { 46 | const types = new Array(); 47 | for (const subtype of type.types) { 48 | const typeNode = generate(subtype); 49 | if (!typeNode) return; 50 | 51 | types.push(typeNode); 52 | } 53 | return type.isIntersection() ? f.intersectionType(types) : f.unionType(types); 54 | } 55 | 56 | const callSignatures = type.getCallSignatures(); 57 | if (callSignatures.length && !type.getProperties().length) { 58 | const declarations = getCallSignatures(type); 59 | if (!declarations) return; 60 | return f.typeLiteralType(declarations); 61 | } 62 | 63 | if (type.isLiteral() || type.flags & ts.TypeFlags.TemplateLiteral || type.flags & ts.TypeFlags.Intrinsic) { 64 | return type.checker.typeToTypeNode(type, location, undefined); 65 | } 66 | 67 | if (type.symbol) { 68 | const accessibility = type.checker.isSymbolAccessible(type.symbol, location, ts.SymbolFlags.Type, false); 69 | if (accessibility.accessibility === ts.SymbolAccessibility.Accessible) { 70 | if (isReferenceType(type)) { 71 | const typeArguments = new Array(); 72 | for (const typeArgument of type.resolvedTypeArguments ?? []) { 73 | const generatedType = generate(typeArgument); 74 | if (!generatedType) return; 75 | 76 | typeArguments.push(generatedType); 77 | } 78 | 79 | return getTypeReference(type, typeArguments); 80 | } 81 | 82 | return getTypeReference(type); 83 | } 84 | 85 | if (type.isClassOrInterface()) { 86 | return getUniversalObjectTypeNode(type); 87 | } 88 | } 89 | 90 | if (isObjectLiteralType(type)) { 91 | return getUniversalObjectTypeNode(type); 92 | } 93 | } 94 | 95 | function getPrereq(type: ts.Type) { 96 | let prereqId = prereq.get(type); 97 | if (!prereqId) prereq.set(type, (prereqId = f.identifier("typeAlias", true))); 98 | 99 | return prereqId; 100 | } 101 | 102 | function getUniversalObjectTypeNode(type: ts.Type) { 103 | const members = new Array(); 104 | members.push(...(getCallSignatures(type) ?? [])); 105 | 106 | for (const prop of type.getApparentProperties()) { 107 | const propType = type.checker.getTypeOfPropertyOfType(type, prop.name); 108 | if (!propType) return undefined; 109 | 110 | const universalTypeNode = generate(propType); 111 | if (!universalTypeNode) return undefined; 112 | 113 | members.push( 114 | f.propertySignatureType( 115 | f.string(prop.name), 116 | universalTypeNode, 117 | propType.checker.isNullableType(propType), 118 | ), 119 | ); 120 | } 121 | 122 | const numberIndexType = type.getNumberIndexType(); 123 | if (numberIndexType) { 124 | const accessibleType = generate(numberIndexType); 125 | if (accessibleType) { 126 | members.push(f.indexSignatureType(f.keywordType(ts.SyntaxKind.NumberKeyword), accessibleType)); 127 | } 128 | } 129 | 130 | const stringIndexType = type.getStringIndexType(); 131 | if (stringIndexType) { 132 | const accessibleType = generate(stringIndexType); 133 | if (accessibleType) { 134 | members.push(f.indexSignatureType(f.keywordType(ts.SyntaxKind.StringKeyword), accessibleType)); 135 | } 136 | } 137 | 138 | return f.typeLiteralType(members); 139 | } 140 | 141 | function getCallSignatures(type: ts.Type) { 142 | const signatures = new Array(); 143 | for (const signature of type.getCallSignatures()) { 144 | const returnTypeNode = generate(signature.getReturnType()); 145 | if (!returnTypeNode) return; 146 | 147 | const parameterDeclarations = new Array(); 148 | 149 | if (isMethod(signature, type.checker)) { 150 | parameterDeclarations.push(f.parameterDeclaration("this", f.keywordType(ts.SyntaxKind.AnyKeyword))); 151 | } 152 | 153 | for (const parameter of signature.getParameters()) { 154 | const parameterType = type.checker.getTypeOfSymbolAtLocation(parameter, location); 155 | const parameterTypeNode = generate(parameterType); 156 | if (!parameterTypeNode) return; 157 | 158 | parameterDeclarations.push(f.parameterDeclaration(parameter.name, parameterTypeNode)); 159 | } 160 | signatures.push(f.callSignatureType(parameterDeclarations, returnTypeNode)); 161 | } 162 | return signatures; 163 | } 164 | 165 | /** 166 | * Crawls parent symbols as getAccessibleSymbolChain does not 167 | */ 168 | function getAccessibleEntityName(symbol: ts.Symbol, typeChecker: ts.TypeChecker): ts.EntityName | undefined { 169 | const symbolChain = typeChecker.getAccessibleSymbolChain(symbol, location, ts.SymbolFlags.All, false); 170 | if (symbolChain) { 171 | return getQualifiedName(symbolChain.map((v) => v.name)); 172 | } 173 | 174 | if (symbol.parent) { 175 | const parentChain = getAccessibleEntityName(symbol.parent, typeChecker); 176 | if (parentChain) { 177 | return f.qualifiedNameType(parentChain, symbol.name); 178 | } 179 | } 180 | } 181 | 182 | function getTypeReference(type: ts.Type, typeArguments?: ts.TypeNode[]) { 183 | const accessibleEntityName = getAccessibleEntityName(type.symbol, type.checker); 184 | const typeNode = type.checker.typeToTypeNode(type, location, FORMAT_FLAGS); 185 | const isTypeOf = f.is.queryType(typeNode) || (f.is.importType(typeNode) && typeNode.isTypeOf); 186 | 187 | if (accessibleEntityName) { 188 | return isTypeOf ? f.queryType(accessibleEntityName) : f.referenceType(accessibleEntityName, typeArguments); 189 | } else { 190 | const [filePath, ...segments] = type.checker.getFullyQualifiedName(type.symbol).split("."); 191 | const accessibleTypeNode = getQualifiedName(segments); 192 | return f.importType(filePath.substr(1, filePath.length - 2), accessibleTypeNode, isTypeOf, typeArguments); 193 | } 194 | } 195 | 196 | function getQualifiedName(segments: string[]) { 197 | if (segments.length === 0) return; 198 | let qualifiedName: ts.QualifiedName | ts.Identifier | undefined = f.identifier(segments[0]); 199 | for (let i = segments.length - 1; i > 0; i--) { 200 | const segment = segments[i]; 201 | qualifiedName = f.qualifiedNameType(qualifiedName, segment); 202 | } 203 | return qualifiedName; 204 | } 205 | } 206 | 207 | function isMethodDeclaration(node: ts.Node, typeChecker: ts.TypeChecker): boolean { 208 | if (ts.isFunctionLike(node)) { 209 | const thisParam = node.parameters[0]; 210 | if (thisParam && f.is.identifier(thisParam.name) && ts.isThisIdentifier(thisParam.name)) { 211 | return !(typeChecker.getTypeAtLocation(thisParam.name).flags & ts.TypeFlags.Void); 212 | } else { 213 | if (ts.isMethodDeclaration(node) || ts.isMethodSignature(node)) { 214 | return true; 215 | } 216 | 217 | return false; 218 | } 219 | } 220 | return false; 221 | } 222 | 223 | function isMethod(signature: ts.Signature, typeChecker: ts.TypeChecker) { 224 | const thisParameter = signature.thisParameter?.valueDeclaration; 225 | if (thisParameter) { 226 | if (!(typeChecker.getTypeAtLocation(thisParameter).flags & ts.TypeFlags.Void)) { 227 | return true; 228 | } 229 | } else if (signature.declaration) { 230 | if (isMethodDeclaration(signature.declaration, typeChecker)) { 231 | return true; 232 | } 233 | } 234 | return false; 235 | } 236 | 237 | function isObjectLiteralType(type: ts.Type): type is ts.InterfaceType { 238 | return !type.isClassOrInterface() && (type.flags & ts.TypeFlags.Object) !== 0; 239 | } 240 | 241 | function isReferenceType(type: ts.Type): type is ts.TypeReference { 242 | return (ts.getObjectFlags(type) & ts.ObjectFlags.Reference) !== 0; 243 | } 244 | -------------------------------------------------------------------------------- /src/classes/buildInfo.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import crypto from "crypto"; 5 | import { v4 as uuid } from "uuid"; 6 | import { PKG_VERSION } from "./pathTranslator/constants"; 7 | import { isPathDescendantOf } from "../util/functions/isPathDescendantOf"; 8 | import { FlameworkConfig } from "./transformState"; 9 | import { validateSchema } from "../util/schema"; 10 | 11 | interface BuildDecorator { 12 | name: string; 13 | internalId: string; 14 | } 15 | 16 | interface BuildClass { 17 | filePath: string; 18 | internalId: string; 19 | decorators: Array; 20 | } 21 | 22 | interface FlameworkMetadata { 23 | config?: FlameworkConfig; 24 | globs?: { 25 | paths?: Record; 26 | origins?: Record; 27 | }; 28 | } 29 | 30 | export interface FlameworkBuildInfo { 31 | version: number; 32 | flameworkVersion: string; 33 | identifierPrefix?: string; 34 | salt?: string; 35 | metadata?: FlameworkMetadata; 36 | stringHashes?: { [key: string]: string }; 37 | identifiers: { [key: string]: string }; 38 | classes?: Array; 39 | } 40 | 41 | export class BuildInfo { 42 | static fromPath(fileName: string) { 43 | if (!ts.sys.fileExists(fileName)) return new BuildInfo(fileName); 44 | 45 | const fileContents = ts.sys.readFile(fileName); 46 | if (!fileContents) throw new Error(`Could not read file ${fileName}`); 47 | 48 | const buildInfo = JSON.parse(fileContents); 49 | if (validateSchema("buildInfo", buildInfo)) { 50 | return new BuildInfo(fileName, buildInfo); 51 | } 52 | 53 | throw new Error(`Found invalid build info at ${fileName}`); 54 | } 55 | 56 | static fromDirectory(directory: string) { 57 | const buildInfoPath = path.join(directory, "flamework.build"); 58 | if (ts.sys.fileExists(buildInfoPath)) { 59 | return this.fromPath(buildInfoPath); 60 | } 61 | 62 | const packageJsonPath = ts.findPackageJson(directory, ts.sys as never); 63 | if (packageJsonPath) { 64 | const buildInfoPath = path.join(path.dirname(packageJsonPath), "flamework.build"); 65 | if (buildInfoPath && ts.sys.fileExists(buildInfoPath)) { 66 | return this.fromPath(buildInfoPath); 67 | } 68 | } 69 | } 70 | 71 | private static candidateCache = new Map(); 72 | static findCandidateUpper(startDirectory: string, depth = 4): string | undefined { 73 | const cache = this.candidateCache.get(startDirectory); 74 | if (cache && cache.result) { 75 | return cache.result; 76 | } 77 | 78 | const buildPath = path.join(startDirectory, "flamework.build"); 79 | if (!cache && fs.existsSync(buildPath)) { 80 | this.candidateCache.set(startDirectory, { result: buildPath }); 81 | return buildPath; 82 | } else { 83 | this.candidateCache.set(startDirectory, {}); 84 | } 85 | 86 | if (depth > 0) { 87 | return this.findCandidateUpper(path.dirname(startDirectory), depth - 1); 88 | } 89 | } 90 | 91 | static findCandidates(searchPath: string, depth = 2, isNodeModules = true): string[] { 92 | const candidates: string[] = []; 93 | 94 | for (const childPath of fs.readdirSync(searchPath)) { 95 | // only search @* (@rbxts, @flamework, @custom, etc) 96 | if (!isNodeModules || childPath.startsWith("@")) { 97 | const fullPath = path.join(searchPath, childPath); 98 | const realPath = fs.realpathSync(fullPath); 99 | if (fs.lstatSync(realPath).isDirectory() && depth !== 0) { 100 | candidates.push(...BuildInfo.findCandidates(fullPath, depth - 1, childPath === "node_modules")); 101 | } else { 102 | if (childPath === "flamework.build") { 103 | candidates.push(fullPath); 104 | } 105 | } 106 | } 107 | } 108 | 109 | return candidates; 110 | } 111 | 112 | private buildInfo: FlameworkBuildInfo; 113 | private buildInfos: BuildInfo[] = []; 114 | private identifiersLookup = new Map(); 115 | constructor(public buildInfoPath: string, buildInfo?: FlameworkBuildInfo) { 116 | // eslint-disable-next-line @typescript-eslint/no-var-requires 117 | this.buildInfo = buildInfo ?? { 118 | version: 1, 119 | flameworkVersion: PKG_VERSION, 120 | identifiers: {}, 121 | }; 122 | if (buildInfo) { 123 | for (const [internalId, id] of Object.entries(buildInfo.identifiers)) { 124 | this.identifiersLookup.set(id, internalId); 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * Saves the build info to a file. 131 | */ 132 | save() { 133 | fs.writeFileSync(this.buildInfoPath, JSON.stringify(this.buildInfo, undefined, "\t")); 134 | } 135 | 136 | /** 137 | * Retrieves the salt previously used to generate identifiers, or creates one. 138 | */ 139 | getSalt() { 140 | if (this.buildInfo.salt) return this.buildInfo.salt; 141 | 142 | const salt = crypto.randomBytes(64).toString("hex"); 143 | this.buildInfo.salt = salt; 144 | 145 | return salt; 146 | } 147 | 148 | /** 149 | * Retrieves the version of flamework that this project was originally compiled on. 150 | */ 151 | getFlameworkVersion() { 152 | return this.buildInfo.flameworkVersion; 153 | } 154 | 155 | /** 156 | * Register a build info from an external source, normally packages. 157 | * @param buildInfo The BuildInfo to add 158 | */ 159 | addBuildInfo(buildInfo: BuildInfo) { 160 | this.buildInfos.push(buildInfo); 161 | } 162 | 163 | /** 164 | * Register a new identifier to be saved with the build info. 165 | * @param internalId The internal, reproducible ID 166 | * @param id The random or incremental ID 167 | */ 168 | addIdentifier(internalId: string, id: string) { 169 | const identifier = this.getIdentifierFromInternal(internalId); 170 | if (identifier) throw new Error(`Attempt to rewrite identifier ${internalId} -> ${id} (from ${identifier})`); 171 | 172 | this.buildInfo.identifiers[internalId] = id; 173 | this.identifiersLookup.set(id, internalId); 174 | } 175 | 176 | addBuildClass(classInfo: BuildClass) { 177 | if (this.getBuildClass(classInfo.internalId)) 178 | throw new Error(`Attempt to overwrite ${classInfo.internalId} class`); 179 | 180 | if (!this.buildInfo.classes) this.buildInfo.classes = []; 181 | this.buildInfo.classes.push(classInfo); 182 | } 183 | 184 | getBuildInfoFromFile(fileName: string): BuildInfo | undefined { 185 | for (const build of this.buildInfos) { 186 | if (isPathDescendantOf(fileName, path.dirname(build.buildInfoPath))) { 187 | return build; 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * Sets metadata which will be exposed at runtime. 194 | */ 195 | setMetadata(key: K, value: FlameworkMetadata[K]) { 196 | this.buildInfo.metadata ??= {}; 197 | this.buildInfo.metadata[key] = value; 198 | } 199 | 200 | /** 201 | * Gets metadata exposed at runtime. 202 | */ 203 | getMetadata(key: K) { 204 | return this.buildInfo.metadata?.[key]; 205 | } 206 | 207 | /** 208 | * Retrieves all metadata of this build info and its children. 209 | */ 210 | getChildrenMetadata(name: K) { 211 | const childrenMetadata = new Map(); 212 | 213 | for (const build of this.buildInfos) { 214 | const key = build.getIdentifierPrefix(); 215 | const metadata = build.getMetadata(name); 216 | if (!key) continue; 217 | if (!metadata) continue; 218 | 219 | childrenMetadata.set(key, metadata); 220 | 221 | for (const [key, metadata] of build.getChildrenMetadata(name)) { 222 | childrenMetadata.set(key, metadata); 223 | } 224 | } 225 | 226 | return childrenMetadata; 227 | } 228 | 229 | getBuildInfoFromPrefix(prefix: string): BuildInfo | undefined { 230 | for (const build of this.buildInfos) { 231 | if (build.getIdentifierPrefix() === prefix) { 232 | return build; 233 | } 234 | 235 | const child = build.getBuildInfoFromPrefix(prefix); 236 | if (child) { 237 | return child; 238 | } 239 | } 240 | } 241 | 242 | /** 243 | * Sets configuration which will be exposed at runtime. 244 | */ 245 | setConfig(value: FlameworkConfig | undefined) { 246 | this.buildInfo.metadata ??= {}; 247 | this.buildInfo.metadata.config = value; 248 | } 249 | 250 | /** 251 | * Adds a glob that will automatically be tracked between compiles. 252 | */ 253 | addGlob(glob: string, origin: string) { 254 | this.buildInfo.metadata ??= {}; 255 | this.buildInfo.metadata.globs ??= {}; 256 | this.buildInfo.metadata.globs.paths ??= {}; 257 | this.buildInfo.metadata.globs.origins ??= {}; 258 | this.buildInfo.metadata.globs.paths[glob] = []; 259 | this.buildInfo.metadata.globs.origins[origin] ??= []; 260 | this.buildInfo.metadata.globs.origins[origin].push(glob); 261 | } 262 | 263 | /** 264 | * Removes all globs related to this file. 265 | */ 266 | invalidateGlobs(origin: string) { 267 | const globs = this.buildInfo.metadata?.globs; 268 | if (globs && globs.paths && globs.origins) { 269 | delete globs.origins[origin]; 270 | 271 | outer: for (const path of Object.keys(globs.paths)) { 272 | for (const origin of Object.values(globs.origins)) { 273 | if (origin.includes(path)) { 274 | continue outer; 275 | } 276 | } 277 | 278 | delete globs.paths[path]; 279 | } 280 | } 281 | } 282 | 283 | /** 284 | * Get the random or incremental Id from the internalId. 285 | * @param internalId The internal, reproducible ID 286 | */ 287 | getIdentifierFromInternal(internalId: string): string | undefined { 288 | const id = this.buildInfo.identifiers[internalId]; 289 | if (id) return id; 290 | 291 | for (const build of this.buildInfos) { 292 | const subId = build.getIdentifierFromInternal(internalId); 293 | if (subId) return subId; 294 | } 295 | } 296 | 297 | /** 298 | * Get the internal, reproducible Id from a random Id. 299 | * @param id The random or incremental Id 300 | */ 301 | getInternalFromIdentifier(id: string): string | undefined { 302 | const internalId = this.identifiersLookup.get(id); 303 | if (internalId) return internalId; 304 | 305 | for (const build of this.buildInfos) { 306 | const subId = build.getIdentifierFromInternal(id); 307 | if (subId) return subId; 308 | } 309 | } 310 | 311 | getBuildClass(internalId: string): BuildClass | undefined { 312 | const buildClass = this.buildInfo.classes?.find((x) => x.internalId === internalId); 313 | if (buildClass) return buildClass; 314 | 315 | for (const build of this.buildInfos) { 316 | const subClass = build.getBuildClass(internalId); 317 | if (subClass) return subClass; 318 | } 319 | } 320 | 321 | /** 322 | * Returns the next Id for incremental generation. 323 | */ 324 | getLatestId() { 325 | return Object.keys(this.buildInfo.identifiers).length + 1; 326 | } 327 | 328 | /** 329 | * Create a UUID, subsequent calls with the same string will have the same UUID. 330 | * @param str The string to hash 331 | */ 332 | hashString(str: string, context = "@") { 333 | str = `${context}:${str}`; 334 | 335 | let stringHashes = this.buildInfo.stringHashes; 336 | if (!stringHashes) this.buildInfo.stringHashes = stringHashes = {}; 337 | 338 | if (stringHashes[str]) return stringHashes[str]; 339 | 340 | const strUuid = uuid(); 341 | stringHashes[str] = strUuid; 342 | return strUuid; 343 | } 344 | 345 | /** 346 | * Sets the prefix used for identifiers. 347 | * Used to generate IDs for packages. 348 | */ 349 | setIdentifierPrefix(prefix: string | undefined) { 350 | this.buildInfo.identifierPrefix = prefix; 351 | } 352 | 353 | /** 354 | * Gets the prefixed used for identifiers. 355 | */ 356 | getIdentifierPrefix() { 357 | return this.buildInfo.identifierPrefix; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/transformations/statements/transformClassDeclaration.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import ts from "typescript"; 3 | import { Diagnostics } from "../../classes/diagnostics"; 4 | import { NodeMetadata } from "../../classes/nodeMetadata"; 5 | import { TransformState } from "../../classes/transformState"; 6 | import { f } from "../../util/factory"; 7 | import { buildGuardFromType } from "../../util/functions/buildGuardFromType"; 8 | import { getNodeUid, getSymbolUid, getTypeUid } from "../../util/uid"; 9 | import { updateComponentConfig } from "../macros/updateComponentConfig"; 10 | import type { ClassInfo } from "../../types/classes"; 11 | 12 | export function transformClassDeclaration(state: TransformState, node: ts.ClassDeclaration) { 13 | const symbol = state.getSymbol(node); 14 | if (!symbol || !node.name) return state.transform(node); 15 | 16 | const classInfo = state.classes.get(symbol); 17 | if (!classInfo) return state.transform(node); 18 | 19 | const importIdentifier = state.addFileImport(state.getSourceFile(node), "@flamework/core", "Reflect"); 20 | const reflectStatements = new Array(); 21 | const decoratorStatements = new Array(); 22 | const metadata = new NodeMetadata(state, node); 23 | 24 | reflectStatements.push(...convertReflectionToStatements(generateClassMetadata(state, classInfo, metadata, node))); 25 | decoratorStatements.push(...getDecoratorStatements(state, node, node, metadata)); 26 | 27 | for (const member of node.members) { 28 | if (!member.name) { 29 | continue; 30 | } 31 | 32 | const propertyName = ts.getPropertyNameForPropertyNameNode(member.name); 33 | if (!propertyName) { 34 | continue; 35 | } 36 | 37 | reflectStatements.push(...convertReflectionToStatements(getNodeReflection(state, member) ?? [], propertyName)); 38 | decoratorStatements.push(...getDecoratorStatements(state, node, member)); 39 | } 40 | 41 | return [updateClass(state, node, reflectStatements), ...decoratorStatements]; 42 | 43 | function convertReflectionToStatements(metadata: [string, f.ConvertableExpression][], property?: string) { 44 | const statements = metadata.map(([name, value]) => { 45 | const args = [node.name!, name, value]; 46 | if (property !== undefined) { 47 | args.push(property); 48 | } 49 | 50 | return f.statement(f.call(f.field(importIdentifier, "defineMetadata"), args)); 51 | }); 52 | 53 | addSectionComment(statements[0], node, property, "metadata"); 54 | 55 | return statements; 56 | } 57 | } 58 | 59 | function generateFieldMetadata(state: TransformState, metadata: NodeMetadata, field: ts.PropertyDeclaration) { 60 | const fields = new Array<[string, f.ConvertableExpression]>(); 61 | const type = state.typeChecker.getTypeAtLocation(field); 62 | 63 | if (metadata.isRequested("flamework:type")) { 64 | if (!field.type) { 65 | const id = getTypeUid(state, type, field.name ?? field); 66 | fields.push(["flamework:type", id]); 67 | } else { 68 | const id = getNodeUid(state, field.type); 69 | fields.push(["flamework:type", id]); 70 | } 71 | } 72 | 73 | if (metadata.isRequested("flamework:guard")) { 74 | const guard = buildGuardFromType(state, field.type ?? field, type); 75 | fields.push(["flamework:guard", guard]); 76 | } 77 | 78 | return fields; 79 | } 80 | 81 | function generateMethodMetadata(state: TransformState, metadata: NodeMetadata, method: ts.FunctionLikeDeclaration) { 82 | const fields = new Array<[string, f.ConvertableExpression]>(); 83 | const baseSignature = state.typeChecker.getSignatureFromDeclaration(method); 84 | if (!baseSignature) return []; 85 | 86 | if (metadata.isRequested("flamework:return_type")) { 87 | if (!method.type) { 88 | const id = getTypeUid(state, baseSignature.getReturnType(), method.name ?? method); 89 | fields.push(["flamework:return_type", id]); 90 | } else { 91 | const id = getNodeUid(state, method.type); 92 | fields.push(["flamework:return_type", id]); 93 | } 94 | } 95 | 96 | if (metadata.isRequested("flamework:return_guard")) { 97 | const guard = buildGuardFromType(state, method.type ?? method, baseSignature.getReturnType()); 98 | fields.push(["flamework:return_guard", guard]); 99 | } 100 | 101 | const parameters = new Array(); 102 | const parameterNames = new Array(); 103 | const parameterGuards = new Array(); 104 | 105 | for (const parameter of method.parameters) { 106 | if (metadata.isRequested("flamework:parameters")) { 107 | if (parameter.type) { 108 | const id = getNodeUid(state, parameter.type); 109 | parameters.push(id); 110 | } else { 111 | const type = state.typeChecker.getTypeAtLocation(parameter); 112 | const id = getTypeUid(state, type, parameter); 113 | parameters.push(id); 114 | } 115 | } 116 | 117 | if (metadata.isRequested("flamework:parameter_names")) { 118 | if (f.is.identifier(parameter.name)) { 119 | parameterNames.push(parameter.name.text); 120 | } else { 121 | parameterNames.push("_binding_"); 122 | } 123 | } 124 | 125 | if (metadata.isRequested("flamework:parameter_guards")) { 126 | const type = state.typeChecker.getTypeAtLocation(parameter); 127 | const guard = buildGuardFromType(state, parameter, type); 128 | parameterGuards.push(guard); 129 | } 130 | } 131 | 132 | if (parameters.length > 0) { 133 | fields.push(["flamework:parameters", parameters]); 134 | } 135 | 136 | if (parameterNames.length > 0) { 137 | fields.push(["flamework:parameter_names", parameterNames]); 138 | } 139 | 140 | if (parameterGuards.length > 0) { 141 | fields.push(["flamework:parameter_guards", parameterGuards]); 142 | } 143 | 144 | return fields; 145 | } 146 | 147 | function transformDecoratorConfig( 148 | state: TransformState, 149 | declaration: ts.ClassDeclaration, 150 | symbol: ts.Symbol, 151 | expr: ts.Expression, 152 | ) { 153 | if (!f.is.call(expr)) { 154 | return []; 155 | } 156 | 157 | const metadata = NodeMetadata.fromSymbol(state, symbol); 158 | if (metadata && metadata.isRequested("intrinsic-component-decorator")) { 159 | assert(!expr.arguments[0] || f.is.object(expr.arguments[0])); 160 | 161 | const baseConfig = expr.arguments[0] ? expr.arguments[0] : f.object([]); 162 | const componentConfig = updateComponentConfig(state, declaration, [...baseConfig.properties]); 163 | return [ 164 | f.update.object( 165 | baseConfig, 166 | componentConfig.map((v) => (baseConfig.properties.includes(v) ? state.transformNode(v) : v)), 167 | ), 168 | ]; 169 | } 170 | 171 | return expr.arguments.map((v) => state.transformNode(v)); 172 | } 173 | 174 | function generateClassMetadata( 175 | state: TransformState, 176 | classInfo: ClassInfo, 177 | metadata: NodeMetadata, 178 | node: ts.ClassDeclaration, 179 | ) { 180 | const fields: [string, f.ConvertableExpression][] = []; 181 | 182 | // Flamework decorators always generate the identifier field, 183 | // but the new decorator system does not require the identifier metadata to be specified. 184 | if (classInfo.containsLegacyDecorator || metadata.isRequested("identifier")) { 185 | fields.push(["identifier", getNodeUid(state, node)]); 186 | } 187 | 188 | const constructor = node.members.find((x): x is ts.ConstructorDeclaration => f.is.constructor(x)); 189 | if (constructor) { 190 | fields.push(...generateMethodMetadata(state, metadata, constructor)); 191 | } 192 | 193 | if (node.heritageClauses) { 194 | const implementClauses = new Array(); 195 | for (const clause of node.heritageClauses) { 196 | if (clause.token !== ts.SyntaxKind.ImplementsKeyword) continue; 197 | 198 | for (const type of clause.types) { 199 | implementClauses.push(f.string(getNodeUid(state, type))); 200 | } 201 | } 202 | 203 | if (implementClauses.length > 0 && metadata.isRequested("flamework:implements")) { 204 | fields.push(["flamework:implements", f.array(implementClauses, false)]); 205 | } 206 | } 207 | 208 | return fields; 209 | } 210 | 211 | function getNodeReflection( 212 | state: TransformState, 213 | node: ts.ClassDeclaration | ts.ClassElement, 214 | metadata = new NodeMetadata(state, node), 215 | ) { 216 | if (f.is.methodDeclaration(node)) { 217 | return generateMethodMetadata(state, metadata, node); 218 | } else if (f.is.propertyDeclaration(node)) { 219 | return generateFieldMetadata(state, metadata, node); 220 | } 221 | } 222 | 223 | function getDecoratorStatements( 224 | state: TransformState, 225 | declaration: ts.ClassDeclaration, 226 | node: ts.ClassDeclaration | ts.ClassElement, 227 | metadata = new NodeMetadata(state, node), 228 | ): ts.Statement[] { 229 | if (!node.name) { 230 | return []; 231 | } 232 | 233 | const isClass = f.is.classDeclaration(node); 234 | const symbol = state.getSymbol(node.name); 235 | const propertyName = ts.getNameFromPropertyName(node.name); 236 | assert(propertyName); 237 | assert(symbol); 238 | const importIdentifier = state.addFileImport(state.getSourceFile(node), "@flamework/core", "Reflect"); 239 | const decoratorStatements = new Array(); 240 | 241 | const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined; 242 | if (decorators) { 243 | // Decorators apply last->first, so we iterate the decorators in reverse. 244 | for (let i = decorators.length - 1; i >= 0; i--) { 245 | const decorator = decorators[i]; 246 | const expr = decorator.expression; 247 | const type = state.typeChecker.getTypeAtLocation(expr); 248 | if (type.getProperty("_flamework_Decorator")) { 249 | const identifier = f.is.call(expr) ? expr.expression : expr; 250 | const symbol = state.getSymbol(identifier); 251 | assert(symbol); 252 | assert(symbol.valueDeclaration); 253 | 254 | const args = transformDecoratorConfig(state, declaration, symbol, expr); 255 | const propertyArgs = !f.is.classDeclaration(node) 256 | ? [propertyName, (node.modifierFlagsCache & ts.ModifierFlags.Static) !== 0] 257 | : []; 258 | 259 | decoratorStatements.push( 260 | f.statement( 261 | f.call(f.field(importIdentifier, "decorate"), [ 262 | declaration.name!, 263 | getSymbolUid(state, symbol, identifier), 264 | identifier, 265 | [...args], 266 | ...propertyArgs, 267 | ]), 268 | ), 269 | ); 270 | } 271 | } 272 | } 273 | 274 | const constraintTypes = metadata.getType("constraint"); 275 | const nodeType = state.typeChecker.getTypeOfSymbolAtLocation(symbol, node); 276 | for (const constraintType of constraintTypes ?? []) { 277 | if (!state.typeChecker.isTypeAssignableTo(nodeType, constraintType)) { 278 | Diagnostics.addDiagnostic( 279 | getAssignabilityDiagnostics( 280 | node.name ?? node, 281 | nodeType, 282 | constraintType, 283 | metadata.getTrace(constraintType), 284 | ), 285 | ); 286 | } 287 | } 288 | 289 | addSectionComment(decoratorStatements[0], declaration, isClass ? undefined : propertyName, "decorators"); 290 | return decoratorStatements; 291 | } 292 | 293 | function addSectionComment( 294 | node: ts.Node | undefined, 295 | declaration: ts.ClassDeclaration, 296 | property: string | undefined, 297 | label: string, 298 | ) { 299 | if (!node) { 300 | return; 301 | } 302 | 303 | const elementName = property === undefined ? `${declaration.name!.text}` : `${declaration.name!.text}.${property}`; 304 | ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, ` (Flamework) ${elementName} ${label}`); 305 | } 306 | 307 | function formatType(type: ts.Type) { 308 | const typeNode = type.checker.typeToTypeNode( 309 | type, 310 | undefined, 311 | ts.NodeBuilderFlags.InTypeAlias | ts.NodeBuilderFlags.IgnoreErrors, 312 | )!; 313 | 314 | const printer = ts.createPrinter(); 315 | return printer.printNode(ts.EmitHint.Unspecified, typeNode, undefined!); 316 | } 317 | 318 | function getAssignabilityDiagnostics( 319 | node: ts.Node, 320 | sourceType: ts.Type, 321 | constraintType: ts.Type, 322 | trace?: ts.Node, 323 | ): ts.DiagnosticWithLocation { 324 | const diagnostic = Diagnostics.createDiagnostic( 325 | node, 326 | ts.DiagnosticCategory.Error, 327 | `Type '${formatType(sourceType)}' does not satify constraint '${formatType(constraintType)}'`, 328 | ); 329 | 330 | if (trace) { 331 | ts.addRelatedInfo( 332 | diagnostic, 333 | Diagnostics.createDiagnostic(trace, ts.DiagnosticCategory.Message, "The constraint is defined here."), 334 | ); 335 | } 336 | 337 | return diagnostic; 338 | } 339 | 340 | function updateClass(state: TransformState, node: ts.ClassDeclaration, staticStatements?: ts.Statement[]) { 341 | const modifiers = getAllModifiers(node); 342 | const members = node.members 343 | .map((node) => state.transformNode(node)) 344 | .map((member) => { 345 | // Strip Flamework decorators from members 346 | const modifiers = getAllModifiers(member); 347 | if (modifiers) { 348 | const filteredModifiers = transformModifiers(state, modifiers); 349 | if (f.is.propertyDeclaration(member)) { 350 | return f.update.propertyDeclaration(member, undefined, undefined, filteredModifiers); 351 | } else if (f.is.methodDeclaration(member)) { 352 | return f.update.methodDeclaration( 353 | member, 354 | undefined, 355 | undefined, 356 | undefined, 357 | undefined, 358 | filteredModifiers, 359 | ); 360 | } 361 | } 362 | 363 | return member; 364 | }); 365 | 366 | if (staticStatements) { 367 | members.push(f.staticBlockDeclaration(staticStatements)); 368 | } 369 | 370 | return f.update.classDeclaration( 371 | node, 372 | node.name ? state.transformNode(node.name) : undefined, 373 | members, 374 | node.heritageClauses, 375 | node.typeParameters, 376 | modifiers && transformModifiers(state, modifiers), 377 | ); 378 | } 379 | 380 | function getAllModifiers(node: ts.Node) { 381 | return ts.canHaveDecorators(node) || ts.canHaveModifiers(node) ? node.modifiers : undefined; 382 | } 383 | 384 | function transformModifiers(state: TransformState, modifiers: readonly ts.ModifierLike[]) { 385 | return modifiers 386 | .filter((modifier) => { 387 | if (!ts.isDecorator(modifier)) { 388 | return true; 389 | } 390 | 391 | const type = state.typeChecker.getTypeAtLocation(modifier.expression); 392 | return type.getProperty("_flamework_Decorator") === undefined; 393 | }) 394 | .map((decorator) => state.transform(decorator)); 395 | } 396 | -------------------------------------------------------------------------------- /src/transformations/transformUserMacro.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from "crypto"; 2 | import ts from "typescript"; 3 | import { Diagnostics } from "../classes/diagnostics"; 4 | import { TransformState } from "../classes/transformState"; 5 | import { f } from "../util/factory"; 6 | import { buildGuardFromTypeWithDedup } from "../util/functions/buildGuardFromType"; 7 | import { getTypeUid } from "../util/uid"; 8 | import { NodeMetadata } from "../classes/nodeMetadata"; 9 | import { buildPathGlobIntrinsic, buildPathIntrinsic } from "./macros/intrinsics/paths"; 10 | import { validateParameterConstIntrinsic } from "./macros/intrinsics/parameters"; 11 | import { 12 | buildDeclarationUidIntrinsic, 13 | transformNetworkingMiddlewareIntrinsic, 14 | transformObfuscatedObjectIntrinsic, 15 | transformShuffleArrayIntrinsic, 16 | } from "./macros/intrinsics/networking"; 17 | import { buildTupleGuardsIntrinsic } from "./macros/intrinsics/guards"; 18 | import { isTupleType } from "../util/functions/isTupleType"; 19 | import { inlineMacroIntrinsic } from "./macros/intrinsics/inlining"; 20 | import { buildSymbolIdIntrinsic } from "./macros/intrinsics/symbol"; 21 | 22 | export function transformUserMacro( 23 | state: TransformState, 24 | node: ts.NewExpression | ts.CallExpression, 25 | signature: ts.Signature, 26 | ): ts.Expression | undefined { 27 | const file = state.getSourceFile(node); 28 | const signatureDeclaration = signature.getDeclaration(); 29 | const nodeMetadata = new NodeMetadata(state, signatureDeclaration); 30 | const args = node.arguments ? [...node.arguments] : []; 31 | const parameters = new Map(); 32 | 33 | let highestParameterIndex = -1; 34 | for (let i = 0; i < getParameterCount(state, signature); i++) { 35 | // This parameter is passed explicitly, so we don't need to evaluate it. 36 | if (!isUndefinedArgument(args[i])) { 37 | continue; 38 | } 39 | 40 | const targetParameter = state.typeChecker.getParameterType(signature, i).getNonNullableType(); 41 | const userMacro = getUserMacroOfUnion(state, node, targetParameter); 42 | if (userMacro) { 43 | parameters.set(i, userMacro); 44 | highestParameterIndex = Math.max(highestParameterIndex, i); 45 | } 46 | } 47 | 48 | for (let i = 0; i <= highestParameterIndex; i++) { 49 | const userMacro = parameters.get(i); 50 | if (userMacro) { 51 | args[i] = buildUserMacro(state, node, userMacro); 52 | } else { 53 | args[i] = args[i] ? state.transform(args[i]) : f.nil(); 54 | } 55 | } 56 | 57 | const networkingMiddleware = nodeMetadata.getSymbol("intrinsic-middleware"); 58 | if (networkingMiddleware) { 59 | transformNetworkingMiddlewareIntrinsic(state, signature, args, networkingMiddleware); 60 | } 61 | 62 | const inlineIntrinsic = nodeMetadata.getSymbol("intrinsic-inline"); 63 | if (inlineIntrinsic && inlineIntrinsic.length === 1) { 64 | return inlineMacroIntrinsic(signature, args, inlineIntrinsic[0]); 65 | } 66 | 67 | validateParameterConstIntrinsic(node, signature, nodeMetadata.getSymbol("intrinsic-const") ?? []); 68 | 69 | let name: ts.Expression | undefined; 70 | 71 | const rewrite = nodeMetadata.getSymbol("intrinsic-flamework-rewrite")?.[0]; 72 | if (rewrite && rewrite.parent) { 73 | const namespace = state.addFileImport(file, "@flamework/core", rewrite.parent.name); 74 | name = f.elementAccessExpression(namespace, rewrite.name); 75 | } 76 | 77 | if (!name) { 78 | name = state.transformNode(node.expression); 79 | } 80 | 81 | if (nodeMetadata.isRequested("intrinsic-arg-shift")) { 82 | args.shift(); 83 | } 84 | 85 | if (ts.isNewExpression(node)) { 86 | return ts.factory.updateNewExpression(node, name, node.typeArguments, args); 87 | } else if (ts.isCallExpression(node)) { 88 | return ts.factory.updateCallExpression(node, name, node.typeArguments, args); 89 | } else { 90 | Diagnostics.error(node, `Macro could not be transformed.`); 91 | } 92 | } 93 | 94 | function isUndefinedArgument(argument: ts.Node | undefined) { 95 | return argument ? f.is.identifier(argument) && argument.text === "undefined" : true; 96 | } 97 | 98 | function getLabels(state: TransformState, type: ts.Type): UserMacro { 99 | if (!isTupleType(state, type)) { 100 | return { 101 | kind: "literal", 102 | value: undefined, 103 | }; 104 | } 105 | 106 | const names = new Array(); 107 | const declarations = type.target.labeledElementDeclarations; 108 | 109 | if (!declarations) { 110 | return { 111 | kind: "literal", 112 | value: undefined, 113 | }; 114 | } 115 | 116 | for (const namedMember of declarations) { 117 | // TypeScript 5.0+ allows nameless tuple elements, so we'll default to an empty string in that case. 118 | names.push({ 119 | kind: "literal", 120 | value: namedMember ? (namedMember.name as ts.Identifier).text : "", 121 | }); 122 | } 123 | 124 | return { 125 | kind: "many", 126 | members: names, 127 | }; 128 | } 129 | 130 | function buildUserMacro(state: TransformState, node: ts.Expression, macro: UserMacro): ts.AsExpression { 131 | if (macro.kind === "generic") { 132 | const metadata = getGenericMetadata(macro); 133 | if (metadata) { 134 | return f.asNever(metadata); 135 | } 136 | } else if (macro.kind === "caller") { 137 | const metadata = getCallerMetadata(macro); 138 | if (metadata) { 139 | return f.asNever(metadata); 140 | } 141 | } else if (macro.kind === "many") { 142 | if (Array.isArray(macro.members)) { 143 | return f.asNever(f.array(macro.members.map((userMacro) => buildUserMacro(state, node, userMacro)))); 144 | } else { 145 | const elements = new Array(); 146 | 147 | for (const [name, userMacro] of macro.members) { 148 | const expression = buildUserMacro(state, node, userMacro); 149 | if (f.is.nil(expression.expression)) { 150 | continue; 151 | } 152 | 153 | elements.push(f.propertyAssignmentDeclaration(f.string(name), expression)); 154 | } 155 | 156 | return f.asNever(f.object(elements, false)); 157 | } 158 | } else if (macro.kind === "literal") { 159 | const value = macro.value; 160 | return f.asNever( 161 | typeof value === "string" 162 | ? f.string(value) 163 | : typeof value === "number" 164 | ? f.number(value) 165 | : typeof value === "boolean" 166 | ? f.bool(value) 167 | : f.nil(), 168 | ); 169 | } else if (macro.kind === "intrinsic") { 170 | return f.asNever(buildIntrinsicMacro(state, node, macro)); 171 | } 172 | 173 | return f.asNever(f.nil()); 174 | 175 | function getGenericMetadata(macro: UserMacro & { kind: "generic" }) { 176 | if (macro.metadata === "id") { 177 | return f.string(getTypeUid(state, macro.target, node)); 178 | } 179 | 180 | if (macro.metadata === "guard") { 181 | const result = buildGuardFromTypeWithDedup(state, node, macro.target); 182 | state.prereqList(result.statements); 183 | 184 | return result.guard; 185 | } 186 | 187 | if (macro.metadata === "text") { 188 | return f.string(state.typeChecker.typeToString(macro.target)); 189 | } 190 | } 191 | 192 | function getCallerMetadata(macro: UserMacro & { kind: "caller" }) { 193 | const lineAndCharacter = ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart()); 194 | 195 | if (macro.metadata === "line") { 196 | return f.number(lineAndCharacter.line + 1); 197 | } 198 | 199 | if (macro.metadata === "character") { 200 | return f.number(lineAndCharacter.character + 1); 201 | } 202 | 203 | if (macro.metadata === "width") { 204 | return f.number(node.getWidth()); 205 | } 206 | 207 | if (macro.metadata === "uuid") { 208 | return f.string(randomUUID()); 209 | } 210 | 211 | if (macro.metadata === "text") { 212 | return f.string(node.getText()); 213 | } 214 | } 215 | } 216 | 217 | function buildIntrinsicMacro(state: TransformState, node: ts.Expression, macro: UserMacro & { kind: "intrinsic" }) { 218 | if (macro.id === "pathglob") { 219 | const [pathType] = macro.inputs; 220 | if (!pathType) { 221 | throw new Error(`Invalid intrinsic usage`); 222 | } 223 | 224 | return buildPathGlobIntrinsic(state, node, pathType); 225 | } 226 | 227 | if (macro.id === "path") { 228 | const [pathType] = macro.inputs; 229 | if (!pathType) { 230 | throw new Error(`Invalid intrinsic usage`); 231 | } 232 | 233 | return buildPathIntrinsic(state, node, pathType); 234 | } 235 | 236 | if (macro.id === "obfuscate-obj") { 237 | const [macroType, hashType] = macro.inputs; 238 | if (!macroType || !hashType) { 239 | throw new Error(`Invalid intrinsic usage`); 240 | } 241 | 242 | const innerMacro = getUserMacroOfMany(state, node, macroType); 243 | if (!innerMacro) { 244 | throw new Error(`Intrinsic obfuscate-obj received no inner macro.`); 245 | } 246 | 247 | transformObfuscatedObjectIntrinsic(state, innerMacro, hashType); 248 | 249 | return buildUserMacro(state, node, innerMacro); 250 | } 251 | 252 | if (macro.id === "shuffle-array") { 253 | const [macroType] = macro.inputs; 254 | if (!macroType) { 255 | throw new Error(`Invalid intrinsic usage`); 256 | } 257 | 258 | const innerMacro = getUserMacroOfMany(state, node, macroType); 259 | if (!innerMacro) { 260 | throw new Error(`Intrinsic obfuscate-obj received no inner macro.`); 261 | } 262 | 263 | transformShuffleArrayIntrinsic(state, innerMacro); 264 | 265 | return buildUserMacro(state, node, innerMacro); 266 | } 267 | 268 | if (macro.id === "tuple-guards") { 269 | const [tupleType] = macro.inputs; 270 | if (!tupleType) { 271 | throw new Error(`Invalid intrinsic usage`); 272 | } 273 | 274 | return buildTupleGuardsIntrinsic(state, node, tupleType); 275 | } 276 | 277 | if (macro.id === "declaration-uid") { 278 | return buildDeclarationUidIntrinsic(state, node); 279 | } 280 | 281 | if (macro.id === "symbol-id") { 282 | const [type] = macro.inputs; 283 | if (!type || !f.is.call(node)) { 284 | throw new Error(`Invalid intrinsic usage`); 285 | } 286 | 287 | return buildSymbolIdIntrinsic(state, node, type); 288 | } 289 | 290 | throw `Unexpected intrinsic ID '${macro.id}' with ${macro.inputs.length} inputs`; 291 | } 292 | 293 | function getMetadataFromType(metadataType: ts.Type) { 294 | if (metadataType.isStringLiteral()) { 295 | return metadataType.value; 296 | } 297 | } 298 | 299 | function getUserMacroOfMany(state: TransformState, node: ts.Expression, target: ts.Type): UserMacro | undefined { 300 | const basicUserMacro = getBasicUserMacro(state, node, target); 301 | if (basicUserMacro) { 302 | return basicUserMacro; 303 | } 304 | 305 | const manyMetadata = state.typeChecker.getTypeOfPropertyOfType(target, "_flamework_macro_many"); 306 | if (manyMetadata) { 307 | return getUserMacroOfMany(state, node, manyMetadata); 308 | } 309 | 310 | if (isTupleType(state, target)) { 311 | const userMacros = new Array(); 312 | 313 | for (const member of state.typeChecker.getTypeArguments(target)) { 314 | const userMacro = getUserMacroOfMany(state, node, member); 315 | if (!userMacro) return; 316 | 317 | userMacros.push(userMacro); 318 | } 319 | 320 | return { 321 | kind: "many", 322 | members: userMacros, 323 | }; 324 | } else if (state.typeChecker.isArrayType(target)) { 325 | const targetType = state.typeChecker.getTypeArguments(target as ts.TypeReference)[0]; 326 | const constituents = targetType.isUnion() ? targetType.types : [targetType]; 327 | const userMacros = new Array(); 328 | 329 | for (const member of constituents) { 330 | // `never` may be encountered when a union has no constituents, so we should just return an empty array. 331 | if (member.flags & ts.TypeFlags.Never) { 332 | break; 333 | } 334 | 335 | const userMacro = getUserMacroOfMany(state, node, member); 336 | if (!userMacro) return; 337 | 338 | userMacros.push(userMacro); 339 | } 340 | 341 | return { 342 | kind: "many", 343 | members: userMacros, 344 | }; 345 | } else if (isObjectType(target)) { 346 | const userMacros = new Map(); 347 | 348 | for (const member of target.getProperties()) { 349 | const memberType = state.typeChecker.getTypeOfPropertyOfType(target, member.name); 350 | if (!memberType) return; 351 | 352 | const userMacro = getUserMacroOfMany(state, node, memberType); 353 | if (!userMacro) return; 354 | 355 | userMacros.set(member.name, userMacro); 356 | } 357 | 358 | return { 359 | kind: "many", 360 | members: userMacros, 361 | }; 362 | } else if (target.isStringLiteral() || target.isNumberLiteral()) { 363 | return { 364 | kind: "literal", 365 | value: target.value, 366 | }; 367 | } else if (target.flags & ts.TypeFlags.Undefined) { 368 | return { 369 | kind: "literal", 370 | value: undefined, 371 | }; 372 | } else if (target.flags & ts.TypeFlags.BooleanLiteral) { 373 | return { 374 | kind: "literal", 375 | value: (target as ts.FreshableType).regularType === state.typeChecker.getTrueType() ? true : false, 376 | }; 377 | } 378 | 379 | Diagnostics.error(node, `Unknown type '${target.checker.typeToString(target)}' encountered`); 380 | } 381 | 382 | function getBasicUserMacro(state: TransformState, node: ts.Expression, target: ts.Type): UserMacro | undefined { 383 | const genericMetadata = state.typeChecker.getTypeOfPropertyOfType(target, "_flamework_macro_generic"); 384 | if (genericMetadata) { 385 | const targetType = state.typeChecker.getTypeOfPropertyOfType(genericMetadata, "0"); 386 | const metadataType = state.typeChecker.getTypeOfPropertyOfType(genericMetadata, "1"); 387 | if (!targetType) return; 388 | if (!metadataType) return; 389 | 390 | const metadata = getMetadataFromType(metadataType); 391 | if (!metadata) { 392 | Diagnostics.error( 393 | node, 394 | `Flamework encountered invalid metadata: '${state.typeChecker.typeToString(metadataType)}'`, 395 | ); 396 | } 397 | 398 | return { 399 | kind: "generic", 400 | target: targetType, 401 | metadata, 402 | }; 403 | } 404 | 405 | const callerMetadata = state.typeChecker.getTypeOfPropertyOfType(target, "_flamework_macro_caller"); 406 | if (callerMetadata) { 407 | const metadata = getMetadataFromType(callerMetadata); 408 | if (!metadata) return; 409 | 410 | return { 411 | kind: "caller", 412 | metadata, 413 | }; 414 | } 415 | 416 | const hashMetadata = state.typeChecker.getTypeOfPropertyOfType(target, "_flamework_macro_hash"); 417 | if (hashMetadata) { 418 | const text = state.typeChecker.getTypeOfPropertyOfType(hashMetadata, "0"); 419 | const context = state.typeChecker.getTypeOfPropertyOfType(hashMetadata, "1"); 420 | const isObfuscation = state.typeChecker.getTypeOfPropertyOfType(hashMetadata, "2"); 421 | if (!text || !text.isStringLiteral()) return; 422 | if (!context) return; 423 | 424 | const contextName = context.isStringLiteral() ? context.value : "@"; 425 | return { 426 | kind: "literal", 427 | value: isObfuscation 428 | ? state.obfuscateText(text.value, contextName) 429 | : state.buildInfo.hashString(text.value, contextName), 430 | }; 431 | } 432 | 433 | const nonNullableTarget = target.getNonNullableType(); 434 | const labelMetadata = state.typeChecker.getTypeOfPropertyOfType(nonNullableTarget, "_flamework_macro_tuple_labels"); 435 | if (labelMetadata) { 436 | return getLabels(state, labelMetadata); 437 | } 438 | 439 | const intrinsicMetadata = state.typeChecker.getTypeOfPropertyOfType(nonNullableTarget, "_flamework_intrinsic"); 440 | if (intrinsicMetadata) { 441 | if (isTupleType(state, intrinsicMetadata) && intrinsicMetadata.typeArguments) { 442 | const [id, ...inputs] = intrinsicMetadata.typeArguments; 443 | if (!id || !id.isStringLiteral()) return; 444 | 445 | return { 446 | kind: "intrinsic", 447 | id: id.value, 448 | inputs, 449 | }; 450 | } 451 | } 452 | } 453 | 454 | function getUserMacroOfType(state: TransformState, node: ts.Expression, target: ts.Type): UserMacro | undefined { 455 | const manyMetadata = state.typeChecker.getTypeOfPropertyOfType(target, "_flamework_macro_many"); 456 | if (manyMetadata) { 457 | return getUserMacroOfMany(state, node, manyMetadata); 458 | } else { 459 | return getBasicUserMacro(state, node, target); 460 | } 461 | } 462 | 463 | /** 464 | * This allows user macros to specify signatures that can accept non-metadata, like in Flamework components. 465 | * Multiple modding types in a single parameter aren't supported, and Flamework will choose a random one. 466 | * 467 | * For example, `string | Modding.Generic`, will generate the ID for `T`, but also allow users to pass in one manually. 468 | */ 469 | function getUserMacroOfUnion(state: TransformState, node: ts.Expression, target: ts.Type) { 470 | if (!target.isUnion()) { 471 | return getUserMacroOfType(state, node, target); 472 | } 473 | 474 | for (const constituent of target.types) { 475 | const macro = getUserMacroOfType(state, node, constituent); 476 | if (macro) { 477 | return macro; 478 | } 479 | } 480 | } 481 | 482 | function isObjectType(type: ts.Type): boolean { 483 | return type.isIntersection() ? type.types.every(isObjectType) : (type.flags & ts.TypeFlags.Object) !== 0; 484 | } 485 | 486 | function getParameterCount(state: TransformState, signature: ts.Signature) { 487 | const length = signature.parameters.length; 488 | if (ts.signatureHasRestParameter(signature)) { 489 | const restType = state.typeChecker.getTypeOfSymbol(signature.parameters[length - 1]); 490 | if (isTupleType(state, restType)) { 491 | return length + restType.target.fixedLength - (restType.target.hasRestElement ? 0 : 1); 492 | } 493 | } 494 | return length; 495 | } 496 | 497 | export type UserMacro = 498 | | { 499 | kind: "generic"; 500 | target: ts.Type; 501 | metadata: string; 502 | } 503 | | { 504 | kind: "caller"; 505 | metadata: string; 506 | } 507 | | { 508 | kind: "many"; 509 | members: Map | Array; 510 | } 511 | | { 512 | kind: "literal"; 513 | value: string | number | boolean | undefined; 514 | } 515 | | { 516 | kind: "intrinsic"; 517 | id: string; 518 | inputs: ts.Type[]; 519 | }; 520 | -------------------------------------------------------------------------------- /src/util/functions/buildGuardFromType.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import { DiagnosticError, Diagnostics } from "../../classes/diagnostics"; 3 | import { TransformState } from "../../classes/transformState"; 4 | import { f } from "../factory"; 5 | import { getDeclarationOfType } from "./getDeclarationOfType"; 6 | import { getInstanceTypeFromType } from "./getInstanceTypeFromType"; 7 | import assert from "assert"; 8 | 9 | /** 10 | * Convert a type into a list of typeguards. 11 | * @param state The TransformState 12 | * @param file The file that this type belongs to 13 | * @param type The type to convert 14 | * @param isInterfaceType Determines whether unknown should be omitted. 15 | * @returns An array of property assignments. 16 | */ 17 | export function buildGuardsFromType( 18 | state: TransformState, 19 | node: ts.Node, 20 | type: ts.Type, 21 | file = state.getSourceFile(node), 22 | isInterfaceType = false, 23 | ): ts.PropertyAssignment[] { 24 | const generator = createGuardGenerator(state, file, node); 25 | return generator.buildGuardsFromType(type, isInterfaceType); 26 | } 27 | 28 | // This compiles directly to `t.typeof` for any userdata that `t` does not have an alias for, or users might not have yet. 29 | const RBX_TYPES_NEW = ["buffer"]; 30 | 31 | const RBX_TYPES = [ 32 | "UDim", 33 | "UDim2", 34 | "BrickColor", 35 | "Color3", 36 | "Vector2", 37 | "Vector3", 38 | "NumberSequence", 39 | "NumberSequenceKeypoint", 40 | "ColorSequence", 41 | "ColorSequenceKeypoint", 42 | "NumberRange", 43 | "Rect", 44 | "DockWidgetPluginGuiInfo", 45 | "CFrame", 46 | "Axes", 47 | "Faces", 48 | "Font", 49 | "Instance", 50 | "Ray", 51 | "Random", 52 | "Region3", 53 | "Region3int16", 54 | "Enum", 55 | "TweenInfo", 56 | "PhysicalProperties", 57 | "Vector3int16", 58 | "Vector2int16", 59 | "PathWaypoint", 60 | "EnumItem", 61 | "RBXScriptSignal", 62 | "RBXScriptConnection", 63 | "FloatCurveKey", 64 | "OverlapParams", 65 | "thread", 66 | ...RBX_TYPES_NEW, 67 | ] as const; 68 | 69 | const OBJECT_IGNORED_FIELD_TYPES = ts.TypeFlags.Unknown | ts.TypeFlags.Never | ts.TypeFlags.UniqueESSymbol; 70 | const DEDUP_HEURISTIC_LIMIT = 5; 71 | const DEDUP_HEURISTIC_FLAGS = ts.TypeFlags.Object | ts.TypeFlags.UnionOrIntersection; 72 | 73 | function getTypesRequiringDedupHeuristic(type: ts.Type, dedupLimit = DEDUP_HEURISTIC_LIMIT) { 74 | const seenCount = new Map(); 75 | 76 | function recurse(type: ts.Type, modifier = 1) { 77 | if (type.flags & DEDUP_HEURISTIC_FLAGS) { 78 | const typeSeenCount = seenCount.get(type) ?? 0; 79 | seenCount.set(type, typeSeenCount + modifier); 80 | } 81 | 82 | if (type.isUnionOrIntersection()) { 83 | type.types.forEach((ty) => recurse(ty, modifier)); 84 | } else if (type.flags & ts.TypeFlags.Object && !isInstanceType(type)) { 85 | for (const property of type.getProperties()) { 86 | const propertyType = type.checker.getTypeOfPropertyOfType(type, property.name); 87 | if (!propertyType) { 88 | continue; 89 | } 90 | 91 | recurse(propertyType, modifier); 92 | } 93 | 94 | for (const indexInfo of type.checker.getIndexInfosOfType(type)) { 95 | recurse(indexInfo.keyType, modifier); 96 | recurse(indexInfo.type, modifier); 97 | } 98 | } 99 | } 100 | 101 | recurse(type); 102 | 103 | const requiresDedup = new Set(); 104 | 105 | for (const [type, count] of seenCount) { 106 | if (count >= dedupLimit) { 107 | requiresDedup.add(type); 108 | 109 | // We subtract all the children, as deduplicating the parent effectively removes `count - 1` of any children from the emit. 110 | recurse(type, -(count - 1)); 111 | } 112 | } 113 | 114 | return requiresDedup; 115 | } 116 | 117 | /** 118 | * Convert a type into a type guard. 119 | * @param state The TransformState 120 | * @param file The file that this type belongs to 121 | * @param type The type to convert 122 | * @returns An array of property assignments. 123 | */ 124 | export function buildGuardFromType( 125 | state: TransformState, 126 | node: ts.Node, 127 | type: ts.Type, 128 | file = state.getSourceFile(node), 129 | ): ts.Expression { 130 | const generator = createGuardGenerator(state, file, node); 131 | return generator.buildGuard(type); 132 | } 133 | 134 | /** 135 | * Convert a type into a type guard, deduplicating large guards. 136 | * @param state The TransformState 137 | * @param file The file that this type belongs to 138 | * @param type The type to convert 139 | * @returns An array of property assignments. 140 | */ 141 | export function buildGuardFromTypeWithDedup( 142 | state: TransformState, 143 | node: ts.Node, 144 | type: ts.Type, 145 | file = state.getSourceFile(node), 146 | ) { 147 | const generator = createGuardGenerator(state, file, node); 148 | const dedupLimit = state.config.optimizations?.guardGenerationDedupLimit; 149 | if (dedupLimit !== undefined) { 150 | generator.calculateDedup(type, Math.max(dedupLimit, 1)); 151 | } 152 | 153 | return { 154 | guard: generator.buildGuard(type), 155 | statements: generator.dedupStatements, 156 | }; 157 | } 158 | 159 | /** 160 | * Creates a stateful guard generator. 161 | */ 162 | export function createGuardGenerator(state: TransformState, file: ts.SourceFile, diagnosticNode: ts.Node) { 163 | const tracking = new Array<[ts.Node, ts.Type]>(); 164 | const dedupStatements = new Array(); 165 | const dedupIds = new Map(); 166 | let requiresDedup = new Set(); 167 | 168 | return { buildGuard, buildGuardsFromType, calculateDedup, dedupStatements }; 169 | 170 | function fail(err: string): never { 171 | const basicDiagnostic = Diagnostics.createDiagnostic(diagnosticNode, ts.DiagnosticCategory.Error, err); 172 | let previousType: ts.Type | undefined; 173 | for (const location of tracking) { 174 | if (location[1] === previousType) { 175 | continue; 176 | } 177 | 178 | previousType = location[1]; 179 | ts.addRelatedInfo( 180 | basicDiagnostic, 181 | Diagnostics.createDiagnostic( 182 | f.is.namedDeclaration(location[0]) ? location[0].name : location[0], 183 | ts.DiagnosticCategory.Error, 184 | `Type was defined here: ${state.typeChecker.typeToString(location[1])}`, 185 | ), 186 | ); 187 | } 188 | throw new DiagnosticError(basicDiagnostic); 189 | } 190 | 191 | function calculateDedup(type: ts.Type, dedupLimit?: number) { 192 | requiresDedup = getTypesRequiringDedupHeuristic(type, dedupLimit); 193 | } 194 | 195 | function buildGuard(type: ts.Type): ts.Expression { 196 | if (requiresDedup.has(type)) { 197 | const existingId = dedupIds.get(type); 198 | if (existingId) { 199 | return existingId; 200 | } 201 | } 202 | 203 | const declaration = getDeclarationOfType(type); 204 | if (declaration) { 205 | tracking.push([declaration, type]); 206 | } 207 | 208 | const guard = buildGuardInner(type); 209 | 210 | if (declaration) { 211 | assert(tracking.pop()?.[0] === declaration, "Popped value was not expected"); 212 | } 213 | 214 | if (requiresDedup.has(type)) { 215 | const dedupId = f.identifier(type.aliasSymbol?.name ?? "dedup", true); 216 | dedupIds.set(type, dedupId); 217 | 218 | dedupStatements.push(f.variableStatement(dedupId, guard)); 219 | 220 | return dedupId; 221 | } 222 | 223 | return guard; 224 | } 225 | 226 | function buildGuardInner(type: ts.Type): ts.Expression { 227 | const typeChecker = state.typeChecker; 228 | const tId = state.getGuardLibrary(file); 229 | 230 | if (type.isUnion()) { 231 | return buildUnionGuard(type); 232 | } 233 | 234 | if (isInstanceType(type)) { 235 | const instanceType = getInstanceTypeFromType(file, type); 236 | const additionalGuards = new Array(); 237 | 238 | for (const property of type.getProperties()) { 239 | const propertyType = type.checker.getTypeOfPropertyOfType(type, property.name); 240 | if (propertyType && !instanceType.getProperty(property.name)) { 241 | // assume intersections are children 242 | additionalGuards.push(f.propertyAssignmentDeclaration(property.name, buildGuard(propertyType))); 243 | } 244 | } 245 | 246 | const baseGuard = f.call(f.field(tId, "instanceIsA"), [instanceType.symbol.name]); 247 | return additionalGuards.length === 0 248 | ? baseGuard 249 | : listLikeGuard("intersection", [ 250 | baseGuard, 251 | f.call(f.field(tId, "children"), [f.object(additionalGuards)]), 252 | ]); 253 | } 254 | 255 | if (type.isIntersection()) { 256 | return buildIntersectionGuard(type); 257 | } 258 | 259 | if (isConditionalType(type)) { 260 | return listLikeGuard("union", [buildGuard(type.resolvedTrueType!), buildGuard(type.resolvedFalseType!)]); 261 | } 262 | 263 | if ((type.flags & ts.TypeFlags.TypeVariable) !== 0) { 264 | const constraint = type.checker.getBaseConstraintOfType(type); 265 | if (!constraint) fail("could not find constraint of type parameter"); 266 | 267 | return buildGuard(constraint); 268 | } 269 | 270 | const literals = getLiteral(type); 271 | if (literals) { 272 | return listLikeGuard("literal", literals); 273 | } 274 | 275 | if (typeChecker.isTupleType(type)) { 276 | const typeArgs = (type as ts.TypeReference).resolvedTypeArguments ?? []; 277 | return f.call( 278 | f.field(tId, "strictArray"), 279 | typeArgs.map((x) => buildGuard(x)), 280 | ); 281 | } 282 | 283 | if (typeChecker.isArrayType(type)) { 284 | const typeArg = (type as ts.GenericType).typeArguments?.[0]; 285 | return f.call(f.field(tId, "array"), [typeArg ? buildGuard(typeArg) : f.field(tId, "any")]); 286 | } 287 | 288 | if (type.getCallSignatures().length > 0) { 289 | return f.field(tId, "callback"); 290 | } 291 | 292 | const voidType = typeChecker.getVoidType(); 293 | const undefinedType = typeChecker.getUndefinedType(); 294 | if (type === voidType || type === undefinedType) { 295 | return f.field(tId, "none"); 296 | } 297 | 298 | const anyType = typeChecker.getAnyType(); 299 | if (type === anyType) { 300 | return f.field(tId, "any"); 301 | } 302 | 303 | const stringType = typeChecker.getStringType(); 304 | if (type === stringType) { 305 | return f.field(tId, "string"); 306 | } 307 | 308 | const numberType = typeChecker.getNumberType(); 309 | if (type === numberType) { 310 | return f.field(tId, "number"); 311 | } 312 | 313 | if ((type.flags & ts.TypeFlags.Unknown) !== 0) { 314 | return listLikeGuard("union", [f.field(tId, "any"), f.field(tId, "none")]); 315 | } 316 | 317 | if (type.flags & ts.TypeFlags.TemplateLiteral) { 318 | fail(`Flamework encountered a template literal which is unsupported: ${type.checker.typeToString(type)}`); 319 | } 320 | 321 | const symbol = type.getSymbol(); 322 | if (!symbol) { 323 | fail(`An unknown type was encountered with no symbol: ${typeChecker.typeToString(type)}`); 324 | } 325 | 326 | const mapSymbol = typeChecker.resolveName("Map", undefined, ts.SymbolFlags.Type, false); 327 | const readonlyMapSymbol = typeChecker.resolveName("ReadonlyMap", undefined, ts.SymbolFlags.Type, false); 328 | const weakMapSymbol = typeChecker.resolveName("WeakMap", undefined, ts.SymbolFlags.Type, false); 329 | if (symbol === mapSymbol || symbol === readonlyMapSymbol || symbol === weakMapSymbol) { 330 | const keyType = (type as ts.GenericType).typeArguments?.[0]; 331 | const valueType = (type as ts.GenericType).typeArguments?.[1]; 332 | return f.call(f.field(tId, "map"), [ 333 | keyType ? buildGuard(keyType) : f.field(tId, "any"), 334 | valueType ? buildGuard(valueType) : f.field(tId, "any"), 335 | ]); 336 | } 337 | 338 | const setSymbol = typeChecker.resolveName("Set", undefined, ts.SymbolFlags.Type, false); 339 | const readonlySetSymbol = typeChecker.resolveName("ReadonlySet", undefined, ts.SymbolFlags.Type, false); 340 | if (symbol === setSymbol || symbol === readonlySetSymbol) { 341 | const valueType = (type as ts.GenericType).typeArguments?.[0]; 342 | return f.call(f.field(tId, "set"), [valueType ? buildGuard(valueType) : f.field(tId, "any")]); 343 | } 344 | 345 | const promiseSymbol = typeChecker.resolveName("Promise", undefined, ts.SymbolFlags.Type, false); 346 | if (symbol === promiseSymbol) { 347 | return f.field("Promise", "is"); 348 | } 349 | 350 | for (const guard of RBX_TYPES) { 351 | const guardSymbol = typeChecker.resolveName(guard, undefined, ts.SymbolFlags.Type, false); 352 | if (!guardSymbol && symbol.name === guard) { 353 | fail(`Could not find symbol for ${guard}`); 354 | } 355 | 356 | if (symbol === guardSymbol) { 357 | if (RBX_TYPES_NEW.includes(guard)) { 358 | return f.call(f.field(tId, "typeof"), [guard]); 359 | } else { 360 | return f.field(tId, guard); 361 | } 362 | } 363 | } 364 | 365 | if (type.isClass()) { 366 | fail( 367 | `Class "${type.symbol.name}" was encountered. Flamework does not support generating guards for classes.`, 368 | ); 369 | } 370 | 371 | const isObject = isObjectType(type); 372 | const indexInfos = type.checker.getIndexInfosOfType(type); 373 | if (isObject && type.getApparentProperties().length === 0 && indexInfos.length === 0) { 374 | return f.field(tId, "any"); 375 | } 376 | 377 | if (isObject || type.isClassOrInterface()) { 378 | const guards = []; 379 | 380 | if (type.getApparentProperties().length > 0) { 381 | guards.push(f.call(f.field(tId, "interface"), [f.object(buildGuardsFromType(type, true))])); 382 | } 383 | 384 | const indexInfo = indexInfos[0]; 385 | if (indexInfo) { 386 | if (indexInfos.length > 1) { 387 | fail("Flamework cannot generate types with multiple index signatures."); 388 | } 389 | 390 | guards.push(f.call(f.field(tId, "map"), [buildGuard(indexInfo.keyType), buildGuard(indexInfo.type)])); 391 | } 392 | 393 | return guards.length > 1 ? listLikeGuard("intersection", guards) : guards[0]; 394 | } 395 | 396 | fail(`An unknown type was encountered: ${typeChecker.typeToString(type)}`); 397 | } 398 | 399 | function buildUnionGuard(type: ts.UnionType) { 400 | const tId = state.getGuardLibrary(file); 401 | 402 | const boolType = type.checker.getBooleanType(); 403 | if (type === boolType) { 404 | return f.field(tId, "boolean"); 405 | } 406 | 407 | const { enums, literals, types: simplifiedTypes } = simplifyUnion(type); 408 | const [isOptional, types] = extractTypes(type.checker, simplifiedTypes); 409 | const guards = types.map((type) => buildGuard(type)); 410 | guards.push(...enums.map((enumId) => f.call(f.field(tId, "enum"), [f.field("Enum", enumId)]))); 411 | 412 | if (literals.length > 0) { 413 | guards.push(listLikeGuard("literal", literals)); 414 | } 415 | 416 | const union = guards.length > 1 ? listLikeGuard("union", guards) : guards[0]; 417 | if (!union) return f.field(tId, "none"); 418 | 419 | return isOptional ? f.call(f.field(tId, "optional"), [union]) : union; 420 | } 421 | 422 | function buildIntersectionGuard(type: ts.IntersectionType) { 423 | if (type.checker.getIndexInfosOfType(type).length > 1) { 424 | fail("Flamework cannot generate intersections with multiple index signatures."); 425 | } 426 | 427 | // We find any disjoint types (strings, numbers, etc) as intersections with them are invalid. 428 | // Most intersections with disjoint types are used to introduce nominal fields. 429 | const disjointType = type.types.find((v) => v.flags & ts.TypeFlags.DisjointDomains); 430 | if (disjointType) { 431 | return buildGuard(disjointType); 432 | } 433 | 434 | const guards = type.types.map(buildGuard); 435 | return listLikeGuard("intersection", guards); 436 | } 437 | 438 | function buildGuardsFromType(type: ts.Type, isInterfaceType = false): ts.PropertyAssignment[] { 439 | const typeChecker = state.typeChecker; 440 | 441 | const declaration = getDeclarationOfType(type); 442 | if (declaration) { 443 | tracking.push([declaration, type]); 444 | } 445 | 446 | const guards = new Array(); 447 | for (const property of type.getProperties()) { 448 | const declaration = property.valueDeclaration; 449 | const propertyType = typeChecker.getTypeOfPropertyOfType(type, property.name); 450 | if (!propertyType) fail("Could not find type for field"); 451 | 452 | if (isInterfaceType && (propertyType.flags & OBJECT_IGNORED_FIELD_TYPES) !== 0) { 453 | continue; 454 | } 455 | 456 | if (declaration) { 457 | tracking.push([declaration, propertyType]); 458 | } 459 | 460 | const attribute = buildGuard(propertyType); 461 | guards.push(f.propertyAssignmentDeclaration(property.name, attribute)); 462 | 463 | if (declaration) { 464 | assert(tracking.pop()?.[0] === declaration, "Popped value was not expected"); 465 | } 466 | } 467 | 468 | if (declaration) { 469 | assert(tracking.pop()?.[0] === declaration, "Popped value was not expected"); 470 | } 471 | 472 | return guards; 473 | } 474 | 475 | /** 476 | * This function creates a guard using either the vararg function or list (array) version. 477 | * 478 | * This is a relatively naive method of checking as it does not keep track of the real register count, 479 | * but fixing this fully would likely involve moving away from `t`. 480 | */ 481 | function listLikeGuard(guard: string, list: ts.Expression[]) { 482 | const tId = state.getGuardLibrary(file); 483 | 484 | if (list.length <= 2) { 485 | return f.call(f.field(tId, guard), list); 486 | } 487 | 488 | return f.call(f.field(tId, `${guard}List`), [list]); 489 | } 490 | } 491 | 492 | function simplifyUnion(type: ts.UnionType) { 493 | const enumType = type.checker.resolveName("Enum", undefined, ts.SymbolFlags.Type, false); 494 | if ( 495 | type.aliasSymbol && 496 | type.aliasSymbol.parent && 497 | type.checker.getMergedSymbol(type.aliasSymbol.parent) === enumType 498 | ) { 499 | return { enums: [type.aliasSymbol.name], types: [], literals: [] }; 500 | } 501 | 502 | const currentTypes = type.types; 503 | const possibleEnums = new Map>(); 504 | const enums = new Array(); 505 | const types = new Array(); 506 | const literals = new Array(); 507 | const isBoolean = currentTypes.filter((v) => v.flags & ts.TypeFlags.BooleanLiteral).length === 2; 508 | 509 | if (isBoolean) { 510 | types.push(type.checker.getBooleanType()); 511 | } 512 | 513 | for (const type of currentTypes) { 514 | // We do not need to generate symbol types as they don't exist in Lua. 515 | if (type.flags & ts.TypeFlags.ESSymbolLike) { 516 | continue; 517 | } 518 | 519 | // This is a full `boolean`, so we can skip the individual literals. 520 | if (isBoolean && type.flags & ts.TypeFlags.BooleanLiteral) { 521 | continue; 522 | } 523 | 524 | const literal = getLiteral(type, true); 525 | if (literal) { 526 | literals.push(...literal); 527 | continue; 528 | } 529 | 530 | if (!type.symbol || !type.symbol.parent) { 531 | types.push(type); 532 | continue; 533 | } 534 | 535 | const enumKind = type.symbol.parent; 536 | if (!enumKind || !enumKind.parent || type.checker.getMergedSymbol(enumKind.parent) !== enumType) { 537 | types.push(type); 538 | continue; 539 | } 540 | 541 | if (type.symbol === enumKind.exports?.get(type.symbol.escapedName)) { 542 | let enumValues = possibleEnums.get(enumKind); 543 | if (!enumValues) possibleEnums.set(enumKind, (enumValues = new Set())); 544 | 545 | enumValues.add(type); 546 | } 547 | } 548 | 549 | for (const [symbol, set] of possibleEnums) { 550 | // Add 1 to account for GetEnumItems() 551 | if (set.size + 1 === symbol.exports?.size) { 552 | enums.push(symbol.name); 553 | } else { 554 | for (const type of set) { 555 | literals.push(f.field(f.field("Enum", symbol.name), type.symbol.name)); 556 | } 557 | } 558 | } 559 | 560 | return { enums, types, literals }; 561 | } 562 | 563 | function extractTypes(typeChecker: ts.TypeChecker, types: ts.Type[]): [isOptional: boolean, types: ts.Type[]] { 564 | const undefinedtype = typeChecker.getUndefinedType(); 565 | const voidType = typeChecker.getVoidType(); 566 | 567 | return [ 568 | types.some((type) => type === undefinedtype || type === voidType), 569 | types.filter((type) => type !== undefinedtype && type !== voidType), 570 | ]; 571 | } 572 | 573 | function getLiteral(type: ts.Type, withoutEnums = false): ts.Expression[] | undefined { 574 | if (type.isStringLiteral() || type.isNumberLiteral()) { 575 | return [typeof type.value === "string" ? f.string(type.value) : f.number(type.value)]; 576 | } 577 | 578 | const trueType = type.checker.getTrueType(); 579 | if (type === trueType) { 580 | return [f.bool(true)]; 581 | } 582 | 583 | const falseType = type.checker.getFalseType(); 584 | if (type === falseType) { 585 | return [f.bool(false)]; 586 | } 587 | 588 | if (type.flags & ts.TypeFlags.Enum) { 589 | const declarations = type.symbol.declarations; 590 | if (!declarations || declarations.length != 1 || !f.is.enumDeclaration(declarations[0])) return; 591 | 592 | const declaration = declarations[0]; 593 | const memberValues = new Array(); 594 | 595 | for (const member of declaration.members) { 596 | const constant = type.checker.getConstantValue(member); 597 | if (constant === undefined) return; 598 | 599 | memberValues.push(typeof constant === "string" ? f.string(constant) : f.number(constant)); 600 | } 601 | 602 | return memberValues; 603 | } 604 | 605 | if (!withoutEnums) { 606 | const symbol = type.getSymbol(); 607 | if (!symbol) return; 608 | 609 | const enumType = type.checker.resolveName("Enum", undefined, ts.SymbolFlags.Type, false); 610 | if (symbol.parent?.parent && type.checker.getMergedSymbol(symbol.parent.parent) === enumType) { 611 | return [f.field(f.field("Enum", symbol.parent.name), symbol.name)]; 612 | } 613 | } 614 | } 615 | 616 | function isObjectType(type: ts.Type): type is ts.InterfaceType { 617 | return (type.flags & ts.TypeFlags.Object) !== 0; 618 | } 619 | 620 | function isInstanceType(type: ts.Type) { 621 | return type.getProperty("_nominal_Instance") !== undefined; 622 | } 623 | 624 | function isConditionalType(type: ts.Type): type is ts.ConditionalType { 625 | return (type.flags & ts.TypeFlags.Conditional) !== 0; 626 | } 627 | -------------------------------------------------------------------------------- /src/classes/transformState.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import fs from "fs"; 3 | import crypto from "crypto"; 4 | import path from "path"; 5 | import Hashids from "hashids"; 6 | import { transformNode } from "../transformations/transformNode"; 7 | import { Cache } from "../util/cache"; 8 | import { getPackageJson } from "../util/functions/getPackageJson"; 9 | import { BuildInfo } from "./buildInfo"; 10 | import { Logger } from "./logger"; 11 | import { f } from "../util/factory"; 12 | import { isPathDescendantOf } from "../util/functions/isPathDescendantOf"; 13 | import { ClassInfo } from "../types/classes"; 14 | import { isCleanBuildDirectory } from "../util/functions/isCleanBuildDirectory"; 15 | import { parseCommandLine } from "../util/functions/parseCommandLine"; 16 | import { createPathTranslator } from "../util/functions/createPathTranslator"; 17 | import { arePathsEqual } from "../util/functions/arePathsEqual"; 18 | import { NodeMetadata } from "./nodeMetadata"; 19 | import { RbxPath, RojoResolver } from "@roblox-ts/rojo-resolver"; 20 | import { PathTranslator } from "./pathTranslator"; 21 | import { assert } from "../util/functions/assert"; 22 | import { getSchemaErrors, validateSchema } from "../util/schema"; 23 | import { shuffle } from "../util/functions/shuffle"; 24 | import glob from "glob"; 25 | import { tryResolveTS } from "../util/functions/tryResolve"; 26 | import { Diagnostics } from "./diagnostics"; 27 | 28 | const IGNORE_RBXTS_REGEX = /node_modules\/@rbxts\/(compiler-types|types)\/.*\.d\.ts$/; 29 | 30 | /** 31 | * Runtime configuration exposed via `flamework.json` 32 | */ 33 | export interface FlameworkConfig { 34 | logLevel?: "none" | "verbose"; 35 | profiling?: boolean; 36 | disableDependencyWarnings?: boolean; 37 | } 38 | 39 | export interface TransformerConfig { 40 | /** 41 | * An internal option that should not be used. 42 | * This is used to compile the framework package, turning this on in your game will cause many errors. 43 | */ 44 | $rbxpackmode$?: boolean; 45 | 46 | /** 47 | * Disables TypeScript's own semantic diagnostics. 48 | * Improves performance, but results in increased risk of incorrect compilation as well as messed up diagnostic spans. 49 | */ 50 | noSemanticDiagnostics?: boolean; 51 | 52 | /** 53 | * This is the salt used for hashes generated by Flamework. 54 | * Defaults to a randomly generated 64 byte salt. 55 | */ 56 | salt?: string; 57 | 58 | /** 59 | * This can be used to lower collision chance with packages. 60 | * Defaults to package name. 61 | */ 62 | hashPrefix?: string; 63 | 64 | /** 65 | * Whether to automatically generate the identifiers for exports. 66 | * This is recommended for packages but it is not recommended to 67 | * enable this in games. 68 | */ 69 | preloadIds?: boolean; 70 | 71 | /** 72 | * Whether to enable flamework's obfuscation. 73 | * 74 | * This comprises of: 75 | * 1. random event names 76 | * 2. shortened ids 77 | */ 78 | obfuscation?: boolean; 79 | 80 | /** 81 | * Determines the id generation mode. 82 | * Defaults to "full" and should only be configured in game projects. 83 | */ 84 | idGenerationMode?: "full" | "short" | "tiny" | "obfuscated"; 85 | 86 | /** 87 | * Some experimental optimizations 88 | */ 89 | optimizations?: { 90 | guardGenerationDedupLimit?: number; 91 | }; 92 | } 93 | 94 | export class TransformState { 95 | public parsedCommandLine = parseCommandLine(); 96 | public currentDirectory = this.parsedCommandLine.project; 97 | public options = this.program.getCompilerOptions(); 98 | public srcDir = this.options.rootDir ?? this.currentDirectory; 99 | public outDir = this.options.outDir ?? this.currentDirectory; 100 | public rootDirs = this.options.rootDirs ? this.options.rootDirs : [this.srcDir]; 101 | public typeChecker = this.program.getTypeChecker(); 102 | 103 | public classes = new Map(); 104 | 105 | public rojoResolver?: RojoResolver; 106 | public pathTranslator!: PathTranslator; 107 | public buildInfo!: BuildInfo; 108 | 109 | public includeDirectory: string; 110 | public rootDirectory: string; 111 | public packageName: string; 112 | public isGame: boolean; 113 | 114 | public isUserMacroCache = new Map(); 115 | public flameworkGuardLibraryPath?: string; 116 | 117 | private setupBuildInfo() { 118 | let baseBuildInfo = BuildInfo.fromDirectory(this.currentDirectory); 119 | if (!baseBuildInfo || (Cache.isInitialCompile && isCleanBuildDirectory(this.options))) { 120 | if (this.options.incremental && this.options.tsBuildInfoFile) { 121 | if (ts.sys.fileExists(this.options.tsBuildInfoFile)) { 122 | throw new Error(`Flamework cannot be built in a dirty environment, please delete your tsbuildinfo`); 123 | } 124 | } 125 | baseBuildInfo = new BuildInfo(path.join(this.currentDirectory, "flamework.build")); 126 | } 127 | this.buildInfo = baseBuildInfo; 128 | this.buildInfo.setConfig(undefined); 129 | 130 | const configPath = path.join(this.rootDirectory, "flamework.json"); 131 | if (fs.existsSync(configPath)) { 132 | const result = JSON.parse(fs.readFileSync(configPath, { encoding: "ascii" })); 133 | if (validateSchema("config", result)) { 134 | this.buildInfo.setConfig(result); 135 | } else { 136 | Logger.error(`Malformed flamework.json`); 137 | for (const error of getSchemaErrors()) { 138 | Logger.error( 139 | `${error.keyword} ${error.instancePath}: ${error.message} ${JSON.stringify(error.params)}`, 140 | ); 141 | } 142 | process.exit(1); 143 | } 144 | } 145 | 146 | const candidates = Cache.buildInfoCandidates ?? []; 147 | if (!Cache.buildInfoCandidates) { 148 | Cache.buildInfoCandidates = candidates; 149 | const candidatesSet = new Set(); 150 | for (const file of this.program.getSourceFiles()) { 151 | const buildCandidate = BuildInfo.findCandidateUpper(path.dirname(file.fileName)); 152 | if ( 153 | buildCandidate && 154 | !arePathsEqual(buildCandidate, baseBuildInfo.buildInfoPath) && 155 | !candidatesSet.has(buildCandidate) 156 | ) { 157 | candidatesSet.add(buildCandidate); 158 | candidates.push(buildCandidate); 159 | } 160 | } 161 | } 162 | 163 | for (const candidate of candidates) { 164 | const relativeCandidate = path.relative(this.currentDirectory, candidate); 165 | const buildInfo = BuildInfo.fromPath(candidate); 166 | if (buildInfo) { 167 | Logger.infoIfVerbose(`Loaded buildInfo at ${relativeCandidate}, next id: ${buildInfo.getLatestId()}`); 168 | baseBuildInfo.addBuildInfo(buildInfo); 169 | } else { 170 | Logger.warn(`Build info not valid at ${relativeCandidate}`); 171 | } 172 | } 173 | } 174 | 175 | private setupRojo() { 176 | this.pathTranslator = createPathTranslator(this.program); 177 | 178 | const rojoArgvIndex = process.argv.findIndex((v) => v === "--rojo"); 179 | const rojoArg = rojoArgvIndex !== -1 ? process.argv[rojoArgvIndex + 1] : undefined; 180 | 181 | let rojoConfig: string | undefined; 182 | if (rojoArg && rojoArg !== "") { 183 | rojoConfig = path.resolve(rojoArg); 184 | } else { 185 | rojoConfig = RojoResolver.findRojoConfigFilePath(this.currentDirectory).path; 186 | } 187 | 188 | if (rojoConfig !== undefined) { 189 | const rojoContents = fs.readFileSync(rojoConfig, { encoding: "ascii" }); 190 | const sum = crypto.createHash("md5").update(rojoContents).digest("hex"); 191 | 192 | if (sum === Cache.rojoSum) { 193 | this.rojoResolver = Cache.rojoResolver; 194 | } else { 195 | this.rojoResolver = RojoResolver.fromPath(rojoConfig); 196 | Cache.rojoSum = sum; 197 | Cache.rojoResolver = this.rojoResolver; 198 | } 199 | } 200 | } 201 | 202 | private getIncludePath() { 203 | const includeArgvIndex = process.argv.findIndex((v) => v === "--i" || v === "--includePath"); 204 | const includePath = includeArgvIndex !== -1 ? process.argv[includeArgvIndex + 1] : undefined; 205 | return path.resolve(includePath || path.join(this.rootDirectory, "include")); 206 | } 207 | 208 | /** 209 | * Since npm modules can be symlinked, TypeScript can resolve them to their real path (outside of the project directory.) 210 | * 211 | * This function attempts to convert the real path of *npm modules* back to their path inside the project directory. 212 | * This is required to have RojoResolver be able to resolve files. 213 | */ 214 | private toModulePath(filePath: string) { 215 | // The module is under our root directory, so it's probably not symlinked. 216 | if (isPathDescendantOf(filePath, this.rootDirectory)) { 217 | return filePath; 218 | } 219 | 220 | const packageJsonPath = ts.findPackageJson(filePath, ts.sys as never); 221 | if (!packageJsonPath) { 222 | throw new Error(`Unable to convert '${filePath}' to module.`); 223 | } 224 | 225 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, { encoding: "utf8" })); 226 | return path.join( 227 | this.rootDirectory, 228 | "node_modules", 229 | packageJson.name, 230 | path.relative(path.dirname(packageJsonPath), filePath), 231 | ); 232 | } 233 | 234 | private calculateGlobs(globs: Record | undefined) { 235 | if (!globs) { 236 | return; 237 | } 238 | 239 | for (const pathGlob in globs) { 240 | const paths = glob.sync(pathGlob, { 241 | root: this.rootDirectory, 242 | cwd: this.rootDirectory, 243 | nocase: true, 244 | }); 245 | 246 | globs[pathGlob] = paths.map((globPath) => { 247 | const outputPath = this.pathTranslator.getOutputPath(globPath); 248 | return path.relative(this.rootDirectory, outputPath).replace(/\\/g, "/"); 249 | }); 250 | } 251 | } 252 | 253 | private convertGlobs( 254 | globs: Record | undefined, 255 | luaOut: Map>>, 256 | pkg?: string, 257 | ) { 258 | if (!globs) { 259 | return; 260 | } 261 | 262 | const pkgInfo = pkg ? this.buildInfo.getBuildInfoFromPrefix(pkg) : undefined; 263 | const root = pkgInfo ? path.dirname(this.toModulePath(pkgInfo.buildInfoPath)) : this.rootDirectory; 264 | 265 | for (const pathGlob in globs) { 266 | const paths = globs[pathGlob]; 267 | const rbxPaths = new Array(); 268 | for (const globPath of paths) { 269 | const rbxPath = this.rojoResolver?.getRbxPathFromFilePath(path.join(root, globPath)); 270 | if (rbxPath) { 271 | rbxPaths.push(rbxPath); 272 | } 273 | } 274 | 275 | luaOut.set(pkgInfo ? pathGlob : this.obfuscateText(pathGlob, "addPaths"), rbxPaths); 276 | } 277 | } 278 | 279 | constructor( 280 | public program: ts.Program, 281 | public context: ts.TransformationContext, 282 | public config: TransformerConfig, 283 | ) { 284 | const { result: packageJson, directory } = getPackageJson(this.currentDirectory); 285 | this.rootDirectory = directory; 286 | assert(packageJson.name); 287 | 288 | this.setupRojo(); 289 | this.setupBuildInfo(); 290 | 291 | config.idGenerationMode ??= config.obfuscation ? "obfuscated" : "full"; 292 | 293 | this.packageName = packageJson.name; 294 | this.isGame = !this.packageName.startsWith("@"); 295 | this.includeDirectory = this.getIncludePath(); 296 | 297 | if (!this.isGame) config.hashPrefix ??= this.packageName; 298 | this.buildInfo.setIdentifierPrefix(config.hashPrefix); 299 | 300 | if (config.hashPrefix?.startsWith("$") && !config.$rbxpackmode$) { 301 | throw new Error(`The hashPrefix $ is used internally by Flamework`); 302 | } 303 | 304 | Cache.isInitialCompile = false; 305 | } 306 | 307 | getFileId(file: ts.SourceFile) { 308 | return path.relative(this.rootDirectory, file.fileName).replace(/\\/g, "/"); 309 | } 310 | 311 | saveArtifacts() { 312 | const start = new Date().getTime(); 313 | 314 | this.calculateGlobs(this.buildInfo.getMetadata("globs")?.paths); 315 | 316 | if (this.isGame) { 317 | const writtenFiles = new Map(); 318 | const files = ["config.json", "globs.json"]; 319 | 320 | const packageConfig = this.buildInfo.getChildrenMetadata("config"); 321 | const config = this.buildInfo.getMetadata("config"); 322 | if (config || packageConfig.size > 0) { 323 | writtenFiles.set( 324 | "config.json", 325 | JSON.stringify({ 326 | game: config, 327 | packages: Object.fromEntries(packageConfig), 328 | }), 329 | ); 330 | } 331 | 332 | const packageGlobs = this.buildInfo.getChildrenMetadata("globs"); 333 | const globs = this.buildInfo.getMetadata("globs"); 334 | if (globs || packageGlobs.size > 0) { 335 | const transformedGlobs = new Map(); 336 | this.convertGlobs(globs?.paths, transformedGlobs); 337 | 338 | const transformedPackageGlobs = new Map>(); 339 | for (const [pkg, packageGlob] of packageGlobs) { 340 | const transformedGlobs = new Map(); 341 | this.convertGlobs(packageGlob?.paths, transformedGlobs, pkg); 342 | transformedPackageGlobs.set(pkg, Object.fromEntries(transformedGlobs)); 343 | } 344 | 345 | writtenFiles.set( 346 | "globs.json", 347 | JSON.stringify({ 348 | game: Object.fromEntries(transformedGlobs), 349 | packages: Object.fromEntries(transformedPackageGlobs), 350 | }), 351 | ); 352 | } 353 | 354 | const metadataPath = path.join(this.includeDirectory, "flamework"); 355 | const metadataExists = fs.existsSync(metadataPath); 356 | 357 | if (!metadataExists && writtenFiles.size > 0) { 358 | fs.mkdirSync(metadataPath); 359 | } 360 | 361 | for (const file of files) { 362 | const filePath = path.join(metadataPath, file); 363 | const contents = writtenFiles.get(file); 364 | if (contents) { 365 | fs.writeFileSync(filePath, contents); 366 | } else if (fs.existsSync(filePath)) { 367 | fs.rmSync(filePath); 368 | } 369 | } 370 | 371 | if (metadataExists && writtenFiles.size === 0) { 372 | fs.rmdirSync(metadataPath); 373 | } 374 | } 375 | 376 | this.buildInfo.save(); 377 | 378 | if (Logger.verbose) { 379 | // Watch mode includes an extra newline when compilation finishes, 380 | // so we remove that newline before Flamework's message. 381 | const watch = process.argv.includes("-w") || process.argv.includes("--watch"); 382 | if (watch) { 383 | process.stdout.write("\x1b[A\x1b[K"); 384 | } 385 | 386 | Logger.info(`Flamework artifacts finished in ${new Date().getTime() - start}ms`); 387 | 388 | if (watch) { 389 | process.stdout.write("\n"); 390 | } 391 | } 392 | } 393 | 394 | isUserMacro(symbol: ts.Symbol) { 395 | const cached = this.isUserMacroCache.get(symbol); 396 | if (cached !== undefined) return cached; 397 | 398 | if (symbol.declarations) { 399 | for (const declaration of symbol.declarations) { 400 | const metadata = new NodeMetadata(this, declaration); 401 | if (metadata.isRequested("macro")) { 402 | this.isUserMacroCache.set(symbol, true); 403 | return true; 404 | } 405 | } 406 | } 407 | 408 | this.isUserMacroCache.set(symbol, false); 409 | return false; 410 | } 411 | 412 | public fileImports = new Map(); 413 | addFileImport(file: ts.SourceFile, importPath: string, name: string): ts.Identifier { 414 | // Flamework itself uses features which require imports, this will rewrite those imports to be valid inside the Flamework package. 415 | if (importPath === "@flamework/core" && this.packageName === "@flamework/core" && this.config.$rbxpackmode$) { 416 | const fileName = path.basename(file.fileName); 417 | if (fileName === "flamework.ts" && name === "Flamework") { 418 | return f.identifier("Flamework"); 419 | } 420 | 421 | const modulePath = path.join(this.rootDirectory, "src", name === "Reflect" ? "reflect" : "flamework"); 422 | importPath = "./" + path.relative(path.dirname(file.fileName), modulePath) || "."; 423 | } 424 | 425 | let importInfos = this.fileImports.get(file.fileName); 426 | if (!importInfos) this.fileImports.set(file.fileName, (importInfos = [])); 427 | 428 | let importInfo = importInfos.find((x) => x.path === importPath); 429 | if (!importInfo) importInfos.push((importInfo = { path: importPath, entries: [] })); 430 | 431 | let identifier = importInfo.entries.find((x) => x.name === name)?.identifier; 432 | 433 | if (!identifier) { 434 | start: for (const statement of file.statements) { 435 | if (!f.is.importDeclaration(statement)) break; 436 | if (!f.is.string(statement.moduleSpecifier)) continue; 437 | if (!f.is.importClauseDeclaration(statement.importClause)) continue; 438 | if (!f.is.namedImports(statement.importClause.namedBindings)) continue; 439 | if (statement.importClause.isTypeOnly) continue; 440 | if (statement.moduleSpecifier.text !== importPath) continue; 441 | 442 | for (const importElement of statement.importClause.namedBindings.elements) { 443 | if (importElement.isTypeOnly) { 444 | continue; 445 | } 446 | 447 | if (importElement.propertyName) { 448 | if (importElement.propertyName.text === name) { 449 | identifier = importElement.name; 450 | break start; 451 | } 452 | } else { 453 | if (importElement.name.text === name) { 454 | identifier = importElement.name; 455 | break start; 456 | } 457 | } 458 | } 459 | } 460 | } 461 | 462 | if (!identifier) { 463 | importInfo.entries.push({ name, identifier: (identifier = f.identifier(name, true)) }); 464 | } 465 | 466 | return identifier; 467 | } 468 | 469 | getSourceFile(node: ts.Node) { 470 | const parseNode = ts.getParseTreeNode(node); 471 | if (!parseNode) throw new Error(`Could not find parse tree node`); 472 | 473 | return ts.getSourceFileOfNode(parseNode); 474 | } 475 | 476 | getSymbol(node: ts.Node, followAlias = true): ts.Symbol | undefined { 477 | if (f.is.namedDeclaration(node)) { 478 | return this.getSymbol(node.name); 479 | } 480 | 481 | const symbol = this.typeChecker.getSymbolAtLocation(node); 482 | 483 | if (symbol && followAlias) { 484 | return ts.skipAlias(symbol, this.typeChecker); 485 | } else { 486 | return symbol; 487 | } 488 | } 489 | 490 | hash(id: number, noPrefix?: boolean) { 491 | const hashPrefix = this.config.hashPrefix; 492 | const salt = this.config.salt ?? this.buildInfo.getSalt(); 493 | const hashGenerator = new Hashids(salt, 2); 494 | if ((this.isGame && !hashPrefix) || noPrefix) { 495 | return `${hashGenerator.encode(id)}`; 496 | } else { 497 | // If the package name is namespaced, then it can be used in 498 | // other projects so we want to add a prefix to the Id to prevent 499 | // collisions with other packages or the game. 500 | return `${hashPrefix ?? this.packageName}:${hashGenerator.encode(id)}`; 501 | } 502 | } 503 | 504 | obfuscateText(text: string, context?: string) { 505 | return this.config.obfuscation ? this.buildInfo.hashString(text, context) : text; 506 | } 507 | 508 | obfuscateArray(array: ReadonlyArray) { 509 | return this.config.obfuscation ? shuffle(array) : array; 510 | } 511 | 512 | addDiagnostic(diag: ts.DiagnosticWithLocation) { 513 | this.context.addDiagnostic(diag); 514 | } 515 | 516 | private prereqStack = new Array>(); 517 | capture(cb: () => T): [T, ts.Statement[]] { 518 | this.prereqStack.push([]); 519 | const result = cb(); 520 | return [result, this.prereqStack.pop()!]; 521 | } 522 | 523 | prereq(statement: ts.Statement) { 524 | const stack = this.prereqStack[this.prereqStack.length - 1]; 525 | if (stack) stack.push(statement); 526 | } 527 | 528 | prereqList(statements: ts.Statement[]) { 529 | const stack = this.prereqStack[this.prereqStack.length - 1]; 530 | if (stack) stack.push(...statements); 531 | } 532 | 533 | isCapturing(threshold = 1) { 534 | return this.prereqStack.length > threshold; 535 | } 536 | 537 | transform(node: T): T { 538 | return ts.visitEachChild(node, (newNode) => transformNode(this, newNode), this.context); 539 | } 540 | 541 | transformNode(node: T): T { 542 | // Technically this isn't guaranteed to return `T`, and TypeScript 5.0+ updated the signature to disallow this, 543 | // but we don't care so we'll just cast it. 544 | return ts.visitNode(node, (newNode) => transformNode(this, newNode)) as T; 545 | } 546 | 547 | private _shouldViewFile(file: ts.SourceFile) { 548 | const fileName = path.posix.normalize(file.fileName); 549 | if (IGNORE_RBXTS_REGEX.test(fileName)) return false; 550 | 551 | const buildCandidates = Cache.buildInfoCandidates!; 552 | for (const candidate of buildCandidates) { 553 | let realPath = Cache.realPath.get(candidate); 554 | if (!realPath) Cache.realPath.set(candidate, (realPath = fs.realpathSync(candidate))); 555 | 556 | const candidateDir = path.dirname(realPath); 557 | if ( 558 | isPathDescendantOf(file.fileName, candidateDir) && 559 | !isPathDescendantOf(file.fileName, path.join(candidateDir, "node_modules")) 560 | ) { 561 | return true; 562 | } 563 | } 564 | 565 | return false; 566 | } 567 | 568 | shouldViewFile(file: ts.SourceFile) { 569 | const cached = Cache.shouldView?.get(file.fileName); 570 | if (cached !== undefined) return cached; 571 | 572 | const result = this._shouldViewFile(file); 573 | Cache.shouldView.set(file.fileName, result); 574 | 575 | return result; 576 | } 577 | 578 | getGuardLibrary(file: ts.SourceFile) { 579 | if (this.flameworkGuardLibraryPath) { 580 | return this.addFileImport(file, this.flameworkGuardLibraryPath, "t"); 581 | } 582 | 583 | const corePath = tryResolveTS(this, "@flamework/core", file.fileName); 584 | if (corePath === undefined) { 585 | Diagnostics.warning(file.endOfFileToken, "Flamework core was not found, guard generation may not work."); 586 | return this.addFileImport(file, "@rbxts/t", "t"); 587 | } 588 | 589 | const fileGuardPath = tryResolveTS(this, "@rbxts/t", path.dirname(file.fileName)); 590 | const coreGuardPath = tryResolveTS(this, "@rbxts/t", corePath); 591 | if (fileGuardPath === coreGuardPath) { 592 | // @flamework/core and the consuming project are using the same @rbxts/t version. 593 | this.flameworkGuardLibraryPath = "@rbxts/t"; 594 | return this.addFileImport(file, "@rbxts/t", "t"); 595 | } 596 | 597 | if ( 598 | coreGuardPath === undefined || 599 | !isPathDescendantOf(coreGuardPath, path.join(path.dirname(corePath), "../node_modules/@rbxts/t")) 600 | ) { 601 | Diagnostics.warning(file.endOfFileToken, "Valid `@rbxts/t` was not found, guard generation may not work."); 602 | return this.addFileImport(file, "@rbxts/t", "t"); 603 | } 604 | 605 | this.flameworkGuardLibraryPath = "@flamework/core/out/prelude"; 606 | return this.addFileImport(file, this.flameworkGuardLibraryPath, "t"); 607 | } 608 | } 609 | 610 | interface ImportItem { 611 | name: string; 612 | identifier: ts.Identifier; 613 | } 614 | 615 | interface ImportInfo { 616 | path: string; 617 | entries: Array; 618 | } 619 | -------------------------------------------------------------------------------- /src/util/factory.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | /** 4 | * Shorthand factory methods. 5 | * 6 | * Naming scheme: 7 | * 8 | * f.expressionType 9 | * f.declarationTypeDeclaration 10 | * f.statementTypeStatement 11 | * f.typeNodeType 12 | * 13 | * f.is.* 14 | * f.update.* 15 | * 16 | * Examples: 17 | * 18 | * f.string() 19 | * f.classDeclaration() 20 | * f.ifStatement() 21 | */ 22 | export namespace f { 23 | let factory = ts.factory; 24 | 25 | type NodeArray = ReadonlyArray | ts.NodeArray; 26 | type ONodeArray = NodeArray | undefined; 27 | export type ConvertableExpression = string | number | ts.Expression | Array | boolean; 28 | export function toExpression( 29 | expression: ConvertableExpression, 30 | stringFn: (param: string) => ts.Expression = string, 31 | ): ts.Expression { 32 | if (typeof expression === "string") { 33 | return stringFn(expression); 34 | } else if (typeof expression === "number") { 35 | return number(expression); 36 | } else if (typeof expression === "boolean") { 37 | return bool(expression); 38 | } else if (Array.isArray(expression)) { 39 | return array(expression.map((x) => toExpression(x))); 40 | } else { 41 | return expression; 42 | } 43 | } 44 | 45 | /// Expressions 46 | 47 | export function string(str: string) { 48 | return factory.createStringLiteral(str); 49 | } 50 | 51 | export function bool(value: boolean) { 52 | return value ? factory.createTrue() : factory.createFalse(); 53 | } 54 | 55 | export function array(values: ts.Expression[], multiLine = true) { 56 | return factory.createArrayLiteralExpression(values, multiLine); 57 | } 58 | 59 | export function number(value: number | string, flags?: ts.TokenFlags) { 60 | return +value < 0 61 | ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(-value, flags)) 62 | : factory.createNumericLiteral(value, flags); 63 | } 64 | 65 | export function identifier(name: string, unique = false) { 66 | return unique 67 | ? factory.createUniqueName(name, ts.GeneratedIdentifierFlags.Optimistic) 68 | : factory.createIdentifier(name); 69 | } 70 | 71 | export function nil() { 72 | return identifier("undefined"); 73 | } 74 | 75 | export function field( 76 | name: ts.Expression | string, 77 | property: ts.Expression | ts.PropertyName | ts.MemberName | string, 78 | expression = false, 79 | ): ts.ElementAccessExpression | ts.PropertyAccessExpression { 80 | if (typeof property === "string") { 81 | return factory.createElementAccessExpression(toExpression(name, identifier), string(property)); 82 | } 83 | 84 | if (ts.isComputedPropertyName(property)) { 85 | return field(name, property.expression); 86 | } 87 | 88 | if (ts.isMemberName(property) && !expression) { 89 | return factory.createPropertyAccessExpression(toExpression(name, identifier), property); 90 | } else { 91 | return factory.createElementAccessExpression(toExpression(name, identifier), toExpression(property)); 92 | } 93 | } 94 | 95 | export function statement(expression?: ConvertableExpression) { 96 | if (expression !== undefined) { 97 | return factory.createExpressionStatement(toExpression(expression)); 98 | } else { 99 | return factory.createExpressionStatement(identifier("undefined")); 100 | } 101 | } 102 | 103 | export function call( 104 | expression: ts.Expression | string, 105 | args?: ConvertableExpression[], 106 | typeArguments?: ts.TypeNode[], 107 | ) { 108 | return factory.createCallExpression( 109 | toExpression(expression, identifier), 110 | typeArguments, 111 | args?.map((x) => toExpression(x)), 112 | ); 113 | } 114 | 115 | export function object( 116 | properties: 117 | | readonly ts.ObjectLiteralElementLike[] 118 | | { [key: string]: ConvertableExpression | Array }, 119 | multiLine = true, 120 | ) { 121 | if (properties instanceof Array) { 122 | return factory.createObjectLiteralExpression(properties, multiLine); 123 | } else { 124 | const realProperties: ts.ObjectLiteralElementLike[] = []; 125 | for (const key of Object.keys(properties)) { 126 | realProperties.push(propertyAssignmentDeclaration(key, properties[key])); 127 | } 128 | return factory.createObjectLiteralExpression(realProperties, multiLine); 129 | } 130 | } 131 | 132 | export function as(expression: ts.Expression, node: ts.TypeNode, explicit = false) { 133 | return explicit 134 | ? factory.createAsExpression( 135 | factory.createAsExpression(expression, keywordType(ts.SyntaxKind.UnknownKeyword)), 136 | node, 137 | ) 138 | : factory.createAsExpression(expression, node); 139 | } 140 | 141 | export function asNever(expression: ts.Expression) { 142 | return f.as(expression, keywordType(ts.SyntaxKind.NeverKeyword)); 143 | } 144 | 145 | export function binary( 146 | left: ConvertableExpression, 147 | op: ts.BinaryOperator | ts.BinaryOperatorToken, 148 | right: ConvertableExpression, 149 | ) { 150 | return factory.createBinaryExpression(toExpression(left), op, toExpression(right)); 151 | } 152 | 153 | export function elementAccessExpression( 154 | expression: ConvertableExpression, 155 | index: ConvertableExpression, 156 | questionToken?: ts.QuestionDotToken, 157 | ) { 158 | return questionToken 159 | ? factory.createElementAccessChain(toExpression(expression), questionToken, toExpression(index)) 160 | : factory.createElementAccessExpression(toExpression(expression), toExpression(index)); 161 | } 162 | 163 | export function propertyAccessExpression(expression: ConvertableExpression, name: ts.MemberName) { 164 | return factory.createPropertyAccessExpression(toExpression(expression), name); 165 | } 166 | 167 | export function arrowFunction( 168 | body: ts.ConciseBody, 169 | parameters?: ts.ParameterDeclaration[], 170 | typeParameters?: ts.TypeParameterDeclaration[], 171 | type?: ts.TypeNode, 172 | ) { 173 | return factory.createArrowFunction( 174 | undefined, 175 | typeParameters, 176 | parameters ?? [], 177 | type, 178 | factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 179 | body, 180 | ); 181 | } 182 | 183 | export function bang(expression: ConvertableExpression) { 184 | return factory.createNonNullExpression(toExpression(expression, identifier)); 185 | } 186 | 187 | export function self() { 188 | return ts.factory.createThis(); 189 | } 190 | 191 | export function superExpression() { 192 | return ts.factory.createSuper(); 193 | } 194 | 195 | /// Statements 196 | 197 | export function block(statements: ts.Statement[], multiLine = true) { 198 | return factory.createBlock(statements, multiLine); 199 | } 200 | 201 | export function returnStatement(expression?: ConvertableExpression) { 202 | return factory.createReturnStatement(expression ? toExpression(expression) : undefined); 203 | } 204 | 205 | export function variableStatement( 206 | name: string | ts.BindingName, 207 | initializer?: ts.Expression, 208 | type?: ts.TypeNode, 209 | isMutable = false, 210 | ) { 211 | return factory.createVariableStatement( 212 | undefined, 213 | factory.createVariableDeclarationList( 214 | [factory.createVariableDeclaration(name, undefined, type, initializer)], 215 | isMutable ? ts.NodeFlags.Let : ts.NodeFlags.Const, 216 | ), 217 | ); 218 | } 219 | 220 | /// Declarations 221 | export function methodDeclaration( 222 | name: string | ts.PropertyName, 223 | body?: ts.Block, 224 | parameters?: ts.ParameterDeclaration[], 225 | type?: ts.TypeNode, 226 | isOptional = false, 227 | typeParameters?: ts.TypeParameterDeclaration[], 228 | ) { 229 | return factory.createMethodDeclaration( 230 | undefined, 231 | undefined, 232 | name, 233 | isOptional ? token(ts.SyntaxKind.QuestionToken) : undefined, 234 | typeParameters, 235 | parameters ?? [], 236 | type, 237 | body, 238 | ); 239 | } 240 | 241 | export function arrayBindingDeclaration(elements: Array) { 242 | return factory.createArrayBindingPattern( 243 | elements.map((x) => factory.createBindingElement(undefined, undefined, x, undefined)), 244 | ); 245 | } 246 | 247 | export function parameterDeclaration( 248 | name: string | ts.BindingName, 249 | type?: ts.TypeNode, 250 | value?: ts.Expression, 251 | isOptional?: boolean, 252 | isSpread?: boolean, 253 | ) { 254 | return factory.createParameterDeclaration( 255 | undefined, 256 | isSpread ? factory.createToken(ts.SyntaxKind.DotDotDotToken) : undefined, 257 | name, 258 | isOptional ? factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, 259 | type, 260 | value, 261 | ); 262 | } 263 | 264 | export function typeParameterDeclaration( 265 | name: string | ts.Identifier, 266 | constraint?: ts.TypeNode, 267 | defaultType?: ts.TypeNode, 268 | ) { 269 | return factory.createTypeParameterDeclaration(undefined, name, constraint, defaultType); 270 | } 271 | 272 | export function propertyAssignmentDeclaration(name: ts.PropertyName | string, value: ConvertableExpression) { 273 | return factory.createPropertyAssignment(typeof name === "string" ? string(name) : name, toExpression(value)); 274 | } 275 | 276 | export function propertyDeclaration( 277 | name: ts.PropertyName | string, 278 | initializer?: ts.Expression, 279 | type?: ts.TypeNode, 280 | tokenType?: ts.QuestionToken | ts.ExclamationToken, 281 | ) { 282 | return factory.createPropertyDeclaration(undefined, name, tokenType, type, initializer); 283 | } 284 | 285 | export function importDeclaration( 286 | path: string | ts.StringLiteral, 287 | imports?: (ts.Identifier | [string | ts.Identifier, ts.Identifier])[], 288 | defaultImport?: ts.Identifier, 289 | typeOnly = false, 290 | ) { 291 | return factory.createImportDeclaration( 292 | undefined, 293 | factory.createImportClause( 294 | typeOnly, 295 | defaultImport, 296 | imports 297 | ? factory.createNamedImports( 298 | imports.map((x) => { 299 | if (Array.isArray(x)) { 300 | return factory.createImportSpecifier( 301 | false, 302 | typeof x[0] === "string" ? f.identifier(x[0]) : x[0], 303 | x[1], 304 | ); 305 | } else { 306 | return factory.createImportSpecifier(false, undefined, x); 307 | } 308 | }), 309 | ) 310 | : undefined, 311 | ), 312 | toExpression(path), 313 | ); 314 | } 315 | 316 | export function functionDeclaration( 317 | name: string | ts.Identifier, 318 | body?: ts.Block, 319 | parameters: ts.ParameterDeclaration[] = [], 320 | type?: ts.TypeNode, 321 | typeParams?: ts.TypeParameterDeclaration[], 322 | ) { 323 | return factory.createFunctionDeclaration(undefined, undefined, name, typeParams, parameters, type, body); 324 | } 325 | 326 | export function typeAliasDeclaration( 327 | name: string | ts.Identifier, 328 | type: ts.TypeNode, 329 | typeParameters?: ts.TypeParameterDeclaration[], 330 | ) { 331 | return factory.createTypeAliasDeclaration(undefined, name, typeParameters, type); 332 | } 333 | 334 | export function computedPropertyName(expression: ts.Expression) { 335 | return factory.createComputedPropertyName(expression); 336 | } 337 | 338 | export function staticBlockDeclaration(statements: ts.Statement[]) { 339 | return factory.createClassStaticBlockDeclaration(block(statements)); 340 | } 341 | 342 | /// Type Nodes 343 | 344 | export function functionType( 345 | parameters: ts.ParameterDeclaration[], 346 | returnType: ts.TypeNode, 347 | typeParameters?: ts.TypeParameterDeclaration[], 348 | ) { 349 | return factory.createFunctionTypeNode(typeParameters ?? [], parameters, returnType); 350 | } 351 | 352 | export function unionType(types: ts.TypeNode[]) { 353 | return factory.createUnionTypeNode(types); 354 | } 355 | 356 | export function intersectionType(types: ts.TypeNode[]) { 357 | return factory.createIntersectionTypeNode(types); 358 | } 359 | 360 | export function importType( 361 | argument: ts.TypeNode | string, 362 | qualifier?: ts.EntityName, 363 | isTypeOf?: boolean, 364 | typeArguments?: ts.TypeNode[], 365 | ) { 366 | return factory.createImportTypeNode( 367 | typeof argument === "string" ? literalType(string(argument)) : argument, 368 | undefined, 369 | qualifier, 370 | typeArguments, 371 | isTypeOf, 372 | ); 373 | } 374 | 375 | export function referenceType(typeName: string | ts.EntityName, typeArguments?: ts.TypeNode[]) { 376 | return factory.createTypeReferenceNode(typeName, typeArguments); 377 | } 378 | 379 | export function keywordType(kind: ts.KeywordTypeSyntaxKind) { 380 | return factory.createKeywordTypeNode(kind); 381 | } 382 | 383 | export function qualifiedNameType(left: ts.EntityName, right: string | ts.Identifier) { 384 | return factory.createQualifiedName(left, right); 385 | } 386 | 387 | export function typeLiteralType(members: ts.TypeElement[]) { 388 | return factory.createTypeLiteralNode(members); 389 | } 390 | 391 | export function indexSignatureType(indexType: ts.TypeNode, valueType: ts.TypeNode) { 392 | return factory.createIndexSignature(undefined, [parameterDeclaration("key", indexType)], valueType); 393 | } 394 | 395 | export function callSignatureType( 396 | parameters: ts.ParameterDeclaration[], 397 | returnType: ts.TypeNode, 398 | typeParameters?: ts.TypeParameterDeclaration[], 399 | ) { 400 | return factory.createCallSignature(typeParameters, parameters, returnType); 401 | } 402 | 403 | export function tupleType(elements: Array) { 404 | return factory.createTupleTypeNode(elements); 405 | } 406 | 407 | export function literalType(expr: ts.LiteralTypeNode["literal"]) { 408 | return factory.createLiteralTypeNode(expr); 409 | } 410 | 411 | export function propertySignatureType(name: string | ts.PropertyName, type: ts.TypeNode, isOptional?: boolean) { 412 | return factory.createPropertySignature( 413 | undefined, 414 | name, 415 | isOptional ? token(ts.SyntaxKind.QuestionToken) : undefined, 416 | type, 417 | ); 418 | } 419 | 420 | export function indexedAccessType(left: ts.TypeNode, right: ts.TypeNode) { 421 | return factory.createIndexedAccessTypeNode(left, right); 422 | } 423 | 424 | export function queryType(expression: ts.EntityName) { 425 | return factory.createTypeQueryNode(expression); 426 | } 427 | 428 | export function selfType() { 429 | return factory.createThisTypeNode(); 430 | } 431 | 432 | // Other 433 | 434 | export function token(kind: T) { 435 | return factory.createToken(kind); 436 | } 437 | 438 | export function modifier(kind: T) { 439 | return factory.createModifier(kind); 440 | } 441 | 442 | export namespace is { 443 | /// Expressions 444 | 445 | export function statement(node?: ts.Node): node is ts.ExpressionStatement { 446 | return node !== undefined && ts.isExpressionStatement(node); 447 | } 448 | 449 | export function string(node?: ts.Node): node is ts.StringLiteral { 450 | return node !== undefined && ts.isStringLiteral(node); 451 | } 452 | 453 | export function bool(node?: ts.Node): node is ts.BooleanLiteral { 454 | return node !== undefined && (node === f.bool(true) || node === f.bool(false)); 455 | } 456 | 457 | export function array(node?: ts.Node): node is ts.ArrayLiteralExpression { 458 | return node !== undefined && ts.isArrayLiteralExpression(node); 459 | } 460 | 461 | export function number(node?: ts.Node): node is ts.NumericLiteral { 462 | return node !== undefined && ts.isNumericLiteral(node); 463 | } 464 | 465 | export function identifier(node?: ts.Node): node is ts.Identifier { 466 | return node !== undefined && ts.isIdentifier(node); 467 | } 468 | 469 | export function nil(node?: ts.Node): node is ts.Identifier & { text: "undefined " } { 470 | return node !== undefined && identifier(node) && node.text === "undefined"; 471 | } 472 | 473 | export function call(node?: ts.Node): node is ts.CallExpression { 474 | return node !== undefined && ts.isCallExpression(node); 475 | } 476 | 477 | export function object(node?: ts.Node): node is ts.ObjectLiteralExpression { 478 | return node !== undefined && ts.isObjectLiteralExpression(node); 479 | } 480 | 481 | export function functionExpression(node?: ts.Node): node is ts.ArrowFunction | ts.FunctionExpression { 482 | return node !== undefined && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)); 483 | } 484 | 485 | export function omitted(node?: ts.Node): node is ts.OmittedExpression { 486 | return node !== undefined && ts.isOmittedExpression(node); 487 | } 488 | 489 | export function accessExpression(node?: ts.Node): node is ts.AccessExpression { 490 | return node !== undefined && ts.isAccessExpression(node); 491 | } 492 | 493 | export function propertyAccessExpression(node?: ts.Node): node is ts.PropertyAccessExpression { 494 | return node !== undefined && ts.isPropertyAccessExpression(node); 495 | } 496 | 497 | export function elementAccessExpression(node?: ts.Node): node is ts.ElementAccessExpression { 498 | return node !== undefined && ts.isElementAccessExpression(node); 499 | } 500 | 501 | export function postfixUnary(node?: ts.Node): node is ts.PostfixUnaryExpression { 502 | return node !== undefined && ts.isPostfixUnaryExpression(node); 503 | } 504 | 505 | export function superExpression(node?: ts.Node): node is ts.SuperExpression { 506 | return node !== undefined && ts.isSuperKeyword(node); 507 | } 508 | 509 | export function asExpression(node?: ts.Node): node is ts.AsExpression { 510 | return node !== undefined && ts.isAsExpression(node); 511 | } 512 | 513 | /// Statements 514 | /// Declarations 515 | 516 | export function enumDeclaration(node?: ts.Node): node is ts.EnumDeclaration { 517 | return node !== undefined && ts.isEnumDeclaration(node); 518 | } 519 | 520 | export function constructor(node?: ts.Node): node is ts.ConstructorDeclaration { 521 | return node !== undefined && ts.isConstructorDeclaration(node); 522 | } 523 | 524 | export function propertyDeclaration(node?: ts.Node): node is ts.PropertyDeclaration { 525 | return node !== undefined && ts.isPropertyDeclaration(node); 526 | } 527 | 528 | export function propertyAssignmentDeclaration(node?: ts.Node): node is ts.PropertyAssignment { 529 | return node !== undefined && ts.isPropertyAssignment(node); 530 | } 531 | 532 | export function importDeclaration(node?: ts.Node): node is ts.ImportDeclaration { 533 | return node !== undefined && ts.isImportDeclaration(node); 534 | } 535 | 536 | export function classDeclaration(node?: ts.Node): node is ts.ClassDeclaration { 537 | return node !== undefined && ts.isClassDeclaration(node); 538 | } 539 | 540 | export function methodDeclaration(node?: ts.Node): node is ts.MethodDeclaration { 541 | return node !== undefined && ts.isMethodDeclaration(node); 542 | } 543 | 544 | export function namespaceDeclaration(node?: ts.Node): node is ts.NamespaceDeclaration { 545 | return ( 546 | (node !== undefined && 547 | ts.isModuleDeclaration(node) && 548 | identifier(node.name) && 549 | node.body && 550 | ts.isNamespaceBody(node.body)) || 551 | false 552 | ); 553 | } 554 | 555 | export function moduleBlockDeclaration(node?: ts.Node): node is ts.ModuleBlock { 556 | return node !== undefined && ts.isModuleBlock(node); 557 | } 558 | 559 | export function importClauseDeclaration(node?: ts.Node): node is ts.ImportClause { 560 | return node !== undefined && ts.isImportClause(node); 561 | } 562 | 563 | export function namedDeclaration(node?: ts.Node): node is ts.NamedDeclaration & { name: ts.DeclarationName } { 564 | return node !== undefined && ts.isNamedDeclaration(node); 565 | } 566 | 567 | export function interfaceDeclaration(node?: ts.Node): node is ts.InterfaceDeclaration { 568 | return node !== undefined && ts.isInterfaceDeclaration(node); 569 | } 570 | 571 | export function typeAliasDeclaration(node?: ts.Node): node is ts.TypeAliasDeclaration { 572 | return node !== undefined && ts.isTypeAliasDeclaration(node); 573 | } 574 | 575 | /// Type Nodes 576 | 577 | export function referenceType(node?: ts.Node): node is ts.TypeReferenceNode { 578 | return node !== undefined && ts.isTypeReferenceNode(node); 579 | } 580 | 581 | export function queryType(node?: ts.Node): node is ts.TypeQueryNode { 582 | return node !== undefined && ts.isTypeQueryNode(node); 583 | } 584 | 585 | export function importType(node?: ts.Node): node is ts.ImportTypeNode { 586 | return node !== undefined && ts.isImportTypeNode(node); 587 | } 588 | 589 | /// OTHERS 590 | export function namedImports(node?: ts.Node): node is ts.NamedImports { 591 | return node !== undefined && ts.isNamedImports(node); 592 | } 593 | 594 | export function file(node?: ts.Node): node is ts.SourceFile { 595 | return node !== undefined && ts.isSourceFile(node); 596 | } 597 | } 598 | 599 | export namespace update { 600 | /// Expressions 601 | 602 | export function call( 603 | node: ts.CallExpression, 604 | expression = node.expression, 605 | args?: ConvertableExpression[], 606 | typeArguments?: ts.TypeNode[], 607 | ) { 608 | return factory.updateCallExpression( 609 | node, 610 | expression, 611 | typeArguments ?? node.typeArguments, 612 | args?.map((x) => toExpression(x)) ?? node.arguments, 613 | ); 614 | } 615 | 616 | export function object(node: ts.ObjectLiteralExpression, properties?: ts.ObjectLiteralElementLike[]) { 617 | return factory.updateObjectLiteralExpression(node, properties ?? node.properties); 618 | } 619 | 620 | export function propertyAccessExpression( 621 | node: ts.PropertyAccessExpression, 622 | expression: ConvertableExpression, 623 | name: ts.MemberName | string, 624 | ) { 625 | return factory.updatePropertyAccessExpression( 626 | node, 627 | toExpression(expression), 628 | typeof name === "string" ? f.identifier(name) : name, 629 | ); 630 | } 631 | 632 | export function elementAccessExpression( 633 | node: ts.ElementAccessExpression, 634 | expression: ConvertableExpression, 635 | name: ConvertableExpression, 636 | ) { 637 | return factory.updateElementAccessExpression(node, toExpression(expression), toExpression(name)); 638 | } 639 | 640 | /// Statements 641 | /// Declarations 642 | 643 | export function classDeclaration( 644 | node: ts.ClassDeclaration, 645 | name = node.name, 646 | members: NodeArray = node.members, 647 | heritageClauses = node.heritageClauses, 648 | typeParameters = node.typeParameters, 649 | modifiers: ONodeArray = node.modifiers, 650 | ) { 651 | return factory.updateClassDeclaration(node, modifiers, name, typeParameters, heritageClauses, members); 652 | } 653 | 654 | export function constructor( 655 | node: ts.ConstructorDeclaration, 656 | parameters: NodeArray, 657 | body: ts.Block, 658 | ) { 659 | return factory.updateConstructorDeclaration(node, node.modifiers, parameters, body); 660 | } 661 | 662 | export function parameterDeclaration( 663 | node: ts.ParameterDeclaration, 664 | name = node.name, 665 | type = node.type, 666 | initializer = node.initializer, 667 | modifiers: ONodeArray = node.modifiers, 668 | isRest = node.dotDotDotToken !== undefined, 669 | isOptional = node.questionToken !== undefined, 670 | ) { 671 | return factory.updateParameterDeclaration( 672 | node, 673 | modifiers?.length ? modifiers : undefined, 674 | isRest ? token(ts.SyntaxKind.DotDotDotToken) : undefined, 675 | name, 676 | isOptional ? token(ts.SyntaxKind.QuestionToken) : undefined, 677 | type, 678 | initializer, 679 | ); 680 | } 681 | 682 | export function methodDeclaration( 683 | node: ts.MethodDeclaration, 684 | name: string | ts.PropertyName = node.name, 685 | body = node.body, 686 | parameters: ONodeArray = node.parameters, 687 | typeParameters: ONodeArray = node.typeParameters, 688 | modifiers: ONodeArray = node.modifiers, 689 | isOptional = node.questionToken !== undefined, 690 | type = node.type, 691 | ) { 692 | return factory.updateMethodDeclaration( 693 | node, 694 | modifiers?.length === 0 ? undefined : modifiers, 695 | node.asteriskToken, 696 | typeof name === "string" ? identifier(name) : name, 697 | isOptional ? factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, 698 | typeParameters?.length === 0 ? undefined : typeParameters, 699 | parameters, 700 | type, 701 | Array.isArray(body) ? f.block(body) : body, 702 | ); 703 | } 704 | 705 | export function propertyDeclaration( 706 | node: ts.PropertyDeclaration, 707 | initializer: ConvertableExpression | null | undefined = node.initializer, 708 | name = node.name, 709 | modifiers: ONodeArray = node.modifiers, 710 | tokenType: "?" | "!" | undefined = node.questionToken ? "?" : node.exclamationToken ? "!" : undefined, 711 | type = node.type, 712 | ) { 713 | const syntaxToken = 714 | tokenType === "!" 715 | ? token(ts.SyntaxKind.ExclamationToken) 716 | : tokenType === "?" 717 | ? token(ts.SyntaxKind.QuestionToken) 718 | : undefined; 719 | return factory.updatePropertyDeclaration( 720 | node, 721 | modifiers?.length === 0 ? undefined : modifiers, 722 | name, 723 | syntaxToken, 724 | type, 725 | !initializer ? undefined : toExpression(initializer), 726 | ); 727 | } 728 | 729 | export function propertyAssignmentDeclaration( 730 | node: ts.PropertyAssignment, 731 | initializer: ConvertableExpression = node.initializer, 732 | name: ts.PropertyName | string = node.name, 733 | ) { 734 | return factory.updatePropertyAssignment( 735 | node, 736 | typeof name === "string" ? f.identifier(name) : name, 737 | toExpression(initializer), 738 | ); 739 | } 740 | 741 | /// Type Nodes 742 | /// Other 743 | export function sourceFile( 744 | sourceFile: ts.SourceFile, 745 | statements: ts.NodeArray | ts.Statement[] = sourceFile.statements, 746 | isDeclarationFile = sourceFile.isDeclarationFile, 747 | referencedFiles = sourceFile.referencedFiles, 748 | typeReferences = sourceFile.typeReferenceDirectives, 749 | hasNoDefaultLib = sourceFile.hasNoDefaultLib, 750 | libReferences = sourceFile.libReferenceDirectives, 751 | ) { 752 | return factory.updateSourceFile( 753 | sourceFile, 754 | statements, 755 | isDeclarationFile, 756 | referencedFiles, 757 | typeReferences, 758 | hasNoDefaultLib, 759 | libReferences, 760 | ); 761 | } 762 | } 763 | 764 | export function setFactory(newFactory: ts.NodeFactory) { 765 | factory = newFactory; 766 | } 767 | } 768 | --------------------------------------------------------------------------------