|
7 | 7 | */
|
8 | 8 |
|
9 | 9 | import { json, logging } from '@angular-devkit/core';
|
| 10 | +import { execFile } from 'child_process'; |
10 | 11 | import { promises as fs } from 'fs';
|
11 | 12 | import * as path from 'path';
|
12 | 13 | import { env } from 'process';
|
@@ -78,6 +79,16 @@ Appended \`source <(ng completion script)\` to \`${rcFile}\`. Restart your termi
|
78 | 79 | `.trim(),
|
79 | 80 | );
|
80 | 81 |
|
| 82 | + if ((await hasGlobalCliInstall()) === false) { |
| 83 | + logger.warn( |
| 84 | + 'Setup completed successfully, but there does not seem to be a global install of the' + |
| 85 | + ' Angular CLI. For autocompletion to work, the CLI will need to be on your `$PATH`, which' + |
| 86 | + ' is typically done with the `-g` flag in `npm install -g @angular/cli`.' + |
| 87 | + '\n\n' + |
| 88 | + 'For more information, see https://angular.io/cli/completion#global-install', |
| 89 | + ); |
| 90 | + } |
| 91 | + |
81 | 92 | // Save configuration to remember that the user was prompted.
|
82 | 93 | await setCompletionConfig({ ...completionConfig, prompted: true });
|
83 | 94 |
|
@@ -147,6 +158,12 @@ async function shouldPromptForAutocompletionSetup(
|
147 | 158 | return false; // Unknown shell.
|
148 | 159 | }
|
149 | 160 |
|
| 161 | + // Don't prompt if the user is missing a global CLI install. Autocompletion won't work after setup |
| 162 | + // anyway and could be annoying for users running one-off commands via `npx` or using `npm start`. |
| 163 | + if ((await hasGlobalCliInstall()) === false) { |
| 164 | + return false; |
| 165 | + } |
| 166 | + |
150 | 167 | // Check each RC file if they already use `ng completion script` in any capacity and don't prompt.
|
151 | 168 | for (const rcFile of rcFiles) {
|
152 | 169 | const contents = await fs.readFile(rcFile, 'utf-8').catch(() => undefined);
|
@@ -246,3 +263,55 @@ function getShellRunCommandCandidates(shell: string, home: string): string[] | u
|
246 | 263 | return undefined;
|
247 | 264 | }
|
248 | 265 | }
|
| 266 | + |
| 267 | +/** |
| 268 | + * Returns whether the user has a global CLI install or `undefined` if this can't be determined. |
| 269 | + * Execution from `npx` is *not* considered a global CLI install. |
| 270 | + * |
| 271 | + * This does *not* mean the current execution is from a global CLI install, only that a global |
| 272 | + * install exists on the system. |
| 273 | + */ |
| 274 | +export async function hasGlobalCliInstall(): Promise<boolean | undefined> { |
| 275 | + // List all binaries with the `ng` name on the user's `$PATH`. |
| 276 | + const proc = execFile('which', ['-a', 'ng']); |
| 277 | + let stdout = ''; |
| 278 | + proc.stdout?.addListener('data', (content) => { |
| 279 | + stdout += content; |
| 280 | + }); |
| 281 | + const exitCode = await new Promise<number | null>((resolve) => { |
| 282 | + proc.addListener('exit', (exitCode) => { |
| 283 | + resolve(exitCode); |
| 284 | + }); |
| 285 | + }); |
| 286 | + |
| 287 | + switch (exitCode) { |
| 288 | + case 0: |
| 289 | + // Successfully listed all `ng` binaries on the `$PATH`. Look for at least one line which is a |
| 290 | + // global install. We can't easily identify global installs, but local installs are typically |
| 291 | + // placed in `node_modules/.bin` by NPM / Yarn. `npx` also currently caches files at |
| 292 | + // `~/.npm/_npx/*/node_modules/.bin/`, so the same logic applies. |
| 293 | + const lines = stdout.split('\n').filter((line) => line !== ''); |
| 294 | + const hasGlobalInstall = lines.some((line) => { |
| 295 | + // A binary is a local install if it is a direct child of a `node_modules/.bin/` directory. |
| 296 | + const parent = path.parse(path.parse(line).dir); |
| 297 | + const grandparent = path.parse(parent.dir); |
| 298 | + const localInstall = grandparent.base === 'node_modules' && parent.base === '.bin'; |
| 299 | + |
| 300 | + return !localInstall; |
| 301 | + }); |
| 302 | + |
| 303 | + return hasGlobalInstall; |
| 304 | + case 1: |
| 305 | + // No instances of `ng` on the user's `$PATH`. |
| 306 | + return false; |
| 307 | + case null: |
| 308 | + // `which` was killed by a signal and did not exit gracefully. Maybe it hung or something else |
| 309 | + // went very wrong, so treat this as inconclusive. |
| 310 | + return undefined; |
| 311 | + default: |
| 312 | + // `which` returns exit code 2 if an invalid option is specified and `-a` doesn't appear to be |
| 313 | + // supported on all systems. Other exit codes mean unknown errors occurred. Can't tell whether |
| 314 | + // CLI is globally installed, so treat this as inconclusive. |
| 315 | + return undefined; |
| 316 | + } |
| 317 | +} |
0 commit comments