Adds experimental support for running TS Server in a web worker (#39656)

* Adds experimental support for running TS Server in a web worker

This change makes it possible to run a syntax old TS server in a webworker. This is will let serverless versions of VS Code web run the TypeScript extension with minimal changes.

As the diff on `server.ts` is difficult to parse, here's an overview of the changes:

- Introduce the concept of a `Runtime`. Valid values are `Node` and `Web`.
- Move calls to `require` into the functions that use these modules
- Wrap existing server logic into `startNodeServer`
- Introduce web server with `startWebServer`. This uses a `WorkerSession`
- Add a custom version of `ts.sys` for web
- Have the worker server start when it is passed an array of arguments in a message

In order to make the server logic more clear, this change also tries to reduce the reliance on closures and better group function declarations vs the server spawning logic.

**Next Steps**
I'd like someone from the TS team to help get these changes into a shippable state. This will involve:

- Adddress todo comments
- Code cleanup
- Make sure these changes do not regress node servers
- Determine if we should add a new `tsserver.web.js` file instead of having the web worker logic all live in `tsserver.js`

* Shim out directoryExists

* Add some regions

* Remove some inlined note types

Use import types instead

* Use switch case for runtime

* Review and updates

* Enable loading std library d.ts files

This implements enough of `ServerHost` that we can load the standard d.ts files using synchronous XMLHttpRequests.

I also had to patch some code in `editorServices`. I don't know if these changes are correct and need someone on the TS team to review

* Update src/tsserver/webServer.ts

* Update src/tsserver/webServer.ts

Co-authored-by: Sheetal Nandi <shkamat@microsoft.com>

* Addressing feedback

* Allow passing in explicit executingFilePath

This is required for cases where `self.location` does not point to the directory where all the typings are stored

* Adding logging support

* Do not create auto import provider in partial semantic mode

* Handle lib files by doing path mapping instead

* TODO

* Add log message

This replaces the console based logger with a logger that post log messages back to the host. VS Code will write these messages to its output window

* Move code around so that exported functions are set on namespace

* Log response

* Map the paths back to https:
// TODO: is this really needed or can vscode take care of this
How do we handle when opening lib.d.ts as response to goto def in open files

* If files are not open dont schedule open file project ensure

* Should also check if there are no external projects before skipping scheduling
Fixes failing tests

* Revert "Map the paths back to https:"

This reverts commit 0edf650622.

* Revert "TODO"

This reverts commit 04a4fe7556.

* Revert "Should also check if there are no external projects before skipping scheduling"

This reverts commit 7e4939014a.

* Refactoring so we can test the changes out

* Feedback

Co-authored-by: Sheetal Nandi <shkamat@microsoft.com>
This commit is contained in:
Matt Bierner
2020-12-08 16:09:43 -08:00
committed by GitHub
parent d8c8e4ff06
commit 49d7de17d6
11 changed files with 1526 additions and 1002 deletions

View File

@@ -0,0 +1,20 @@
{
"extends": "../tsconfig-base",
"compilerOptions": {
"removeComments": false,
"outFile": "../../built/local/webServer.js",
"preserveConstEnums": true,
"types": [
"node"
]
},
"references": [
{ "path": "../compiler" },
{ "path": "../jsTyping" },
{ "path": "../services" },
{ "path": "../server" }
],
"files": [
"webServer.ts",
]
}

216
src/webServer/webServer.ts Normal file
View File

@@ -0,0 +1,216 @@
/*@internal*/
namespace ts.server {
export interface HostWithWriteMessage {
writeMessage(s: any): void;
}
export interface WebHost extends HostWithWriteMessage {
readFile(path: string): string | undefined;
fileExists(path: string): boolean;
}
export class BaseLogger implements Logger {
private seq = 0;
private inGroup = false;
private firstInGroup = true;
constructor(protected readonly level: LogLevel) {
}
static padStringRight(str: string, padding: string) {
return (str + padding).slice(0, padding.length);
}
close() {
}
getLogFileName(): string | undefined {
return undefined;
}
perftrc(s: string) {
this.msg(s, Msg.Perf);
}
info(s: string) {
this.msg(s, Msg.Info);
}
err(s: string) {
this.msg(s, Msg.Err);
}
startGroup() {
this.inGroup = true;
this.firstInGroup = true;
}
endGroup() {
this.inGroup = false;
}
loggingEnabled() {
return true;
}
hasLevel(level: LogLevel) {
return this.loggingEnabled() && this.level >= level;
}
msg(s: string, type: Msg = Msg.Err) {
switch (type) {
case Msg.Info:
perfLogger.logInfoEvent(s);
break;
case Msg.Perf:
perfLogger.logPerfEvent(s);
break;
default: // Msg.Err
perfLogger.logErrEvent(s);
break;
}
if (!this.canWrite()) return;
s = `[${nowString()}] ${s}\n`;
if (!this.inGroup || this.firstInGroup) {
const prefix = BaseLogger.padStringRight(type + " " + this.seq.toString(), " ");
s = prefix + s;
}
this.write(s, type);
if (!this.inGroup) {
this.seq++;
}
}
protected canWrite() {
return true;
}
protected write(_s: string, _type: Msg) {
}
}
export type MessageLogLevel = "info" | "perf" | "error";
export interface LoggingMessage {
readonly type: "log";
readonly level: MessageLogLevel;
readonly body: string
}
export class MainProcessLogger extends BaseLogger {
constructor(level: LogLevel, private host: HostWithWriteMessage) {
super(level);
}
protected write(body: string, type: Msg) {
let level: MessageLogLevel;
switch (type) {
case Msg.Info:
level = "info";
break;
case Msg.Perf:
level = "perf";
break;
case Msg.Err:
level = "error";
break;
default:
Debug.assertNever(type);
}
this.host.writeMessage(<LoggingMessage>{
type: "log",
level,
body,
});
}
}
export function createWebSystem(host: WebHost, args: string[], getExecutingFilePath: () => string): ServerHost {
const returnEmptyString = () => "";
const getExecutingDirectoryPath = memoize(() => memoize(() => ensureTrailingDirectorySeparator(getDirectoryPath(getExecutingFilePath()))));
// Later we could map ^memfs:/ to do something special if we want to enable more functionality like module resolution or something like that
const getWebPath = (path: string) => startsWith(path, directorySeparator) ? path.replace(directorySeparator, getExecutingDirectoryPath()) : undefined;
return {
args,
newLine: "\r\n", // This can be configured by clients
useCaseSensitiveFileNames: false, // Use false as the default on web since that is the safest option
readFile: path => {
const webPath = getWebPath(path);
return webPath && host.readFile(webPath);
},
write: host.writeMessage.bind(host),
watchFile: returnNoopFileWatcher,
watchDirectory: returnNoopFileWatcher,
getExecutingFilePath: () => directorySeparator,
getCurrentDirectory: returnEmptyString, // For inferred project root if projectRoot path is not set, normalizing the paths
/* eslint-disable no-restricted-globals */
setTimeout,
clearTimeout,
setImmediate: x => setTimeout(x, 0),
clearImmediate: clearTimeout,
/* eslint-enable no-restricted-globals */
require: () => ({ module: undefined, error: new Error("Not implemented") }),
exit: notImplemented,
// Debugging related
getEnvironmentVariable: returnEmptyString, // TODO:: Used to enable debugging info
// tryEnableSourceMapsForHost?(): void;
// debugMode?: boolean;
// For semantic server mode
fileExists: path => {
const webPath = getWebPath(path);
return !!webPath && host.fileExists(webPath);
},
directoryExists: returnFalse, // Module resolution
readDirectory: notImplemented, // Configured project, typing installer
getDirectories: () => [], // For automatic type reference directives
createDirectory: notImplemented, // compile On save
writeFile: notImplemented, // compile on save
resolvePath: identity, // Plugins
// realpath? // Module resolution, symlinks
// getModifiedTime // File watching
// createSHA256Hash // telemetry of the project
// Logging related
// /*@internal*/ bufferFrom?(input: string, encoding?: string): Buffer;
// gc?(): void;
// getMemoryUsage?(): number;
};
}
export interface StartSessionOptions {
globalPlugins: SessionOptions["globalPlugins"];
pluginProbeLocations: SessionOptions["pluginProbeLocations"];
allowLocalPluginLoads: SessionOptions["allowLocalPluginLoads"];
useSingleInferredProject: SessionOptions["useSingleInferredProject"];
useInferredProjectPerProjectRoot: SessionOptions["useInferredProjectPerProjectRoot"];
suppressDiagnosticEvents: SessionOptions["suppressDiagnosticEvents"];
noGetErrOnBackgroundUpdate: SessionOptions["noGetErrOnBackgroundUpdate"];
syntaxOnly: SessionOptions["syntaxOnly"];
serverMode: SessionOptions["serverMode"];
}
export class WorkerSession extends Session<{}> {
constructor(host: ServerHost, private webHost: HostWithWriteMessage, options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, hrtime: SessionOptions["hrtime"]) {
super({
host,
cancellationToken,
...options,
typingsInstaller: nullTypingsInstaller,
byteLength: notImplemented, // Formats the message text in send of Session which is overriden in this class so not needed
hrtime,
logger,
canUseEvents: false,
});
}
public send(msg: protocol.Message) {
if (msg.type === "event" && !this.canUseEvents) {
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`);
}
return;
}
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`${msg.type}:${indent(JSON.stringify(msg))}`);
}
this.webHost.writeMessage(msg);
}
protected parseMessage(message: {}): protocol.Request {
return <protocol.Request>message;
}
protected toStringMessage(message: {}) {
return JSON.stringify(message, undefined, 2);
}
}
}