-
Notifications
You must be signed in to change notification settings - Fork 30.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactors webServer.ts to split it into multiple files and encapsulate some functionality in classes
- Loading branch information
Showing
15 changed files
with
1,005 additions
and
868 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
extensions/typescript-language-features/web/src/fileWatcherManager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Sorry, something went wrong. |
||
|
||
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
60
extensions/typescript-language-features/web/src/logging.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
112
extensions/typescript-language-features/web/src/pathMapper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.
@mjbvz stray console log