@@ -21,29 +21,110 @@ interface ResolutionResult {
2121 diagnostics : ts . Diagnostic [ ] ;
2222}
2323
24+ class ResolutionContext {
25+ private resultsCache = new Map < string , ResolutionResult > ( ) ;
26+
27+ constructor (
28+ public readonly program : ts . Program ,
29+ public readonly options : CompilerOptions ,
30+ private readonly emitHost : EmitHost
31+ ) { }
32+
33+ public resolve ( file : ProcessedFile , required : string ) : ResolutionResult {
34+ const resolvedDependency = resolveDependency ( file , required , this . program , this . emitHost ) ;
35+ if ( resolvedDependency ) {
36+ if ( this . options . tstlVerbose ) {
37+ console . log ( `Resolved ${ required } to ${ normalizeSlashes ( resolvedDependency ) } ` ) ;
38+ }
39+
40+ // Figure out resolved require path and dependency output path
41+ const resolvedRequire = getEmitPathRelativeToOutDir ( resolvedDependency , this . program ) ;
42+
43+ if ( shouldRewriteRequires ( resolvedDependency , this . program ) ) {
44+ replaceRequireInCode ( file , required , resolvedRequire ) ;
45+ replaceRequireInSourceMap ( file , required , resolvedRequire ) ;
46+ }
47+
48+ // Check cache to prevent resolving nested dependencies double to break dependency loops
49+ if ( this . resultsCache . has ( resolvedDependency ) ) {
50+ if ( this . options . tstlVerbose ) {
51+ console . log ( `Resolution cache hit for ${ normalizeSlashes ( resolvedDependency ) } ` ) ;
52+ }
53+ return this . resultsCache . get ( resolvedDependency ) ! ;
54+ }
55+
56+ // If dependency is not part of project, add dependency to output and resolve its dependencies recursively
57+ if ( shouldIncludeDependency ( resolvedDependency , this . program ) ) {
58+ // If dependency resolved successfully, read its content
59+ const dependencyContent = this . emitHost . readFile ( resolvedDependency ) ;
60+ if ( dependencyContent === undefined ) {
61+ return { resolvedFiles : [ ] , diagnostics : [ couldNotReadDependency ( resolvedDependency ) ] } ;
62+ }
63+
64+ const dependency = {
65+ fileName : resolvedDependency ,
66+ code : dependencyContent ,
67+ } ;
68+ const nestedDependencies = resolveFileDependencies ( dependency , this ) ;
69+
70+ // Cache result and return
71+ const result = {
72+ resolvedFiles : [ dependency , ...nestedDependencies . resolvedFiles ] ,
73+ diagnostics : [ ...nestedDependencies . diagnostics ] ,
74+ } ;
75+ this . resultsCache . set ( resolvedDependency , result ) ;
76+ return result ;
77+ } else {
78+ const result = {
79+ resolvedFiles : [ ] ,
80+ diagnostics : [ ] ,
81+ } ;
82+ this . resultsCache . set ( resolvedDependency , result ) ;
83+ return result ;
84+ }
85+ } else {
86+ const fallbackRequire = fallbackResolve ( required , getSourceDir ( this . program ) , path . dirname ( file . fileName ) ) ;
87+ replaceRequireInCode ( file , required , fallbackRequire ) ;
88+ replaceRequireInSourceMap ( file , required , fallbackRequire ) ;
89+
90+ return {
91+ resolvedFiles : [ ] ,
92+ diagnostics : [
93+ couldNotResolveRequire ( required , path . relative ( getProjectRoot ( this . program ) , file . fileName ) ) ,
94+ ] ,
95+ } ;
96+ }
97+ }
98+ }
99+
24100export function resolveDependencies ( program : ts . Program , files : ProcessedFile [ ] , emitHost : EmitHost ) : ResolutionResult {
25101 const outFiles : ProcessedFile [ ] = [ ...files ] ;
26102 const diagnostics : ts . Diagnostic [ ] = [ ] ;
27103 const options = program . getCompilerOptions ( ) as CompilerOptions ;
28104
105+ const resolutionContext = new ResolutionContext ( program , options , emitHost ) ;
106+
29107 // Resolve dependencies for all processed files
30108 for ( const file of files ) {
31109 if ( options . tstlVerbose ) {
32110 console . log ( `Resolving dependencies for ${ normalizeSlashes ( file . fileName ) } ` ) ;
33111 }
34112
35- const resolutionResult = resolveFileDependencies ( file , program , emitHost ) ;
113+ const resolutionResult = resolveFileDependencies ( file , resolutionContext ) ;
36114 outFiles . push ( ...resolutionResult . resolvedFiles ) ;
37115 diagnostics . push ( ...resolutionResult . diagnostics ) ;
38116 }
39117
40- return { resolvedFiles : outFiles , diagnostics } ;
118+ return { resolvedFiles : deduplicateResolvedFiles ( outFiles ) , diagnostics } ;
41119}
42120
43- function resolveFileDependencies ( file : ProcessedFile , program : ts . Program , emitHost : EmitHost ) : ResolutionResult {
121+ function deduplicateResolvedFiles ( files : ProcessedFile [ ] ) : ProcessedFile [ ] {
122+ return [ ...new Map ( files . map ( f => [ f . fileName , f ] ) ) . values ( ) ] ;
123+ }
124+
125+ function resolveFileDependencies ( file : ProcessedFile , context : ResolutionContext ) : ResolutionResult {
44126 const dependencies : ProcessedFile [ ] = [ ] ;
45127 const diagnostics : ts . Diagnostic [ ] = [ ] ;
46- const options = program . getCompilerOptions ( ) as CompilerOptions ;
47128
48129 for ( const required of findRequiredPaths ( file . code ) ) {
49130 // Do no resolve lualib
@@ -60,57 +141,22 @@ function resolveFileDependencies(file: ProcessedFile, program: ts.Program, emitH
60141 }
61142
62143 // Try to resolve the import starting from the directory `file` is in
63- const fileDir = path . dirname ( file . fileName ) ;
64- const resolvedDependency = resolveDependency ( fileDir , required , program , emitHost ) ;
65- if ( resolvedDependency ) {
66- if ( options . tstlVerbose ) {
67- console . log ( `Resolved ${ required } to ${ normalizeSlashes ( resolvedDependency ) } ` ) ;
68- }
69-
70- // Figure out resolved require path and dependency output path
71- const resolvedRequire = getEmitPathRelativeToOutDir ( resolvedDependency , program ) ;
72-
73- if ( shouldRewriteRequires ( resolvedDependency , program ) ) {
74- replaceRequireInCode ( file , required , resolvedRequire ) ;
75- replaceRequireInSourceMap ( file , required , resolvedRequire ) ;
76- }
77-
78- // If dependency is not part of project, add dependency to output and resolve its dependencies recursively
79- if ( shouldIncludeDependency ( resolvedDependency , program ) ) {
80- // If dependency resolved successfully, read its content
81- const dependencyContent = emitHost . readFile ( resolvedDependency ) ;
82- if ( dependencyContent === undefined ) {
83- diagnostics . push ( couldNotReadDependency ( resolvedDependency ) ) ;
84- continue ;
85- }
86-
87- const dependency = {
88- fileName : resolvedDependency ,
89- code : dependencyContent ,
90- } ;
91- const nestedDependencies = resolveFileDependencies ( dependency , program , emitHost ) ;
92- dependencies . push ( dependency , ...nestedDependencies . resolvedFiles ) ;
93- diagnostics . push ( ...nestedDependencies . diagnostics ) ;
94- }
95- } else {
96- // Could not resolve dependency, add a diagnostic and make some fallback path
97- diagnostics . push ( couldNotResolveRequire ( required , path . relative ( getProjectRoot ( program ) , file . fileName ) ) ) ;
98-
99- const fallbackRequire = fallbackResolve ( required , getSourceDir ( program ) , fileDir ) ;
100- replaceRequireInCode ( file , required , fallbackRequire ) ;
101- replaceRequireInSourceMap ( file , required , fallbackRequire ) ;
102- }
144+ const resolvedDependency = context . resolve ( file , required ) ;
145+ dependencies . push ( ...resolvedDependency . resolvedFiles ) ;
146+ diagnostics . push ( ...resolvedDependency . diagnostics ) ;
103147 }
104148 return { resolvedFiles : dependencies , diagnostics } ;
105149}
106150
107151function resolveDependency (
108- fileDirectory : string ,
152+ requiringFile : ProcessedFile ,
109153 dependency : string ,
110154 program : ts . Program ,
111155 emitHost : EmitHost
112156) : string | undefined {
113157 const options = program . getCompilerOptions ( ) as CompilerOptions ;
158+
159+ const fileDirectory = path . dirname ( requiringFile . fileName ) ;
114160 if ( options . tstlVerbose ) {
115161 console . log ( `Resolving "${ dependency } " from ${ normalizeSlashes ( fileDirectory ) } ` ) ;
116162 }
@@ -132,10 +178,24 @@ function resolveDependency(
132178 }
133179 }
134180
181+ // Check if this is a lua file in the project sources
182+ const possibleLuaProjectFiles = [
183+ resolvedPath + ".lua" , // lua file in sources
184+ path . join ( resolvedPath , "index.lua" ) , // lua index file in sources
185+ ] ;
186+
187+ for ( const possibleFile of possibleLuaProjectFiles ) {
188+ if ( emitHost . fileExists ( possibleFile ) ) {
189+ return possibleFile ;
190+ }
191+ }
192+
135193 // Check if this is a sibling of a required lua file
136- const luaFilePath = path . resolve ( fileDirectory , dependency + ".lua" ) ;
137- if ( emitHost . fileExists ( luaFilePath ) ) {
138- return luaFilePath ;
194+ if ( requiringFile . fileName . endsWith ( ".lua" ) ) {
195+ const luaFilePath = resolveLuaPath ( fileDirectory , dependency , emitHost ) ;
196+ if ( luaFilePath ) {
197+ return luaFilePath ;
198+ }
139199 }
140200
141201 // Not a TS file in our project sources, use resolver to check if we can find dependency
@@ -151,6 +211,39 @@ function resolveDependency(
151211 return undefined ;
152212}
153213
214+ function resolveLuaPath ( fromPath : string , dependency : string , emitHost : EmitHost ) {
215+ const splitDependency = dependency . split ( "." ) ;
216+ if ( splitDependency . length === 1 ) {
217+ // If dependency has just one part (the file), look for a lua file with that name
218+ const fileDirectory = walkUpFileTreeUntil ( fromPath , dir =>
219+ emitHost . fileExists ( path . join ( dir , dependency ) + ".lua" )
220+ ) ;
221+ if ( fileDirectory ) {
222+ return path . join ( fileDirectory , dependency ) + ".lua" ;
223+ }
224+ } else {
225+ // If dependency has multiple parts, look for the first directory of the require path, which must be in the lua root
226+ const luaRoot = walkUpFileTreeUntil ( fromPath , dir =>
227+ emitHost . directoryExists ( path . join ( dir , splitDependency [ 0 ] ) )
228+ ) ;
229+ if ( luaRoot ) {
230+ return path . join ( luaRoot , dependency . replace ( "." , path . sep ) ) + ".lua" ;
231+ }
232+ }
233+ }
234+
235+ function walkUpFileTreeUntil ( fromDirectory : string , predicate : ( dir : string ) => boolean ) {
236+ const currentDir = path . normalize ( fromDirectory ) . split ( path . sep ) ;
237+ while ( currentDir . length > 0 ) {
238+ const dir = currentDir . join ( path . sep ) ;
239+ if ( predicate ( dir ) ) {
240+ return dir ;
241+ }
242+ currentDir . pop ( ) ;
243+ }
244+ return undefined ;
245+ }
246+
154247function shouldRewriteRequires ( resolvedDependency : string , program : ts . Program ) {
155248 return ! isNodeModulesFile ( resolvedDependency ) || ! isBuildModeLibrary ( program ) ;
156249}
0 commit comments