Skip to content
Open
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 replaced with underscores for Lua module resolution.`
);
26 changes: 20 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,17 @@ export class Transpiler {
diagnostics.push(...bundleDiagnostics);
emitPlan = [bundleFile];
} else {
emitPlan = resolutionResult.resolvedFiles.map(file => ({
...file,
outputPath: getEmitPath(file.fileName, program),
}));
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 +197,17 @@ export function getEmitPathRelativeToOutDir(fileName: string, program: ts.Progra
emitPathSplits[0] = "lua_modules";
}

// Replace dots with underscores in path segments so that Lua's require()
// resolves correctly. Dots are path separators in Lua's module system, so
// "Foo.Bar/index.lua" would be unreachable via require("Foo.Bar.index")
// since Lua interprets it as "Foo/Bar/index.lua".
emitPathSplits[emitPathSplits.length - 1] = trimExtension(emitPathSplits[emitPathSplits.length - 1]);
emitPathSplits = emitPathSplits.map(segment => segment.replace(/\./g, "_"));

// 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
34 changes: 33 additions & 1 deletion test/unit/modules/resolution.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as path from "path";
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 +167,37 @@ 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.
test("dots in directory names are replaced with underscores in output", () => {
const { transpiledFiles } = util.testModule`
import { answer } from "./Foo.Bar";
Copy link
Copy Markdown
Member

@Perryvw Perryvw Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add a test where you're including a file like "Foo.Bar.ts", or maybe "foo.test.ts" since that was mentioned in the issue, rather than only directories with dots

export const result = answer;
`
.addExtraFile("Foo.Bar/index.ts", "export const answer = 42;")
.setOptions({ rootDir: "." })
.getLuaResult();

const dottedFile = transpiledFiles.find(f => f.lua?.includes("answer = 42"));
expect(dottedFile).toBeDefined();
expect(dottedFile!.outPath).toContain(path.join("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