Skip to content

Commit 2c47c3b

Browse files
hazzard993Perryvw
authored andcommitted
Export default and export equals (TypeScriptToLua#670)
* Fix import and export bugs * Fix spelling mistake * Add full import and export support * Remove exportedIdentifier stringliteral change * Fix export default from use case * Move default export modifier checking to tsHelper * Remove duplicate test * Add export equals test, fix require test formatting * Fix hasDefaultExportModifier return type * Enable export default to work on name-less classes * Fix public and protected method signatures for import and export methods * Make default class declaration expressions use default as name * Make export equals transform to a local statement * Add undefined condition if export equals is ellidable * Inline isExportable with getExportable * Move visitedExportEquals to setupState * Merge class tests and ensure classes retain their names * Change transformExportSpecifier to protected * Make importSpecifier protected and use if statement instead of nested ternary
1 parent d7f76fb commit 2c47c3b

File tree

9 files changed

+599
-257
lines changed

9 files changed

+599
-257
lines changed

src/LuaTransformer.ts

Lines changed: 304 additions & 234 deletions
Large diffs are not rendered by default.

src/TSHelper.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as ts from "typescript";
2+
import * as path from "path";
23
import { Decorator, DecoratorKind } from "./Decorator";
34
import * as tstl from "./LuaAST";
5+
import * as TSTLErrors from "./TSTLErrors";
6+
import { EmitResolver } from "./LuaTransformer";
47

58
export enum ContextType {
69
None,
@@ -57,6 +60,47 @@ export function isAssignmentPattern(node: ts.Node): node is ts.AssignmentPattern
5760
return ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node);
5861
}
5962

