mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-21 00:59:03 +01:00
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:
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user