Skip to content

Commit 09c8f0f

Browse files
authored
Fixes bundle mode with enabled sourceMapTraceback (#1110)
* #1109 In Progress - Join code chunk, due to its correctness and existness * Removed unused `sourceMapNode` * Provided SourceMapTraceBack for bundled files (#1109) * Moved helpful function `lineAndColumnOf` into utils.ts file * Covered bundling with source map traceback (#1109) * Removed unnecessary comments
1 parent dc78114 commit 09c8f0f

12 files changed

Lines changed: 315 additions & 47 deletions

File tree

src/LuaPrinter.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Mapping, SourceMapGenerator, SourceNode } from "source-map";
22
import * as ts from "typescript";
3-
import { CompilerOptions, LuaLibImportKind } from "./CompilerOptions";
3+
import { CompilerOptions, isBundleEnabled, LuaLibImportKind } from "./CompilerOptions";
44
import * as lua from "./LuaAST";
55
import { loadLuaLibFeatures, LuaLibFeature } from "./LuaLib";
66
import { isValidLuaIdentifier } from "./transformation/utils/safe-names";
@@ -123,6 +123,8 @@ export class LuaPrinter {
123123
private sourceFile: string;
124124
private options: CompilerOptions;
125125

126+
public static readonly sourceMapTracebackPlaceholder = "{#SourceMapTraceback}";
127+
126128
constructor(private emitHost: EmitHost, program: ts.Program, fileName: string) {
127129
this.options = program.getCompilerOptions();
128130
this.sourceFile = fileName;
@@ -149,7 +151,7 @@ export class LuaPrinter {
149151

150152
if (this.options.sourceMapTraceback) {
151153
const stackTraceOverride = this.printStackTraceOverride(rootSourceNode);
152-
code = code.replace("{#SourceMapTraceback}", stackTraceOverride);
154+
code = code.replace(LuaPrinter.sourceMapTracebackPlaceholder, stackTraceOverride);
153155
}
154156

155157
return { code, sourceMap: sourceMap.toString(), sourceMapNode: rootSourceNode };
@@ -203,8 +205,10 @@ export class LuaPrinter {
203205
header += loadLuaLibFeatures(file.luaLibFeatures, this.emitHost);
204206
}
205207

206-
if (this.options.sourceMapTraceback) {
207-
header += "{#SourceMapTraceback}\n";
208+
if (this.options.sourceMapTraceback && !isBundleEnabled(this.options)) {
209+
// In bundle mode the traceback is being generated for the entire file in getBundleResult
210+
// Otherwise, traceback is being generated locally
211+
header += `${LuaPrinter.sourceMapTracebackPlaceholder}\n`;
208212
}
209213

210214
return this.concatNodes(header, ...this.printStatementArray(file.statements));

src/lualib/SourceMapTraceBack.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
// TODO: In the future, change this to __TS__RegisterFileInfo and provide tstl interface to
22
// get some metadata about transpilation.
3+
4+
interface SourceMap {
5+
[line: number]: number | { line: number; file: string };
6+
}
7+
38
declare function __TS__originalTraceback(this: void, thread?: LuaThread, message?: string, level?: number);
4-
function __TS__SourceMapTraceBack(this: void, fileName: string, sourceMap: { [line: number]: number }): void {
9+
10+
function __TS__SourceMapTraceBack(this: void, fileName: string, sourceMap: SourceMap): void {
511
globalThis.__TS__sourcemap = globalThis.__TS__sourcemap || {};
612
globalThis.__TS__sourcemap[fileName] = sourceMap;
713

@@ -19,13 +25,26 @@ function __TS__SourceMapTraceBack(this: void, fileName: string, sourceMap: { [li
1925
return trace;
2026
}
2127

22-
const [result] = string.gsub(trace, "(%S+).lua:(%d+)", (file, line) => {
23-
const fileSourceMap = globalThis.__TS__sourcemap[file + ".lua"];
28+
const replacer = (file: string, srcFile: string, line: string) => {
29+
const fileSourceMap: SourceMap = globalThis.__TS__sourcemap[file];
2430
if (fileSourceMap && fileSourceMap[line]) {
25-
return `${file}.ts:${fileSourceMap[line]}`;
31+
const data = fileSourceMap[line];
32+
if (typeof data === "number") {
33+
return `${srcFile}:${data}`;
34+
}
35+
36+
return `${data.file}:${data.line}`;
2637
}
27-
return `${file}.lua:${line}`;
28-
});
38+
39+
return `${file}:${line}`;
40+
};
41+
42+
let [result] = string.gsub(trace, "(%S+)%.lua:(%d+)", (file, line) =>
43+
replacer(`${file}.lua`, `${file}.ts`, line)
44+
);
45+
[result] = string.gsub(result, '(%[string "[^"]+"%]):(%d+)', (file, line) =>
46+
replacer(file, "unknown", line)
47+
);
2948

3049
return result;
3150
}) as typeof debug.traceback;

src/transpilation/bundle.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,49 @@ local function require(file, ...)
3434
end
3535
`;
3636

37+
export const sourceMapTracebackBundlePlaceholder = "{#SourceMapTracebackBundle}";
38+
39+
type SourceMapLineData = number | { line: number; file: string };
40+
41+
export function printStackTraceBundleOverride(rootNode: SourceNode): string {
42+
const map: Record<number, SourceMapLineData> = {};
43+
const getLineNumber = (line: number, fallback: number) => {
44+
const data: SourceMapLineData | undefined = map[line];
45+
if (data === undefined) {
46+
return fallback;
47+
}
48+
if (typeof data === "number") {
49+
return data;
50+
}
51+
return data.line;
52+
};
53+
const transformLineData = (data: SourceMapLineData) => {
54+
if (typeof data === "number") {
55+
return data;
56+
}
57+
return `{line = ${data.line}, file = "${data.file}"}`;
58+
};
59+
60+
let currentLine = 1;
61+
rootNode.walk((chunk, mappedPosition) => {
62+
if (mappedPosition.line !== undefined && mappedPosition.line > 0) {
63+
const line = getLineNumber(currentLine, mappedPosition.line);
64+
65+
map[currentLine] = {
66+
line,
67+
file: path.basename(mappedPosition.source),
68+
};
69+
}
70+
71+
currentLine += chunk.split("\n").length - 1;
72+
});
73+
74+
const mapItems = Object.entries(map).map(([line, original]) => `["${line}"] = ${transformLineData(original)}`);
75+
const mapString = "{" + mapItems.join(",") + "}";
76+
77+
return `__TS__SourceMapTraceBack(debug.getinfo(1).short_src, ${mapString});`;
78+
}
79+
3780
export function getBundleResult(program: ts.Program, files: ProcessedFile[]): [ts.Diagnostic[], EmitFile] {
3881
const diagnostics: ts.Diagnostic[] = [];
3982

@@ -58,14 +101,22 @@ export function getBundleResult(program: ts.Program, files: ProcessedFile[]): [t
58101
// return require("<entry module path>")
59102
const entryPoint = `return require(${createModulePath(entryModule, program)}, ...)\n`;
60103

61-
const sourceChunks = [requireOverride, moduleTable, entryPoint];
104+
const footers: string[] = [];
105+
if (options.sourceMapTraceback) {
106+
// Generates SourceMapTraceback for the entire file
107+
footers.push('require("lualib_bundle")\n');
108+
footers.push(`${sourceMapTracebackBundlePlaceholder}\n`);
109+
}
110+
111+
const sourceChunks = [requireOverride, moduleTable, ...footers, entryPoint];
62112

63113
if (!options.noHeader) {
64114
sourceChunks.unshift(tstlHeader);
65115
}
66116

67117
const bundleNode = joinSourceChunks(sourceChunks);
68-
const { code, map } = bundleNode.toStringWithSourceMap();
118+
let { code, map } = bundleNode.toStringWithSourceMap();
119+
code = code.replace(sourceMapTracebackBundlePlaceholder, printStackTraceBundleOverride(bundleNode));
69120

70121
return [
71122
diagnostics,
@@ -79,7 +130,7 @@ export function getBundleResult(program: ts.Program, files: ProcessedFile[]): [t
79130
}
80131

81132
function moduleSourceNode({ code, sourceMapNode }: ProcessedFile, modulePath: string): SourceNode {
82-
const tableEntryHead = `[${modulePath}] = function(...) `;
133+
const tableEntryHead = `[${modulePath}] = function(...) \n`;
83134
const tableEntryTail = " end,\n";
84135

85136
return joinSourceChunks([tableEntryHead, sourceMapNode ?? code, tableEntryTail]);
@@ -93,6 +144,7 @@ function createModuleTableNode(fileChunks: SourceChunk[]): SourceNode {
93144
}
94145

95146
type SourceChunk = string | SourceNode;
147+
96148
function joinSourceChunks(chunks: SourceChunk[]): SourceNode {
97149
return new SourceNode(null, null, null, chunks);
98150
}

test/transpile/bundle.spec.ts

Lines changed: 122 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,132 @@
11
import * as path from "path";
22
import * as util from "../util";
3+
import { TranspileVirtualProjectResult } from "../../src";
4+
import { lineAndColumnOf } from "../unit/printer/utils";
5+
import * as fs from "fs";
36

4-
const projectDir = path.join(__dirname, "bundle");
5-
const inputProject = path.join(projectDir, "tsconfig.json");
7+
describe("bundle two files", () => {
8+
const projectDir = path.join(__dirname, "bundle", "bundle-two-files");
9+
const inputProject = path.join(projectDir, "tsconfig.json");
610

7-
test("should transpile into one file", () => {
8-
const { diagnostics, transpiledFiles } = util.testProject(inputProject).getLuaResult();
11+
let transpileResult: TranspileVirtualProjectResult = {
12+
transpiledFiles: [],
13+
diagnostics: [],
14+
};
915

10-
expect(diagnostics).not.toHaveDiagnostics();
11-
expect(transpiledFiles).toHaveLength(1);
16+
beforeAll(() => {
17+
transpileResult = util.testProject(inputProject).getLuaResult();
18+
});
19+
20+
test("should transpile into one file (with no errors)", () => {
21+
expect(transpileResult.diagnostics).not.toHaveDiagnostics();
22+
expect(transpileResult.transpiledFiles).toHaveLength(1);
23+
});
1224

13-
const { outPath, lua } = transpiledFiles[0];
1425
// Verify the name is as specified in tsconfig
15-
expect(outPath.endsWith(path.join("bundle", "bundle.lua"))).toBe(true);
26+
test("should have name, specified in tsconfig.json", () => {
27+
const { outPath } = transpileResult.transpiledFiles[0];
28+
expect(outPath.endsWith(path.join(projectDir, "bundle.lua"))).toBe(true);
29+
});
30+
31+
// Verify exported module by executing
32+
// Use an empty TS string because we already transpiled the TS project
33+
test("executing should act correctly", () => {
34+
const { lua } = transpileResult.transpiledFiles[0];
35+
util.testModule("").setLuaHeader(lua!).expectToEqual({ myNumber: 3 });
36+
});
37+
});
38+
39+
describe("bundle with source maps", () => {
40+
const projectDir = path.join(__dirname, "bundle", "bundle-source-maps");
41+
const inputProject = path.join(projectDir, "tsconfig.json");
42+
43+
let transpileResult: TranspileVirtualProjectResult = {
44+
transpiledFiles: [],
45+
diagnostics: [],
46+
};
47+
48+
beforeAll(() => {
49+
transpileResult = util.testProject(inputProject).getLuaResult();
50+
});
51+
52+
// See https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1109
53+
test('the result file should not contain "{#SourceMapTraceback}" macro-string', () => {
54+
const { lua } = transpileResult.transpiledFiles[0];
55+
expect(lua).not.toBeUndefined();
56+
expect(lua!).not.toContain("{#SourceMapTraceback}");
57+
});
58+
1659
// Verify exported module by executing
1760
// Use an empty TS string because we already transpiled the TS project
18-
util.testModule("").setLuaHeader(lua!).expectToEqual({ myNumber: 3 });
61+
test("executing should act correctly", () => {
62+
const { lua } = transpileResult.transpiledFiles[0];
63+
const result = util.testModule("").setLuaHeader(lua!).getLuaExecutionResult();
64+
65+
expect(result.myNumber).toEqual(3 * 4 * (5 + 6));
66+
});
67+
68+
test("sourceMapTraceback saves correct sourcemap", () => {
69+
const code = {
70+
index: fs.readFileSync(path.join(projectDir, "index.ts"), "utf8"),
71+
largeFile: fs.readFileSync(path.join(projectDir, "largeFile.ts"), "utf8"),
72+
};
73+
74+
const { lua } = transpileResult.transpiledFiles[0];
75+
const builder = util.testModule("").setLuaHeader(lua!);
76+
const result = builder.getLuaExecutionResult();
77+
const sourceMap = result.sourceMap;
78+
79+
expect(sourceMap).toEqual(expect.any(Object));
80+
const sourceMapFiles = Object.keys(sourceMap);
81+
expect(sourceMapFiles).toHaveLength(1);
82+
const mainSourceMap = sourceMap[sourceMapFiles[0]];
83+
84+
const transpiledLua = builder.getMainLuaCodeChunk();
85+
86+
const assertPatterns: Array<{
87+
file: keyof typeof code;
88+
luaPattern: string;
89+
typeScriptPattern: string;
90+
}> = [
91+
{
92+
file: "index",
93+
luaPattern: "____exports.myNumber = getNumber(",
94+
typeScriptPattern: "const myNumber = getNumber(",
95+
},
96+
{
97+
file: "largeFile",
98+
luaPattern: "local Calculator = __TS__Class()",
99+
typeScriptPattern: "abstract class Calculator",
100+
},
101+
{
102+
file: "largeFile",
103+
luaPattern: "local CalculatorMul = __TS__Class()",
104+
typeScriptPattern: "class CalculatorMul extends Calculator {",
105+
},
106+
{
107+
file: "largeFile",
108+
luaPattern: "local function resolveCalculatorClass(",
109+
typeScriptPattern: "function resolveCalculatorClass(",
110+
},
111+
{
112+
file: "largeFile",
113+
luaPattern: "function ____exports.getNumber(",
114+
typeScriptPattern: "export function getNumber(",
115+
},
116+
{
117+
file: "largeFile",
118+
luaPattern: 'Error,\n "Unknown operation "',
119+
typeScriptPattern: "throw new Error(",
120+
},
121+
];
122+
123+
for (const { file: currentFile, luaPattern, typeScriptPattern } of assertPatterns) {
124+
const luaPosition = lineAndColumnOf(transpiledLua, luaPattern);
125+
const mappedLine: { file: string; line: number } = mainSourceMap[luaPosition.line.toString()];
126+
127+
const typescriptPosition = lineAndColumnOf(code[currentFile], typeScriptPattern);
128+
expect(mappedLine.line).toEqual(typescriptPosition.line);
129+
expect(mappedLine.file).toEqual(`${currentFile}.ts`);
130+
}
131+
});
19132
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { getNumber, Operation } from "./largeFile";
2+
3+
// Local variables
4+
const left = getNumber(3, 4, Operation.MUL);
5+
const right = getNumber(5, 6, Operation.SUM);
6+
7+
export const myNumber = getNumber(left, right, Operation.MUL);
8+
export const sourceMap = (globalThis as any).__TS__sourcemap;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Some comments here to check source map correctness
2+
// Some comments here to check source map correctness
3+
4+
abstract class Calculator {
5+
protected left: number;
6+
protected right: number;
7+
8+
constructor(left: number, right: number) {
9+
this.left = left;
10+
this.right = right;
11+
}
12+
13+
public abstract calc(): number;
14+
}
15+
16+
/**
17+
* Sums two numbers
18+
*/
19+
class CalculatorSum extends Calculator {
20+
public calc(): number {
21+
return this.left + this.right;
22+
}
23+
}
24+
25+
class CalculatorMul extends Calculator {
26+
public calc(): number {
27+
return this.left * this.right;
28+
}
29+
}
30+
31+
// Some comments here to check source map correctness
32+
33+
export const enum Operation {
34+
SUM = "SUM",
35+
MUL = "MUL",
36+
}
37+
38+
// Local internal function
39+
function resolveCalculatorClass(left: number, right: number, operation: Operation): Calculator {
40+
if (operation === Operation.MUL) {
41+
return new CalculatorMul(left, right);
42+
}
43+
if (operation === Operation.SUM) {
44+
return new CalculatorSum(left, right);
45+
}
46+
47+
throw new Error(`Unknown operation ${operation}`);
48+
}
49+
50+
// Some comments here to check source map correctness
51+
export function getNumber(left: number, right: number, operation: Operation): number {
52+
return resolveCalculatorClass(left, right, operation).calc();
53+
}

0 commit comments

Comments
 (0)