Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions src/transpilation/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ export interface Plugin {
emitHost: EmitHost,
result: EmitFile[]
) => ts.Diagnostic[] | void;

moduleResolution?: (
moduleIdentifier: string,
requiringFile: string,
options: CompilerOptions,
emitHost: EmitHost
) => string | undefined;
}

export function getPlugins(program: ts.Program): { diagnostics: ts.Diagnostic[]; plugins: Plugin[] } {
Expand Down
85 changes: 73 additions & 12 deletions src/transpilation/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { formatPathToLuaPath, normalizeSlashes, trimExtension } from "../utils";
import { couldNotReadDependency, couldNotResolveRequire } from "./diagnostics";
import { BuildMode, CompilerOptions } from "../CompilerOptions";
import { findLuaRequires, LuaRequire } from "./find-lua-requires";
import { Plugin } from "./plugins";

const resolver = resolve.ResolverFactory.createResolver({
extensions: [".lua"],
Expand All @@ -33,7 +34,8 @@ class ResolutionContext {
constructor(
public readonly program: ts.Program,
public readonly options: CompilerOptions,
private readonly emitHost: EmitHost
private readonly emitHost: EmitHost,
private readonly plugins: Plugin[]
) {
this.noResolvePaths = new Set(options.noResolvePaths);
}
Expand Down Expand Up @@ -77,7 +79,10 @@ class ResolutionContext {
return;
}

const dependencyPath = this.resolveDependencyPath(file, required.requirePath);
const dependencyPath =
this.resolveDependencyPathsWithPlugins(file, required.requirePath) ??
this.resolveDependencyPath(file, required.requirePath);

if (!dependencyPath) return this.couldNotResolveImport(required, file);

if (this.options.tstlVerbose) {
Expand All @@ -93,8 +98,65 @@ class ResolutionContext {
}
}

private resolveDependencyPathsWithPlugins(requiringFile: ProcessedFile, dependency: string) {
const requiredFromLuaFile = requiringFile.fileName.endsWith(".lua");
for (const plugin of this.plugins) {
if (plugin.moduleResolution != null) {
const pluginResolvedPath = plugin.moduleResolution(
dependency,
requiringFile.fileName,
this.options,
this.emitHost
);
if (pluginResolvedPath !== undefined) {
// If lua file is in node_module
if (requiredFromLuaFile && isNodeModulesFile(requiringFile.fileName)) {
// If requiring file is in lua module, try to resolve sibling in that file first
const resolvedNodeModulesFile = this.resolveLuaDependencyPathFromNodeModules(
requiringFile,
pluginResolvedPath
);
if (resolvedNodeModulesFile) {
if (this.options.tstlVerbose) {
console.log(
`Resolved file path for module ${dependency} to path ${pluginResolvedPath} using plugin.`
);
}
return resolvedNodeModulesFile;
}
}

const resolvedPath = this.formatPathToFile(pluginResolvedPath, requiringFile);
const fileFromPath = this.getFileFromPath(resolvedPath);

if (fileFromPath) {
if (this.options.tstlVerbose) {
console.log(
`Resolved file path for module ${dependency} to path ${pluginResolvedPath} using plugin.`
);
}
return fileFromPath;
}
}
}
}
}

public processedDependencies = new Set<string>();

private formatPathToFile(targetPath: string, required: ProcessedFile) {
const isRelative = ["/", "./", "../"].some(p => targetPath.startsWith(p));

// // If the import is relative, always resolve it relative to the requiring file
// // If the import is not relative, resolve it relative to options.baseUrl if it is set
const fileDirectory = path.dirname(required.fileName);
const relativeTo = isRelative ? fileDirectory : this.options.baseUrl ?? fileDirectory;

// // Check if file is a file in the project
const resolvedPath = path.join(relativeTo, targetPath);
return resolvedPath;
}

private processDependency(dependencyPath: string): void {
if (this.processedDependencies.has(dependencyPath)) return;
this.processedDependencies.add(dependencyPath);
Expand Down Expand Up @@ -140,15 +202,8 @@ class ResolutionContext {
if (resolvedNodeModulesFile) return resolvedNodeModulesFile;
}

// Check if the import is relative
const isRelative = ["/", "./", "../"].some(p => dependency.startsWith(p));

// If the import is relative, always resolve it relative to the requiring file
// If the import is not relative, resolve it relative to options.baseUrl if it is set
const relativeTo = isRelative ? fileDirectory : this.options.baseUrl ?? fileDirectory;

// Check if file is a file in the project
const resolvedPath = path.join(relativeTo, dependencyPath);
const resolvedPath = this.formatPathToFile(dependencyPath, requiringFile);
const fileFromPath = this.getFileFromPath(resolvedPath);
if (fileFromPath) return fileFromPath;

Expand Down Expand Up @@ -235,6 +290,7 @@ class ResolutionContext {
path.join(resolvedPath, "index.lua"), // lua index file in sources
path.join(resolvedPath, "init.lua"), // lua looks for <require>/init.lua if it cannot find <require>.lua
];

for (const possibleFile of possibleLuaProjectFiles) {
if (this.emitHost.fileExists(possibleFile)) {
return possibleFile;
Expand Down Expand Up @@ -277,10 +333,15 @@ class ResolutionContext {
}
}

export function resolveDependencies(program: ts.Program, files: ProcessedFile[], emitHost: EmitHost): ResolutionResult {
export function resolveDependencies(
program: ts.Program,
files: ProcessedFile[],
emitHost: EmitHost,
plugins: Plugin[]
): ResolutionResult {
const options = program.getCompilerOptions() as CompilerOptions;

const resolutionContext = new ResolutionContext(program, options, emitHost);
const resolutionContext = new ResolutionContext(program, options, emitHost, plugins);

// Resolve dependencies for all processed files
for (const file of files) {
Expand Down
7 changes: 4 additions & 3 deletions src/transpilation/transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class Transpiler {
}
);

const { emitPlan } = this.getEmitPlan(program, transpileDiagnostics, freshFiles);
const { emitPlan } = this.getEmitPlan(program, transpileDiagnostics, freshFiles, plugins);

const emitDiagnostics = this.emitFiles(program, plugins, emitPlan, writeFile);

Expand Down Expand Up @@ -102,7 +102,8 @@ export class Transpiler {
protected getEmitPlan(
program: ts.Program,
diagnostics: ts.Diagnostic[],
files: ProcessedFile[]
files: ProcessedFile[],
plugins: Plugin[]
): { emitPlan: EmitFile[] } {
performance.startSection("getEmitPlan");
const options = program.getCompilerOptions() as CompilerOptions;
Expand All @@ -112,7 +113,7 @@ export class Transpiler {
}

// Resolve imported modules and modify output Lua requires
const resolutionResult = resolveDependencies(program, files, this.emitHost);
const resolutionResult = resolveDependencies(program, files, this.emitHost, plugins);
diagnostics.push(...resolutionResult.diagnostics);

const lualibRequired = resolutionResult.resolvedFiles.some(f => f.fileName === "lualib_bundle");
Expand Down
30 changes: 30 additions & 0 deletions test/transpile/module-resolution.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,36 @@ test("paths without baseUrl is error", () => {
util.testFunction``.setOptions({ paths: {} }).expectToHaveDiagnostics([pathsWithoutBaseUrl.code]);
});

test("module resolution using plugin", () => {
const baseProjectPath = path.resolve(__dirname, "module-resolution", "project-with-module-resolution-plugin");
const projectTsConfig = path.join(baseProjectPath, "tsconfig.json");
const mainFile = path.join(baseProjectPath, "src", "main.ts");

const luaResult = util
.testProject(projectTsConfig)
.setMainFileName(mainFile)
.setOptions({
luaPlugins: [
{
name: path.join(__dirname, "./plugins/moduleResolution.ts"),
},
],
})
.expectToHaveNoDiagnostics()
.getLuaResult();

expect(luaResult.transpiledFiles).toHaveLength(2);
let hasResolvedFile = false;
for (const f of luaResult.transpiledFiles) {
hasResolvedFile = f.outPath.endsWith("bar.lua");
if (hasResolvedFile) {
break;
}
}

expect(hasResolvedFile).toBe(true);
});

function snapshotPaths(files: tstl.TranspiledFile[]) {
return files.map(f => normalizeSlashes(f.outPath).split("module-resolution")[1]).sort();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
local ____exports = {}
function ____exports.foo(self)
return "foo"
end
return ____exports
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export declare function foo(): string;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { foo } from "./lua_sources/foo";
export const result = foo();
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"strict": true,
"target": "esnext",
"lib": ["esnext"],
"types": [],
"outDir": "./dist"
}
}
14 changes: 14 additions & 0 deletions test/transpile/plugins/moduleResolution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import path = require("path");
import type * as tstl from "../../../src";

const plugin: tstl.Plugin = {
moduleResolution(moduleIdentifier) {
const modulePath = moduleIdentifier.replace(/\./g, path.sep);
if (moduleIdentifier.includes("foo")) {
return modulePath.replace("foo", "bar");
}
},
};

// eslint-disable-next-line import/no-default-export
export default plugin;