diff --git a/extensions/typescript-language-features/web/src/fileWatcherManager.ts b/extensions/typescript-language-features/web/src/fileWatcherManager.ts index f1b6f90f292b7..8c8d7403740ec 100644 --- a/extensions/typescript-language-features/web/src/fileWatcherManager.ts +++ b/extensions/typescript-language-features/web/src/fileWatcherManager.ts @@ -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}`); } diff --git a/extensions/typescript-language-features/web/src/serverHost.ts b/extensions/typescript-language-features/web/src/serverHost.ts index ef03ace9e59f2..f2f9ca95996b2 100644 --- a/extensions/typescript-language-features/web/src/serverHost.ts +++ b/extensions/typescript-language-features/web/src/serverHost.ts @@ -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 }; 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: (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) }; } } diff --git a/extensions/typescript-language-features/web/src/typingsInstaller/typingsInstaller.ts b/extensions/typescript-language-features/web/src/typingsInstaller/typingsInstaller.ts index 7b9b164c40c52..7c40993d6df29 100644 --- a/extensions/typescript-language-features/web/src/typingsInstaller/typingsInstaller.ts +++ b/extensions/typescript-language-features/web/src/typingsInstaller/typingsInstaller.ts @@ -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; diff --git a/extensions/typescript-language-features/web/src/util/args.ts b/extensions/typescript-language-features/web/src/util/args.ts index a0f1cf10179e6..8a9224ddf6b8f 100644 --- a/extensions/typescript-language-features/web/src/util/args.ts +++ b/extensions/typescript-language-features/web/src/util/args.ts @@ -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; } } diff --git a/extensions/typescript-language-features/web/src/webServer.ts b/extensions/typescript-language-features/web/src/webServer.ts index 3747bf1cfa7e5..0ea25aee76525 100644 --- a/extensions/typescript-language-features/web/src/webServer.ts +++ b/extensions/typescript-language-features/web/src/webServer.ts @@ -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(); } diff --git a/extensions/typescript-language-features/web/src/workerSession.ts b/extensions/typescript-language-features/web/src/workerSession.ts index ec1007570337b..c752654b7d601 100644 --- a/extensions/typescript-language-features/web/src/workerSession.ts +++ b/extensions/typescript-language-features/web/src/workerSession.ts @@ -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<{}> { - - 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; + } + }(); }