Skip to content

Commit 49d7de1

Browse files
mjbvzsheetalkamat
andauthored
Adds experimental support for running TS Server in a web worker (microsoft#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 <[email protected]> * 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 0edf650. * Revert "TODO" This reverts commit 04a4fe7. * Revert "Should also check if there are no external projects before skipping scheduling" This reverts commit 7e49390. * Refactoring so we can test the changes out * Feedback Co-authored-by: Sheetal Nandi <[email protected]>
1 parent d8c8e4f commit 49d7de1

File tree

11 files changed

+1523
-999
lines changed

11 files changed

+1523
-999
lines changed

Gulpfile.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ const watchLssl = () => watch([
308308
"src/services/**/*.ts",
309309
"src/server/tsconfig.json",
310310
"src/server/**/*.ts",
311+
"src/webServer/tsconfig.json",
312+
"src/webServer/**/*.ts",
311313
"src/tsserver/tsconfig.json",
312314
"src/tsserver/**/*.ts",
313315
], buildLssl);

src/server/session.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ namespace ts.server {
689689
typesMapLocation?: string;
690690
}
691691

692-
export class Session implements EventSender {
692+
export class Session<TMessage = string> implements EventSender {
693693
private readonly gcTimer: GcTimer;
694694
protected projectService: ProjectService;
695695
private changeSeq = 0;
@@ -2907,7 +2907,7 @@ namespace ts.server {
29072907
}
29082908
}
29092909

2910-
public onMessage(message: string) {
2910+
public onMessage(message: TMessage) {
29112911
this.gcTimer.scheduleCollect();
29122912

29132913
this.performanceData = undefined;
@@ -2916,18 +2916,18 @@ namespace ts.server {
29162916
if (this.logger.hasLevel(LogLevel.requestTime)) {
29172917
start = this.hrtime();
29182918
if (this.logger.hasLevel(LogLevel.verbose)) {
2919-
this.logger.info(`request:${indent(message)}`);
2919+
this.logger.info(`request:${indent(this.toStringMessage(message))}`);
29202920
}
29212921
}
29222922

29232923
let request: protocol.Request | undefined;
29242924
let relevantFile: protocol.FileRequestArgs | undefined;
29252925
try {
2926-
request = <protocol.Request>JSON.parse(message);
2926+
request = this.parseMessage(message);
29272927
relevantFile = request.arguments && (request as protocol.FileRequest).arguments.file ? (request as protocol.FileRequest).arguments : undefined;
29282928

29292929
tracing.instant(tracing.Phase.Session, "request", { seq: request.seq, command: request.command });
2930-
perfLogger.logStartCommand("" + request.command, message.substring(0, 100));
2930+
perfLogger.logStartCommand("" + request.command, this.toStringMessage(message).substring(0, 100));
29312931

29322932
tracing.push(tracing.Phase.Session, "executeCommand", { seq: request.seq, command: request.command }, /*separateBeginAndEnd*/ true);
29332933
const { response, responseRequired } = this.executeCommand(request);
@@ -2965,7 +2965,7 @@ namespace ts.server {
29652965
return;
29662966
}
29672967

2968-
this.logErrorWorker(err, message, relevantFile);
2968+
this.logErrorWorker(err, this.toStringMessage(message), relevantFile);
29692969
perfLogger.logStopCommand("" + (request && request.command), "Error: " + err);
29702970
tracing.instant(tracing.Phase.Session, "commandError", { seq: request?.seq, command: request?.command, message: (<Error>err).message });
29712971

@@ -2978,6 +2978,14 @@ namespace ts.server {
29782978
}
29792979
}
29802980

2981+
protected parseMessage(message: TMessage): protocol.Request {
2982+
return <protocol.Request>JSON.parse(message as any as string);
2983+
}
2984+
2985+
protected toStringMessage(message: TMessage): string {
2986+
return message as any as string;
2987+
}
2988+
29812989
private getFormatOptions(file: NormalizedPath): FormatCodeSettings {
29822990
return this.projectService.getFormatCodeOptions(file);
29832991
}

src/testRunner/tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
{ "path": "../services", "prepend": true },
2222
{ "path": "../jsTyping", "prepend": true },
2323
{ "path": "../server", "prepend": true },
24+
{ "path": "../webServer", "prepend": true },
2425
{ "path": "../typingsInstallerCore", "prepend": true },
2526
{ "path": "../harness", "prepend": true }
2627
],
@@ -205,6 +206,7 @@
205206
"unittests/tsserver/typingsInstaller.ts",
206207
"unittests/tsserver/versionCache.ts",
207208
"unittests/tsserver/watchEnvironment.ts",
209+
"unittests/tsserver/webServer.ts",
208210
"unittests/debugDeprecation.ts"
209211
]
210212
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
namespace ts.projectSystem {
2+
describe("unittests:: tsserver:: webServer", () => {
3+
class TestWorkerSession extends server.WorkerSession {
4+
constructor(host: server.ServerHost, webHost: server.HostWithWriteMessage, options: Partial<server.StartSessionOptions>, logger: server.Logger) {
5+
super(
6+
host,
7+
webHost,
8+
{
9+
globalPlugins: undefined,
10+
pluginProbeLocations: undefined,
11+
allowLocalPluginLoads: undefined,
12+
useSingleInferredProject: true,
13+
useInferredProjectPerProjectRoot: false,
14+
suppressDiagnosticEvents: false,
15+
noGetErrOnBackgroundUpdate: true,
16+
syntaxOnly: undefined,
17+
serverMode: undefined,
18+
...options
19+
},
20+
logger,
21+
server.nullCancellationToken,
22+
() => emptyArray
23+
);
24+
}
25+
26+
getProjectService() {
27+
return this.projectService;
28+
}
29+
}
30+
function setup(logLevel: server.LogLevel | undefined) {
31+
const host = createServerHost([libFile], { windowsStyleRoot: "c:/" });
32+
const messages: any[] = [];
33+
const webHost: server.WebHost = {
34+
readFile: s => host.readFile(s),
35+
fileExists: s => host.fileExists(s),
36+
writeMessage: s => messages.push(s),
37+
};
38+
const webSys = server.createWebSystem(webHost, emptyArray, () => host.getExecutingFilePath());
39+
const logger = logLevel !== undefined ? new server.MainProcessLogger(logLevel, webHost) : nullLogger;
40+
const session = new TestWorkerSession(webSys, webHost, { serverMode: LanguageServiceMode.PartialSemantic }, logger);
41+
return { getMessages: () => messages, clearMessages: () => messages.length = 0, session };
42+
43+
}
44+
45+
describe("open files are added to inferred project and semantic operations succeed", () => {
46+
function verify(logLevel: server.LogLevel | undefined) {
47+
const { session, clearMessages, getMessages } = setup(logLevel);
48+
const service = session.getProjectService();
49+
const file: File = {
50+
path: "^memfs:/sample-folder/large.ts",
51+
content: "export const numberConst = 10; export const arrayConst: Array<string> = [];"
52+
};
53+
session.executeCommand({
54+
seq: 1,
55+
type: "request",
56+
command: protocol.CommandTypes.Open,
57+
arguments: {
58+
file: file.path,
59+
fileContent: file.content
60+
}
61+
});
62+
checkNumberOfProjects(service, { inferredProjects: 1 });
63+
const project = service.inferredProjects[0];
64+
checkProjectActualFiles(project, ["/lib.d.ts", file.path]); // Lib files are rooted
65+
verifyQuickInfo();
66+
verifyGotoDefInLib();
67+
68+
function verifyQuickInfo() {
69+
clearMessages();
70+
const start = protocolFileLocationFromSubstring(file, "numberConst");
71+
session.onMessage({
72+
seq: 2,
73+
type: "request",
74+
command: protocol.CommandTypes.Quickinfo,
75+
arguments: start
76+
});
77+
assert.deepEqual(last(getMessages()), {
78+
seq: 0,
79+
type: "response",
80+
command: protocol.CommandTypes.Quickinfo,
81+
request_seq: 2,
82+
success: true,
83+
performanceData: undefined,
84+
body: {
85+
kind: ScriptElementKind.constElement,
86+
kindModifiers: "export",
87+
start: { line: start.line, offset: start.offset },
88+
end: { line: start.line, offset: start.offset + "numberConst".length },
89+
displayString: "const numberConst: 10",
90+
documentation: "",
91+
tags: []
92+
}
93+
});
94+
verifyLogger();
95+
}
96+
97+
function verifyGotoDefInLib() {
98+
clearMessages();
99+
const start = protocolFileLocationFromSubstring(file, "Array");
100+
session.onMessage({
101+
seq: 3,
102+
type: "request",
103+
command: protocol.CommandTypes.DefinitionAndBoundSpan,
104+
arguments: start
105+
});
106+
assert.deepEqual(last(getMessages()), {
107+
seq: 0,
108+
type: "response",
109+
command: protocol.CommandTypes.DefinitionAndBoundSpan,
110+
request_seq: 3,
111+
success: true,
112+
performanceData: undefined,
113+
body: {
114+
definitions: [{
115+
file: "/lib.d.ts",
116+
...protocolTextSpanWithContextFromSubstring({
117+
fileText: libFile.content,
118+
text: "Array",
119+
contextText: "interface Array<T> { length: number; [n: number]: T; }"
120+
})
121+
}],
122+
textSpan: {
123+
start: { line: start.line, offset: start.offset },
124+
end: { line: start.line, offset: start.offset + "Array".length },
125+
}
126+
}
127+
});
128+
verifyLogger();
129+
}
130+
131+
function verifyLogger() {
132+
const messages = getMessages();
133+
assert.equal(messages.length, logLevel === server.LogLevel.verbose ? 4 : 1, `Expected ${JSON.stringify(messages)}`);
134+
if (logLevel === server.LogLevel.verbose) {
135+
verifyLogMessages(messages[0], "info");
136+
verifyLogMessages(messages[1], "perf");
137+
verifyLogMessages(messages[2], "info");
138+
}
139+
clearMessages();
140+
}
141+
142+
function verifyLogMessages(actual: any, expectedLevel: server.MessageLogLevel) {
143+
assert.equal(actual.type, "log");
144+
assert.equal(actual.level, expectedLevel);
145+
}
146+
}
147+
148+
it("with logging enabled", () => {
149+
verify(server.LogLevel.verbose);
150+
});
151+
152+
it("with logging disabled", () => {
153+
verify(/*logLevel*/ undefined);
154+
});
155+
});
156+
});
157+
}

0 commit comments

Comments
 (0)