Skip to content

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

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

Merged
merged 31 commits into from
Dec 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e50aedc
Adds experimental support for running TS Server in a web worker
mjbvz Jul 18, 2020
30cb1a6
Shim out directoryExists
mjbvz Jul 23, 2020
fb900a0
Add some regions
mjbvz Aug 31, 2020
4ce8d4a
Remove some inlined note types
mjbvz Aug 31, 2020
94faf66
Use switch case for runtime
mjbvz Aug 31, 2020
f6c0749
Merge branch 'master' into web-worker-server
sheetalkamat Sep 22, 2020
f3178b6
Review and updates
sheetalkamat Sep 23, 2020
6202c51
Enable loading std library d.ts files
mjbvz Sep 25, 2020
b6e8137
Update src/tsserver/webServer.ts
sheetalkamat Sep 28, 2020
dd331e2
Update src/tsserver/webServer.ts
mjbvz Sep 29, 2020
2d4c726
Addressing feedback
mjbvz Sep 29, 2020
69a6523
Allow passing in explicit executingFilePath
mjbvz Sep 29, 2020
5185f6e
Adding logging support
mjbvz Sep 30, 2020
1db3ebc
Do not create auto import provider in partial semantic mode
sheetalkamat Oct 1, 2020
9a1eace
Handle lib files by doing path mapping instead
sheetalkamat Oct 1, 2020
c92d22d
Merge branch 'master' into web-worker-server
sheetalkamat Oct 2, 2020
14498b8
Merge branch 'master' into web-worker-server
sheetalkamat Oct 27, 2020
04a4fe7
TODO
sheetalkamat Oct 27, 2020
b959f3e
Add log message
mjbvz Nov 12, 2020
2359c83
Move code around so that exported functions are set on namespace
sheetalkamat Nov 13, 2020
f29b2e7
Log response
sheetalkamat Nov 13, 2020
0edf650
Map the paths back to https:
sheetalkamat Nov 14, 2020
0820519
If files are not open dont schedule open file project ensure
sheetalkamat Nov 14, 2020
7e49390
Should also check if there are no external projects before skipping s…
sheetalkamat Nov 14, 2020
16ff1ce
Revert "Map the paths back to https:"
sheetalkamat Nov 18, 2020
9952bab
Revert "TODO"
sheetalkamat Nov 18, 2020
2bf35c9
Revert "Should also check if there are no external projects before sk…
sheetalkamat Nov 18, 2020
3c94c98
Merge branch 'master' into web-worker-server
sheetalkamat Nov 21, 2020
542a8b0
Refactoring so we can test the changes out
sheetalkamat Nov 21, 2020
245e49d
Feedback
sheetalkamat Dec 2, 2020
9704738
Merge branch 'master' into web-worker-server
sheetalkamat Dec 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ const watchLssl = () => watch([
"src/services/**/*.ts",
"src/server/tsconfig.json",
"src/server/**/*.ts",
"src/webServer/tsconfig.json",
"src/webServer/**/*.ts",
"src/tsserver/tsconfig.json",
"src/tsserver/**/*.ts",
], buildLssl);
Expand Down
20 changes: 14 additions & 6 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ namespace ts.server {
typesMapLocation?: string;
}

