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

Add custom Node option to run TS Server #191019

Merged
merged 12 commits into from
Sep 6, 2023
8 changes: 7 additions & 1 deletion extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"restrictedConfigurations": [
"typescript.tsdk",
"typescript.tsserver.pluginPaths",
"typescript.npm"
"typescript.npm",
"typescript.tsserver.nodePath"
]
}
},
Expand Down Expand Up @@ -1250,6 +1251,11 @@
"description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%",
"scope": "window"
},
"typescript.tsserver.nodePath": {
"type": "string",
"description": "%configuration.tsserver.nodePath%",
"scope": "window"
},
"typescript.preferGoToSourceDefinition": {
"type": "boolean",
"default": false,
Expand Down
3 changes: 2 additions & 1 deletion extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,6 @@
"configuration.suggest.classMemberSnippets.enabled": "Enable/disable snippet completions for class members.",
"configuration.suggest.objectLiteralMethodSnippets.enabled": "Enable/disable snippet completions for methods in object literals. Requires using TypeScript 4.7+ in the workspace.",
"configuration.tsserver.web.projectWideIntellisense.enabled": "Enable/disable project-wide IntelliSense on web. Requires that VS Code is running in a trusted context.",
"configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors. This is needed when using external packages as these can't be included analyzed on web."
"configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors. This is needed when using external packages as these can't be included analyzed on web.",
"configuration.tsserver.nodePath": "Run TS Server on a custom Node installation."
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,13 @@ export class BrowserServiceConfigurationProvider extends BaseServiceConfiguratio
protected readLocalTsdk(_configuration: vscode.WorkspaceConfiguration): string | null {
return null;
}

// On browsers, we don't run TSServer on Node
protected readLocalNodePath(_configuration: vscode.WorkspaceConfiguration): string | null {
return null;
}

protected override readGlobalNodePath(_configuration: vscode.WorkspaceConfiguration): string | null {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import { BaseServiceConfigurationProvider } from './configuration';
import { RelativeWorkspacePathResolver } from '../utils/relativePathResolver';

export class ElectronServiceConfigurationProvider extends BaseServiceConfigurationProvider {

Expand Down Expand Up @@ -35,4 +36,28 @@ export class ElectronServiceConfigurationProvider extends BaseServiceConfigurati
}
return null;
}

protected readLocalNodePath(configuration: vscode.WorkspaceConfiguration): string | null {
const inspect = configuration.inspect('typescript.tsserver.nodePath');
if (inspect && typeof inspect.workspaceValue === 'string') {
const fixedPath = this.fixPathPrefixes(inspect.workspaceValue);
if (!path.isAbsolute(fixedPath)) {
const workspacePath = RelativeWorkspacePathResolver.asAbsoluteWorkspacePath(fixedPath);
return workspacePath || null;
}
return fixedPath;
}
return null;
}

protected readGlobalNodePath(configuration: vscode.WorkspaceConfiguration): string | null {
const inspect = configuration.inspect('typescript.tsserver.nodePath');
if (inspect && typeof inspect.globalValue === 'string') {
const fixedPath = this.fixPathPrefixes(inspect.globalValue);
if (path.isAbsolute(fixedPath)) {
return fixedPath;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export interface TypeScriptServiceConfiguration {
readonly watchOptions: Proto.WatchOptions | undefined;
readonly includePackageJsonAutoImports: 'auto' | 'on' | 'off' | undefined;
readonly enableTsServerTracing: boolean;
readonly localNodePath: string | null;
readonly globalNodePath: string | null;
}

export function areServiceConfigurationsEqual(a: TypeScriptServiceConfiguration, b: TypeScriptServiceConfiguration): boolean {
Expand Down Expand Up @@ -150,11 +152,15 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
watchOptions: this.readWatchOptions(configuration),
includePackageJsonAutoImports: this.readIncludePackageJsonAutoImports(configuration),
enableTsServerTracing: this.readEnableTsServerTracing(configuration),
localNodePath: this.readLocalNodePath(configuration),
globalNodePath: this.readGlobalNodePath(configuration),
};
}

protected abstract readGlobalTsdk(configuration: vscode.WorkspaceConfiguration): string | null;
protected abstract readLocalTsdk(configuration: vscode.WorkspaceConfiguration): string | null;
protected abstract readLocalNodePath(configuration: vscode.WorkspaceConfiguration): string | null;
protected abstract readGlobalNodePath(configuration: vscode.WorkspaceConfiguration): string | null;

protected readTsServerLogLevel(configuration: vscode.WorkspaceConfiguration): TsServerLogLevel {
const setting = configuration.get<string>('typescript.tsserver.log', 'off');
Expand Down
130 changes: 130 additions & 0 deletions extensions/typescript-language-features/src/tsServer/nodeManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { TypeScriptServiceConfiguration } from '../configuration/configuration';
import { setImmediate } from '../utils/async';
import { Disposable } from '../utils/dispose';


const useWorkspaceNodeStorageKey = 'typescript.useWorkspaceNode';

export class NodeVersionManager extends Disposable {
private _currentVersion: string | undefined;
Copy link
Member

Choose a reason for hiding this comment

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

I assume the "version" moniker is left over from copying the tsdk code? Was confused as to whether or not this was checking versions or something until I realized that this was acutally managing the node path.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, do you think simply NodeManager would be better?

Copy link
Member

Choose a reason for hiding this comment

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

I was imagining NodePathManager and _currentPath, but I also haven't gone to look for examples that would indicate what's canonical to this repo's style.


public constructor(
private configuration: TypeScriptServiceConfiguration,
private readonly workspaceState: vscode.Memento
) {
super();

this._currentVersion = this.configuration.globalNodePath || undefined;
if (vscode.workspace.isTrusted) {
if (this.configuration.localNodePath) {
if (this.useWorkspaceNodeSetting === undefined) {
setImmediate(() => {
this.promptAndSetWorkspaceNode();
});
}
else if (this.useWorkspaceNodeSetting) {
this._currentVersion = this.configuration.localNodePath;
}
Copy link
Member

Choose a reason for hiding this comment

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

This code looks the same as the code in the block below; probably could be pulled out into a method.

Copy link
Member Author

Choose a reason for hiding this comment

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

They're the same except the version below uses this.updateActiveVersion instead of assigning directly as in this._currentVersion = this.configuration.localNodePath (which is kinda special because we're initializing the class at that point), so didn't seem worth it to extract into a method with an option

}
}
else {
this._disposables.push(vscode.workspace.onDidGrantWorkspaceTrust(() => {
if (this.configuration.localNodePath) {
if (this.useWorkspaceNodeSetting === undefined) {
setImmediate(() => {
this.promptAndSetWorkspaceNode();
});
}
else if (this.useWorkspaceNodeSetting) {
this.updateActiveVersion(this.configuration.localNodePath);
}
}
}));
}
}

private readonly _onDidPickNewVersion = this._register(new vscode.EventEmitter<void>());
public readonly onDidPickNewVersion = this._onDidPickNewVersion.event;

public get currentVersion(): string | undefined {
return this._currentVersion;
}

public reset(): void {
this._currentVersion = undefined;
}

public async updateConfiguration(nextConfiguration: TypeScriptServiceConfiguration) {
const oldConfiguration = this.configuration;
this.configuration = nextConfiguration;
if (oldConfiguration.globalNodePath !== nextConfiguration.globalNodePath
|| oldConfiguration.localNodePath !== nextConfiguration.localNodePath) {
await this.computeNewVersion();
}
}

private async computeNewVersion() {
let version = this.configuration.globalNodePath || undefined;
if (vscode.workspace.isTrusted && this.configuration.localNodePath) {
if (this.useWorkspaceNodeSetting === undefined) {
version = await this.promptUseWorkspaceNode();
}
else if (this.useWorkspaceNodeSetting) {
version = this.configuration.localNodePath;
}
}
this.updateActiveVersion(version);
}

private async promptUseWorkspaceNode(): Promise<string | undefined> {
const workspaceVersion = this.configuration.localNodePath;

if (workspaceVersion === null) {
throw new Error('Could not prompt to use workspace Node installation because no workspace Node installation is specified');
}

const allow = vscode.l10n.t("Allow");
const dismiss = vscode.l10n.t("Dismiss");
const neverAllow = vscode.l10n.t("Never in this workspace");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Once the user selects never, is there a way for them still opt in later on?

Copy link
Member Author

Choose a reason for hiding this comment

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

I updated the code to remember the prompt answer for a specific node path. So if you change the workspace node path to point to a different path, the prompt will show up again. However, it's a bit contrived to do that if you select "never" by accident. Should I add a command to use the workspace node path?


const result = await vscode.window.showInformationMessage(vscode.l10n.t("This workspace specifies a custom Node installation to run TS Server. Would you like to use this workspace's custom Node installation to run TS Server?"),
allow,
dismiss,
neverAllow
);

let version = undefined;
if (result === allow) {
await this.workspaceState.update(useWorkspaceNodeStorageKey, true);
version = workspaceVersion;
} else if (result === neverAllow) {
await this.workspaceState.update(useWorkspaceNodeStorageKey, false);
}
return version;
}

private async promptAndSetWorkspaceNode(): Promise<void> {
const version = await this.promptUseWorkspaceNode();
if (version !== undefined) {
this.updateActiveVersion(version);
}
}

private updateActiveVersion(pickedVersion: string | undefined): void {
const oldVersion = this.currentVersion;
this._currentVersion = pickedVersion;
if (oldVersion !== pickedVersion) {
this._onDidPickNewVersion.fire();
}
}

private get useWorkspaceNodeSetting(): boolean | undefined {
return this.workspaceState.get<boolean>(useWorkspaceNodeStorageKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type * as Proto from './protocol/protocol';
import { EventName } from './protocol/protocol.const';
import { TypeScriptVersionManager } from './versionManager';
import { TypeScriptVersion } from './versionProvider';
import { NodeVersionManager } from './nodeManager';

export enum ExecutionTarget {
Semantic,
Expand Down Expand Up @@ -70,6 +71,7 @@ export interface TsServerProcessFactory {
kind: TsServerProcessKind,
configuration: TypeScriptServiceConfiguration,
versionManager: TypeScriptVersionManager,
nodeVersionManager: NodeVersionManager,
tsServerLog: TsServerLog | undefined,
): TsServerProcess;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type * as Proto from './protocol/protocol';
import { TsServerLog, TsServerProcess, TsServerProcessFactory, TsServerProcessKind } from './server';
import { TypeScriptVersionManager } from './versionManager';
import { TypeScriptVersion } from './versionProvider';
import { NodeVersionManager } from './nodeManager';

type BrowserWatchEvent = {
type: 'watchDirectory' | 'watchFile';
Expand Down Expand Up @@ -40,6 +41,7 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory {
kind: TsServerProcessKind,
_configuration: TypeScriptServiceConfiguration,
_versionManager: TypeScriptVersionManager,
_nodeVersionManager: NodeVersionManager,
tsServerLog: TsServerLog | undefined,
) {
const tsServerPath = version.tsServerPath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type * as Proto from './protocol/protocol';
import { TsServerLog, TsServerProcess, TsServerProcessFactory, TsServerProcessKind } from './server';
import { TypeScriptVersionManager } from './versionManager';
import { TypeScriptVersion } from './versionProvider';
import { NodeVersionManager } from './nodeManager';


const defaultSize: number = 8192;
Expand Down Expand Up @@ -134,10 +135,12 @@ class Reader<T> extends Disposable {
}
}

function generatePatchedEnv(env: any, modulePath: string): any {
function generatePatchedEnv(env: any, modulePath: string, hasExecPath: boolean): any {
const newEnv = Object.assign({}, env);

newEnv['ELECTRON_RUN_AS_NODE'] = '1';
if (!hasExecPath) {
newEnv['ELECTRON_RUN_AS_NODE'] = '1';
}
newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..');

// Ensure we always have a PATH set
Expand Down Expand Up @@ -253,6 +256,7 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory {
kind: TsServerProcessKind,
configuration: TypeScriptServiceConfiguration,
versionManager: TypeScriptVersionManager,
nodeVersionManager: NodeVersionManager,
_tsserverLog: TsServerLog | undefined,
): TsServerProcess {
let tsServerPath = version.tsServerPath;
Expand All @@ -263,7 +267,13 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory {
tsServerPath = versionManager.currentVersion.tsServerPath;
}

const useIpc = version.apiVersion?.gte(API.v460);
let execPath = nodeVersionManager.currentVersion;
if (execPath && !fs.existsSync(execPath)) {
vscode.window.showWarningMessage(vscode.l10n.t("The path {0} doesn\'t point to a valid Node installation. Falling back to bundled Node.", execPath!));
nodeVersionManager.reset();
execPath = nodeVersionManager.currentVersion;
}
const useIpc = !execPath && version.apiVersion?.gte(API.v460);

const runtimeArgs = [...args];
if (useIpc) {
Expand All @@ -273,8 +283,9 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory {
const childProcess = child_process.fork(tsServerPath, runtimeArgs, {
silent: true,
cwd: undefined,
env: generatePatchedEnv(process.env, tsServerPath),
env: generatePatchedEnv(process.env, tsServerPath, !!execPath),
execArgv: getExecArgv(kind, configuration),
execPath,
stdio: useIpc ? ['pipe', 'pipe', 'pipe', 'ipc'] : undefined,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { PluginManager } from './plugins';
import { GetErrRoutingTsServer, ITypeScriptServer, SingleTsServer, SyntaxRoutingTsServer, TsServerDelegate, TsServerLog, TsServerProcessFactory, TsServerProcessKind } from './server';
import { TypeScriptVersionManager } from './versionManager';
import { ITypeScriptVersionProvider, TypeScriptVersion } from './versionProvider';
import { NodeVersionManager } from './nodeManager';

const enum CompositeServerType {
/** Run a single server that handles all commands */
Expand All @@ -44,6 +45,7 @@ export class TypeScriptServerSpawner {
public constructor(
private readonly _versionProvider: ITypeScriptVersionProvider,
private readonly _versionManager: TypeScriptVersionManager,
private readonly _nodeVersionManager: NodeVersionManager,
private readonly _logDirectoryProvider: ILogDirectoryProvider,
private readonly _pluginPathsProvider: TypeScriptPluginPathsProvider,
private readonly _logger: Logger,
Expand Down Expand Up @@ -160,7 +162,7 @@ export class TypeScriptServerSpawner {
}

this._logger.info(`<${kind}> Forking...`);
const process = this._factory.fork(version, args, kind, configuration, this._versionManager, tsServerLog);
const process = this._factory.fork(version, args, kind, configuration, this._versionManager, this._nodeVersionManager, tsServerLog);
this._logger.info(`<${kind}> Starting...`);

return new SingleTsServer(
Expand Down
Loading
Loading