mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-26 10:43:51 -05:00
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 commit0edf650622. * Revert "TODO" This reverts commit04a4fe7556. * Revert "Should also check if there are no external projects before skipping scheduling" This reverts commit7e4939014a. * Refactoring so we can test the changes out * Feedback Co-authored-by: Sheetal Nandi <shkamat@microsoft.com>
This commit is contained in:
20
src/webServer/tsconfig.json
Normal file
20
src/webServer/tsconfig.json
Normal 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
216
src/webServer/webServer.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user