Skip to content
2 changes: 1 addition & 1 deletion build_lualib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ compile([
"./src/lualib",
"--noHeader",
"true",
...glob.sync("./src/lualib/*.ts"),
...glob.sync("./src/lualib/**/*.ts"),
]);

if (fs.existsSync(bundlePath)) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"build-lualib": "ts-node ./build_lualib.ts",
"pretest": "ts-node --transpile-only ./build_lualib.ts",
"test": "jest",
"lint": "npm run lint:tslint && npm run lint:prettier",
"lint": "npm run lint:tslint",
"lint:prettier": "prettier --check **/*.{js,ts,yml,json}",
"lint:tslint": "tslint -p . && tslint -p test && tslint src/lualib/*.ts",
"release-major": "npm version major",
Expand Down
5 changes: 5 additions & 0 deletions src/CommandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ const optionDeclarations: {[key: string]: CLIOption<any>} = {
describe: "Disables hoisting.",
type: "boolean",
} as CLIOption<boolean>,
sourceMapTraceback: {
default: false,
describe: "Applies the source map to show source TS files and lines in error tracebacks.",
type: "boolean",
} as CLIOption<boolean>,
};

export const { version } = require("../package.json");
Expand Down
1 change: 1 addition & 0 deletions src/CompilerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface CompilerOptions extends ts.CompilerOptions {
luaTarget?: LuaTarget;
luaLibImport?: LuaLibImportKind;
noHoisting?: boolean;
sourceMapTraceback?: boolean;
}

