Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable search of local Typescript Language Service Plugins (fix #45856) #45858

Merged
merged 2 commits into from
Mar 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions extensions/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@
"description": "%typescript.tsserver.log%",
"scope": "window"
},
"typescript.tsserver.pluginPaths": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add "isExecutable": true here. This prevents this from ever being used as a workspace setting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you think it should be a user-only setting?
Specifying pluginPaths on workspace level can be quite useful, especially in conjunction with relative paths. In this case plugins configuration can be committed to a source control and shared between team members.
If this is this because of the security concerns, I don't believe this is a more dangerous option than ability to specify typescript.tsdk on a workspace-level.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is for security reasons. Tsdk asks for user confirmation before executing any code in the workspace but that flow is complicated so I don't want to duplicate it here. Instead we should just limit these this to be a user only setting

"type": "array",
"items": {
"type": "string",
"description": "%typescript.tsserver.pluginPaths.item%"
},
"default": [],
"description": "%typescript.tsserver.pluginPaths%",
"scope": "window",
"isExecutable": true
},
"typescript.tsserver.trace": {
"type": "string",
"enum": [
Expand Down
2 changes: 2 additions & 0 deletions extensions/typescript/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"typescript.tsdk.desc": "Specifies the folder path containing the tsserver and lib*.d.ts files to use.",
"typescript.disableAutomaticTypeAcquisition": "Disables automatic type acquisition. Requires TypeScript >= 2.0.6.",
"typescript.tsserver.log": "Enables logging of the TS server to a file. This log can be used to diagnose TS Server issues. The log may contain file paths, source code, and other potentially sensitive information from your project.",
"typescript.tsserver.pluginPaths": "Additional paths to discover Typescript Language Service plugins. Requires TypeScript >= 2.3.0.",
"typescript.tsserver.pluginPaths.item": "Either an absolute or relative path. Relative path will be resolved against workspace folder(s).",
"typescript.tsserver.trace": "Enables tracing of messages sent to the TS server. This trace can be used to diagnose TS Server issues. The trace may contain file paths, source code, and other potentially sensitive information from your project.",
"typescript.validate.enable": "Enable/disable TypeScript validation.",
"typescript.format.enable": "Enable/disable default TypeScript formatter.",
Expand Down
16 changes: 13 additions & 3 deletions extensions/typescript/src/typescriptServiceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { inferredProjectConfig } from './utils/tsconfig';
import LogDirectoryProvider from './utils/logDirectoryProvider';
import { disposeAll } from './utils/dipose';
import { DiagnosticKind } from './features/diagnostics';
import { TypeScriptPluginPathsProvider } from './utils/pluginPathsProvider';

const localize = nls.loadMessageBundle();

Expand Down Expand Up @@ -154,6 +155,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
private _onReady?: { promise: Promise<void>; resolve: () => void; reject: () => void; };
private _configuration: TypeScriptServiceConfiguration;
private versionProvider: TypeScriptVersionProvider;
private pluginPathsProvider: TypeScriptPluginPathsProvider;
private versionPicker: TypeScriptVersionPicker;

private tracer: Tracer;
Expand Down Expand Up @@ -210,6 +212,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
this.callbacks = new CallbackMap();
this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace();
this.versionProvider = new TypeScriptVersionProvider(this._configuration);
this.pluginPathsProvider = new TypeScriptPluginPathsProvider(this._configuration);
this.versionPicker = new TypeScriptVersionPicker(this.versionProvider, this.workspaceState);

this._apiVersion = API.defaultVersion;
Expand All @@ -221,6 +224,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace();

this.versionProvider.updateConfiguration(this._configuration);
this.pluginPathsProvider.updateConfiguration(this._configuration);
this.tracer.updateConfiguration();

if (this.servicePromise) {
Expand Down Expand Up @@ -791,7 +795,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
this.tracer.traceEvent(event);
this.dispatchEvent(event);
} else {
throw new Error('Unknown message type ' + message.type + ' recevied');
throw new Error('Unknown message type ' + message.type + ' received');
}
} finally {
this.sendNextRequests();
Expand Down Expand Up @@ -938,12 +942,19 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
}

if (this.apiVersion.has230Features()) {
const pluginPaths = this.pluginPathsProvider.getPluginPaths();

if (this.plugins.length) {
args.push('--globalPlugins', this.plugins.map(x => x.name).join(','));

if (currentVersion.path === this.versionProvider.defaultVersion.path) {
args.push('--pluginProbeLocations', this.plugins.map(x => x.path).join(','));
pluginPaths.push(...this.plugins.map(x => x.path));
}
}

if (pluginPaths.length !== 0) {
args.push('--pluginProbeLocations', pluginPaths.join(','));
}
}

if (this.apiVersion.has234Features()) {
Expand All @@ -961,7 +972,6 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
return args;
}


private getDebugPort(): number | undefined {
const value = process.env['TSS_DEBUG'];
if (value) {
Expand Down
17 changes: 17 additions & 0 deletions extensions/typescript/src/utils/arrays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function equals<T>(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean {
if (one.length !== other.length) {
return false;
}

for (let i = 0, len = one.length; i < len; i++) {
if (!itemEquals(one[i], other[i])) {
return false;
}
}

return true;
}
10 changes: 9 additions & 1 deletion extensions/typescript/src/utils/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { WorkspaceConfiguration, workspace } from 'vscode';
import * as arrays from './arrays';

export enum TsServerLogLevel {
Off,
Expand Down Expand Up @@ -47,6 +48,7 @@ export class TypeScriptServiceConfiguration {
public readonly localTsdk: string | null;
public readonly npmLocation: string | null;
public readonly tsServerLogLevel: TsServerLogLevel = TsServerLogLevel.Off;
public readonly tsServerPluginPaths: string[];
public readonly checkJs: boolean;
public readonly experimentalDecorators: boolean;
public readonly disableAutomaticTypeAcquisition: boolean;
Expand All @@ -63,6 +65,7 @@ export class TypeScriptServiceConfiguration {
this.localTsdk = TypeScriptServiceConfiguration.extractLocalTsdk(configuration);
this.npmLocation = TypeScriptServiceConfiguration.readNpmLocation(configuration);
this.tsServerLogLevel = TypeScriptServiceConfiguration.readTsServerLogLevel(configuration);
this.tsServerPluginPaths = TypeScriptServiceConfiguration.readTsServerPluginPaths(configuration);
this.checkJs = TypeScriptServiceConfiguration.readCheckJs(configuration);
this.experimentalDecorators = TypeScriptServiceConfiguration.readExperimentalDecorators(configuration);
this.disableAutomaticTypeAcquisition = TypeScriptServiceConfiguration.readDisableAutomaticTypeAcquisition(configuration);
Expand All @@ -76,7 +79,8 @@ export class TypeScriptServiceConfiguration {
&& this.tsServerLogLevel === other.tsServerLogLevel
&& this.checkJs === other.checkJs
&& this.experimentalDecorators === other.experimentalDecorators
&& this.disableAutomaticTypeAcquisition === other.disableAutomaticTypeAcquisition;
&& this.disableAutomaticTypeAcquisition === other.disableAutomaticTypeAcquisition
&& arrays.equals(this.tsServerPluginPaths, other.tsServerPluginPaths);
}

private static extractGlobalTsdk(configuration: WorkspaceConfiguration): string | null {
Expand All @@ -100,6 +104,10 @@ export class TypeScriptServiceConfiguration {
return TsServerLogLevel.fromString(setting);
}

private static readTsServerPluginPaths(configuration: WorkspaceConfiguration): string[] {
return configuration.get<string[]>('typescript.tsserver.pluginPaths', []);
}

private static readCheckJs(configuration: WorkspaceConfiguration): boolean {
return configuration.get<boolean>('javascript.implicitProjectConfig.checkJs', false);
}
Expand Down
43 changes: 43 additions & 0 deletions extensions/typescript/src/utils/pluginPathsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { workspace } from 'vscode';

import { TypeScriptServiceConfiguration } from './configuration';
import { RelativeWorkspacePathResolver } from './relativePathResolver';

export class TypeScriptPluginPathsProvider {
public readonly relativePathResolver: RelativeWorkspacePathResolver = new RelativeWorkspacePathResolver();

public constructor(
private configuration: TypeScriptServiceConfiguration
) { }

public updateConfiguration(configuration: TypeScriptServiceConfiguration): void {
this.configuration = configuration;
}

public getPluginPaths(): string[] {
const pluginPaths = [];
for (const pluginPath of this.configuration.tsServerPluginPaths) {
pluginPaths.push(...this.resolvePluginPath(pluginPath));
}
return pluginPaths;
}

private resolvePluginPath(pluginPath: string): string[] {
if (path.isAbsolute(pluginPath)) {
return [pluginPath];
}

const workspacePath = this.relativePathResolver.asAbsoluteWorkspacePath(pluginPath);
if (workspacePath !== undefined) {
return [workspacePath];
}

return (workspace.workspaceFolders || [])
.map(workspaceFolder => path.join(workspaceFolder.uri.fsPath, pluginPath));
}
}
21 changes: 21 additions & 0 deletions extensions/typescript/src/utils/relativePathResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { workspace } from 'vscode';

export class RelativeWorkspacePathResolver {
public asAbsoluteWorkspacePath(relativePath: string): string | undefined {
for (const root of workspace.workspaceFolders || []) {
const rootPrefixes = [`./${root.name}/`, `${root.name}/`, `.\\${root.name}\\`, `${root.name}\\`];
for (const rootPrefix of rootPrefixes) {
if (relativePath.startsWith(rootPrefix)) {
return path.join(root.uri.fsPath, relativePath.replace(rootPrefix, ''));
}
}
}

return undefined;
}
}
14 changes: 6 additions & 8 deletions extensions/typescript/src/utils/versionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as fs from 'fs';
import { workspace, window } from 'vscode';

import { TypeScriptServiceConfiguration } from './configuration';
import { RelativeWorkspacePathResolver } from './relativePathResolver';
import API from './api';


Expand Down Expand Up @@ -91,6 +92,8 @@ export class TypeScriptVersion {


export class TypeScriptVersionProvider {
private readonly relativePathResolver: RelativeWorkspacePathResolver = new RelativeWorkspacePathResolver();

public constructor(
private configuration: TypeScriptServiceConfiguration
) { }
Expand Down Expand Up @@ -165,14 +168,9 @@ export class TypeScriptVersionProvider {
return [new TypeScriptVersion(tsdkPathSetting)];
}

for (const root of workspace.workspaceFolders || []) {
const rootPrefixes = [`./${root.name}/`, `${root.name}/`, `.\\${root.name}\\`, `${root.name}\\`];
for (const rootPrefix of rootPrefixes) {
if (tsdkPathSetting.startsWith(rootPrefix)) {
const workspacePath = path.join(root.uri.fsPath, tsdkPathSetting.replace(rootPrefix, ''));
return [new TypeScriptVersion(workspacePath, tsdkPathSetting)];
}
}
const workspacePath = this.relativePathResolver.asAbsoluteWorkspacePath(tsdkPathSetting);
if (workspacePath !== undefined) {
return [new TypeScriptVersion(workspacePath, tsdkPathSetting)];
}

return this.loadTypeScriptVersionsFromPath(tsdkPathSetting);
Expand Down