1namespace ts.server.typingsInstaller { 2 const fs: { 3 appendFileSync(file: string, content: string): void 4 } = require("fs"); 5 6 const path: { 7 join(...parts: string[]): string; 8 dirname(path: string): string; 9 basename(path: string, extension?: string): string; 10 } = require("path"); 11 12 class FileLog implements Log { 13 constructor(private logFile: string | undefined) { 14 } 15 16 isEnabled = () => { 17 return typeof this.logFile === "string"; 18 }; 19 writeLine = (text: string) => { 20 if (typeof this.logFile !== "string") return; 21 22 try { 23 fs.appendFileSync(this.logFile, `[${nowString()}] ${text}${sys.newLine}`); 24 } 25 catch (e) { 26 this.logFile = undefined; 27 } 28 }; 29 } 30 31 /** Used if `--npmLocation` is not passed. */ 32 function getDefaultNPMLocation(processName: string, validateDefaultNpmLocation: boolean, host: InstallTypingHost): string { 33 if (path.basename(processName).indexOf("node") === 0) { 34 const npmPath = path.join(path.dirname(process.argv[0]), "npm"); 35 if (!validateDefaultNpmLocation) { 36 return npmPath; 37 } 38 if (host.fileExists(npmPath)) { 39 return `"${npmPath}"`; 40 } 41 } 42 return "npm"; 43 } 44 45 interface TypesRegistryFile { 46 entries: MapLike<MapLike<string>>; 47 } 48 49 function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): ESMap<string, MapLike<string>> { 50 if (!host.fileExists(typesRegistryFilePath)) { 51 if (log.isEnabled()) { 52 log.writeLine(`Types registry file '${typesRegistryFilePath}' does not exist`); 53 } 54 return new Map<string, MapLike<string>>(); 55 } 56 try { 57 const content = <TypesRegistryFile>JSON.parse(host.readFile(typesRegistryFilePath)!); 58 return new Map(getEntries(content.entries)); 59 } 60 catch (e) { 61 if (log.isEnabled()) { 62 log.writeLine(`Error when loading types registry file '${typesRegistryFilePath}': ${(<Error>e).message}, ${(<Error>e).stack}`); 63 } 64 return new Map<string, MapLike<string>>(); 65 } 66 } 67 68 const typesRegistryPackageName = "types-registry"; 69 function getTypesRegistryFileLocation(globalTypingsCacheLocation: string): string { 70 return combinePaths(normalizeSlashes(globalTypingsCacheLocation), `node_modules/${typesRegistryPackageName}/index.json`); 71 } 72 73 interface ExecSyncOptions { 74 cwd: string; 75 encoding: "utf-8"; 76 } 77 type ExecSync = (command: string, options: ExecSyncOptions) => string; 78 79 export class NodeTypingsInstaller extends TypingsInstaller { 80 private readonly nodeExecSync: ExecSync; 81 private readonly npmPath: string; 82 readonly typesRegistry: ESMap<string, MapLike<string>>; 83 84 private delayedInitializationError: InitializationFailedResponse | undefined; 85 86 constructor(globalTypingsCacheLocation: string, typingSafeListLocation: string, typesMapLocation: string, npmLocation: string | undefined, validateDefaultNpmLocation: boolean, throttleLimit: number, log: Log) { 87 super( 88 sys, 89 globalTypingsCacheLocation, 90 typingSafeListLocation ? toPath(typingSafeListLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), 91 typesMapLocation ? toPath(typesMapLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typesMap.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), 92 throttleLimit, 93 log); 94 this.npmPath = npmLocation !== undefined ? npmLocation : getDefaultNPMLocation(process.argv[0], validateDefaultNpmLocation, this.installTypingHost); 95 96 // If the NPM path contains spaces and isn't wrapped in quotes, do so. 97 if (stringContains(this.npmPath, " ") && this.npmPath[0] !== `"`) { 98 this.npmPath = `"${this.npmPath}"`; 99 } 100 if (this.log.isEnabled()) { 101 this.log.writeLine(`Process id: ${process.pid}`); 102 this.log.writeLine(`NPM location: ${this.npmPath} (explicit '${Arguments.NpmLocation}' ${npmLocation === undefined ? "not " : ""} provided)`); 103 this.log.writeLine(`validateDefaultNpmLocation: ${validateDefaultNpmLocation}`); 104 } 105 ({ execSync: this.nodeExecSync } = require("child_process")); 106 107 this.ensurePackageDirectoryExists(globalTypingsCacheLocation); 108 109 try { 110 if (this.log.isEnabled()) { 111 this.log.writeLine(`Updating ${typesRegistryPackageName} npm package...`); 112 } 113 this.execSyncAndLog(`${this.npmPath} install --ignore-scripts ${typesRegistryPackageName}@${this.latestDistTag}`, { cwd: globalTypingsCacheLocation }); 114 if (this.log.isEnabled()) { 115 this.log.writeLine(`Updated ${typesRegistryPackageName} npm package`); 116 } 117 } 118 catch (e) { 119 if (this.log.isEnabled()) { 120 this.log.writeLine(`Error updating ${typesRegistryPackageName} package: ${(<Error>e).message}`); 121 } 122 // store error info to report it later when it is known that server is already listening to events from typings installer 123 this.delayedInitializationError = { 124 kind: "event::initializationFailed", 125 message: (<Error>e).message, 126 stack: (<Error>e).stack, 127 }; 128 } 129 130 this.typesRegistry = loadTypesRegistryFile(getTypesRegistryFileLocation(globalTypingsCacheLocation), this.installTypingHost, this.log); 131 } 132 133 listen() { 134 process.on("message", (req: TypingInstallerRequestUnion) => { 135 if (this.delayedInitializationError) { 136 // report initializationFailed error 137 this.sendResponse(this.delayedInitializationError); 138 this.delayedInitializationError = undefined; 139 } 140 switch (req.kind) { 141 case "discover": 142 this.install(req); 143 break; 144 case "closeProject": 145 this.closeProject(req); 146 break; 147 case "typesRegistry": { 148 const typesRegistry: { [key: string]: MapLike<string> } = {}; 149 this.typesRegistry.forEach((value, key) => { 150 typesRegistry[key] = value; 151 }); 152 const response: TypesRegistryResponse = { kind: EventTypesRegistry, typesRegistry }; 153 this.sendResponse(response); 154 break; 155 } 156 case "installPackage": { 157 const { fileName, packageName, projectName, projectRootPath } = req; 158 const cwd = getDirectoryOfPackageJson(fileName, this.installTypingHost) || projectRootPath; 159 if (cwd) { 160 this.installWorker(-1, [packageName], cwd, success => { 161 const message = success ? `Package ${packageName} installed.` : `There was an error installing ${packageName}.`; 162 const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success, message }; 163 this.sendResponse(response); 164 }); 165 } 166 else { 167 const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success: false, message: "Could not determine a project root path." }; 168 this.sendResponse(response); 169 } 170 break; 171 } 172 default: 173 Debug.assertNever(req); 174 } 175 }); 176 } 177 178 protected sendResponse(response: TypingInstallerResponseUnion) { 179 if (this.log.isEnabled()) { 180 this.log.writeLine(`Sending response:\n ${JSON.stringify(response)}`); 181 } 182 process.send!(response); // TODO: GH#18217 183 if (this.log.isEnabled()) { 184 this.log.writeLine(`Response has been sent.`); 185 } 186 } 187 188 protected installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { 189 if (this.log.isEnabled()) { 190 this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(packageNames)}'.`); 191 } 192 const start = Date.now(); 193 const hasError = installNpmPackages(this.npmPath, version, packageNames, command => this.execSyncAndLog(command, { cwd })); 194 if (this.log.isEnabled()) { 195 this.log.writeLine(`npm install #${requestId} took: ${Date.now() - start} ms`); 196 } 197 onRequestCompleted(!hasError); 198 } 199 200 /** Returns 'true' in case of error. */ 201 private execSyncAndLog(command: string, options: Pick<ExecSyncOptions, "cwd">): boolean { 202 if (this.log.isEnabled()) { 203 this.log.writeLine(`Exec: ${command}`); 204 } 205 try { 206 const stdout = this.nodeExecSync(command, { ...options, encoding: "utf-8" }); 207 if (this.log.isEnabled()) { 208 this.log.writeLine(` Succeeded. stdout:${indent(sys.newLine, stdout)}`); 209 } 210 return false; 211 } 212 catch (error) { 213 const { stdout, stderr } = error; 214 this.log.writeLine(` Failed. stdout:${indent(sys.newLine, stdout)}${sys.newLine} stderr:${indent(sys.newLine, stderr)}`); 215 return true; 216 } 217 } 218 } 219 220 function getDirectoryOfPackageJson(fileName: string, host: InstallTypingHost): string | undefined { 221 return forEachAncestorDirectory(getDirectoryPath(fileName), directory => { 222 if (host.fileExists(combinePaths(directory, "package.json"))) { 223 return directory; 224 } 225 }); 226 } 227 228 const logFilePath = findArgument(Arguments.LogFile); 229 const globalTypingsCacheLocation = findArgument(Arguments.GlobalCacheLocation); 230 const typingSafeListLocation = findArgument(Arguments.TypingSafeListLocation); 231 const typesMapLocation = findArgument(Arguments.TypesMapLocation); 232 const npmLocation = findArgument(Arguments.NpmLocation); 233 const validateDefaultNpmLocation = hasArgument(Arguments.ValidateDefaultNpmLocation); 234 235 const log = new FileLog(logFilePath); 236 if (log.isEnabled()) { 237 process.on("uncaughtException", (e: Error) => { 238 log.writeLine(`Unhandled exception: ${e} at ${e.stack}`); 239 }); 240 } 241 process.on("disconnect", () => { 242 if (log.isEnabled()) { 243 log.writeLine(`Parent process has exited, shutting down...`); 244 } 245 process.exit(0); 246 }); 247 const installer = new NodeTypingsInstaller(globalTypingsCacheLocation!, typingSafeListLocation!, typesMapLocation!, npmLocation, validateDefaultNpmLocation, /*throttleLimit*/5, log); // TODO: GH#18217 248 installer.listen(); 249 250 function indent(newline: string, str: string | undefined): string { 251 return str && str.length 252 ? `${newline} ` + str.replace(/\r?\n/, `${newline} `) 253 : ""; 254 } 255} 256