export enum LuaLibImportKind {
Expand Down
1 change: 1 addition & 0 deletions src/LuaLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export enum LuaLibFeature {
Set = "Set",
WeakMap = "WeakMap",
WeakSet = "WeakSet",
SourceMapTraceBack = "SourceMapTraceBack",
StringReplace = "StringReplace",
StringSplit = "StringSplit",
StringConcat = "StringConcat",
Expand Down
68 changes: 37 additions & 31 deletions src/LuaPrinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,50 +50,45 @@ export class LuaPrinter {
this.currentIndent = "";
}

public print(block: tstl.Block, luaLibFeatures?: Set<LuaLibFeature>, sourceFile?: string): string {
if (this.options.inlineSourceMap === true) {
const rootSourceNode = this.printImplementation(block, luaLibFeatures, sourceFile);
public print(block: tstl.Block, luaLibFeatures?: Set<LuaLibFeature>, sourceFile?: string): [string, string] {
// Add traceback lualib if sourcemap traceback option is enabled
if (this.options.sourceMapTraceback) {
if (luaLibFeatures === undefined) {
luaLibFeatures = new Set();
}
luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack);
}

const codeWithMap = rootSourceNode
// TODO is the file: part really required? and should this be handled in the printer?
.toStringWithSourceMap({file: path.basename(sourceFile, path.extname(sourceFile)) + ".lua"});
const rootSourceNode = this.printImplementation(block, luaLibFeatures, sourceFile);

let inlineSourceMap = this.printInlineSourceMap(codeWithMap.map);
const codeWithSourceMap = rootSourceNode
// TODO is the file: part really required? and should this be handled in the printer?
.toStringWithSourceMap({file: path.basename(sourceFile, path.extname(sourceFile)) + ".lua"});

// TODO: Put this behind a compiler option?
const stackTraceOverride = this.printStackTraceOverride(rootSourceNode);
inlineSourceMap = stackTraceOverride + inlineSourceMap;
let codeResult = codeWithSourceMap.code;

return codeWithMap.code + "\n" + inlineSourceMap;
} else {
return this.printImplementation(block, luaLibFeatures, sourceFile).toString();
if (this.options.inlineSourceMap) {
codeResult += "\n" + this.printInlineSourceMap(codeWithSourceMap.map);
}
}

public printWithSourceMap(
block: tstl.Block,
luaLibFeatures?: Set<LuaLibFeature>,
sourceFile?: string): [string, string] {

const codeWithMap =
this.printImplementation(block, luaLibFeatures, sourceFile)
// TODO is the file: part really required? and should this be handled in the printer?
.toStringWithSourceMap({file: path.basename(sourceFile, path.extname(sourceFile)) + ".lua"});

if (this.options.sourceMapTraceback) {
const stackTraceOverride = this.printStackTraceOverride(rootSourceNode);
codeResult = codeResult.replace("{#SourceMapTraceback}", stackTraceOverride);
}

return [codeWithMap.code, codeWithMap.map.toString()];
return [codeResult, codeWithSourceMap.map.toString()];
}

private printInlineSourceMap(sourceMap: SourceMapGenerator): string {
const map = sourceMap.toString();
const base64Map = Buffer.from(map).toString('base64');

return "//# sourceMappingURL=data:application/json;base64," + base64Map;
return `//# sourceMappingURL=data:application/json;base64,${base64Map}\n`;
}

private printStackTraceOverride(rootNode: SourceNode): string {
let line = 1;
const map = {};
const map: {[line: number]: number} = {};
rootNode.walk((chunk, mappedPosition) => {
if (mappedPosition.line !== undefined && mappedPosition.line > 0) {
if (map[line] === undefined) {
Expand All @@ -104,8 +99,15 @@ export class LuaPrinter {
}
line += chunk.split("\n").length - 1;
});
console.log(map);
return "";

const mapItems = [];
for (const lineNr in map) {
mapItems.push(`["${lineNr}"] = ${map[lineNr]}`);
}

const mapString = "{" + mapItems.join(",") + "}";

return `__TS__SourceMapTraceBack(debug.getinfo(1).short_src, ${mapString});`;
}

private printImplementation(
Expand Down Expand Up @@ -136,9 +138,13 @@ export class LuaPrinter {

this.sourceFile = path.basename(sourceFile);

const blockNode = this.createSourceNode(block, this.printBlock(block));
if (this.options.sourceMapTraceback) {
header += "{#SourceMapTraceback}\n";
}

const fileBlockNode = this.createSourceNode(block, this.printBlock(block));

return this.concatNodes(header, blockNode);
return this.concatNodes(header, fileBlockNode);
}

private pushIndent(): void {
Expand Down
8 changes: 5 additions & 3 deletions src/LuaTranspiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,21 +141,23 @@ export class LuaTranspiler {
// Transform AST
const [luaAST, lualibFeatureSet] = this.luaTransformer.transformSourceFile(sourceFile);
// Print AST
return this.luaPrinter.print(luaAST, lualibFeatureSet, sourceFile.fileName);
const [code, sourceMap] = this.luaPrinter.print(luaAST, lualibFeatureSet, sourceFile.fileName);
return code;
}

public transpileSourceFileWithSourceMap(sourceFile: ts.SourceFile): [string, string] {
// Transform AST
const [luaAST, lualibFeatureSet] = this.luaTransformer.transformSourceFile(sourceFile);
// Print AST
return this.luaPrinter.printWithSourceMap(luaAST, lualibFeatureSet, sourceFile.fileName);
return this.luaPrinter.print(luaAST, lualibFeatureSet, sourceFile.fileName);
}

public transpileSourceFileKeepAST(sourceFile: ts.SourceFile): [tstl.Block, string] {
// Transform AST
const [luaAST, lualibFeatureSet] = this.luaTransformer.transformSourceFile(sourceFile);
// Print AST
return [luaAST, this.luaPrinter.print(luaAST, lualibFeatureSet, sourceFile.fileName)];
const [code, sourceMap] = this.luaPrinter.print(luaAST, lualibFeatureSet, sourceFile.fileName);
return [luaAST, code];
}

public reportDiagnostic(diagnostic: ts.Diagnostic): void {
Expand Down
37 changes: 25 additions & 12 deletions src/lualib/SourceMapTraceBack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,32 @@ declare const debug: {
traceback: (this: void, ...args: any[]) => string;
};

declare function getfenv(obj: any): {[key: string]: any};
type TraceBackFunction = (this: void, thread?: any, message?: string, level?: number) => string;

function __TS__SourceMapTraceBack(fileName: string, sourceMap: {[line: number]: number}): void {
getfenv(1)["traceback"] = getfenv(1)["traceback"] || {};
getfenv(1)["traceback"][fileName] = getfenv(1)["traceback"][fileName] || debug.traceback;
debug.traceback = (...args: any[]) => {
let trace = getfenv(1)["traceback"][fileName](...args);
declare const _G: {[key: string]: any} & {__TS__originalTraceback: TraceBackFunction};

const matches = string.gmatch(trace, `${fileName}.lua:(%d+)`);
for (const match in matches) {
trace = string.gsub(trace, `${fileName}.lua:${match}`, `${fileName}.ts:${sourceMap[match] || "??"}`);
}
// TODO: In the future, change this to __TS__RegisterFileInfo and provide tstl interface to
// get some metadata about transpilation.
function __TS__SourceMapTraceBack(this: void, fileName: string, sourceMap: {[line: number]: number}): void {
_G["__TS__sourcemap"] = _G["__TS__sourcemap"] || {};
_G["__TS__sourcemap"][fileName] = sourceMap;

return trace;
};
if (_G.__TS__originalTraceback === undefined) {
_G.__TS__originalTraceback = debug.traceback;
debug.traceback = (thread, message, level) => {
const trace = _G["__TS__originalTraceback"](thread, message, level);
const [result, occurrences] = string.gsub(
trace,
"(%S+).lua:(%d+)",
(file, line) => {
if (_G["__TS__sourcemap"][file + ".lua"] && _G["__TS__sourcemap"][file + ".lua"][line]) {
return `${file}.ts:${_G["__TS__sourcemap"][file + ".lua"][line]}`;
}
return `${file}.lua:${line}`;
}
);

return result;
};
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/** @luaIterator */
interface GMatchResult extends Iterable<string> { }
interface GMatchResult extends Array<string> { }

/** @noSelf */
declare namespace string {
/** @tupleReturn */
function gsub(source: string, searchValue: string, replaceValue: string): [string, number];
/** @tupleReturn */
function gsub(source: string, searchValue: string, replaceValue: (...groups: string[]) => string): [string, number];
Comment thread
tomblind marked this conversation as resolved.

function gmatch(haystack: string, pattern: string): GMatchResult;
}
1 change: 1 addition & 0 deletions test/unit/compiler/configuration/mixed/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ test("tsconfig.json mixed with cmd line args", () => {
noHeader: false,
project: tsConfigPath,
noHoisting: false,
sourceMapTraceback: false,
} as CompilerOptions);
} else {
expect(parsedArgs.isValid).toBeTruthy();
Expand Down
73 changes: 73 additions & 0 deletions test/unit/sourcemaps.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as util from "../util";
import { LuaLibImportKind } from "../../src/CompilerOptions";

test("sourceMapTraceback saves sourcemap in _G", () => {
const typeScriptSource = `
function abc() {
return "foo";
}
return JSONStringify(_G.__TS__sourcemap);`;

const options = {sourceMapTraceback: true, luaLibImport: LuaLibImportKind.Inline};

const transpiledLua = util.transpileString(typeScriptSource, options);

const sourceMapJson = util.transpileAndExecute(
typeScriptSource,
options,
undefined,
"declare const _G: {__TS__sourcemap: any};"
);

expect(sourceMapJson).toBeDefined();

const sourceMap = JSON.parse(sourceMapJson);

const sourceMapFiles = Object.keys(sourceMap);

expect(sourceMapFiles.length).toBe(1);
expect(sourceMap[sourceMapFiles[0]]).toBeDefined();

expectCorrectMapping(typeScriptSource, transpiledLua, sourceMap[sourceMapFiles[0]], [
["function abc()", "abc = function("],
["return \"foo\"", "return \"foo\""]
]);
});

// Helper functions

function expectCorrectMapping(
original: string,
lua: string,
sourceMap: {[line: string]: number},
patterns: Array<[string, string]>
): void {
for (const [tsPattern, luaPattern] of patterns) {
const originalLine = lineOf(original, "function abc()") + 1; // Add 1 for util-added header
const luaLine = lineOf(lua, "abc = function(");
const mappedLuaLine = sourceMap[luaLine.toString()];

expect(mappedLuaLine).toBe(originalLine);
}
}

// Find the line of the first occurrence of a pattern.
function lineOf(text: string, pattern: string): number {
const pos = text.indexOf(pattern);
if (pos === -1) {
return pos;
}

const lineLengths = text.split("\n").map(s => s.length);

let totalPos = 0;
for (let line = 1; line <= lineLengths.length; line++) {
// Add length of the line + 1 for the removed \n
totalPos += lineLengths[line - 1] + 1;
if (pos < totalPos) {
return line;
}
}

return -1;
}