export class Session implements EventSender {
export class Session<TMessage = string> implements EventSender {
private readonly gcTimer: GcTimer;
protected projectService: ProjectService;
private changeSeq = 0;
Expand Down Expand Up @@ -2907,7 +2907,7 @@ namespace ts.server {
}
}

public onMessage(message: string) {
public onMessage(message: TMessage) {
this.gcTimer.scheduleCollect();

this.performanceData = undefined;
Expand All @@ -2916,18 +2916,18 @@ namespace ts.server {
if (this.logger.hasLevel(LogLevel.requestTime)) {
start = this.hrtime();
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`request:${indent(message)}`);
this.logger.info(`request:${indent(this.toStringMessage(message))}`);
}
}

let request: protocol.Request | undefined;
let relevantFile: protocol.FileRequestArgs | undefined;
try {
request = <protocol.Request>JSON.parse(message);
request = this.parseMessage(message);
relevantFile = request.arguments && (request as protocol.FileRequest).arguments.file ? (request as protocol.FileRequest).arguments : undefined;

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

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

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

Expand All @@ -2978,6 +2978,14 @@ namespace ts.server {
}
}

protected parseMessage(message: TMessage): protocol.Request {
return <protocol.Request>JSON.parse(message as any as string);
}

protected toStringMessage(message: TMessage): string {
return message as any as string;
}

private getFormatOptions(file: NormalizedPath): FormatCodeSettings {
return this.projectService.getFormatCodeOptions(file);
}
Expand Down
2 changes: 2 additions & 0 deletions src/testRunner/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
{ "path": "../services", "prepend": true },
{ "path": "../jsTyping", "prepend": true },
{ "path": "../server", "prepend": true },
{ "path": "../webServer", "prepend": true },
{ "path": "../typingsInstallerCore", "prepend": true },
{ "path": "../harness", "prepend": true }
],
Expand Down Expand Up @@ -204,6 +205,7 @@
"unittests/tsserver/typingsInstaller.ts",
"unittests/tsserver/versionCache.ts",
"unittests/tsserver/watchEnvironment.ts",
"unittests/tsserver/webServer.ts",
"unittests/debugDeprecation.ts"
]
}
157 changes: 157 additions & 0 deletions src/testRunner/unittests/tsserver/webServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: webServer", () => {
class TestWorkerSession extends server.WorkerSession {
constructor(host: server.ServerHost, webHost: server.HostWithWriteMessage, options: Partial<server.StartSessionOptions>, logger: server.Logger) {
super(
host,
webHost,
{
globalPlugins: undefined,
pluginProbeLocations: undefined,
allowLocalPluginLoads: undefined,
useSingleInferredProject: true,
useInferredProjectPerProjectRoot: false,
suppressDiagnosticEvents: false,
noGetErrOnBackgroundUpdate: true,
syntaxOnly: undefined,
serverMode: undefined,
...options
},
logger,
server.nullCancellationToken,
() => emptyArray
);
}

getProjectService() {
return this.projectService;
}
}
function setup(logLevel: server.LogLevel | undefined) {
const host = createServerHost([libFile], { windowsStyleRoot: "c:/" });
const messages: any[] = [];
const webHost: server.WebHost = {
readFile: s => host.readFile(s),
fileExists: s => host.fileExists(s),
writeMessage: s => messages.push(s),
};
const webSys = server.createWebSystem(webHost, emptyArray, () => host.getExecutingFilePath());
const logger = logLevel !== undefined ? new server.MainProcessLogger(logLevel, webHost) : nullLogger;
const session = new TestWorkerSession(webSys, webHost, { serverMode: LanguageServiceMode.PartialSemantic }, logger);
return { getMessages: () => messages, clearMessages: () => messages.length = 0, session };

}

describe("open files are added to inferred project and semantic operations succeed", () => {
function verify(logLevel: server.LogLevel | undefined) {
const { session, clearMessages, getMessages } = setup(logLevel);
const service = session.getProjectService();
const file: File = {
path: "^memfs:/sample-folder/large.ts",
content: "export const numberConst = 10; export const arrayConst: Array<string> = [];"
};
session.executeCommand({
seq: 1,
type: "request",
command: protocol.CommandTypes.Open,
arguments: {
file: file.path,
fileContent: file.content
}
});
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, ["/lib.d.ts", file.path]); // Lib files are rooted
verifyQuickInfo();
verifyGotoDefInLib();

function verifyQuickInfo() {
clearMessages();
const start = protocolFileLocationFromSubstring(file, "numberConst");
session.onMessage({
seq: 2,
type: "request",
command: protocol.CommandTypes.Quickinfo,
arguments: start
});
assert.deepEqual(last(getMessages()), {
seq: 0,
type: "response",
command: protocol.CommandTypes.Quickinfo,
request_seq: 2,
success: true,
performanceData: undefined,
body: {
kind: ScriptElementKind.constElement,
kindModifiers: "export",
start: { line: start.line, offset: start.offset },
end: { line: start.line, offset: start.offset + "numberConst".length },
displayString: "const numberConst: 10",
documentation: "",
tags: []
}
});
verifyLogger();
}

function verifyGotoDefInLib() {
clearMessages();
const start = protocolFileLocationFromSubstring(file, "Array");
session.onMessage({
seq: 3,
type: "request",
command: protocol.CommandTypes.DefinitionAndBoundSpan,
arguments: start
});
assert.deepEqual(last(getMessages()), {
seq: 0,
type: "response",
command: protocol.CommandTypes.DefinitionAndBoundSpan,
request_seq: 3,
success: true,
performanceData: undefined,
body: {
definitions: [{
file: "/lib.d.ts",
...protocolTextSpanWithContextFromSubstring({
fileText: libFile.content,
text: "Array",
contextText: "interface Array<T> { length: number; [n: number]: T; }"
})
}],
textSpan: {
start: { line: start.line, offset: start.offset },
end: { line: start.line, offset: start.offset + "Array".length },
}
}
});
verifyLogger();
}

function verifyLogger() {
const messages = getMessages();
assert.equal(messages.length, logLevel === server.LogLevel.verbose ? 4 : 1, `Expected ${JSON.stringify(messages)}`);
if (logLevel === server.LogLevel.verbose) {
verifyLogMessages(messages[0], "info");
verifyLogMessages(messages[1], "perf");
verifyLogMessages(messages[2], "info");
}
clearMessages();
}

function verifyLogMessages(actual: any, expectedLevel: server.MessageLogLevel) {
assert.equal(actual.type, "log");
assert.equal(actual.level, expectedLevel);
}
}

it("with logging enabled", () => {
verify(server.LogLevel.verbose);
});

it("with logging disabled", () => {
verify(/*logLevel*/ undefined);
});
});
});
}
Loading