Merge pull request #10960 from Microsoft/throttleTypingsInstallerRequestsFinal
Add throttle limit to typings installer requests
This commit is contained in:
commit
6275da2a52
|
@ -3,6 +3,10 @@
|
|||
/// <reference path="../../server/typingsInstaller/typingsInstaller.ts" />
|
||||
|
||||
namespace ts.projectSystem {
|
||||
function createTestTypingsInstaller(host: server.ServerHost) {
|
||||
return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host);
|
||||
}
|
||||
|
||||
describe("CompileOnSave affected list", () => {
|
||||
function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: FileOrFolder[] }[]) {
|
||||
const response: server.protocol.CompileOnSaveAffectedFileListSingleProject[] = session.executeCommand(request).response;
|
||||
|
@ -105,7 +109,7 @@ namespace ts.projectSystem {
|
|||
|
||||
it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => {
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
|
||||
openFilesForSession([moduleFile1, file1Consumer1], session);
|
||||
|
@ -130,7 +134,7 @@ namespace ts.projectSystem {
|
|||
|
||||
it("should be up-to-date with the reference map changes", () => {
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
|
||||
openFilesForSession([moduleFile1, file1Consumer1], session);
|
||||
|
@ -177,7 +181,7 @@ namespace ts.projectSystem {
|
|||
|
||||
it("should be up-to-date with changes made in non-open files", () => {
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
|
||||
openFilesForSession([moduleFile1], session);
|
||||
|
@ -195,7 +199,7 @@ namespace ts.projectSystem {
|
|||
|
||||
it("should be up-to-date with deleted files", () => {
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
|
||||
openFilesForSession([moduleFile1], session);
|
||||
|
@ -210,7 +214,7 @@ namespace ts.projectSystem {
|
|||
|
||||
it("should be up-to-date with newly created files", () => {
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
|
||||
openFilesForSession([moduleFile1], session);
|
||||
|
@ -247,7 +251,7 @@ namespace ts.projectSystem {
|
|||
};
|
||||
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
|
||||
openFilesForSession([moduleFile1, file1Consumer1], session);
|
||||
|
@ -264,7 +268,7 @@ namespace ts.projectSystem {
|
|||
|
||||
it("should return all files if a global file changed shape", () => {
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
|
||||
openFilesForSession([globalFile3], session);
|
||||
|
@ -290,7 +294,7 @@ namespace ts.projectSystem {
|
|||
};
|
||||
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
openFilesForSession([moduleFile1], session);
|
||||
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []);
|
||||
|
@ -308,7 +312,7 @@ namespace ts.projectSystem {
|
|||
};
|
||||
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
openFilesForSession([moduleFile1], session);
|
||||
|
||||
|
@ -337,7 +341,7 @@ namespace ts.projectSystem {
|
|||
};
|
||||
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
openFilesForSession([moduleFile1], session);
|
||||
|
||||
|
@ -359,7 +363,7 @@ namespace ts.projectSystem {
|
|||
content: `import {y} from "./file1Consumer1";`
|
||||
};
|
||||
const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
|
||||
openFilesForSession([moduleFile1, file1Consumer1], session);
|
||||
|
@ -392,7 +396,7 @@ namespace ts.projectSystem {
|
|||
export var t2 = 10;`
|
||||
};
|
||||
const host = createServerHost([file1, file2, configFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
|
||||
openFilesForSession([file1, file2], session);
|
||||
|
@ -475,7 +479,7 @@ namespace ts.projectSystem {
|
|||
content: `{}`
|
||||
};
|
||||
const host = createServerHost([file1, file2, configFile, libFile]);
|
||||
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
const typingsInstaller = createTestTypingsInstaller(host);
|
||||
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
|
||||
openFilesForSession([file1, file2], session);
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
/// <reference path="../../server/typingsInstaller/typingsInstaller.ts" />
|
||||
|
||||
namespace ts.projectSystem {
|
||||
import TI = server.typingsInstaller;
|
||||
|
||||
const safeList = {
|
||||
path: <Path>"/safeList.json",
|
||||
content: JSON.stringify({
|
||||
|
@ -13,6 +15,14 @@ namespace ts.projectSystem {
|
|||
})
|
||||
};
|
||||
|
||||
export interface PostExecAction {
|
||||
readonly requestKind: TI.RequestKind;
|
||||
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");
|
||||
}
|
||||
|
@ -39,21 +49,27 @@ namespace ts.projectSystem {
|
|||
content: libFileContent
|
||||
};
|
||||
|
||||
export class TestTypingsInstaller extends server.typingsInstaller.TypingsInstaller implements server.ITypingsInstaller {
|
||||
export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller {
|
||||
protected projectService: server.ProjectService;
|
||||
constructor(readonly globalTypingsCacheLocation: string, readonly installTypingHost: server.ServerHost) {
|
||||
super(globalTypingsCacheLocation, safeList.path);
|
||||
constructor(readonly globalTypingsCacheLocation: string, throttleLimit: number, readonly installTypingHost: server.ServerHost) {
|
||||
super(globalTypingsCacheLocation, "npm", safeList.path, throttleLimit);
|
||||
this.init();
|
||||
}
|
||||
|
||||
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);
|
||||
executePendingCommands() {
|
||||
const actionsToRun = this.postExecActions;
|
||||
this.postExecActions = [];
|
||||
for (const action of actionsToRun) {
|
||||
action.callback(action.error, action.stdout, action.stderr);
|
||||
}
|
||||
this.postInstallActions = [];
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
onProjectClosed(p: server.Project) {
|
||||
|
@ -67,14 +83,15 @@ 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));
|
||||
});
|
||||
runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: (err: Error, stdout: string, stderr: string) => void): void {
|
||||
switch (requestKind) {
|
||||
case TI.NpmViewRequest:
|
||||
case TI.NpmInstallRequest:
|
||||
break;
|
||||
default:
|
||||
assert.isTrue(false, `request ${requestKind} is not supported`);
|
||||
}
|
||||
this.addPostExecAction(requestKind, "success", cb);
|
||||
}
|
||||
|
||||
sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) {
|
||||
|
@ -85,6 +102,26 @@ namespace ts.projectSystem {
|
|||
const request = server.createInstallTypingsRequest(project, typingOptions, this.globalTypingsCacheLocation);
|
||||
this.install(request);
|
||||
}
|
||||
|
||||
addPostExecAction(requestKind: TI.RequestKind, stdout: string | string[], cb: TI.RequestCompletedAction) {
|
||||
const out = typeof stdout === "string" ? stdout : createNpmPackageJsonString(stdout);
|
||||
const action: PostExecAction = {
|
||||
error: undefined,
|
||||
stdout: out,
|
||||
stderr: "",
|
||||
callback: cb,
|
||||
requestKind
|
||||
};
|
||||
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 {
|
||||
|
@ -124,7 +161,7 @@ namespace ts.projectSystem {
|
|||
|
||||
export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller) {
|
||||
if (typingsInstaller === undefined) {
|
||||
typingsInstaller = new TestTypingsInstaller("/a/data/", host);
|
||||
typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host);
|
||||
}
|
||||
return new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
|
||||
}
|
||||
|
@ -1615,7 +1652,7 @@ namespace ts.projectSystem {
|
|||
const host: TestServerHost & ModuleResolutionHost = createServerHost([file1, lib]);
|
||||
const resolutionTrace: string[] = [];
|
||||
host.trace = resolutionTrace.push.bind(resolutionTrace);
|
||||
const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller("/a/cache", host) });
|
||||
const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller("/a/cache", /*throttleLimit*/5, host) });
|
||||
|
||||
projectService.setCompilerOptionsForInferredProjects({ traceResolution: true, allowJs: true });
|
||||
projectService.openClientFile(file1.path);
|
||||
|
|
|
@ -1,9 +1,50 @@
|
|||
/// <reference path="../harness.ts" />
|
||||
/// <reference path="../harness.ts" />
|
||||
/// <reference path="./tsserverProjectSystem.ts" />
|
||||
/// <reference path="../../server/typingsInstaller/typingsInstaller.ts" />
|
||||
|
||||
namespace ts.projectSystem {
|
||||
describe("typings installer", () => {
|
||||
import TI = server.typingsInstaller;
|
||||
|
||||
interface InstallerParams {
|
||||
globalTypingsCacheLocation?: string;
|
||||
throttleLimit?: number;
|
||||
}
|
||||
|
||||
class Installer extends TestTypingsInstaller {
|
||||
constructor(host: server.ServerHost, p?: InstallerParams) {
|
||||
super(
|
||||
(p && p.globalTypingsCacheLocation) || "/a/data",
|
||||
(p && p.throttleLimit) || 5,
|
||||
host);
|
||||
}
|
||||
|
||||
installAll(expectedView: typeof TI.NpmViewRequest[], expectedInstall: typeof TI.NpmInstallRequest[]) {
|
||||
this.checkPendingCommands(expectedView);
|
||||
this.executePendingCommands();
|
||||
this.checkPendingCommands(expectedInstall);
|
||||
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, (err, stdout, stderr) => {
|
||||
for (const file of typingFiles) {
|
||||
host.createFileOrFolder(file, /*createParentDirectory*/ true);
|
||||
}
|
||||
cb(err, stdout, stderr);
|
||||
});
|
||||
break;
|
||||
case TI.NpmViewRequest:
|
||||
self.addPostExecAction(requestKind, installedTypings, cb);
|
||||
break;
|
||||
default:
|
||||
assert.isTrue(false, `unexpected request kind ${requestKind}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
it("configured projects (typings installed) 1", () => {
|
||||
const file1 = {
|
||||
path: "/a/b/app.js",
|
||||
|
@ -34,9 +75,18 @@ 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 Installer {
|
||||
constructor() {
|
||||
super(host);
|
||||
}
|
||||
runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
|
||||
const installedTypings = ["@types/jquery"];
|
||||
const typingFiles = [jquery];
|
||||
executeCommand(this, host, installedTypings, typingFiles, requestKind, cb);
|
||||
}
|
||||
})();
|
||||
|
||||
const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
|
||||
projectService.openClientFile(file1.path);
|
||||
|
||||
|
@ -44,13 +94,8 @@ namespace ts.projectSystem {
|
|||
const p = projectService.configuredProjects[0];
|
||||
checkProjectActualFiles(p, [file1.path]);
|
||||
|
||||
assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "package.json")));
|
||||
installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]);
|
||||
|
||||
installer.runPostInstallActions(t => {
|
||||
assert.deepEqual(t, ["jquery"]);
|
||||
host.createFileOrFolder(jquery, /*createParentDirectory*/ true);
|
||||
return ["@types/jquery"];
|
||||
});
|
||||
checkNumberOfProjects(projectService, { configuredProjects: 1 });
|
||||
checkProjectActualFiles(p, [file1.path, jquery.path]);
|
||||
});
|
||||
|
@ -75,7 +120,16 @@ 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 Installer {
|
||||
constructor() {
|
||||
super(host);
|
||||
}
|
||||
runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
|
||||
const installedTypings = ["@types/jquery"];
|
||||
const typingFiles = [jquery];
|
||||
executeCommand(this, host, installedTypings, typingFiles, requestKind, cb);
|
||||
}
|
||||
})();
|
||||
|
||||
const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
|
||||
projectService.openClientFile(file1.path);
|
||||
|
@ -84,13 +138,8 @@ namespace ts.projectSystem {
|
|||
const p = projectService.inferredProjects[0];
|
||||
checkProjectActualFiles(p, [file1.path]);
|
||||
|
||||
assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "package.json")));
|
||||
installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]);
|
||||
|
||||
installer.runPostInstallActions(t => {
|
||||
assert.deepEqual(t, ["jquery"]);
|
||||
host.createFileOrFolder(jquery, /*createParentDirectory*/ true);
|
||||
return ["@types/jquery"];
|
||||
});
|
||||
checkNumberOfProjects(projectService, { inferredProjects: 1 });
|
||||
checkProjectActualFiles(p, [file1.path, jquery.path]);
|
||||
});
|
||||
|
@ -101,9 +150,9 @@ namespace ts.projectSystem {
|
|||
content: ""
|
||||
};
|
||||
const host = createServerHost([file1]);
|
||||
const installer = new (class extends TestTypingsInstaller {
|
||||
const installer = new (class extends Installer {
|
||||
constructor() {
|
||||
super("", host);
|
||||
super(host);
|
||||
}
|
||||
enqueueInstallTypingsRequest() {
|
||||
assert(false, "auto discovery should not be enabled");
|
||||
|
@ -117,6 +166,7 @@ namespace ts.projectSystem {
|
|||
options: {},
|
||||
rootFiles: [toExternalFile(file1.path)]
|
||||
});
|
||||
installer.checkPendingCommands([]);
|
||||
// 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 });
|
||||
|
@ -128,9 +178,9 @@ namespace ts.projectSystem {
|
|||
content: ""
|
||||
};
|
||||
const host = createServerHost([file1]);
|
||||
const installer = new (class extends TestTypingsInstaller {
|
||||
const installer = new (class extends Installer {
|
||||
constructor() {
|
||||
super("", host);
|
||||
super(host);
|
||||
}
|
||||
enqueueInstallTypingsRequest() {
|
||||
assert(false, "auto discovery should not be enabled");
|
||||
|
@ -145,6 +195,7 @@ namespace ts.projectSystem {
|
|||
rootFiles: [toExternalFile(file1.path)],
|
||||
typingOptions: { include: ["jquery"] }
|
||||
});
|
||||
installer.checkPendingCommands([]);
|
||||
// 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 });
|
||||
|
@ -155,21 +206,24 @@ 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;
|
||||
const installer = new (class extends TestTypingsInstaller {
|
||||
const installer = new (class extends Installer {
|
||||
constructor() {
|
||||
super("", host);
|
||||
super(host);
|
||||
}
|
||||
enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions) {
|
||||
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);
|
||||
runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: TI.RequestCompletedAction): void {
|
||||
const installedTypings = ["@types/jquery"];
|
||||
const typingFiles = [jquery];
|
||||
executeCommand(this, host, installedTypings, typingFiles, requestKind, cb);
|
||||
}
|
||||
})();
|
||||
|
||||
|
@ -181,10 +235,12 @@ namespace ts.projectSystem {
|
|||
rootFiles: [toExternalFile(file1.path)],
|
||||
typingOptions: { enableAutoDiscovery: true, include: ["node"] }
|
||||
});
|
||||
|
||||
assert.isTrue(enqueueIsCalled, "expected enqueueIsCalled to be true");
|
||||
installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]);
|
||||
|
||||
// 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");
|
||||
assert.isTrue(runInstallIsCalled, "expected 'runInstallIsCalled' to be true");
|
||||
});
|
||||
|
||||
it("external project - no typing options, with only js, jsx, d.ts files", () => {
|
||||
|
@ -214,7 +270,16 @@ namespace ts.projectSystem {
|
|||
};
|
||||
|
||||
const host = createServerHost([file1, file2, file3]);
|
||||
const installer = new TestTypingsInstaller("/a/data/", host);
|
||||
const installer = new (class extends Installer {
|
||||
constructor() {
|
||||
super(host);
|
||||
}
|
||||
runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: TI.RequestCompletedAction): void {
|
||||
const installedTypings = ["@types/lodash", "@types/react"];
|
||||
const typingFiles = [lodash, react];
|
||||
executeCommand(this, host, installedTypings, typingFiles, requestKind, cb);
|
||||
}
|
||||
})();
|
||||
|
||||
const projectFileName = "/a/app/test.csproj";
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
|
@ -229,12 +294,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.installAll([TI.NpmViewRequest, TI.NpmViewRequest], [TI.NpmInstallRequest], );
|
||||
|
||||
checkNumberOfProjects(projectService, { externalProjects: 1 });
|
||||
checkProjectActualFiles(p, [file1.path, file2.path, file3.path, lodash.path, react.path]);
|
||||
|
@ -254,19 +314,18 @@ namespace ts.projectSystem {
|
|||
|
||||
const host = createServerHost([file1, file2]);
|
||||
let enqueueIsCalled = false;
|
||||
let runInstallIsCalled = false;
|
||||
let runPostInstallIsCalled = false;
|
||||
const installer = new (class extends TestTypingsInstaller {
|
||||
const installer = new (class extends Installer {
|
||||
constructor() {
|
||||
super("/a/data/", host);
|
||||
super(host);
|
||||
}
|
||||
enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions) {
|
||||
enqueueIsCalled = true;
|
||||
super.enqueueInstallTypingsRequest(project, typingOptions);
|
||||
}
|
||||
runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void {
|
||||
runInstallIsCalled = true;
|
||||
super.runInstall(cachePath, typingsToInstall, postInstallAction);
|
||||
runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: TI.RequestCompletedAction): void {
|
||||
const installedTypings: string[] = [];
|
||||
const typingFiles: FileOrFolder[] = [];
|
||||
executeCommand(this, host, installedTypings, typingFiles, requestKind, cb);
|
||||
}
|
||||
})();
|
||||
|
||||
|
@ -283,16 +342,10 @@ namespace ts.projectSystem {
|
|||
projectService.checkNumberOfProjects({ externalProjects: 1 });
|
||||
checkProjectActualFiles(p, [file1.path, file2.path]);
|
||||
|
||||
installer.runPostInstallActions(t => {
|
||||
runPostInstallIsCalled = true;
|
||||
return [];
|
||||
});
|
||||
installer.checkPendingCommands([]);
|
||||
|
||||
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 +393,16 @@ namespace ts.projectSystem {
|
|||
};
|
||||
|
||||
const host = createServerHost([file1, file2, file3, packageJson]);
|
||||
const installer = new TestTypingsInstaller("/a/data/", host);
|
||||
const installer = new (class extends Installer {
|
||||
constructor() {
|
||||
super(host);
|
||||
}
|
||||
runCommand(requestKind: TI.RequestKind, requestId: number, command: 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);
|
||||
}
|
||||
})();
|
||||
|
||||
const projectFileName = "/a/app/test.csproj";
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
|
@ -355,17 +417,215 @@ 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.installAll(
|
||||
[TI.NpmViewRequest, TI.NpmViewRequest, TI.NpmViewRequest, TI.NpmViewRequest],
|
||||
[TI.NpmInstallRequest]
|
||||
);
|
||||
|
||||
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 lodashJs = {
|
||||
path: "/a/b/lodash.js",
|
||||
content: ""
|
||||
};
|
||||
const commanderJs = {
|
||||
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"
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
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 typingFiles = [commander, express, jquery, moment, lodash];
|
||||
const host = createServerHost([lodashJs, commanderJs, file3, packageJson]);
|
||||
const installer = new (class extends Installer {
|
||||
constructor() {
|
||||
super(host, { throttleLimit: 3 });
|
||||
}
|
||||
runCommand(requestKind: TI.RequestKind, requestId: number, command: 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);
|
||||
}
|
||||
})();
|
||||
|
||||
const projectFileName = "/a/app/test.csproj";
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
projectService.openExternalProject({
|
||||
projectFileName,
|
||||
options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs },
|
||||
rootFiles: [toExternalFile(lodashJs.path), toExternalFile(commanderJs.path), toExternalFile(file3.path)],
|
||||
typingOptions: { include: ["jquery", "moment"] }
|
||||
});
|
||||
|
||||
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.executePendingCommands();
|
||||
// expected all typings file to exist
|
||||
for (const f of typingFiles) {
|
||||
assert.isTrue(host.fileExists(f.path), `expected file ${f.path} to exist`);
|
||||
}
|
||||
|
||||
checkNumberOfProjects(projectService, { externalProjects: 1 });
|
||||
checkProjectActualFiles(p, [lodashJs.path, commanderJs.path, file3.path, commander.path, express.path, jquery.path, moment.path, lodash.path]);
|
||||
});
|
||||
|
||||
it("Throttle - delayed run install requests", () => {
|
||||
const lodashJs = {
|
||||
path: "/a/b/lodash.js",
|
||||
content: ""
|
||||
};
|
||||
const commanderJs = {
|
||||
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 }",
|
||||
typings: "@types/commander"
|
||||
};
|
||||
const jquery = {
|
||||
path: "/a/data/node_modules/@types/jquery/index.d.ts",
|
||||
content: "declare const jquery: { x: number }",
|
||||
typings: "@types/jquery"
|
||||
};
|
||||
const lodash = {
|
||||
path: "/a/data/node_modules/@types/lodash/index.d.ts",
|
||||
content: "declare const lodash: { x: number }",
|
||||
typings: "@types/lodash"
|
||||
};
|
||||
const cordova = {
|
||||
path: "/a/data/node_modules/@types/cordova/index.d.ts",
|
||||
content: "declare const cordova: { x: number }",
|
||||
typings: "@types/cordova"
|
||||
};
|
||||
const grunt = {
|
||||
path: "/a/data/node_modules/@types/grunt/index.d.ts",
|
||||
content: "declare const grunt: { x: number }",
|
||||
typings: "@types/grunt"
|
||||
};
|
||||
const gulp = {
|
||||
path: "/a/data/node_modules/@types/gulp/index.d.ts",
|
||||
content: "declare const gulp: { x: number }",
|
||||
typings: "@types/gulp"
|
||||
};
|
||||
|
||||
const host = createServerHost([lodashJs, commanderJs, file3]);
|
||||
const installer = new (class extends Installer {
|
||||
constructor() {
|
||||
super(host, { throttleLimit: 3 });
|
||||
}
|
||||
runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: TI.RequestCompletedAction): void {
|
||||
if (requestKind === TI.NpmInstallRequest) {
|
||||
let typingFiles: (FileOrFolder & { typings: string}) [] = [];
|
||||
if (command.indexOf("commander") >= 0) {
|
||||
typingFiles = [commander, jquery, lodash, cordova];
|
||||
}
|
||||
else {
|
||||
typingFiles = [grunt, gulp];
|
||||
}
|
||||
executeCommand(this, host, typingFiles.map(f => f.typings), typingFiles, requestKind, cb);
|
||||
}
|
||||
else {
|
||||
executeCommand(this, host, [], [], requestKind, cb);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Create project #1 with 4 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(lodashJs.path), toExternalFile(commanderJs.path), toExternalFile(file3.path)],
|
||||
typingOptions: { include: ["jquery", "cordova" ] }
|
||||
});
|
||||
|
||||
installer.checkPendingCommands([TI.NpmViewRequest, TI.NpmViewRequest, TI.NpmViewRequest]);
|
||||
assert.equal(installer.pendingRunRequests.length, 1, "expect one throttled request");
|
||||
|
||||
// Create project #2 with 2 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"] }
|
||||
});
|
||||
assert.equal(installer.pendingRunRequests.length, 3, "expect three throttled request");
|
||||
|
||||
const p1 = projectService.externalProjects[0];
|
||||
const p2 = projectService.externalProjects[1];
|
||||
projectService.checkNumberOfProjects({ externalProjects: 2 });
|
||||
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();
|
||||
|
||||
// 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]);
|
||||
checkProjectActualFiles(p2, [file3.path, grunt.path, gulp.path ]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,12 +1,16 @@
|
|||
/// <reference path="typingsInstaller.ts"/>
|
||||
/// <reference path="typingsInstaller.ts"/>
|
||||
/// <reference types="node" />
|
||||
|
||||
namespace ts.server.typingsInstaller {
|
||||
|
||||
const fs: {
|
||||
appendFileSync(file: string, content: string): void
|
||||
} = require("fs");
|
||||
|
||||
const path: {
|
||||
join(...parts: string[]): string;
|
||||
dirname(path: string): string;
|
||||
} = require("path");
|
||||
|
||||
class FileLog implements Log {
|
||||
constructor(private readonly logFile?: string) {
|
||||
}
|
||||
|
@ -20,37 +24,26 @@ 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 readonly exec: { (command: string, options: { cwd: string }, callback?: (error: Error, stdout: string, stderr: string) => void): any };
|
||||
|
||||
private installRunCount = 1;
|
||||
readonly installTypingHost: InstallTypingHost = sys;
|
||||
|
||||
constructor(globalTypingsCacheLocation: string, log: Log) {
|
||||
super(globalTypingsCacheLocation, toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), log);
|
||||
constructor(globalTypingsCacheLocation: string, throttleLimit: number, log: Log) {
|
||||
super(
|
||||
globalTypingsCacheLocation,
|
||||
/*npmPath*/ `"${path.join(path.dirname(process.argv[0]), "npm")}"`,
|
||||
toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)),
|
||||
throttleLimit,
|
||||
log);
|
||||
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":
|
||||
|
@ -62,23 +55,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)}`);
|
||||
|
@ -89,61 +65,16 @@ namespace ts.server.typingsInstaller {
|
|||
}
|
||||
}
|
||||
|
||||
protected runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void {
|
||||
const id = this.installRunCount;
|
||||
this.installRunCount++;
|
||||
let execInstallCmdCount = 0;
|
||||
const filteredTypings: string[] = [];
|
||||
for (const typing of typingsToInstall) {
|
||||
const command = `npm view @types/${typing} --silent name`;
|
||||
this.execAsync("npm view", command, cachePath, id, (err, stdout, stderr) => {
|
||||
if (stdout) {
|
||||
filteredTypings.push(typing);
|
||||
}
|
||||
execInstallCmdCount++;
|
||||
if (execInstallCmdCount === typingsToInstall.length) {
|
||||
installFilteredTypings(this, filteredTypings);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function installFilteredTypings(self: NodeTypingsInstaller, filteredTypings: string[]) {
|
||||
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 runCommand(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void {
|
||||
if (this.log.isEnabled()) {
|
||||
this.log.writeLine(`#${requestId} running command '${command}'.`);
|
||||
}
|
||||
this.exec(command, { cwd }, (err, stdout, stderr) => {
|
||||
if (this.log.isEnabled()) {
|
||||
this.log.writeLine(`${prefix} #${requestId} stdout: ${stdout}`);
|
||||
this.log.writeLine(`${prefix} #${requestId} stderr: ${stderr}`);
|
||||
this.log.writeLine(`${requestKind} #${requestId} stdout: ${stdout}`);
|
||||
this.log.writeLine(`${requestKind} #${requestId} stderr: ${stderr}`);
|
||||
}
|
||||
cb(err, stdout, stderr);
|
||||
onRequestCompleted(err, stdout, stderr);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -169,6 +100,6 @@ namespace ts.server.typingsInstaller {
|
|||
}
|
||||
process.exit(0);
|
||||
});
|
||||
const installer = new NodeTypingsInstaller(globalTypingsCacheLocation, log);
|
||||
const installer = new NodeTypingsInstaller(globalTypingsCacheLocation, /*throttleLimit*/5, log);
|
||||
installer.init();
|
||||
}
|
|
@ -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"/>
|
||||
|
@ -23,15 +23,38 @@ namespace ts.server.typingsInstaller {
|
|||
return result.resolvedModule && result.resolvedModule.resolvedFileName;
|
||||
}
|
||||
|
||||
export const NpmViewRequest: "npm view" = "npm view";
|
||||
export const NpmInstallRequest: "npm install" = "npm install";
|
||||
|
||||
export type RequestKind = typeof NpmViewRequest | typeof NpmInstallRequest;
|
||||
|
||||
export type RequestCompletedAction = (err: Error, stdout: string, stderr: string) => void;
|
||||
type PendingRequest = {
|
||||
requestKind: RequestKind;
|
||||
requestId: number;
|
||||
command: string;
|
||||
cwd: string;
|
||||
onRequestCompleted: RequestCompletedAction
|
||||
};
|
||||
|
||||
export abstract class TypingsInstaller {
|
||||
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 readonly packageNameToTypingLocation: Map<string> = createMap<string>();
|
||||
private readonly missingTypingsSet: Map<true> = createMap<true>();
|
||||
private readonly knownCachesSet: Map<true> = createMap<true>();
|
||||
private readonly projectWatchers: Map<FileWatcher[]> = createMap<FileWatcher[]>();
|
||||
readonly pendingRunRequests: PendingRequest[] = [];
|
||||
|
||||
private installRunCount = 1;
|
||||
private inFlightRequestCount = 0;
|
||||
|
||||
abstract readonly installTypingHost: InstallTypingHost;
|
||||
|
||||
constructor(readonly globalCachePath: string, readonly safeListPath: Path, protected readonly log = nullLog) {
|
||||
constructor(
|
||||
readonly globalCachePath: string,
|
||||
readonly npmPath: string,
|
||||
readonly safeListPath: Path,
|
||||
readonly throttleLimit: number,
|
||||
protected readonly log = nullLog) {
|
||||
if (this.log.isEnabled()) {
|
||||
this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}'`);
|
||||
}
|
||||
|
@ -224,6 +247,42 @@ namespace ts.server.typingsInstaller {
|
|||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
execNpmViewTyping(this, typing);
|
||||
}
|
||||
|
||||
function execNpmViewTyping(self: TypingsInstaller, typing: string) {
|
||||
const command = `${self.npmPath} view @types/${typing} --silent name`;
|
||||
self.execAsync(NpmViewRequest, requestId, command, cachePath, (err, stdout, stderr) => {
|
||||
if (stdout) {
|
||||
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);
|
||||
const command = `${self.npmPath} install ${scopedTypings.join(" ")} --save-dev`;
|
||||
self.execAsync(NpmInstallRequest, requestId, command, cachePath, (err, stdout, stderr) => {
|
||||
postInstallAction(stdout ? scopedTypings : []);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private ensureDirectoryExists(directory: string, host: InstallTypingHost): void {
|
||||
const directoryName = getDirectoryPath(directory);
|
||||
if (!host.directoryExists(directoryName)) {
|
||||
|
@ -267,8 +326,24 @@ namespace ts.server.typingsInstaller {
|
|||
};
|
||||
}
|
||||
|
||||
protected abstract isPackageInstalled(packageName: string): boolean;
|
||||
private execAsync(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void {
|
||||
this.pendingRunRequests.unshift({ requestKind, requestId, command, cwd, onRequestCompleted });
|
||||
this.executeWithThrottling();
|
||||
}
|
||||
|
||||
private executeWithThrottling() {
|
||||
while (this.inFlightRequestCount < this.throttleLimit && this.pendingRunRequests.length) {
|
||||
this.inFlightRequestCount++;
|
||||
const request = this.pendingRunRequests.pop();
|
||||
this.runCommand(request.requestKind, request.requestId, request.command, request.cwd, (err, stdout, stderr) => {
|
||||
this.inFlightRequestCount--;
|
||||
request.onRequestCompleted(err, stdout, stderr);
|
||||
this.executeWithThrottling();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract runCommand(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void;
|
||||
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void;
|
||||
protected abstract runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue