From 69f9b6b9bb17a8b01483565e2139039e936931ff Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Thu, 3 Oct 2019 13:00:59 -0700 Subject: [PATCH 1/4] Use @definitelytyped/types-registry for ATA Fall back to types-registry if `npm install @definitelytyped/types-registry` doesn't work. Notes: 1. This currently requires you to be authenticated with npm. After the Github package registry leaves beta, that will no longer be true. 2. The error handling was incorrect -- it double-handled an exception and ignored the return code of execSyncAndLog. I corrected it so that it logs if either npm or GHPR fails. 3. I haven't added tests for this change yet. I see some references to types-registry in the tests so I'll see if that's the right place. --- src/typingsInstaller/nodeTypingsInstaller.ts | 54 +++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/typingsInstaller/nodeTypingsInstaller.ts b/src/typingsInstaller/nodeTypingsInstaller.ts index 56b490e709d54..14272a7f4be9e 100644 --- a/src/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/typingsInstaller/nodeTypingsInstaller.ts @@ -66,8 +66,9 @@ namespace ts.server.typingsInstaller { } const typesRegistryPackageName = "types-registry"; - function getTypesRegistryFileLocation(globalTypingsCacheLocation: string): string { - return combinePaths(normalizeSlashes(globalTypingsCacheLocation), `node_modules/${typesRegistryPackageName}/index.json`); + const definitelyTypedTypesRegistryPackageName = "@definitelytyped/types-registry"; + function getTypesRegistryFileLocation(globalTypingsCacheLocation: string, packageName: string): string { + return combinePaths(normalizeSlashes(globalTypingsCacheLocation), `node_modules/${packageName}/index.json`); } interface ExecSyncOptions { @@ -105,28 +106,45 @@ namespace ts.server.typingsInstaller { ({ execSync: this.nodeExecSync } = require("child_process")); this.ensurePackageDirectoryExists(globalTypingsCacheLocation); + const packageName = this.installTypesRegistry(globalTypingsCacheLocation); + this.typesRegistry = loadTypesRegistryFile(getTypesRegistryFileLocation(globalTypingsCacheLocation, packageName), this.installTypingHost, this.log); + } - try { - if (this.log.isEnabled()) { - this.log.writeLine(`Updating ${typesRegistryPackageName} npm package...`); - } - this.execSyncAndLog(`${this.npmPath} install --ignore-scripts ${typesRegistryPackageName}@${this.latestDistTag}`, { cwd: globalTypingsCacheLocation }); - if (this.log.isEnabled()) { - this.log.writeLine(`Updated ${typesRegistryPackageName} npm package`); + private installTypesRegistry(globalTypingsCacheLocation: string) { + let result: typeof typesRegistryPackageName | typeof definitelyTypedTypesRegistryPackageName | "UPDATE FAILED" = + this.installTypesRegistryFromPackageName(definitelyTypedTypesRegistryPackageName, globalTypingsCacheLocation); + if (result === "UPDATE FAILED") { + result = this.installTypesRegistryFromPackageName(typesRegistryPackageName, globalTypingsCacheLocation); + if (result === "UPDATE FAILED") { + if (this.log.isEnabled()) { + this.log.writeLine(`Error updating ${typesRegistryPackageName} package`); + } + // store error info to report it later when it is known that server is already listening to events from typings installer + this.delayedInitializationError = { + kind: "event::initializationFailed", + message: result + }; + return "types-registry"; } } - catch (e) { + return result; + } + + private installTypesRegistryFromPackageName(packageName: typeof typesRegistryPackageName | typeof definitelyTypedTypesRegistryPackageName, globalTypingsCacheLocation: string) { + if (this.log.isEnabled()) { + this.log.writeLine(`Updating ${packageName} npm package...`); + } + const registry = packageName === typesRegistryPackageName ? "" : "--registry=https://npm.pkg.github.com"; + const failed = this.execSyncAndLog(`${this.npmPath} install ${registry} --ignore-scripts ${packageName}@${this.latestDistTag}`, { cwd: globalTypingsCacheLocation }); + if (failed) { + return "UPDATE FAILED"; + } + else { if (this.log.isEnabled()) { - this.log.writeLine(`Error updating ${typesRegistryPackageName} package: ${(e).message}`); + this.log.writeLine(`Updated ${packageName} npm package`); } - // store error info to report it later when it is known that server is already listening to events from typings installer - this.delayedInitializationError = { - kind: "event::initializationFailed", - message: (e).message - }; + return packageName; } - - this.typesRegistry = loadTypesRegistryFile(getTypesRegistryFileLocation(globalTypingsCacheLocation), this.installTypingHost, this.log); } listen() { From 71e0a6d1b89ebe619c65b4d8f9d85e4bf7046bf8 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Mon, 14 Oct 2019 09:31:11 -0700 Subject: [PATCH 2/4] Make nodeTypingsInstaller testable --- .../unittests/tsserver/typingsInstaller.ts | 72 ++++- src/typingsInstaller/nodeTypingsInstaller.ts | 252 +++--------------- .../nodeTypingsInstallerCore.ts | 200 ++++++++++++++ src/typingsInstallerCore/tsconfig.json | 3 +- src/typingsInstallerCore/typingsInstaller.ts | 9 +- 5 files changed, 304 insertions(+), 232 deletions(-) create mode 100644 src/typingsInstallerCore/nodeTypingsInstallerCore.ts diff --git a/src/testRunner/unittests/tsserver/typingsInstaller.ts b/src/testRunner/unittests/tsserver/typingsInstaller.ts index 1b7f156057df3..3edbd00a865bf 100644 --- a/src/testRunner/unittests/tsserver/typingsInstaller.ts +++ b/src/testRunner/unittests/tsserver/typingsInstaller.ts @@ -1724,21 +1724,21 @@ namespace ts.projectSystem { ]; it("works when the command is too long to install all packages at once", () => { const commands: string[] = []; - const hasError = TI.installNpmPackages(npmPath, tsVersion, packageNames, command => { + const succeeded = TI.installNpmPackages(npmPath, tsVersion, packageNames, command => { commands.push(command); - return false; + return true; }); - assert.isFalse(hasError); + assert.isTrue(succeeded); assert.deepEqual(commands, expectedCommands, "commands"); }); it("installs remaining packages when one of the partial command fails", () => { const commands: string[] = []; - const hasError = TI.installNpmPackages(npmPath, tsVersion, packageNames, command => { + const succeeded = TI.installNpmPackages(npmPath, tsVersion, packageNames, command => { commands.push(command); - return commands.length === 1; + return commands.length === 0; }); - assert.isTrue(hasError); + assert.isFalse(succeeded); assert.deepEqual(commands, expectedCommands, "commands"); }); }); @@ -1955,4 +1955,64 @@ declare module "stream" { checkProjectActualFiles(project, [file.path]); }); }); + + describe("unittests:: tsserver:: nodeTypingsInstaller", () => { + function createHost() { + const log = { + out: "", + isEnabled: () => true, + writeLine(text: string) { + this.out += text + sys.newLine; + }, + }; + return { + log, + useCaseSensitiveFileNames: true, + execSyncAndLog(command: string, options: string): boolean { + log.writeLine(`Running ${command} ${options}`); + return true; + }, + writeFile(): void { }, + createDirectory(): void { }, + + directoryExists(): boolean { + return true; + }, + fileExists(): boolean { + return true; + }, + readFile(): string | undefined { + return '{ "yep": true }'; + }, + readDirectory(): string[] { + return []; + }, + }; + } + it("constructs successfully from @definitelytyped", () => { + const host = createHost(); + const i = new TI.NodeTypingsInstaller(host, "a", "b", "c", "d", true, 1, host.log); + assert.isUndefined((i as any).delayedInitializationError); + assert.include(host.log.out, "Updated @definitelytyped/types-registry"); + }); + it("constructs successfully from npm (falling back from @definitelytyped)", () => { + const host = createHost(); + host.execSyncAndLog = function(command) { + return !command.includes("@definitelytyped"); + } + const i = new TI.NodeTypingsInstaller(host, "a", "b", "c", "d", true, 1, host.log); + assert.isUndefined((i as any).delayedInitializationError); + assert.notInclude(host.log.out, "Updated @definitelytyped/types-registry"); + assert.include(host.log.out, "Updated types-registry"); + }); + it("fails construction", () => { + const host = createHost(); + host.execSyncAndLog = function() { + return false; + } + const i = new TI.NodeTypingsInstaller(host, "a", "b", "c", "d", true, 1, host.log); + assert.equal("event::initializationFailed", (i as any).delayedInitializationError.kind); + assert.equal("UPDATE FAILED", (i as any).delayedInitializationError.message); + }); + }); } diff --git a/src/typingsInstaller/nodeTypingsInstaller.ts b/src/typingsInstaller/nodeTypingsInstaller.ts index 14272a7f4be9e..5de0420c03fd7 100644 --- a/src/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/typingsInstaller/nodeTypingsInstaller.ts @@ -3,12 +3,6 @@ namespace ts.server.typingsInstaller { appendFileSync(file: string, content: string): void } = require("fs"); - const path: { - join(...parts: string[]): string; - dirname(path: string): string; - basename(path: string, extension?: string): string; - } = require("path"); - class FileLog implements Log { constructor(private logFile: string | undefined) { } @@ -28,220 +22,6 @@ namespace ts.server.typingsInstaller { }; } - /** Used if `--npmLocation` is not passed. */ - function getDefaultNPMLocation(processName: string, validateDefaultNpmLocation: boolean, host: InstallTypingHost): string { - if (path.basename(processName).indexOf("node") === 0) { - const npmPath = path.join(path.dirname(process.argv[0]), "npm"); - if (!validateDefaultNpmLocation) { - return npmPath; - } - if (host.fileExists(npmPath)) { - return `"${npmPath}"`; - } - } - return "npm"; - } - - interface TypesRegistryFile { - entries: MapLike>; - } - - function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map> { - if (!host.fileExists(typesRegistryFilePath)) { - if (log.isEnabled()) { - log.writeLine(`Types registry file '${typesRegistryFilePath}' does not exist`); - } - return createMap>(); - } - try { - const content = JSON.parse(host.readFile(typesRegistryFilePath)!); - return createMapFromTemplate(content.entries); - } - catch (e) { - if (log.isEnabled()) { - log.writeLine(`Error when loading types registry file '${typesRegistryFilePath}': ${(e).message}, ${(e).stack}`); - } - return createMap>(); - } - } - - const typesRegistryPackageName = "types-registry"; - const definitelyTypedTypesRegistryPackageName = "@definitelytyped/types-registry"; - function getTypesRegistryFileLocation(globalTypingsCacheLocation: string, packageName: string): string { - return combinePaths(normalizeSlashes(globalTypingsCacheLocation), `node_modules/${packageName}/index.json`); - } - - interface ExecSyncOptions { - cwd: string; - encoding: "utf-8"; - } - type ExecSync = (command: string, options: ExecSyncOptions) => string; - - export class NodeTypingsInstaller extends TypingsInstaller { - private readonly nodeExecSync: ExecSync; - private readonly npmPath: string; - readonly typesRegistry: Map>; - - private delayedInitializationError: InitializationFailedResponse | undefined; - - constructor(globalTypingsCacheLocation: string, typingSafeListLocation: string, typesMapLocation: string, npmLocation: string | undefined, validateDefaultNpmLocation: boolean, throttleLimit: number, log: Log) { - super( - sys, - globalTypingsCacheLocation, - typingSafeListLocation ? toPath(typingSafeListLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), - typesMapLocation ? toPath(typesMapLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typesMap.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), - throttleLimit, - log); - this.npmPath = npmLocation !== undefined ? npmLocation : getDefaultNPMLocation(process.argv[0], validateDefaultNpmLocation, this.installTypingHost); - - // If the NPM path contains spaces and isn't wrapped in quotes, do so. - if (stringContains(this.npmPath, " ") && this.npmPath[0] !== `"`) { - this.npmPath = `"${this.npmPath}"`; - } - if (this.log.isEnabled()) { - this.log.writeLine(`Process id: ${process.pid}`); - this.log.writeLine(`NPM location: ${this.npmPath} (explicit '${Arguments.NpmLocation}' ${npmLocation === undefined ? "not " : ""} provided)`); - this.log.writeLine(`validateDefaultNpmLocation: ${validateDefaultNpmLocation}`); - } - ({ execSync: this.nodeExecSync } = require("child_process")); - - this.ensurePackageDirectoryExists(globalTypingsCacheLocation); - const packageName = this.installTypesRegistry(globalTypingsCacheLocation); - this.typesRegistry = loadTypesRegistryFile(getTypesRegistryFileLocation(globalTypingsCacheLocation, packageName), this.installTypingHost, this.log); - } - - private installTypesRegistry(globalTypingsCacheLocation: string) { - let result: typeof typesRegistryPackageName | typeof definitelyTypedTypesRegistryPackageName | "UPDATE FAILED" = - this.installTypesRegistryFromPackageName(definitelyTypedTypesRegistryPackageName, globalTypingsCacheLocation); - if (result === "UPDATE FAILED") { - result = this.installTypesRegistryFromPackageName(typesRegistryPackageName, globalTypingsCacheLocation); - if (result === "UPDATE FAILED") { - if (this.log.isEnabled()) { - this.log.writeLine(`Error updating ${typesRegistryPackageName} package`); - } - // store error info to report it later when it is known that server is already listening to events from typings installer - this.delayedInitializationError = { - kind: "event::initializationFailed", - message: result - }; - return "types-registry"; - } - } - return result; - } - - private installTypesRegistryFromPackageName(packageName: typeof typesRegistryPackageName | typeof definitelyTypedTypesRegistryPackageName, globalTypingsCacheLocation: string) { - if (this.log.isEnabled()) { - this.log.writeLine(`Updating ${packageName} npm package...`); - } - const registry = packageName === typesRegistryPackageName ? "" : "--registry=https://npm.pkg.github.com"; - const failed = this.execSyncAndLog(`${this.npmPath} install ${registry} --ignore-scripts ${packageName}@${this.latestDistTag}`, { cwd: globalTypingsCacheLocation }); - if (failed) { - return "UPDATE FAILED"; - } - else { - if (this.log.isEnabled()) { - this.log.writeLine(`Updated ${packageName} npm package`); - } - return packageName; - } - } - - listen() { - process.on("message", (req: TypingInstallerRequestUnion) => { - if (this.delayedInitializationError) { - // report initializationFailed error - this.sendResponse(this.delayedInitializationError); - this.delayedInitializationError = undefined; - } - switch (req.kind) { - case "discover": - this.install(req); - break; - case "closeProject": - this.closeProject(req); - break; - case "typesRegistry": { - const typesRegistry: { [key: string]: MapLike } = {}; - this.typesRegistry.forEach((value, key) => { - typesRegistry[key] = value; - }); - const response: TypesRegistryResponse = { kind: EventTypesRegistry, typesRegistry }; - this.sendResponse(response); - break; - } - case "installPackage": { - const { fileName, packageName, projectName, projectRootPath } = req; - const cwd = getDirectoryOfPackageJson(fileName, this.installTypingHost) || projectRootPath; - if (cwd) { - this.installWorker(-1, [packageName], cwd, success => { - const message = success ? `Package ${packageName} installed.` : `There was an error installing ${packageName}.`; - const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success, message }; - this.sendResponse(response); - }); - } - else { - const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success: false, message: "Could not determine a project root path." }; - this.sendResponse(response); - } - break; - } - default: - Debug.assertNever(req); - } - }); - } - - protected sendResponse(response: TypingInstallerResponseUnion) { - if (this.log.isEnabled()) { - this.log.writeLine(`Sending response:\n ${JSON.stringify(response)}`); - } - process.send!(response); // TODO: GH#18217 - if (this.log.isEnabled()) { - this.log.writeLine(`Response has been sent.`); - } - } - - protected installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { - if (this.log.isEnabled()) { - this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(packageNames)}'.`); - } - const start = Date.now(); - const hasError = installNpmPackages(this.npmPath, version, packageNames, command => this.execSyncAndLog(command, { cwd })); - if (this.log.isEnabled()) { - this.log.writeLine(`npm install #${requestId} took: ${Date.now() - start} ms`); - } - onRequestCompleted(!hasError); - } - - /** Returns 'true' in case of error. */ - private execSyncAndLog(command: string, options: Pick): boolean { - if (this.log.isEnabled()) { - this.log.writeLine(`Exec: ${command}`); - } - try { - const stdout = this.nodeExecSync(command, { ...options, encoding: "utf-8" }); - if (this.log.isEnabled()) { - this.log.writeLine(` Succeeded. stdout:${indent(sys.newLine, stdout)}`); - } - return false; - } - catch (error) { - const { stdout, stderr } = error; - this.log.writeLine(` Failed. stdout:${indent(sys.newLine, stdout)}${sys.newLine} stderr:${indent(sys.newLine, stderr)}`); - return true; - } - } - } - - function getDirectoryOfPackageJson(fileName: string, host: InstallTypingHost): string | undefined { - return forEachAncestorDirectory(getDirectoryPath(fileName), directory => { - if (host.fileExists(combinePaths(directory, "package.json"))) { - return directory; - } - }); - } - const logFilePath = findArgument(Arguments.LogFile); const globalTypingsCacheLocation = findArgument(Arguments.GlobalCacheLocation); const typingSafeListLocation = findArgument(Arguments.TypingSafeListLocation); @@ -261,7 +41,37 @@ namespace ts.server.typingsInstaller { } process.exit(0); }); - const installer = new NodeTypingsInstaller(globalTypingsCacheLocation!, typingSafeListLocation!, typesMapLocation!, npmLocation, validateDefaultNpmLocation, /*throttleLimit*/5, log); // TODO: GH#18217 + + const nodeHost: NodeInstallTypingHost = { + useCaseSensitiveFileNames: sys.useCaseSensitiveFileNames, + writeFile: sys.writeFile, + createDirectory: sys.createDirectory, + directoryExists: sys.directoryExists, + fileExists: sys.fileExists, + readFile: sys.readFile, + readDirectory: sys.readDirectory, + watchFile: sys.watchFile, + + /** Returns false on error */ + execSyncAndLog(command: string, options: string, log: Log): boolean { + if (log.isEnabled()) { + log.writeLine(`Exec: ${command}`); + } + try { + const stdout = require("child_process").execSync(command, { cwd: options, encoding: "utf-8" }); + if (log.isEnabled()) { + log.writeLine(` Succeeded. stdout:${indent(sys.newLine, stdout)}`); + } + return true; + } + catch (error) { + const { stdout, stderr } = error; + log.writeLine(` Failed. stdout:${indent(sys.newLine, stdout)}${sys.newLine} stderr:${indent(sys.newLine, stderr)}`); + return false; + } + } + } + const installer = new NodeTypingsInstaller(nodeHost, globalTypingsCacheLocation, typingSafeListLocation!, typesMapLocation!, npmLocation, validateDefaultNpmLocation, /*throttleLimit*/5, log); // TODO: GH#18217 installer.listen(); function indent(newline: string, str: string): string { diff --git a/src/typingsInstallerCore/nodeTypingsInstallerCore.ts b/src/typingsInstallerCore/nodeTypingsInstallerCore.ts new file mode 100644 index 0000000000000..2b63b7e1b196b --- /dev/null +++ b/src/typingsInstallerCore/nodeTypingsInstallerCore.ts @@ -0,0 +1,200 @@ +namespace ts.server.typingsInstaller { + const path: { + join(...parts: string[]): string; + dirname(path: string): string; + basename(path: string, extension?: string): string; + } = require("path"); + + /** Used if `--npmLocation` is not passed. */ + function getDefaultNPMLocation(processName: string, validateDefaultNpmLocation: boolean, host: InstallTypingHost): string { + if (path.basename(processName).indexOf("node") === 0) { + const npmPath = path.join(path.dirname(process.argv[0]), "npm"); + if (!validateDefaultNpmLocation) { + return npmPath; + } + if (host.fileExists(npmPath)) { + return `"${npmPath}"`; + } + } + return "npm"; + } + + interface TypesRegistryFile { + entries: MapLike>; + } + + function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map> { + if (!host.fileExists(typesRegistryFilePath)) { + if (log.isEnabled()) { + log.writeLine(`Types registry file '${typesRegistryFilePath}' does not exist`); + } + return createMap>(); + } + try { + const content = JSON.parse(host.readFile(typesRegistryFilePath)!); + return createMapFromTemplate(content.entries); + } + catch (e) { + if (log.isEnabled()) { + log.writeLine(`Error when loading types registry file '${typesRegistryFilePath}': ${(e).message}, ${(e).stack}`); + } + return createMap>(); + } + } + + const typesRegistryPackageName = "types-registry"; + const definitelyTypedTypesRegistryPackageName = "@definitelytyped/types-registry"; + function getTypesRegistryFileLocation(globalTypingsCacheLocation: string, packageName: string): string { + return combinePaths(normalizeSlashes(globalTypingsCacheLocation), `node_modules/${packageName}/index.json`); + } + + export interface NodeInstallTypingHost extends InstallTypingHost { + execSyncAndLog(command: string, options: string, log: Log): boolean; + } + + export class NodeTypingsInstaller extends TypingsInstaller { + private readonly npmPath: string; + readonly typesRegistry!: Map>; + + private delayedInitializationError: InitializationFailedResponse | undefined; + + constructor(private host: NodeInstallTypingHost, globalTypingsCacheLocation: string | undefined, typingSafeListLocation: string, typesMapLocation: string, npmLocation: string | undefined, validateDefaultNpmLocation: boolean, throttleLimit: number, log: Log) { + super( + host, + globalTypingsCacheLocation!, + typingSafeListLocation ? toPath(typingSafeListLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), + typesMapLocation ? toPath(typesMapLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typesMap.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), + throttleLimit, + log); + this.npmPath = npmLocation !== undefined ? npmLocation : getDefaultNPMLocation(process.argv[0], validateDefaultNpmLocation, this.installTypingHost); + + // If the NPM path contains spaces and isn't wrapped in quotes, do so. + if (stringContains(this.npmPath, " ") && this.npmPath[0] !== `"`) { + this.npmPath = `"${this.npmPath}"`; + } + if (this.log.isEnabled()) { + this.log.writeLine(`Process id: ${process.pid}`); + this.log.writeLine(`NPM location: ${this.npmPath} (explicit '${Arguments.NpmLocation}' ${npmLocation === undefined ? "not " : ""} provided)`); + this.log.writeLine(`validateDefaultNpmLocation: ${validateDefaultNpmLocation}`); + } + if (globalTypingsCacheLocation) { + this.ensurePackageDirectoryExists(globalTypingsCacheLocation); + const packageName = this.installTypesRegistry(globalTypingsCacheLocation); + this.typesRegistry = loadTypesRegistryFile(getTypesRegistryFileLocation(globalTypingsCacheLocation, packageName), this.installTypingHost, this.log); + } + else if (this.log.isEnabled()) { + this.log.writeLine("Error: Missing --globalTypingsCache argument on command line"); + this.delayedInitializationError = { + kind: "event::initializationFailed", + message: "Missing --globalTypingsCache argument", + }; + } + } + + private installTypesRegistry(globalTypingsCacheLocation: string) { + let result: typeof typesRegistryPackageName | typeof definitelyTypedTypesRegistryPackageName | undefined = + this.installTypesRegistryFromPackageName(definitelyTypedTypesRegistryPackageName, globalTypingsCacheLocation) + || this.installTypesRegistryFromPackageName(typesRegistryPackageName, globalTypingsCacheLocation); + if (!result) { + if (this.log.isEnabled()) { + this.log.writeLine(`Error updating ${typesRegistryPackageName} package`); + } + // store error info to report it later when it is known that server is already listening to events from typings installer + this.delayedInitializationError = { + kind: "event::initializationFailed", + message: result || "UPDATE FAILED" + }; + return "types-registry"; + } + return result; + } + + private installTypesRegistryFromPackageName(packageName: typeof typesRegistryPackageName | typeof definitelyTypedTypesRegistryPackageName, globalTypingsCacheLocation: string) { + if (this.log.isEnabled()) { + this.log.writeLine(`Updating ${packageName} npm package...`); + } + const registry = packageName === typesRegistryPackageName ? "" : "--registry=https://npm.pkg.github.com"; + if (this.host.execSyncAndLog(`${this.npmPath} install ${registry} --ignore-scripts ${packageName}@${this.latestDistTag}`, globalTypingsCacheLocation, this.log)) { + if (this.log.isEnabled()) { + this.log.writeLine(`Updated ${packageName} npm package`); + } + return packageName; + } + } + + listen() { + process.on("message", (req: TypingInstallerRequestUnion) => { + if (this.delayedInitializationError) { + // report initializationFailed error + this.sendResponse(this.delayedInitializationError); + this.delayedInitializationError = undefined; + } + switch (req.kind) { + case "discover": + this.install(req); + break; + case "closeProject": + this.closeProject(req); + break; + case "typesRegistry": { + const typesRegistry: { [key: string]: MapLike } = {}; + this.typesRegistry.forEach((value, key) => { + typesRegistry[key] = value; + }); + const response: TypesRegistryResponse = { kind: EventTypesRegistry, typesRegistry }; + this.sendResponse(response); + break; + } + case "installPackage": { + const { fileName, packageName, projectName, projectRootPath } = req; + const cwd = getDirectoryOfPackageJson(fileName, this.installTypingHost) || projectRootPath; + if (cwd) { + this.installWorker(-1, [packageName], cwd, success => { + const message = success ? `Package ${packageName} installed.` : `There was an error installing ${packageName}.`; + const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success, message }; + this.sendResponse(response); + }); + } + else { + const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success: false, message: "Could not determine a project root path." }; + this.sendResponse(response); + } + break; + } + default: + Debug.assertNever(req); + } + }); + } + + protected sendResponse(response: TypingInstallerResponseUnion) { + if (this.log.isEnabled()) { + this.log.writeLine(`Sending response:\n ${JSON.stringify(response)}`); + } + process.send!(response); // TODO: GH#18217 + if (this.log.isEnabled()) { + this.log.writeLine(`Response has been sent.`); + } + } + + protected installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { + if (this.log.isEnabled()) { + this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(packageNames)}'.`); + } + const start = Date.now(); + const succeeded = installNpmPackages(this.npmPath, version, packageNames, command => this.host.execSyncAndLog(command, cwd, this.log)); + if (this.log.isEnabled()) { + this.log.writeLine(`npm install #${requestId} took: ${Date.now() - start} ms`); + } + onRequestCompleted(succeeded); + } + } + + function getDirectoryOfPackageJson(fileName: string, host: InstallTypingHost): string | undefined { + return forEachAncestorDirectory(getDirectoryPath(fileName), directory => { + if (host.fileExists(combinePaths(directory, "package.json"))) { + return directory; + } + }); + } +} diff --git a/src/typingsInstallerCore/tsconfig.json b/src/typingsInstallerCore/tsconfig.json index 2cc0d52d8de47..b6833a69cff60 100644 --- a/src/typingsInstallerCore/tsconfig.json +++ b/src/typingsInstallerCore/tsconfig.json @@ -15,6 +15,7 @@ { "path": "../jsTyping" } ], "files": [ - "typingsInstaller.ts" + "typingsInstaller.ts", + "nodeTypingsInstallerCore.ts" ] } diff --git a/src/typingsInstallerCore/typingsInstaller.ts b/src/typingsInstallerCore/typingsInstaller.ts index 2b94b8874203f..9dad8e7643dd8 100644 --- a/src/typingsInstallerCore/typingsInstaller.ts +++ b/src/typingsInstallerCore/typingsInstaller.ts @@ -32,13 +32,13 @@ namespace ts.server.typingsInstaller { /*@internal*/ export function installNpmPackages(npmPath: string, tsVersion: string, packageNames: string[], install: (command: string) => boolean) { - let hasError = false; + let succeeded = true; for (let remaining = packageNames.length; remaining > 0;) { const result = getNpmCommandForInstallation(npmPath, tsVersion, packageNames, remaining); remaining = result.remaining; - hasError = install(result.command) || hasError; + succeeded = install(result.command) && succeeded; } - return hasError; + return succeeded; } /*@internal*/ @@ -391,7 +391,7 @@ namespace ts.server.typingsInstaller { private ensureDirectoryExists(directory: string, host: InstallTypingHost): void { const directoryName = getDirectoryPath(directory); - if (!host.directoryExists(directoryName)) { + if (!host.directoryExists(directoryName) && directory !== directoryName) { this.ensureDirectoryExists(directoryName, host); } if (!host.directoryExists(directory)) { @@ -540,4 +540,5 @@ namespace ts.server.typingsInstaller { export function typingsName(packageName: string): string { return `@types/${packageName}@ts${versionMajorMinor}`; } + } From 73707680fd4d89b8d20b54c35a034feef56a1063 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Mon, 14 Oct 2019 09:41:30 -0700 Subject: [PATCH 3/4] Add explanatory comment to nodeTypingsInstaller --- src/typingsInstaller/nodeTypingsInstaller.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/typingsInstaller/nodeTypingsInstaller.ts b/src/typingsInstaller/nodeTypingsInstaller.ts index 5de0420c03fd7..6592c49051c0b 100644 --- a/src/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/typingsInstaller/nodeTypingsInstaller.ts @@ -1,3 +1,11 @@ +/** + * Create a NodeTypingsInstaller for use in ATA. + * + * This file creates an instance of NodeTypingsInstaller *upon evaluation*, + * so is not suitable for inclusion in other compilations. + * See ../typingsInstallerCore/nodeTypingsInstallerCore.ts for the class definition, + * and ../testRunner/unittests/tsserver/typingsInstaller.ts for the tests. + */ namespace ts.server.typingsInstaller { const fs: { appendFileSync(file: string, content: string): void From 8b6f2a8c7bbe15308c17ebfcd9ac3716a25acf80 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Mon, 14 Oct 2019 10:59:03 -0700 Subject: [PATCH 4/4] Fix lint --- .../unittests/tsserver/typingsInstaller.ts | 8 +- src/typingsInstaller/nodeTypingsInstaller.ts | 2 +- .../nodeTypingsInstallerCore.ts | 400 +++++++++--------- 3 files changed, 203 insertions(+), 207 deletions(-) diff --git a/src/testRunner/unittests/tsserver/typingsInstaller.ts b/src/testRunner/unittests/tsserver/typingsInstaller.ts index 3edbd00a865bf..8bd2ee8004374 100644 --- a/src/testRunner/unittests/tsserver/typingsInstaller.ts +++ b/src/testRunner/unittests/tsserver/typingsInstaller.ts @@ -1997,9 +1997,7 @@ declare module "stream" { }); it("constructs successfully from npm (falling back from @definitelytyped)", () => { const host = createHost(); - host.execSyncAndLog = function(command) { - return !command.includes("@definitelytyped"); - } + host.execSyncAndLog = command => !command.includes("@definitelytyped"); const i = new TI.NodeTypingsInstaller(host, "a", "b", "c", "d", true, 1, host.log); assert.isUndefined((i as any).delayedInitializationError); assert.notInclude(host.log.out, "Updated @definitelytyped/types-registry"); @@ -2007,9 +2005,7 @@ declare module "stream" { }); it("fails construction", () => { const host = createHost(); - host.execSyncAndLog = function() { - return false; - } + host.execSyncAndLog = () => false; const i = new TI.NodeTypingsInstaller(host, "a", "b", "c", "d", true, 1, host.log); assert.equal("event::initializationFailed", (i as any).delayedInitializationError.kind); assert.equal("UPDATE FAILED", (i as any).delayedInitializationError.message); diff --git a/src/typingsInstaller/nodeTypingsInstaller.ts b/src/typingsInstaller/nodeTypingsInstaller.ts index 6592c49051c0b..3e916732a47c2 100644 --- a/src/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/typingsInstaller/nodeTypingsInstaller.ts @@ -78,7 +78,7 @@ namespace ts.server.typingsInstaller { return false; } } - } + }; const installer = new NodeTypingsInstaller(nodeHost, globalTypingsCacheLocation, typingSafeListLocation!, typesMapLocation!, npmLocation, validateDefaultNpmLocation, /*throttleLimit*/5, log); // TODO: GH#18217 installer.listen(); diff --git a/src/typingsInstallerCore/nodeTypingsInstallerCore.ts b/src/typingsInstallerCore/nodeTypingsInstallerCore.ts index 2b63b7e1b196b..c88bec74c42df 100644 --- a/src/typingsInstallerCore/nodeTypingsInstallerCore.ts +++ b/src/typingsInstallerCore/nodeTypingsInstallerCore.ts @@ -1,200 +1,200 @@ -namespace ts.server.typingsInstaller { - const path: { - join(...parts: string[]): string; - dirname(path: string): string; - basename(path: string, extension?: string): string; - } = require("path"); - - /** Used if `--npmLocation` is not passed. */ - function getDefaultNPMLocation(processName: string, validateDefaultNpmLocation: boolean, host: InstallTypingHost): string { - if (path.basename(processName).indexOf("node") === 0) { - const npmPath = path.join(path.dirname(process.argv[0]), "npm"); - if (!validateDefaultNpmLocation) { - return npmPath; - } - if (host.fileExists(npmPath)) { - return `"${npmPath}"`; - } - } - return "npm"; - } - - interface TypesRegistryFile { - entries: MapLike>; - } - - function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map> { - if (!host.fileExists(typesRegistryFilePath)) { - if (log.isEnabled()) { - log.writeLine(`Types registry file '${typesRegistryFilePath}' does not exist`); - } - return createMap>(); - } - try { - const content = JSON.parse(host.readFile(typesRegistryFilePath)!); - return createMapFromTemplate(content.entries); - } - catch (e) { - if (log.isEnabled()) { - log.writeLine(`Error when loading types registry file '${typesRegistryFilePath}': ${(e).message}, ${(e).stack}`); - } - return createMap>(); - } - } - - const typesRegistryPackageName = "types-registry"; - const definitelyTypedTypesRegistryPackageName = "@definitelytyped/types-registry"; - function getTypesRegistryFileLocation(globalTypingsCacheLocation: string, packageName: string): string { - return combinePaths(normalizeSlashes(globalTypingsCacheLocation), `node_modules/${packageName}/index.json`); - } - - export interface NodeInstallTypingHost extends InstallTypingHost { - execSyncAndLog(command: string, options: string, log: Log): boolean; - } - - export class NodeTypingsInstaller extends TypingsInstaller { - private readonly npmPath: string; - readonly typesRegistry!: Map>; - - private delayedInitializationError: InitializationFailedResponse | undefined; - - constructor(private host: NodeInstallTypingHost, globalTypingsCacheLocation: string | undefined, typingSafeListLocation: string, typesMapLocation: string, npmLocation: string | undefined, validateDefaultNpmLocation: boolean, throttleLimit: number, log: Log) { - super( - host, - globalTypingsCacheLocation!, - typingSafeListLocation ? toPath(typingSafeListLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), - typesMapLocation ? toPath(typesMapLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typesMap.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), - throttleLimit, - log); - this.npmPath = npmLocation !== undefined ? npmLocation : getDefaultNPMLocation(process.argv[0], validateDefaultNpmLocation, this.installTypingHost); - - // If the NPM path contains spaces and isn't wrapped in quotes, do so. - if (stringContains(this.npmPath, " ") && this.npmPath[0] !== `"`) { - this.npmPath = `"${this.npmPath}"`; - } - if (this.log.isEnabled()) { - this.log.writeLine(`Process id: ${process.pid}`); - this.log.writeLine(`NPM location: ${this.npmPath} (explicit '${Arguments.NpmLocation}' ${npmLocation === undefined ? "not " : ""} provided)`); - this.log.writeLine(`validateDefaultNpmLocation: ${validateDefaultNpmLocation}`); - } - if (globalTypingsCacheLocation) { - this.ensurePackageDirectoryExists(globalTypingsCacheLocation); - const packageName = this.installTypesRegistry(globalTypingsCacheLocation); - this.typesRegistry = loadTypesRegistryFile(getTypesRegistryFileLocation(globalTypingsCacheLocation, packageName), this.installTypingHost, this.log); - } - else if (this.log.isEnabled()) { - this.log.writeLine("Error: Missing --globalTypingsCache argument on command line"); - this.delayedInitializationError = { - kind: "event::initializationFailed", - message: "Missing --globalTypingsCache argument", - }; - } - } - - private installTypesRegistry(globalTypingsCacheLocation: string) { - let result: typeof typesRegistryPackageName | typeof definitelyTypedTypesRegistryPackageName | undefined = - this.installTypesRegistryFromPackageName(definitelyTypedTypesRegistryPackageName, globalTypingsCacheLocation) - || this.installTypesRegistryFromPackageName(typesRegistryPackageName, globalTypingsCacheLocation); - if (!result) { - if (this.log.isEnabled()) { - this.log.writeLine(`Error updating ${typesRegistryPackageName} package`); - } - // store error info to report it later when it is known that server is already listening to events from typings installer - this.delayedInitializationError = { - kind: "event::initializationFailed", - message: result || "UPDATE FAILED" - }; - return "types-registry"; - } - return result; - } - - private installTypesRegistryFromPackageName(packageName: typeof typesRegistryPackageName | typeof definitelyTypedTypesRegistryPackageName, globalTypingsCacheLocation: string) { - if (this.log.isEnabled()) { - this.log.writeLine(`Updating ${packageName} npm package...`); - } - const registry = packageName === typesRegistryPackageName ? "" : "--registry=https://npm.pkg.github.com"; - if (this.host.execSyncAndLog(`${this.npmPath} install ${registry} --ignore-scripts ${packageName}@${this.latestDistTag}`, globalTypingsCacheLocation, this.log)) { - if (this.log.isEnabled()) { - this.log.writeLine(`Updated ${packageName} npm package`); - } - return packageName; - } - } - - listen() { - process.on("message", (req: TypingInstallerRequestUnion) => { - if (this.delayedInitializationError) { - // report initializationFailed error - this.sendResponse(this.delayedInitializationError); - this.delayedInitializationError = undefined; - } - switch (req.kind) { - case "discover": - this.install(req); - break; - case "closeProject": - this.closeProject(req); - break; - case "typesRegistry": { - const typesRegistry: { [key: string]: MapLike } = {}; - this.typesRegistry.forEach((value, key) => { - typesRegistry[key] = value; - }); - const response: TypesRegistryResponse = { kind: EventTypesRegistry, typesRegistry }; - this.sendResponse(response); - break; - } - case "installPackage": { - const { fileName, packageName, projectName, projectRootPath } = req; - const cwd = getDirectoryOfPackageJson(fileName, this.installTypingHost) || projectRootPath; - if (cwd) { - this.installWorker(-1, [packageName], cwd, success => { - const message = success ? `Package ${packageName} installed.` : `There was an error installing ${packageName}.`; - const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success, message }; - this.sendResponse(response); - }); - } - else { - const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success: false, message: "Could not determine a project root path." }; - this.sendResponse(response); - } - break; - } - default: - Debug.assertNever(req); - } - }); - } - - protected sendResponse(response: TypingInstallerResponseUnion) { - if (this.log.isEnabled()) { - this.log.writeLine(`Sending response:\n ${JSON.stringify(response)}`); - } - process.send!(response); // TODO: GH#18217 - if (this.log.isEnabled()) { - this.log.writeLine(`Response has been sent.`); - } - } - - protected installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { - if (this.log.isEnabled()) { - this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(packageNames)}'.`); - } - const start = Date.now(); - const succeeded = installNpmPackages(this.npmPath, version, packageNames, command => this.host.execSyncAndLog(command, cwd, this.log)); - if (this.log.isEnabled()) { - this.log.writeLine(`npm install #${requestId} took: ${Date.now() - start} ms`); - } - onRequestCompleted(succeeded); - } - } - - function getDirectoryOfPackageJson(fileName: string, host: InstallTypingHost): string | undefined { - return forEachAncestorDirectory(getDirectoryPath(fileName), directory => { - if (host.fileExists(combinePaths(directory, "package.json"))) { - return directory; - } - }); - } -} +namespace ts.server.typingsInstaller { + const path: { + join(...parts: string[]): string; + dirname(path: string): string; + basename(path: string, extension?: string): string; + } = require("path"); + + /** Used if `--npmLocation` is not passed. */ + function getDefaultNPMLocation(processName: string, validateDefaultNpmLocation: boolean, host: InstallTypingHost): string { + if (path.basename(processName).indexOf("node") === 0) { + const npmPath = path.join(path.dirname(process.argv[0]), "npm"); + if (!validateDefaultNpmLocation) { + return npmPath; + } + if (host.fileExists(npmPath)) { + return `"${npmPath}"`; + } + } + return "npm"; + } + + interface TypesRegistryFile { + entries: MapLike>; + } + + function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map> { + if (!host.fileExists(typesRegistryFilePath)) { + if (log.isEnabled()) { + log.writeLine(`Types registry file '${typesRegistryFilePath}' does not exist`); + } + return createMap>(); + } + try { + const content = JSON.parse(host.readFile(typesRegistryFilePath)!); + return createMapFromTemplate(content.entries); + } + catch (e) { + if (log.isEnabled()) { + log.writeLine(`Error when loading types registry file '${typesRegistryFilePath}': ${(e).message}, ${(e).stack}`); + } + return createMap>(); + } + } + + const typesRegistryPackageName = "types-registry"; + const definitelyTypedTypesRegistryPackageName = "@definitelytyped/types-registry"; + function getTypesRegistryFileLocation(globalTypingsCacheLocation: string, packageName: string): string { + return combinePaths(normalizeSlashes(globalTypingsCacheLocation), `node_modules/${packageName}/index.json`); + } + + export interface NodeInstallTypingHost extends InstallTypingHost { + execSyncAndLog(command: string, options: string, log: Log): boolean; + } + + export class NodeTypingsInstaller extends TypingsInstaller { + private readonly npmPath: string; + readonly typesRegistry!: Map>; + + private delayedInitializationError: InitializationFailedResponse | undefined; + + constructor(private host: NodeInstallTypingHost, globalTypingsCacheLocation: string | undefined, typingSafeListLocation: string, typesMapLocation: string, npmLocation: string | undefined, validateDefaultNpmLocation: boolean, throttleLimit: number, log: Log) { + super( + host, + globalTypingsCacheLocation!, + typingSafeListLocation ? toPath(typingSafeListLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), + typesMapLocation ? toPath(typesMapLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typesMap.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), + throttleLimit, + log); + this.npmPath = npmLocation !== undefined ? npmLocation : getDefaultNPMLocation(process.argv[0], validateDefaultNpmLocation, this.installTypingHost); + + // If the NPM path contains spaces and isn't wrapped in quotes, do so. + if (stringContains(this.npmPath, " ") && this.npmPath[0] !== `"`) { + this.npmPath = `"${this.npmPath}"`; + } + if (this.log.isEnabled()) { + this.log.writeLine(`Process id: ${process.pid}`); + this.log.writeLine(`NPM location: ${this.npmPath} (explicit '${Arguments.NpmLocation}' ${npmLocation === undefined ? "not " : ""} provided)`); + this.log.writeLine(`validateDefaultNpmLocation: ${validateDefaultNpmLocation}`); + } + if (globalTypingsCacheLocation) { + this.ensurePackageDirectoryExists(globalTypingsCacheLocation); + const packageName = this.installTypesRegistry(globalTypingsCacheLocation); + this.typesRegistry = loadTypesRegistryFile(getTypesRegistryFileLocation(globalTypingsCacheLocation, packageName), this.installTypingHost, this.log); + } + else if (this.log.isEnabled()) { + this.log.writeLine("Error: Missing --globalTypingsCache argument on command line"); + this.delayedInitializationError = { + kind: "event::initializationFailed", + message: "Missing --globalTypingsCache argument", + }; + } + } + + private installTypesRegistry(globalTypingsCacheLocation: string) { + const result: typeof typesRegistryPackageName | typeof definitelyTypedTypesRegistryPackageName | undefined = + this.installTypesRegistryFromPackageName(definitelyTypedTypesRegistryPackageName, globalTypingsCacheLocation) + || this.installTypesRegistryFromPackageName(typesRegistryPackageName, globalTypingsCacheLocation); + if (!result) { + if (this.log.isEnabled()) { + this.log.writeLine(`Error updating ${typesRegistryPackageName} package`); + } + // store error info to report it later when it is known that server is already listening to events from typings installer + this.delayedInitializationError = { + kind: "event::initializationFailed", + message: result || "UPDATE FAILED" + }; + return "types-registry"; + } + return result; + } + + private installTypesRegistryFromPackageName(packageName: typeof typesRegistryPackageName | typeof definitelyTypedTypesRegistryPackageName, globalTypingsCacheLocation: string) { + if (this.log.isEnabled()) { + this.log.writeLine(`Updating ${packageName} npm package...`); + } + const registry = packageName === typesRegistryPackageName ? "" : "--registry=https://npm.pkg.github.com"; + if (this.host.execSyncAndLog(`${this.npmPath} install ${registry} --ignore-scripts ${packageName}@${this.latestDistTag}`, globalTypingsCacheLocation, this.log)) { + if (this.log.isEnabled()) { + this.log.writeLine(`Updated ${packageName} npm package`); + } + return packageName; + } + } + + listen() { + process.on("message", (req: TypingInstallerRequestUnion) => { + if (this.delayedInitializationError) { + // report initializationFailed error + this.sendResponse(this.delayedInitializationError); + this.delayedInitializationError = undefined; + } + switch (req.kind) { + case "discover": + this.install(req); + break; + case "closeProject": + this.closeProject(req); + break; + case "typesRegistry": { + const typesRegistry: { [key: string]: MapLike } = {}; + this.typesRegistry.forEach((value, key) => { + typesRegistry[key] = value; + }); + const response: TypesRegistryResponse = { kind: EventTypesRegistry, typesRegistry }; + this.sendResponse(response); + break; + } + case "installPackage": { + const { fileName, packageName, projectName, projectRootPath } = req; + const cwd = getDirectoryOfPackageJson(fileName, this.installTypingHost) || projectRootPath; + if (cwd) { + this.installWorker(-1, [packageName], cwd, success => { + const message = success ? `Package ${packageName} installed.` : `There was an error installing ${packageName}.`; + const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success, message }; + this.sendResponse(response); + }); + } + else { + const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success: false, message: "Could not determine a project root path." }; + this.sendResponse(response); + } + break; + } + default: + Debug.assertNever(req); + } + }); + } + + protected sendResponse(response: TypingInstallerResponseUnion) { + if (this.log.isEnabled()) { + this.log.writeLine(`Sending response:\n ${JSON.stringify(response)}`); + } + process.send!(response); // TODO: GH#18217 + if (this.log.isEnabled()) { + this.log.writeLine(`Response has been sent.`); + } + } + + protected installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { + if (this.log.isEnabled()) { + this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(packageNames)}'.`); + } + const start = Date.now(); + const succeeded = installNpmPackages(this.npmPath, version, packageNames, command => this.host.execSyncAndLog(command, cwd, this.log)); + if (this.log.isEnabled()) { + this.log.writeLine(`npm install #${requestId} took: ${Date.now() - start} ms`); + } + onRequestCompleted(succeeded); + } + } + + function getDirectoryOfPackageJson(fileName: string, host: InstallTypingHost): string | undefined { + return forEachAncestorDirectory(getDirectoryPath(fileName), directory => { + if (host.fileExists(combinePaths(directory, "package.json"))) { + return directory; + } + }); + } +}