Skip to content

Commit

Permalink
Split up webServer.ts (#198802)
Browse files Browse the repository at this point in the history
Refactors webServer.ts to split it into multiple files and encapsulate some functionality in classes
  • Loading branch information
mjbvz authored Nov 21, 2023
1 parent f54183b commit fbfabc5
Show file tree
Hide file tree
Showing 15 changed files with 1,005 additions and 868 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ module.exports = [withBrowserDefaults({
}), withBrowserDefaults({
context: __dirname,
entry: {
'typescript/tsserver.web': './web/webServer.ts'
'typescript/tsserver.web': './web/src/webServer.ts'
},
module: {
exprContextCritical: false,
Expand Down
2 changes: 1 addition & 1 deletion extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"scripts": {
"vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:typescript-language-features",
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch"
},
"activationEvents": [
"onLanguage:javascript",
Expand Down
119 changes: 119 additions & 0 deletions extensions/typescript-language-features/web/src/fileWatcherManager.ts
Original file line number Diff line number Diff line change
@@ -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 comment has been minimized.

Copy link
@bpasero

bpasero Nov 23, 2023

Member

@mjbvz stray console log


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}`);
}
}

60 changes: 60 additions & 0 deletions extensions/typescript-language-features/web/src/logging.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
112 changes: 112 additions & 0 deletions extensions/typescript-language-features/web/src/pathMapper.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit fbfabc5

Please sign in to comment.