9
9
import { Architect , Target } from '@angular-devkit/architect' ;
10
10
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node' ;
11
11
import { json } from '@angular-devkit/core' ;
12
+ import { spawnSync } from 'child_process' ;
12
13
import { existsSync } from 'fs' ;
13
14
import { resolve } from 'path' ;
14
15
import { isPackageNameSafeForAnalytics } from '../analytics/analytics' ;
16
+ import { askConfirmation , askQuestion } from '../utilities/prompt' ;
17
+ import { isTTY } from '../utilities/tty' ;
15
18
import {
16
19
CommandModule ,
17
20
CommandModuleError ,
@@ -21,13 +24,18 @@ import {
21
24
} from './command-module' ;
22
25
import { Option , parseJsonSchemaToOptions } from './utilities/json-schema' ;
23
26
27
+ export interface MissingTargetChoice {
28
+ name : string ;
29
+ value : string ;
30
+ }
31
+
24
32
export abstract class ArchitectBaseCommandModule < T >
25
33
extends CommandModule < T >
26
34
implements CommandModuleImplementation < T >
27
35
{
28
36
static override scope = CommandScope . In ;
29
37
protected override shouldReportAnalytics = false ;
30
- protected readonly missingErrorTarget : string | undefined ;
38
+ protected readonly missingTargetChoices : MissingTargetChoice [ ] | undefined ;
31
39
32
40
protected async runSingleTarget ( target : Target , options : OtherOptions ) : Promise < number > {
33
41
const architectHost = await this . getArchitectHost ( ) ;
@@ -36,7 +44,7 @@ export abstract class ArchitectBaseCommandModule<T>
36
44
try {
37
45
builderName = await architectHost . getBuilderNameForTarget ( target ) ;
38
46
} catch ( e ) {
39
- throw new CommandModuleError ( this . missingErrorTarget ?? e . message ) ;
47
+ return this . onMissingTarget ( e . message ) ;
40
48
}
41
49
42
50
await this . reportAnalytics ( {
@@ -137,4 +145,77 @@ export abstract class ArchitectBaseCommandModule<T>
137
145
`Node packages may not be installed. Try installing with '${ this . context . packageManager } install'.` ,
138
146
) ;
139
147
}
148
+
149
+ protected getArchitectTarget ( ) : string {
150
+ return this . commandName ;
151
+ }
152
+
153
+ protected async onMissingTarget ( defaultMessage : string ) : Promise < 1 > {
154
+ const { logger } = this . context ;
155
+ const choices = this . missingTargetChoices ;
156
+
157
+ if ( ! choices ?. length ) {
158
+ logger . error ( defaultMessage ) ;
159
+
160
+ return 1 ;
161
+ }
162
+
163
+ const missingTargetMessage =
164
+ `Cannot find "${ this . getArchitectTarget ( ) } " target for the specified project.\n` +
165
+ `You can add a package that implements these capabilities.\n\n` +
166
+ `For example:\n` +
167
+ choices . map ( ( { name, value } ) => ` ${ name } : ng add ${ value } ` ) . join ( '\n' ) +
168
+ '\n' ;
169
+
170
+ if ( isTTY ( ) ) {
171
+ // Use prompts to ask the user if they'd like to install a package.
172
+ logger . warn ( missingTargetMessage ) ;
173
+
174
+ const packageToInstall = await this . getMissingTargetPackageToInstall ( choices ) ;
175
+ if ( packageToInstall ) {
176
+ // Example run: `ng add @angular-eslint/schematics`.
177
+ const binPath = resolve ( __dirname , '../../bin/ng.js' ) ;
178
+ const { error } = spawnSync ( process . execPath , [ binPath , 'add' , packageToInstall ] , {
179
+ stdio : 'inherit' ,
180
+ } ) ;
181
+
182
+ if ( error ) {
183
+ throw error ;
184
+ }
185
+ }
186
+ } else {
187
+ // Non TTY display error message.
188
+ logger . error ( missingTargetMessage ) ;
189
+ }
190
+
191
+ return 1 ;
192
+ }
193
+
194
+ private async getMissingTargetPackageToInstall (
195
+ choices : MissingTargetChoice [ ] ,
196
+ ) : Promise < string | null > {
197
+ if ( choices . length === 1 ) {
198
+ // Single choice
199
+ const { name, value } = choices [ 0 ] ;
200
+ if ( await askConfirmation ( `Would you like to add ${ name } now?` , true , false ) ) {
201
+ return value ;
202
+ }
203
+
204
+ return null ;
205
+ }
206
+
207
+ // Multiple choice
208
+ return askQuestion (
209
+ `Would you like to add a package with "${ this . getArchitectTarget ( ) } " capabilities now?` ,
210
+ [
211
+ {
212
+ name : 'No' ,
213
+ value : null ,
214
+ } ,
215
+ ...choices ,
216
+ ] ,
217
+ 0 ,
218
+ null ,
219
+ ) ;
220
+ }
140
221
}
0 commit comments