From 609e56ed8e533cd2a37a85f357f919dc7c74326c Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Mon, 19 Sep 2016 13:56:30 -0700 Subject: [PATCH] - Reorganize nodeTypingsInstaller and typingsInstaller for testing purposes - Add throttle tests - Add full npm path --- .../unittests/tsserverProjectSystem.ts | 54 ++- src/harness/unittests/typingsInstaller.ts | 387 ++++++++++++++++-- .../typingsInstaller/nodeTypingsInstaller.ts | 122 +----- .../typingsInstaller/typingsInstaller.ts | 95 ++++- 4 files changed, 478 insertions(+), 180 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index e22f1752ce4..5275c4c89fc 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -13,6 +13,13 @@ namespace ts.projectSystem { }) }; + export interface PostExecAction { + readonly error: Error; + readonly stdout: string; + readonly stderr: string; + readonly callback: (err: Error, stdout: string, stderr: string) => void; + } + export function notImplemented(): any { throw new Error("Not yet implemented"); } @@ -47,13 +54,13 @@ namespace ts.projectSystem { } safeFileList = safeList.path; - postInstallActions: ((map: (t: string[]) => string[]) => void)[] = []; + protected postExecActions: PostExecAction[] = []; - runPostInstallActions(map: (t: string[]) => string[]) { - for (const f of this.postInstallActions) { - f(map); + runPostExecActions() { + for (const action of this.postExecActions) { + action.callback(action.error, action.stdout, action.stderr); } - this.postInstallActions = []; + this.postExecActions = []; } onProjectClosed(p: server.Project) { @@ -67,14 +74,16 @@ namespace ts.projectSystem { return this.installTypingHost; } - isPackageInstalled(packageName: string) { - return true; - } - - runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void) { - this.postInstallActions.push(map => { - postInstallAction(map(typingsToInstall)); - }); + execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void): void { + switch (prefix) { + case "npm view": + case "npm install": + case "npm ls": + break; + default: + throw new Error("TypingsInstaller: execAsync command not yet implemented"); + } + this.addPostExecAction("success", cb); } sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { @@ -85,6 +94,25 @@ namespace ts.projectSystem { const request = server.createInstallTypingsRequest(project, typingOptions, this.globalTypingsCacheLocation); this.install(request); } + + addPostExecAction(stdout: string | string[], cb: (err: Error, stdout: string, stderr: string) => void) { + const out = typeof stdout === "string" ? stdout : createNpmPackageJsonString(stdout); + const action: PostExecAction = { + error: undefined, + stdout: out, + stderr: "", + callback: cb + }; + this.postExecActions.push(action); + } + } + + function createNpmPackageJsonString(installedTypings: string[]): string { + const dependencies: MapLike = {}; + for (const typing of installedTypings) { + dependencies[typing] = "1.0.0"; + } + return JSON.stringify({ dependencies: dependencies }); } export function getExecutingFilePathFromLibFile(libFilePath: string): string { diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 47373bfd261..4ff70e0d3dc 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -1,9 +1,26 @@ -/// +/// /// /// namespace ts.projectSystem { - describe("typings installer", () => { + describe("typingsInstaller", () => { + function execHelper(self: TestTypingsInstaller, host: TestServerHost, installedTypings: string[], typingFiles: FileOrFolder[], prefix: string, cb: (err: Error, stdout: string, stderr: string) => void): boolean { + let execSuper = true; + switch (prefix) { + case "npm install": + for (const file of typingFiles) { + host.createFileOrFolder(file, /*createParentDirectory*/ true); + } + break; + case "npm ls": + self.addPostExecAction(installedTypings, cb); + execSuper = false; + break; + default: + break; + } + return execSuper; + } it("configured projects (typings installed) 1", () => { const file1 = { path: "/a/b/app.js", @@ -34,9 +51,20 @@ namespace ts.projectSystem { path: "/a/data/node_modules/@types/jquery/index.d.ts", content: "declare const $: { x: number }" }; - const host = createServerHost([file1, tsconfig, packageJson]); - const installer = new TestTypingsInstaller("/a/data/", host); + const installer = new (class extends TestTypingsInstaller { + constructor() { + super("/a/data/", host); + } + execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void): void { + const installedTypings = ["@types/jquery"]; + const typingFiles = [jquery]; + if (execHelper(this, host, installedTypings, typingFiles, prefix, cb)) { + super.execAsync(prefix, command, cwd, requestId, cb); + } + } + })(); + const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer }); projectService.openClientFile(file1.path); @@ -46,11 +74,8 @@ namespace ts.projectSystem { assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "package.json"))); - installer.runPostInstallActions(t => { - assert.deepEqual(t, ["jquery"]); - host.createFileOrFolder(jquery, /*createParentDirectory*/ true); - return ["@types/jquery"]; - }); + installer.runPostExecActions(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectActualFiles(p, [file1.path, jquery.path]); }); @@ -75,7 +100,18 @@ namespace ts.projectSystem { content: "declare const $: { x: number }" }; const host = createServerHost([file1, packageJson]); - const installer = new TestTypingsInstaller("/a/data/", host); + const installer = new (class extends TestTypingsInstaller { + constructor() { + super("/a/data/", host); + } + execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void): void { + const installedTypings = ["@types/jquery"]; + const typingFiles = [jquery]; + if (execHelper(this, host, installedTypings, typingFiles, prefix, cb)) { + super.execAsync(prefix, command, cwd, requestId, cb); + } + } + })(); const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer }); projectService.openClientFile(file1.path); @@ -86,11 +122,8 @@ namespace ts.projectSystem { assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "package.json"))); - installer.runPostInstallActions(t => { - assert.deepEqual(t, ["jquery"]); - host.createFileOrFolder(jquery, /*createParentDirectory*/ true); - return ["@types/jquery"]; - }); + installer.runPostExecActions(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); checkProjectActualFiles(p, [file1.path, jquery.path]); }); @@ -155,6 +188,10 @@ namespace ts.projectSystem { path: "/a/b/app.ts", content: "" }; + const jquery = { + path: "/a/data/node_modules/@types/jquery/index.d.ts", + content: "declare const $: { x: number }" + }; const host = createServerHost([file1]); let enqueueIsCalled = false; let runInstallIsCalled = false; @@ -166,10 +203,15 @@ namespace ts.projectSystem { enqueueIsCalled = true; super.enqueueInstallTypingsRequest(project, typingOptions); } - runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void { - assert.deepEqual(typingsToInstall, ["node"]); - runInstallIsCalled = true; - super.runInstall(cachePath, typingsToInstall, postInstallAction); + execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void): void { + const installedTypings = ["@types/jquery"]; + const typingFiles = [jquery]; + if (execHelper(this, host, installedTypings, typingFiles, prefix, cb)) { + super.execAsync(prefix, command, cwd, requestId, cb); + } + if (prefix === "npm install") { + runInstallIsCalled = true; + } } })(); @@ -181,6 +223,9 @@ namespace ts.projectSystem { rootFiles: [toExternalFile(file1.path)], typingOptions: { enableAutoDiscovery: true, include: ["node"] } }); + + installer.runPostExecActions(); + // autoDiscovery is set in typing options - use it even if project contains only .ts files projectService.checkNumberOfProjects({ externalProjects: 1 }); assert.isTrue(enqueueIsCalled, "expected 'enqueueIsCalled' to be true"); @@ -214,7 +259,18 @@ namespace ts.projectSystem { }; const host = createServerHost([file1, file2, file3]); - const installer = new TestTypingsInstaller("/a/data/", host); + const installer = new (class extends TestTypingsInstaller { + constructor() { + super("/a/data/", host); + } + execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void): void { + const installedTypings = ["@types/lodash", "@types/react"]; + const typingFiles = [lodash, react]; + if (execHelper(this, host, installedTypings, typingFiles, prefix, cb)) { + super.execAsync(prefix, command, cwd, requestId, cb); + } + } + })(); const projectFileName = "/a/app/test.csproj"; const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -229,12 +285,7 @@ namespace ts.projectSystem { projectService.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path, file3.path]); - installer.runPostInstallActions(t => { - assert.deepEqual(t, ["lodash", "react"]); - host.createFileOrFolder(lodash, /*createParentDirectory*/ true); - host.createFileOrFolder(react, /*createParentDirectory*/ true); - return ["@types/lodash", "@types/react"]; - }); + installer.runPostExecActions(); checkNumberOfProjects(projectService, { externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path, file3.path, lodash.path, react.path]); @@ -255,7 +306,6 @@ namespace ts.projectSystem { const host = createServerHost([file1, file2]); let enqueueIsCalled = false; let runInstallIsCalled = false; - let runPostInstallIsCalled = false; const installer = new (class extends TestTypingsInstaller { constructor() { super("/a/data/", host); @@ -264,9 +314,15 @@ namespace ts.projectSystem { enqueueIsCalled = true; super.enqueueInstallTypingsRequest(project, typingOptions); } - runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void { - runInstallIsCalled = true; - super.runInstall(cachePath, typingsToInstall, postInstallAction); + execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void): void { + const installedTypings: string[] = []; + const typingFiles: FileOrFolder[] = []; + if (execHelper(this, host, installedTypings, typingFiles, prefix, cb)) { + super.execAsync(prefix, command, cwd, requestId, cb); + } + if (prefix === "npm install") { + runInstallIsCalled = true; + } } })(); @@ -283,16 +339,12 @@ namespace ts.projectSystem { projectService.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path]); - installer.runPostInstallActions(t => { - runPostInstallIsCalled = true; - return []; - }); + installer.runPostExecActions(); checkNumberOfProjects(projectService, { externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path]); assert.isFalse(enqueueIsCalled, "expected 'enqueueIsCalled' to be false"); assert.isFalse(runInstallIsCalled, "expected 'runInstallIsCalled' to be false"); - assert.isFalse(runPostInstallIsCalled, "expected 'runPostInstallIsCalled' to be false"); }); it("external project - with typing options, with only js, d.ts files", () => { @@ -340,7 +392,18 @@ namespace ts.projectSystem { }; const host = createServerHost([file1, file2, file3, packageJson]); - const installer = new TestTypingsInstaller("/a/data/", host); + const installer = new (class extends TestTypingsInstaller { + constructor() { + super("/a/data/", host); + } + execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void): void { + const installedTypings = ["@types/commander", "@types/express", "@types/jquery", "@types/moment"]; + const typingFiles = [commander, express, jquery, moment]; + if (execHelper(this, host, installedTypings, typingFiles, prefix, cb)) { + super.execAsync(prefix, command, cwd, requestId, cb); + } + } + })(); const projectFileName = "/a/app/test.csproj"; const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -355,17 +418,253 @@ namespace ts.projectSystem { projectService.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path, file3.path]); - installer.runPostInstallActions(t => { - assert.deepEqual(t, ["jquery", "moment", "express", "commander" ]); - host.createFileOrFolder(commander, /*createParentDirectory*/ true); - host.createFileOrFolder(express, /*createParentDirectory*/ true); - host.createFileOrFolder(jquery, /*createParentDirectory*/ true); - host.createFileOrFolder(moment, /*createParentDirectory*/ true); - return ["@types/commander", "@types/express", "@types/jquery", "@types/moment"]; - }); + installer.runPostExecActions(); checkNumberOfProjects(projectService, { externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path, file3.path, commander.path, express.path, jquery.path, moment.path]); }); + + it("Throttle - delayed typings to install", () => { + const file1 = { + path: "/a/b/lodash.js", + content: "" + }; + const file2 = { + path: "/a/b/commander.js", + content: "" + }; + const file3 = { + path: "/a/b/file3.d.ts", + content: "" + }; + const packageJson = { + path: "/a/b/package.json", + content: JSON.stringify({ + name: "test", + dependencies: { + express: "^3.1.0", + cordova: "1.0.0", + grunt: "1.0.0", + gulp: "1.0.0", + forever: "1.0.0", + } + }) + }; + + const commander = { + path: "/a/data/node_modules/@types/commander/index.d.ts", + content: "declare const commander: { x: number }" + }; + const express = { + path: "/a/data/node_modules/@types/express/index.d.ts", + content: "declare const express: { x: number }" + }; + const jquery = { + path: "/a/data/node_modules/@types/jquery/index.d.ts", + content: "declare const jquery: { x: number }" + }; + const moment = { + path: "/a/data/node_modules/@types/moment/index.d.ts", + content: "declare const moment: { x: number }" + }; + const lodash = { + path: "/a/data/node_modules/@types/lodash/index.d.ts", + content: "declare const lodash: { x: number }" + }; + const cordova = { + path: "/a/data/node_modules/@types/cordova/index.d.ts", + content: "declare const cordova: { x: number }" + }; + const grunt = { + path: "/a/data/node_modules/@types/grunt/index.d.ts", + content: "declare const grunt: { x: number }" + }; + const gulp = { + path: "/a/data/node_modules/@types/gulp/index.d.ts", + content: "declare const gulp: { x: number }" + }; + const forever = { + path: "/a/data/node_modules/@types/forever/index.d.ts", + content: "declare const forever: { x: number }" + }; + + let npmViewCount = 0; + let npmInstallCount = 0; + const host = createServerHost([file1, file2, file3, packageJson]); + const installer = new (class extends TestTypingsInstaller { + constructor() { + super("/a/data/", host); + } + execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void): void { + if (prefix === "npm view") { + npmViewCount++; + } + if (prefix === "npm install") { + npmInstallCount++; + } + const installedTypings = ["@types/commander", "@types/express", "@types/jquery", "@types/moment", "@types/lodash", "@types/cordova", "@types/grunt", "@types/gulp", "@types/forever"]; + const typingFiles = [commander, express, jquery, moment, lodash, cordova, grunt, gulp, forever]; + if (execHelper(this, host, installedTypings, typingFiles, prefix, cb)) { + super.execAsync(prefix, command, cwd, requestId, cb); + } + } + })(); + + const projectFileName = "/a/app/test.csproj"; + const projectService = createProjectService(host, { typingsInstaller: installer }); + projectService.openExternalProject({ + projectFileName, + options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs }, + rootFiles: [toExternalFile(file1.path), toExternalFile(file2.path), toExternalFile(file3.path)], + typingOptions: { include: ["jquery", "moment"] } + }); + + const p = projectService.externalProjects[0]; + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(p, [file1.path, file2.path, file3.path]); + // The npm view count should be 5 even though there are 9 typings to acquire. + // The throttle limit has been reached so no more execAsync requests will be + // queued until these have been processed. + assert.isTrue(npmViewCount === 5); + assert.isTrue(npmInstallCount === 0); + + installer.runPostExecActions(); + + assert.isTrue(npmViewCount === 9); + assert.isTrue(npmInstallCount === 1); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectActualFiles(p, [file1.path, file2.path, file3.path, commander.path, express.path, jquery.path, moment.path, lodash.path, cordova.path, grunt.path, gulp.path, forever.path]); + }); + + it("Throttle - delayed run install requests", () => { + const file1 = { + path: "/a/b/lodash.js", + content: "" + }; + const file2 = { + path: "/a/b/commander.js", + content: "" + }; + const file3 = { + path: "/a/b/file3.d.ts", + content: "" + }; + + const commander = { + path: "/a/data/node_modules/@types/commander/index.d.ts", + content: "declare const commander: { x: number }" + }; + const express = { + path: "/a/data/node_modules/@types/express/index.d.ts", + content: "declare const express: { x: number }" + }; + const jquery = { + path: "/a/data/node_modules/@types/jquery/index.d.ts", + content: "declare const jquery: { x: number }" + }; + const moment = { + path: "/a/data/node_modules/@types/moment/index.d.ts", + content: "declare const moment: { x: number }" + }; + const lodash = { + path: "/a/data/node_modules/@types/lodash/index.d.ts", + content: "declare const lodash: { x: number }" + }; + const cordova = { + path: "/a/data/node_modules/@types/cordova/index.d.ts", + content: "declare const cordova: { x: number }" + }; + const grunt = { + path: "/a/data/node_modules/@types/grunt/index.d.ts", + content: "declare const grunt: { x: number }" + }; + const gulp = { + path: "/a/data/node_modules/@types/gulp/index.d.ts", + content: "declare const gulp: { x: number }" + }; + const forever = { + path: "/a/data/node_modules/@types/forever/index.d.ts", + content: "declare const forever: { x: number }" + }; + + let npmViewCount = 0; + let npmInstallCount = 0; + let hasRunInstall2Typings = false; + const host = createServerHost([file1, file2, file3]); + const installer = new (class extends TestTypingsInstaller { + constructor() { + super("/a/data/", host); + } + execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void): void { + switch (prefix) { + case "npm view": + if (command.match("grunt|gulp|forever")) { + hasRunInstall2Typings = true; + } + npmViewCount++; + break; + case "npm install": + npmInstallCount++; + break; + } + + let installedTypings: string[]; + let typingFiles: FileOrFolder[]; + if (npmInstallCount <= 1) { + installedTypings = ["@types/commander", "@types/express", "@types/jquery", "@types/moment", "@types/lodash", "@types/cordova"]; + typingFiles = [commander, express, jquery, moment, lodash, cordova]; + } + else { + installedTypings = ["@types/grunt", "@types/gulp", "@types/forever"]; + typingFiles = [grunt, gulp, forever]; + } + + if (execHelper(this, host, installedTypings, typingFiles, prefix, cb)) { + super.execAsync(prefix, command, cwd, requestId, cb); + } + } + })(); + + // Create project #1 with 6 typings + const projectService = createProjectService(host, { typingsInstaller: installer }); + const projectFileName1 = "/a/app/test1.csproj"; + projectService.openExternalProject({ + projectFileName: projectFileName1, + options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs }, + rootFiles: [toExternalFile(file1.path), toExternalFile(file2.path), toExternalFile(file3.path)], + typingOptions: { include: ["jquery", "moment", "express", "cordova"] } + }); + + // Create project #2 with 3 typings + const projectFileName2 = "/a/app/test2.csproj"; + projectService.openExternalProject({ + projectFileName: projectFileName2, + options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs }, + rootFiles: [toExternalFile(file3.path)], + typingOptions: { include: ["grunt", "gulp", "forever"] } + }); + + const p1 = projectService.externalProjects[0]; + const p2 = projectService.externalProjects[1]; + projectService.checkNumberOfProjects({ externalProjects: 2 }); + checkProjectActualFiles(p1, [file1.path, file2.path, file3.path]); + checkProjectActualFiles(p2, [file3.path]); + + // The npm view count should be 5 even though there are 9 typings to acquire. + // The throttle limit has been reached so no more run execAsync requests will be + // queued until these have been processed. Assert that typings from the second + // run install request have not been queued. + assert.isTrue(npmViewCount === 5); + assert.isTrue(npmInstallCount === 0); + assert.isFalse(hasRunInstall2Typings); + + installer.runPostExecActions(); + + assert.isTrue(npmViewCount === 9); + assert.isTrue(npmInstallCount === 2); + assert.isTrue(hasRunInstall2Typings); + checkProjectActualFiles(p1, [file1.path, file2.path, file3.path, commander.path, express.path, jquery.path, moment.path, lodash.path, cordova.path]); + checkProjectActualFiles(p2, [file3.path, grunt.path, gulp.path, forever.path]); + }); }); } \ No newline at end of file diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index 1e3448e0685..3dc4be7e3da 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -2,13 +2,6 @@ /// namespace ts.server.typingsInstaller { - interface RunInstallRequest { - readonly cachePath: string; - readonly typingsToInstall: string[]; - readonly postInstallAction: (installedTypings: string[]) => void; - } - - const throttleLimit = 5; const fs: { appendFileSync(file: string, content: string): void } = require("fs"); @@ -26,13 +19,8 @@ namespace ts.server.typingsInstaller { } export class NodeTypingsInstaller extends TypingsInstaller { - private execSync: { (command: string, options: { stdio: "ignore" | "pipe", cwd?: string }): Buffer | string }; private exec: { (command: string, options: { cwd: string }, callback?: (error: Error, stdout: string, stderr: string) => void): any }; - private npmBinPath: string; - private installRunCount = 1; - private throttleCount = 0; - private delayedRunInstallRequests: RunInstallRequest[] = []; readonly installTypingHost: InstallTypingHost = sys; constructor(globalTypingsCacheLocation: string, log: Log) { @@ -40,25 +28,12 @@ namespace ts.server.typingsInstaller { if (this.log.isEnabled()) { this.log.writeLine(`Process id: ${process.pid}`); } - const { exec, execSync } = require("child_process"); - this.execSync = execSync; + const { exec } = require("child_process"); this.exec = exec; } init() { super.init(); - try { - this.npmBinPath = this.execSync("npm -g bin", { stdio: "pipe" }).toString().trim(); - if (this.log.isEnabled()) { - this.log.writeLine(`Global npm bin path '${this.npmBinPath}'`); - } - } - catch (e) { - this.npmBinPath = ""; - if (this.log.isEnabled()) { - this.log.writeLine(`Error when getting npm bin path: ${e}. Set bin path to ""`); - } - } process.on("message", (req: DiscoverTypings | CloseProject) => { switch (req.kind) { case "discover": @@ -70,23 +45,6 @@ namespace ts.server.typingsInstaller { }); } - protected isPackageInstalled(packageName: string) { - try { - const output = this.execSync(`npm list --silent --global --depth=1 ${packageName}`, { stdio: "pipe" }).toString(); - if (this.log.isEnabled()) { - this.log.writeLine(`IsPackageInstalled::stdout '${output}'`); - } - return true; - } - catch (e) { - if (this.log.isEnabled()) { - this.log.writeLine(`IsPackageInstalled::err::stdout '${e.stdout && e.stdout.toString()}'`); - this.log.writeLine(`IsPackageInstalled::err::stderr '${e.stdout && e.stderr.toString()}'`); - } - return false; - } - } - protected sendResponse(response: SetTypings | InvalidateCachedTypings) { if (this.log.isEnabled()) { this.log.writeLine(`Sending response: ${JSON.stringify(response)}`); @@ -97,83 +55,7 @@ namespace ts.server.typingsInstaller { } } - protected runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void { - if (this.throttleCount === throttleLimit) { - const request = { - cachePath: cachePath, - typingsToInstall: typingsToInstall, - postInstallAction: postInstallAction - }; - this.delayedRunInstallRequests.push(request); - return; - } - const id = this.installRunCount; - this.installRunCount++; - let execInstallCmdCount = 0; - const filteredTypings: string[] = []; - const delayedTypingsToInstall: string[] = []; - for (const typing of typingsToInstall) { - if (this.throttleCount === throttleLimit) { - delayedTypingsToInstall.push(typing); - continue; - } - execNpmViewTyping(this, typing); - } - - function execNpmViewTyping(self: NodeTypingsInstaller, typing: string) { - self.throttleCount++; - const command = `npm view @types/${typing} --silent name`; - self.execAsync("npm view", command, cachePath, id, (err, stdout, stderr) => { - if (stdout) { - filteredTypings.push(typing); - } - execInstallCmdCount++; - self.throttleCount--; - if (delayedTypingsToInstall.length > 0) { - return execNpmViewTyping(self, delayedTypingsToInstall.pop()); - } - if (execInstallCmdCount === typingsToInstall.length) { - installFilteredTypings(self, filteredTypings); - if (self.delayedRunInstallRequests.length > 0) { - const request = self.delayedRunInstallRequests.pop(); - return self.runInstall(request.cachePath, request.typingsToInstall, request.postInstallAction); - } - } - }); - } - - function installFilteredTypings(self: NodeTypingsInstaller, filteredTypings: string[]) { - if (filteredTypings.length === 0) { - reportInstalledTypings(self); - return; - } - const command = `npm install ${filteredTypings.map(t => "@types/" + t).join(" ")} --save-dev`; - self.execAsync("npm install", command, cachePath, id, (err, stdout, stderr) => { - if (stdout) { - reportInstalledTypings(self); - } - }); - } - - function reportInstalledTypings(self: NodeTypingsInstaller) { - const command = "npm ls -json"; - self.execAsync("npm ls", command, cachePath, id, (err, stdout, stderr) => { - let installedTypings: string[]; - try { - const response = JSON.parse(stdout); - if (response.dependencies) { - installedTypings = getOwnKeys(response.dependencies); - } - } - catch (e) { - self.log.writeLine(`Error parsing installed @types dependencies. Error details: ${e.message}`); - } - postInstallAction(installedTypings || []); - }); - } - } - - private execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void) { + protected execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void) { if (this.log.isEnabled()) { this.log.writeLine(`#${requestId} running command '${command}'.`); } diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index e4870a568c4..24139436932 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -1,4 +1,4 @@ -/// +/// /// /// /// @@ -8,11 +8,19 @@ namespace ts.server.typingsInstaller { devDependencies: MapLike; } + interface RunInstallRequest { + readonly cachePath: string; + readonly typingsToInstall: string[]; + readonly postInstallAction: (installedTypings: string[]) => void; + } + export interface Log { isEnabled(): boolean; writeLine(text: string): void; } + const throttleLimit = 5; + const nullLog: Log = { isEnabled: () => false, writeLine: () => {} @@ -24,10 +32,14 @@ namespace ts.server.typingsInstaller { } export abstract class TypingsInstaller { + private installRunCount = 1; + private throttleCount = 0; private packageNameToTypingLocation: Map = createMap(); private missingTypingsSet: Map = createMap(); private knownCachesSet: Map = createMap(); private projectWatchers: Map = createMap(); + private delayedRunInstallRequests: RunInstallRequest[] = []; + private npmPath: string; abstract readonly installTypingHost: InstallTypingHost; @@ -38,6 +50,8 @@ namespace ts.server.typingsInstaller { } init() { + const path = require("path"); + this.npmPath = `"${path.join(path.dirname(process.argv[0]), "npm")}"`; this.processCacheLocation(this.globalCachePath); } @@ -224,6 +238,82 @@ namespace ts.server.typingsInstaller { }); } + private runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void { + if (this.throttleCount === throttleLimit) { + const request = { + cachePath: cachePath, + typingsToInstall: typingsToInstall, + postInstallAction: postInstallAction + }; + this.delayedRunInstallRequests.push(request); + return; + } + const id = this.installRunCount; + this.installRunCount++; + let execInstallCmdCount = 0; + const filteredTypings: string[] = []; + const delayedTypingsToInstall: string[] = []; + for (const typing of typingsToInstall) { + if (this.throttleCount === throttleLimit) { + delayedTypingsToInstall.push(typing); + continue; + } + execNpmViewTyping(this, typing); + } + + function execNpmViewTyping(self: TypingsInstaller, typing: string) { + self.throttleCount++; + const command = `${self.npmPath} view @types/${typing} --silent name`; + self.execAsync("npm view", command, cachePath, id, (err, stdout, stderr) => { + if (stdout) { + filteredTypings.push(typing); + } + execInstallCmdCount++; + self.throttleCount--; + if (delayedTypingsToInstall.length > 0) { + return execNpmViewTyping(self, delayedTypingsToInstall.pop()); + } + if (execInstallCmdCount === typingsToInstall.length) { + installFilteredTypings(self, filteredTypings); + if (self.delayedRunInstallRequests.length > 0) { + const request = self.delayedRunInstallRequests.pop(); + return self.runInstall(request.cachePath, request.typingsToInstall, request.postInstallAction); + } + } + }); + } + + function installFilteredTypings(self: TypingsInstaller, filteredTypings: string[]) { + if (filteredTypings.length === 0) { + reportInstalledTypings(self); + return; + } + const command = `${self.npmPath} install ${filteredTypings.map(t => "@types/" + t).join(" ")} --save-dev`; + self.execAsync("npm install", command, cachePath, id, (err, stdout, stderr) => { + if (stdout) { + reportInstalledTypings(self); + } + }); + } + + function reportInstalledTypings(self: TypingsInstaller) { + const command = `${self.npmPath} ls -json`; + self.execAsync("npm ls", command, cachePath, id, (err, stdout, stderr) => { + let installedTypings: string[]; + try { + const response = JSON.parse(stdout); + if (response.dependencies) { + installedTypings = getOwnKeys(response.dependencies); + } + } + catch (e) { + self.log.writeLine(`Error parsing installed @types dependencies. Error details: ${e.message}`); + } + postInstallAction(installedTypings || []); + }); + } + } + private ensureDirectoryExists(directory: string, host: InstallTypingHost): void { const directoryName = getDirectoryPath(directory); if (!host.directoryExists(directoryName)) { @@ -267,8 +357,7 @@ namespace ts.server.typingsInstaller { }; } - protected abstract isPackageInstalled(packageName: string): boolean; + protected abstract execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void): void; protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void; - protected abstract runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void; } } \ No newline at end of file