@@ -8,7 +8,9 @@ import * as ts from 'typescript';
8
8
import { Disposable } from './disposable' ;
9
9
import { FileSystemUpdater } from './fs' ;
10
10
import { Logger , NoopLogger } from './logging' ;
11
+ import { combinePaths } from './match-files' ;
11
12
import { InMemoryFileSystem } from './memfs' ;
13
+ import { InitializationOptions } from './request-type' ;
12
14
import { traceObservable , traceSync } from './tracing' ;
13
15
import {
14
16
isConfigFile ,
@@ -146,6 +148,11 @@ export class ProjectManager implements Disposable {
146
148
*/
147
149
private subscriptions = new Subscription ( ) ;
148
150
151
+ /**
152
+ * Options passed to the language server at startup
153
+ */
154
+ private initializationOptions ?: InitializationOptions ;
155
+
149
156
/**
150
157
* @param rootPath root path as passed to `initialize`
151
158
* @param inMemoryFileSystem File system that keeps structure and contents in memory
@@ -157,12 +164,14 @@ export class ProjectManager implements Disposable {
157
164
inMemoryFileSystem : InMemoryFileSystem ,
158
165
updater : FileSystemUpdater ,
159
166
traceModuleResolution ?: boolean ,
167
+ initializationOptions ?: InitializationOptions ,
160
168
protected logger : Logger = new NoopLogger ( )
161
169
) {
162
170
this . rootPath = rootPath ;
163
171
this . updater = updater ;
164
172
this . inMemoryFs = inMemoryFileSystem ;
165
173
this . versions = new Map < string , number > ( ) ;
174
+ this . initializationOptions = initializationOptions ;
166
175
this . traceModuleResolution = traceModuleResolution || false ;
167
176
168
177
// Share DocumentRegistry between all ProjectConfigurations
@@ -190,6 +199,7 @@ export class ProjectManager implements Disposable {
190
199
'' ,
191
200
tsConfig ,
192
201
this . traceModuleResolution ,
202
+ this . initializationOptions ,
193
203
this . logger
194
204
) ;
195
205
configs . set ( trimmedRootPath , config ) ;
@@ -219,6 +229,7 @@ export class ProjectManager implements Disposable {
219
229
filePath ,
220
230
undefined ,
221
231
this . traceModuleResolution ,
232
+ this . initializationOptions ,
222
233
this . logger
223
234
) ) ;
224
235
// Remove catch-all config (if exists)
@@ -817,6 +828,10 @@ export class ProjectConfiguration {
817
828
*/
818
829
private traceModuleResolution : boolean ;
819
830
831
+ private allowLocalPluginLoads : boolean = false ;
832
+ private globalPlugins : string [ ] = [ ] ;
833
+ private pluginProbeLocations : string [ ] = [ ] ;
834
+
820
835
/**
821
836
* Root file path, relative to workspace hierarchy root
822
837
*/
@@ -848,12 +863,18 @@ export class ProjectConfiguration {
848
863
configFilePath : string ,
849
864
configContent ?: any ,
850
865
traceModuleResolution ?: boolean ,
866
+ initializationOptions ?: InitializationOptions ,
851
867
private logger : Logger = new NoopLogger ( )
852
868
) {
853
869
this . fs = fs ;
854
870
this . configFilePath = configFilePath ;
855
871
this . configContent = configContent ;
856
872
this . versions = versions ;
873
+ if ( initializationOptions ) {
874
+ this . allowLocalPluginLoads = initializationOptions . allowLocalPluginLoads || false ;
875
+ this . globalPlugins = initializationOptions . globalPlugins || [ ] ;
876
+ this . pluginProbeLocations = initializationOptions . pluginProbeLocations || [ ] ;
877
+ }
857
878
this . traceModuleResolution = traceModuleResolution || false ;
858
879
this . rootFilePath = rootFilePath ;
859
880
}
@@ -961,27 +982,41 @@ export class ProjectConfiguration {
961
982
}
962
983
963
984
private enablePlugins ( options : ts . CompilerOptions ) {
964
- const searchPaths = [ ] ;
965
- // TODO: support pluginProbeLocations?
966
- // TODO: add peer node_modules to source path.
967
-
968
- // TODO: determine how to expose this setting.
969
- // VS Code starts tsserver with --allowLocalPluginLoads by default: https://github.com/Microsoft/TypeScript/pull/15924
970
- const allowLocalPluginLoads = true ;
971
- if ( allowLocalPluginLoads ) {
985
+ // Search our peer node_modules, then any globally-specified probe paths
986
+ // ../../.. to walk from X/node_modules/javascript-typescript-langserver/lib/project-manager.js to X/node_modules/
987
+ const searchPaths = [ combinePaths ( __filename , '../../..' ) , ...this . pluginProbeLocations ] ;
988
+
989
+ // Corresponds to --allowLocalPluginLoads, opt-in to avoid remote code execution.
990
+ if ( this . allowLocalPluginLoads ) {
972
991
const local = this . rootFilePath ;
973
992
this . logger . info ( `Local plugin loading enabled; adding ${ local } to search paths` ) ;
974
993
searchPaths . unshift ( local ) ;
975
994
}
976
995
996
+ let pluginImports : ts . PluginImport [ ] = [ ] ;
997
+ if ( options . plugins ) {
998
+ pluginImports = options . plugins as ts . PluginImport [ ] ;
999
+ }
1000
+
977
1001
// Enable tsconfig-specified plugins
978
1002
if ( options . plugins ) {
979
- for ( const pluginConfigEntry of options . plugins as ts . PluginImport [ ] ) {
1003
+ for ( const pluginConfigEntry of pluginImports ) {
980
1004
this . enablePlugin ( pluginConfigEntry , searchPaths ) ;
981
1005
}
982
1006
}
983
1007
984
- // TODO: support globalPlugins flag if desired
1008
+ if ( this . globalPlugins ) {
1009
+ // Enable global plugins with synthetic configuration entries
1010
+ for ( const globalPluginName of this . globalPlugins ) {
1011
+ // Skip already-locally-loaded plugins
1012
+ if ( ! pluginImports || pluginImports . some ( p => p . name === globalPluginName ) ) {
1013
+ continue ;
1014
+ }
1015
+
1016
+ // Provide global: true so plugins can detect why they can't find their config
1017
+ this . enablePlugin ( { name : globalPluginName , global : true } as ts . PluginImport , searchPaths ) ;
1018
+ }
1019
+ }
985
1020
}
986
1021
987
1022
/**
@@ -1006,9 +1041,7 @@ export class ProjectConfiguration {
1006
1041
* @param initialDir
1007
1042
*/
1008
1043
private resolveModule ( moduleName : string , initialDir : string ) : { } | undefined {
1009
- // const resolvedPath = path.resolve(initialDir, 'node_modules');
1010
- const resolvedPath = '' ;
1011
- this . logger . info ( `Loading ${ moduleName } from ${ initialDir } (resolved to ${ resolvedPath } )` ) ;
1044
+ this . logger . info ( `Loading ${ moduleName } from ${ initialDir } ` ) ;
1012
1045
const result = this . requirePlugin ( initialDir , moduleName ) ;
1013
1046
if ( result . error ) {
1014
1047
this . logger . info ( `Failed to load module: ${ JSON . stringify ( result . error ) } ` ) ;
@@ -1031,20 +1064,18 @@ export class ProjectConfiguration {
1031
1064
}
1032
1065
}
1033
1066
1034
- // TODO: stolen from moduleNameResolver.ts because marked as internal
1035
1067
/**
1036
1068
* Expose resolution logic to allow us to use Node module resolution logic from arbitrary locations.
1037
1069
* No way to do this with `require()`: https://github.com/nodejs/node/issues/5963
1038
1070
* Throws an error if the module can't be resolved.
1071
+ * stolen from moduleNameResolver.ts because marked as internal
1039
1072
*/
1040
1073
private resolveJavaScriptModule ( moduleName : string , initialDir : string , host : ts . ModuleResolutionHost ) : string {
1041
- // const { resolvedModule /* , failedLookupLocations */ } =
1074
+ // TODO: this should set jsOnly=true to the internal resolver, but this parameter is not exposed on a public api.
1042
1075
const result =
1043
1076
ts . nodeModuleNameResolver ( moduleName , /* containingFile */ initialDir . replace ( '\\' , '/' ) + '/package.json' , { moduleResolution : ts . ModuleResolutionKind . NodeJs , allowJs : true } , this . fs , undefined ) ;
1044
- // TODO: jsOnly flag missing :(
1045
1077
if ( ! result . resolvedModule ) {
1046
- // TODO: add Looked in: ${failedLookupLocations.join(', ')} back into error.
1047
- // this.logger.error(result.failedLookupLocations!);
1078
+ // this.logger.error(result.failedLookupLocations);
1048
1079
throw new Error ( `Could not resolve JS module ${ moduleName } starting at ${ initialDir } .` ) ;
1049
1080
}
1050
1081
return result . resolvedModule . resolvedFileName ;
0 commit comments