diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index f83c6897f..a1e5f414b 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -144,6 +144,8 @@ export class LuaTransformer { case ts.SyntaxKind.Block: return this.transformBlockAsDoStatement(node as ts.Block); // Declaration Statements + case ts.SyntaxKind.ExportDeclaration: + return this.transformExportDeclaration(node as ts.ExportDeclaration); case ts.SyntaxKind.ImportDeclaration: return this.transformImportDeclaration(node as ts.ImportDeclaration); case ts.SyntaxKind.ClassDeclaration: @@ -219,6 +221,90 @@ export class LuaTransformer { return tstl.createDoStatement(statements, block); } + public transformExportDeclaration(statement: ts.ExportDeclaration): StatementVisitResult { + if (statement.moduleSpecifier === undefined) { + const result = []; + for (const exportElement of statement.exportClause.elements) { + result.push( + tstl.createAssignmentStatement( + this.createExportedIdentifier(this.transformIdentifier(exportElement.name)), + this.transformIdentifier(exportElement.propertyName || exportElement.name) + ) + ); + } + return result; + } + + if (statement.exportClause) { + if (statement.exportClause.elements.some(e => + (e.name && e.name.originalKeywordKind === ts.SyntaxKind.DefaultKeyword) + || (e.propertyName && e.propertyName.originalKeywordKind === ts.SyntaxKind.DefaultKeyword)) + ) { + throw TSTLErrors.UnsupportedDefaultExport(statement); + } + + // First transpile as import clause + const importClause = ts.createImportClause( + undefined, + ts.createNamedImports(statement.exportClause.elements + .map(e => ts.createImportSpecifier(e.propertyName, e.name)) + ) + ); + + const importDeclaration = ts.createImportDeclaration( + statement.decorators, + statement.modifiers, + importClause, + statement.moduleSpecifier + ); + + const importResult = this.transformImportDeclaration(importDeclaration); + + const result = Array.isArray(importResult) ? importResult : [importResult]; + + // Now the module is imported, add the imports to the export table + for (const exportVariable of statement.exportClause.elements) { + result.push( + tstl.createAssignmentStatement( + this.createExportedIdentifier(this.transformIdentifier(exportVariable.name)), + this.transformIdentifier(exportVariable.name) + ) + ); + } + + // Wrap this in a DoStatement to prevent polluting the scope. + return tstl.createDoStatement(result, statement); + } else { + const moduleRequire = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral); + const tempModuleIdentifier = tstl.createIdentifier("__TSTL_export"); + + const declaration = tstl.createVariableDeclarationStatement(tempModuleIdentifier, moduleRequire); + + const forKey = tstl.createIdentifier("____exportKey"); + const forValue = tstl.createIdentifier("____exportValue"); + + const body = tstl.createBlock( + [tstl.createAssignmentStatement( + tstl.createTableIndexExpression( + tstl.createIdentifier("exports"), + forKey + ), + forValue + )] + ); + + const pairsIdentifier = tstl.createIdentifier("pairs"); + const forIn = tstl.createForInStatement( + body, + [tstl.cloneIdentifier(forKey), tstl.cloneIdentifier(forValue)], + [tstl.createCallExpression(pairsIdentifier, [tstl.cloneIdentifier(tempModuleIdentifier)])] + ); + + // Wrap this in a DoStatement to prevent polluting the scope. + return tstl.createDoStatement([declaration, forIn], statement); + } + } + public transformImportDeclaration(statement: ts.ImportDeclaration): StatementVisitResult { if (statement.importClause && !statement.importClause.namedBindings) { throw TSTLErrors.DefaultImportsNotSupported(statement); @@ -228,9 +314,8 @@ export class LuaTransformer { const moduleSpecifier = statement.moduleSpecifier as ts.StringLiteral; const importPath = moduleSpecifier.text.replace(new RegExp("\"", "g"), ""); - const resolvedModuleSpecifier = tstl.createStringLiteral(this.getImportPath(importPath)); - const requireCall = tstl.createCallExpression(tstl.createIdentifier("require"), [resolvedModuleSpecifier]); + const requireCall = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral); if (!statement.importClause) { result.push(tstl.createExpressionStatement(requireCall)); @@ -291,6 +376,13 @@ export class LuaTransformer { } } + private createModuleRequire(moduleSpecifier: ts.StringLiteral): tstl.CallExpression { + const importPath = moduleSpecifier.text.replace(new RegExp("\"", "g"), ""); + const resolvedModuleSpecifier = tstl.createStringLiteral(this.getImportPath(importPath)); + + return tstl.createCallExpression(tstl.createIdentifier("require"), [resolvedModuleSpecifier]); + } + public transformClassDeclaration( statement: ts.ClassLikeDeclaration, nameOverride?: tstl.Identifier diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts index 793fdbf54..7f9717b6e 100644 --- a/src/TSTLErrors.ts +++ b/src/TSTLErrors.ts @@ -4,28 +4,23 @@ import {TranspileError} from "./TranspileError"; import {TSHelper as tsHelper} from "./TSHelper"; export class TSTLErrors { - public static CouldNotFindEnumMember = - (enumDeclaration: ts.EnumDeclaration, enumMember: string, node: ts.Node) => new TranspileError( - `Could not find ${enumMember} in ${enumDeclaration.name.text}`, node - ); + public static CouldNotFindEnumMember = (enumDeclaration: ts.EnumDeclaration, enumMember: string, node: ts.Node) => + new TranspileError(`Could not find ${enumMember} in ${enumDeclaration.name.text}`, node); public static DefaultImportsNotSupported = (node: ts.Node) => new TranspileError(`Default Imports are not supported, please use named imports instead!`, node); - public static ForbiddenEllipsisDestruction = - (node: ts.Node) => new TranspileError(`Ellipsis destruction is not allowed.`, node); + public static ForbiddenEllipsisDestruction = (node: ts.Node) => + new TranspileError(`Ellipsis destruction is not allowed.`, node); - public static ForbiddenForIn = - (node: ts.Node) => new TranspileError(`Iterating over arrays with 'for ... in' is not allowed.`, node); + public static ForbiddenForIn = (node: ts.Node) => + new TranspileError(`Iterating over arrays with 'for ... in' is not allowed.`, node); public static HeterogeneousEnum = (node: ts.Node) => new TranspileError( `Invalid heterogeneous enum. Enums should either specify no member values, ` + `or specify values (of the same type) for all members.`, node); - public static InvalidEnumMember = - (node: ts.Node) => new TranspileError(`Only numeric or string initializers allowed for enums.`, node); - public static InvalidDecoratorArgumentNumber = (name: string, got: number, expected: number, node: ts.Node) => new TranspileError(`${name} expects ${expected} argument(s) but got ${got}.`, node); @@ -44,6 +39,9 @@ export class TSTLErrors { public static InvalidInstanceOfExtension = (node: ts.Node) => new TranspileError(`Cannot use instanceof on classes with decorator '@extension' or '@metaExtension'.`, node); + public static InvalidJsonFileContent = (node: ts.Node) => + new TranspileError("Invalid JSON file content", node); + public static InvalidPropertyCall = (node: ts.Node) => new TranspileError(`Tried to transpile a non-property call as property call.`, node); @@ -56,19 +54,23 @@ export class TSTLErrors { public static KeywordIdentifier = (node: ts.Identifier) => new TranspileError(`Cannot use Lua keyword ${node.escapedText} as identifier.`, node); - public static MissingClassName = - (node: ts.Node) => new TranspileError(`Class declarations must have a name.`, node); + public static MissingClassName = (node: ts.Node) => + new TranspileError(`Class declarations must have a name.`, node); public static MissingMetaExtension = (node: ts.Node) => - new TranspileError(`!MetaExtension requires the extension of the metatable class.`, node); + new TranspileError(`@metaExtension requires the extension of the metatable class.`, node); + + public static UnsupportedDefaultExport = (node: ts.Node) => + new TranspileError(`Default exports are not supported.`, node); - public static UnsupportedImportType = (node: ts.Node) => new TranspileError(`Unsupported import type.`, node); + public static UnsupportedImportType = (node: ts.Node) => + new TranspileError(`Unsupported import type.`, node); - public static UnsupportedKind = - (description: string, kind: ts.SyntaxKind, node: ts.Node) => { + public static UnsupportedKind = (description: string, kind: ts.SyntaxKind, node: ts.Node) => + { const kindName = tsHelper.enumName(kind, ts.SyntaxKind); return new TranspileError(`Unsupported ${description} kind: ${kindName}`, node); - } + }; public static UnsupportedProperty = (parentName: string, property: string, node: ts.Node) => new TranspileError(`Unsupported property on ${parentName}: ${property}`, node); @@ -76,12 +78,6 @@ export class TSTLErrors { public static UnsupportedForTarget = (functionality: string, version: string, node: ts.Node) => new TranspileError(`${functionality} is/are not supported for target Lua ${version}.`, node); - public static UnsupportedObjectLiteralElement = (elementKind: ts.SyntaxKind, node: ts.Node) => - new TranspileError(`Unsupported object literal element: ${elementKind}.`, node); - - public static UnsupportedUnionAccessor = (node: ts.Node) => - new TranspileError(`Unsupported mixed union of accessor and non-accessor types for the same property.`, node); - public static UnsupportedFunctionConversion = (node: ts.Node, name?: string) => { if (name) { return new TranspileError( @@ -141,6 +137,4 @@ export class TSTLErrors { node ); } - - public static InvalidJsonFileContent = (node: ts.Node) => new TranspileError("Invalid JSON file content", node); } diff --git a/test/translation/lua/exportStatement.lua b/test/translation/lua/exportStatement.lua new file mode 100644 index 000000000..2369b004b --- /dev/null +++ b/test/translation/lua/exportStatement.lua @@ -0,0 +1,23 @@ +local exports = exports or {}; +local xyz = 4; +exports.xyz = xyz; +exports.uwv = xyz; +do + local __TSTL_export = require("xyz"); + for ____exportKey, ____exportValue in pairs(__TSTL_export) do + exports[____exportKey] = ____exportValue; + end +end +do + local __TSTL_xyz = require("xyz"); + local abc = __TSTL_xyz.abc; + local def = __TSTL_xyz.def; + exports.abc = abc; + exports.def = def; +end +do + local __TSTL_xyz = require("xyz"); + local def = __TSTL_xyz.abc; + exports.def = def; +end +return exports; \ No newline at end of file diff --git a/test/translation/ts/exportStatement.ts b/test/translation/ts/exportStatement.ts new file mode 100644 index 000000000..524be0bd9 --- /dev/null +++ b/test/translation/ts/exportStatement.ts @@ -0,0 +1,6 @@ +const xyz = 4; +export {xyz}; +export {xyz as uwv}; +export * from "xyz"; +export {abc, def} from "xyz"; +export {abc as def} from "xyz"; diff --git a/test/unit/decoratorMetaExtension.spec.ts b/test/unit/decoratorMetaExtension.spec.ts index f0a5eabdc..6fdedf191 100644 --- a/test/unit/decoratorMetaExtension.spec.ts +++ b/test/unit/decoratorMetaExtension.spec.ts @@ -2,6 +2,7 @@ import { Expect, Test } from "alsatian"; import * as util from "../src/util"; import { TranspileError } from "../../src/TranspileError"; +import { TSTLErrors } from "../../src/TSTLErrors"; export class DecoratorMetaExtension { @@ -29,6 +30,7 @@ export class DecoratorMetaExtension { @Test("IncorrectUsage") public incorrectUsage(): void { + const expectedMessage = TSTLErrors.MissingMetaExtension(undefined).message; Expect(() => { util.transpileString( ` @@ -40,8 +42,7 @@ export class DecoratorMetaExtension { } ` ); - }).toThrowError(TranspileError, - "!MetaExtension requires the extension of the metatable class."); + }).toThrowError(TranspileError, expectedMessage); } @Test("DontAllowInstantiation") diff --git a/test/unit/importexport.spec.ts b/test/unit/importexport.spec.ts new file mode 100644 index 000000000..7aca64caf --- /dev/null +++ b/test/unit/importexport.spec.ts @@ -0,0 +1,17 @@ +import { Expect, Test, TestCase } from "alsatian"; + +import * as util from "../src/util"; +import { TSTLErrors } from "../../src/TSTLErrors"; +import { TranspileError } from "../../src/TranspileError"; + +export class ImportExportTests +{ + @TestCase("export { default } from '...'") + @TestCase("export { x as default } from '...';") + @TestCase("export { default as x } from '...';") + @Test("Export default keyword disallowed") + public exportDefaultKeywordError(exportStatement: string): void { + const expectedTest = TSTLErrors.UnsupportedDefaultExport(undefined).message; + Expect(() => util.transpileString(exportStatement)).toThrowError(TranspileError, expectedTest); + } +}