Skip to content

Commit

Permalink
Reduce direct dependencies on ts in web server (#198809)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mjbvz authored Nov 21, 2023
1 parent 7c44528 commit fbbdb79
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() { } };

Expand Down Expand Up @@ -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}`);
}
Expand Down
41 changes: 21 additions & 20 deletions extensions/typescript-language-features/web/src/serverHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -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;
Expand Down Expand Up @@ -404,6 +404,7 @@ function createServerHost(
}

export async function createSys(
ts: typeof import('typescript/lib/tsserverlibrary'),
args: readonly string[],
fsPort: MessagePort,
logger: Logger,
Expand All @@ -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) };
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
17 changes: 13 additions & 4 deletions extensions/typescript-language-features/web/src/util/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
6 changes: 3 additions & 3 deletions extensions/typescript-language-features/web/src/webServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
}

Expand Down
170 changes: 86 additions & 84 deletions extensions/typescript-language-features/web/src/workerSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -25,98 +22,103 @@ export interface StartSessionOptions {
readonly disableAutomaticTypingAcquisition: boolean;
}

export class WorkerSession extends ts.server.Session<{}> {

readonly wasmCancellationToken: WasmCancellationToken;
readonly listener: (message: any) => void;

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');

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;
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;

return new class WorkerSession extends ts.server.Session<{}> {

private readonly wasmCancellationToken: WasmCancellationToken;
private readonly listener: (message: any) => void;

constructor() {
const cancellationToken = new WasmCancellationToken();
const typingsInstaller = options.disableAutomaticTypingAcquisition || !fs ? ts.server.nullTypingsInstaller : new WebTypingsInstallerClient(host, '/vscode-global-typings/ts-nul-authority/projects');

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);
};
}
this.onMessage(message.data);
};
}

public override send(msg: ts.server.protocol.Message) {
if (msg.type === 'event' && !this.canUseEvents) {
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;
}
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;
}
if (this.logger.hasLevel(ts.server.LogLevel.verbose)) {
this.logger.info(`${msg.type}:${indent(JSON.stringify(msg))}`);
port.postMessage(msg);
}
this.port.postMessage(msg);
}

protected override parseMessage(message: {}): ts.server.protocol.Request {
return message as ts.server.protocol.Request;
}
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...');
this.port.removeEventListener('message', this.listener);
this.projectService.closeLog();
close();
}
override exit() {
this.logger.info('Exiting...');
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;
}
}();
}

0 comments on commit fbbdb79

Please sign in to comment.