Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/transpilation/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,9 @@ export const unsupportedJsxEmit = createDiagnosticFactory(() => 'JSX is only sup
export const pathsWithoutBaseUrl = createDiagnosticFactory(
() => "When configuring 'paths' in tsconfig.json, the option 'baseUrl' must also be provided."
);

export const emitPathCollision = createDiagnosticFactory(
(outputPath: string, file1: string, file2: string) =>
`Output path '${outputPath}' is used by both '${file1}' and '${file2}'. ` +
`Dots in file/directory names are expanded to nested directories for Lua module resolution.`
);
28 changes: 22 additions & 6 deletions src/transpilation/transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CompilerOptions, isBundleEnabled, LuaLibImportKind, LuaTarget } from ".
import { buildMinimalLualibBundle, findUsedLualibFeatures, getLuaLibBundle } from "../LuaLib";
import { normalizeSlashes, trimExtension } from "../utils";
import { getBundleResult } from "./bundle";
import { emitPathCollision } from "./diagnostics";
import { getPlugins, Plugin } from "./plugins";
import { resolveDependencies } from "./resolve";
import { getProgramTranspileResult, TranspileOptions } from "./transpile";
Expand Down Expand Up @@ -143,10 +144,18 @@ export class Transpiler {
diagnostics.push(...bundleDiagnostics);
emitPlan = [bundleFile];
} else {
emitPlan = resolutionResult.resolvedFiles.map(file => ({
...file,
outputPath: getEmitPath(file.fileName, program),
}));
// Check for output path collisions caused by dot expansion
const outputPathMap = new Map<string, string>();
emitPlan = resolutionResult.resolvedFiles.map(file => {
const outputPath = getEmitPath(file.fileName, program);
const existing = outputPathMap.get(outputPath);
if (existing) {
diagnostics.push(emitPathCollision(outputPath, existing, file.fileName));
} else {
outputPathMap.set(outputPath, file.fileName);
}
return { ...file, outputPath };
});
}

performance.endSection("getEmitPlan");
Expand Down Expand Up @@ -189,11 +198,18 @@ export function getEmitPathRelativeToOutDir(fileName: string, program: ts.Progra
emitPathSplits[0] = "lua_modules";
}

// Expand dots in path segments into nested directories so that Lua's require()
// resolves correctly (e.g. "Foo.Bar/index.ts" -> "Foo/Bar/index.lua").
// Dots are path separators in Lua's module system, so a file at "Foo.Bar/index.lua"
// would be unreachable via require("Foo.Bar.index") since Lua looks for "Foo/Bar/index.lua".
// Strip the source extension first, split all dots, then re-add the output extension.
emitPathSplits[emitPathSplits.length - 1] = trimExtension(emitPathSplits[emitPathSplits.length - 1]);
emitPathSplits = emitPathSplits.flatMap(segment => segment.split("."));

// Set extension
const extension = ((program.getCompilerOptions() as CompilerOptions).extension ?? "lua").trim();
const trimmedExtension = extension.startsWith(".") ? extension.substring(1) : extension;
emitPathSplits[emitPathSplits.length - 1] =
trimExtension(emitPathSplits[emitPathSplits.length - 1]) + "." + trimmedExtension;
emitPathSplits[emitPathSplits.length - 1] += "." + trimmedExtension;

return path.join(...emitPathSplits);
}
Expand Down
35 changes: 34 additions & 1 deletion test/unit/modules/resolution.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as ts from "typescript";
import { couldNotResolveRequire } from "../../../src/transpilation/diagnostics";
import { couldNotResolveRequire, emitPathCollision } from "../../../src/transpilation/diagnostics";
import * as util from "../../util";

const requireRegex = /require\("(.*?)"\)/;
Expand Down Expand Up @@ -166,6 +166,39 @@ test.each([
.tap(expectToRequire(expectedPath));
});

// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1445
// Can't test this via execution because the test harness uses package.preload
// instead of real filesystem resolution, so require() always finds the module
// regardless of output path. We check the output path directly instead.
// TODO: test via actual Lua execution once the harness supports filesystem resolution.
test("dots in directory names emit to nested directories", () => {
const { transpiledFiles } = util.testModule`
import { answer } from "./Foo.Bar";
export const result = answer;
`
.addExtraFile("Foo.Bar/index.ts", "export const answer = 42;")
.setOptions({ rootDir: "." })
.getLuaResult();

// Foo.Bar/index.ts should emit to Foo/Bar/index.lua, not Foo.Bar/index.lua
const dottedFile = transpiledFiles.find(f => f.lua?.includes("answer = 42"));
expect(dottedFile).toBeDefined();
expect(dottedFile!.outPath).toContain("Foo/Bar/index.lua");
expect(dottedFile!.outPath).not.toContain("Foo.Bar");
});

test("dots in paths that collide with existing paths produce a diagnostic", () => {
util.testModule`
import { a } from "./Foo.Bar";
import { b } from "./Foo/Bar";
export const result = a + b;
`
.addExtraFile("Foo.Bar/index.ts", "export const a = 1;")
.addExtraFile("Foo/Bar/index.ts", "export const b = 2;")
.setOptions({ rootDir: "." })
.expectToHaveDiagnostics([emitPathCollision.code]);
});

test("import = require", () => {
util.testModule`
import foo = require("./foo/bar");
Expand Down
3 changes: 2 additions & 1 deletion test/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,8 +534,9 @@ end)());`;
const moduleExports = {};
globalContext.exports = moduleExports;
globalContext.module = { exports: moduleExports };
const baseName = fileName.replace("./", "");
const transpiledExtraFile = transpiledFiles.find(({ sourceFiles }) =>
sourceFiles.some(f => f.fileName === fileName.replace("./", "") + ".ts")
sourceFiles.some(f => f.fileName === baseName + ".ts" || f.fileName === baseName + "/index.ts")
);

if (transpiledExtraFile?.js) {
Expand Down
Loading