- Reorganize nodeTypingsInstaller and typingsInstaller for testing purposes

- Add throttle tests
- Add full npm path
This commit is contained in:
Jason Ramsay
2016-09-19 13:56:30 -07:00
parent 4b9e554494
commit 609e56ed8e
4 changed files with 478 additions and 180 deletions

View File

@@ -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<any> = {};
for (const typing of installedTypings) {
dependencies[typing] = "1.0.0";
}
return JSON.stringify({ dependencies: dependencies });
}
export function getExecutingFilePathFromLibFile(libFilePath: string): string {

View File

@@ -1,9 +1,26 @@
/// <reference path="../harness.ts" />
/// <reference path="../harness.ts" />
/// <reference path="./tsserverProjectSystem.ts" />
/// <reference path="../../server/typingsInstaller/typingsInstaller.ts" />
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]);
});
});
}

View File

@@ -2,13 +2,6 @@
/// <reference types="node" />
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}'.`);
}

View File

@@ -1,4 +1,4 @@
/// <reference path="../../compiler/core.ts" />
/// <reference path="../../compiler/core.ts" />
/// <reference path="../../compiler/moduleNameResolver.ts" />
/// <reference path="../../services/jsTyping.ts"/>
/// <reference path="../types.d.ts"/>
@@ -8,11 +8,19 @@ namespace ts.server.typingsInstaller {
devDependencies: MapLike<any>;
}
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<string> = createMap<string>();
private missingTypingsSet: Map<true> = createMap<true>();
private knownCachesSet: Map<true> = createMap<true>();
private projectWatchers: Map<FileWatcher[]> = createMap<FileWatcher[]>();
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;
}
}