roblox-ts
Version:
<div align="center"><img width=25% src="https://i.imgur.com/yCjHmng.png"></div> <h1 align="center"><a href="https://roblox-ts.github.io/">roblox-ts</a></h1> <div align="center">A TypeScript-to-Lua Compiler for Roblox</div> <br> <div align="center"> <a hr
439 lines • 19.3 kB
JavaScript
;
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const ts = __importStar(require("ts-morph"));
const _1 = require(".");
const CompilerError_1 = require("../errors/CompilerError");
const typeUtilities_1 = require("../typeUtilities");
const utility_1 = require("../utility");
const ROACT_ELEMENT_TYPE = "Roact.Element";
exports.ROACT_COMPONENT_TYPE = "Roact.Component";
exports.ROACT_PURE_COMPONENT_TYPE = "Roact.PureComponent";
exports.ROACT_DERIVED_CLASSES_ERROR = utility_1.suggest("Composition is preferred over inheritance with Roact components.\n" +
"...\tsee https://reactjs.org/docs/composition-vs-inheritance.html for more info about composition over inheritance.");
const CONSTRUCTOR_METHOD_NAME = "init";
const INHERITANCE_METHOD_NAME = "extend";
const RESERVED_METHOD_NAMES = [
CONSTRUCTOR_METHOD_NAME,
"setState",
"_update",
"getElementTraceback",
"_forceUpdate",
"_mount",
"_unmount",
INHERITANCE_METHOD_NAME,
];
/**
* A list of lowercase names that map to Roblox elements for JSX
*/
const INTRINSIC_MAPPINGS = {
billboardgui: "BillboardGui",
frame: "Frame",
imagebutton: "ImageButton",
imagelabel: "ImageLabel",
screengui: "ScreenGui",
scrollingframe: "ScrollingFrame",
surfacegui: "SurfaceGui",
textbox: "TextBox",
textbutton: "TextButton",
textlabel: "TextLabel",
uigridlayout: "UIGridLayout",
uilistlayout: "UIListLayout",
uipagelayout: "UIPageLayout",
uitablelayout: "UITableLayout",
uipadding: "UIPadding",
uiscale: "UIScale",
uiaspectratioconstraint: "UIAspectRatioConstraint",
uisizeconstraint: "UISizeConstraint",
uitextsizeconstraint: "UITextSizeConstraint",
viewportframe: "ViewportFrame",
};
function isRoactElementType(type) {
const allowed = [ROACT_ELEMENT_TYPE, `${ROACT_ELEMENT_TYPE}[]`];
const types = type.getUnionTypes();
if (types.length > 0) {
for (const unionType of types) {
const unionTypeName = unionType.getText();
if (allowed.indexOf(unionTypeName) === -1 && unionTypeName !== "undefined") {
return false;
}
}
}
else {
return allowed.indexOf(type.getText()) !== -1;
}
return true;
}
function getFullTypeList(type) {
const symbol = type.getSymbol();
const typeArray = new Array();
if (symbol) {
symbol.getDeclarations().forEach(declaration => {
const declarationType = typeUtilities_1.getType(declaration);
typeArray.push(declarationType.getText());
for (const baseType of declarationType.getBaseTypes()) {
typeArray.push(...getFullTypeList(baseType));
}
});
}
return typeArray;
}
function inheritsFromRoact(type) {
let isRoactClass = false;
for (const name of getFullTypeList(type)) {
if (name.startsWith(exports.ROACT_COMPONENT_TYPE) || name.startsWith(exports.ROACT_PURE_COMPONENT_TYPE)) {
isRoactClass = true;
break;
}
}
return isRoactClass;
}
exports.inheritsFromRoact = inheritsFromRoact;
function getRoactType(node) {
const extendsExp = node.getExtends();
if (extendsExp) {
const extendsText = utility_1.skipNodesDownwards(extendsExp.getExpression()).getText();
if (extendsText.startsWith(exports.ROACT_COMPONENT_TYPE)) {
return exports.ROACT_COMPONENT_TYPE;
}
else if (extendsText.startsWith(exports.ROACT_PURE_COMPONENT_TYPE)) {
return exports.ROACT_PURE_COMPONENT_TYPE;
}
}
return undefined;
}
exports.getRoactType = getRoactType;
function inheritsFromRoactComponent(node) {
const extendsExp = node.getExtends();
if (extendsExp) {
const symbol = extendsExp.getType().getSymbol();
if (symbol) {
const valueDec = symbol.getValueDeclaration();
if (valueDec && (ts.TypeGuards.isClassDeclaration(valueDec) || ts.TypeGuards.isClassExpression(valueDec))) {
if (getRoactType(node) !== undefined) {
return true;
}
else {
return inheritsFromRoactComponent(valueDec);
}
}
}
}
return false;
}
exports.inheritsFromRoactComponent = inheritsFromRoactComponent;
function checkRoactReserved(className, name, node) {
if (RESERVED_METHOD_NAMES.indexOf(name) !== -1) {
let userError = `Member ${utility_1.bold(name)} in component ${utility_1.bold(className)} is a reserved Roact method name.`;
if (name === CONSTRUCTOR_METHOD_NAME) {
userError += `\n ... Use the constructor ${utility_1.bold("constructor(props)")} instead of the method ${utility_1.bold("init(props)")}.`;
}
else if (name === INHERITANCE_METHOD_NAME) {
userError += "\n" + exports.ROACT_DERIVED_CLASSES_ERROR;
}
throw new CompilerError_1.CompilerError(userError, node, CompilerError_1.CompilerErrorType.RoactNoReservedMethods);
}
}
exports.checkRoactReserved = checkRoactReserved;
function compileSymbolPropertyCallback(state, node) {
const symbol = node.getSymbolOrThrow();
const name = symbol.getName();
const value = symbol.getValueDeclarationOrThrow();
if (ts.TypeGuards.isFunctionLikeDeclaration(value)) {
if (ts.TypeGuards.isMethodDeclaration(value)) {
throw new CompilerError_1.CompilerError("Do not use Method signatures directly as callbacks for Roact Event, Changed or Ref.\n" +
utility_1.suggest(`Change the declaration of \`${name}(...) {...}\` to \`${name} = () => { ... }\`, ` +
` or use an arrow function: \`() => { this.${name}() }\``), node, CompilerError_1.CompilerErrorType.RoactInvalidCallExpression);
}
}
return _1.compileExpression(state, node);
}
function generateRoactSymbolProperty(state, roactSymbol, node, attributeCollection, hasExtraAttributes = false) {
const expr = node.getChildrenOfKind(ts.SyntaxKind.JsxExpression);
for (const expression of expr) {
const innerExpression = expression.getExpressionOrThrow();
if (ts.TypeGuards.isObjectLiteralExpression(innerExpression)) {
const properties = innerExpression.getProperties();
for (const property of properties) {
if (ts.TypeGuards.isPropertyAssignment(property) ||
ts.TypeGuards.isShorthandPropertyAssignment(property)) {
const propName = property.getName();
const rhs = property.getInitializerOrThrow();
let value;
if (ts.TypeGuards.isPropertyAccessExpression(rhs)) {
value = compileSymbolPropertyCallback(state, rhs);
}
else {
if (hasExtraAttributes) {
state.pushIndent(); // fix indentation with extra props
}
value = _1.compileExpression(state, rhs);
}
if (hasExtraAttributes) {
state.popIndent(); // fix indentation with extra props
}
attributeCollection.push(`[Roact.${roactSymbol}.${propName}] = ${value}`);
}
}
}
else if (roactSymbol === "Ref") {
let value;
if (ts.TypeGuards.isPropertyAccessExpression(innerExpression)) {
const getAccessExpression = innerExpression.getExpression();
if (ts.TypeGuards.isThisExpression(getAccessExpression)) {
value = compileSymbolPropertyCallback(state, innerExpression);
}
else {
if (hasExtraAttributes) {
state.pushIndent(); // fix indentation with extra props
}
value = _1.compileExpression(state, getAccessExpression);
}
}
else {
if (hasExtraAttributes) {
state.pushIndent(); // fix indentation with extra props
}
value = _1.compileExpression(state, innerExpression);
}
if (hasExtraAttributes) {
state.popIndent();
}
attributeCollection.push(`[Roact.Ref] = ${value}`);
}
else {
throw new CompilerError_1.CompilerError(`Roact symbol ${roactSymbol} does not support (${innerExpression.getKindName()})`, node, CompilerError_1.CompilerErrorType.RoactInvalidSymbol);
}
}
}
exports.generateRoactSymbolProperty = generateRoactSymbolProperty;
function generateRoactElement(state,
// name: string,
nameNode, attributes, children) {
let str = `Roact.createElement(`;
const name = nameNode.getText();
const attributeCollection = new Array();
const extraAttributeCollections = new Array();
const extraChildrenCollection = new Array();
const childCollection = new Array();
let key;
state.roactIndent++;
if (name.match(/^[a-z]+$/)) {
// if lowercase
// Check if defined as a intrinsic mapping
const rbxName = INTRINSIC_MAPPINGS[name];
if (rbxName) {
str += `"${rbxName}"`;
}
else {
throw new CompilerError_1.CompilerError(`"${utility_1.bold(name)}" is not a valid primitive type.\n` + utility_1.suggest("Your roblox-ts may be out of date."), nameNode, CompilerError_1.CompilerErrorType.RoactInvalidPrimitive);
}
}
else {
str += name;
}
if (attributes.length > 0) {
state.pushIndent();
const extraAttributes = attributes.filter(attr => ts.TypeGuards.isJsxSpreadAttribute(attr));
for (const attributeLike of attributes) {
if (ts.TypeGuards.isJsxSpreadAttribute(attributeLike)) {
const expression = attributeLike.getExpression();
extraAttributeCollections.push(_1.compileExpression(state, expression));
}
else {
const attribute = attributeLike;
const attributeName = attribute.getName();
const value = _1.compileExpression(state, attribute.getInitializerOrThrow());
if (attributeName === "Key") {
// handle setting a key for this element
key = value;
}
else if (attributeName === "Event") {
// handle [Roact.Event]
generateRoactSymbolProperty(state, "Event", attributeLike, attributeCollection, extraAttributes.length > 0);
}
else if (attributeName === "Change") {
// handle [Roact.Change]
generateRoactSymbolProperty(state, "Change", attributeLike, attributeCollection, extraAttributes.length > 0);
}
else if (attributeName === "Ref") {
// handle [Roact.Ref]
generateRoactSymbolProperty(state, "Ref", attributeLike, attributeCollection, extraAttributes.length > 0);
}
else {
attributeCollection.push(`${attributeName} = ${value}`);
}
}
}
state.popIndent();
// use Object.assign if we have extra attributes
if (extraAttributeCollections.length > 0) {
str += ", \n";
state.pushIndent();
state.usesTSLibrary = true;
str += state.indent + "TS.Roact_combine(";
// If it has other attributes
if (attributeCollection.length > 0) {
str += "{\n";
state.pushIndent();
str += state.indent + attributeCollection.join(",\n" + state.indent);
state.popIndent();
str += ` \n${state.indent}},\n${state.indent}`;
}
else {
str += `{}, `;
}
str += extraAttributeCollections.join(",\n" + state.indent);
str += ")\n";
state.popIndent();
}
else {
str += ", {\n";
state.pushIndent();
str += state.indent + attributeCollection.join(",\n" + state.indent);
state.popIndent();
str += ` \n${state.indent}}`;
}
}
else {
str += ", {}";
}
if (children.length > 0) {
state.pushIndent();
for (const child of children) {
if (ts.TypeGuards.isJsxElement(child) || ts.TypeGuards.isJsxSelfClosingElement(child)) {
const value = _1.compileExpression(state, child);
childCollection.push(`${state.indent}${value}`);
}
else if (ts.TypeGuards.isJsxText(child)) {
// If the inner text isn't just indentation/spaces
if (child.getText().match(/[^\s]/)) {
throw new CompilerError_1.CompilerError("Roact does not support text!", child, CompilerError_1.CompilerErrorType.RoactJsxTextNotSupported);
}
}
else if (ts.TypeGuards.isJsxExpression(child)) {
const expression = child.getExpressionOrThrow();
if (ts.TypeGuards.isCallExpression(expression)) {
// Must return Roact.Element :(
const returnType = expression.getReturnType();
if (isRoactElementType(returnType)) {
if (typeUtilities_1.isArrayType(returnType)) {
// Roact.Element[]
extraChildrenCollection.push(state.indent + _1.compileExpression(state, expression));
}
else {
// Roact.Element
extraChildrenCollection.push(state.indent + `{ ${_1.compileExpression(state, expression)} }`);
}
}
else {
throw new CompilerError_1.CompilerError(`Function call in an expression must return Roact.Element or Roact.Element[]`, expression, CompilerError_1.CompilerErrorType.RoactInvalidCallExpression);
}
}
else if (ts.TypeGuards.isIdentifier(expression)) {
const definitionNodes = expression.getDefinitionNodes();
for (const definitionNode of definitionNodes) {
const type = typeUtilities_1.getType(definitionNode);
if (isRoactElementType(type)) {
extraChildrenCollection.push(state.indent + _1.compileExpression(state, expression));
}
else {
throw new CompilerError_1.CompilerError(`Roact does not support identifiers that have the return type ` + type.getText(), expression, CompilerError_1.CompilerErrorType.RoactInvalidIdentifierExpression);
}
}
}
else if (ts.TypeGuards.isPropertyAccessExpression(expression) ||
ts.TypeGuards.isElementAccessExpression(expression)) {
const propertyType = typeUtilities_1.getType(expression);
if (isRoactElementType(propertyType)) {
extraChildrenCollection.push(_1.compileExpression(state, expression));
}
else {
throw new CompilerError_1.CompilerError(`Roact does not support the property type ` + propertyType.getText(), expression, CompilerError_1.CompilerErrorType.RoactInvalidPropertyExpression);
}
}
else {
throw new CompilerError_1.CompilerError(`Roact does not support this type of expression ` +
`{${expression.getText()}} (${expression.getKindName()})`, expression, CompilerError_1.CompilerErrorType.RoactInvalidExpression);
}
}
}
state.popIndent();
if (extraChildrenCollection.length > 0) {
state.usesTSLibrary = true;
str += `, TS.Roact_combine(`;
if (childCollection.length > 0) {
str += "{\n" + state.indent;
str += childCollection.join(",\n") + `\n${state.indent}}, `;
}
str += "\n";
str += extraChildrenCollection.join(",\n") + `\n`;
str += state.indent + ")";
str += ")";
}
else {
// state.pushIndent();
str += state.indent + ", {\n";
str += childCollection.join(",\n") + `\n${state.indent}})`;
}
}
else {
if (extraAttributeCollections.length > 0) {
str += state.indent + ")";
}
else {
str += ")";
}
}
state.roactIndent--;
if (key && state.roactIndent > 0) {
return `[${key}] = ${str}`;
}
else {
return str;
}
}
exports.generateRoactElement = generateRoactElement;
function compileJsxElement(state, node) {
if (!state.hasRoactImport) {
throw new CompilerError_1.CompilerError("Cannot use JSX without importing Roact first!\n" +
utility_1.suggest('To fix this, put `import Roact from "@rbxts/roact"` at the top of this file.'), node, CompilerError_1.CompilerErrorType.RoactJsxWithoutImport);
}
const open = node.getOpeningElement();
const tagNameNode = open.getTagNameNode();
const children = node.getJsxChildren();
const isArrayExpressionParent = node.getParentIfKind(ts.ts.SyntaxKind.ArrayLiteralExpression);
if (isArrayExpressionParent) {
state.roactIndent++;
}
const element = generateRoactElement(state, tagNameNode, open.getAttributes(), children);
if (isArrayExpressionParent) {
state.roactIndent--;
}
return element;
}
exports.compileJsxElement = compileJsxElement;
function compileJsxSelfClosingElement(state, node) {
if (!state.hasRoactImport) {
throw new CompilerError_1.CompilerError("Cannot use JSX without importing Roact first!\n" +
utility_1.suggest('To fix this, put `import Roact from "@rbxts/roact"` at the top of this file.'), node, CompilerError_1.CompilerErrorType.RoactJsxWithoutImport);
}
const tagNameNode = node.getTagNameNode();
const isArrayExpressionParent = node.getParentIfKind(ts.ts.SyntaxKind.ArrayLiteralExpression);
if (isArrayExpressionParent) {
state.roactIndent++;
}
const element = generateRoactElement(state, tagNameNode, node.getAttributes(), []);
if (isArrayExpressionParent) {
state.roactIndent--;
}
return element;
}
exports.compileJsxSelfClosingElement = compileJsxSelfClosingElement;
//# sourceMappingURL=roact.js.map