Skip to content

Commit a1544f9

Browse files
authored
Added support for ExportDeclaration (#417)
* add support for export declaration * Added support for other variants of the the export declaration * Added error for default exports and cleaned up TSTLErrors a little * Fixed broken test
1 parent 9a23ce3 commit a1544f9

File tree

6 files changed

+163
-30
lines changed

6 files changed

+163
-30
lines changed

src/LuaTransformer.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ export class LuaTransformer {
144144
case ts.SyntaxKind.Block:
145145
return this.transformBlockAsDoStatement(node as ts.Block);
146146
// Declaration Statements
147+
case ts.SyntaxKind.ExportDeclaration:
148+
return this.transformExportDeclaration(node as ts.ExportDeclaration);
147149
case ts.SyntaxKind.ImportDeclaration:
148150
return this.transformImportDeclaration(node as ts.ImportDeclaration);
149151
case ts.SyntaxKind.ClassDeclaration:
@@ -219,6 +221,90 @@ export class LuaTransformer {
219221
return tstl.createDoStatement(statements, block);
220222
}
221223

224+
public transformExportDeclaration(statement: ts.ExportDeclaration): StatementVisitResult {
225+
if (statement.moduleSpecifier === undefined) {
226+
const result = [];
227+
for (const exportElement of statement.exportClause.elements) {
228+
result.push(
229+
tstl.createAssignmentStatement(
230+
this.createExportedIdentifier(this.transformIdentifier(exportElement.name)),
231+
this.transformIdentifier(exportElement.propertyName || exportElement.name)
232+
)
233+
);
234+
}
235+
return result;
236+
}
237+
238+
if (statement.exportClause) {
239+
if (statement.exportClause.elements.some(e =>
240+
(e.name && e.name.originalKeywordKind === ts.SyntaxKind.DefaultKeyword)
241+
|| (e.propertyName && e.propertyName.originalKeywordKind === ts.SyntaxKind.DefaultKeyword))
242+
) {
243+
throw TSTLErrors.UnsupportedDefaultExport(statement);
244+
}
245+
246+
// First transpile as import clause
247+
const importClause = ts.createImportClause(
248+
undefined,
249+
ts.createNamedImports(statement.exportClause.elements
250+
.map(e => ts.createImportSpecifier(e.propertyName, e.name))
251+
)
252+
);
253+
254+
const importDeclaration = ts.createImportDeclaration(
255+
statement.decorators,
256+
statement.modifiers,
257+
importClause,
258+
statement.moduleSpecifier
259+
);
260+
261+
const importResult = this.transformImportDeclaration(importDeclaration);
262+
263+
const result = Array.isArray(importResult) ? importResult : [importResult];
264+
265+
// Now the module is imported, add the imports to the export table
266+
for (const exportVariable of statement.exportClause.elements) {
267+
result.push(
268+
tstl.createAssignmentStatement(
269+
this.createExportedIdentifier(this.transformIdentifier(exportVariable.name)),
270+
this.transformIdentifier(exportVariable.name)
271+
)
272+
);
273+
}
274+
275+
// Wrap this in a DoStatement to prevent polluting the scope.
276+
return tstl.createDoStatement(result, statement);
277+
} else {
278+
const moduleRequire = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral);
279+
const tempModuleIdentifier = tstl.createIdentifier("__TSTL_export");
280+
281+
const declaration = tstl.createVariableDeclarationStatement(tempModuleIdentifier, moduleRequire);
282+
283+
const forKey = tstl.createIdentifier("____exportKey");
284+
const forValue = tstl.createIdentifier("____exportValue");
285+
286+
const body = tstl.createBlock(
287+
[tstl.createAssignmentStatement(
288+
tstl.createTableIndexExpression(
289+
tstl.createIdentifier("exports"),
290+
forKey
291+
),
292+
forValue
293+
)]
294+
);
295+
296+
const pairsIdentifier = tstl.createIdentifier("pairs");
297+
const forIn = tstl.createForInStatement(
298+
body,
299+
[tstl.cloneIdentifier(forKey), tstl.cloneIdentifier(forValue)],
300+
[tstl.createCallExpression(pairsIdentifier, [tstl.cloneIdentifier(tempModuleIdentifier)])]
301+
);
302+
303+
// Wrap this in a DoStatement to prevent polluting the scope.
304+
return tstl.createDoStatement([declaration, forIn], statement);
305+
}
306+
}
307+
222308
public transformImportDeclaration(statement: ts.ImportDeclaration): StatementVisitResult {
223309
if (statement.importClause && !statement.importClause.namedBindings) {
224310
throw TSTLErrors.DefaultImportsNotSupported(statement);
@@ -228,9 +314,8 @@ export class LuaTransformer {
228314

229315
const moduleSpecifier = statement.moduleSpecifier as ts.StringLiteral;
230316
const importPath = moduleSpecifier.text.replace(new RegExp("\"", "g"), "");
231-
const resolvedModuleSpecifier = tstl.createStringLiteral(this.getImportPath(importPath));
232317

233-
const requireCall = tstl.createCallExpression(tstl.createIdentifier("require"), [resolvedModuleSpecifier]);
318+
const requireCall = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral);
234319

235320
if (!statement.importClause) {
236321
result.push(tstl.createExpressionStatement(requireCall));
@@ -291,6 +376,13 @@ export class LuaTransformer {
291376
}
292377
}
293378

379+
private createModuleRequire(moduleSpecifier: ts.StringLiteral): tstl.CallExpression {
380+
const importPath = moduleSpecifier.text.replace(new RegExp("\"", "g"), "");
381+
const resolvedModuleSpecifier = tstl.createStringLiteral(this.getImportPath(importPath));
382+
383+
return tstl.createCallExpression(tstl.createIdentifier("require"), [resolvedModuleSpecifier]);
384+
}
385+
294386
public transformClassDeclaration(
295387
statement: ts.ClassLikeDeclaration,
296388
nameOverride?: tstl.Identifier

src/TSTLErrors.ts

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,23 @@ import {TranspileError} from "./TranspileError";
44
import {TSHelper as tsHelper} from "./TSHelper";
55

66
export class TSTLErrors {
7-
public static CouldNotFindEnumMember =
8-
(enumDeclaration: ts.EnumDeclaration, enumMember: string, node: ts.Node) => new TranspileError(
9-
`Could not find ${enumMember} in ${enumDeclaration.name.text}`, node
10-
);
7+
public static CouldNotFindEnumMember = (enumDeclaration: ts.EnumDeclaration, enumMember: string, node: ts.Node) =>
8+
new TranspileError(`Could not find ${enumMember} in ${enumDeclaration.name.text}`, node);
119

1210
public static DefaultImportsNotSupported = (node: ts.Node) =>
1311
new TranspileError(`Default Imports are not supported, please use named imports instead!`, node);
1412

15-
public static ForbiddenEllipsisDestruction =
16-
(node: ts.Node) => new TranspileError(`Ellipsis destruction is not allowed.`, node);
13+
public static ForbiddenEllipsisDestruction = (node: ts.Node) =>
14+
new TranspileError(`Ellipsis destruction is not allowed.`, node);
1715

18-
public static ForbiddenForIn =
19-
(node: ts.Node) => new TranspileError(`Iterating over arrays with 'for ... in' is not allowed.`, node);
16+
public static ForbiddenForIn = (node: ts.Node) =>
17+
new TranspileError(`Iterating over arrays with 'for ... in' is not allowed.`, node);
2018

2119
public static HeterogeneousEnum = (node: ts.Node) => new TranspileError(
2220
`Invalid heterogeneous enum. Enums should either specify no member values, ` +
2321
`or specify values (of the same type) for all members.`,
2422
node);
2523

26-
public static InvalidEnumMember =
27-
(node: ts.Node) => new TranspileError(`Only numeric or string initializers allowed for enums.`, node);
28-
2924
public static InvalidDecoratorArgumentNumber = (name: string, got: number, expected: number, node: ts.Node) =>
3025
new TranspileError(`${name} expects ${expected} argument(s) but got ${got}.`, node);
3126

@@ -44,6 +39,9 @@ export class TSTLErrors {
4439
public static InvalidInstanceOfExtension = (node: ts.Node) =>
4540
new TranspileError(`Cannot use instanceof on classes with decorator '@extension' or '@metaExtension'.`, node);
4641

42+
public static InvalidJsonFileContent = (node: ts.Node) =>
43+
new TranspileError("Invalid JSON file content", node);
44+
4745
public static InvalidPropertyCall = (node: ts.Node) =>
4846
new TranspileError(`Tried to transpile a non-property call as property call.`, node);
4947

@@ -56,32 +54,30 @@ export class TSTLErrors {
5654
public static KeywordIdentifier = (node: ts.Identifier) =>
5755
new TranspileError(`Cannot use Lua keyword ${node.escapedText} as identifier.`, node);
5856

59-
public static MissingClassName =
60-
(node: ts.Node) => new TranspileError(`Class declarations must have a name.`, node);
57+
public static MissingClassName = (node: ts.Node) =>
58+
new TranspileError(`Class declarations must have a name.`, node);
6159

6260
public static MissingMetaExtension = (node: ts.Node) =>
63-
new TranspileError(`!MetaExtension requires the extension of the metatable class.`, node);
61+
new TranspileError(`@metaExtension requires the extension of the metatable class.`, node);
62+
63+
public static UnsupportedDefaultExport = (node: ts.Node) =>
64+
new TranspileError(`Default exports are not supported.`, node);
6465

65-
public static UnsupportedImportType = (node: ts.Node) => new TranspileError(`Unsupported import type.`, node);
66+
public static UnsupportedImportType = (node: ts.Node) =>
67+
new TranspileError(`Unsupported import type.`, node);
6668

67-
public static UnsupportedKind =
68-
(description: string, kind: ts.SyntaxKind, node: ts.Node) => {
69+
public static UnsupportedKind = (description: string, kind: ts.SyntaxKind, node: ts.Node) =>
70+
{
6971
const kindName = tsHelper.enumName(kind, ts.SyntaxKind);
7072
return new TranspileError(`Unsupported ${description} kind: ${kindName}`, node);
71-
}
73+
};
7274

7375
public static UnsupportedProperty = (parentName: string, property: string, node: ts.Node) =>
7476
new TranspileError(`Unsupported property on ${parentName}: ${property}`, node);
7577

7678
public static UnsupportedForTarget = (functionality: string, version: string, node: ts.Node) =>
7779
new TranspileError(`${functionality} is/are not supported for target Lua ${version}.`, node);
7880

79-
public static UnsupportedObjectLiteralElement = (elementKind: ts.SyntaxKind, node: ts.Node) =>
80-
new TranspileError(`Unsupported object literal element: ${elementKind}.`, node);
81-
82-
public static UnsupportedUnionAccessor = (node: ts.Node) =>
83-
new TranspileError(`Unsupported mixed union of accessor and non-accessor types for the same property.`, node);
84-
8581
public static UnsupportedFunctionConversion = (node: ts.Node, name?: string) => {
8682
if (name) {
8783
return new TranspileError(
@@ -148,6 +144,4 @@ export class TSTLErrors {
148144
node
149145
);
150146
}
151-
152-
public static InvalidJsonFileContent = (node: ts.Node) => new TranspileError("Invalid JSON file content", node);
153147
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
local exports = exports or {};
2+
local xyz = 4;
3+
exports.xyz = xyz;
4+
exports.uwv = xyz;
5+
do
6+
local __TSTL_export = require("xyz");
7+
for ____exportKey, ____exportValue in pairs(__TSTL_export) do
8+
exports[____exportKey] = ____exportValue;
9+
end
10+
end
11+
do
12+
local __TSTL_xyz = require("xyz");
13+
local abc = __TSTL_xyz.abc;
14+
local def = __TSTL_xyz.def;
15+
exports.abc = abc;
16+
exports.def = def;
17+
end
18+
do
19+
local __TSTL_xyz = require("xyz");
20+
local def = __TSTL_xyz.abc;
21+
exports.def = def;
22+
end
23+
return exports;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const xyz = 4;
2+
export {xyz};
3+
export {xyz as uwv};
4+
export * from "xyz";
5+
export {abc, def} from "xyz";
6+
export {abc as def} from "xyz";

test/unit/decoratorMetaExtension.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Expect, Test } from "alsatian";
22
import * as util from "../src/util";
33

44
import { TranspileError } from "../../src/TranspileError";
5+
import { TSTLErrors } from "../../src/TSTLErrors";
56

67
export class DecoratorMetaExtension {
78

@@ -29,6 +30,7 @@ export class DecoratorMetaExtension {
2930

3031
@Test("IncorrectUsage")
3132
public incorrectUsage(): void {
33+
const expectedMessage = TSTLErrors.MissingMetaExtension(undefined).message;
3234
Expect(() => {
3335
util.transpileString(
3436
`
@@ -40,8 +42,7 @@ export class DecoratorMetaExtension {
4042
}
4143
`
4244
);
43-
}).toThrowError(TranspileError,
44-
"!MetaExtension requires the extension of the metatable class.");
45+
}).toThrowError(TranspileError, expectedMessage);
4546
}
4647

4748
@Test("DontAllowInstantiation")

test/unit/importexport.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Expect, Test, TestCase } from "alsatian";
2+
3+
import * as util from "../src/util";
4+
import { TSTLErrors } from "../../src/TSTLErrors";
5+
import { TranspileError } from "../../src/TranspileError";
6+
7+
export class ImportExportTests
8+
{
9+
@TestCase("export { default } from '...'")
10+
@TestCase("export { x as default } from '...';")
11+
@TestCase("export { default as x } from '...';")
12+
@Test("Export default keyword disallowed")
13+
public exportDefaultKeywordError(exportStatement: string): void {
14+
const expectedTest = TSTLErrors.UnsupportedDefaultExport(undefined).message;
15+
Expect(() => util.transpileString(exportStatement)).toThrowError(TranspileError, expectedTest);
16+
}
17+
}

0 commit comments

Comments
 (0)