11import * as ts from "./typescript" ;
22import { TypeTable } from "./type_table" ;
3+ import * as pathlib from "path" ;
4+ import { VirtualSourceRoot } from "./virtual_source_root" ;
5+
6+ /**
7+ * Extracts the package name from the prefix of an import string.
8+ */
9+ const packageNameRex = / ^ (?: @ [ \w . - ] + [ / \\ ] + ) ? \w [ \w . - ] * (? = [ / \\ ] | $ ) / ;
10+ const extensions = [ '.ts' , '.tsx' , '.d.ts' , '.js' , '.jsx' ] ;
11+
12+ function getPackageName ( importString : string ) {
13+ let packageNameMatch = packageNameRex . exec ( importString ) ;
14+ if ( packageNameMatch == null ) return null ;
15+ let packageName = packageNameMatch [ 0 ] ;
16+ if ( packageName . charAt ( 0 ) === '@' ) {
17+ packageName = packageName . replace ( / [ / \\ ] + / g, '/' ) ; // Normalize slash after the scope.
18+ }
19+ return packageName ;
20+ }
321
422export class Project {
523 public program : ts . Program = null ;
24+ private host : ts . CompilerHost ;
25+ private resolutionCache : ts . ModuleResolutionCache ;
626
7- constructor ( public tsConfig : string , public config : ts . ParsedCommandLine , public typeTable : TypeTable ) { }
27+ constructor (
28+ public tsConfig : string ,
29+ public config : ts . ParsedCommandLine ,
30+ public typeTable : TypeTable ,
31+ public packageEntryPoints : Map < string , string > ,
32+ public virtualSourceRoot : VirtualSourceRoot ) {
33+
34+ this . resolveModuleNames = this . resolveModuleNames . bind ( this ) ;
35+
36+ this . resolutionCache = ts . createModuleResolutionCache ( pathlib . dirname ( tsConfig ) , ts . sys . realpath , config . options ) ;
37+ let host = ts . createCompilerHost ( config . options , true ) ;
38+ host . resolveModuleNames = this . resolveModuleNames ;
39+ host . trace = undefined ; // Disable tracing which would otherwise go to standard out
40+ this . host = host ;
41+ }
842
943 public unload ( ) : void {
1044 this . typeTable . releaseProgram ( ) ;
1145 this . program = null ;
1246 }
1347
1448 public load ( ) : void {
15- let host = ts . createCompilerHost ( this . config . options , true ) ;
16- host . trace = undefined ; // Disable tracing which would otherwise go to standard out
17- this . program = ts . createProgram ( this . config . fileNames , this . config . options , host ) ;
49+ const { config, host } = this ;
50+ this . program = ts . createProgram ( config . fileNames , config . options , host ) ;
1851 this . typeTable . setProgram ( this . program ) ;
1952 }
2053
@@ -27,4 +60,73 @@ export class Project {
2760 this . unload ( ) ;
2861 this . load ( ) ;
2962 }
63+
64+ /**
65+ * Override for module resolution in the TypeScript compiler host.
66+ */
67+ private resolveModuleNames (
68+ moduleNames : string [ ] ,
69+ containingFile : string ,
70+ reusedNames : string [ ] ,
71+ redirectedReference : ts . ResolvedProjectReference ,
72+ options : ts . CompilerOptions ) {
73+
74+ const { host, resolutionCache } = this ;
75+ return moduleNames . map ( ( moduleName ) => {
76+ let redirected = this . redirectModuleName ( moduleName , containingFile , options ) ;
77+ if ( redirected != null ) return redirected ;
78+ return ts . resolveModuleName ( moduleName , containingFile , options , host , resolutionCache ) . resolvedModule ;
79+ } ) ;
80+ }
81+
82+ /**
83+ * Returns the path that the given import string should be redirected to, or null if it should
84+ * fall back to standard module resolution.
85+ */
86+ private redirectModuleName ( moduleName : string , containingFile : string , options : ts . CompilerOptions ) : ts . ResolvedModule {
87+ // Get a package name from the leading part of the module name, e.g. '@scope/foo' from '@scope/foo/bar'.
88+ let packageName = getPackageName ( moduleName ) ;
89+ if ( packageName == null ) return null ;
90+
91+ // Get the overridden location of this package, if one exists.
92+ let packageEntryPoint = this . packageEntryPoints . get ( packageName ) ;
93+ if ( packageEntryPoint == null ) {
94+ // The package is not overridden, but we have established that it begins with a valid package name.
95+ // Do a lookup in the virtual source root (where dependencies are installed) by changing the 'containing file'.
96+ let virtualContainingFile = this . virtualSourceRoot . toVirtualPath ( containingFile ) ;
97+ if ( virtualContainingFile != null ) {
98+ return ts . resolveModuleName ( moduleName , virtualContainingFile , options , this . host , this . resolutionCache ) . resolvedModule ;
99+ }
100+ return null ;
101+ }
102+
103+ // If the requested module name is exactly the overridden package name,
104+ // return the entry point file (it is not necessarily called `index.ts`).
105+ if ( moduleName === packageName ) {
106+ return { resolvedFileName : packageEntryPoint , isExternalLibraryImport : true } ;
107+ }
108+
109+ // Get the suffix after the package name, e.g. the '/bar' in '@scope/foo/bar'.
110+ let suffix = moduleName . substring ( packageName . length ) ;
111+
112+ // Resolve the suffix relative to the package directory.
113+ let packageDir = pathlib . dirname ( packageEntryPoint ) ;
114+ let joinedPath = pathlib . join ( packageDir , suffix ) ;
115+
116+ // Add implicit '/index'
117+ if ( ts . sys . directoryExists ( joinedPath ) ) {
118+ joinedPath = pathlib . join ( joinedPath , 'index' ) ;
119+ }
120+
121+ // Try each recognized extension. We must not return a file whose extension is not
122+ // recognized by TypeScript.
123+ for ( let ext of extensions ) {
124+ let candidate = joinedPath . endsWith ( ext ) ? joinedPath : ( joinedPath + ext ) ;
125+ if ( ts . sys . fileExists ( candidate ) ) {
126+ return { resolvedFileName : candidate , isExternalLibraryImport : true } ;
127+ }
128+ }
129+
130+ return null ;
131+ }
30132}
0 commit comments