Skip to content

Commit ceff8f2

Browse files
committed
Move sourcemap handling to chunk level
1 parent 68e7ffa commit ceff8f2

7 files changed

Lines changed: 172 additions & 197 deletions

File tree

src/LuaPrinter.ts

Lines changed: 18 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as path from "path";
2-
import { Mapping, SourceMapGenerator, SourceNode } from "source-map";
2+
import { SourceNode } from "source-map";
33
import * as ts from "typescript";
44
import { CompilerOptions, LuaLibImportKind } from "./CompilerOptions";
55
import * as lua from "./LuaAST";
66
import { loadLuaLibFeatures, LuaLibFeature } from "./LuaLib";
77
import { isValidLuaIdentifier } from "./transformation/utils/safe-names";
88
import { TranspilerHost } from "./transpilation";
9-
import { assert, intersperse, invertObject, normalizeSlashes, trimExtension } from "./utils";
9+
import { assert, intersperse, invertObject, normalizeSlashes } from "./utils";
1010

1111
// https://www.lua.org/pil/2.4.html
1212
// https://www.ecma-international.org/ecma-262/10.0/index.html#table-34
@@ -91,13 +91,7 @@ export type Printer = (
9191
fileName: string,
9292
block: lua.Block,
9393
luaLibFeatures: Set<LuaLibFeature>
94-
) => PrintResult;
95-
96-
export interface PrintResult {
97-
code: string;
98-
sourceMap: string;
99-
sourceMapNode: SourceNode;
100-
}
94+
) => SourceNode;
10195

