@@ -7,7 +7,9 @@ import * as ts from 'typescript';
7
7
import { Disposable } from './disposable' ;
8
8
import { FileSystemUpdater } from './fs' ;
9
9
import { Logger , NoopLogger } from './logging' ;
10
+ import { combinePaths } from './match-files' ;
10
11
import { InMemoryFileSystem } from './memfs' ;
12
+ import { InitializationOptions } from './request-type' ;
11
13
import { traceObservable , traceSync } from './tracing' ;
12
14
import {
13
15
isConfigFile ,
@@ -140,6 +142,11 @@ export class ProjectManager implements Disposable {
140
142
*/
141
143
private subscriptions = new Subscription ( ) ;
142
144
145
+ /**
146
+ * Options passed to the language server at startup
147
+ */
148
+ private initializationOptions ?: InitializationOptions ;
149
+
143
150
/**
144
151
* @param rootPath root path as passed to `initialize`
145
152
* @param inMemoryFileSystem File system that keeps structure and contents in memory
@@ -151,12 +158,14 @@ export class ProjectManager implements Disposable {
151
158
inMemoryFileSystem : InMemoryFileSystem ,
152
159
updater : FileSystemUpdater ,
153
160
traceModuleResolution ?: boolean ,
161
+ initializationOptions ?: InitializationOptions ,
154
162
protected logger : Logger = new NoopLogger ( )
155
163
) {
156
164
this . rootPath = rootPath ;
157
165
this . updater = updater ;
158
166
this . inMemoryFs = inMemoryFileSystem ;
159
167
this . versions = new Map < string , number > ( ) ;
168
+ this . initializationOptions = initializationOptions ;
160
169
this . traceModuleResolution = traceModuleResolution || false ;
161
170
162
171
// Share DocumentRegistry between all ProjectConfigurations
@@ -184,6 +193,7 @@ export class ProjectManager implements Disposable {
184
193
'' ,
185
194
tsConfig ,
186
195
this . traceModuleResolution ,
196
+ this . initializationOptions ,
187
197
this . logger
188
198
) ;
189
199
configs . set ( trimmedRootPath , config ) ;
@@ -213,6 +223,7 @@ export class ProjectManager implements Disposable {
213
223
filePath ,
214
224
undefined ,
215
225
this . traceModuleResolution ,
226
+ this . initializationOptions ,
216
227
this . logger
217
228
) ) ;
218
229
// Remove catch-all config (if exists)
@@ -769,6 +780,10 @@ export class ProjectConfiguration {
769
780
*/
770
781
private traceModuleResolution : boolean ;
771
782
783
+ private allowLocalPluginLoads : boolean = false ;
784
+ private globalPlugins : string [ ] = [ ] ;
785
+ private pluginProbeLocations : string [ ] = [ ] ;
786
+
772
787
/**
773
788
* Root file path, relative to workspace hierarchy root
774
789
*/
@@ -795,12 +810,18 @@ export class ProjectConfiguration {
795
810
configFilePath : string ,
796
811
configContent ?: any ,
797
812
traceModuleResolution ?: boolean ,
813
+ initializationOptions ?: InitializationOptions ,
798
814
private logger : Logger = new NoopLogger ( )
799
815
) {
800
816
this . fs = fs ;
801
817
this . configFilePath = configFilePath ;
802
818
this . configContent = configContent ;
803
819
this . versions = versions ;
820
+ if ( initializationOptions ) {
821
+ this . allowLocalPluginLoads = initializationOptions . allowLocalPluginLoads || false ;
822
+ this . globalPlugins = initializationOptions . globalPlugins || [ ] ;
823
+ this . pluginProbeLocations = initializationOptions . pluginProbeLocations || [ ] ;
824
+ }
804
825
this . traceModuleResolution = traceModuleResolution || false ;
805
826
this . rootFilePath = rootFilePath ;
806
827
}
@@ -903,27 +924,41 @@ export class ProjectConfiguration {
903
924
}
904
925
905
926
private enablePlugins ( options : ts . CompilerOptions ) {
906
- const searchPaths = [ ] ;
907
- // TODO: support pluginProbeLocations?
908
- // TODO: add peer node_modules to source path.
909
-
910
- // TODO: determine how to expose this setting.
911
- // VS Code starts tsserver with --allowLocalPluginLoads by default: https://github.com/Microsoft/TypeScript/pull/15924
912
- const allowLocalPluginLoads = true ;
913
- if ( allowLocalPluginLoads ) {
927
+ // Search our peer node_modules, then any globally-specified probe paths
928
+ // ../../.. to walk from X/node_modules/javascript-typescript-langserver/lib/project-manager.js to X/node_modules/
929
+ const searchPaths = [ combinePaths ( __filename , '../../..' ) , ...this . pluginProbeLocations ] ;
930
+
931
+ // Corresponds to --allowLocalPluginLoads, opt-in to avoid remote code execution.
932
+ if ( this . allowLocalPluginLoads ) {
914
933
const local = this . rootFilePath ;
915
934
this . logger . info ( `Local plugin loading enabled; adding ${ local } to search paths` ) ;
916
935
searchPaths . unshift ( local ) ;
917
936
}
918
937
938
+ let pluginImports : ts . PluginImport [ ] = [ ] ;
939
+ if ( options . plugins ) {
940
+ pluginImports = options . plugins as ts . PluginImport [ ] ;
941
+ }
942
+
919
943
// Enable tsconfig-specified plugins
920
944
if ( options . plugins ) {
921
- for ( const pluginConfigEntry of options . plugins as ts . PluginImport [ ] ) {
945
+ for ( const pluginConfigEntry of pluginImports ) {
922
946
this . enablePlugin ( pluginConfigEntry , searchPaths ) ;
923
947
}
924
948
}
925
949
926
- // TODO: support globalPlugins flag if desired
950
+ if ( this . globalPlugins ) {
951
+ // Enable global plugins with synthetic configuration entries
952
+ for ( const globalPluginName of this . globalPlugins ) {
953
+ // Skip already-locally-loaded plugins
954
+ if ( ! pluginImports || pluginImports . some ( p => p . name === globalPluginName ) ) {
955
+ continue ;
956
+ }
957
+
958
+ // Provide global: true so plugins can detect why they can't find their config
959
+ this . enablePlugin ( { name : globalPluginName , global : true } as ts . PluginImport , searchPaths ) ;
960
+ }
961
+ }
927
962
}
928
963
929
964
/**
@@ -948,9 +983,7 @@ export class ProjectConfiguration {
948
983
* @param initialDir
949
984
*/
950
985
private resolveModule ( moduleName : string , initialDir : string ) : { } | undefined {
951
- // const resolvedPath = path.resolve(initialDir, 'node_modules');
952
- const resolvedPath = '' ;
953
- this . logger . info ( `Loading ${ moduleName } from ${ initialDir } (resolved to ${ resolvedPath } )` ) ;
986
+ this . logger . info ( `Loading ${ moduleName } from ${ initialDir } ` ) ;
954
987
const result = this . requirePlugin ( initialDir , moduleName ) ;
955
988
if ( result . error ) {
956
989
this . logger . info ( `Failed to load module: ${ JSON . stringify ( result . error ) } ` ) ;
@@ -973,20 +1006,18 @@ export class ProjectConfiguration {
973
1006
}
974
1007
}
975
1008
976
- // TODO: stolen from moduleNameResolver.ts because marked as internal
977
1009
/**
978
1010
* Expose resolution logic to allow us to use Node module resolution logic from arbitrary locations.
979
1011
* No way to do this with `require()`: https://github.com/nodejs/node/issues/5963
980
1012
* Throws an error if the module can't be resolved.
1013
+ * stolen from moduleNameResolver.ts because marked as internal
981
1014
*/
982
1015
private resolveJavaScriptModule ( moduleName : string , initialDir : string , host : ts . ModuleResolutionHost ) : string {
983
- // const { resolvedModule /* , failedLookupLocations */ } =
1016
+ // TODO: this should set jsOnly=true to the internal resolver, but this parameter is not exposed on a public api.
984
1017
const result =
985
1018
ts . nodeModuleNameResolver ( moduleName , /* containingFile */ initialDir . replace ( '\\' , '/' ) + '/package.json' , { moduleResolution : ts . ModuleResolutionKind . NodeJs , allowJs : true } , this . fs , undefined ) ;
986
- // TODO: jsOnly flag missing :(
987
1019
if ( ! result . resolvedModule ) {
988
- // TODO: add Looked in: ${failedLookupLocations.join(', ')} back into error.
989
- // this.logger.error(result.failedLookupLocations!);
1020
+ // this.logger.error(result.failedLookupLocations);
990
1021
throw new Error ( `Could not resolve JS module ${ moduleName } starting at ${ initialDir } .` ) ;
991
1022
}
992
1023
return result . resolvedModule . resolvedFileName ;
0 commit comments