Reduce direct dependencies on ts in web server (#198809)

Reduce direct dependencies on ts in web server

This reduces the number of direct imports of `ts` in `webServer.ts`. This sets us up so that we can eventually swap out the TS versions at runtime instead of being limited to the TS version webServer is bundled against
This commit is contained in:
Matt Bierner
2023-11-21 15:31:10 -08:00
committed by GitHub
parent 7c44528063
commit fbbdb7912e
6 changed files with 134 additions and 113 deletions

View File

@@ -3,11 +3,20 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as ts from 'typescript/lib/tsserverlibrary';
import type * as ts from 'typescript/lib/tsserverlibrary';
import { URI } from 'vscode-uri';
import { Logger } from './logging';
import { PathMapper, fromResource, looksLikeLibDtsPath, looksLikeNodeModules, mapUri } from './pathMapper';
/**
* Copied from `ts.FileWatcherEventKind` to avoid direct dependency.
*/
enum FileWatcherEventKind {
Created = 0,
Changed = 1,
Deleted = 2,
}
export class FileWatcherManager {
private static readonly noopWatcher: ts.FileWatcher = { close() { } };
@@ -107,11 +116,11 @@ export class FileWatcherManager {
private toTsWatcherKind(event: 'create' | 'change' | 'delete') {
if (event === 'create') {
return ts.FileWatcherEventKind.Created;
return FileWatcherEventKind.Created;
} else if (event === 'change') {
return ts.FileWatcherEventKind.Changed;
return FileWatcherEventKind.Changed;
} else if (event === 'delete') {
return ts.FileWatcherEventKind.Deleted;
return FileWatcherEventKind.Deleted;
}
throw new Error(`Unknown event: ${event}`);
}

View File

@@ -6,32 +6,16 @@
import { ApiClient, FileStat, FileType, Requests } from '@vscode/sync-api-client';
import { ClientConnection } from '@vscode/sync-api-common/browser';
import { basename } from 'path';
import * as ts from 'typescript/lib/tsserverlibrary';
import type * as ts from 'typescript/lib/tsserverlibrary';
import { FileWatcherManager } from './fileWatcherManager';
import { Logger } from './logging';
import { PathMapper, looksLikeNodeModules, mapUri } from './pathMapper';
import { findArgument, hasArgument } from './util/args';
// BEGIN misc internals
const combinePaths: (path: string, ...paths: (string | undefined)[]) => string = (ts as any).combinePaths;
const byteOrderMarkIndicator = '\uFEFF';
const matchFiles: (
path: string,
extensions: readonly string[] | undefined,
excludes: readonly string[] | undefined,
includes: readonly string[] | undefined,
useCaseSensitiveFileNames: boolean,
currentDirectory: string,
depth: number | undefined,
getFileSystemEntries: (path: string) => { files: readonly string[]; directories: readonly string[] },
realpath: (path: string) => string
) => string[] = (ts as any).matchFiles;
const generateDjb2Hash = (ts as any).generateDjb2Hash;
// End misc internals
type ServerHostWithImport = ts.server.ServerHost & { importPlugin(root: string, moduleName: string): Promise<ts.server.ModuleImportResult> };
function createServerHost(
ts: typeof import('typescript/lib/tsserverlibrary'),
logger: Logger,
apiClient: ApiClient | undefined,
args: readonly string[],
@@ -43,6 +27,22 @@ function createServerHost(
const currentDirectory = '/';
const fs = apiClient?.vscode.workspace.fileSystem;
// Internals
const combinePaths: (path: string, ...paths: (string | undefined)[]) => string = (ts as any).combinePaths;
const byteOrderMarkIndicator = '\uFEFF';
const matchFiles: (
path: string,
extensions: readonly string[] | undefined,
excludes: readonly string[] | undefined,
includes: readonly string[] | undefined,
useCaseSensitiveFileNames: boolean,
currentDirectory: string,
depth: number | undefined,
getFileSystemEntries: (path: string) => { files: readonly string[]; directories: readonly string[] },
realpath: (path: string) => string
) => string[] = (ts as any).matchFiles;
const generateDjb2Hash = (ts as any).generateDjb2Hash;
// Legacy web
const memoize: <T>(callback: () => T) => () => T = (ts as any).memoize;
const ensureTrailingDirectorySeparator: (path: string) => string = (ts as any).ensureTrailingDirectorySeparator;
@@ -404,6 +404,7 @@ function createServerHost(
}
export async function createSys(
ts: typeof import('typescript/lib/tsserverlibrary'),
args: readonly string[],
fsPort: MessagePort,
logger: Logger,
@@ -418,10 +419,10 @@ export async function createSys(
const apiClient = new ApiClient(connection);
const fs = apiClient.vscode.workspace.fileSystem;
const sys = createServerHost(logger, apiClient, args, watchManager, pathMapper, enabledExperimentalTypeAcquisition, onExit);
const sys = createServerHost(ts, logger, apiClient, args, watchManager, pathMapper, enabledExperimentalTypeAcquisition, onExit);
return { sys, fs };
} else {
return { sys: createServerHost(logger, undefined, args, watchManager, pathMapper, false, onExit) };
return { sys: createServerHost(ts, logger, undefined, args, watchManager, pathMapper, false, onExit) };
}
}

View File

@@ -34,7 +34,7 @@ type InstallerResponse = ts.server.PackageInstalledResponse | ts.server.SetTypin
* The "server" part of the "server/client" model. This is the part that
* actually gets instantiated and passed to tsserver.
*/
export default class WebTypingsInstallerClient implements ts.server.ITypingsInstaller {
export class WebTypingsInstallerClient implements ts.server.ITypingsInstaller {
private projectService: ts.server.ProjectService | undefined;

View File

@@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as ts from 'typescript/lib/tsserverlibrary';
import type * as ts from 'typescript/lib/tsserverlibrary';
export function hasArgument(args: readonly string[], name: string): boolean {
return args.indexOf(name) >= 0;
@@ -20,14 +20,23 @@ export function findArgumentStringArray(args: readonly string[], name: string):
return arg === undefined ? [] : arg.split(',').filter(name => name !== '');
}
/**
* Copied from `ts.LanguageServiceMode` to avoid direct dependency.
*/
export enum LanguageServiceMode {
Semantic = 0,
PartialSemantic = 1,
Syntactic = 2,
}
export function parseServerMode(args: readonly string[]): ts.LanguageServiceMode | string | undefined {
const mode = findArgument(args, '--serverMode');
if (!mode) { return undefined; }
switch (mode.toLowerCase()) {
case 'semantic': return ts.LanguageServiceMode.Semantic;
case 'partialsemantic': return ts.LanguageServiceMode.PartialSemantic;
case 'syntactic': return ts.LanguageServiceMode.Syntactic;
case 'semantic': return LanguageServiceMode.Semantic;
case 'partialsemantic': return LanguageServiceMode.PartialSemantic;
case 'syntactic': return LanguageServiceMode.Syntactic;
default: return mode;
}
}

View File

@@ -12,7 +12,7 @@ import { Logger, parseLogLevel } from './logging';
import { PathMapper } from './pathMapper';
import { createSys } from './serverHost';
import { findArgument, findArgumentStringArray, hasArgument, parseServerMode } from './util/args';
import { StartSessionOptions, WorkerSession } from './workerSession';
import { StartSessionOptions, createWorkerSession } from './workerSession';
const setSys: (s: ts.System) => void = (ts as any).setSys;
@@ -42,11 +42,11 @@ async function initializeSession(
const pathMapper = new PathMapper(extensionUri);
const watchManager = new FileWatcherManager(ports.watcher, extensionUri, enabledExperimentalTypeAcquisition, pathMapper, logger);
const { sys, fs } = await createSys(args, ports.sync, logger, watchManager, pathMapper, () => {
const { sys, fs } = await createSys(ts, args, ports.sync, logger, watchManager, pathMapper, () => {
removeEventListener('message', listener);
});
setSys(sys);
session = new WorkerSession(sys, fs, sessionOptions, ports.tsserver, pathMapper, logger);
session = createWorkerSession(ts, sys, fs, sessionOptions, ports.tsserver, pathMapper, logger);
session.listen();
}

View File

@@ -3,16 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { FileSystem } from '@vscode/sync-api-client';
import * as ts from 'typescript/lib/tsserverlibrary';
import type * as ts from 'typescript/lib/tsserverlibrary';
import { Logger } from './logging';
import WebTypingsInstaller from './typingsInstaller/typingsInstaller';
import { WebTypingsInstallerClient } from './typingsInstaller/typingsInstaller';
import { hrtime } from './util/hrtime';
import { WasmCancellationToken } from './wasmCancellationToken';
import { PathMapper } from './pathMapper';
const indent: (str: string) => string = (ts as any).server.indent;
export interface StartSessionOptions {
readonly globalPlugins: ts.server.SessionOptions['globalPlugins'];
readonly pluginProbeLocations: ts.server.SessionOptions['pluginProbeLocations'];
@@ -25,98 +22,103 @@ export interface StartSessionOptions {
readonly disableAutomaticTypingAcquisition: boolean;
}
export class WorkerSession extends ts.server.Session<{}> {
export function createWorkerSession(
ts: typeof import('typescript/lib/tsserverlibrary'),
host: ts.server.ServerHost,
fs: FileSystem | undefined,
options: StartSessionOptions,
port: MessagePort,
pathMapper: PathMapper,
logger: Logger,
) {
const indent: (str: string) => string = (ts as any).server.indent;
readonly wasmCancellationToken: WasmCancellationToken;
readonly listener: (message: any) => void;
return new class WorkerSession extends ts.server.Session<{}> {
constructor(
host: ts.server.ServerHost,
fs: FileSystem | undefined,
options: StartSessionOptions,
private readonly port: MessagePort,
pathMapper: PathMapper,
logger: Logger
) {
const cancellationToken = new WasmCancellationToken();
const typingsInstaller = options.disableAutomaticTypingAcquisition || !fs ? ts.server.nullTypingsInstaller : new WebTypingsInstaller(host, '/vscode-global-typings/ts-nul-authority/projects');
private readonly wasmCancellationToken: WasmCancellationToken;
private readonly listener: (message: any) => void;
super({
host,
cancellationToken,
...options,
typingsInstaller,
byteLength: () => { throw new Error('Not implemented'); }, // Formats the message text in send of Session which is overridden in this class so not needed
hrtime,
logger: logger.tsLogger,
canUseEvents: true,
});
this.wasmCancellationToken = cancellationToken;
constructor() {
const cancellationToken = new WasmCancellationToken();
const typingsInstaller = options.disableAutomaticTypingAcquisition || !fs ? ts.server.nullTypingsInstaller : new WebTypingsInstallerClient(host, '/vscode-global-typings/ts-nul-authority/projects');
this.listener = (message: any) => {
// TEMP fix since Cancellation.retrieveCheck is not correct
function retrieveCheck2(data: any) {
if (!globalThis.crossOriginIsolated || !(data.$cancellationData instanceof SharedArrayBuffer)) {
return () => false;
super({
host,
cancellationToken,
...options,
typingsInstaller,
byteLength: () => { throw new Error('Not implemented'); }, // Formats the message text in send of Session which is overridden in this class so not needed
hrtime,
logger: logger.tsLogger,
canUseEvents: true,
});
this.wasmCancellationToken = cancellationToken;
this.listener = (message: any) => {
// TEMP fix since Cancellation.retrieveCheck is not correct
function retrieveCheck2(data: any) {
if (!globalThis.crossOriginIsolated || !(data.$cancellationData instanceof SharedArrayBuffer)) {
return () => false;
}
const typedArray = new Int32Array(data.$cancellationData, 0, 1);
return () => {
return Atomics.load(typedArray, 0) === 1;
};
}
const typedArray = new Int32Array(data.$cancellationData, 0, 1);
return () => {
return Atomics.load(typedArray, 0) === 1;
};
}
const shouldCancel = retrieveCheck2(message.data);
if (shouldCancel) {
this.wasmCancellationToken.shouldCancel = shouldCancel;
}
const shouldCancel = retrieveCheck2(message.data);
if (shouldCancel) {
this.wasmCancellationToken.shouldCancel = shouldCancel;
}
try {
if (message.data.command === 'updateOpen') {
const args = message.data.arguments as ts.server.protocol.UpdateOpenRequestArgs;
for (const open of args.openFiles ?? []) {
if (open.projectRootPath) {
pathMapper.addProjectRoot(open.projectRootPath);
try {
if (message.data.command === 'updateOpen') {
const args = message.data.arguments as ts.server.protocol.UpdateOpenRequestArgs;
for (const open of args.openFiles ?? []) {
if (open.projectRootPath) {
pathMapper.addProjectRoot(open.projectRootPath);
}
}
}
} catch {
// Noop
}
} catch {
// Noop
this.onMessage(message.data);
};
}
public override send(msg: ts.server.protocol.Message) {
if (msg.type === 'event' && !this.canUseEvents) {
if (this.logger.hasLevel(ts.server.LogLevel.verbose)) {
this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`);
}
return;
}
this.onMessage(message.data);
};
}
public override send(msg: ts.server.protocol.Message) {
if (msg.type === 'event' && !this.canUseEvents) {
if (this.logger.hasLevel(ts.server.LogLevel.verbose)) {
this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`);
this.logger.info(`${msg.type}:${indent(JSON.stringify(msg))}`);
}
return;
port.postMessage(msg);
}
if (this.logger.hasLevel(ts.server.LogLevel.verbose)) {
this.logger.info(`${msg.type}:${indent(JSON.stringify(msg))}`);
protected override parseMessage(message: {}): ts.server.protocol.Request {
return message as ts.server.protocol.Request;
}
this.port.postMessage(msg);
}
protected override parseMessage(message: {}): ts.server.protocol.Request {
return message as ts.server.protocol.Request;
}
protected override toStringMessage(message: {}) {
return JSON.stringify(message, undefined, 2);
}
protected override toStringMessage(message: {}) {
return JSON.stringify(message, undefined, 2);
}
override exit() {
this.logger.info('Exiting...');
port.removeEventListener('message', this.listener);
this.projectService.closeLog();
close();
}
override exit() {
this.logger.info('Exiting...');
this.port.removeEventListener('message', this.listener);
this.projectService.closeLog();
close();
}
listen() {
this.logger.info(`webServer.ts: tsserver starting to listen for messages on 'message'...`);
this.port.onmessage = this.listener;
}
listen() {
this.logger.info(`webServer.ts: tsserver starting to listen for messages on 'message'...`);
port.onmessage = this.listener;
}
}();
}