Skip to content

Commit 4273fd7

Browse files
committed
configurePlugins command for tsserver
Implements microsoft#18604
1 parent 53105b3 commit 4273fd7

File tree

5 files changed

+97
-28
lines changed

5 files changed

+97
-28
lines changed

src/server/editorServices.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,8 @@ namespace ts.server {
471471
public readonly globalPlugins: ReadonlyArray<string>;
472472
public readonly pluginProbeLocations: ReadonlyArray<string>;
473473
public readonly allowLocalPluginLoads: boolean;
474+
private currentPluginConfigOverrides: Map<any> | undefined;
475+
474476
public readonly typesMapLocation: string | undefined;
475477

476478
public readonly syntaxOnly?: boolean;
@@ -1667,7 +1669,7 @@ namespace ts.server {
16671669
project.enableLanguageService();
16681670
project.watchWildcards(createMapFromTemplate(parsedCommandLine.wildcardDirectories!)); // TODO: GH#18217
16691671
}
1670-
project.enablePluginsWithOptions(compilerOptions);
1672+
project.enablePluginsWithOptions(compilerOptions, this.currentPluginConfigOverrides);
16711673
const filesToAdd = parsedCommandLine.fileNames.concat(project.getExternalFiles());
16721674
this.updateRootAndOptionsOfNonInferredProject(project, filesToAdd, fileNamePropertyReader, compilerOptions, parsedCommandLine.typeAcquisition!, parsedCommandLine.compileOnSave!); // TODO: GH#18217
16731675
}
@@ -1857,7 +1859,7 @@ namespace ts.server {
18571859

18581860
private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject {
18591861
const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects;
1860-
const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory);
1862+
const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory, this.currentPluginConfigOverrides);
18611863
if (isSingleInferredProject) {
18621864
this.inferredProjects.unshift(project);
18631865
}
@@ -2806,6 +2808,16 @@ namespace ts.server {
28062808

28072809
return false;
28082810
}
2811+
2812+
configurePlugin(args: protocol.ConfigurePluginRequestArguments) {
2813+
// For any projects that already have the plugin loaded, configure the plugin
2814+
this.forEachEnabledProject(project => project.onPluginConfigurationChanged(args.pluginName, args.configuration));
2815+
2816+
// Also save the current configuration to pass on to any projects that are yet to be loaded.
2817+
// If a plugin is configured twice, only the latest configuration will be remembered.
2818+
this.currentPluginConfigOverrides = this.currentPluginConfigOverrides || createMap();
2819+
this.currentPluginConfigOverrides.set(args.pluginName, args.configuration);
2820+
}
28092821
}
28102822

28112823
/* @internal */

