5
5
* Use of this source code is governed by an MIT-style license that can be
6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
+ import { normalize , virtualFs } from '@angular-devkit/core' ;
9
+ import { NodeJsSyncHost } from '@angular-devkit/core/node' ;
10
+ import { UnsuccessfulWorkflowExecution } from '@angular-devkit/schematics' ;
11
+ import { NodeWorkflow , validateOptionsWithSchema } from '@angular-devkit/schematics/tools' ;
8
12
import { execSync } from 'child_process' ;
9
13
import * as fs from 'fs' ;
10
14
import * as path from 'path' ;
11
15
import * as semver from 'semver' ;
12
- import { Arguments , Option } from '../models/interface' ;
13
- import { SchematicCommand } from '../models/schematic-command' ;
16
+ import { Command } from '../models/command' ;
17
+ import { Arguments } from '../models/interface' ;
18
+ import { colors } from '../utilities/color' ;
14
19
import { getPackageManager } from '../utilities/package-manager' ;
15
20
import {
16
21
PackageIdentifier ,
@@ -28,11 +33,156 @@ const npa = require('npm-package-arg');
28
33
29
34
const oldConfigFileNames = [ '.angular-cli.json' , 'angular-cli.json' ] ;
30
35
31
- export class UpdateCommand extends SchematicCommand < UpdateCommandSchema > {
36
+ export class UpdateCommand extends Command < UpdateCommandSchema > {
32
37
public readonly allowMissingWorkspace = true ;
33
38
34
- async parseArguments ( _schematicOptions : string [ ] , _schema : Option [ ] ) : Promise < Arguments > {
35
- return { } ;
39
+ private workflow : NodeWorkflow ;
40
+
41
+ async initialize ( ) {
42
+ this . workflow = new NodeWorkflow (
43
+ new virtualFs . ScopedHost ( new NodeJsSyncHost ( ) , normalize ( this . workspace . root ) ) ,
44
+ {
45
+ packageManager : await getPackageManager ( this . workspace . root ) ,
46
+ root : normalize ( this . workspace . root ) ,
47
+ } ,
48
+ ) ;
49
+
50
+ this . workflow . engineHost . registerOptionsTransform (
51
+ validateOptionsWithSchema ( this . workflow . registry ) ,
52
+ ) ;
53
+ }
54
+
55
+ async executeSchematic (
56
+ collection : string ,
57
+ schematic : string ,
58
+ options = { } ,
59
+ ) : Promise < { success : boolean ; files : Set < string > } > {
60
+ let error = false ;
61
+ const logs : string [ ] = [ ] ;
62
+ const files = new Set < string > ( ) ;
63
+
64
+ const reporterSubscription = this . workflow . reporter . subscribe ( event => {
65
+ // Strip leading slash to prevent confusion.
66
+ const eventPath = event . path . startsWith ( '/' ) ? event . path . substr ( 1 ) : event . path ;
67
+
68
+ switch ( event . kind ) {
69
+ case 'error' :
70
+ error = true ;
71
+ const desc = event . description == 'alreadyExist' ? 'already exists' : 'does not exist.' ;
72
+ this . logger . error ( `ERROR! ${ eventPath } ${ desc } .` ) ;
73
+ break ;
74
+ case 'update' :
75
+ logs . push ( `${ colors . whiteBright ( 'UPDATE' ) } ${ eventPath } (${ event . content . length } bytes)` ) ;
76
+ files . add ( eventPath ) ;
77
+ break ;
78
+ case 'create' :
79
+ logs . push ( `${ colors . green ( 'CREATE' ) } ${ eventPath } (${ event . content . length } bytes)` ) ;
80
+ files . add ( eventPath ) ;
81
+ break ;
82
+ case 'delete' :
83
+ logs . push ( `${ colors . yellow ( 'DELETE' ) } ${ eventPath } ` ) ;
84
+ files . add ( eventPath ) ;
85
+ break ;
86
+ case 'rename' :
87
+ logs . push ( `${ colors . blue ( 'RENAME' ) } ${ eventPath } => ${ event . to } ` ) ;
88
+ files . add ( eventPath ) ;
89
+ break ;
90
+ }
91
+ } ) ;
92
+
93
+ const lifecycleSubscription = this . workflow . lifeCycle . subscribe ( event => {
94
+ if ( event . kind == 'end' || event . kind == 'post-tasks-start' ) {
95
+ if ( ! error ) {
96
+ // Output the logging queue, no error happened.
97
+ logs . forEach ( log => this . logger . info ( log ) ) ;
98
+ }
99
+ }
100
+ } ) ;
101
+
102
+ // TODO: Allow passing a schematic instance directly
103
+ try {
104
+ await this . workflow
105
+ . execute ( {
106
+ collection,
107
+ schematic,
108
+ options,
109
+ logger : this . logger ,
110
+ } )
111
+ . toPromise ( ) ;
112
+
113
+ reporterSubscription . unsubscribe ( ) ;
114
+ lifecycleSubscription . unsubscribe ( ) ;
115
+
116
+ return { success : ! error , files } ;
117
+ } catch ( e ) {
118
+ if ( e instanceof UnsuccessfulWorkflowExecution ) {
119
+ this . logger . error ( 'The update failed. See above.' ) ;
120
+ } else {
121
+ this . logger . fatal ( e . message ) ;
122
+ }
123
+
124
+ return { success : false , files } ;
125
+ }
126
+ }
127
+
128
+ async executeMigrations (
129
+ packageName : string ,
130
+ collectionPath : string ,
131
+ range : semver . Range ,
132
+ commit = false ,
133
+ ) {
134
+ const collection = this . workflow . engine . createCollection ( collectionPath ) ;
135
+
136
+ const migrations = [ ] ;
137
+ for ( const name of collection . listSchematicNames ( ) ) {
138
+ const schematic = this . workflow . engine . createSchematic ( name , collection ) ;
139
+ const description = schematic . description as typeof schematic . description & {
140
+ version ?: string ;
141
+ } ;
142
+ if ( ! description . version ) {
143
+ continue ;
144
+ }
145
+
146
+ if ( semver . satisfies ( description . version , range , { includePrerelease : true } ) ) {
147
+ migrations . push ( description as typeof schematic . description & { version : string } ) ;
148
+ }
149
+ }
150
+
151
+ if ( migrations . length === 0 ) {
152
+ return true ;
153
+ }
154
+
155
+ const startingGitSha = this . findCurrentGitSha ( ) ;
156
+
157
+ migrations . sort ( ( a , b ) => semver . compare ( a . version , b . version ) || a . name . localeCompare ( b . name ) ) ;
158
+
159
+ for ( const migration of migrations ) {
160
+ this . logger . info (
161
+ `** Executing migrations for version ${ migration . version } of package '${ packageName } ' **` ,
162
+ ) ;
163
+
164
+ const result = await this . executeSchematic ( migration . collection . name , migration . name ) ;
165
+ if ( ! result . success ) {
166
+ if ( startingGitSha !== null ) {
167
+ const currentGitSha = this . findCurrentGitSha ( ) ;
168
+ if ( currentGitSha !== startingGitSha ) {
169
+ this . logger . warn ( `git HEAD was at ${ startingGitSha } before migrations.` ) ;
170
+ }
171
+ }
172
+
173
+ return false ;
174
+ }
175
+
176
+ // Commit migration
177
+ if ( commit ) {
178
+ let message = `migrate workspace for ${ packageName } @${ migration . version } ` ;
179
+ if ( migration . description ) {
180
+ message += '\n' + migration . description ;
181
+ }
182
+ // TODO: Use result.files once package install tasks are accounted
183
+ this . createCommit ( message , [ ] ) ;
184
+ }
185
+ }
36
186
}
37
187
38
188
// tslint:disable-next-line:no-big-function
@@ -112,9 +262,9 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
112
262
this . workspace . configFile &&
113
263
oldConfigFileNames . includes ( this . workspace . configFile )
114
264
) {
115
- options . migrateOnly = true ;
116
- options . from = '1.0.0' ;
117
- }
265
+ options . migrateOnly = true ;
266
+ options . from = '1.0.0' ;
267
+ }
118
268
119
269
this . logger . info ( 'Collecting installed dependencies...' ) ;
120
270
@@ -125,19 +275,15 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
125
275
126
276
if ( options . all || packages . length === 0 ) {
127
277
// Either update all packages or show status
128
- return this . runSchematic ( {
129
- collectionName : '@schematics/update' ,
130
- schematicName : 'update' ,
131
- dryRun : ! ! options . dryRun ,
132
- showNothingDone : false ,
133
- additionalOptions : {
134
- force : options . force || false ,
135
- next : options . next || false ,
136
- verbose : options . verbose || false ,
137
- packageManager,
138
- packages : options . all ? Object . keys ( rootDependencies ) : [ ] ,
139
- } ,
278
+ const { success } = await this . executeSchematic ( '@schematics/update' , 'update' , {
279
+ force : options . force || false ,
280
+ next : options . next || false ,
281
+ verbose : options . verbose || false ,
282
+ packageManager,
283
+ packages : options . all ? Object . keys ( rootDependencies ) : [ ] ,
140
284
} ) ;
285
+
286
+ return success ? 0 : 1 ;
141
287
}
142
288
143
289
if ( options . migrateOnly ) {
@@ -153,6 +299,13 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
153
299
return 1 ;
154
300
}
155
301
302
+ const from = coerceVersionNumber ( options . from ) ;
303
+ if ( ! from ) {
304
+ this . logger . error ( `"from" value [${ options . from } ] is not a valid version.` ) ;
305
+
306
+ return 1 ;
307
+ }
308
+
156
309
if ( options . next ) {
157
310
this . logger . warn ( '"next" option has no effect when using "migrate-only" option.' ) ;
158
311
}
@@ -230,20 +383,18 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
230
383
}
231
384
}
232
385
233
- return this . runSchematic ( {
234
- collectionName : '@schematics/update' ,
235
- schematicName : 'migrate' ,
236
- dryRun : ! ! options . dryRun ,
237
- force : false ,
238
- showNothingDone : false ,
239
- additionalOptions : {
240
- package : packageName ,
241
- collection : migrations ,
242
- from : options . from ,
243
- verbose : options . verbose || false ,
244
- to : options . to || packageNode . package . version ,
245
- } ,
246
- } ) ;
386
+ const migrationRange = new semver . Range (
387
+ '>' + from + ' <=' + ( options . to || packageNode . package . version ) ,
388
+ ) ;
389
+
390
+ const result = await this . executeMigrations (
391
+ packageName ,
392
+ migrations ,
393
+ migrationRange ,
394
+ ! options . skipCommits ,
395
+ ) ;
396
+
397
+ return result ? 1 : 0 ;
247
398
}
248
399
249
400
const requests : {
@@ -287,7 +438,9 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
287
438
try {
288
439
// Metadata requests are internally cached; multiple requests for same name
289
440
// does not result in additional network traffic
290
- metadata = await fetchPackageMetadata ( packageName , this . logger , { verbose : options . verbose } ) ;
441
+ metadata = await fetchPackageMetadata ( packageName , this . logger , {
442
+ verbose : options . verbose ,
443
+ } ) ;
291
444
} catch ( e ) {
292
445
this . logger . error ( `Error fetching metadata for '${ packageName } ': ` + e . message ) ;
293
446
@@ -334,18 +487,14 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
334
487
return 0 ;
335
488
}
336
489
337
- return this . runSchematic ( {
338
- collectionName : '@schematics/update' ,
339
- schematicName : 'update' ,
340
- dryRun : ! ! options . dryRun ,
341
- showNothingDone : false ,
342
- additionalOptions : {
343
- verbose : options . verbose || false ,
344
- force : options . force || false ,
345
- packageManager,
346
- packages : packagesToUpdate ,
347
- } ,
490
+ const { success } = await this . executeSchematic ( '@schematics/update' , 'update' , {
491
+ verbose : options . verbose || false ,
492
+ force : options . force || false ,
493
+ packageManager,
494
+ packages : packagesToUpdate ,
348
495
} ) ;
496
+
497
+ return success ? 0 : 1 ;
349
498
}
350
499
351
500
checkCleanGit ( ) {
@@ -366,9 +515,46 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
366
515
return false ;
367
516
}
368
517
}
369
-
370
- } catch { }
518
+ } catch { }
371
519
372
520
return true ;
373
521
}
522
+
523
+ createCommit ( message : string , files : string [ ] ) {
524
+ try {
525
+ execSync ( 'git add -A ' + files . join ( ' ' ) , { encoding : 'utf8' , stdio : 'pipe' } ) ;
526
+
527
+ execSync ( `git commit --no-verify -m "${ message } "` , { encoding : 'utf8' , stdio : 'pipe' } ) ;
528
+ } catch ( error ) { }
529
+ }
530
+
531
+ findCurrentGitSha ( ) : string | null {
532
+ try {
533
+ const result = execSync ( 'git rev-parse HEAD' , { encoding : 'utf8' , stdio : 'pipe' } ) ;
534
+
535
+ return result . trim ( ) ;
536
+ } catch {
537
+ return null ;
538
+ }
539
+ }
540
+ }
541
+
542
+ function coerceVersionNumber ( version : string ) : string | null {
543
+ if ( ! version . match ( / ^ \d { 1 , 30 } \. \d { 1 , 30 } \. \d { 1 , 30 } / ) ) {
544
+ const match = version . match ( / ^ \d { 1 , 30 } ( \. \d { 1 , 30 } ) * / ) ;
545
+
546
+ if ( ! match ) {
547
+ return null ;
548
+ }
549
+
550
+ if ( ! match [ 1 ] ) {
551
+ version = version . substr ( 0 , match [ 0 ] . length ) + '.0.0' + version . substr ( match [ 0 ] . length ) ;
552
+ } else if ( ! match [ 2 ] ) {
553
+ version = version . substr ( 0 , match [ 0 ] . length ) + '.0' + version . substr ( match [ 0 ] . length ) ;
554
+ } else {
555
+ return null ;
556
+ }
557
+ }
558
+
559
+ return semver . valid ( version ) ;
374
560
}
0 commit comments