63+
export function getExportable(exportSpecifiers: ts.NamedExports, resolver: EmitResolver): ts.ExportSpecifier[] {
64+
return exportSpecifiers.elements.filter(exportSpecifier => resolver.isValueAliasDeclaration(exportSpecifier));
65+
}
66+
67+
export function isDefaultExportSpecifier(node: ts.ExportSpecifier): boolean {
68+
return (
69+
(node.name !== undefined && node.name.originalKeywordKind === ts.SyntaxKind.DefaultKeyword) ||
70+
(node.propertyName !== undefined && node.propertyName.originalKeywordKind === ts.SyntaxKind.DefaultKeyword)
71+
);
72+
}
73+
74+
export function hasDefaultExportModifier(modifiers?: ts.NodeArray<ts.Modifier>): boolean {
75+
return modifiers ? modifiers.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) : false;
76+
}
77+
78+
export function shouldResolveModulePath(moduleSpecifier: ts.Expression, checker: ts.TypeChecker): boolean {
79+
const moduleOwnerSymbol = checker.getSymbolAtLocation(moduleSpecifier);
80+
if (moduleOwnerSymbol) {
81+
const decorators = new Map<DecoratorKind, Decorator>();
82+
collectCustomDecorators(moduleOwnerSymbol, checker, decorators);
83+
if (decorators.has(DecoratorKind.NoResolution)) {
84+
return false;
85+
}
86+
}
87+
return true;
88+
}
89+
90+
export function shouldBeImported(
91+
importNode: ts.ImportClause | ts.ImportSpecifier,
92+
checker: ts.TypeChecker,
93+
resolver: EmitResolver
94+
): boolean {
95+
const decorators = getCustomDecorators(checker.getTypeAtLocation(importNode), checker);
96+
97+
return (
98+
resolver.isReferencedAliasDeclaration(importNode) &&
99+
!decorators.has(DecoratorKind.Extension) &&
100+
!decorators.has(DecoratorKind.MetaExtension)
101+
);
102+
}
103+
60104
export function isFileModule(sourceFile: ts.SourceFile): boolean {
61105
return sourceFile.statements.some(isStatementExported);
62106
}
@@ -968,3 +1012,55 @@ export function isSimpleExpression(expression: tstl.Expression): boolean {
9681012
}
9691013
return true;
9701014
}
1015+
1016+
export function getAbsoluteImportPath(
1017+
relativePath: string,
1018+
directoryPath: string,
1019+
options: ts.CompilerOptions
1020+
): string {
1021+
if (relativePath.charAt(0) !== "." && options.baseUrl) {
1022+
return path.resolve(options.baseUrl, relativePath);
1023+
}
1024+
1025+
return path.resolve(directoryPath, relativePath);
1026+
}
1027+
1028+
export function getImportPath(
1029+
fileName: string,
1030+
relativePath: string,
1031+
node: ts.Node,
1032+
options: ts.CompilerOptions
1033+
): string {
1034+
const rootDir = options.rootDir ? path.resolve(options.rootDir) : path.resolve(".");
1035+
1036+
const absoluteImportPath = path.format(
1037+
path.parse(getAbsoluteImportPath(relativePath, path.dirname(fileName), options))
1038+
);
1039+
const absoluteRootDirPath = path.format(path.parse(rootDir));
1040+
if (absoluteImportPath.includes(absoluteRootDirPath)) {
1041+
return formatPathToLuaPath(absoluteImportPath.replace(absoluteRootDirPath, "").slice(1));
1042+
} else {
1043+
throw TSTLErrors.UnresolvableRequirePath(
1044+
node,
1045+
`Cannot create require path. Module does not exist within --rootDir`,
1046+
relativePath
1047+
);
1048+
}
1049+
}
1050+
1051+
export function getExportPath(fileName: string, options: ts.CompilerOptions): string {
1052+
const rootDir = options.rootDir ? path.resolve(options.rootDir) : path.resolve(".");
1053+
1054+
const absolutePath = path.resolve(fileName.replace(/.ts$/, ""));
1055+
const absoluteRootDirPath = path.format(path.parse(rootDir));
1056+
return formatPathToLuaPath(absolutePath.replace(absoluteRootDirPath, "").slice(1));
1057+
}
1058+
1059+
export function formatPathToLuaPath(filePath: string): string {
1060+
filePath = filePath.replace(/\.json$/, "");
1061+
if (process.platform === "win32") {
1062+
// Windows can use backslashes
1063+
filePath = filePath.replace(/\.\\/g, "").replace(/\\/g, ".");
1064+
}
1065+
return filePath.replace(/\.\//g, "").replace(/\//g, ".");
1066+
}

src/TSTLErrors.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ const getLuaTargetName = (version: LuaTarget) => (version === LuaTarget.LuaJIT ?
77
export const CouldNotCast = (castName: string) =>
88
new Error(`Failed to cast all elements to expected type using ${castName}.`);
99

10-
export const DefaultImportsNotSupported = (node: ts.Node) =>
11-
new TranspileError(`Default Imports are not supported, please use named imports instead!`, node);
12-
1310
export const ForbiddenEllipsisDestruction = (node: ts.Node) =>
1411
new TranspileError(`Ellipsis destruction is not allowed.`, node);
1512

@@ -103,9 +100,6 @@ export const UndefinedTypeNode = (node: ts.Node) => new TranspileError("Failed t
103100
export const UnknownSuperType = (node: ts.Node) =>
104101
new TranspileError("Unable to resolve type of super expression.", node);
105102

106-
export const UnsupportedDefaultExport = (node: ts.Node) =>
107-
new TranspileError(`Default exports are not supported.`, node);
108-
109103
export const UnsupportedImportType = (node: ts.Node) => new TranspileError(`Unsupported import type.`, node);
110104

111105
export const UnsupportedKind = (description: string, kind: ts.SyntaxKind, node: ts.Node) =>

test/translation/__snapshots__/transformation.spec.ts.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ TestEnum.val3 = \\"baz\\"
194194
TestEnum.baz = \\"val3\\""
195195
`;
196196

197+
exports[`Transformation (exportEquals) 1`] = `
198+
"local ____exports = true
199+
return ____exports"
200+
`;
201+
197202
exports[`Transformation (exportStatement) 1`] = `
198203
"local ____exports = {}
199204
local xyz = 4
@@ -660,6 +665,11 @@ exports[`Transformation (typeAssert) 1`] = `
660665
local test2 = 10"
661666
`;
662667

668+
exports[`Transformation (unusedDefaultWithNamespaceImport) 1`] = `
669+
"local x = require(\\"module\\")
670+
local ____ = x"
671+
`;
672+
663673
exports[`Transformation (while) 1`] = `
664674
"local d = 10
665675
while d > 0 do
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export = true;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import def, * as x from "module";
2+
x;

test/unit/modules.spec.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as ts from "typescript";
22
import * as tstl from "../../src";
3-
import * as TSTLErrors from "../../src/TSTLErrors";
43
import * as util from "../util";
54

65
describe("module import/export elision", () => {
@@ -62,16 +61,6 @@ describe("module import/export elision", () => {
6261
});
6362
});
6463

65-
test.each([
66-
"export { default } from '...'",
67-
"export { x as default } from '...';",
68-
"export { default as x } from '...';",
69-
])("Export default keyword disallowed (%p)", exportStatement => {
70-
expect(() => util.transpileString(exportStatement)).toThrowExactError(
71-
TSTLErrors.UnsupportedDefaultExport(util.nodeStub)
72-
);
73-
});
74-
7564
test.each(["ke-bab", "dollar$", "singlequote'", "hash#", "s p a c e", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"])(
7665
"Import module names with invalid lua identifier characters (%p)",
7766
name => {
@@ -89,12 +78,6 @@ test.each(["ke-bab", "dollar$", "singlequote'", "hash#", "s p a c e", "ɥɣɎɌ
8978
}
9079
);
9180

92-
test("defaultImport", () => {
93-
expect(() => {
94-
util.transpileString(`import TestClass from "test"`);
95-
}).toThrowExactError(TSTLErrors.DefaultImportsNotSupported(util.nodeStub));
96-
});
97-
9881
test("lualibRequire", () => {
9982
const lua = util.transpileString(`let a = b instanceof c;`, {
10083
luaLibImport: tstl.LuaLibImportKind.Require,

test/unit/require.spec.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,148 @@ test("ImportEquals declaration require", () => {
187187
expect(match[1]).toBe("foo.bar");
188188
}
189189
});
190+
191+
test.each(["export default value;", "export { value as default };"])("Export Default From (%p)", exportStatement => {
192+
const [result] = util.transpileAndExecuteProjectReturningMainExport(
193+
{
194+
"main.ts": `
195+
export { default } from "./module";
196+
`,
197+
"module.ts": `
198+
export const value = true;
199+
${exportStatement};
200+
`,
201+
},
202+
"default"
203+
);
204+
205+
expect(result).toBe(true);
206+
});
207+
208+
test("Default Import and Export Expression", () => {
209+
const [result] = util.transpileAndExecuteProjectReturningMainExport(
210+
{
211+
"main.ts": `
212+
import defaultExport from "./module";
213+
export const value = defaultExport;
214+
`,
215+
"module.ts": `
216+
export default 1 + 2 + 3;
217+
`,
218+
},
219+
"value"
220+
);
221+
222+
expect(result).toBe(6);
223+
});
224+
225+
test("Import and Export Assignment", () => {
226+
const [result] = util.transpileAndExecuteProjectReturningMainExport(
227+
{
228+
"main.ts": `
229+
import * as m from "./module";
230+
export const value = m;
231+
`,
232+
"module.ts": `
233+
export = true;
234+
`,
235+
},
236+
"value"
237+
);
238+
239+
expect(result).toBe(true);
240+
});
241+
242+
test("Mixed Exports, Default and Named Imports", () => {
243+
const [result] = util.transpileAndExecuteProjectReturningMainExport(
244+
{
245+
"main.ts": `
246+
import defaultExport, { a, b, c } from "./module";
247+
export const value = defaultExport + b + c;
248+
`,
249+
"module.ts": `
250+
export const a = 1;
251+
export default a;
252+
export const b = 2;
253+
export const c = 3;
254+
`,
255+
},
256+
"value"
257+
);
258+
259+
expect(result).toBe(6);
260+
});
261+
262+
test("Mixed Exports, Default and Namespace Import", () => {
263+
const [result] = util.transpileAndExecuteProjectReturningMainExport(
264+
{
265+
"main.ts": `
266+
import defaultExport, * as ns from "./module";
267+
export const value = defaultExport + ns.b + ns.c;
268+
`,
269+
"module.ts": `
270+
export const a = 1;
271+
export default a;
272+
export const b = 2;
273+
export const c = 3;
274+
`,
275+
},
276+
"value"
277+
);
278+
279+
expect(result).toBe(6);
280+
});
281+
282+
test("Export Default Function", () => {
283+
const [result] = util.transpileAndExecuteProjectReturningMainExport(
284+
{
285+
"main.ts": `
286+
import defaultExport from "./module";
287+
export const value = defaultExport();
288+
`,
289+
"module.ts": `
290+
export default function() {
291+
return true;
292+
}
293+
`,
294+
},
295+
"value"
296+
);
297+
298+
expect(result).toBe(true);
299+
});
300+
301+
test.each([
302+
["Test", "export default class Test { static method() { return true; } }"],
303+
["default", "export default class { static method() { return true; } }"],
304+
])("Export Default Class Name (%p)", (expectedClassName, classDeclarationStatement) => {
305+
const [result] = util.transpileAndExecuteProjectReturningMainExport(
306+
{
307+
"main.ts": `
308+
import defaultExport from "./module";
309+
export const value = defaultExport.name;
310+
`,
311+
"module.ts": classDeclarationStatement,
312+
},
313+
"value"
314+
);
315+
316+
expect(result).toBe(expectedClassName);
317+
});
318+
319+
test("Export Equals", () => {
320+
const [result] = util.transpileAndExecuteProjectReturningMainExport(
321+
{
322+
"main.ts": `
323+
import * as module from "./module";
324+
export const value = module;
325+
`,
326+
"module.ts": `
327+
export = true;
328+
`,
329+
},
330+
"value"
331+
);
332+
333+
expect(result).toBe(true);
334+
});

0 commit comments

Comments
 (0)