src/server/project.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ namespace ts.server {
7272
export interface PluginModule {
7373
create(createInfo: PluginCreateInfo): LanguageService;
7474
getExternalFiles?(proj: Project): string[];
75+
onConfigurationChanged?(config: any): void;
76+
}
77+
78+
export interface PluginModuleWithName {
79+
name: string;
80+
module: PluginModule;
7581
}
7682

7783
export type PluginModuleFactory = (mod: { typescript: typeof ts }) => PluginModule;
@@ -92,7 +98,7 @@ namespace ts.server {
9298
private program: Program;
9399
private externalFiles: SortedReadonlyArray<string>;
94100
private missingFilesMap: Map<FileWatcher>;
95-
private plugins: PluginModule[] = [];
101+
private plugins: PluginModuleWithName[] = [];
96102

97103
/*@internal*/
98104
/**
@@ -549,9 +555,9 @@ namespace ts.server {
549555

550556
getExternalFiles(): SortedReadonlyArray<string> {
551557
return toSortedArray(flatMap(this.plugins, plugin => {
552-
if (typeof plugin.getExternalFiles !== "function") return;
558+
if (typeof plugin.module.getExternalFiles !== "function") return;
553559
try {
554-
return plugin.getExternalFiles(this);
560+
return plugin.module.getExternalFiles(this);
555561
}
556562
catch (e) {
557563
this.projectService.logger.info(`A plugin threw an exception in getExternalFiles: ${e}`);
@@ -1105,7 +1111,7 @@ namespace ts.server {
11051111
this.rootFilesMap.delete(info.path);
11061112
}
11071113

1108-
protected enableGlobalPlugins(options: CompilerOptions) {
1114+
protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map<any> | undefined) {
11091115
const host = this.projectService.host;
11101116

11111117
if (!host.require) {
@@ -1128,12 +1134,13 @@ namespace ts.server {
11281134

11291135
// Provide global: true so plugins can detect why they can't find their config
11301136
this.projectService.logger.info(`Loading global plugin ${globalPluginName}`);
1131-
this.enablePlugin({ name: globalPluginName, global: true } as PluginImport, searchPaths);
1137+
1138+
this.enablePlugin({ name: globalPluginName, global: true } as PluginImport, searchPaths, pluginConfigOverrides);
11321139
}
11331140
}
11341141
}
11351142

1136-
protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[]) {
1143+
protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map<any> | undefined) {
11371144
this.projectService.logger.info(`Enabling plugin ${pluginConfigEntry.name} from candidate paths: ${searchPaths.join(",")}`);
11381145

11391146
const log = (message: string) => {
@@ -1143,18 +1150,21 @@ namespace ts.server {
11431150
const resolvedModule = firstDefined(searchPaths, searchPath =>
11441151
<PluginModuleFactory | undefined>Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, log));
11451152
if (resolvedModule) {
1153+
const configurationOverride = pluginConfigOverrides && pluginConfigOverrides.get(pluginConfigEntry.name);
1154+
if (configurationOverride) {
1155+
// Preserve the name property since it's immutable
1156+
const pluginName = pluginConfigEntry.name;
1157+
pluginConfigEntry = configurationOverride;
1158+
pluginConfigEntry.name = pluginName;
1159+
}
1160+
11461161
this.enableProxy(resolvedModule, pluginConfigEntry);
11471162
}
11481163
else {
11491164
this.projectService.logger.info(`Couldn't find ${pluginConfigEntry.name}`);
11501165
}
11511166
}
11521167

1153-
/** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */
1154-
refreshDiagnostics() {
1155-
this.projectService.sendProjectsUpdatedInBackgroundEvent();
1156-
}
1157-
11581168
private enableProxy(pluginModuleFactory: PluginModuleFactory, configEntry: PluginImport) {
11591169
try {
11601170
if (typeof pluginModuleFactory !== "function") {
@@ -1180,12 +1190,26 @@ namespace ts.server {
11801190
}
11811191
this.projectService.logger.info(`Plugin validation succeded`);
11821192
this.languageService = newLS;
1183-
this.plugins.push(pluginModule);
1193+
this.plugins.push({ name: configEntry.name, module: pluginModule });
11841194
}
11851195
catch (e) {
11861196
this.projectService.logger.info(`Plugin activation failed: ${e}`);
11871197
}
11881198
}
1199+
1200+
/*@internal*/
1201+
onPluginConfigurationChanged(pluginName: string, configuration: any) {
1202+
this.plugins.filter(plugin => plugin.name === pluginName).forEach(plugin => {
1203+
if (plugin.module.onConfigurationChanged) {
1204+
plugin.module.onConfigurationChanged(configuration);
1205+
}
1206+
});
1207+
}
1208+
1209+
/** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */
1210+
refreshDiagnostics() {
1211+
this.projectService.sendProjectsUpdatedInBackgroundEvent();
1212+
}
11891213
}
11901214

11911215
/**
@@ -1241,7 +1265,8 @@ namespace ts.server {
12411265
documentRegistry: DocumentRegistry,
12421266
compilerOptions: CompilerOptions,
12431267
projectRootPath: NormalizedPath | undefined,
1244-
currentDirectory: string | undefined) {
1268+
currentDirectory: string | undefined,
1269+
pluginConfigOverrides: Map<any> | undefined) {
12451270
super(InferredProject.newName(),
12461271
ProjectKind.Inferred,
12471272
projectService,
@@ -1257,7 +1282,7 @@ namespace ts.server {
12571282
if (!projectRootPath && !projectService.useSingleInferredProject) {
12581283
this.canonicalCurrentDirectory = projectService.toCanonicalFileName(this.currentDirectory);
12591284
}
1260-
this.enableGlobalPlugins(this.getCompilerOptions());
1285+
this.enableGlobalPlugins(this.getCompilerOptions(), pluginConfigOverrides);
12611286
}
12621287

12631288
addRoot(info: ScriptInfo) {
@@ -1402,12 +1427,8 @@ namespace ts.server {
14021427
return program && program.getResolvedProjectReferences();
14031428
}
14041429

1405-
enablePlugins() {
1406-
this.enablePluginsWithOptions(this.getCompilerOptions());
1407-
}
1408-
14091430
/*@internal*/
1410-
enablePluginsWithOptions(options: CompilerOptions) {
1431+
enablePluginsWithOptions(options: CompilerOptions, pluginConfigOverrides: Map<any> | undefined) {
14111432
const host = this.projectService.host;
14121433

14131434
if (!host.require) {
@@ -1428,11 +1449,11 @@ namespace ts.server {
14281449
// Enable tsconfig-specified plugins
14291450
if (options.plugins) {
14301451
for (const pluginConfigEntry of options.plugins) {
1431-
this.enablePlugin(pluginConfigEntry, searchPaths);
1452+
this.enablePlugin(pluginConfigEntry, searchPaths, pluginConfigOverrides);
14321453
}
14331454
}
14341455

1435-
this.enableGlobalPlugins(options);
1456+
this.enableGlobalPlugins(options, pluginConfigOverrides);
14361457
}
14371458

14381459
/**

src/server/protocol.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ namespace ts.server.protocol {
129129
GetEditsForFileRename = "getEditsForFileRename",
130130
/* @internal */
131131
GetEditsForFileRenameFull = "getEditsForFileRename-full",
132+
ConfigurePlugin = "configurePlugin"
132133

133134
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
134135
}
@@ -1368,6 +1369,16 @@ namespace ts.server.protocol {
13681369
export interface ConfigureResponse extends Response {
13691370
}
13701371

1372+
export interface ConfigurePluginRequestArguments {
1373+
pluginName: string;
1374+
configuration: any;
1375+
}
1376+
1377+
export interface ConfigurePluginRequest extends Request {
1378+
command: CommandTypes.ConfigurePlugin;
1379+
arguments: ConfigurePluginRequestArguments;
1380+
}
1381+
13711382
/**
13721383
* Information found in an "open" request.
13731384
*/

src/server/session.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1946,6 +1946,10 @@ namespace ts.server {
19461946
this.updateErrorCheck(next, checkList, delay, /*requireOpen*/ false);
19471947
}
19481948

1949+
private configurePlugin(args: protocol.ConfigurePluginRequestArguments) {
1950+
this.projectService.configurePlugin(args);
1951+
}
1952+
19491953
getCanonicalFileName(fileName: string) {
19501954
const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
19511955
return normalizePath(name);
@@ -2267,6 +2271,10 @@ namespace ts.server {
22672271
[CommandNames.GetEditsForFileRenameFull]: (request: protocol.GetEditsForFileRenameRequest) => {
22682272
return this.requiredResponse(this.getEditsForFileRename(request.arguments, /*simplifiedResult*/ false));
22692273
},
2274+
[CommandNames.ConfigurePlugin]: (request: protocol.ConfigurePluginRequest) => {
2275+
this.configurePlugin(request.arguments);
2276+
return this.notRequired();
2277+
}
22702278
});
22712279

22722280
public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) {

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5668,7 +5668,8 @@ declare namespace ts.server.protocol {
56685668
GetApplicableRefactors = "getApplicableRefactors",
56695669
GetEditsForRefactor = "getEditsForRefactor",
56705670
OrganizeImports = "organizeImports",
5671-
GetEditsForFileRename = "getEditsForFileRename"
5671+
GetEditsForFileRename = "getEditsForFileRename",
5672+
ConfigurePlugin = "configurePlugin"
56725673
}
56735674
/**
56745675
* A TypeScript Server message
@@ -6609,6 +6610,14 @@ declare namespace ts.server.protocol {
66096610
*/
66106611
interface ConfigureResponse extends Response {
66116612
}
6613+
interface ConfigurePluginRequestArguments {
6614+
pluginName: string;
6615+
configuration: any;
6616+
}
6617+
interface ConfigurePluginRequest extends Request {
6618+
command: CommandTypes.ConfigurePlugin;
6619+
arguments: ConfigurePluginRequestArguments;
6620+
}
66126621
/**
66136622
* Information found in an "open" request.
66146623
*/
@@ -8035,6 +8044,11 @@ declare namespace ts.server {
80358044
interface PluginModule {
80368045
create(createInfo: PluginCreateInfo): LanguageService;
80378046
getExternalFiles?(proj: Project): string[];
8047+
onConfigurationChanged?(config: any): void;
8048+
}
8049+
interface PluginModuleWithName {
8050+
name: string;
8051+
module: PluginModule;
80388052
}
80398053
type PluginModuleFactory = (mod: {
80408054
typescript: typeof ts;
@@ -8173,11 +8187,12 @@ declare namespace ts.server {
81738187
filesToString(writeProjectFileNames: boolean): string;
81748188
setCompilerOptions(compilerOptions: CompilerOptions): void;
81758189
protected removeRoot(info: ScriptInfo): void;
8176-
protected enableGlobalPlugins(options: CompilerOptions): void;
8177-
protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[]): void;
8190+
protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map<any> | undefined): void;
8191+
protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map<any> | undefined): void;
8192+
private enableProxy;
8193+
onPluginConfigurationChanged(pluginName: string, configuration: any): void;
81788194
/** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */
81798195
refreshDiagnostics(): void;
8180-
private enableProxy;
81818196
}
81828197
/**
81838198
* If a file is opened and no tsconfig (or jsconfig) is found,
@@ -8218,7 +8233,6 @@ declare namespace ts.server {
82188233
getConfigFilePath(): NormalizedPath;
82198234
getProjectReferences(): ReadonlyArray<ProjectReference>;
82208235
updateReferences(refs: ReadonlyArray<ProjectReference> | undefined): void;
8221-
enablePlugins(): void;
82228236
/**
82238237
* Get the errors that dont have any file name associated
82248238
*/
@@ -8463,6 +8477,7 @@ declare namespace ts.server {
84638477
readonly globalPlugins: ReadonlyArray<string>;
84648478
readonly pluginProbeLocations: ReadonlyArray<string>;
84658479
readonly allowLocalPluginLoads: boolean;
8480+
private currentPluginConfigOverrides;
84668481
readonly typesMapLocation: string | undefined;
84678482
readonly syntaxOnly?: boolean;
84688483
/** Tracks projects that we have already sent telemetry for. */
@@ -8638,6 +8653,7 @@ declare namespace ts.server {
86388653
applySafeList(proj: protocol.ExternalProject): NormalizedPath[];
86398654
openExternalProject(proj: protocol.ExternalProject): void;
86408655
hasDeferredExtension(): boolean;
8656+
configurePlugin(args: protocol.ConfigurePluginRequestArguments): void;
86418657
}
86428658
}
86438659
declare namespace ts.server {
@@ -8808,6 +8824,7 @@ declare namespace ts.server {
88088824
private convertTextChangeToCodeEdit;
88098825
private getBraceMatching;
88108826
private getDiagnosticsForProject;
8827+
private configurePlugin;
88118828
getCanonicalFileName(fileName: string): string;
88128829
exit(): void;
88138830
private notRequired;

0 commit comments

Comments
 (0)