Skip to content

Commit 278ead6

Browse files
Add import resolution plugin hook (#1455)
* added a plugin hook for custom module resolution strategies * fixed an issue with the plugin module resolution when importing scripts from other scripts * fixed a mistake I made when changing packageRoot to currentPackage * passed plugins directory to the emitPlan function so we can avoid using the getPlugins call again * delete an accidental output * moved module resolution logic to resolveImport and made the requested changes to the interface * reverted resolveLuaDependencyPathFromNodeModules because the changes are no longer needed * replaced some comments and change a conditional statement * - included compiler options and emit host in module resolution plugin interface - removed try catch on plugin execution * - some fixes to the module resolution - added test case * fixed passing the dependency path instead of the required path and fixed the test plugin * removed transformation to luaRequirePath so the module identifier meets the format specified in the interface * renamed param for formatPathToFile
1 parent 0795695 commit 278ead6

9 files changed

Lines changed: 145 additions & 15 deletions

File tree

src/transpilation/plugins.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ export interface Plugin {
4545
emitHost: EmitHost,
4646
result: EmitFile[]
4747
) => ts.Diagnostic[] | void;
48+
49+
moduleResolution?: (
50+
moduleIdentifier: string,
51+
requiringFile: string,
52+
options: CompilerOptions,
53+
emitHost: EmitHost
54+
) => string | undefined;
4855
}
4956

