Split up webServer.ts (#198802)

Refactors webServer.ts to split it into multiple files and encapsulate some functionality in classes
This commit is contained in:
Matt Bierner
2023-11-21 12:06:47 -08:00
committed by GitHub
parent f54183b7e6
commit fbfabc523d
15 changed files with 1005 additions and 868 deletions

View File

@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* 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 { URI } from 'vscode-uri';
import { Logger } from './logging';
import { PathMapper, fromResource, looksLikeLibDtsPath, looksLikeNodeModules, mapUri } from './pathMapper';
export class FileWatcherManager {
private static readonly noopWatcher: ts.FileWatcher = { close() { } };
private readonly watchFiles = new Map<string, { callback: ts.FileWatcherCallback; pollingInterval?: number; options?: ts.WatchOptions }>();
private readonly watchDirectories = new Map<string, { callback: ts.DirectoryWatcherCallback; recursive?: boolean; options?: ts.WatchOptions }>();
private watchId = 0;
constructor(
private readonly watchPort: MessagePort,
extensionUri: URI,
private readonly enabledExperimentalTypeAcquisition: boolean,
private readonly pathMapper: PathMapper,
private readonly logger: Logger
) {
watchPort.onmessage = (e: any) => this.updateWatch(e.data.event, URI.from(e.data.uri), extensionUri);
}
watchFile(path: string, callback: ts.FileWatcherCallback, pollingInterval?: number, options?: ts.WatchOptions): ts.FileWatcher {
if (looksLikeLibDtsPath(path)) { // We don't support watching lib files on web since they are readonly
return FileWatcherManager.noopWatcher;
}
console.log('watching file:', path);
this.logger.logVerbose('fs.watchFile', { path });
let uri: URI;
try {
uri = this.pathMapper.toResource(path);
} catch (e) {
console.error(e);
return FileWatcherManager.noopWatcher;
}
this.watchFiles.set(path, { callback, pollingInterval, options });
const watchIds = [++this.watchId];
this.watchPort.postMessage({ type: 'watchFile', uri: uri, id: watchIds[0] });
if (this.enabledExperimentalTypeAcquisition && looksLikeNodeModules(path)) {
watchIds.push(++this.watchId);
this.watchPort.postMessage({ type: 'watchFile', uri: mapUri(uri, 'vscode-node-modules'), id: watchIds[1] });
}
return {
close: () => {
this.logger.logVerbose('fs.watchFile.close', { path });
this.watchFiles.delete(path);
for (const id of watchIds) {
this.watchPort.postMessage({ type: 'dispose', id });
}
}
};
}
watchDirectory(path: string, callback: ts.DirectoryWatcherCallback, recursive?: boolean, options?: ts.WatchOptions): ts.FileWatcher {
this.logger.logVerbose('fs.watchDirectory', { path });
let uri: URI;
try {
uri = this.pathMapper.toResource(path);
} catch (e) {
console.error(e);
return FileWatcherManager.noopWatcher;
}
this.watchDirectories.set(path, { callback, recursive, options });
const watchIds = [++this.watchId];
this.watchPort.postMessage({ type: 'watchDirectory', recursive, uri, id: this.watchId });
return {
close: () => {
this.logger.logVerbose('fs.watchDirectory.close', { path });
this.watchDirectories.delete(path);
for (const id of watchIds) {
this.watchPort.postMessage({ type: 'dispose', id });
}
}
};
}
private updateWatch(event: 'create' | 'change' | 'delete', uri: URI, extensionUri: URI) {
const kind = this.toTsWatcherKind(event);
const path = fromResource(extensionUri, uri);
const fileWatcher = this.watchFiles.get(path);
if (fileWatcher) {
fileWatcher.callback(path, kind);
return;
}
for (const watch of Array.from(this.watchDirectories.keys()).filter(dir => path.startsWith(dir))) {
this.watchDirectories.get(watch)!.callback(path);
return;
}
console.error(`no watcher found for ${path}`);
}
private toTsWatcherKind(event: 'create' | 'change' | 'delete') {
if (event === 'create') {
return ts.FileWatcherEventKind.Created;
} else if (event === 'change') {
return ts.FileWatcherEventKind.Changed;
} else if (event === 'delete') {
return ts.FileWatcherEventKind.Deleted;
}
throw new Error(`Unknown event: ${event}`);
}
}

View File

@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as ts from 'typescript/lib/tsserverlibrary';
/**
* Matches the ts.server.LogLevel enum
*/
export enum LogLevel {
terse = 0,
normal = 1,
requestTime = 2,
verbose = 3,
}
export class Logger {
public readonly tsLogger: ts.server.Logger;
constructor(logLevel: LogLevel | undefined) {
const doLog = typeof logLevel === 'undefined'
? (_message: string) => { }
: (message: string) => { postMessage({ type: 'log', body: message }); };
this.tsLogger = {
close: () => { },
hasLevel: level => typeof logLevel === 'undefined' ? false : level <= logLevel,
loggingEnabled: () => true,
perftrc: () => { },
info: doLog,
msg: doLog,
startGroup: () => { },
endGroup: () => { },
getLogFileName: () => undefined
};
}
log(level: LogLevel, message: string, data?: any) {
if (this.tsLogger.hasLevel(level)) {
this.tsLogger.info(message + (data ? ' ' + JSON.stringify(data) : ''));
}
}
logNormal(message: string, data?: any) {
this.log(LogLevel.normal, message, data);
}
logVerbose(message: string, data?: any) {
this.log(LogLevel.verbose, message, data);
}
}
export function parseLogLevel(input: string | undefined): LogLevel | undefined {
switch (input) {
case 'normal': return LogLevel.normal;
case 'terse': return LogLevel.terse;
case 'verbose': return LogLevel.verbose;
default: return undefined;
}
}

View File

@@ -0,0 +1,112 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vscode-uri';
export class PathMapper {
private readonly projectRootPaths = new Map</* original path*/ string, /* parsed URI */ URI>();
constructor(
private readonly extensionUri: URI
) { }
/**
* Copied from toResource in typescriptServiceClient.ts
*/
toResource(filepath: string): URI {
if (looksLikeLibDtsPath(filepath)) {
return URI.from({
scheme: this.extensionUri.scheme,
authority: this.extensionUri.authority,
path: this.extensionUri.path + '/dist/browser/typescript/' + filepath.slice(1)
});
}
const uri = filePathToResourceUri(filepath);
if (!uri) {
throw new Error(`Could not parse path ${filepath}`);
}
// Check if TS is trying to read a file outside of the project root.
// We allow reading files on unknown scheme as these may be loose files opened by the user.
// However we block reading files on schemes that are on a known file system with an unknown root
let allowRead: 'implicit' | 'block' | 'allow' = 'implicit';
for (const projectRoot of this.projectRootPaths.values()) {
if (uri.scheme === projectRoot.scheme) {
if (uri.toString().startsWith(projectRoot.toString())) {
allowRead = 'allow';
break;
}
// Tentatively block the read but a future loop may allow it
allowRead = 'block';
}
}
if (allowRead === 'block') {
throw new AccessOutsideOfRootError(filepath, Array.from(this.projectRootPaths.keys()));
}
return uri;
}
addProjectRoot(projectRootPath: string) {
const uri = filePathToResourceUri(projectRootPath);
if (uri) {
this.projectRootPaths.set(projectRootPath, uri);
}
}
}
class AccessOutsideOfRootError extends Error {
constructor(
public readonly filepath: string,
public readonly projectRootPaths: readonly string[]
) {
super(`Could not read file outside of project root ${filepath}`);
}
}
export function fromResource(extensionUri: URI, uri: URI) {
if (uri.scheme === extensionUri.scheme
&& uri.authority === extensionUri.authority
&& uri.path.startsWith(extensionUri.path + '/dist/browser/typescript/lib.')
&& uri.path.endsWith('.d.ts')) {
return uri.path;
}
return `/${uri.scheme}/${uri.authority}${uri.path}`;
}
export function looksLikeLibDtsPath(filepath: string) {
return filepath.startsWith('/lib.') && filepath.endsWith('.d.ts');
}
export function looksLikeNodeModules(filepath: string) {
return filepath.includes('/node_modules');
}
function filePathToResourceUri(filepath: string): URI | undefined {
const parts = filepath.match(/^\/([^\/]+)\/([^\/]*)(?:\/(.+))?$/);
if (!parts) {
return undefined;
}
const scheme = parts[1];
const authority = parts[2] === 'ts-nul-authority' ? '' : parts[2];
const path = parts[3];
return URI.from({ scheme, authority, path: (path ? '/' + path : path) });
}
export function mapUri(uri: URI, mappedScheme: string): URI {
if (uri.scheme === 'vscode-global-typings') {
throw new Error('can\'t map vscode-global-typings');
}
if (!uri.authority) {
uri = uri.with({ authority: 'ts-nul-authority' });
}
uri = uri.with({ scheme: mappedScheme, path: `/${uri.scheme}/${uri.authority || 'ts-nul-authority'}${uri.path}` });
return uri;
}

View File

@@ -0,0 +1,427 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { 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(
logger: Logger,
apiClient: ApiClient | undefined,
args: readonly string[],
watchManager: FileWatcherManager,
pathMapper: PathMapper,
enabledExperimentalTypeAcquisition: boolean,
exit: () => void,
): ServerHostWithImport {
const currentDirectory = '/';
const fs = apiClient?.vscode.workspace.fileSystem;
// Legacy web
const memoize: <T>(callback: () => T) => () => T = (ts as any).memoize;
const ensureTrailingDirectorySeparator: (path: string) => string = (ts as any).ensureTrailingDirectorySeparator;
const getDirectoryPath: (path: string) => string = (ts as any).getDirectoryPath;
const directorySeparator: string = (ts as any).directorySeparator;
const executingFilePath = findArgument(args, '--executingFilePath') || location + '';
const getExecutingDirectoryPath = memoize(() => memoize(() => ensureTrailingDirectorySeparator(getDirectoryPath(executingFilePath))));
const getWebPath = (path: string) => path.startsWith(directorySeparator) ? path.replace(directorySeparator, getExecutingDirectoryPath()) : undefined;
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
return {
watchFile: watchManager.watchFile.bind(watchManager),
watchDirectory: watchManager.watchDirectory.bind(watchManager),
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any {
return setTimeout(callback, ms, ...args);
},
clearTimeout(timeoutId: any): void {
clearTimeout(timeoutId);
},
setImmediate(callback: (...args: any[]) => void, ...args: any[]): any {
return this.setTimeout(callback, 0, ...args);
},
clearImmediate(timeoutId: any): void {
this.clearTimeout(timeoutId);
},
importPlugin: async (root, moduleName) => {
const packageRoot = combinePaths(root, moduleName);
let packageJson: any | undefined;
try {
const packageJsonResponse = await fetch(combinePaths(packageRoot, 'package.json'));
packageJson = await packageJsonResponse.json();
} catch (e) {
return { module: undefined, error: new Error(`Could not load plugin. Could not load 'package.json'.`) };
}
const browser = packageJson.browser;
if (!browser) {
return { module: undefined, error: new Error(`Could not load plugin. No 'browser' field found in package.json.`) };
}
const scriptPath = combinePaths(packageRoot, browser);
try {
const { default: module } = await import(/* webpackIgnore: true */ scriptPath);
return { module, error: undefined };
} catch (e) {
return { module: undefined, error: e };
}
},
args: Array.from(args),
newLine: '\n',
useCaseSensitiveFileNames: true,
write: s => {
apiClient?.vscode.terminal.write(s);
},
writeOutputIsTTY() {
return true;
},
readFile(path) {
logger.logVerbose('fs.readFile', { path });
if (!fs) {
const webPath = getWebPath(path);
if (webPath) {
const request = new XMLHttpRequest();
request.open('GET', webPath, /* asynchronous */ false);
request.send();
return request.status === 200 ? request.responseText : undefined;
} else {
return undefined;
}
}
let uri;
try {
uri = pathMapper.toResource(path);
} catch (e) {
return undefined;
}
let contents: Uint8Array | undefined;
try {
// We need to slice the bytes since we can't pass a shared array to text decoder
contents = fs.readFile(uri);
} catch (error) {
if (!enabledExperimentalTypeAcquisition) {
return undefined;
}
try {
contents = fs.readFile(mapUri(uri, 'vscode-node-modules'));
} catch (e) {
return undefined;
}
}
return textDecoder.decode(contents.slice());
},
getFileSize(path) {
logger.logVerbose('fs.getFileSize', { path });
if (!fs) {
throw new Error('not supported');
}
const uri = pathMapper.toResource(path);
let ret = 0;
try {
ret = fs.stat(uri).size;
} catch (_error) {
if (enabledExperimentalTypeAcquisition) {
try {
ret = fs.stat(mapUri(uri, 'vscode-node-modules')).size;
} catch (_error) {
}
}
}
return ret;
},
writeFile(path, data, writeByteOrderMark) {
logger.logVerbose('fs.writeFile', { path });
if (!fs) {
throw new Error('not supported');
}
if (writeByteOrderMark) {
data = byteOrderMarkIndicator + data;
}
let uri;
try {
uri = pathMapper.toResource(path);
} catch (e) {
return;
}
const encoded = textEncoder.encode(data);
try {
fs.writeFile(uri, encoded);
const name = basename(uri.path);
if (uri.scheme !== 'vscode-global-typings' && (name === 'package.json' || name === 'package-lock.json' || name === 'package-lock.kdl')) {
fs.writeFile(mapUri(uri, 'vscode-node-modules'), encoded);
}
} catch (error) {
console.error('fs.writeFile', { path, error });
}
},
resolvePath(path: string): string {
return path;
},
fileExists(path: string): boolean {
logger.logVerbose('fs.fileExists', { path });
if (!fs) {
const webPath = getWebPath(path);
if (!webPath) {
return false;
}
const request = new XMLHttpRequest();
request.open('HEAD', webPath, /* asynchronous */ false);
request.send();
return request.status === 200;
}
let uri;
try {
uri = pathMapper.toResource(path);
} catch (e) {
return false;
}
let ret = false;
try {
ret = fs.stat(uri).type === FileType.File;
} catch (_error) {
if (enabledExperimentalTypeAcquisition) {
try {
ret = fs.stat(mapUri(uri, 'vscode-node-modules')).type === FileType.File;
} catch (_error) {
}
}
}
return ret;
},
directoryExists(path: string): boolean {
logger.logVerbose('fs.directoryExists', { path });
if (!fs) {
return false;
}
let uri;
try {
uri = pathMapper.toResource(path);
} catch (_error) {
return false;
}
let stat: FileStat | undefined = undefined;
try {
stat = fs.stat(uri);
} catch (_error) {
if (enabledExperimentalTypeAcquisition) {
try {
stat = fs.stat(mapUri(uri, 'vscode-node-modules'));
} catch (_error) {
}
}
}
if (stat) {
if (path.startsWith('/https') && !path.endsWith('.d.ts')) {
// TODO: Hack, https 'file system' can't actually tell what is a file vs directory
return stat.type === FileType.File || stat.type === FileType.Directory;
}
return stat.type === FileType.Directory;
} else {
return false;
}
},
createDirectory(path: string): void {
logger.logVerbose('fs.createDirectory', { path });
if (!fs) {
throw new Error('not supported');
}
try {
fs.createDirectory(pathMapper.toResource(path));
} catch (error) {
logger.logNormal('Error fs.createDirectory', { path, error: error + '' });
}
},
getExecutingFilePath(): string {
return currentDirectory;
},
getCurrentDirectory(): string {
return currentDirectory;
},
getDirectories(path: string): string[] {
logger.logVerbose('fs.getDirectories', { path });
return getAccessibleFileSystemEntries(path).directories.slice();
},
readDirectory(path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] {
logger.logVerbose('fs.readDirectory', { path });
return matchFiles(path, extensions, excludes, includes, /*useCaseSensitiveFileNames*/ true, currentDirectory, depth, getAccessibleFileSystemEntries, realpath);
},
getModifiedTime(path: string): Date | undefined {
logger.logVerbose('fs.getModifiedTime', { path });
if (!fs) {
throw new Error('not supported');
}
const uri = pathMapper.toResource(path);
let s: FileStat | undefined = undefined;
try {
s = fs.stat(uri);
} catch (_e) {
if (enabledExperimentalTypeAcquisition) {
try {
s = fs.stat(mapUri(uri, 'vscode-node-modules'));
} catch (_e) {
}
}
}
return s && new Date(s.mtime);
},
deleteFile(path: string): void {
logger.logVerbose('fs.deleteFile', { path });
if (!fs) {
throw new Error('not supported');
}
try {
fs.delete(pathMapper.toResource(path));
} catch (error) {
logger.logNormal('Error fs.deleteFile', { path, error: error + '' });
}
},
createHash: generateDjb2Hash,
/** This must be cryptographically secure.
The browser implementation, crypto.subtle.digest, is async so not possible to call from tsserver. */
createSHA256Hash: undefined,
exit: exit,
realpath,
base64decode: input => Buffer.from(input, 'base64').toString('utf8'),
base64encode: input => Buffer.from(input).toString('base64'),
};
// For module resolution only. `node_modules` is also automatically mapped
// as if all node_modules-like paths are symlinked.
function realpath(path: string): string {
const isNm = looksLikeNodeModules(path) && !path.startsWith('/vscode-global-typings/');
// skip paths without .. or ./ or /. And things that look like node_modules
if (!isNm && !path.match(/\.\.|\/\.|\.\//)) {
return path;
}
let uri = pathMapper.toResource(path);
if (isNm) {
uri = mapUri(uri, 'vscode-node-modules');
}
const out = [uri.scheme];
if (uri.authority) { out.push(uri.authority); }
for (const part of uri.path.split('/')) {
switch (part) {
case '':
case '.':
break;
case '..':
//delete if there is something there to delete
out.pop();
break;
default:
out.push(part);
}
}
return '/' + out.join('/');
}
function getAccessibleFileSystemEntries(path: string): { files: readonly string[]; directories: readonly string[] } {
if (!fs) {
throw new Error('not supported');
}
const uri = pathMapper.toResource(path || '.');
let entries: [string, FileType][] = [];
const files: string[] = [];
const directories: string[] = [];
try {
entries = fs.readDirectory(uri);
} catch (_e) {
try {
entries = fs.readDirectory(mapUri(uri, 'vscode-node-modules'));
} catch (_e) {
}
}
for (const [entry, type] of entries) {
// This is necessary because on some file system node fails to exclude
// '.' and '..'. See https://github.com/nodejs/node/issues/4002
if (entry === '.' || entry === '..') {
continue;
}
if (type === FileType.File) {
files.push(entry);
}
else if (type === FileType.Directory) {
directories.push(entry);
}
}
files.sort();
directories.sort();
return { files, directories };
}
}
export async function createSys(
args: readonly string[],
fsPort: MessagePort,
logger: Logger,
watchManager: FileWatcherManager,
pathMapper: PathMapper,
onExit: () => void,
) {
if (hasArgument(args, '--enableProjectWideIntelliSenseOnWeb')) {
const enabledExperimentalTypeAcquisition = hasArgument(args, '--experimentalTypeAcquisition');
const connection = new ClientConnection<Requests>(fsPort);
await connection.serviceReady();
const apiClient = new ApiClient(connection);
const fs = apiClient.vscode.workspace.fileSystem;
const sys = createServerHost(logger, apiClient, args, watchManager, pathMapper, enabledExperimentalTypeAcquisition, onExit);
return { sys, fs };
} else {
return { sys: createServerHost(logger, undefined, args, watchManager, pathMapper, false, onExit) };
}
}

View File

@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* 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';
export function hasArgument(args: readonly string[], name: string): boolean {
return args.indexOf(name) >= 0;
}
export function findArgument(args: readonly string[], name: string): string | undefined {
const index = args.indexOf(name);
return 0 <= index && index < args.length - 1
? args[index + 1]
: undefined;
}
export function findArgumentStringArray(args: readonly string[], name: string): readonly string[] {
const arg = findArgument(args, name);
return arg === undefined ? [] : arg.split(',').filter(name => name !== '');
}
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;
default: return mode;
}
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function hrtime(previous?: [number, number]): [number, number] {
const now = self.performance.now() * 0.001;
let seconds = Math.floor(now);
let nanoseconds = Math.floor((now % 1) * 1000000000);
// NOTE: This check is added probably because it's missed without strictFunctionTypes on
if (previous?.[0] !== undefined && previous?.[1] !== undefined) {
seconds = seconds - previous[0];
nanoseconds = nanoseconds - previous[1];
if (nanoseconds < 0) {
seconds--;
nanoseconds += 1000000000;
}
}
return [seconds, nanoseconds];
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as ts from 'typescript/lib/tsserverlibrary';
export class WasmCancellationToken implements ts.server.ServerCancellationToken {
shouldCancel: (() => boolean) | undefined;
currentRequestId: number | undefined = undefined;
setRequest(requestId: number) {
this.currentRequestId = requestId;
}
resetRequest(requestId: number) {
if (requestId === this.currentRequestId) {
this.currentRequestId = undefined;
} else {
throw new Error(`Mismatched request id, expected ${this.currentRequestId} but got ${requestId}`);
}
}
isCancellationRequested(): boolean {
return this.currentRequestId !== undefined && !!this.shouldCancel && this.shouldCancel();
}
}

View File

@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference lib='webworker.importscripts' />
/// <reference lib='webworker' />
import ts from 'typescript/lib/tsserverlibrary';
import { URI } from 'vscode-uri';
import { FileWatcherManager } from './fileWatcherManager';
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';
const setSys: (s: ts.System) => void = (ts as any).setSys;
// GLOBALS
let session: WorkerSession | undefined;
// END GLOBALS
async function initializeSession(
args: readonly string[],
extensionUri: URI,
ports: { tsserver: MessagePort; sync: MessagePort; watcher: MessagePort },
): Promise<void> {
const logLevel = parseLogLevel(findArgument(args, '--logVerbosity'));
const logger = new Logger(logLevel);
const modeOrUnknown = parseServerMode(args);
const serverMode = typeof modeOrUnknown === 'number' ? modeOrUnknown : undefined;
const unknownServerMode = typeof modeOrUnknown === 'string' ? modeOrUnknown : undefined;
logger.tsLogger.info(`Starting TS Server`);
logger.tsLogger.info(`Version: 0.0.0`);
logger.tsLogger.info(`Arguments: ${args.join(' ')}`);
logger.tsLogger.info(`ServerMode: ${serverMode} unknownServerMode: ${unknownServerMode}`);
const sessionOptions = parseSessionOptions(args, serverMode);
const enabledExperimentalTypeAcquisition = hasArgument(args, '--enableProjectWideIntelliSenseOnWeb') && hasArgument(args, '--experimentalTypeAcquisition');
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, () => {
removeEventListener('message', listener);
});
setSys(sys);
session = new WorkerSession(sys, fs, sessionOptions, ports.tsserver, pathMapper, logger);
session.listen();
}
function parseSessionOptions(args: readonly string[], serverMode: ts.LanguageServiceMode | undefined): StartSessionOptions {
return {
globalPlugins: findArgumentStringArray(args, '--globalPlugins'),
pluginProbeLocations: findArgumentStringArray(args, '--pluginProbeLocations'),
allowLocalPluginLoads: hasArgument(args, '--allowLocalPluginLoads'),
useSingleInferredProject: hasArgument(args, '--useSingleInferredProject'),
useInferredProjectPerProjectRoot: hasArgument(args, '--useInferredProjectPerProjectRoot'),
suppressDiagnosticEvents: hasArgument(args, '--suppressDiagnosticEvents'),
noGetErrOnBackgroundUpdate: hasArgument(args, '--noGetErrOnBackgroundUpdate'),
serverMode,
disableAutomaticTypingAcquisition: hasArgument(args, '--disableAutomaticTypingAcquisition'),
};
}
let hasInitialized = false;
const listener = async (e: any) => {
if (!hasInitialized) {
hasInitialized = true;
if ('args' in e.data) {
const args = e.data.args;
const extensionUri = URI.from(e.data.extensionUri);
const [sync, tsserver, watcher] = e.ports as MessagePort[];
await initializeSession(args, extensionUri, { sync, tsserver, watcher });
} else {
console.error('unexpected message in place of initial message: ' + JSON.stringify(e.data));
}
return;
}
console.error(`unexpected message on main channel: ${JSON.stringify(e)}`);
};
addEventListener('message', listener);

View File

@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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 { Logger } from './logging';
import WebTypingsInstaller 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'];
readonly allowLocalPluginLoads: ts.server.SessionOptions['allowLocalPluginLoads'];
readonly useSingleInferredProject: ts.server.SessionOptions['useSingleInferredProject'];
readonly useInferredProjectPerProjectRoot: ts.server.SessionOptions['useInferredProjectPerProjectRoot'];
readonly suppressDiagnosticEvents: ts.server.SessionOptions['suppressDiagnosticEvents'];
readonly noGetErrOnBackgroundUpdate: ts.server.SessionOptions['noGetErrOnBackgroundUpdate'];
readonly serverMode: ts.server.SessionOptions['serverMode'];
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;
}
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;
}
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
}
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;
}
if (this.logger.hasLevel(ts.server.LogLevel.verbose)) {
this.logger.info(`${msg.type}:${indent(JSON.stringify(msg))}`);
}
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);
}
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;
}
}

