Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split up webServer.ts #198802

Merged
merged 1 commit into from Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
@@ -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}`);
}
}

60 changes: 60 additions & 0 deletions extensions/typescript-language-features/web/src/logging.ts
@@ -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
@@ -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;
}