Skip to content

Commit a1825a1

Browse files
committed
Implement bundle transformation
1 parent c1e77c9 commit a1825a1

File tree

5 files changed

+228
-23
lines changed

5 files changed

+228
-23
lines changed

src/LuaTransformer.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,21 +104,43 @@ export class LuaTransformer {
104104

105105
protected currentSourceFile!: ts.SourceFile;
106106
protected isModule!: boolean;
107+
protected isWithinBundle!: boolean;
107108
protected resolver!: EmitResolver;
108109

109110
/** @internal */
110-
public transform(sourceFile: ts.SourceFile): [tstl.Block, Set<LuaLibFeature>] {
111+
public transform(node: ts.Bundle | ts.SourceFile): [tstl.Block, Set<LuaLibFeature>] {
111112
this.setupState();
112-
this.currentSourceFile = sourceFile;
113-
this.isModule = tsHelper.isFileModule(sourceFile);
113+
if (ts.isSourceFile(node)) {
114+
this.currentSourceFile = node;
115+
this.isModule = tsHelper.isFileModule(node);
114116

115-
// Use `getParseTreeNode` to get original SourceFile node, before it was substituted by custom transformers.
116-
// It's required because otherwise `getEmitResolver` won't use cached diagnostics, produced in `emitWorker`
117-
// and would try to re-analyze the file, which would fail because of replaced nodes.
118-
const originalSourceFile = ts.getParseTreeNode(sourceFile, ts.isSourceFile) || sourceFile;
119-
this.resolver = this.checker.getEmitResolver(originalSourceFile);
117+
// Use `getParseTreeNode` to get original SourceFile node, before it was substituted by custom transformers.
118+
// It's required because otherwise `getEmitResolver` won't use cached diagnostics, produced in `emitWorker`
119+
// and would try to re-analyze the file, which would fail because of replaced nodes.
120+
const originalSourceFile = ts.getParseTreeNode(node, ts.isSourceFile) || node;
121+
this.resolver = this.checker.getEmitResolver(originalSourceFile);
120122

121-
return [this.transformSourceFile(sourceFile), this.luaLibFeatureSet];
123+
return [this.transformSourceFile(node), this.luaLibFeatureSet];
124+
} else {
125+
return [this.transformBundle(node), this.luaLibFeatureSet];
126+
}
127+
}
128+
129+
public transformBundle(bundle: ts.Bundle): tstl.Block {
130+
this.isWithinBundle = true;
131+
const combinedStatements = bundle.sourceFiles.reduce(
132+
(statements: tstl.Statement[], sourceFile: ts.SourceFile) => {
133+
this.currentSourceFile = sourceFile;
134+
this.isModule = tsHelper.isFileModule(sourceFile);
135+
const originalSourceFile = ts.getParseTreeNode(sourceFile, ts.isSourceFile) || sourceFile;
136+
this.resolver = this.checker.getEmitResolver(originalSourceFile);
137+
const transformResult = this.transformSourceFile(sourceFile);
138+
return [...statements, ...transformResult.statements];
139+
},
140+
[]
141+
);
142+
143+
return tstl.createBlock(combinedStatements, bundle);
122144
}
123145

124146
public transformSourceFile(sourceFile: ts.SourceFile): tstl.Block {
@@ -149,6 +171,19 @@ export class LuaTransformer {
149171

150172
// return exports
151173
statements.push(tstl.createReturnStatement([this.createExportsIdentifier()]));
174+
175+
if (this.isWithinBundle) {
176+
const packagePreload = tstl.createTableIndexExpression(
177+
tstl.createIdentifier("package"),
178+
tstl.createStringLiteral("preload")
179+
);
180+
const exportPath = tsHelper.getExportPath(sourceFile.fileName, this.options);
181+
const packagePreloadDeclaration = tstl.createAssignmentStatement(
182+
tstl.createTableIndexExpression(packagePreload, tstl.createStringLiteral(exportPath)),
183+
tstl.createFunctionExpression(tstl.createBlock(statements, sourceFile))
184+
);
185+
return tstl.createBlock([packagePreloadDeclaration], sourceFile);
186+
}
152187
}
153188
}
154189

src/TSTransformers.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ export function getCustomTransformers(
99
program: ts.Program,
1010
diagnostics: ts.Diagnostic[],
1111
customTransformers: ts.CustomTransformers,
12-
onSourceFile: (sourceFile: ts.SourceFile) => void
12+
onRootNode: (node: ts.Bundle | ts.SourceFile) => void
1313
): ts.CustomTransformers {
14-
const luaTransformer: ts.TransformerFactory<ts.SourceFile> = () => sourceFile => {
15-
onSourceFile(sourceFile);
16-
return ts.createSourceFile(sourceFile.fileName, "", ts.ScriptTarget.ESNext);
17-
};
14+
const luaTransformer: ts.CustomTransformerFactory = () => ({
15+
transformBundle: node => {
16+
onRootNode(node);
17+
return ts.createBundle([]);
18+
},
19+
transformSourceFile: node => {
20+
onRootNode(node);
21+
return ts.createSourceFile(node.fileName, "", ts.ScriptTarget.ESNext);
22+
},
23+
});
1824

1925
const transformersFromOptions = loadTransformersFromOptions(program, diagnostics);
2026

src/Transpile.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,26 +78,32 @@ export function transpile({
7878
}
7979
}
8080

81-
const processSourceFile = (sourceFile: ts.SourceFile) => {
81+
const processRootNode = (node: ts.Bundle | ts.SourceFile) => {
82+
if (ts.isBundle(node) && !options.outFile) {
83+
throw new Error("The option outFile must be specified when transforming a bundle.");
84+
}
85+
86+
const fileName = ts.isBundle(node) ? options.outFile! : node.fileName;
87+
8288
try {
83-
const [luaAst, lualibFeatureSet] = transformer.transform(sourceFile);
89+
const [luaAst, lualibFeatureSet] = transformer.transform(node);
8490
if (!options.noEmit && !options.emitDeclarationOnly) {
85-
const [lua, sourceMap] = printer.print(luaAst, lualibFeatureSet, sourceFile.fileName);
86-
updateTranspiledFile(sourceFile.fileName, { luaAst, lua, sourceMap });
91+
const [lua, sourceMap] = printer.print(luaAst, lualibFeatureSet, fileName);
92+
updateTranspiledFile(fileName, { luaAst, lua, sourceMap });
8793
}
8894
} catch (err) {
8995
if (!(err instanceof TranspileError)) throw err;
9096

9197
diagnostics.push(diagnosticFactories.transpileError(err));
9298

93-
updateTranspiledFile(sourceFile.fileName, {
99+
updateTranspiledFile(fileName, {
94100
lua: `error(${JSON.stringify(err.message)})\n`,
95101
sourceMap: "",
96102
});
97103
}
98104
};
99105

100-
const transformers = getCustomTransformers(program, diagnostics, customTransformers, processSourceFile);
106+
const transformers = getCustomTransformers(program, diagnostics, customTransformers, processRootNode);
101107

102108
const writeFile: ts.WriteFileCallback = (fileName, data, _bom, _onError, sourceFiles = []) => {
103109
for (const sourceFile of sourceFiles) {
@@ -123,7 +129,7 @@ export function transpile({
123129
if (targetSourceFiles) {
124130
for (const file of targetSourceFiles) {
125131
if (isEmittableJsonFile(file)) {
126-
processSourceFile(file);
132+
processRootNode(file);
127133
} else {
128134
diagnostics.push(...program.emit(file, writeFile, undefined, false, transformers).diagnostics);
129135
}
@@ -135,7 +141,7 @@ export function transpile({
135141
program
136142
.getSourceFiles()
137143
.filter(isEmittableJsonFile)
138-
.forEach(processSourceFile);
144+
.forEach(processRootNode);
139145
}
140146

141147
options.noEmit = oldNoEmit;

test/unit/outFile.spec.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import * as util from "../util";
2+
import * as ts from "typescript";
3+
4+
const exportValueSource = `
5+
export const value = true;
6+
`;
7+
8+
const reexportValueSource = `
9+
export { value } from "./export";
10+
`;
11+
12+
test.each<[string, Record<string, string>]>([
13+
[
14+
"Import module -> main",
15+
{
16+
"main.ts": `
17+
import { value } from "./module";
18+
if (value !== true) {
19+
throw "Failed to import x";
20+
}
21+
`,
22+
"module.ts": exportValueSource,
23+
},
24+
],
25+
[
26+
"Import chain export -> reexport -> main",
27+
{
28+
"main.ts": `
29+
import { value } from "./reexport";
30+
if (value !== true) {
31+
throw "Failed to import value";
32+
}
33+
`,
34+
"reexport.ts": reexportValueSource,
35+
"export.ts": exportValueSource,
36+
},
37+
],
38+
[
39+
"Import chain with a different order",
40+
{
41+
"main.ts": `
42+
import { value } from "./reexport";
43+
if (value !== true) {
44+
throw "Failed to import value";
45+
}
46+
`,
47+
"export.ts": exportValueSource,
48+
"reexport.ts": reexportValueSource,
49+
},
50+
],
51+
[
52+
"Import diamond export -> reexport1 & reexport2 -> main",
53+
{
54+
"main.ts": `
55+
import { value as a } from "./reexport1";
56+
import { value as b } from "./reexport2";
57+
if (a !== true || b !== true) {
58+
throw "Failed to import a or b";
59+
}
60+
`,
61+
"export.ts": exportValueSource,
62+
"reexport1.ts": reexportValueSource,
63+
"reexport2.ts": reexportValueSource,
64+
},
65+
],
66+
[
67+
"Import diamond different order",
68+
{
69+
"reexport1.ts": reexportValueSource,
70+
"reexport2.ts": reexportValueSource,
71+
"export.ts": exportValueSource,
72+
"main.ts": `
73+
import { value as a } from "./reexport1";
74+
import { value as b } from "./reexport2";
75+
if (a !== true || b !== true) {
76+
throw "Failed to import a or b";
77+
}
78+
`,
79+
},
80+
],
81+
[
82+
"Modules in directories",
83+
{
84+
"main.ts": `
85+
import { value } from "./module/module";
86+
if (value !== true) {
87+
throw "Failed to import value";
88+
}
89+
`,
90+
"module/module.ts": `
91+
export const value = true;
92+
`,
93+
},
94+
],
95+
[
96+
"Modules aren't ordered by name",
97+
{
98+
"main.ts": `
99+
import { value } from "./a";
100+
if (value !== true) {
101+
throw "Failed to import value";
102+
}
103+
`,
104+
"a.ts": `
105+
export const value = true;
106+
`,
107+
},
108+
],
109+
[
110+
"Modules in directories",
111+
{
112+
"main/main.ts": `
113+
import { value } from "../module";
114+
if (value !== true) {
115+
throw "Failed to import value";
116+
}
117+
`,
118+
"module.ts": `
119+
export const value = true;
120+
`,
121+
},
122+
],
123+
[
124+
"LuaLibs are usable",
125+
{
126+
"module.ts": `
127+
export const array = [1, 2];
128+
array.push(3);
129+
`,
130+
"main.ts": `
131+
import { array } from "./module";
132+
if (array[2] !== 3) {
133+
throw "Array's third item is not three";
134+
}
135+
`,
136+
},
137+
],
138+
])("outFile tests (%s)", (_, files) => {
139+
const testBuilder = util.testBundle`
140+
${files["main.ts"]}
141+
`
142+
.setOptions({ outFile: "main.lua", module: ts.ModuleKind.AMD })
143+
.setMainFileName("main.ts");
144+
145+
const extraFiles = Object.keys(files)
146+
.map(file => ({ fileName: file, code: files[file] }))
147+
.filter(file => file.fileName !== "main.ts");
148+
149+
extraFiles.forEach(extraFile => {
150+
testBuilder.addExtraFile(extraFile.fileName, extraFile.code);
151+
});
152+
153+
testBuilder.expectNoExecutionError();
154+
});

test/util.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ export abstract class TestBuilder {
223223
@memoize
224224
public getMainLuaFileResult(): ExecutableTranspiledFile {
225225
const { transpiledFiles } = this.getLuaResult();
226-
const mainFile = transpiledFiles.find(x => x.fileName === this.mainFileName);
226+
const mainFileName = this.options.outFile ? this.options.outFile : this.mainFileName;
227+
const mainFile = transpiledFiles.find(x => x.fileName === mainFileName);
227228
expect(mainFile).toMatchObject({ lua: expect.any(String), sourceMap: expect.any(String) });
228229
return mainFile as ExecutableTranspiledFile;
229230
}
@@ -418,6 +419,8 @@ class AccessorTestBuilder extends TestBuilder {
418419
}
419420
}
420421

422+
class BundleTestBuilder extends AccessorTestBuilder {}
423+
421424
class ModuleTestBuilder extends AccessorTestBuilder {
422425
public setReturnExport(name: string): this {
423426
expect(this.hasProgram).toBe(false);
@@ -462,6 +465,7 @@ const createTestBuilderFactory = <T extends TestBuilder>(
462465
return new builder(tsCode);
463466
};
464467

468+
export const testBundle = createTestBuilderFactory(BundleTestBuilder, false);
465469
export const testModule = createTestBuilderFactory(ModuleTestBuilder, false);
466470
export const testModuleTemplate = createTestBuilderFactory(ModuleTestBuilder, true);
467471
export const testFunction = createTestBuilderFactory(FunctionTestBuilder, false);

0 commit comments

Comments
 (0)