@@ -11,6 +11,7 @@ import { execSync } from 'child_process';
11
11
import * as fs from 'fs' ;
12
12
import * as path from 'path' ;
13
13
import * as semver from 'semver' ;
14
+ import { VERSION } from '../lib/cli' ;
14
15
import { PackageManager } from '../lib/config/schema' ;
15
16
import { Command } from '../models/command' ;
16
17
import { Arguments } from '../models/interface' ;
@@ -42,10 +43,6 @@ const pickManifest = require('npm-pick-manifest') as (
42
43
43
44
const oldConfigFileNames = [ '.angular-cli.json' , 'angular-cli.json' ] ;
44
45
45
- const NG_VERSION_9_POST_MSG = colors . cyan (
46
- '\nYour project has been updated to Angular version 9!\n' +
47
- 'For more info, please see: https://v9.angular.io/guide/updating-to-version-9' ,
48
- ) ;
49
46
50
47
/**
51
48
* Disable CLI version mismatch checks and forces usage of the invoked CLI
@@ -57,24 +54,23 @@ const disableVersionCheck =
57
54
disableVersionCheckEnv !== '0' &&
58
55
disableVersionCheckEnv . toLowerCase ( ) !== 'false' ;
59
56
57
+ const ANGULAR_PACKAGES_REGEXP = / ^ @ (?: a n g u l a r | n g u n i v e r s a l ) \/ / ;
58
+
60
59
export class UpdateCommand extends Command < UpdateCommandSchema > {
61
60
public readonly allowMissingWorkspace = true ;
62
61
private workflow ! : NodeWorkflow ;
63
62
private packageManager = PackageManager . Npm ;
64
63
65
64
async initialize ( ) {
66
65
this . packageManager = await getPackageManager ( this . context . root ) ;
67
- this . workflow = new NodeWorkflow (
68
- this . context . root ,
69
- {
70
- packageManager : this . packageManager ,
71
- // __dirname -> favor @schematics/update from this package
72
- // Otherwise, use packages from the active workspace (migrations)
73
- resolvePaths : [ __dirname , this . context . root ] ,
74
- schemaValidation : true ,
75
- engineHostCreator : ( options ) => new SchematicEngineHost ( options . resolvePaths ) ,
76
- } ,
77
- ) ;
66
+ this . workflow = new NodeWorkflow ( this . context . root , {
67
+ packageManager : this . packageManager ,
68
+ // __dirname -> favor @schematics/update from this package
69
+ // Otherwise, use packages from the active workspace (migrations)
70
+ resolvePaths : [ __dirname , this . context . root ] ,
71
+ schemaValidation : true ,
72
+ engineHostCreator : ( options ) => new SchematicEngineHost ( options . resolvePaths ) ,
73
+ } ) ;
78
74
}
79
75
80
76
private async executeSchematic (
@@ -86,7 +82,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
86
82
let logs : string [ ] = [ ] ;
87
83
const files = new Set < string > ( ) ;
88
84
89
- const reporterSubscription = this . workflow . reporter . subscribe ( event => {
85
+ const reporterSubscription = this . workflow . reporter . subscribe ( ( event ) => {
90
86
// Strip leading slash to prevent confusion.
91
87
const eventPath = event . path . startsWith ( '/' ) ? event . path . substr ( 1 ) : event . path ;
92
88
@@ -116,11 +112,11 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
116
112
}
117
113
} ) ;
118
114
119
- const lifecycleSubscription = this . workflow . lifeCycle . subscribe ( event => {
115
+ const lifecycleSubscription = this . workflow . lifeCycle . subscribe ( ( event ) => {
120
116
if ( event . kind == 'end' || event . kind == 'post-tasks-start' ) {
121
117
if ( ! error ) {
122
118
// Output the logging queue, no error happened.
123
- logs . forEach ( log => this . logger . info ( ` ${ log } ` ) ) ;
119
+ logs . forEach ( ( log ) => this . logger . info ( ` ${ log } ` ) ) ;
124
120
logs = [ ] ;
125
121
}
126
122
}
@@ -143,12 +139,14 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
143
139
return { success : ! error , files } ;
144
140
} catch ( e ) {
145
141
if ( e instanceof UnsuccessfulWorkflowExecution ) {
146
- this . logger . error ( `${ colors . symbols . cross } Migration failed. See above for further details.\n` ) ;
142
+ this . logger . error (
143
+ `${ colors . symbols . cross } Migration failed. See above for further details.\n` ,
144
+ ) ;
147
145
} else {
148
146
const logPath = writeErrorToLogFile ( e ) ;
149
147
this . logger . fatal (
150
148
`${ colors . symbols . cross } Migration failed: ${ e . message } \n` +
151
- ` See "${ logPath } " for further details.\n` ,
149
+ ` See "${ logPath } " for further details.\n` ,
152
150
) ;
153
151
}
154
152
@@ -166,7 +164,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
166
164
commit ?: boolean ,
167
165
) : Promise < boolean > {
168
166
const collection = this . workflow . engine . createCollection ( collectionPath ) ;
169
- const name = collection . listSchematicNames ( ) . find ( name => name === migrationName ) ;
167
+ const name = collection . listSchematicNames ( ) . find ( ( name ) => name === migrationName ) ;
170
168
if ( ! name ) {
171
169
this . logger . error ( `Cannot find migration '${ migrationName } ' in '${ packageName } '.` ) ;
172
170
@@ -215,24 +213,23 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
215
213
return true ;
216
214
}
217
215
218
- this . logger . info (
219
- colors . cyan ( `** Executing migrations of package '${ packageName } ' **\n` ) ,
220
- ) ;
216
+ this . logger . info ( colors . cyan ( `** Executing migrations of package '${ packageName } ' **\n` ) ) ;
221
217
222
218
return this . executePackageMigrations ( migrations , packageName , commit ) ;
223
219
}
224
220
225
221
private async executePackageMigrations (
226
- migrations : Iterable < { name : string ; description : string ; collection : { name : string } } > ,
222
+ migrations : Iterable < { name : string ; description : string ; collection : { name : string } } > ,
227
223
packageName : string ,
228
224
commit = false ,
229
225
) : Promise < boolean > {
230
226
for ( const migration of migrations ) {
231
227
const [ title , ...description ] = migration . description . split ( '. ' ) ;
232
228
233
229
this . logger . info (
234
- colors . cyan ( colors . symbols . pointer ) + ' ' +
235
- colors . bold ( title . endsWith ( '.' ) ? title : title + '.' ) ,
230
+ colors . cyan ( colors . symbols . pointer ) +
231
+ ' ' +
232
+ colors . bold ( title . endsWith ( '.' ) ? title : title + '.' ) ,
236
233
) ;
237
234
238
235
if ( description . length ) {
@@ -269,19 +266,27 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
269
266
async run ( options : UpdateCommandSchema & Arguments ) {
270
267
await ensureCompatibleNpm ( this . context . root ) ;
271
268
272
- // Check if the current installed CLI version is older than the latest version.
273
- if ( ! disableVersionCheck && await this . checkCLILatestVersion ( options . verbose , options . next ) ) {
274
- this . logger . warn (
275
- `The installed local Angular CLI version is older than the latest ${ options . next ? 'pre-release' : 'stable' } version.\n` +
276
- 'Installing a temporary version to perform the update.' ,
269
+ // Check if the current installed CLI version is older than the latest compatible version.
270
+ if ( ! disableVersionCheck ) {
271
+ const cliVersionToInstall = await this . checkCLIVersion (
272
+ options [ '--' ] ,
273
+ options . verbose ,
274
+ options . next ,
277
275
) ;
278
276
279
- return runTempPackageBin (
280
- `@angular/cli@${ options . next ? 'next' : 'latest' } ` ,
281
- this . logger ,
282
- this . packageManager ,
283
- process . argv . slice ( 2 ) ,
284
- ) ;
277
+ if ( cliVersionToInstall ) {
278
+ this . logger . warn (
279
+ 'The installed Angular CLI version is outdated.\n' +
280
+ `Installing a temporary Angular CLI versioned ${ cliVersionToInstall } to perform the update.` ,
281
+ ) ;
282
+
283
+ return runTempPackageBin (
284
+ `@angular/cli@${ cliVersionToInstall } ` ,
285
+ this . logger ,
286
+ this . packageManager ,
287
+ process . argv . slice ( 2 ) ,
288
+ ) ;
289
+ }
285
290
}
286
291
287
292
const logVerbose = ( message : string ) => {
@@ -291,9 +296,10 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
291
296
} ;
292
297
293
298
if ( options . all ) {
294
- const updateCmd = this . packageManager === PackageManager . Yarn
295
- ? `'yarn upgrade-interactive' or 'yarn upgrade'`
296
- : `'${ this . packageManager } update'` ;
299
+ const updateCmd =
300
+ this . packageManager === PackageManager . Yarn
301
+ ? `'yarn upgrade-interactive' or 'yarn upgrade'`
302
+ : `'${ this . packageManager } update'` ;
297
303
298
304
this . logger . warn ( `
299
305
'--all' functionality has been removed as updating multiple packages at once is not recommended.
@@ -316,7 +322,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
316
322
return 1 ;
317
323
}
318
324
319
- if ( packages . some ( v => v . name === packageIdentifier . name ) ) {
325
+ if ( packages . some ( ( v ) => v . name === packageIdentifier . name ) ) {
320
326
this . logger . error ( `Duplicate package '${ packageIdentifier . name } ' specified.` ) ;
321
327
322
328
return 1 ;
@@ -397,7 +403,9 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
397
403
398
404
if ( options . migrateOnly ) {
399
405
if ( ! options . from && typeof options . migrateOnly !== 'string' ) {
400
- this . logger . error ( '"from" option is required when using the "migrate-only" option without a migration name.' ) ;
406
+ this . logger . error (
407
+ '"from" option is required when using the "migrate-only" option without a migration name.' ,
408
+ ) ;
401
409
402
410
return 1 ;
403
411
} else if ( packages . length !== 1 ) {
@@ -460,8 +468,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
460
468
461
469
if ( migrations . startsWith ( '../' ) ) {
462
470
this . logger . error (
463
- 'Package contains an invalid migrations field. ' +
464
- 'Paths outside the package root are not permitted.' ,
471
+ 'Package contains an invalid migrations field. Paths outside the package root are not permitted.' ,
465
472
) ;
466
473
467
474
return 1 ;
@@ -487,9 +494,8 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
487
494
}
488
495
}
489
496
490
- let success = false ;
491
497
if ( typeof options . migrateOnly == 'string' ) {
492
- success = await this . executeMigration (
498
+ await this . executeMigration (
493
499
packageName ,
494
500
migrations ,
495
501
options . migrateOnly ,
@@ -506,28 +512,14 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
506
512
const migrationRange = new semver . Range (
507
513
'>' + from + ' <=' + ( options . to || packageNode . version ) ,
508
514
) ;
509
-
510
- success = await this . executeMigrations (
515
+ await this . executeMigrations (
511
516
packageName ,
512
517
migrations ,
513
518
migrationRange ,
514
519
options . createCommits ,
515
520
) ;
516
521
}
517
522
518
- if ( success ) {
519
- if (
520
- packageName === '@angular/core'
521
- && options . from
522
- && + options . from . split ( '.' ) [ 0 ] < 9
523
- && ( options . to || packageNode . version ) . split ( '.' ) [ 0 ] === '9'
524
- ) {
525
- this . logger . info ( NG_VERSION_9_POST_MSG ) ;
526
- }
527
-
528
- return 0 ;
529
- }
530
-
531
523
return 1 ;
532
524
}
533
525
@@ -623,7 +615,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
623
615
continue ;
624
616
}
625
617
626
- if ( node . package && / ^ @ (?: a n g u l a r | n g u n i v e r s a l ) \/ / . test ( node . package . name ) ) {
618
+ if ( node . package && ANGULAR_PACKAGES_REGEXP . test ( node . package . name ) ) {
627
619
const { name, version } = node . package ;
628
620
const toBeInstalledMajorVersion = + manifest . version . split ( '.' ) [ 0 ] ;
629
621
const currentMajorVersion = + version . split ( '.' ) [ 0 ] ;
@@ -670,7 +662,8 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
670
662
671
663
if ( success && options . createCommits ) {
672
664
const committed = this . commit (
673
- `Angular CLI update for packages - ${ packagesToUpdate . join ( ', ' ) } ` ) ;
665
+ `Angular CLI update for packages - ${ packagesToUpdate . join ( ', ' ) } ` ,
666
+ ) ;
674
667
if ( ! committed ) {
675
668
return 1 ;
676
669
}
@@ -759,10 +752,6 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
759
752
return 0 ;
760
753
}
761
754
}
762
-
763
- if ( migrations . some ( m => m . package === '@angular/core' && m . to . split ( '.' ) [ 0 ] === '9' && + m . from . split ( '.' ) [ 0 ] < 9 ) ) {
764
- this . logger . info ( NG_VERSION_9_POST_MSG ) ;
765
- }
766
755
}
767
756
768
757
return success ? 0 : 1 ;
@@ -792,8 +781,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
792
781
try {
793
782
createCommit ( message ) ;
794
783
} catch ( err ) {
795
- this . logger . error (
796
- `Failed to commit update (${ message } ):\n${ err . stderr } ` ) ;
784
+ this . logger . error ( `Failed to commit update (${ message } ):\n${ err . stderr } ` ) ;
797
785
798
786
return false ;
799
787
}
@@ -802,8 +790,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
802
790
const hash = findCurrentGitSha ( ) ;
803
791
const shortMessage = message . split ( '\n' ) [ 0 ] ;
804
792
if ( hash ) {
805
- this . logger . info ( ` Committed migration step (${ getShortHash ( hash ) } ): ${
806
- shortMessage } .`) ;
793
+ this . logger . info ( ` Committed migration step (${ getShortHash ( hash ) } ): ${ shortMessage } .` ) ;
807
794
} else {
808
795
// Commit was successful, but reading the hash was not. Something weird happened,
809
796
// but nothing that would stop the update. Just log the weirdness and continue.
@@ -816,7 +803,10 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
816
803
817
804
private checkCleanGit ( ) : boolean {
818
805
try {
819
- const topLevel = execSync ( 'git rev-parse --show-toplevel' , { encoding : 'utf8' , stdio : 'pipe' } ) ;
806
+ const topLevel = execSync ( 'git rev-parse --show-toplevel' , {
807
+ encoding : 'utf8' ,
808
+ stdio : 'pipe' ,
809
+ } ) ;
820
810
const result = execSync ( 'git status --porcelain' , { encoding : 'utf8' , stdio : 'pipe' } ) ;
821
811
if ( result . trim ( ) . length === 0 ) {
822
812
return true ;
@@ -839,22 +829,55 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
839
829
}
840
830
841
831
/**
842
- * Checks if the current installed CLI version is older than the latest version.
843
- * @returns `true` when the installed version is older.
844
- */
845
- private async checkCLILatestVersion ( verbose = false , next = false ) : Promise < boolean > {
846
- const { version : installedCLIVersion } = require ( '../package.json' ) ;
847
-
848
- const LatestCLIManifest = await fetchPackageManifest (
849
- `@angular/cli@${ next ? 'next' : 'latest' } ` ,
832
+ * Checks if the current installed CLI version is older or newer than a compatible version.
833
+ * @returns the version to install or null when there is no update to install.
834
+ */
835
+ private async checkCLIVersion (
836
+ packagesToUpdate : string [ ] | undefined ,
837
+ verbose = false ,
838
+ next = false ,
839
+ ) : Promise < string | null > {
840
+ const { version } = await fetchPackageManifest (
841
+ `@angular/cli@${ this . getCLIUpdateRunnerVersion ( packagesToUpdate , next ) } ` ,
850
842
this . logger ,
851
843
{
852
844
verbose,
853
845
usingYarn : this . packageManager === PackageManager . Yarn ,
854
846
} ,
855
847
) ;
856
848
857
- return semver . lt ( installedCLIVersion , LatestCLIManifest . version ) ;
849
+ return VERSION . full === version ? null : version ;
850
+ }
851
+
852
+ private getCLIUpdateRunnerVersion (
853
+ packagesToUpdate : string [ ] | undefined ,
854
+ next : boolean ,
855
+ ) : string | number {
856
+ if ( next ) {
857
+ return 'next' ;
858
+ }
859
+
860
+ const updatingAngularPackage = packagesToUpdate ?. find ( ( r ) => ANGULAR_PACKAGES_REGEXP . test ( r ) ) ;
861
+ if ( updatingAngularPackage ) {
862
+ // If we are updating any Angular package we can update the CLI to the target version because
863
+ // migrations for @angular /core@13 can be executed using Angular/cli@13.
864
+ // This is same behaviour as `npx @angular/cli@13 update @angular/core@13`.
865
+
866
+ // `@angular/cli@13` -> ['', 'angular/cli', '13']
867
+ // `@angular/cli` -> ['', 'angular/cli']
868
+ const tempVersion = coerceVersionNumber ( updatingAngularPackage . split ( '@' ) [ 2 ] ) ;
869
+
870
+ return semver . parse ( tempVersion ) ?. major ?? 'latest' ;
871
+ }
872
+
873
+ // When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in.
874
+ // Typically, we can assume that the `@angular/cli` was updated previously.
875
+ // Example: Angular official packages are typically updated prior to NGRX etc...
876
+ // Therefore, we only update to the latest patch version of the installed major version of the Angular CLI.
877
+
878
+ // This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12.
879
+ // We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic.
880
+ return VERSION . major ;
858
881
}
859
882
}
860
883
@@ -887,7 +910,7 @@ function createCommit(message: string) {
887
910
*/
888
911
function findCurrentGitSha ( ) : string | null {
889
912
try {
890
- const hash = execSync ( 'git rev-parse HEAD' , { encoding : 'utf8' , stdio : 'pipe' } ) ;
913
+ const hash = execSync ( 'git rev-parse HEAD' , { encoding : 'utf8' , stdio : 'pipe' } ) ;
891
914
892
915
return hash . trim ( ) ;
893
916
} catch {
0 commit comments