Skip to content

Commit 01e24e4

Browse files
committed
Replace requires in source maps too
1 parent 3ff3b49 commit 01e24e4

11 files changed

Lines changed: 172 additions & 53 deletions

File tree

.eslintignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
/test/cli/errors
44
/test/cli/watch
55
/test/transpile/directories
6-
/test/transpile/module-resolution/node_modules
6+
/test/transpile/module-resolution/*/node_modules
77
/test/transpile/outFile

src/transpilation/bundle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export function getBundleResult(
8383

8484
function moduleSourceNode({ code, sourceMapNode }: ProcessedFile, modulePath: string): SourceNode {
8585
const tableEntryHead = `[${modulePath}] = function() `;
86-
const tableEntryTail = "end,\n";
86+
const tableEntryTail = " end,\n";
8787

8888
return joinSourceChunks([tableEntryHead, sourceMapNode ?? code, tableEntryTail]);
8989
}

src/transpilation/resolve.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as resolve from "enhanced-resolve";
33
import * as ts from "typescript";
44
import * as fs from "fs";
55
import { EmitHost, ProcessedFile } from "./utils";
6+
import { SourceNode } from "source-map";
67

78
const resolver = resolve.ResolverFactory.createResolver({
89
extensions: [".lua", ".ts"],
@@ -24,27 +25,33 @@ function resolveFileDependencies(file: ProcessedFile, rootDir: string, emitHost:
2425
const fileDir = path.dirname(file.fileName);
2526
const dependencies: ProcessedFile[] = [];
2627
for (const required of findRequiredPaths(file.code)) {
28+
// Try to resolve the import starting from the directory `file` is in
2729
const resolvedDependency = resolveDependency(fileDir, required);
2830
if (resolvedDependency) {
31+
// If dependency resolved successfully, read its content
2932
const dependencyContent = emitHost.readFile(resolvedDependency);
3033
if (dependencyContent === undefined) {
3134
throw `TODO: FAILED TO READ ${resolvedDependency}`;
3235
}
3336

34-
let relativePath = path.relative(fileDir, resolvedDependency);
35-
let outPath = resolvedDependency;
36-
if (relativePath.includes("..")) {
37-
relativePath = path.relative(rootDir, resolvedDependency);
38-
outPath = path.join(fileDir, relativePath);
37+
// Figure out resolved require path and dependency output path
38+
let resolvedRequire = path.relative(fileDir, resolvedDependency);
39+
let dependencyOutPath = resolvedDependency;
40+
if (resolvedRequire.includes("..")) {
41+
// If the resolved require includes a parent, copy the dependency to a new path
42+
// to avoid require paths with parent directories
43+
resolvedRequire = path.relative(rootDir, resolvedDependency);
44+
dependencyOutPath = path.join(fileDir, resolvedRequire);
3945
}
40-
const requirePath = relativePath.replace(".lua", "").replace(/\\/g, ".");
41-
file.code = file.code.replace(`require("${required}")`, `require("${requirePath}")`);
4246

47+
replaceRequireInCode(file, required, resolvedRequire);
48+
replaceRequireInSourceMap(file, required, resolvedRequire);
49+
50+
// Add dependency to output and resolve its dependencies recursively
4351
const dependency = {
44-
fileName: outPath,
52+
fileName: dependencyOutPath,
4553
code: dependencyContent,
4654
};
47-
4855
dependencies.push(dependency, ...resolveFileDependencies(dependency, rootDir, emitHost));
4956
} else {
5057
//throw `TODO: COULD NOT RESOLVE ${required}`;
@@ -53,7 +60,37 @@ function resolveFileDependencies(file: ProcessedFile, rootDir: string, emitHost:
5360
return dependencies;
5461
}
5562

63+
function replaceRequireInCode(file: ProcessedFile, originalRequire: string, newRequire: string) {
64+
const requirePath = newRequire.replace(".lua", "").replace(/\\/g, ".");
65+
file.code = file.code.replace(`require("${originalRequire}")`, `require("${requirePath}")`);
66+
}
67+
68+
function replaceRequireInSourceMap(file: ProcessedFile, originalRequire: string, newRequire: string) {
69+
const requirePath = newRequire.replace(".lua", "").replace(/\\/g, ".");
70+
if (file.sourceMapNode) {
71+
replaceInSourceMap(file.sourceMapNode, file.sourceMapNode, `"${originalRequire}"`, `"${requirePath}"`);
72+
}
73+
}
74+
75+
function replaceInSourceMap(node: SourceNode, parent: SourceNode, require: string, resolvedRequire: string): boolean {
76+
if ((!node.children || node.children.length === 0) && node.toString() === require) {
77+
parent.children = [new SourceNode(node.line, node.column, node.source, [resolvedRequire])];
78+
return true; // Stop after finding the first occurrence
79+
}
80+
81+
if (node.children) {
82+
for (const c of node.children) {
83+
if (replaceInSourceMap(c, node, require, resolvedRequire)) {
84+
return true; // Occurrence found in one of the children
85+
}
86+
}
87+
}
88+
89+
return false; // Did not find the require
90+
}
91+
5692
function findRequiredPaths(code: string): string[] {
93+
// Find all require("<path>") paths in the code
5794
const paths: string[] = [];
5895
const pattern = /require\("(.+)"\)/g;
5996
// eslint-disable-next-line @typescript-eslint/ban-types
@@ -72,7 +109,7 @@ function resolveDependency(fromDirectory: string, dependency: string): string |
72109
return resolveResult;
73110
}
74111
} catch {
75-
// TODO
112+
// resolveSync errors if it fails to resolve
76113
}
77114

78115
return undefined;

src/transpilation/transpiler.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ export class Transpiler {
3535
emitOptions
3636
);
3737

38-
const resolvedFiles = resolveDependencies(program, freshFiles, this.emitHost);
39-
40-
const { emitPlan } = this.getEmitPlan(program, diagnostics, resolvedFiles);
38+
const { emitPlan } = this.getEmitPlan(program, diagnostics, freshFiles);
4139

4240
const options = program.getCompilerOptions();
4341
const emitBOM = options.emitBOM ?? false;
@@ -66,13 +64,16 @@ export class Transpiler {
6664
files.unshift({ fileName, code: getLuaLibBundle(this.emitHost) });
6765
}
6866

67+
// Resolve imported modules and modify output Lua
68+
const resolvedFiles = resolveDependencies(program, files, this.emitHost);
69+
6970
let emitPlan: EmitFile[];
7071
if (isBundleEnabled(options)) {
71-
const [bundleDiagnostics, bundleFile] = getBundleResult(program, this.emitHost, files);
72+
const [bundleDiagnostics, bundleFile] = getBundleResult(program, this.emitHost, resolvedFiles);
7273
diagnostics.push(...bundleDiagnostics);
7374
emitPlan = [bundleFile];
7475
} else {
75-
emitPlan = files.map(file => {
76+
emitPlan = resolvedFiles.map(file => {
7677
const pathInOutDir = path.resolve(outDir, path.relative(rootDir, file.fileName));
7778
const outputPath = normalizeSlashes(trimExtension(pathInOutDir) + ".lua");
7879
return { ...file, outputPath };
Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,102 @@
11
import * as path from "path";
22
import * as util from "../util";
33

4-
const projectPath = path.resolve(__dirname, "module-resolution", "project-with-node-modules");
4+
describe("basic module resolution", () => {
5+
const projectPath = path.resolve(__dirname, "module-resolution", "project-with-node-modules");
56

6-
const projectWithNodeModules = util
7-
.testProject(path.join(projectPath, "tsconfig.json"))
8-
.setMainFileName(path.join(projectPath, "main.ts"));
7+
const projectWithNodeModules = util
8+
.testProject(path.join(projectPath, "tsconfig.json"))
9+
.setMainFileName(path.join(projectPath, "main.ts"));
910

10-
test("can resolve global dependencies with declarations", () => {
11-
// Declarations in the node_modules directory
12-
expect(projectWithNodeModules.getLuaExecutionResult().globalWithDeclarationsResults).toEqual({
13-
foo: "foo from lua global with decls",
14-
bar: "bar from lua global with decls: global with declarations!",
15-
baz: "baz from lua global with decls",
11+
test("can resolve global dependencies with declarations", () => {
12+
// Declarations in the node_modules directory
13+
expect(projectWithNodeModules.getLuaExecutionResult().globalWithDeclarationsResults).toEqual({
14+
foo: "foo from lua global with decls",
15+
bar: "bar from lua global with decls: global with declarations!",
16+
baz: "baz from lua global with decls",
17+
});
1618
});
17-
});
1819

19-
test("can resolve global dependencies with hand-written declarations", () => {
20-
// No declarations in the node_modules directory, but written by hand in project dir
21-
expect(projectWithNodeModules.getLuaExecutionResult().globalWithoutDeclarationsResults).toEqual({
22-
foo: "foo from lua global without decls",
23-
bar: "bar from lua global without decls: global without declarations!",
24-
baz: "baz from lua global without decls",
20+
test("can resolve global dependencies with hand-written declarations", () => {
21+
// No declarations in the node_modules directory, but written by hand in project dir
22+
expect(projectWithNodeModules.getLuaExecutionResult().globalWithoutDeclarationsResults).toEqual({
23+
foo: "foo from lua global without decls",
24+
bar: "bar from lua global without decls: global without declarations!",
25+
baz: "baz from lua global without decls",
26+
});
2527
});
26-
});
2728

28-
test("can resolve module dependencies with declarations", () => {
29-
// Declarations in the node_modules directory
30-
expect(projectWithNodeModules.getLuaExecutionResult().moduleWithDeclarationsResults).toEqual({
31-
foo: "foo from lua module with decls",
32-
bar: "bar from lua module with decls: module with declarations!",
33-
baz: "baz from lua module with decls",
29+
test("can resolve module dependencies with declarations", () => {
30+
// Declarations in the node_modules directory
31+
expect(projectWithNodeModules.getLuaExecutionResult().moduleWithDeclarationsResults).toEqual({
32+
foo: "foo from lua module with decls",
33+
bar: "bar from lua module with decls: module with declarations!",
34+
baz: "baz from lua module with decls",
35+
});
3436
});
35-
});
3637

37-
test("can resolve module dependencies with hand-written declarations", () => {
38-
// Declarations in the node_modules directory
39-
expect(projectWithNodeModules.getLuaExecutionResult().moduleWithoutDeclarationsResults).toEqual({
40-
foo: "foo from lua module without decls",
41-
bar: "bar from lua module without decls: module without declarations!",
42-
baz: "baz from lua module without decls",
38+
test("can resolve module dependencies with hand-written declarations", () => {
39+
// Declarations in the node_modules directory
40+
expect(projectWithNodeModules.getLuaExecutionResult().moduleWithoutDeclarationsResults).toEqual({
41+
foo: "foo from lua module without decls",
42+
bar: "bar from lua module without decls: module without declarations!",
43+
baz: "baz from lua module without decls",
44+
});
45+
});
46+
47+
test("can resolve package depencency with a dependency on another package", () => {
48+
// Declarations in the node_modules directory
49+
expect(projectWithNodeModules.getLuaExecutionResult().moduleWithDependencyResult).toEqual(
50+
"Calling dependency: foo from lua module with decls"
51+
);
52+
});
53+
54+
test("resolved package dependency included in bundle", () => {
55+
const mainFile = path.join(projectPath, "main.ts");
56+
util.testProject(path.join(projectPath, "tsconfig.json"))
57+
.setMainFileName(mainFile)
58+
.setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile })
59+
.expectToEqual({
60+
globalWithDeclarationsResults: {
61+
foo: "foo from lua global with decls",
62+
bar: "bar from lua global with decls: global with declarations!",
63+
baz: "baz from lua global with decls",
64+
},
65+
globalWithoutDeclarationsResults: {
66+
foo: "foo from lua global without decls",
67+
bar: "bar from lua global without decls: global without declarations!",
68+
baz: "baz from lua global without decls",
69+
},
70+
moduleWithDeclarationsResults: {
71+
foo: "foo from lua module with decls",
72+
bar: "bar from lua module with decls: module with declarations!",
73+
baz: "baz from lua module with decls",
74+
},
75+
moduleWithDependencyResult: "Calling dependency: foo from lua module with decls",
76+
moduleWithoutDeclarationsResults: {
77+
foo: "foo from lua module without decls",
78+
bar: "bar from lua module without decls: module without declarations!",
79+
baz: "baz from lua module without decls",
80+
},
81+
});
4382
});
4483
});
4584

46-
test("can resolve package depencency with a dependency on another package", () => {
47-
// Declarations in the node_modules directory
48-
expect(projectWithNodeModules.getLuaExecutionResult().moduleWithDependencyResult).toEqual(
49-
"Calling dependency: foo from lua module with decls"
50-
);
85+
describe("module resolution with chained dependencies", () => {
86+
const projectPath = path.resolve(__dirname, "module-resolution", "project-with-dependency-chain");
87+
88+
test("can resolve dependencies in chain", () => {
89+
util.testProject(path.join(projectPath, "tsconfig.json"))
90+
.setMainFileName(path.join(projectPath, "main.ts"))
91+
.expectToEqual({});
92+
});
93+
94+
test.only("resolved package dependency included in bundle", () => {
95+
const mainFile = path.join(projectPath, "main.ts");
96+
util.testProject(path.join(projectPath, "tsconfig.json"))
97+
.setMainFileName(mainFile)
98+
.setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile })
99+
.debug()
100+
.expectToEqual({});
101+
});
51102
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import * as dependency1 from "dependency1";
2+
3+
export const result = dependency1.f1();

test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.d.ts

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.lua

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency2/index.lua

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency3/index.lua

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)