5057
export function getPlugins(program: ts.Program): { diagnostics: ts.Diagnostic[]; plugins: Plugin[] } {

src/transpilation/resolve.ts

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { formatPathToLuaPath, normalizeSlashes, trimExtension } from "../utils";
99
import { couldNotReadDependency, couldNotResolveRequire } from "./diagnostics";
1010
import { BuildMode, CompilerOptions } from "../CompilerOptions";
1111
import { findLuaRequires, LuaRequire } from "./find-lua-requires";
12+
import { Plugin } from "./plugins";
1213

1314
const resolver = resolve.ResolverFactory.createResolver({
1415
extensions: [".lua"],
@@ -33,7 +34,8 @@ class ResolutionContext {
3334
constructor(
3435
public readonly program: ts.Program,
3536
public readonly options: CompilerOptions,
36-
private readonly emitHost: EmitHost
37+
private readonly emitHost: EmitHost,
38+
private readonly plugins: Plugin[]
3739
) {
3840
this.noResolvePaths = new Set(options.noResolvePaths);
3941
}
@@ -77,7 +79,10 @@ class ResolutionContext {
7779
return;
7880
}
7981

80-
const dependencyPath = this.resolveDependencyPath(file, required.requirePath);
82+
const dependencyPath =
83+
this.resolveDependencyPathsWithPlugins(file, required.requirePath) ??
84+
this.resolveDependencyPath(file, required.requirePath);
85+
8186
if (!dependencyPath) return this.couldNotResolveImport(required, file);
8287

8388
if (this.options.tstlVerbose) {
@@ -93,8 +98,65 @@ class ResolutionContext {
9398
}
9499
}
95100

101+
private resolveDependencyPathsWithPlugins(requiringFile: ProcessedFile, dependency: string) {
102+
const requiredFromLuaFile = requiringFile.fileName.endsWith(".lua");
103+
for (const plugin of this.plugins) {
104+
if (plugin.moduleResolution != null) {
105+
const pluginResolvedPath = plugin.moduleResolution(
106+
dependency,
107+
requiringFile.fileName,
108+
this.options,
109+
this.emitHost
110+
);
111+
if (pluginResolvedPath !== undefined) {
112+
// If lua file is in node_module
113+
if (requiredFromLuaFile && isNodeModulesFile(requiringFile.fileName)) {
114+
// If requiring file is in lua module, try to resolve sibling in that file first
115+
const resolvedNodeModulesFile = this.resolveLuaDependencyPathFromNodeModules(
116+
requiringFile,
117+
pluginResolvedPath
118+
);
119+
if (resolvedNodeModulesFile) {
120+
if (this.options.tstlVerbose) {
121+
console.log(
122+
`Resolved file path for module ${dependency} to path ${pluginResolvedPath} using plugin.`
123+
);
124+
}
125+
return resolvedNodeModulesFile;
126+
}
127+
}
128+
129+
const resolvedPath = this.formatPathToFile(pluginResolvedPath, requiringFile);
130+
const fileFromPath = this.getFileFromPath(resolvedPath);
131+
132+
if (fileFromPath) {
133+
if (this.options.tstlVerbose) {
134+
console.log(
135+
`Resolved file path for module ${dependency} to path ${pluginResolvedPath} using plugin.`
136+
);
137+
}
138+
return fileFromPath;
139+
}
140+
}
141+
}
142+
}
143+
}
144+
96145
public processedDependencies = new Set<string>();
97146

147+
private formatPathToFile(targetPath: string, required: ProcessedFile) {
148+
const isRelative = ["/", "./", "../"].some(p => targetPath.startsWith(p));
149+
150+
// // If the import is relative, always resolve it relative to the requiring file
151+
// // If the import is not relative, resolve it relative to options.baseUrl if it is set
152+
const fileDirectory = path.dirname(required.fileName);
153+
const relativeTo = isRelative ? fileDirectory : this.options.baseUrl ?? fileDirectory;
154+
155+
// // Check if file is a file in the project
156+
const resolvedPath = path.join(relativeTo, targetPath);
157+
return resolvedPath;
158+
}
159+
98160
private processDependency(dependencyPath: string): void {
99161
if (this.processedDependencies.has(dependencyPath)) return;
100162
this.processedDependencies.add(dependencyPath);
@@ -140,15 +202,8 @@ class ResolutionContext {
140202
if (resolvedNodeModulesFile) return resolvedNodeModulesFile;
141203
}
142204

143-
// Check if the import is relative
144-
const isRelative = ["/", "./", "../"].some(p => dependency.startsWith(p));
145-
146-
// If the import is relative, always resolve it relative to the requiring file
147-
// If the import is not relative, resolve it relative to options.baseUrl if it is set
148-
const relativeTo = isRelative ? fileDirectory : this.options.baseUrl ?? fileDirectory;
149-
150205
// Check if file is a file in the project
151-
const resolvedPath = path.join(relativeTo, dependencyPath);
206+
const resolvedPath = this.formatPathToFile(dependencyPath, requiringFile);
152207
const fileFromPath = this.getFileFromPath(resolvedPath);
153208
if (fileFromPath) return fileFromPath;
154209

@@ -235,6 +290,7 @@ class ResolutionContext {
235290
path.join(resolvedPath, "index.lua"), // lua index file in sources
236291
path.join(resolvedPath, "init.lua"), // lua looks for <require>/init.lua if it cannot find <require>.lua
237292
];
293+
238294
for (const possibleFile of possibleLuaProjectFiles) {
239295
if (this.emitHost.fileExists(possibleFile)) {
240296
return possibleFile;
@@ -277,10 +333,15 @@ class ResolutionContext {
277333
}
278334
}
279335

280-
export function resolveDependencies(program: ts.Program, files: ProcessedFile[], emitHost: EmitHost): ResolutionResult {
336+
export function resolveDependencies(
337+
program: ts.Program,
338+
files: ProcessedFile[],
339+
emitHost: EmitHost,
340+
plugins: Plugin[]
341+
): ResolutionResult {
281342
const options = program.getCompilerOptions() as CompilerOptions;
282343

283-
const resolutionContext = new ResolutionContext(program, options, emitHost);
344+
const resolutionContext = new ResolutionContext(program, options, emitHost, plugins);
284345

285346
// Resolve dependencies for all processed files
286347
for (const file of files) {

src/transpilation/transpiler.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class Transpiler {
4545
}
4646
);
4747

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

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

@@ -102,7 +102,8 @@ export class Transpiler {
102102
protected getEmitPlan(
103103
program: ts.Program,
104104
diagnostics: ts.Diagnostic[],
105-
files: ProcessedFile[]
105+
files: ProcessedFile[],
106+
plugins: Plugin[]
106107
): { emitPlan: EmitFile[] } {
107108
performance.startSection("getEmitPlan");
108109
const options = program.getCompilerOptions() as CompilerOptions;
@@ -112,7 +113,7 @@ export class Transpiler {
112113
}
113114

114115
// Resolve imported modules and modify output Lua requires
115-
const resolutionResult = resolveDependencies(program, files, this.emitHost);
116+
const resolutionResult = resolveDependencies(program, files, this.emitHost, plugins);
116117
diagnostics.push(...resolutionResult.diagnostics);
117118

118119
const lualibRequired = resolutionResult.resolvedFiles.some(f => f.fileName === "lualib_bundle");

test/transpile/module-resolution.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,36 @@ test("paths without baseUrl is error", () => {
647647
util.testFunction``.setOptions({ paths: {} }).expectToHaveDiagnostics([pathsWithoutBaseUrl.code]);
648648
});
649649

650+
test("module resolution using plugin", () => {
651+
const baseProjectPath = path.resolve(__dirname, "module-resolution", "project-with-module-resolution-plugin");
652+
const projectTsConfig = path.join(baseProjectPath, "tsconfig.json");
653+
const mainFile = path.join(baseProjectPath, "src", "main.ts");
654+
655+
const luaResult = util
656+
.testProject(projectTsConfig)
657+
.setMainFileName(mainFile)
658+
.setOptions({
659+
luaPlugins: [
660+
{
661+
name: path.join(__dirname, "./plugins/moduleResolution.ts"),
662+
},
663+
],
664+
})
665+
.expectToHaveNoDiagnostics()
666+
.getLuaResult();
667+
668+
expect(luaResult.transpiledFiles).toHaveLength(2);
669+
let hasResolvedFile = false;
670+
for (const f of luaResult.transpiledFiles) {
671+
hasResolvedFile = f.outPath.endsWith("bar.lua");
672+
if (hasResolvedFile) {
673+
break;
674+
}
675+
}
676+
677+
expect(hasResolvedFile).toBe(true);
678+
});
679+
650680
function snapshotPaths(files: tstl.TranspiledFile[]) {
651681
return files.map(f => normalizeSlashes(f.outPath).split("module-resolution")[1]).sort();
652682
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
local ____exports = {}
2+
function ____exports.foo(self)
3+
return "foo"
4+
end
5+
return ____exports
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export declare function foo(): string;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { foo } from "./lua_sources/foo";
2+
export const result = foo();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"target": "esnext",
5+
"lib": ["esnext"],
6+
"types": [],
7+
"outDir": "./dist"
8+
}
9+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import path = require("path");
2+
import type * as tstl from "../../../src";
3+
4+
const plugin: tstl.Plugin = {
5+
moduleResolution(moduleIdentifier) {
6+
const modulePath = moduleIdentifier.replace(/\./g, path.sep);
7+
if (moduleIdentifier.includes("foo")) {
8+
return modulePath.replace("foo", "bar");
9+
}
10+
},
11+
};
12+
13+
// eslint-disable-next-line import/no-default-export
14+
export default plugin;

0 commit comments

Comments
 (0)