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