From 1271d877ffa400686577b1d22d1abc4ff60531c1 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 3 Nov 2016 14:13:39 -0700 Subject: [PATCH 1/2] use local registry to check if typings package exist (#12014) use local registry to check if typings package exist --- .../unittests/tsserverProjectSystem.ts | 32 ++- src/harness/unittests/typingsInstaller.ts | 209 ++++++++---------- .../typingsInstaller/nodeTypingsInstaller.ts | 123 ++++++----- .../typingsInstaller/typingsInstaller.ts | 115 ++++------ 4 files changed, 206 insertions(+), 273 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 15cfa3f8c8e7c..83ffb81706f6f 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -18,7 +18,6 @@ namespace ts.projectSystem { }; export interface PostExecAction { - readonly requestKind: TI.RequestKind; readonly success: boolean; readonly callback: TI.RequestCompletedAction; } @@ -47,9 +46,13 @@ namespace ts.projectSystem { export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller { protected projectService: server.ProjectService; - constructor(readonly globalTypingsCacheLocation: string, throttleLimit: number, readonly installTypingHost: server.ServerHost, log?: TI.Log) { - super(globalTypingsCacheLocation, safeList.path, throttleLimit, log); - this.init(); + constructor( + readonly globalTypingsCacheLocation: string, + throttleLimit: number, + installTypingHost: server.ServerHost, + readonly typesRegistry = createMap(), + log?: TI.Log) { + super(installTypingHost, globalTypingsCacheLocation, safeList.path, throttleLimit, log); } safeFileList = safeList.path; @@ -63,9 +66,8 @@ namespace ts.projectSystem { } } - checkPendingCommands(expected: TI.RequestKind[]) { - assert.equal(this.postExecActions.length, expected.length, `Expected ${expected.length} post install actions`); - this.postExecActions.forEach((act, i) => assert.equal(act.requestKind, expected[i], "Unexpected post install action")); + checkPendingCommands(expectedCount: number) { + assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); } onProjectClosed() { @@ -79,15 +81,8 @@ namespace ts.projectSystem { return this.installTypingHost; } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { - switch (requestKind) { - case TI.NpmViewRequest: - case TI.NpmInstallRequest: - break; - default: - assert.isTrue(false, `request ${requestKind} is not supported`); - } - this.addPostExecAction(requestKind, "success", cb); + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + this.addPostExecAction("success", cb); } sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { @@ -99,12 +94,11 @@ namespace ts.projectSystem { this.install(request); } - addPostExecAction(requestKind: TI.RequestKind, stdout: string | string[], cb: TI.RequestCompletedAction) { + addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { const out = typeof stdout === "string" ? stdout : createNpmPackageJsonString(stdout); const action: PostExecAction = { success: !!out, - callback: cb, - requestKind + callback: cb }; this.postExecActions.push(action); } diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 356992d57176e..637d7516d852b 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -8,6 +8,15 @@ namespace ts.projectSystem { interface InstallerParams { globalTypingsCacheLocation?: string; throttleLimit?: number; + typesRegistry?: Map; + } + + function createTypesRegistry(...list: string[]): Map { + const map = createMap(); + for (const l of list) { + map[l] = undefined; + } + return map; } class Installer extends TestTypingsInstaller { @@ -16,35 +25,24 @@ namespace ts.projectSystem { (p && p.globalTypingsCacheLocation) || "/a/data", (p && p.throttleLimit) || 5, host, + (p && p.typesRegistry), log); } - installAll(expectedView: typeof TI.NpmViewRequest[], expectedInstall: typeof TI.NpmInstallRequest[]) { - this.checkPendingCommands(expectedView); - this.executePendingCommands(); - this.checkPendingCommands(expectedInstall); + installAll(expectedCount: number) { + this.checkPendingCommands(expectedCount); this.executePendingCommands(); } } describe("typingsInstaller", () => { - function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[], typingFiles: FileOrFolder[], requestKind: TI.RequestKind, cb: TI.RequestCompletedAction): void { - switch (requestKind) { - case TI.NpmInstallRequest: - self.addPostExecAction(requestKind, installedTypings, success => { - for (const file of typingFiles) { - host.createFileOrFolder(file, /*createParentDirectory*/ true); - } - cb(success); - }); - break; - case TI.NpmViewRequest: - self.addPostExecAction(requestKind, installedTypings, cb); - break; - default: - assert.isTrue(false, `unexpected request kind ${requestKind}`); - break; - } + function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[], typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void { + self.addPostExecAction(installedTypings, success => { + for (const file of typingFiles) { + host.createFileOrFolder(file, /*createParentDirectory*/ true); + } + cb(success); + }); } it("configured projects (typings installed) 1", () => { const file1 = { @@ -79,12 +77,12 @@ namespace ts.projectSystem { const host = createServerHost([file1, tsconfig, packageJson]); const installer = new (class extends Installer { constructor() { - super(host); + super(host, { typesRegistry: createTypesRegistry("jquery") }); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/jquery"]; const typingFiles = [jquery]; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); @@ -95,7 +93,7 @@ namespace ts.projectSystem { const p = projectService.configuredProjects[0]; checkProjectActualFiles(p, [file1.path]); - installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]); + installer.installAll(/*expectedCount*/ 1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectActualFiles(p, [file1.path, jquery.path]); @@ -123,12 +121,12 @@ namespace ts.projectSystem { const host = createServerHost([file1, packageJson]); const installer = new (class extends Installer { constructor() { - super(host); + super(host, { typesRegistry: createTypesRegistry("jquery") }); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/jquery"]; const typingFiles = [jquery]; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); @@ -139,7 +137,7 @@ namespace ts.projectSystem { const p = projectService.inferredProjects[0]; checkProjectActualFiles(p, [file1.path]); - installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]); + installer.installAll(/*expectedCount*/ 1); checkNumberOfProjects(projectService, { inferredProjects: 1 }); checkProjectActualFiles(p, [file1.path, jquery.path]); @@ -167,7 +165,7 @@ namespace ts.projectSystem { options: {}, rootFiles: [toExternalFile(file1.path)] }); - installer.checkPendingCommands([]); + installer.checkPendingCommands(/*expectedCount*/ 0); // by default auto discovery will kick in if project contain only .js/.d.ts files // in this case project contain only ts files - no auto discovery projectService.checkNumberOfProjects({ externalProjects: 1 }); @@ -181,7 +179,7 @@ namespace ts.projectSystem { const host = createServerHost([file1]); const installer = new (class extends Installer { constructor() { - super(host); + super(host, { typesRegistry: createTypesRegistry("jquery") }); } enqueueInstallTypingsRequest() { assert(false, "auto discovery should not be enabled"); @@ -196,7 +194,7 @@ namespace ts.projectSystem { rootFiles: [toExternalFile(file1.path)], typingOptions: { include: ["jquery"] } }); - installer.checkPendingCommands([]); + installer.checkPendingCommands(/*expectedCount*/ 0); // by default auto discovery will kick in if project contain only .js/.d.ts files // in this case project contain only ts files - no auto discovery even if typing options is set projectService.checkNumberOfProjects({ externalProjects: 1 }); @@ -215,16 +213,16 @@ namespace ts.projectSystem { let enqueueIsCalled = false; const installer = new (class extends Installer { constructor() { - super(host); + super(host, { typesRegistry: createTypesRegistry("jquery") }); } enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions, unresolvedImports: server.SortedReadonlyArray) { enqueueIsCalled = true; super.enqueueInstallTypingsRequest(project, typingOptions, unresolvedImports); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { - const installedTypings = ["@types/jquery"]; + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + const installedTypings = ["@types/node"]; const typingFiles = [jquery]; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); @@ -234,11 +232,11 @@ namespace ts.projectSystem { projectFileName, options: {}, rootFiles: [toExternalFile(file1.path)], - typingOptions: { enableAutoDiscovery: true, include: ["node"] } + typingOptions: { enableAutoDiscovery: true, include: ["jquery"] } }); assert.isTrue(enqueueIsCalled, "expected enqueueIsCalled to be true"); - installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]); + installer.installAll(/*expectedCount*/ 1); // autoDiscovery is set in typing options - use it even if project contains only .ts files projectService.checkNumberOfProjects({ externalProjects: 1 }); @@ -273,12 +271,12 @@ namespace ts.projectSystem { const host = createServerHost([file1, file2, file3]); const installer = new (class extends Installer { constructor() { - super(host); + super(host, { typesRegistry: createTypesRegistry("lodash", "react") }); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { const installedTypings = ["@types/lodash", "@types/react"]; const typingFiles = [lodash, react]; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); @@ -295,7 +293,7 @@ namespace ts.projectSystem { projectService.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path, file3.path]); - installer.installAll([TI.NpmViewRequest, TI.NpmViewRequest], [TI.NpmInstallRequest], ); + installer.installAll(/*expectedCount*/ 1); checkNumberOfProjects(projectService, { externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path, file3.path, lodash.path, react.path]); @@ -317,16 +315,16 @@ namespace ts.projectSystem { let enqueueIsCalled = false; const installer = new (class extends Installer { constructor() { - super(host); + super(host, { typesRegistry: createTypesRegistry("jquery") }); } enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions, unresolvedImports: server.SortedReadonlyArray) { enqueueIsCalled = true; super.enqueueInstallTypingsRequest(project, typingOptions, unresolvedImports); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { const installedTypings: string[] = []; const typingFiles: FileOrFolder[] = []; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); @@ -343,7 +341,7 @@ namespace ts.projectSystem { projectService.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path]); - installer.checkPendingCommands([]); + installer.checkPendingCommands(/*expectedCount*/ 0); checkNumberOfProjects(projectService, { externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path]); @@ -396,12 +394,12 @@ namespace ts.projectSystem { const host = createServerHost([file1, file2, file3, packageJson]); const installer = new (class extends Installer { constructor() { - super(host); + super(host, { typesRegistry: createTypesRegistry("jquery", "commander", "moment", "express") }); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { const installedTypings = ["@types/commander", "@types/express", "@types/jquery", "@types/moment"]; const typingFiles = [commander, express, jquery, moment]; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); @@ -418,10 +416,7 @@ namespace ts.projectSystem { projectService.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path, file3.path]); - installer.installAll( - [TI.NpmViewRequest, TI.NpmViewRequest, TI.NpmViewRequest, TI.NpmViewRequest], - [TI.NpmInstallRequest] - ); + installer.installAll(/*expectedCount*/ 1); checkNumberOfProjects(projectService, { externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path, file3.path, commander.path, express.path, jquery.path, moment.path]); @@ -475,11 +470,11 @@ namespace ts.projectSystem { const host = createServerHost([lodashJs, commanderJs, file3, packageJson]); const installer = new (class extends Installer { constructor() { - super(host, { throttleLimit: 3 }); + super(host, { throttleLimit: 3, typesRegistry: createTypesRegistry("commander", "express", "jquery", "moment", "lodash") }); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { const installedTypings = ["@types/commander", "@types/express", "@types/jquery", "@types/moment", "@types/lodash"]; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); @@ -495,18 +490,7 @@ namespace ts.projectSystem { const p = projectService.externalProjects[0]; projectService.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(p, [lodashJs.path, commanderJs.path, file3.path]); - // expected 3 view requests in the queue - installer.checkPendingCommands([TI.NpmViewRequest, TI.NpmViewRequest, TI.NpmViewRequest]); - assert.equal(installer.pendingRunRequests.length, 2, "expected 2 pending requests"); - - // push view requests - installer.executePendingCommands(); - // expected 2 remaining view requests in the queue - installer.checkPendingCommands([TI.NpmViewRequest, TI.NpmViewRequest]); - // push view requests - installer.executePendingCommands(); - // expected one install request - installer.checkPendingCommands([TI.NpmInstallRequest]); + installer.checkPendingCommands(/*expectedCount*/ 1); installer.executePendingCommands(); // expected all typings file to exist for (const f of typingFiles) { @@ -565,22 +549,17 @@ namespace ts.projectSystem { const host = createServerHost([lodashJs, commanderJs, file3]); const installer = new (class extends Installer { constructor() { - super(host, { throttleLimit: 3 }); - } - executeRequest(requestKind: TI.RequestKind, _requestId: number, args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { - if (requestKind === TI.NpmInstallRequest) { - let typingFiles: (FileOrFolder & { typings: string })[] = []; - if (args.indexOf("@types/commander") >= 0) { - typingFiles = [commander, jquery, lodash, cordova]; - } - else { - typingFiles = [grunt, gulp]; - } - executeCommand(this, host, typingFiles.map(f => f.typings), typingFiles, requestKind, cb); + super(host, { throttleLimit: 1, typesRegistry: createTypesRegistry("commander", "jquery", "lodash", "cordova", "gulp", "grunt") }); + } + installWorker(_requestId: number, args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + let typingFiles: (FileOrFolder & { typings: string })[] = []; + if (args.indexOf("@types/commander") >= 0) { + typingFiles = [commander, jquery, lodash, cordova]; } else { - executeCommand(this, host, [], [], requestKind, cb); + typingFiles = [grunt, gulp]; } + executeCommand(this, host, typingFiles.map(f => f.typings), typingFiles, cb); } })(); @@ -594,8 +573,8 @@ namespace ts.projectSystem { typingOptions: { include: ["jquery", "cordova"] } }); - installer.checkPendingCommands([TI.NpmViewRequest, TI.NpmViewRequest, TI.NpmViewRequest]); - assert.equal(installer.pendingRunRequests.length, 1, "expect one throttled request"); + installer.checkPendingCommands(/*expectedCount*/ 1); + assert.equal(installer.pendingRunRequests.length, 0, "expect no throttled requests"); // Create project #2 with 2 typings const projectFileName2 = "/a/app/test2.csproj"; @@ -605,7 +584,7 @@ namespace ts.projectSystem { rootFiles: [toExternalFile(file3.path)], typingOptions: { include: ["grunt", "gulp"] } }); - assert.equal(installer.pendingRunRequests.length, 3, "expect three throttled request"); + assert.equal(installer.pendingRunRequests.length, 1, "expect one throttled request"); const p1 = projectService.externalProjects[0]; const p2 = projectService.externalProjects[1]; @@ -613,16 +592,12 @@ namespace ts.projectSystem { checkProjectActualFiles(p1, [lodashJs.path, commanderJs.path, file3.path]); checkProjectActualFiles(p2, [file3.path]); - installer.executePendingCommands(); - // expected one view request from the first project and two - from the second one - installer.checkPendingCommands([TI.NpmViewRequest, TI.NpmViewRequest, TI.NpmViewRequest]); - assert.equal(installer.pendingRunRequests.length, 0, "expected no throttled requests"); - installer.executePendingCommands(); + // expected one install request from the second project + installer.checkPendingCommands(/*expectedCount*/ 1); + assert.equal(installer.pendingRunRequests.length, 0, "expected no throttled requests"); - // should be two install requests from both projects - installer.checkPendingCommands([TI.NpmInstallRequest, TI.NpmInstallRequest]); installer.executePendingCommands(); checkProjectActualFiles(p1, [lodashJs.path, commanderJs.path, file3.path, commander.path, jquery.path, lodash.path, cordova.path]); @@ -653,12 +628,12 @@ namespace ts.projectSystem { const host = createServerHost([app, jsconfig, jquery, jqueryPackage]); const installer = new (class extends Installer { constructor() { - super(host, { globalTypingsCacheLocation: "/tmp" }); + super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery") }); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/jquery"]; const typingFiles = [jqueryDTS]; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); @@ -669,7 +644,7 @@ namespace ts.projectSystem { const p = projectService.configuredProjects[0]; checkProjectActualFiles(p, [app.path]); - installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]); + installer.installAll(/*expectedCount*/ 1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectActualFiles(p, [app.path, jqueryDTS.path]); @@ -699,12 +674,12 @@ namespace ts.projectSystem { const host = createServerHost([app, jsconfig, bowerJson]); const installer = new (class extends Installer { constructor() { - super(host, { globalTypingsCacheLocation: "/tmp" }); + super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery") }); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/jquery"]; const typingFiles = [jqueryDTS]; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); @@ -715,7 +690,7 @@ namespace ts.projectSystem { const p = projectService.configuredProjects[0]; checkProjectActualFiles(p, [app.path]); - installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]); + installer.installAll(/*expectedCount*/ 1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectActualFiles(p, [app.path, jqueryDTS.path]); @@ -742,23 +717,23 @@ namespace ts.projectSystem { const host = createServerHost([f, brokenPackageJson]); const installer = new (class extends Installer { constructor() { - super(host, { globalTypingsCacheLocation: cachePath }); + super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/commander"]; const typingFiles = [commander]; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); const service = createProjectService(host, { typingsInstaller: installer }); service.openClientFile(f.path); - installer.checkPendingCommands([]); + installer.checkPendingCommands(/*expectedCount*/ 0); host.reloadFS([f, fixedPackageJson]); host.triggerFileWatcherCallback(fixedPackageJson.path, /*removed*/ false); - // expected one view and one install request - installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]); + // expected install request + installer.installAll(/*expectedCount*/ 1); service.checkNumberOfProjects({ inferredProjects: 1 }); checkProjectActualFiles(service.inferredProjects[0], [f.path, commander.path]); @@ -783,12 +758,12 @@ namespace ts.projectSystem { const host = createServerHost([file]); const installer = new (class extends Installer { constructor() { - super(host, { globalTypingsCacheLocation: cachePath }); + super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("node", "commander") }); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/node", "@types/commander"]; const typingFiles = [node, commander]; - executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); + executeCommand(this, host, installedTypings, typingFiles, cb); } })(); const service = createProjectService(host, { typingsInstaller: installer }); @@ -797,7 +772,7 @@ namespace ts.projectSystem { service.checkNumberOfProjects({ inferredProjects: 1 }); checkProjectActualFiles(service.inferredProjects[0], [file.path]); - installer.installAll([TI.NpmViewRequest, TI.NpmViewRequest], [TI.NpmInstallRequest]); + installer.installAll(/*expectedCount*/1); assert.isTrue(host.fileExists(node.path), "typings for 'node' should be created"); assert.isTrue(host.fileExists(commander.path), "typings for 'commander' should be created"); @@ -822,14 +797,10 @@ namespace ts.projectSystem { const host = createServerHost([f1]); const installer = new (class extends Installer { constructor() { - super(host, { globalTypingsCacheLocation: "/tmp" }); + super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("foo") }); } - executeRequest(requestKind: TI.RequestKind, _requestId: number, args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { - if (requestKind === TI.NpmViewRequest) { - // args should have only non-scoped packages - scoped packages are not yet supported - assert.deepEqual(args, ["foo"]); - } - executeCommand(this, host, ["foo"], [], requestKind, cb); + installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + executeCommand(this, host, ["foo"], [], cb); } })(); const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -844,7 +815,7 @@ namespace ts.projectSystem { ["foo", "foo", "foo", "@bar/router", "@bar/common", "@bar/common"] ); - installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]); + installer.installAll(/*expectedCount*/ 1); }); it("cached unresolved typings are not recomputed if program structure did not change", () => { @@ -936,14 +907,14 @@ namespace ts.projectSystem { constructor() { super(host, { globalTypingsCacheLocation: "/tmp" }, { isEnabled: () => true, writeLine: msg => messages.push(msg) }); } - executeRequest() { + installWorker(_requestId: number, _args: string[], _cwd: string, _cb: server.typingsInstaller.RequestCompletedAction) { assert(false, "runCommand should not be invoked"); } })(); const projectService = createProjectService(host, { typingsInstaller: installer }); projectService.openClientFile(f1.path); - installer.checkPendingCommands([]); + installer.checkPendingCommands(/*expectedCount*/ 0); assert.isTrue(messages.indexOf("Package name '; say ‘Hello from TypeScript!’ #' contains non URI safe characters") > 0, "should find package with invalid name"); }); }); diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index 8941926493051..1eba0c5d1598f 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -33,28 +33,51 @@ namespace ts.server.typingsInstaller { } } - type HttpGet = { - (url: string, callback: (response: HttpResponse) => void): NodeJS.EventEmitter; - }; + interface TypesRegistryFile { + entries: MapLike; + } - interface HttpResponse extends NodeJS.ReadableStream { - statusCode: number; - statusMessage: string; - destroy(): void; + function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map { + if (!host.fileExists(typesRegistryFilePath)) { + if (log.isEnabled()) { + log.writeLine(`Types registry file '${typesRegistryFilePath}' does not exist`); + } + return createMap(); + } + try { + const content = JSON.parse(host.readFile(typesRegistryFilePath)); + return createMap(content.entries); + } + catch (e) { + if (log.isEnabled()) { + log.writeLine(`Error when loading types registry file '${typesRegistryFilePath}': ${(e).message}, ${(e).stack}`); + } + return createMap(); + } + } + + const TypesRegistryPackageName = "types-registry"; + function getTypesRegistryFileLocation(globalTypingsCacheLocation: string): string { + return combinePaths(normalizeSlashes(globalTypingsCacheLocation), `node_modules/${TypesRegistryPackageName}/index.json`); } + type Exec = { (command: string, options: { cwd: string }, callback?: (error: Error, stdout: string, stderr: string) => void): any }; + type ExecSync = { + (command: string, options: { cwd: string, stdio: "ignore" }): any + }; + export class NodeTypingsInstaller extends TypingsInstaller { private readonly exec: Exec; - private readonly httpGet: HttpGet; private readonly npmPath: string; - readonly installTypingHost: InstallTypingHost = sys; + readonly typesRegistry: Map; constructor(globalTypingsCacheLocation: string, throttleLimit: number, log: Log) { super( + sys, globalTypingsCacheLocation, toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), throttleLimit, @@ -63,12 +86,27 @@ namespace ts.server.typingsInstaller { this.log.writeLine(`Process id: ${process.pid}`); } this.npmPath = getNPMLocation(process.argv[0]); - this.exec = require("child_process").exec; - this.httpGet = require("http").get; + let execSync: ExecSync; + ({ exec: this.exec, execSync } = require("child_process")); + + this.ensurePackageDirectoryExists(globalTypingsCacheLocation); + + try { + if (this.log.isEnabled()) { + this.log.writeLine(`Updating ${TypesRegistryPackageName} npm package...`); + } + execSync(`${this.npmPath} install ${TypesRegistryPackageName}`, { cwd: globalTypingsCacheLocation, stdio: "ignore" }); + } + catch (e) { + if (this.log.isEnabled()) { + this.log.writeLine(`Error updating ${TypesRegistryPackageName} package: ${(e).message}`); + } + } + + this.typesRegistry = loadTypesRegistryFile(getTypesRegistryFileLocation(globalTypingsCacheLocation), this.installTypingHost, this.log); } - init() { - super.init(); + listen() { process.on("message", (req: DiscoverTypings | CloseProject) => { switch (req.kind) { case "discover": @@ -90,54 +128,19 @@ namespace ts.server.typingsInstaller { } } - protected executeRequest(requestKind: RequestKind, requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { + protected installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { if (this.log.isEnabled()) { - this.log.writeLine(`#${requestId} executing ${requestKind}, arguments'${JSON.stringify(args)}'.`); - } - switch (requestKind) { - case NpmViewRequest: { - // const command = `${self.npmPath} view @types/${typing} --silent name`; - // use http request to global npm registry instead of running npm view - Debug.assert(args.length === 1); - const url = `http://registry.npmjs.org/@types%2f${args[0]}`; - const start = Date.now(); - this.httpGet(url, response => { - let ok = false; - if (this.log.isEnabled()) { - this.log.writeLine(`${requestKind} #${requestId} request to ${url}:: status code ${response.statusCode}, status message '${response.statusMessage}', took ${Date.now() - start} ms`); - } - switch (response.statusCode) { - case 200: // OK - case 301: // redirect - Moved - treat package as present - case 302: // redirect - Found - treat package as present - ok = true; - break; - } - response.destroy(); - onRequestCompleted(ok); - }).on("error", (err: Error) => { - if (this.log.isEnabled()) { - this.log.writeLine(`${requestKind} #${requestId} query to npm registry failed with error ${err.message}, stack ${err.stack}`); - } - onRequestCompleted(/*success*/ false); - }); - } - break; - case NpmInstallRequest: { - const command = `${this.npmPath} install ${args.join(" ")} --save-dev`; - const start = Date.now(); - this.exec(command, { cwd }, (_err, stdout, stderr) => { - if (this.log.isEnabled()) { - this.log.writeLine(`${requestKind} #${requestId} took: ${Date.now() - start} ms${sys.newLine}stdout: ${stdout}${sys.newLine}stderr: ${stderr}`); - } - // treat any output on stdout as success - onRequestCompleted(!!stdout); - }); - } - break; - default: - Debug.assert(false, `Unknown request kind ${requestKind}`); + this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(args)}'.`); } + const command = `${this.npmPath} install ${args.join(" ")} --save-dev`; + const start = Date.now(); + this.exec(command, { cwd }, (_err, stdout, stderr) => { + if (this.log.isEnabled()) { + this.log.writeLine(`npm install #${requestId} took: ${Date.now() - start} ms${sys.newLine}stdout: ${stdout}${sys.newLine}stderr: ${stderr}`); + } + // treat any output on stdout as success + onRequestCompleted(!!stdout); + }); } } @@ -163,5 +166,5 @@ namespace ts.server.typingsInstaller { process.exit(0); }); const installer = new NodeTypingsInstaller(globalTypingsCacheLocation, /*throttleLimit*/5, log); - installer.init(); + installer.listen(); } \ No newline at end of file diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index df043fc26ae06..82a279f3de3c2 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -63,14 +63,8 @@ namespace ts.server.typingsInstaller { return PackageNameValidationResult.Ok; } - export const NpmViewRequest: "npm view" = "npm view"; - export const NpmInstallRequest: "npm install" = "npm install"; - - export type RequestKind = typeof NpmViewRequest | typeof NpmInstallRequest; - export type RequestCompletedAction = (success: boolean) => void; type PendingRequest = { - requestKind: RequestKind; requestId: number; args: string[]; cwd: string; @@ -87,9 +81,10 @@ namespace ts.server.typingsInstaller { private installRunCount = 1; private inFlightRequestCount = 0; - abstract readonly installTypingHost: InstallTypingHost; + abstract readonly typesRegistry: Map; constructor( + readonly installTypingHost: InstallTypingHost, readonly globalCachePath: string, readonly safeListPath: Path, readonly throttleLimit: number, @@ -97,9 +92,6 @@ namespace ts.server.typingsInstaller { if (this.log.isEnabled()) { this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}'`); } - } - - init() { this.processCacheLocation(this.globalCachePath); } @@ -224,7 +216,7 @@ namespace ts.server.typingsInstaller { this.knownCachesSet[cacheLocation] = true; } - private filterTypings(typingsToInstall: string[]) { + private filterAndMapToScopedName(typingsToInstall: string[]) { if (typingsToInstall.length === 0) { return typingsToInstall; } @@ -235,7 +227,14 @@ namespace ts.server.typingsInstaller { } const validationResult = validatePackageName(typing); if (validationResult === PackageNameValidationResult.Ok) { - result.push(typing); + if (typing in this.typesRegistry) { + result.push(`@types/${typing}`); + } + else { + if (this.log.isEnabled()) { + this.log.writeLine(`Entry for package '${typing}' does not exist in local types registry - skipping...`); + } + } } else { // add typing name to missing set so we won't process it again @@ -267,43 +266,51 @@ namespace ts.server.typingsInstaller { return result; } - private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) { + protected ensurePackageDirectoryExists(directory: string) { + const npmConfigPath = combinePaths(directory, "package.json"); if (this.log.isEnabled()) { - this.log.writeLine(`Installing typings ${JSON.stringify(typingsToInstall)}`); + this.log.writeLine(`Npm config file: ${npmConfigPath}`); } - typingsToInstall = this.filterTypings(typingsToInstall); - if (typingsToInstall.length === 0) { + if (!this.installTypingHost.fileExists(npmConfigPath)) { if (this.log.isEnabled()) { - this.log.writeLine(`All typings are known to be missing or invalid - no need to go any further`); + this.log.writeLine(`Npm config file: '${npmConfigPath}' is missing, creating new one...`); } - return; + this.ensureDirectoryExists(directory, this.installTypingHost); + this.installTypingHost.writeFile(npmConfigPath, "{}"); } + } - const npmConfigPath = combinePaths(cachePath, "package.json"); + private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) { if (this.log.isEnabled()) { - this.log.writeLine(`Npm config file: ${npmConfigPath}`); + this.log.writeLine(`Installing typings ${JSON.stringify(typingsToInstall)}`); } - if (!this.installTypingHost.fileExists(npmConfigPath)) { + const scopedTypings = this.filterAndMapToScopedName(typingsToInstall); + if (scopedTypings.length === 0) { if (this.log.isEnabled()) { - this.log.writeLine(`Npm config file: '${npmConfigPath}' is missing, creating new one...`); + this.log.writeLine(`All typings are known to be missing or invalid - no need to go any further`); } - this.ensureDirectoryExists(cachePath, this.installTypingHost); - this.installTypingHost.writeFile(npmConfigPath, "{}"); + return; } - this.runInstall(cachePath, typingsToInstall, installedTypings => { + this.ensurePackageDirectoryExists(cachePath); + + const requestId = this.installRunCount; + this.installRunCount++; + + this.installTypingsAsync(requestId, scopedTypings, cachePath, ok => { + if (!ok) { + return; + } // TODO: watch project directory if (this.log.isEnabled()) { - this.log.writeLine(`Requested to install typings ${JSON.stringify(typingsToInstall)}, installed typings ${JSON.stringify(installedTypings)}`); + this.log.writeLine(`Requested to install typings ${JSON.stringify(scopedTypings)}, installed typings ${JSON.stringify(scopedTypings)}`); } - const installedPackages: Map = createMap(); const installedTypingFiles: string[] = []; - for (const t of installedTypings) { + for (const t of scopedTypings) { const packageName = getBaseFileName(t); if (!packageName) { continue; } - installedPackages[packageName] = true; const typingFile = typingToFileName(cachePath, packageName, this.installTypingHost); if (!typingFile) { continue; @@ -316,53 +323,11 @@ namespace ts.server.typingsInstaller { if (this.log.isEnabled()) { this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`); } - for (const toInstall of typingsToInstall) { - if (!installedPackages[toInstall]) { - if (this.log.isEnabled()) { - this.log.writeLine(`New missing typing package '${toInstall}'`); - } - this.missingTypingsSet[toInstall] = true; - } - } this.sendResponse(this.createSetTypings(req, currentlyCachedTypings.concat(installedTypingFiles))); }); } - private runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void { - const requestId = this.installRunCount; - - this.installRunCount++; - let execInstallCmdCount = 0; - const filteredTypings: string[] = []; - for (const typing of typingsToInstall) { - filterExistingTypings(this, typing); - } - - function filterExistingTypings(self: TypingsInstaller, typing: string) { - self.execAsync(NpmViewRequest, requestId, [typing], cachePath, ok => { - if (ok) { - filteredTypings.push(typing); - } - execInstallCmdCount++; - if (execInstallCmdCount === typingsToInstall.length) { - installFilteredTypings(self, filteredTypings); - } - }); - } - - function installFilteredTypings(self: TypingsInstaller, filteredTypings: string[]) { - if (filteredTypings.length === 0) { - postInstallAction([]); - return; - } - const scopedTypings = filteredTypings.map(t => "@types/" + t); - self.execAsync(NpmInstallRequest, requestId, scopedTypings, cachePath, ok => { - postInstallAction(ok ? scopedTypings : []); - }); - } - } - private ensureDirectoryExists(directory: string, host: InstallTypingHost): void { const directoryName = getDirectoryPath(directory); if (!host.directoryExists(directoryName)) { @@ -409,8 +374,8 @@ namespace ts.server.typingsInstaller { }; } - private execAsync(requestKind: RequestKind, requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { - this.pendingRunRequests.unshift({ requestKind, requestId, args, cwd, onRequestCompleted }); + private installTypingsAsync(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { + this.pendingRunRequests.unshift({ requestId, args, cwd, onRequestCompleted }); this.executeWithThrottling(); } @@ -418,7 +383,7 @@ namespace ts.server.typingsInstaller { while (this.inFlightRequestCount < this.throttleLimit && this.pendingRunRequests.length) { this.inFlightRequestCount++; const request = this.pendingRunRequests.pop(); - this.executeRequest(request.requestKind, request.requestId, request.args, request.cwd, ok => { + this.installWorker(request.requestId, request.args, request.cwd, ok => { this.inFlightRequestCount--; request.onRequestCompleted(ok); this.executeWithThrottling(); @@ -426,7 +391,7 @@ namespace ts.server.typingsInstaller { } } - protected abstract executeRequest(requestKind: RequestKind, requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void; + protected abstract installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void; protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void; } } \ No newline at end of file From c8a15f52f1389581368880178b9ffc757ea9d125 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 4 Nov 2016 09:59:44 -0700 Subject: [PATCH 2/2] enable sending telemetry events to tsserver client (#12035) enable sending telemetry events --- Jakefile.js | 2 + .../unittests/tsserverProjectSystem.ts | 3 +- src/harness/unittests/typingsInstaller.ts | 68 +++++++++++++--- src/server/editorServices.ts | 4 +- src/server/protocol.ts | 26 +++++++ src/server/server.ts | 77 +++++++++++++------ src/server/session.ts | 6 +- src/server/shared.ts | 24 ++++++ src/server/tsconfig.json | 1 + src/server/tsconfig.library.json | 1 + src/server/types.d.ts | 24 ++++-- .../typingsInstaller/nodeTypingsInstaller.ts | 16 ++-- src/server/typingsInstaller/tsconfig.json | 1 + .../typingsInstaller/typingsInstaller.ts | 16 +++- src/server/utilities.ts | 2 + 15 files changed, 214 insertions(+), 57 deletions(-) create mode 100644 src/server/shared.ts diff --git a/Jakefile.js b/Jakefile.js index 8ce70eb353f9e..fc2b6359355c5 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -171,6 +171,7 @@ var servicesSources = [ var serverCoreSources = [ "types.d.ts", + "shared.ts", "utilities.ts", "scriptVersionCache.ts", "typingsCache.ts", @@ -193,6 +194,7 @@ var cancellationTokenSources = [ var typingsInstallerSources = [ "../types.d.ts", + "../shared.ts", "typingsInstaller.ts", "nodeTypingsInstaller.ts" ].map(function (f) { diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 83ffb81706f6f..ee6139b1215c6 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -51,8 +51,9 @@ namespace ts.projectSystem { throttleLimit: number, installTypingHost: server.ServerHost, readonly typesRegistry = createMap(), + telemetryEnabled?: boolean, log?: TI.Log) { - super(installTypingHost, globalTypingsCacheLocation, safeList.path, throttleLimit, log); + super(installTypingHost, globalTypingsCacheLocation, safeList.path, throttleLimit, telemetryEnabled, log); } safeFileList = safeList.path; diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 637d7516d852b..2dda506ad58f8 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -20,12 +20,13 @@ namespace ts.projectSystem { } class Installer extends TestTypingsInstaller { - constructor(host: server.ServerHost, p?: InstallerParams, log?: TI.Log) { + constructor(host: server.ServerHost, p?: InstallerParams, telemetryEnabled?: boolean, log?: TI.Log) { super( (p && p.globalTypingsCacheLocation) || "/a/data", (p && p.throttleLimit) || 5, host, (p && p.typesRegistry), + telemetryEnabled, log); } @@ -35,15 +36,16 @@ namespace ts.projectSystem { } } + function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[], typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void { + self.addPostExecAction(installedTypings, success => { + for (const file of typingFiles) { + host.createFileOrFolder(file, /*createParentDirectory*/ true); + } + cb(success); + }); + } + describe("typingsInstaller", () => { - function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[], typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void { - self.addPostExecAction(installedTypings, success => { - for (const file of typingFiles) { - host.createFileOrFolder(file, /*createParentDirectory*/ true); - } - cb(success); - }); - } it("configured projects (typings installed) 1", () => { const file1 = { path: "/a/b/app.js", @@ -905,7 +907,7 @@ namespace ts.projectSystem { const host = createServerHost([f1, packageJson]); const installer = new (class extends Installer { constructor() { - super(host, { globalTypingsCacheLocation: "/tmp" }, { isEnabled: () => true, writeLine: msg => messages.push(msg) }); + super(host, { globalTypingsCacheLocation: "/tmp" }, /*telemetryEnabled*/ false, { isEnabled: () => true, writeLine: msg => messages.push(msg) }); } installWorker(_requestId: number, _args: string[], _cwd: string, _cb: server.typingsInstaller.RequestCompletedAction) { assert(false, "runCommand should not be invoked"); @@ -949,4 +951,50 @@ namespace ts.projectSystem { assert.deepEqual(result.newTypingNames, ["bar"]); }); }); + + describe("telemetry events", () => { + it ("should be received", () => { + const f1 = { + path: "/a/app.js", + content: "" + }; + const package = { + path: "/a/package.json", + content: JSON.stringify({ dependencies: { "commander": "1.0.0" } }) + }; + const cachePath = "/a/cache/"; + const commander = { + path: cachePath + "node_modules/@types/commander/index.d.ts", + content: "export let x: number" + }; + const host = createServerHost([f1, package]); + let seenTelemetryEvent = false; + const installer = new (class extends Installer { + constructor() { + super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }, /*telemetryEnabled*/ true); + } + installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + const installedTypings = ["@types/commander"]; + const typingFiles = [commander]; + executeCommand(this, host, installedTypings, typingFiles, cb); + } + sendResponse(response: server.SetTypings | server.InvalidateCachedTypings | server.TypingsInstallEvent) { + if (response.kind === server.EventInstall) { + assert.deepEqual(response.packagesToInstall, ["@types/commander"]); + seenTelemetryEvent = true; + return; + } + super.sendResponse(response); + } + })(); + const projectService = createProjectService(host, { typingsInstaller: installer }); + projectService.openClientFile(f1.path); + + installer.installAll(/*expectedCount*/ 1); + + assert.isTrue(seenTelemetryEvent); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [f1.path, commander.path]); + }); + }); } \ No newline at end of file diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index a02becd87c486..bf760106b7435 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -286,10 +286,10 @@ namespace ts.server { return; } switch (response.kind) { - case "set": + case ActionSet: this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typingOptions, response.unresolvedImports, response.typings); break; - case "invalidate": + case ActionInvalidate: this.typingsCache.deleteTypingsForProject(response.projectName); break; } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 7da95e49575f5..d13caf7f01b0c 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2057,6 +2057,32 @@ namespace ts.server.protocol { childItems?: NavigationTree[]; } + export type TelemetryEventName = "telemetry"; + + export interface TelemetryEvent extends Event { + event: TelemetryEventName; + body: TelemetryEventBody; + } + + export interface TelemetryEventBody { + telemetryEventName: string; + payload: any; + } + + export type TypingsInstalledTelemetryEventName = "typingsInstalled"; + + export interface TypingsInstalledTelemetryEventBody extends TelemetryEventBody { + telemetryEventName: TypingsInstalledTelemetryEventName; + payload: TypingsInstalledTelemetryEventPayload; + } + + export interface TypingsInstalledTelemetryEventPayload { + /** + * Comma separated list of installed typing packages + */ + installedPackages: string; + } + export interface NavBarResponse extends Response { body?: NavigationBarItem[]; } diff --git a/src/server/server.ts b/src/server/server.ts index 5057a13dbd1bc..3b33554aaba6f 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,4 +1,5 @@ /// +/// /// // used in fs.writeSync /* tslint:disable:no-null-keyword */ @@ -196,8 +197,10 @@ namespace ts.server { private socket: NodeSocket; private projectService: ProjectService; private throttledOperations: ThrottledOperations; + private telemetrySender: EventSender; constructor( + private readonly telemetryEnabled: boolean, private readonly logger: server.Logger, host: ServerHost, eventPort: number, @@ -226,15 +229,22 @@ namespace ts.server { this.socket.write(formatMessage({ seq, type: "event", event, body }, this.logger, Buffer.byteLength, this.newLine), "utf8"); } + setTelemetrySender(telemetrySender: EventSender) { + this.telemetrySender = telemetrySender; + } + attach(projectService: ProjectService) { this.projectService = projectService; if (this.logger.hasLevel(LogLevel.requestTime)) { this.logger.info("Binding..."); } - const args: string[] = ["--globalTypingsCacheLocation", this.globalTypingsCacheLocation]; + const args: string[] = [Arguments.GlobalCacheLocation, this.globalTypingsCacheLocation]; + if (this.telemetryEnabled) { + args.push(Arguments.EnableTelemetry); + } if (this.logger.loggingEnabled() && this.logger.getLogFileName()) { - args.push("--logFile", combinePaths(getDirectoryPath(normalizeSlashes(this.logger.getLogFileName())), `ti-${process.pid}.log`)); + args.push(Arguments.LogFile, combinePaths(getDirectoryPath(normalizeSlashes(this.logger.getLogFileName())), `ti-${process.pid}.log`)); } const execArgv: string[] = []; { @@ -280,12 +290,25 @@ namespace ts.server { }); } - private handleMessage(response: SetTypings | InvalidateCachedTypings) { + private handleMessage(response: SetTypings | InvalidateCachedTypings | TypingsInstallEvent) { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`Received response: ${JSON.stringify(response)}`); } + if (response.kind === EventInstall) { + if (this.telemetrySender) { + const body: protocol.TypingsInstalledTelemetryEventBody = { + telemetryEventName: "typingsInstalled", + payload: { + installedPackages: response.packagesToInstall.join(",") + } + }; + const eventName: protocol.TelemetryEventName = "telemetry"; + this.telemetrySender.event(body, eventName); + } + return; + } this.projectService.updateTypingsForProject(response); - if (response.kind == "set" && this.socket) { + if (response.kind == ActionSet && this.socket) { this.sendEvent(0, "setTypings", response); } } @@ -300,18 +323,25 @@ namespace ts.server { useSingleInferredProject: boolean, disableAutomaticTypingAcquisition: boolean, globalTypingsCacheLocation: string, + telemetryEnabled: boolean, logger: server.Logger) { - super( - host, - cancellationToken, - useSingleInferredProject, - disableAutomaticTypingAcquisition - ? nullTypingsInstaller - : new NodeTypingsInstaller(logger, host, installerEventPort, globalTypingsCacheLocation, host.newLine), - Buffer.byteLength, - process.hrtime, - logger, - canUseEvents); + const typingsInstaller = disableAutomaticTypingAcquisition + ? undefined + : new NodeTypingsInstaller(telemetryEnabled, logger, host, installerEventPort, globalTypingsCacheLocation, host.newLine); + + super( + host, + cancellationToken, + useSingleInferredProject, + typingsInstaller || nullTypingsInstaller, + Buffer.byteLength, + process.hrtime, + logger, + canUseEvents); + + if (telemetryEnabled && typingsInstaller) { + typingsInstaller.setTelemetrySender(this); + } } exit() { @@ -538,17 +568,17 @@ namespace ts.server { let eventPort: number; { - const index = sys.args.indexOf("--eventPort"); - if (index >= 0 && index < sys.args.length - 1) { - const v = parseInt(sys.args[index + 1]); - if (!isNaN(v)) { - eventPort = v; - } + const str = findArgument("--eventPort"); + const v = str && parseInt(str); + if (!isNaN(v)) { + eventPort = v; } } - const useSingleInferredProject = sys.args.indexOf("--useSingleInferredProject") >= 0; - const disableAutomaticTypingAcquisition = sys.args.indexOf("--disableAutomaticTypingAcquisition") >= 0; + const useSingleInferredProject = hasArgument("--useSingleInferredProject"); + const disableAutomaticTypingAcquisition = hasArgument("--disableAutomaticTypingAcquisition"); + const telemetryEnabled = hasArgument(Arguments.EnableTelemetry); + const ioSession = new IOSession( sys, cancellationToken, @@ -557,6 +587,7 @@ namespace ts.server { useSingleInferredProject, disableAutomaticTypingAcquisition, getGlobalTypingsCacheLocation(), + telemetryEnabled, logger); process.on("uncaughtException", function (err: Error) { ioSession.logError(err, "unknown"); diff --git a/src/server/session.ts b/src/server/session.ts index 545701f1449fa..b250393b7ffdb 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -73,6 +73,10 @@ namespace ts.server { project: Project; } + export interface EventSender { + event(payload: any, eventName: string): void; + } + function allEditsBeforePos(edits: ts.TextChange[], pos: number) { for (const edit of edits) { if (textSpanEnd(edit.span) >= pos) { @@ -165,7 +169,7 @@ namespace ts.server { return `Content-Length: ${1 + len}\r\n\r\n${json}${newLine}`; } - export class Session { + export class Session implements EventSender { private readonly gcTimer: GcTimer; protected projectService: ProjectService; private errorTimer: any; /*NodeJS.Timer | number*/ diff --git a/src/server/shared.ts b/src/server/shared.ts new file mode 100644 index 0000000000000..81a1f7fb55bc2 --- /dev/null +++ b/src/server/shared.ts @@ -0,0 +1,24 @@ +/// + +namespace ts.server { + export const ActionSet: ActionSet = "action::set"; + export const ActionInvalidate: ActionInvalidate = "action::invalidate"; + export const EventInstall: EventInstall = "event::install"; + + export namespace Arguments { + export const GlobalCacheLocation = "--globalTypingsCacheLocation"; + export const LogFile = "--logFile"; + export const EnableTelemetry = "--enableTelemetry"; + } + + export function hasArgument(argumentName: string) { + return sys.args.indexOf(argumentName) >= 0; + } + + export function findArgument(argumentName: string) { + const index = sys.args.indexOf(argumentName); + return index >= 0 && index < sys.args.length - 1 + ? sys.args[index + 1] + : undefined; + } +} \ No newline at end of file diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index a99994d97c5f9..85c8867916421 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -18,6 +18,7 @@ "files": [ "../services/shims.ts", "../services/utilities.ts", + "shared.ts", "utilities.ts", "scriptVersionCache.ts", "scriptInfo.ts", diff --git a/src/server/tsconfig.library.json b/src/server/tsconfig.library.json index e269629b55898..5483cc8ec289a 100644 --- a/src/server/tsconfig.library.json +++ b/src/server/tsconfig.library.json @@ -15,6 +15,7 @@ "files": [ "../services/shims.ts", "../services/utilities.ts", + "shared.ts", "utilities.ts", "scriptVersionCache.ts", "scriptInfo.ts", diff --git a/src/server/types.d.ts b/src/server/types.d.ts index ec2befe8fa9b6..95f9519cc388c 100644 --- a/src/server/types.d.ts +++ b/src/server/types.d.ts @@ -41,23 +41,33 @@ declare namespace ts.server { readonly kind: "closeProject"; } - export type SetRequest = "set"; - export type InvalidateRequest = "invalidate"; + export type ActionSet = "action::set"; + export type ActionInvalidate = "action::invalidate"; + export type EventInstall = "event::install"; + export interface TypingInstallerResponse { + readonly kind: ActionSet | ActionInvalidate | EventInstall; + } + + export interface ProjectResponse extends TypingInstallerResponse { readonly projectName: string; - readonly kind: SetRequest | InvalidateRequest; } - export interface SetTypings extends TypingInstallerResponse { + export interface SetTypings extends ProjectResponse { readonly typingOptions: ts.TypingOptions; readonly compilerOptions: ts.CompilerOptions; readonly typings: string[]; readonly unresolvedImports: SortedReadonlyArray; - readonly kind: SetRequest; + readonly kind: ActionSet; + } + + export interface InvalidateCachedTypings extends ProjectResponse { + readonly kind: ActionInvalidate; } - export interface InvalidateCachedTypings extends TypingInstallerResponse { - readonly kind: InvalidateRequest; + export interface TypingsInstallEvent extends TypingInstallerResponse { + readonly packagesToInstall: ReadonlyArray; + readonly kind: EventInstall; } export interface InstallTypingHost extends JsTyping.TypingResolutionHost { diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index 1eba0c5d1598f..7020b6aa4f80b 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -75,12 +75,13 @@ namespace ts.server.typingsInstaller { private readonly npmPath: string; readonly typesRegistry: Map; - constructor(globalTypingsCacheLocation: string, throttleLimit: number, log: Log) { + constructor(globalTypingsCacheLocation: string, throttleLimit: number, telemetryEnabled: boolean, log: Log) { super( sys, globalTypingsCacheLocation, toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), throttleLimit, + telemetryEnabled, log); if (this.log.isEnabled()) { this.log.writeLine(`Process id: ${process.pid}`); @@ -144,15 +145,10 @@ namespace ts.server.typingsInstaller { } } - function findArgument(argumentName: string) { - const index = sys.args.indexOf(argumentName); - return index >= 0 && index < sys.args.length - 1 - ? sys.args[index + 1] - : undefined; - } + const logFilePath = findArgument(server.Arguments.LogFile); + const globalTypingsCacheLocation = findArgument(server.Arguments.GlobalCacheLocation); + const telemetryEnabled = hasArgument(server.Arguments.EnableTelemetry); - const logFilePath = findArgument("--logFile"); - const globalTypingsCacheLocation = findArgument("--globalTypingsCacheLocation"); const log = new FileLog(logFilePath); if (log.isEnabled()) { process.on("uncaughtException", (e: Error) => { @@ -165,6 +161,6 @@ namespace ts.server.typingsInstaller { } process.exit(0); }); - const installer = new NodeTypingsInstaller(globalTypingsCacheLocation, /*throttleLimit*/5, log); + const installer = new NodeTypingsInstaller(globalTypingsCacheLocation, /*throttleLimit*/5, telemetryEnabled, log); installer.listen(); } \ No newline at end of file diff --git a/src/server/typingsInstaller/tsconfig.json b/src/server/typingsInstaller/tsconfig.json index c9b4d8f0ad14a..c6031b19aae48 100644 --- a/src/server/typingsInstaller/tsconfig.json +++ b/src/server/typingsInstaller/tsconfig.json @@ -17,6 +17,7 @@ }, "files": [ "../types.d.ts", + "../shared.ts", "typingsInstaller.ts", "nodeTypingsInstaller.ts" ] diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index 82a279f3de3c2..a602a9f1a26c8 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -2,6 +2,7 @@ /// /// /// +/// namespace ts.server.typingsInstaller { interface NpmConfig { @@ -88,6 +89,7 @@ namespace ts.server.typingsInstaller { readonly globalCachePath: string, readonly safeListPath: Path, readonly throttleLimit: number, + readonly telemetryEnabled: boolean, protected readonly log = nullLog) { if (this.log.isEnabled()) { this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}'`); @@ -298,9 +300,17 @@ namespace ts.server.typingsInstaller { this.installRunCount++; this.installTypingsAsync(requestId, scopedTypings, cachePath, ok => { + if (this.telemetryEnabled) { + this.sendResponse({ + kind: EventInstall, + packagesToInstall: scopedTypings + }); + } + if (!ok) { return; } + // TODO: watch project directory if (this.log.isEnabled()) { this.log.writeLine(`Requested to install typings ${JSON.stringify(scopedTypings)}, installed typings ${JSON.stringify(scopedTypings)}`); @@ -354,7 +364,7 @@ namespace ts.server.typingsInstaller { this.log.writeLine(`Got FS notification for ${f}, handler is already invoked '${isInvoked}'`); } if (!isInvoked) { - this.sendResponse({ projectName: projectName, kind: "invalidate" }); + this.sendResponse({ projectName: projectName, kind: server.ActionInvalidate }); isInvoked = true; } }); @@ -370,7 +380,7 @@ namespace ts.server.typingsInstaller { compilerOptions: request.compilerOptions, typings, unresolvedImports: request.unresolvedImports, - kind: "set" + kind: server.ActionSet }; } @@ -392,6 +402,6 @@ namespace ts.server.typingsInstaller { } protected abstract installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void; - protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void; + protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | TypingsInstallEvent): void; } } \ No newline at end of file diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 8806b759e3f8d..9a832d22c8d5c 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,4 +1,5 @@ /// +/// namespace ts.server { export enum LogLevel { @@ -10,6 +11,7 @@ namespace ts.server { export const emptyArray: ReadonlyArray = []; + export interface Logger { close(): void; hasLevel(level: LogLevel): boolean;