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

Reduce direct dependencies on ts in web server #198809

Merged
merged 1 commit into from
Nov 21, 2023
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
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;
}
}();
}