View File

@@ -8,7 +8,7 @@
"node"
]
},
"files": [
"webServer.ts"
"include": [
"src/**/*.ts"
]
}

View File

@@ -1,864 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference lib='webworker.importscripts' />
/// <reference lib='webworker' />
import { ApiClient, FileStat, FileSystem, 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 { URI } from 'vscode-uri';
import WebTypingsInstaller from './typingsInstaller';
// GLOBALS
const watchFiles: Map<string, { path: string; callback: ts.FileWatcherCallback; pollingInterval?: number; options?: ts.WatchOptions }> = new Map();
const watchDirectories: Map<string, { path: string; callback: ts.DirectoryWatcherCallback; recursive?: boolean; options?: ts.WatchOptions }> = new Map();
let session: WorkerSession | undefined;
const projectRootPaths = new Map</* original path*/ string, /* parsed URI */ URI>();
// END GLOBALS
// BEGIN misc internals
const indent: (str: string) => string = (ts as any).server.indent;
const setSys: (s: ts.System) => void = (ts as any).setSys;
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
function fromResource(extensionUri: URI, uri: URI) {
if (uri.scheme === extensionUri.scheme
&& uri.authority === extensionUri.authority
&& uri.path.startsWith(extensionUri.path + '/dist/browser/typescript/lib.')
&& uri.path.endsWith('.d.ts')) {
return uri.path;
}
return `/${uri.scheme}/${uri.authority}${uri.path}`;
}
function updateWatch(event: 'create' | 'change' | 'delete', uri: URI, extensionUri: URI) {
const kind = toTsWatcherKind(event);
const path = fromResource(extensionUri, uri);
const fileWatcher = watchFiles.get(path);
if (fileWatcher) {
fileWatcher.callback(path, kind);
return;
}
for (const watch of Array.from(watchDirectories.keys()).filter(dir => path.startsWith(dir))) {
watchDirectories.get(watch)!.callback(path);
return;
}
console.error(`no watcher found for ${path}`);
}
function toTsWatcherKind(event: 'create' | 'change' | 'delete') {
if (event === 'create') {
return ts.FileWatcherEventKind.Created;
} else if (event === 'change') {
return ts.FileWatcherEventKind.Changed;
} else if (event === 'delete') {
return ts.FileWatcherEventKind.Deleted;
}
throw new Error(`Unknown event: ${event}`);
}
class AccessOutsideOfRootError extends Error {
constructor(
public readonly filepath: string,
public readonly projectRootPaths: readonly string[]
) {
super(`Could not read file outside of project root ${filepath}`);
}
}
type ServerHostWithImport = ts.server.ServerHost & { importPlugin(root: string, moduleName: string): Promise<ts.server.ModuleImportResult> };
function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient: ApiClient | undefined, args: string[], fsWatcher: MessagePort, enabledExperimentalTypeAcquisition: boolean): ServerHostWithImport {
const currentDirectory = '/';
const fs = apiClient?.vscode.workspace.fileSystem;
let watchId = 0;
// Legacy web
const memoize: <T>(callback: () => T) => () => T = (ts as any).memoize;
const ensureTrailingDirectorySeparator: (path: string) => string = (ts as any).ensureTrailingDirectorySeparator;
const getDirectoryPath: (path: string) => string = (ts as any).getDirectoryPath;
const directorySeparator: string = (ts as any).directorySeparator;
const executingFilePath = findArgument(args, '--executingFilePath') || location + '';
const getExecutingDirectoryPath = memoize(() => memoize(() => ensureTrailingDirectorySeparator(getDirectoryPath(executingFilePath))));
const getWebPath = (path: string) => path.startsWith(directorySeparator) ? path.replace(directorySeparator, getExecutingDirectoryPath()) : undefined;
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
const log = (level: ts.server.LogLevel, message: string, data?: any) => {
if (logger.hasLevel(level)) {
logger.info(message + (data ? ' ' + JSON.stringify(data) : ''));
}
};
const logNormal = log.bind(null, ts.server.LogLevel.normal);
const logVerbose = log.bind(null, ts.server.LogLevel.verbose);
const noopWatcher: ts.FileWatcher = { close() { } };
return {
watchFile(path: string, callback: ts.FileWatcherCallback, pollingInterval?: number, options?: ts.WatchOptions): ts.FileWatcher {
if (looksLikeLibDtsPath(path)) { // We don't support watching lib files on web since they are readonly
return noopWatcher;
}
console.log('watching file:', path);
logVerbose('fs.watchFile', { path });
let uri: URI;
try {
uri = toResource(path);
} catch (e) {
console.error(e);
return noopWatcher;
}
watchFiles.set(path, { path, callback, pollingInterval, options });
const watchIds = [++watchId];
fsWatcher.postMessage({ type: 'watchFile', uri: uri, id: watchIds[0] });
if (enabledExperimentalTypeAcquisition && looksLikeNodeModules(path)) {
watchIds.push(++watchId);
fsWatcher.postMessage({ type: 'watchFile', uri: mapUri(uri, 'vscode-node-modules'), id: watchIds[1] });
}
return {
close() {
logVerbose('fs.watchFile.close', { path });
watchFiles.delete(path);
for (const id of watchIds) {
fsWatcher.postMessage({ type: 'dispose', id });
}
}
};
},
watchDirectory(path: string, callback: ts.DirectoryWatcherCallback, recursive?: boolean, options?: ts.WatchOptions): ts.FileWatcher {
logVerbose('fs.watchDirectory', { path });
let uri: URI;
try {
uri = toResource(path);
} catch (e) {
console.error(e);
return noopWatcher;
}
watchDirectories.set(path, { path, callback, recursive, options });
const watchIds = [++watchId];
fsWatcher.postMessage({ type: 'watchDirectory', recursive, uri, id: watchId });
return {
close() {
logVerbose('fs.watchDirectory.close', { path });
watchDirectories.delete(path);
for (const id of watchIds) {
fsWatcher.postMessage({ type: 'dispose', id });
}
}
};
},
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any {
return setTimeout(callback, ms, ...args);
},
clearTimeout(timeoutId: any): void {
clearTimeout(timeoutId);
},
setImmediate(callback: (...args: any[]) => void, ...args: any[]): any {
return this.setTimeout(callback, 0, ...args);
},
clearImmediate(timeoutId: any): void {
this.clearTimeout(timeoutId);
},
importPlugin: async (root, moduleName) => {
const packageRoot = combinePaths(root, moduleName);
let packageJson: any | undefined;
try {
const packageJsonResponse = await fetch(combinePaths(packageRoot, 'package.json'));
packageJson = await packageJsonResponse.json();
} catch (e) {
return { module: undefined, error: new Error(`Could not load plugin. Could not load 'package.json'.`) };
}
const browser = packageJson.browser;
if (!browser) {
return { module: undefined, error: new Error(`Could not load plugin. No 'browser' field found in package.json.`) };
}
const scriptPath = combinePaths(packageRoot, browser);
try {
const { default: module } = await import(/* webpackIgnore: true */scriptPath);
return { module, error: undefined };
} catch (e) {
return { module: undefined, error: e };
}
},
args,
newLine: '\n',
useCaseSensitiveFileNames: true,
write: s => {
apiClient?.vscode.terminal.write(s);
},
writeOutputIsTTY() {
return true;
},
readFile(path) {
logVerbose('fs.readFile', { path });
if (!fs) {
const webPath = getWebPath(path);
if (webPath) {
const request = new XMLHttpRequest();
request.open('GET', webPath, /* asynchronous */ false);
request.send();
return request.status === 200 ? request.responseText : undefined;
} else {
return undefined;
}
}
let uri;
try {
uri = toResource(path);
} catch (e) {
return undefined;
}
let contents: Uint8Array | undefined;
try {
// We need to slice the bytes since we can't pass a shared array to text decoder
contents = fs.readFile(uri);
} catch (error) {
if (!enabledExperimentalTypeAcquisition) {
return undefined;
}
try {
contents = fs.readFile(mapUri(uri, 'vscode-node-modules'));
} catch (e) {
return undefined;
}
}
return textDecoder.decode(contents.slice());
},
getFileSize(path) {
logVerbose('fs.getFileSize', { path });
if (!fs) {
throw new Error('not supported');
}
const uri = toResource(path);
let ret = 0;
try {
ret = fs.stat(uri).size;
} catch (_error) {
if (enabledExperimentalTypeAcquisition) {
try {
ret = fs.stat(mapUri(uri, 'vscode-node-modules')).size;
} catch (_error) {
}
}
}
return ret;
},
writeFile(path, data, writeByteOrderMark) {
logVerbose('fs.writeFile', { path });
if (!fs) {
throw new Error('not supported');
}
if (writeByteOrderMark) {
data = byteOrderMarkIndicator + data;
}
let uri;
try {
uri = toResource(path);
} catch (e) {
return;
}
const encoded = textEncoder.encode(data);
try {
fs.writeFile(uri, encoded);
const name = basename(uri.path);
if (uri.scheme !== 'vscode-global-typings' && (name === 'package.json' || name === 'package-lock.json' || name === 'package-lock.kdl')) {
fs.writeFile(mapUri(uri, 'vscode-node-modules'), encoded);
}
} catch (error) {
console.error('fs.writeFile', { path, error });
}
},
resolvePath(path: string): string {
return path;
},
fileExists(path: string): boolean {
logVerbose('fs.fileExists', { path });
if (!fs) {
const webPath = getWebPath(path);
if (!webPath) {
return false;
}
const request = new XMLHttpRequest();
request.open('HEAD', webPath, /* asynchronous */ false);
request.send();
return request.status === 200;
}
let uri;
try {
uri = toResource(path);
} catch (e) {
return false;
}
let ret = false;
try {
ret = fs.stat(uri).type === FileType.File;
} catch (_error) {
if (enabledExperimentalTypeAcquisition) {
try {
ret = fs.stat(mapUri(uri, 'vscode-node-modules')).type === FileType.File;
} catch (_error) {
}
}
}
return ret;
},
directoryExists(path: string): boolean {
logVerbose('fs.directoryExists', { path });
if (!fs) {
return false;
}
let uri;
try {
uri = toResource(path);
} catch (_error) {
return false;
}
let stat: FileStat | undefined = undefined;
try {
stat = fs.stat(uri);
} catch (_error) {
if (enabledExperimentalTypeAcquisition) {
try {
stat = fs.stat(mapUri(uri, 'vscode-node-modules'));
} catch (_error) {
}
}
}
if (stat) {
if (path.startsWith('/https') && !path.endsWith('.d.ts')) {
// TODO: Hack, https "file system" can't actually tell what is a file vs directory
return stat.type === FileType.File || stat.type === FileType.Directory;
}
return stat.type === FileType.Directory;
} else {
return false;
}
},
createDirectory(path: string): void {
logVerbose('fs.createDirectory', { path });
if (!fs) {
throw new Error('not supported');
}
try {
fs.createDirectory(toResource(path));
} catch (error) {
logNormal('Error fs.createDirectory', { path, error: error + '' });
}
},
getExecutingFilePath(): string {
return currentDirectory;
},
getCurrentDirectory(): string {
return currentDirectory;
},
getDirectories(path: string): string[] {
logVerbose('fs.getDirectories', { path });
return getAccessibleFileSystemEntries(path).directories.slice();
},
readDirectory(path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] {
logVerbose('fs.readDirectory', { path });
return matchFiles(path, extensions, excludes, includes, /*useCaseSensitiveFileNames*/ true, currentDirectory, depth, getAccessibleFileSystemEntries, realpath);
},
getModifiedTime(path: string): Date | undefined {
logVerbose('fs.getModifiedTime', { path });
if (!fs) {
throw new Error('not supported');
}
const uri = toResource(path);
let s: FileStat | undefined = undefined;
try {
s = fs.stat(uri);
} catch (_e) {
if (enabledExperimentalTypeAcquisition) {
try {
s = fs.stat(mapUri(uri, 'vscode-node-modules'));
} catch (_e) {
}
}
}
return s && new Date(s.mtime);
},
deleteFile(path: string): void {
logVerbose('fs.deleteFile', { path });
if (!fs) {
throw new Error('not supported');
}
try {
fs.delete(toResource(path));
} catch (error) {
logNormal('Error fs.deleteFile', { path, error: error + '' });
}
},
createHash: generateDjb2Hash,
/** This must be cryptographically secure.
The browser implementation, crypto.subtle.digest, is async so not possible to call from tsserver. */
createSHA256Hash: undefined,
exit(): void {
removeEventListener('message', listener);
},
realpath,
base64decode: input => Buffer.from(input, 'base64').toString('utf8'),
base64encode: input => Buffer.from(input).toString('base64'),
};
// For module resolution only. `node_modules` is also automatically mapped
// as if all node_modules-like paths are symlinked.
function realpath(path: string): string {
const isNm = looksLikeNodeModules(path) && !path.startsWith('/vscode-global-typings/');
// skip paths without .. or ./ or /. And things that look like node_modules
if (!isNm && !path.match(/\.\.|\/\.|\.\//)) {
return path;
}
let uri = toResource(path);
if (isNm) {
uri = mapUri(uri, 'vscode-node-modules');
}
const out = [uri.scheme];
if (uri.authority) { out.push(uri.authority); }
for (const part of uri.path.split('/')) {
switch (part) {
case '':
case '.':
break;
case '..':
//delete if there is something there to delete
out.pop();
break;
default:
out.push(part);
}
}
return '/' + out.join('/');
}
function getAccessibleFileSystemEntries(path: string): { files: readonly string[]; directories: readonly string[] } {
if (!fs) {
throw new Error('not supported');
}
const uri = toResource(path || '.');
let entries: [string, FileType][] = [];
const files: string[] = [];
const directories: string[] = [];
try {
entries = fs.readDirectory(uri);
} catch (_e) {
try {
entries = fs.readDirectory(mapUri(uri, 'vscode-node-modules'));
} catch (_e) {
}
}
for (const [entry, type] of entries) {
// This is necessary because on some file system node fails to exclude
// '.' and '..'. See https://github.com/nodejs/node/issues/4002
if (entry === '.' || entry === '..') {
continue;
}
if (type === FileType.File) {
files.push(entry);
}
else if (type === FileType.Directory) {
directories.push(entry);
}
}
files.sort();
directories.sort();
return { files, directories };
}
/**
* Copied from toResource in typescriptServiceClient.ts
*/
function toResource(filepath: string): URI {
if (looksLikeLibDtsPath(filepath)) {
return URI.from({
scheme: extensionUri.scheme,
authority: extensionUri.authority,
path: extensionUri.path + '/dist/browser/typescript/' + filepath.slice(1)
});
}
const uri = filePathToResourceUri(filepath);
if (!uri) {
throw new Error(`Could not parse path ${filepath}`);
}
// Check if TS is trying to read a file outside of the project root.
// We allow reading files on unknown scheme as these may be loose files opened by the user.
// However we block reading files on schemes that are on a known file system with an unknown root
let allowRead: 'implicit' | 'block' | 'allow' = 'implicit';
for (const projectRoot of projectRootPaths.values()) {
if (uri.scheme === projectRoot.scheme) {
if (uri.toString().startsWith(projectRoot.toString())) {
allowRead = 'allow';
break;
}
// Tentatively block the read but a future loop may allow it
allowRead = 'block';
}
}
if (allowRead === 'block') {
throw new AccessOutsideOfRootError(filepath, Array.from(projectRootPaths.keys()));
}
return uri;
}
}
function looksLikeLibDtsPath(filepath: string) {
return filepath.startsWith('/lib.') && filepath.endsWith('.d.ts');
}
function looksLikeNodeModules(filepath: string) {
return filepath.includes('/node_modules');
}
function filePathToResourceUri(filepath: string): URI | undefined {
const parts = filepath.match(/^\/([^\/]+)\/([^\/]*)(?:\/(.+))?$/);
if (!parts) {
return undefined;
}
const scheme = parts[1];
const authority = parts[2] === 'ts-nul-authority' ? '' : parts[2];
const path = parts[3];
return URI.from({ scheme, authority, path: (path ? '/' + path : path) });
}
class WasmCancellationToken implements ts.server.ServerCancellationToken {
shouldCancel: (() => boolean) | undefined;
currentRequestId: number | undefined = undefined;
setRequest(requestId: number) {
this.currentRequestId = requestId;
}
resetRequest(requestId: number) {
if (requestId === this.currentRequestId) {
this.currentRequestId = undefined;
} else {
throw new Error(`Mismatched request id, expected ${this.currentRequestId} but got ${requestId}`);
}
}
isCancellationRequested(): boolean {
return this.currentRequestId !== undefined && !!this.shouldCancel && this.shouldCancel();
}
}
interface StartSessionOptions {
readonly globalPlugins: ts.server.SessionOptions['globalPlugins'];
readonly pluginProbeLocations: ts.server.SessionOptions['pluginProbeLocations'];
readonly allowLocalPluginLoads: ts.server.SessionOptions['allowLocalPluginLoads'];
readonly useSingleInferredProject: ts.server.SessionOptions['useSingleInferredProject'];
readonly useInferredProjectPerProjectRoot: ts.server.SessionOptions['useInferredProjectPerProjectRoot'];
readonly suppressDiagnosticEvents: ts.server.SessionOptions['suppressDiagnosticEvents'];
readonly noGetErrOnBackgroundUpdate: ts.server.SessionOptions['noGetErrOnBackgroundUpdate'];
readonly serverMode: ts.server.SessionOptions['serverMode'];
readonly disableAutomaticTypingAcquisition: boolean;
}
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,
logger: ts.server.Logger,
hrtime: ts.server.SessionOptions['hrtime']
) {
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,
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 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) {
const uri = filePathToResourceUri(open.projectRootPath);
if (uri) {
projectRootPaths.set(open.projectRootPath, uri);
}
}
}
}
} 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;
}
if (this.logger.hasLevel(ts.server.LogLevel.verbose)) {
this.logger.info(`${msg.type}:${indent(JSON.stringify(msg))}`);
}
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);
}
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;
}
}
function parseServerMode(args: 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;
default:
return mode;
}
}
function hrtime(previous?: [number, number]): [number, number] {
const now = self.performance.now() * 1e-3;
let seconds = Math.floor(now);
let nanoseconds = Math.floor((now % 1) * 1e9);
// NOTE: This check is added probably because it's missed without strictFunctionTypes on
if (previous?.[0] !== undefined && previous?.[1] !== undefined) {
seconds = seconds - previous[0];
nanoseconds = nanoseconds - previous[1];
if (nanoseconds < 0) {
seconds--;
nanoseconds += 1e9;
}
}
return [seconds, nanoseconds];
}
function hasArgument(args: readonly string[], name: string): boolean {
return args.indexOf(name) >= 0;
}
function findArgument(args: readonly string[], name: string): string | undefined {
const index = args.indexOf(name);
return 0 <= index && index < args.length - 1
? args[index + 1]
: undefined;
}
function findArgumentStringArray(args: readonly string[], name: string): readonly string[] {
const arg = findArgument(args, name);
return arg === undefined ? [] : arg.split(',').filter(name => name !== '');
}
async function initializeSession(args: string[], extensionUri: URI, ports: { tsserver: MessagePort; sync: MessagePort; watcher: MessagePort }, logger: ts.server.Logger): Promise<void> {
const modeOrUnknown = parseServerMode(args);
const serverMode = typeof modeOrUnknown === 'number' ? modeOrUnknown : undefined;
const unknownServerMode = typeof modeOrUnknown === 'string' ? modeOrUnknown : undefined;
logger.info(`Starting TS Server`);
logger.info(`Version: 0.0.0`);
logger.info(`Arguments: ${args.join(' ')}`);
logger.info(`ServerMode: ${serverMode} unknownServerMode: ${unknownServerMode}`);
const options: StartSessionOptions = {
globalPlugins: findArgumentStringArray(args, '--globalPlugins'),
pluginProbeLocations: findArgumentStringArray(args, '--pluginProbeLocations'),
allowLocalPluginLoads: hasArgument(args, '--allowLocalPluginLoads'),
useSingleInferredProject: hasArgument(args, '--useSingleInferredProject'),
useInferredProjectPerProjectRoot: hasArgument(args, '--useInferredProjectPerProjectRoot'),
suppressDiagnosticEvents: hasArgument(args, '--suppressDiagnosticEvents'),
noGetErrOnBackgroundUpdate: hasArgument(args, '--noGetErrOnBackgroundUpdate'),
serverMode,
disableAutomaticTypingAcquisition: hasArgument(args, '--disableAutomaticTypingAcquisition'),
};
let sys: ServerHostWithImport;
let fs: FileSystem | undefined;
if (hasArgument(args, '--enableProjectWideIntelliSenseOnWeb')) {
const enabledExperimentalTypeAcquisition = hasArgument(args, '--experimentalTypeAcquisition');
const connection = new ClientConnection<Requests>(ports.sync);
await connection.serviceReady();
const apiClient = new ApiClient(connection);
fs = apiClient.vscode.workspace.fileSystem;
sys = createServerHost(extensionUri, logger, apiClient, args, ports.watcher, enabledExperimentalTypeAcquisition);
} else {
sys = createServerHost(extensionUri, logger, undefined, args, ports.watcher, false);
}
setSys(sys);
session = new WorkerSession(sys, fs, options, ports.tsserver, logger, hrtime);
session.listen();
}
function parseLogLevel(input: string | undefined): ts.server.LogLevel | undefined {
switch (input) {
case 'normal': return ts.server.LogLevel.normal;
case 'terse': return ts.server.LogLevel.terse;
case 'verbose': return ts.server.LogLevel.verbose;
default: return undefined;
}
}
let hasInitialized = false;
const listener = async (e: any) => {
if (!hasInitialized) {
hasInitialized = true;
if ('args' in e.data) {
const args = e.data.args;
const logLevel = parseLogLevel(findArgument(args, '--logVerbosity'));
const doLog = typeof logLevel === 'undefined'
? (_message: string) => { }
: (message: string) => { postMessage({ type: 'log', body: message }); };
const logger: ts.server.Logger = {
close: () => { },
hasLevel: level => typeof logLevel === 'undefined' ? false : level <= logLevel,
loggingEnabled: () => true,
perftrc: () => { },
info: doLog,
msg: doLog,
startGroup: () => { },
endGroup: () => { },
getLogFileName: () => undefined
};
const [sync, tsserver, watcher] = e.ports as MessagePort[];
const extensionUri = URI.from(e.data.extensionUri);
watcher.onmessage = (e: any) => updateWatch(e.data.event, URI.from(e.data.uri), extensionUri);
await initializeSession(args, extensionUri, { sync, tsserver, watcher }, logger);
} else {
console.error('unexpected message in place of initial message: ' + JSON.stringify(e.data));
}
return;
}
console.error(`unexpected message on main channel: ${JSON.stringify(e)}`);
};
addEventListener('message', listener);
function mapUri(uri: URI, mappedScheme: string): URI {
if (uri.scheme === 'vscode-global-typings') {
throw new Error('can\'t map vscode-global-typings');
}
if (!uri.authority) {
uri = uri.with({ authority: 'ts-nul-authority' });
}
uri = uri.with({ scheme: mappedScheme, path: `/${uri.scheme}/${uri.authority || 'ts-nul-authority'}${uri.path}` });
return uri;
}