55 * Use of this source code is governed by an MIT-style license that can be
66 * found in the LICENSE file at https://angular.io/license
77 */
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' ;
812import { execSync } from 'child_process' ;
913import * as fs from 'fs' ;
1014import * as path from 'path' ;
1115import * 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' ;
1419import { getPackageManager } from '../utilities/package-manager' ;
1520import {
1621 PackageIdentifier ,
@@ -28,11 +33,156 @@ const npa = require('npm-package-arg');
2833
2934const oldConfigFileNames = [ '.angular-cli.json' , 'angular-cli.json' ] ;
3035
31- export class UpdateCommand extends SchematicCommand < UpdateCommandSchema > {
36+ export class UpdateCommand extends Command < UpdateCommandSchema > {
3237 public readonly allowMissingWorkspace = true ;
3338
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+ }
36186 }
37187
38188 // tslint:disable-next-line:no-big-function
@@ -112,9 +262,9 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
112262 this . workspace . configFile &&
113263 oldConfigFileNames . includes ( this . workspace . configFile )
114264 ) {
115- options . migrateOnly = true ;
116- options . from = '1.0.0' ;
117- }
265+ options . migrateOnly = true ;
266+ options . from = '1.0.0' ;
267+ }
118268
119269 this . logger . info ( 'Collecting installed dependencies...' ) ;
120270
@@ -125,19 +275,15 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
125275
126276 if ( options . all || packages . length === 0 ) {
127277 // 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 ) : [ ] ,
140284 } ) ;
285+
286+ return success ? 0 : 1 ;
141287 }
142288
143289 if ( options . migrateOnly ) {
@@ -153,6 +299,13 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
153299 return 1 ;
154300 }
155301
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+
156309 if ( options . next ) {
157310 this . logger . warn ( '"next" option has no effect when using "migrate-only" option.' ) ;
158311 }
@@ -230,20 +383,18 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
230383 }
231384 }
232385
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 ;
247398 }
248399
249400 const requests : {
@@ -287,7 +438,9 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
287438 try {
288439 // Metadata requests are internally cached; multiple requests for same name
289440 // 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+ } ) ;
291444 } catch ( e ) {
292445 this . logger . error ( `Error fetching metadata for '${ packageName } ': ` + e . message ) ;
293446
@@ -334,18 +487,14 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
334487 return 0 ;
335488 }
336489
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 ,
348495 } ) ;
496+
497+ return success ? 0 : 1 ;
349498 }
350499
351500 checkCleanGit ( ) {
@@ -366,9 +515,46 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
366515 return false ;
367516 }
368517 }
369-
370- } catch { }
518+ } catch { }
371519
372520 return true ;
373521 }
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 ) ;
374560}
0 commit comments