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
10 changes: 8 additions & 2 deletions 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 @@ -1132,7 +1133,7 @@
"typescript.tsserver.maxTsServerMemory": {
"type": "number",
"default": 3072,
"description": "%configuration.tsserver.maxTsServerMemory%",
"markdownDescription": "%configuration.tsserver.maxTsServerMemory%",
"scope": "window"
},
"typescript.tsserver.experimental.enableProjectDiagnostics": {
Expand Down Expand Up @@ -1251,6 +1252,11 @@
"description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%",
"scope": "window"
},
"typescript.tsserver.nodePath": {
"type": "string",
"description": "%configuration.tsserver.nodePath%",
"scope": "window"
},
"typescript.experimental.tsserver.web.typeAcquisition.enabled": {
"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 @@ -70,7 +70,7 @@
"configuration.tsserver.useSyntaxServer.always": "Use a lighter weight syntax server to handle all IntelliSense operations. This syntax server can only provide IntelliSense for opened files.",
"configuration.tsserver.useSyntaxServer.never": "Don't use a dedicated syntax server. Use a single server to handle all IntelliSense operations.",
"configuration.tsserver.useSyntaxServer.auto": "Spawn both a full server and a lighter weight server dedicated to syntax operations. The syntax server is used to speed up syntax operations and provide IntelliSense while projects are loading.",
"configuration.tsserver.maxTsServerMemory": "The maximum amount of memory (in MB) to allocate to the TypeScript server process.",
"configuration.tsserver.maxTsServerMemory": "The maximum amount of memory (in MB) to allocate to the TypeScript server process. To use a memory limit greater than 4 GB, use `#typescript.tsserver.nodePath#` to run TS Server with a custom Node installation.",
"configuration.tsserver.experimental.enableProjectDiagnostics": "(Experimental) Enables project wide error reporting.",
"typescript.locale": "Sets the locale used to report JavaScript and TypeScript errors. Defaults to use VS Code's locale.",
"configuration.implicitProjectConfig.module": "Sets the module system for the program. See more: https://www.typescriptlang.org/tsconfig#module.",
Expand Down Expand Up @@ -213,6 +213,7 @@
"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.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.",
"configuration.experimental.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web.",
"walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js",
"walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.",
Expand Down
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 @@ -6,7 +6,10 @@
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import * as child_process from 'child_process';
import * as fs from 'fs';
import { BaseServiceConfigurationProvider } from './configuration';
import { RelativeWorkspacePathResolver } from '../utils/relativePathResolver';

export class ElectronServiceConfigurationProvider extends BaseServiceConfigurationProvider {

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

protected readLocalNodePath(configuration: vscode.WorkspaceConfiguration): string | null {
return this.validatePath(this.readLocalNodePathWorker(configuration));
}

private readLocalNodePathWorker(configuration: vscode.WorkspaceConfiguration): string | null {
const inspect = configuration.inspect('typescript.tsserver.nodePath');
if (inspect?.workspaceValue && typeof inspect.workspaceValue === 'string') {
if (inspect.workspaceValue === 'node') {
return this.findNodePath();
}
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 {
return this.validatePath(this.readGlobalNodePathWorker(configuration));
}

private readGlobalNodePathWorker(configuration: vscode.WorkspaceConfiguration): string | null {
const inspect = configuration.inspect('typescript.tsserver.nodePath');
if (inspect?.globalValue && typeof inspect.globalValue === 'string') {
if (inspect.globalValue === 'node') {
return this.findNodePath();
}
const fixedPath = this.fixPathPrefixes(inspect.globalValue);
if (path.isAbsolute(fixedPath)) {
return fixedPath;
}
}
return null;
}

private findNodePath(): string | null {
try {
const out = child_process.execFileSync('node', ['-e', 'console.log(process.execPath)'], {
Copy link
Member Author

Choose a reason for hiding this comment

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

Context: I tried doing the node detection using which, like other language extensions do, but it does not work if you have a node version manager like volta, because if you do, your path points to volta's node.exe wrapper, and if you use that to run TS Server, it crashes.
This alternative approach was suggested by @jakebailey.

Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason to not do this for all paths?

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you mean for all paths?

windowsHide: true,
timeout: 2000,
cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath,
encoding: 'utf-8',
});
return out.trim();
} catch (error) {
vscode.window.showWarningMessage(vscode.l10n.t("Could not detect a Node installation to run TS Server."));
return null;
}
}

private validatePath(nodePath: string | null): string | null {
if (nodePath && (!fs.existsSync(nodePath) || fs.lstatSync(nodePath).isDirectory())) {
vscode.window.showWarningMessage(vscode.l10n.t("The path {0} doesn\'t point to a valid Node installation to run TS Server. Falling back to bundled Node.", nodePath));
return null;
}
return nodePath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,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 @@ -154,11 +156,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
149 changes: 149 additions & 0 deletions extensions/typescript-language-features/src/tsServer/nodeManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* 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';
const lastKnownWorkspaceNodeStorageKey = 'typescript.lastKnownWorkspaceNode';
type UseWorkspaceNodeState = undefined | boolean;
type LastKnownWorkspaceNodeState = undefined | string;

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) {
const workspaceVersion = this.configuration.localNodePath;
if (workspaceVersion) {
const useWorkspaceNode = this.canUseWorkspaceNode(workspaceVersion);
if (useWorkspaceNode === undefined) {
setImmediate(() => {
this.promptAndSetWorkspaceNode();
});
}
else if (useWorkspaceNode) {
this._currentVersion = workspaceVersion;
}
}
}
else {
this._disposables.push(vscode.workspace.onDidGrantWorkspaceTrust(() => {
const workspaceVersion = this.configuration.localNodePath;
if (workspaceVersion) {
const useWorkspaceNode = this.canUseWorkspaceNode(workspaceVersion);
if (useWorkspaceNode === undefined) {
setImmediate(() => {
this.promptAndSetWorkspaceNode();
});
}
else if (useWorkspaceNode) {
this.updateActiveVersion(workspaceVersion);
}
}
}));
}
}

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

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

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;
const workspaceVersion = this.configuration.localNodePath;
if (vscode.workspace.isTrusted && workspaceVersion) {
const useWorkspaceNode = this.canUseWorkspaceNode(workspaceVersion);
if (useWorkspaceNode === undefined) {
version = await this.promptUseWorkspaceNode() || version;
}
else if (useWorkspaceNode) {
version = workspaceVersion;
}
}
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("Yes");
const disallow = vscode.l10n.t("No");
const dismiss = vscode.l10n.t("Not now");

const result = await vscode.window.showInformationMessage(vscode.l10n.t("This workspace wants to use the Node installation at '{0}' to run TS Server. Would you like to use it?", workspaceVersion),
allow,
disallow,
dismiss,
);

let version = undefined;
switch (result) {
case allow:
await this.setUseWorkspaceNodeState(true, workspaceVersion);
version = workspaceVersion;
break;
case disallow:
await this.setUseWorkspaceNodeState(false, workspaceVersion);
break;
case dismiss:
await this.setUseWorkspaceNodeState(undefined, workspaceVersion);
break;
}
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 canUseWorkspaceNode(nodeVersion: string): boolean | undefined {
const lastKnownWorkspaceNode = this.workspaceState.get<LastKnownWorkspaceNodeState>(lastKnownWorkspaceNodeStorageKey);
if (lastKnownWorkspaceNode === nodeVersion) {
return this.workspaceState.get<UseWorkspaceNodeState>(useWorkspaceNodeStorageKey);
}
return undefined;
}

private async setUseWorkspaceNodeState(allow: boolean | undefined, nodeVersion: string) {
await this.workspaceState.update(lastKnownWorkspaceNodeStorageKey, nodeVersion);
await this.workspaceState.update(useWorkspaceNodeStorageKey, allow);
}
}
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
Loading
Loading