@@ -91,19 +91,38 @@ namespace ts.server {
9191 }
9292 }
9393
94+ export interface PluginCreateInfo {
95+ project : Project ;
96+ languageService : LanguageService ;
97+ languageServiceHost : LanguageServiceHost ;
98+ serverHost : ServerHost ;
99+ config : any ;
100+ }
101+
102+ export interface PluginModule {
103+ create ( createInfo : PluginCreateInfo ) : LanguageService ;
104+ getExternalFiles ?( proj : Project ) : string [ ] ;
105+ }
106+
107+ export interface PluginModuleFactory {
108+ ( mod : { typescript : typeof ts } ) : PluginModule ;
109+ }
110+
94111 export abstract class Project {
95112 private rootFiles : ScriptInfo [ ] = [ ] ;
96113 private rootFilesMap : FileMap < ScriptInfo > = createFileMap < ScriptInfo > ( ) ;
97- private lsHost : LSHost ;
98114 private program : ts . Program ;
99115
100116 private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap ( ) ;
101117 private lastCachedUnresolvedImportsList : SortedReadonlyArray < string > ;
102118
103- private readonly languageService : LanguageService ;
119+ // wrapper over the real language service that will suppress all semantic operations
120+ protected languageService : LanguageService ;
104121
105122 public languageServiceEnabled = true ;
106123
124+ protected readonly lsHost : LSHost ;
125+
107126 builder : Builder ;
108127 /**
109128 * Set of files names that were updated since the last call to getChangesSinceVersion.
@@ -150,6 +169,17 @@ namespace ts.server {
150169 return this . cachedUnresolvedImportsPerFile ;
151170 }
152171
172+ public static resolveModule ( moduleName : string , initialDir : string , host : ServerHost , log : ( message : string ) => void ) : { } {
173+ const resolvedPath = normalizeSlashes ( host . resolvePath ( combinePaths ( initialDir , "node_modules" ) ) ) ;
174+ log ( `Loading ${ moduleName } from ${ initialDir } (resolved to ${ resolvedPath } )` ) ;
175+ const result = host . require ( resolvedPath , moduleName ) ;
176+ if ( result . error ) {
177+ log ( `Failed to load module: ${ JSON . stringify ( result . error ) } ` ) ;
178+ return undefined ;
179+ }
180+ return result . module ;
181+ }
182+
153183 constructor (
154184 private readonly projectName : string ,
155185 readonly projectKind : ProjectKind ,
@@ -237,6 +267,10 @@ namespace ts.server {
237267 abstract getProjectRootPath ( ) : string | undefined ;
238268 abstract getTypeAcquisition ( ) : TypeAcquisition ;
239269
270+ getExternalFiles ( ) : string [ ] {
271+ return [ ] ;
272+ }
273+
240274 getSourceFile ( path : Path ) {
241275 if ( ! this . program ) {
242276 return undefined ;
@@ -804,10 +838,12 @@ namespace ts.server {
804838 private typeRootsWatchers : FileWatcher [ ] ;
805839 readonly canonicalConfigFilePath : NormalizedPath ;
806840
841+ private plugins : PluginModule [ ] = [ ] ;
842+
807843 /** Used for configured projects which may have multiple open roots */
808844 openRefCount = 0 ;
809845
810- constructor ( configFileName : NormalizedPath ,
846+ constructor ( private configFileName : NormalizedPath ,
811847 projectService : ProjectService ,
812848 documentRegistry : ts . DocumentRegistry ,
813849 hasExplicitListOfFiles : boolean ,
@@ -817,12 +853,64 @@ namespace ts.server {
817853 public compileOnSaveEnabled : boolean ) {
818854 super ( configFileName , ProjectKind . Configured , projectService , documentRegistry , hasExplicitListOfFiles , languageServiceEnabled , compilerOptions , compileOnSaveEnabled ) ;
819855 this . canonicalConfigFilePath = asNormalizedPath ( projectService . toCanonicalFileName ( configFileName ) ) ;
856+ this . enablePlugins ( ) ;
820857 }
821858
822859 getConfigFilePath ( ) {
823860 return this . getProjectName ( ) ;
824861 }
825862
863+ enablePlugins ( ) {
864+ const host = this . projectService . host ;
865+ const options = this . getCompilerOptions ( ) ;
866+ const log = ( message : string ) => {
867+ this . projectService . logger . info ( message ) ;
868+ } ;
869+
870+ if ( ! ( options . plugins && options . plugins . length ) ) {
871+ this . projectService . logger . info ( "No plugins exist" ) ;
872+ // No plugins
873+ return ;
874+ }
875+
876+ if ( ! host . require ) {
877+ this . projectService . logger . info ( "Plugins were requested but not running in environment that supports 'require'. Nothing will be loaded" ) ;
878+ return ;
879+ }
880+
881+ for ( const pluginConfigEntry of options . plugins ) {
882+ const searchPath = getDirectoryPath ( this . configFileName ) ;
883+ const resolvedModule = < PluginModuleFactory > Project . resolveModule ( pluginConfigEntry . name , searchPath , host , log ) ;
884+ if ( resolvedModule ) {
885+ this . enableProxy ( resolvedModule , pluginConfigEntry ) ;
886+ }
887+ }
888+ }
889+
890+ private enableProxy ( pluginModuleFactory : PluginModuleFactory , configEntry : PluginImport ) {
891+ try {
892+ if ( typeof pluginModuleFactory !== "function" ) {
893+ this . projectService . logger . info ( `Skipped loading plugin ${ configEntry . name } because it did expose a proper factory function` ) ;
894+ return ;
895+ }
896+
897+ const info : PluginCreateInfo = {
898+ config : configEntry ,
899+ project : this ,
900+ languageService : this . languageService ,
901+ languageServiceHost : this . lsHost ,
902+ serverHost : this . projectService . host
903+ } ;
904+
905+ const pluginModule = pluginModuleFactory ( { typescript : ts } ) ;
906+ this . languageService = pluginModule . create ( info ) ;
907+ this . plugins . push ( pluginModule ) ;
908+ }
909+ catch ( e ) {
910+ this . projectService . logger . info ( `Plugin activation failed: ${ e } ` ) ;
911+ }
912+ }
913+
826914 getProjectRootPath ( ) {
827915 return getDirectoryPath ( this . getConfigFilePath ( ) ) ;
828916 }
@@ -839,6 +927,21 @@ namespace ts.server {
839927 return this . typeAcquisition ;
840928 }
841929
930+ getExternalFiles ( ) : string [ ] {
931+ const items : string [ ] = [ ] ;
932+ for ( const plugin of this . plugins ) {
933+ if ( typeof plugin . getExternalFiles === "function" ) {
934+ try {
935+ items . push ( ...plugin . getExternalFiles ( this ) ) ;
936+ }
937+ catch ( e ) {
938+ this . projectService . logger . info ( `A plugin threw an exception in getExternalFiles: ${ e } ` ) ;
939+ }
940+ }
941+ }
942+ return items ;
943+ }
944+
842945 watchConfigFile ( callback : ( project : ConfiguredProject ) => void ) {
843946 this . projectFileWatcher = this . projectService . host . watchFile ( this . getConfigFilePath ( ) , _ => callback ( this ) ) ;
844947 }
0 commit comments