Skip to content

Commit 737e266

Browse files
committed
Get all module-resolution testcases to work
1 parent 73a350a commit 737e266

File tree

6 files changed

+116
-81
lines changed

6 files changed

+116
-81
lines changed

src/transformation/visitors/modules/import.ts

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as path from "path";
22
import * as ts from "typescript";
33
import * as lua from "../../../LuaAST";
4-
import { formatPathToLuaPath } from "../../../utils";
54
import { FunctionVisitor, TransformationContext } from "../../context";
65
import { AnnotationKind, getSymbolAnnotations } from "../../utils/annotations";
76
import { createDefaultExportStringLiteral } from "../../utils/export";
@@ -10,36 +9,13 @@ import { createSafeName } from "../../utils/safe-names";
109
import { peekScope } from "../../utils/scope";
1110
import { transformIdentifier } from "../identifier";
1211
import { transformPropertyName } from "../literal";
13-
import { unresolvableRequirePath } from "../../utils/diagnostics";
1412

15-
const getAbsoluteImportPath = (relativePath: string, directoryPath: string, options: ts.CompilerOptions): string =>
16-
!relativePath.startsWith(".") && options.baseUrl
17-
? path.resolve(options.baseUrl, relativePath)
18-
: path.resolve(directoryPath, relativePath);
19-
20-
function getImportPath(context: TransformationContext, relativePath: string, node: ts.Node): string {
21-
const { options, sourceFile } = context;
22-
const { fileName } = sourceFile;
23-
const rootDir = options.rootDir ? path.resolve(options.rootDir) : path.resolve(".");
24-
25-
const absoluteImportPath = path.format(
26-
path.parse(getAbsoluteImportPath(relativePath, path.dirname(fileName), options))
27-
);
28-
const absoluteRootDirPath = path.format(path.parse(rootDir));
29-
if (absoluteImportPath.includes(absoluteRootDirPath)) {
30-
return formatPathToLuaPath(absoluteImportPath.replace(absoluteRootDirPath, "").slice(1));
31-
} else {
32-
context.diagnostics.push(unresolvableRequirePath(node, relativePath));
33-
return relativePath;
34-
}
35-
}
36-
37-
function shouldResolveModulePath(context: TransformationContext, moduleSpecifier: ts.Expression): boolean {
13+
function isNoResolutionPath(context: TransformationContext, moduleSpecifier: ts.Expression): boolean {
3814
const moduleOwnerSymbol = context.checker.getSymbolAtLocation(moduleSpecifier);
39-
if (!moduleOwnerSymbol) return true;
15+
if (!moduleOwnerSymbol) return false;
4016

4117
const annotations = getSymbolAnnotations(moduleOwnerSymbol);
42-
return !annotations.has(AnnotationKind.NoResolution);
18+
return annotations.has(AnnotationKind.NoResolution);
4319
}
4420

4521
export function createModuleRequire(
@@ -49,8 +25,8 @@ export function createModuleRequire(
4925
): lua.CallExpression {
5026
const params: lua.Expression[] = [];
5127
if (ts.isStringLiteral(moduleSpecifier)) {
52-
const modulePath = shouldResolveModulePath(context, moduleSpecifier)
53-
? getImportPath(context, moduleSpecifier.text.replace(/"/g, ""), moduleSpecifier)
28+
const modulePath = isNoResolutionPath(context, moduleSpecifier)
29+
? `@NoResolution:${moduleSpecifier.text}`
5430
: moduleSpecifier.text;
5531

5632
params.push(lua.createStringLiteral(modulePath));

src/transpilation/bundle.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { CompilerOptions } from "../CompilerOptions";
55
import { escapeString } from "../LuaPrinter";
66
import { cast, formatPathToLuaPath, isNonNull, normalizeSlashes, trimExtension } from "../utils";
77
import { couldNotFindBundleEntryPoint } from "./diagnostics";
8-
import { EmitFile, EmitHost, ProcessedFile } from "./utils";
8+
import { getEmitOutDir, getEmitPathRelativeToOutDir, getSourceDir } from "./transpiler";
9+
import { EmitFile, ProcessedFile } from "./utils";
910

10-
const createModulePath = (baseDir: string, pathToResolve: string) =>
11-
escapeString(formatPathToLuaPath(trimExtension(path.relative(baseDir, pathToResolve))));
11+
const createModulePath = (pathToResolve: string, program: ts.Program) =>
12+
escapeString(formatPathToLuaPath(trimExtension(getEmitPathRelativeToOutDir(pathToResolve, program))));
1213

1314
// Override `require` to read from ____modules table.
1415
const requireOverride = `
@@ -34,7 +35,6 @@ end
3435

3536
export function getBundleResult(
3637
program: ts.Program,
37-
emitHost: EmitHost,
3838
files: ProcessedFile[]
3939
): [ts.Diagnostic[], EmitFile] {
4040
const diagnostics: ts.Diagnostic[] = [];
@@ -43,29 +43,23 @@ export function getBundleResult(
4343
const bundleFile = cast(options.luaBundle, isNonNull);
4444
const entryModule = cast(options.luaBundleEntry, isNonNull);
4545

46-
const rootDir = program.getCommonSourceDirectory();
47-
const outDir = options.outDir ?? rootDir;
48-
const projectRootDir = options.configFilePath
49-
? path.dirname(options.configFilePath)
50-
: emitHost.getCurrentDirectory();
51-
5246
// Resolve project settings relative to project file.
53-
const resolvedEntryModule = path.resolve(projectRootDir, entryModule);
54-
const outputPath = normalizeSlashes(path.resolve(projectRootDir, bundleFile));
47+
const resolvedEntryModule = path.resolve(getSourceDir(program), entryModule);
48+
const outputPath = normalizeSlashes(path.resolve(getEmitOutDir(program), bundleFile));
5549

5650
if (!files.some(f => f.fileName === resolvedEntryModule)) {
5751
diagnostics.push(couldNotFindBundleEntryPoint(entryModule));
5852
return [diagnostics, { outputPath, code: "" }];
5953
}
6054

6155
// For each file: ["<module path>"] = function() <lua content> end,
62-
const moduleTableEntries = files.map(f => moduleSourceNode(f, createModulePath(outDir, f.fileName)));
56+
const moduleTableEntries = files.map(f => moduleSourceNode(f, createModulePath(f.fileName, program)));
6357

6458
// Create ____modules table containing all entries from moduleTableEntries
6559
const moduleTable = createModuleTableNode(moduleTableEntries);
6660

6761
// return require("<entry module path>")
68-
const entryPoint = `return require(${createModulePath(outDir, resolvedEntryModule)})\n`;
62+
const entryPoint = `return require(${createModulePath(resolvedEntryModule, program)})\n`;
6963

7064
const bundleNode = joinSourceChunks([requireOverride, moduleTable, entryPoint]);
7165
const { code, map } = bundleNode.toStringWithSourceMap();

src/transpilation/resolve.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ts from "typescript";
44
import * as fs from "fs";
55
import { EmitHost, ProcessedFile } from "./utils";
66
import { SourceNode } from "source-map";
7+
import { getEmitPathRelativeToOutDir, getSourceDir } from "./transpiler";
78

89
const resolver = resolve.ResolverFactory.createResolver({
910
extensions: [".lua"],
@@ -15,19 +16,28 @@ export function resolveDependencies(program: ts.Program, files: ProcessedFile[],
1516
const outFiles = [];
1617

1718
for (const file of files) {
18-
outFiles.push(file, ...resolveFileDependencies(file, program.getCompilerOptions().rootDir ?? program.getCommonSourceDirectory(), emitHost));
19+
outFiles.push(file, ...resolveFileDependencies(file, program, emitHost));
1920
}
2021

2122
return outFiles;
2223
}
2324

24-
function resolveFileDependencies(file: ProcessedFile, projectRootDir: string, emitHost: EmitHost): ProcessedFile[] {
25+
function resolveFileDependencies(file: ProcessedFile, program: ts.Program, emitHost: EmitHost): ProcessedFile[] {
26+
const projectRootDir = getSourceDir(program);
2527
const dependencies: ProcessedFile[] = [];
2628
for (const required of findRequiredPaths(file.code)) {
2729
// Do no resolve lualib
2830
if (required === "lualib_bundle") {
2931
continue;
3032
}
33+
34+
// Do not resolve noResolution paths
35+
if (required.startsWith("@NoResolution:")) {
36+
const path = required.replace("@NoResolution:", "")
37+
replaceRequireInCode(file, required, path);
38+
replaceRequireInSourceMap(file, required, path);
39+
continue;
40+
}
3141

3242
// Try to resolve the import starting from the directory `file` is in
3343
const fileDir = path.dirname(file.fileName);
@@ -39,20 +49,21 @@ function resolveFileDependencies(file: ProcessedFile, projectRootDir: string, em
3949
throw `TODO: FAILED TO READ ${resolvedDependency}`;
4050
}
4151

42-
// Figure out resolved require path and dependency output path
43-
const resolvedRequire = path.relative(projectRootDir, resolvedDependency);
52+
// Figure out resolved require path and dependency output path
53+
const resolvedRequire = getEmitPathRelativeToOutDir(resolvedDependency, program);
4454

4555
replaceRequireInCode(file, required, resolvedRequire);
4656
replaceRequireInSourceMap(file, required, resolvedRequire);
4757

48-
// Add dependency to output and resolve its dependencies recursively
49-
const dependency = {
50-
fileName: resolvedDependency,
51-
code: dependencyContent,
52-
};
53-
dependencies.push(dependency, ...resolveFileDependencies(dependency, projectRootDir, emitHost));
58+
// If dependency is not part of sources, add dependency to output and resolve its dependencies recursively
59+
if (!program.getSourceFile(resolvedDependency)) {
60+
const dependency = {
61+
fileName: resolvedDependency,
62+
code: dependencyContent,
63+
};
64+
dependencies.push(dependency, ...resolveFileDependencies(dependency, program, emitHost));
65+
}
5466
} else {
55-
//throw `TODO: COULD NOT RESOLVE ${required}`;
5667
console.error(`Failed to resolve ${required} referenced in ${file.fileName}.`);
5768
console.error(projectRootDir);
5869
}
@@ -103,14 +114,16 @@ function findRequiredPaths(code: string): string[] {
103114
}
104115

105116
function resolveDependency(fileDirectory: string, rootDirectory: string, dependency: string, emitHost: EmitHost): string | undefined {
106-
// Check if
107-
const dependencyPath = dependency.replace(".", "/");
108-
const projectFilePath = path.join(fileDirectory, dependencyPath + ".ts");
109-
if (emitHost.fileExists(projectFilePath)) {
110-
return projectFilePath;
117+
// Check if file is a TS file in the project
118+
const dependencyPath = dependency;
119+
const resolvedPath = path.resolve(fileDirectory, dependencyPath);
120+
const resolvedFile = resolvedPath + ".ts";
121+
122+
if (emitHost.fileExists(resolvedFile)) {
123+
return resolvedPath + ".ts";
111124
}
112125

113-
const projectIndexPath = path.join(fileDirectory, dependencyPath, "index.ts");
126+
const projectIndexPath = path.resolve(fileDirectory, dependencyPath, "index.ts");
114127
if (emitHost.fileExists(projectIndexPath)) {
115128
return projectIndexPath;
116129
}

src/transpilation/transpiler.ts

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,31 +55,74 @@ export class Transpiler {
5555
files: ProcessedFile[]
5656
): { emitPlan: EmitFile[] } {
5757
const options = program.getCompilerOptions();
58-
const rootDir = program.getCommonSourceDirectory();
59-
const outDir = options.outDir ?? rootDir;
6058

6159
const lualibRequired = files.some(f => f.code.includes('require("lualib_bundle")'));
6260
if (lualibRequired) {
63-
const fileName = normalizeSlashes(path.resolve(rootDir, "lualib_bundle.lua"));
61+
const fileName = normalizeSlashes(path.resolve(getEmitOutDir(program), "lualib_bundle.lua"));
6462
files.unshift({ fileName, code: getLuaLibBundle(this.emitHost) });
6563
}
6664

67-
// Resolve imported modules and modify output Lua
65+
// Resolve imported modules and modify output Lua requires
6866
const resolvedFiles = resolveDependencies(program, files, this.emitHost);
6967

7068
let emitPlan: EmitFile[];
7169
if (isBundleEnabled(options)) {
72-
const [bundleDiagnostics, bundleFile] = getBundleResult(program, this.emitHost, resolvedFiles);
70+
const [bundleDiagnostics, bundleFile] = getBundleResult(program, resolvedFiles);
7371
diagnostics.push(...bundleDiagnostics);
7472
emitPlan = [bundleFile];
7573
} else {
76-
emitPlan = resolvedFiles.map(file => {
77-
const pathInOutDir = path.resolve(outDir, path.relative(rootDir, file.fileName));
78-
const outputPath = normalizeSlashes(trimExtension(pathInOutDir) + ".lua");
79-
return { ...file, outputPath };
80-
});
74+
emitPlan = resolvedFiles.map(file => ({ ...file, outputPath: getEmitPath(file.fileName, program) }));
8175
}
8276

8377
return { emitPlan };
8478
}
8579
}
80+
81+
export function getEmitPath(file: string, program: ts.Program): string {
82+
const relativeOutputPath = getEmitPathRelativeToOutDir(file, program);
83+
const outDir = getEmitOutDir(program);
84+
85+
return path.join(outDir, relativeOutputPath);
86+
}
87+
88+
export function getEmitPathRelativeToOutDir(fileName: string, program: ts.Program): string {
89+
const sourceDir = getSourceDir(program);
90+
// Default output path is relative path in source dir
91+
let emitPath = path.relative(sourceDir, fileName).split(path.sep);
92+
93+
// If source is in a parent directory of source dir, move it into the source dir
94+
emitPath = emitPath.filter(s => s !== "..");
95+
96+
// To avoid overwriting lua sources in node_modules, emit into lua_modules
97+
if (emitPath[0] === "node_modules") {
98+
emitPath[0] = "lua_modules";
99+
}
100+
101+
// Make extension lua
102+
emitPath[emitPath.length - 1] = trimExtension(emitPath[emitPath.length - 1]) + ".lua";
103+
104+
return path.join(...emitPath);
105+
}
106+
107+
export function getSourceDir(program: ts.Program): string {
108+
const rootDir = program.getCompilerOptions().rootDir;
109+
if (rootDir && rootDir.length > 0) {
110+
return path.isAbsolute(rootDir) ? rootDir : path.resolve(getProjectRoot(program), rootDir);
111+
}
112+
return program.getCommonSourceDirectory();
113+
}
114+
115+
export function getEmitOutDir(program: ts.Program): string {
116+
const outDir = program.getCompilerOptions().outDir;
117+
if (outDir && outDir.length > 0) {
118+
return path.isAbsolute(outDir) ? outDir : path.resolve(getProjectRoot(program), outDir);
119+
}
120+
return program.getCommonSourceDirectory();
121+
}
122+
123+
export function getProjectRoot(program: ts.Program): string {
124+
// Try to get the directory the tsconfig is in
125+
const tsConfigPath = program.getCompilerOptions().configFilePath;
126+
// If no tsconfig is known, use common source directory
127+
return tsConfigPath ? path.dirname(tsConfigPath) : program.getCommonSourceDirectory();
128+
}

test/transpile/module-resolution.spec.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,26 +104,34 @@ describe("module resolution with outDir", () => {
104104
const projectPath = path.resolve(__dirname, "module-resolution", "project-with-dependency-chain");
105105

106106
test("emits files in outDir", () => {
107-
const builder = util.testProject(path.join(projectPath, "tsconfig.json"))
107+
const builder = util
108+
.testProject(path.join(projectPath, "tsconfig.json"))
108109
.setMainFileName(path.join(projectPath, "main.ts"))
109110
.setOptions({ outDir: "tstl-out" })
110111
.expectToEqual({ result: "dependency3" });
111112

112113
// Get the output paths relative to the project path
113114
const outPaths = builder.getLuaResult().transpiledFiles.map(f => path.relative(projectPath, f.outPath));
114115
expect(outPaths).toHaveLength(4);
115-
expect(outPaths).toContain("tstl-out/main.lua");
116-
expect(outPaths).toContain("tstl-out/node_modules/dependency1/index.lua");
117-
expect(outPaths).toContain("tstl-out/node_modules/dependency2/index.lua");
118-
expect(outPaths).toContain("tstl-out/node_modules/dependency3/index.lua");
116+
expect(outPaths).toContain(path.join("tstl-out", "main.lua"));
117+
// Note: outputs to lua_modules
118+
expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency1", "index.lua"));
119+
expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency2", "index.lua"));
120+
expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency3", "index.lua"));
119121
});
120122

121123
test("emits bundle in outDir", () => {
122124
const mainFile = path.join(projectPath, "main.ts");
123-
const builder = util.testProject(path.join(projectPath, "tsconfig.json"))
125+
const builder = util
126+
.testProject(path.join(projectPath, "tsconfig.json"))
124127
.setMainFileName(mainFile)
125-
.setOptions({ luaBundle: "tstl-out/bundle.lua", luaBundleEntry: mainFile })
128+
.setOptions({ outDir: "tstl-out", luaBundle: "bundle.lua", luaBundleEntry: mainFile })
126129
.expectToEqual({ result: "dependency3" });
130+
131+
// Get the output paths relative to the project path
132+
const outPaths = builder.getLuaResult().transpiledFiles.map(f => path.relative(projectPath, f.outPath));
133+
expect(outPaths).toHaveLength(1);
134+
expect(outPaths).toContain(path.join("tstl-out", "bundle.lua"));
127135
});
128136
});
129137

@@ -134,14 +142,14 @@ describe("module resolution with sourceDir", () => {
134142
util.testProject(path.join(projectPath, "tsconfig.json"))
135143
.setMainFileName(path.join(projectPath, "src", "main.ts"))
136144
.setOptions({ outDir: "tstl-out" })
137-
.expectToEqual({ result: "dependency3" });
145+
.expectToEqual({ result: "dependency3", result2: "non-node_modules import" });
138146
});
139147

140148
test("can resolve dependencies and bundle files with sourceDir", () => {
141149
const mainFile = path.join(projectPath, "src", "main.ts");
142150
util.testProject(path.join(projectPath, "tsconfig.json"))
143151
.setMainFileName(mainFile)
144152
.setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile })
145-
.expectToEqual({ result: "dependency3" });
153+
.expectToEqual({ result: "dependency3", result2: "non-node_modules import" });
146154
});
147155
});

test/util.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as ts from "typescript";
99
import * as vm from "vm";
1010
import * as tstl from "../src";
1111
import { createEmitOutputCollector } from "../src/transpilation/output-collector";
12-
import { transpileProject } from "../src";
12+
import { getEmitOutDir, transpileProject } from "../src";
1313
import { normalizeSlashes } from "../src/utils";
1414

1515
const jsonLib = fs.readFileSync(path.join(__dirname, "json.lua"), "utf8");
@@ -385,7 +385,7 @@ export abstract class TestBuilder {
385385
const { transpiledFiles } = this.getLuaResult();
386386
for (const transpiledFile of transpiledFiles) {
387387
if (transpiledFile.lua) {
388-
const filePath = path.relative(this.options.outDir ?? this.getProgram().getCommonSourceDirectory(), transpiledFile.outPath);
388+
const filePath = path.relative(getEmitOutDir(this.getProgram()), transpiledFile.outPath);
389389
this.packagePreloadLuaFile(L, lua, lauxlib, filePath, transpiledFile.lua);
390390
}
391391
}
@@ -544,6 +544,7 @@ class ExpressionTestBuilder extends AccessorTestBuilder {
544544
class ProjectTestBuilder extends ModuleTestBuilder {
545545
constructor(private tsConfig: string) {
546546
super("");
547+
this.setOptions({ configFilePath: this.tsConfig });
547548
}
548549

549550
@memoize

0 commit comments

Comments
 (0)