diff --git a/build_lualib.ts b/build_lualib.ts index f968f5968..be376fef6 100644 --- a/build_lualib.ts +++ b/build_lualib.ts @@ -20,7 +20,7 @@ compile([ "./src/lualib", "--noHeader", "true", - ...glob.sync("./src/lualib/*.ts"), + ...glob.sync("./src/lualib/**/*.ts"), ]); if (fs.existsSync(bundlePath)) { diff --git a/package.json b/package.json index d38187f3c..76b1ec51a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/CommandLineParser.ts b/src/CommandLineParser.ts index d2fdc9f0d..50a1fa738 100644 --- a/src/CommandLineParser.ts +++ b/src/CommandLineParser.ts @@ -52,6 +52,11 @@ const optionDeclarations: {[key: string]: CLIOption} = { describe: "Disables hoisting.", type: "boolean", } as CLIOption, + sourceMapTraceback: { + default: false, + describe: "Applies the source map to show source TS files and lines in error tracebacks.", + type: "boolean", + } as CLIOption, }; export const { version } = require("../package.json"); diff --git a/src/CompilerOptions.ts b/src/CompilerOptions.ts index 24fae4374..49ad4eabf 100644 --- a/src/CompilerOptions.ts +++ b/src/CompilerOptions.ts @@ -5,6 +5,7 @@ export interface CompilerOptions extends ts.CompilerOptions { luaTarget?: LuaTarget; luaLibImport?: LuaLibImportKind; noHoisting?: boolean; + sourceMapTraceback?: boolean; } export enum LuaLibImportKind { diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 3db60196c..c5614bc10 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -34,6 +34,7 @@ export enum LuaLibFeature { Set = "Set", WeakMap = "WeakMap", WeakSet = "WeakSet", + SourceMapTraceBack = "SourceMapTraceBack", StringReplace = "StringReplace", StringSplit = "StringSplit", StringConcat = "StringConcat", diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index 1699b1e4d..8ea637c2a 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -50,50 +50,45 @@ export class LuaPrinter { this.currentIndent = ""; } - public print(block: tstl.Block, luaLibFeatures?: Set, sourceFile?: string): string { - if (this.options.inlineSourceMap === true) { - const rootSourceNode = this.printImplementation(block, luaLibFeatures, sourceFile); + public print(block: tstl.Block, luaLibFeatures?: Set, 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, - 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) { @@ -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( @@ -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 { diff --git a/src/LuaTranspiler.ts b/src/LuaTranspiler.ts index ee75331c4..9581ca4cc 100644 --- a/src/LuaTranspiler.ts +++ b/src/LuaTranspiler.ts @@ -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 { diff --git a/src/lualib/SourceMapTraceBack.ts b/src/lualib/SourceMapTraceBack.ts index 35478bf39..eb5215d64 100644 --- a/src/lualib/SourceMapTraceBack.ts +++ b/src/lualib/SourceMapTraceBack.ts @@ -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; + }; + } } diff --git a/src/lualib/string.d.ts b/src/lualib/declarations/string.d.ts similarity index 53% rename from src/lualib/string.d.ts rename to src/lualib/declarations/string.d.ts index 4361889b3..6e387692b 100644 --- a/src/lualib/string.d.ts +++ b/src/lualib/declarations/string.d.ts @@ -1,9 +1,12 @@ /** @luaIterator */ -interface GMatchResult extends Iterable { } +interface GMatchResult extends Array { } +/** @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]; function gmatch(haystack: string, pattern: string): GMatchResult; } diff --git a/test/unit/compiler/configuration/mixed/index.spec.ts b/test/unit/compiler/configuration/mixed/index.spec.ts index 119a91365..cb98a7d45 100644 --- a/test/unit/compiler/configuration/mixed/index.spec.ts +++ b/test/unit/compiler/configuration/mixed/index.spec.ts @@ -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(); diff --git a/test/unit/sourcemaps.spec.ts b/test/unit/sourcemaps.spec.ts new file mode 100644 index 000000000..88910e622 --- /dev/null +++ b/test/unit/sourcemaps.spec.ts @@ -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; +} \ No newline at end of file