• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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