10296
export class LuaPrinter {
10397
private static operatorMap: Record<lua.Operator, string> = {
@@ -129,7 +123,7 @@ export class LuaPrinter {
129123
};
130124

131125
private currentIndent = "";
132-
private sourceFile: string;
126+
private fileName: string;
133127
private options: CompilerOptions;
134128

135129
constructor(private host: TranspilerHost, program: ts.Program, fileName: string) {
@@ -139,81 +133,31 @@ export class LuaPrinter {
139133
const relativeFileName = path.relative(program.getCommonSourceDirectory(), fileName);
140134
if (this.options.sourceRoot) {
141135
// When sourceRoot is specified, just use relative path inside rootDir
142-
this.sourceFile = relativeFileName;
136+
this.fileName = relativeFileName;
143137
} else {
144138
// Calculate relative path from rootDir to outDir
145139
const outputPath = path.resolve(this.options.outDir, relativeFileName);
146-
this.sourceFile = path.relative(path.dirname(outputPath), fileName);
140+
this.fileName = path.relative(path.dirname(outputPath), fileName);
147141
}
148142
// We want forward slashes, even in windows
149-
this.sourceFile = normalizeSlashes(this.sourceFile);
143+
this.fileName = normalizeSlashes(this.fileName);
150144
} else {
151-
this.sourceFile = path.basename(fileName); // File will be in same dir as source
145+
this.fileName = path.basename(fileName); // File will be in same dir as source
152146
}
153147
}
154148

155-
public print(block: lua.Block, luaLibFeatures: Set<LuaLibFeature>): PrintResult {
156-
// Add traceback lualib if sourcemap traceback option is enabled
157-
if (this.options.sourceMapTraceback) {
158-
luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack);
159-
}
160-
161-
const sourceRoot = this.options.sourceRoot
162-
? // According to spec, sourceRoot is simply prepended to the source name, so the slash should be included
163-
this.options.sourceRoot.replace(/[\\/]+$/, "") + "/"
164-
: "";
165-
const rootSourceNode = this.printImplementation(block, luaLibFeatures);
166-
const sourceMap = this.buildSourceMap(sourceRoot, rootSourceNode);
167-
168-
let code = rootSourceNode.toString();
169-
170-
if (this.options.inlineSourceMap) {
171-
code += "\n" + this.printInlineSourceMap(sourceMap);
172-
}
173-
174-
if (this.options.sourceMapTraceback) {
175-
const stackTraceOverride = this.printStackTraceOverride(rootSourceNode);
176-
code = code.replace("{#SourceMapTraceback}", stackTraceOverride);
177-
}
178-
179-
return { code, sourceMap: sourceMap.toString(), sourceMapNode: rootSourceNode };
180-
}
181-
182-
private printInlineSourceMap(sourceMap: SourceMapGenerator): string {
183-
const map = sourceMap.toString();
184-
const base64Map = Buffer.from(map).toString("base64");
185-
186-
return `--# sourceMappingURL=data:application/json;base64,${base64Map}\n`;
187-
}
188-
189-
private printStackTraceOverride(rootNode: SourceNode): string {
190-
let currentLine = 1;
191-
const map: Record<number, number> = {};
192-
rootNode.walk((chunk, mappedPosition) => {
193-
if (mappedPosition.line !== undefined && mappedPosition.line > 0) {
194-
if (map[currentLine] === undefined) {
195-
map[currentLine] = mappedPosition.line;
196-
} else {
197-
map[currentLine] = Math.min(map[currentLine], mappedPosition.line);
198-
}
199-
}
200-
201-
currentLine += chunk.split("\n").length - 1;
202-
});
203-
204-
const mapItems = Object.entries(map).map(([line, original]) => `["${line}"] = ${original}`);
205-
const mapString = "{" + mapItems.join(",") + "}";
206-
207-
return `__TS__SourceMapTraceBack(debug.getinfo(1).short_src, ${mapString});`;
208-
}
209-
210-
private printImplementation(block: lua.Block, luaLibFeatures: Set<LuaLibFeature>): SourceNode {
149+
public print(block: lua.Block, luaLibFeatures: Set<LuaLibFeature>): SourceNode {
211150
let header = "";
212151

213152
if (!this.options.noHeader) {
214153
header += "--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]\n";
215154
}
216155

156+
// Add traceback lualib if sourcemap traceback option is enabled
157+
if (this.options.sourceMapTraceback) {
158+
luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack);
159+
}
160+
217161
const luaLibImport = this.options.luaLibImport ?? LuaLibImportKind.Require;
218162
if (
219163
luaLibImport === LuaLibImportKind.Always ||
@@ -252,12 +196,12 @@ export class LuaPrinter {
252196
const { line, column } = lua.getOriginalPos(node);
253197

254198
return line !== undefined && column !== undefined
255-
? new SourceNode(line + 1, column, this.sourceFile, chunks, name)
256-
: new SourceNode(null, null, this.sourceFile, chunks, name);
199+
? new SourceNode(line + 1, column, this.fileName, chunks, name)
200+
: new SourceNode(null, null, this.fileName, chunks, name);
257201
}
258202

259203
protected concatNodes(...chunks: SourceChunk[]): SourceNode {
260-
return new SourceNode(null, null, this.sourceFile, chunks);
204+
return new SourceNode(null, null, this.fileName, chunks);
261205
}
262206

263207
protected printBlock(block: lua.Block): SourceNode {
@@ -743,7 +687,7 @@ export class LuaPrinter {
743687
}
744688

745689
public printOperator(kind: lua.Operator): SourceNode {
746-
return new SourceNode(null, null, this.sourceFile, LuaPrinter.operatorMap[kind]);
690+
return new SourceNode(null, null, this.fileName, LuaPrinter.operatorMap[kind]);
747691
}
748692

749693
protected joinChunksWithComma(chunks: SourceChunk[]): SourceChunk[] {
@@ -768,67 +712,4 @@ export class LuaPrinter {
768712

769713
return chunks;
770714
}
771-
772-
// The key difference between this and SourceNode.toStringWithSourceMap() is that SourceNodes with null line/column
773-
// will not generate 'empty' mappings in the source map that point to nothing in the original TS.
774-
private buildSourceMap(sourceRoot: string, rootSourceNode: SourceNode): SourceMapGenerator {
775-
const map = new SourceMapGenerator({
776-
file: trimExtension(this.sourceFile) + ".lua",
777-
sourceRoot,
778-
});
779-
780-
let generatedLine = 1;
781-
let generatedColumn = 0;
782-
let currentMapping: Mapping | undefined;
783-
784-
const isNewMapping = (sourceNode: SourceNode) => {
785-
if (sourceNode.line === null) {
786-
return false;
787-
}
788-
if (currentMapping === undefined) {
789-
return true;
790-
}
791-
if (
792-
currentMapping.generated.line === generatedLine &&
793-
currentMapping.generated.column === generatedColumn &&
794-
currentMapping.name === sourceNode.name
795-
) {
796-
return false;
797-
}
798-
return (
799-
currentMapping.original.line !== sourceNode.line ||
800-
currentMapping.original.column !== sourceNode.column ||
801-
currentMapping.name !== sourceNode.name
802-
);
803-
};
804-
805-
const build = (sourceNode: SourceNode) => {
806-
if (isNewMapping(sourceNode)) {
807-
currentMapping = {
808-
source: sourceNode.source,
809-
original: { line: sourceNode.line, column: sourceNode.column },
810-
generated: { line: generatedLine, column: generatedColumn },
811-
name: sourceNode.name,
812-
};
813-
map.addMapping(currentMapping);
814-
}
815-
816-
for (const chunk of sourceNode.children) {
817-
if (typeof chunk === "string") {
818-
const lines = (chunk as string).split("\n");
819-
if (lines.length > 1) {
820-
generatedLine += lines.length - 1;
821-
generatedColumn = 0;
822-
currentMapping = undefined; // Mappings end at newlines
823-
}
824-
generatedColumn += lines[lines.length - 1].length;
825-
} else {
826-
build(chunk);
827-
}
828-
}
829-
};
830-
build(rootSourceNode);
831-
832-
return map;
833-
}
834715
}

src/transpilation/chunk.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ import { getConfigDirectory } from "./utils";
1111

1212
export interface Chunk {
1313
outputPath: string;
14-
code: string;
15-
sourceMap?: string;
14+
source: SourceNode;
1615
sourceFiles?: ts.SourceFile[];
1716
}
1817

1918
export function modulesToChunks(transpilation: Transpilation, modules: Module[]): Chunk[] {
2019
return modules.map(module => {
2120
const moduleId = transpilation.getModuleId(module);
2221
const outputPath = normalizeSlashes(path.resolve(transpilation.outDir, `${moduleId.replace(/\./g, "/")}.lua`));
23-
return { outputPath, code: module.code, sourceMap: module.sourceMap, sourceFiles: module.sourceFiles };
22+
return { outputPath, source: module.source, sourceFiles: module.sourceFiles };
2423
});
2524
}
2625

@@ -57,11 +56,11 @@ export function modulesToBundleChunks(transpilation: Transpilation, modules: Mod
5756
const entryModule = modules.find(m => m.request === entryFileName);
5857
if (entryModule === undefined) {
5958
transpilation.diagnostics.push(couldNotFindBundleEntryPoint(options.luaBundleEntry));
60-
return [{ outputPath, code: "" }];
59+
return [{ outputPath, source: new SourceNode() }];
6160
}
6261

6362
// For each file: ["<module path>"] = function() <lua content> end,
64-
const moduleTableEntries = modules.map(m => moduleSourceNode(m, escapeString(transpilation.getModuleId(m))));
63+
const moduleTableEntries = modules.map(m => moduleSourceNode(m, transpilation.getModuleId(m)));
6564

6665
// Create ____modules table containing all entries from moduleTableEntries
6766
const moduleTable = createModuleTableNode(moduleTableEntries);
@@ -70,20 +69,12 @@ export function modulesToBundleChunks(transpilation: Transpilation, modules: Mod
7069
const bootstrap = `return require(${escapeString(transpilation.getModuleId(entryModule))})\n`;
7170

7271
const bundleNode = joinSourceChunks([requireOverride, moduleTable, bootstrap]);
73-
const { code, map } = bundleNode.toStringWithSourceMap();
74-
75-
return [
76-
{
77-
outputPath,
78-
code,
79-
sourceMap: map.toString(),
80-
sourceFiles: modules.flatMap(x => x.sourceFiles ?? []),
81-
},
82-
];
72+
const sourceFiles = modules.flatMap(x => x.sourceFiles ?? []);
73+
return [{ outputPath, source: bundleNode, sourceFiles }];
8374
}
8475

85-
function moduleSourceNode(module: Module, modulePath: string): SourceNode {
86-
return joinSourceChunks([`[${modulePath}] = function()\n`, module.sourceMapNode ?? module.code, "\nend,\n"]);
76+
function moduleSourceNode(module: Module, moduleId: string): SourceNode {
77+
return joinSourceChunks([`[${escapeString(moduleId)}] = function()\n`, module.source, "\nend,\n"]);
8778
}
8879

8980
function createModuleTableNode(fileChunks: SourceChunk[]): SourceNode {

src/transpilation/module.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import { escapeString, unescapeLuaString } from "../LuaPrinter";
55
export interface Module {
66
request: string;
77
isBuilt: boolean;
8-
code: string;
9-
sourceMap?: string;
10-
sourceMapNode?: SourceNode;
8+
source: SourceNode;
119
sourceFiles?: ts.SourceFile[];
1210
}
1311

@@ -16,37 +14,29 @@ export type ModuleDependencyResolver = (request: string) => string | { error: st
1614
export function buildModule(module: Module, dependencyResolver: ModuleDependencyResolver) {
1715
if (module.isBuilt) return;
1816
module.isBuilt = true;
17+
replaceResolveRequests(module.source, dependencyResolver);
18+
}
1919

20-
if (module.sourceMapNode) {
21-
replaceResolveMacroSourceNodes(module.sourceMapNode, dependencyResolver);
22-
const { code, map } = module.sourceMapNode.toStringWithSourceMap();
23-
module.code = code;
24-
module.sourceMap = JSON.stringify(map.toJSON());
25-
} else {
26-
module.code = replaceResolveMacroInSource(module.code, dependencyResolver);
20+
function replaceResolveRequests(rootNode: SourceNode, dependencyResolver: ModuleDependencyResolver) {
21+
function replaceInString(source: string) {
22+
return source.replace(/__TS__Resolve\((".*?")\)/g, (_, match) => {
23+
const request = unescapeLuaString(match);
24+
const replacement = dependencyResolver(request);
25+
return typeof replacement === "string"
26+
? escapeString(replacement)
27+
: `--[[ ${request} ]] error(${escapeString(replacement.error)})`;
28+
});
2729
}
28-
}
2930

30-
function replaceResolveMacroSourceNodes(rootNode: SourceNode, replacer: ModuleDependencyResolver) {
3131
function walkSourceNode(node: SourceNode, parent: SourceNode) {
32-
for (const child of node.children) {
33-
if ((child as any) === "__TS__Resolve") {
34-
parent.children = [replaceResolveMacroInSource(parent.toString(), replacer) as any];
35-
} else if (typeof child === "object") {
32+
for (const child of node.children as Array<SourceNode | string>) {
33+
if (typeof child === "object") {
3634
walkSourceNode(child, node);
35+
} else if (child.includes("__TS__Resolve")) {
36+
parent.children = [replaceInString(parent.toString()) as any];
3737
}
3838
}
3939
}
4040

4141
walkSourceNode(rootNode, rootNode);
4242
}
43-
44-
function replaceResolveMacroInSource(source: string, replacer: ModuleDependencyResolver) {
45-
return source.replace(/__TS__Resolve\((".*?")\)/, (_, match) => {
46-
const request = unescapeLuaString(match);
47-
const replacement = replacer(request);
48-
return typeof replacement === "string"
49-
? escapeString(replacement)
50-
: `--[[ ${request} ]] error(${escapeString(replacement.error)})`;
51-
});
52-
}

0 commit comments

Comments
 (0)