@@ -13,15 +13,13 @@ import {
1313 NodeWorkflow ,
1414} from '@angular-devkit/schematics/tools' ;
1515import { Listr } from 'listr2' ;
16- import { SpawnSyncReturns , execSync , spawnSync } from 'node:child_process' ;
16+ import { SpawnSyncReturns } from 'node:child_process' ;
1717import { existsSync , promises as fs } from 'node:fs' ;
1818import { createRequire } from 'node:module' ;
1919import * as path from 'node:path' ;
20- import { join , resolve } from 'node:path' ;
2120import npa from 'npm-package-arg' ;
2221import * as semver from 'semver' ;
2322import { Argv } from 'yargs' ;
24- import { PackageManager } from '../../../lib/config/workspace-schema' ;
2523import {
2624 CommandModule ,
2725 CommandModuleError ,
@@ -37,7 +35,6 @@ import { writeErrorToLogFile } from '../../utilities/log-file';
3735import {
3836 PackageIdentifier ,
3937 PackageManifest ,
40- fetchPackageManifest ,
4138 fetchPackageMetadata ,
4239} from '../../utilities/package-metadata' ;
4340import {
@@ -48,7 +45,20 @@ import {
4845} from '../../utilities/package-tree' ;
4946import { askChoices } from '../../utilities/prompt' ;
5047import { isTTY } from '../../utilities/tty' ;
51- import { VERSION } from '../../utilities/version' ;
48+ import {
49+ checkCLIVersion ,
50+ coerceVersionNumber ,
51+ runTempBinary ,
52+ shouldForcePackageManager ,
53+ } from './utilities/cli-version' ;
54+ import { ANGULAR_PACKAGES_REGEXP } from './utilities/constants' ;
55+ import {
56+ checkCleanGit ,
57+ createCommit ,
58+ findCurrentGitSha ,
59+ getShortHash ,
60+ hasChangesToCommit ,
61+ } from './utilities/git' ;
5262
5363interface UpdateCommandArgs {
5464 packages ?: string [ ] ;
@@ -63,8 +73,10 @@ interface UpdateCommandArgs {
6373 'create-commits' : boolean ;
6474}
6575
66- interface MigrationSchematicDescription
67- extends SchematicDescription < FileSystemCollectionDescription , FileSystemSchematicDescription > {
76+ interface MigrationSchematicDescription extends SchematicDescription <
77+ FileSystemCollectionDescription ,
78+ FileSystemSchematicDescription
79+ > {
6880 version ?: string ;
6981 optional ?: boolean ;
7082 recommended ?: boolean ;
@@ -77,7 +89,6 @@ interface MigrationSchematicDescriptionWithVersion extends MigrationSchematicDes
7789
7890class CommandError extends Error { }
7991
80- const ANGULAR_PACKAGES_REGEXP = / ^ @ (?: a n g u l a r | n g u n i v e r s a l ) \/ / ;
8192const UPDATE_SCHEMATIC_COLLECTION = path . join ( __dirname , 'schematic/collection.json' ) ;
8293
8394export default class UpdateCommandModule extends CommandModule < UpdateCommandArgs > {
@@ -87,7 +98,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
8798
8899 command = 'update [packages..]' ;
89100 describe = 'Updates your workspace and its dependencies. See https://update.angular.dev/.' ;
90- longDescriptionPath = join ( __dirname , 'long-description.md' ) ;
101+ longDescriptionPath = path . join ( __dirname , 'long-description.md' ) ;
91102
92103 builder ( localYargs : Argv ) : Argv < UpdateCommandArgs > {
93104 return localYargs
@@ -161,7 +172,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
161172 const { logger } = this . context ;
162173
163174 // This allows the user to easily reset any changes from the update.
164- if ( packages ?. length && ! this . checkCleanGit ( ) ) {
175+ if ( packages ?. length && ! checkCleanGit ( this . context . root ) ) {
165176 if ( allowDirty ) {
166177 logger . warn (
167178 'Repository is not clean. Update changes will be mixed with pre-existing changes.' ,
@@ -192,8 +203,10 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
192203 // Check if the current installed CLI version is older than the latest compatible version.
193204 // Skip when running `ng update` without a package name as this will not trigger an actual update.
194205 if ( ! disableVersionCheck && options . packages ?. length ) {
195- const cliVersionToInstall = await this . checkCLIVersion (
206+ const cliVersionToInstall = await checkCLIVersion (
196207 options . packages ,
208+ logger ,
209+ packageManager ,
197210 options . verbose ,
198211 options . next ,
199212 ) ;
@@ -204,7 +217,11 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
204217 `Installing a temporary Angular CLI versioned ${ cliVersionToInstall } to perform the update.` ,
205218 ) ;
206219
207- return this . runTempBinary ( `@angular/cli@${ cliVersionToInstall } ` , process . argv . slice ( 2 ) ) ;
220+ return runTempBinary (
221+ `@angular/cli@${ cliVersionToInstall } ` ,
222+ packageManager ,
223+ process . argv . slice ( 2 ) ,
224+ ) ;
208225 }
209226 }
210227
@@ -254,7 +271,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
254271
255272 const workflow = new NodeWorkflow ( this . context . root , {
256273 packageManager : packageManager . name ,
257- packageManagerForce : this . packageManagerForce ( options . verbose ) ,
274+ packageManagerForce : shouldForcePackageManager ( packageManager , logger , options . verbose ) ,
258275 // __dirname -> favor @schematics/update from this package
259276 // Otherwise, use packages from the active workspace (migrations)
260277 resolvePaths : this . resolvePaths ,
@@ -771,7 +788,9 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
771788
772789 if ( success ) {
773790 const { root : commandRoot , packageManager } = this . context ;
774- const installArgs = this . packageManagerForce ( options . verbose ) ? [ '--force' ] : [ ] ;
791+ const installArgs = shouldForcePackageManager ( packageManager , logger , options . verbose )
792+ ? [ '--force' ]
793+ : [ ] ;
775794 const tasks = new Listr ( [
776795 {
777796 title : 'Cleaning node modules directory' ,
@@ -961,158 +980,6 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
961980 return true ;
962981 }
963982
964- private checkCleanGit ( ) : boolean {
965- try {
966- const topLevel = execSync ( 'git rev-parse --show-toplevel' , {
967- encoding : 'utf8' ,
968- stdio : 'pipe' ,
969- } ) ;
970- const result = execSync ( 'git status --porcelain' , { encoding : 'utf8' , stdio : 'pipe' } ) ;
971- if ( result . trim ( ) . length === 0 ) {
972- return true ;
973- }
974-
975- // Only files inside the workspace root are relevant
976- for ( const entry of result . split ( '\n' ) ) {
977- const relativeEntry = path . relative (
978- path . resolve ( this . context . root ) ,
979- path . resolve ( topLevel . trim ( ) , entry . slice ( 3 ) . trim ( ) ) ,
980- ) ;
981-
982- if ( ! relativeEntry . startsWith ( '..' ) && ! path . isAbsolute ( relativeEntry ) ) {
983- return false ;
984- }
985- }
986- } catch { }
987-
988- return true ;
989- }
990-
991- /**
992- * Checks if the current installed CLI version is older or newer than a compatible version.
993- * @returns the version to install or null when there is no update to install.
994- */
995- private async checkCLIVersion (
996- packagesToUpdate : string [ ] ,
997- verbose = false ,
998- next = false ,
999- ) : Promise < string | null > {
1000- const { version } = await fetchPackageManifest (
1001- `@angular/cli@${ this . getCLIUpdateRunnerVersion ( packagesToUpdate , next ) } ` ,
1002- this . context . logger ,
1003- {
1004- verbose,
1005- usingYarn : this . context . packageManager . name === PackageManager . Yarn ,
1006- } ,
1007- ) ;
1008-
1009- return VERSION . full === version ? null : version ;
1010- }
1011-
1012- private getCLIUpdateRunnerVersion (
1013- packagesToUpdate : string [ ] | undefined ,
1014- next : boolean ,
1015- ) : string | number {
1016- if ( next ) {
1017- return 'next' ;
1018- }
1019-
1020- const updatingAngularPackage = packagesToUpdate ?. find ( ( r ) => ANGULAR_PACKAGES_REGEXP . test ( r ) ) ;
1021- if ( updatingAngularPackage ) {
1022- // If we are updating any Angular package we can update the CLI to the target version because
1023- // migrations for @angular /core@13 can be executed using Angular/cli@13.
1024- // This is same behaviour as `npx @angular/cli@13 update @angular/core@13`.
1025-
1026- // `@angular/cli@13` -> ['', 'angular/cli', '13']
1027- // `@angular/cli` -> ['', 'angular/cli']
1028- const tempVersion = coerceVersionNumber ( updatingAngularPackage . split ( '@' ) [ 2 ] ) ;
1029-
1030- return semver . parse ( tempVersion ) ?. major ?? 'latest' ;
1031- }
1032-
1033- // When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in.
1034- // Typically, we can assume that the `@angular/cli` was updated previously.
1035- // Example: Angular official packages are typically updated prior to NGRX etc...
1036- // Therefore, we only update to the latest patch version of the installed major version of the Angular CLI.
1037-
1038- // This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12.
1039- // We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic.
1040- return VERSION . major ;
1041- }
1042-
1043- private async runTempBinary ( packageName : string , args : string [ ] = [ ] ) : Promise < number > {
1044- const { success, tempNodeModules } = await this . context . packageManager . installTemp ( packageName ) ;
1045- if ( ! success ) {
1046- return 1 ;
1047- }
1048-
1049- // Remove version/tag etc... from package name
1050- // Ex: @angular /cli@latest -> @angular/cli
1051- const packageNameNoVersion = packageName . substring ( 0 , packageName . lastIndexOf ( '@' ) ) ;
1052- const pkgLocation = join ( tempNodeModules , packageNameNoVersion ) ;
1053- const packageJsonPath = join ( pkgLocation , 'package.json' ) ;
1054-
1055- // Get a binary location for this package
1056- let binPath : string | undefined ;
1057- if ( existsSync ( packageJsonPath ) ) {
1058- const content = await fs . readFile ( packageJsonPath , 'utf-8' ) ;
1059- if ( content ) {
1060- const { bin = { } } = JSON . parse ( content ) as { bin : Record < string , string > } ;
1061- const binKeys = Object . keys ( bin ) ;
1062-
1063- if ( binKeys . length ) {
1064- binPath = resolve ( pkgLocation , bin [ binKeys [ 0 ] ] ) ;
1065- }
1066- }
1067- }
1068-
1069- if ( ! binPath ) {
1070- throw new Error ( `Cannot locate bin for temporary package: ${ packageNameNoVersion } .` ) ;
1071- }
1072-
1073- const { status, error } = spawnSync ( process . execPath , [ binPath , ...args ] , {
1074- stdio : 'inherit' ,
1075- env : {
1076- ...process . env ,
1077- NG_DISABLE_VERSION_CHECK : 'true' ,
1078- NG_CLI_ANALYTICS : 'false' ,
1079- } ,
1080- } ) ;
1081-
1082- if ( status === null && error ) {
1083- throw error ;
1084- }
1085-
1086- return status ?? 0 ;
1087- }
1088-
1089- private packageManagerForce ( verbose : boolean ) : boolean {
1090- // npm 7+ can fail due to it incorrectly resolving peer dependencies that have valid SemVer
1091- // ranges during an update. Update will set correct versions of dependencies within the
1092- // package.json file. The force option is set to workaround these errors.
1093- // Example error:
1094- // npm ERR! Conflicting peer dependency: @angular/compiler-cli@14.0.0-rc.0
1095- // npm ERR! node_modules/@angular/compiler-cli
1096- // npm ERR! peer @angular/compiler-cli@"^14.0.0 || ^14.0.0-rc" from @angular-devkit/build-angular@14.0.0-rc.0
1097- // npm ERR! node_modules/@angular-devkit/build-angular
1098- // npm ERR! dev @angular-devkit/build-angular@"~14.0.0-rc.0" from the root project
1099- if (
1100- this . context . packageManager . name === PackageManager . Npm &&
1101- this . context . packageManager . version &&
1102- semver . gte ( this . context . packageManager . version , '7.0.0' )
1103- ) {
1104- if ( verbose ) {
1105- this . context . logger . info (
1106- 'NPM 7+ detected -- enabling force option for package installation' ,
1107- ) ;
1108- }
1109-
1110- return true ;
1111- }
1112-
1113- return false ;
1114- }
1115-
1116983 private async getOptionalMigrationsToRun (
1117984 optionalMigrations : MigrationSchematicDescription [ ] ,
1118985 packageName : string ,
@@ -1161,68 +1028,6 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
11611028 }
11621029}
11631030
1164- /**
1165- * @return Whether or not the working directory has Git changes to commit.
1166- */
1167- function hasChangesToCommit ( ) : boolean {
1168- // List all modified files not covered by .gitignore.
1169- // If any files are returned, then there must be something to commit.
1170-
1171- return execSync ( 'git ls-files -m -d -o --exclude-standard' ) . toString ( ) !== '' ;
1172- }
1173-
1174- /**
1175- * Precondition: Must have pending changes to commit, they do not need to be staged.
1176- * Postcondition: The Git working tree is committed and the repo is clean.
1177- * @param message The commit message to use.
1178- */
1179- function createCommit ( message : string ) {
1180- // Stage entire working tree for commit.
1181- execSync ( 'git add -A' , { encoding : 'utf8' , stdio : 'pipe' } ) ;
1182-
1183- // Commit with the message passed via stdin to avoid bash escaping issues.
1184- execSync ( 'git commit --no-verify -F -' , { encoding : 'utf8' , stdio : 'pipe' , input : message } ) ;
1185- }
1186-
1187- /**
1188- * @return The Git SHA hash of the HEAD commit. Returns null if unable to retrieve the hash.
1189- */
1190- function findCurrentGitSha ( ) : string | null {
1191- try {
1192- return execSync ( 'git rev-parse HEAD' , { encoding : 'utf8' , stdio : 'pipe' } ) . trim ( ) ;
1193- } catch {
1194- return null ;
1195- }
1196- }
1197-
1198- function getShortHash ( commitHash : string ) : string {
1199- return commitHash . slice ( 0 , 9 ) ;
1200- }
1201-
1202- function coerceVersionNumber ( version : string | undefined ) : string | undefined {
1203- if ( ! version ) {
1204- return undefined ;
1205- }
1206-
1207- if ( ! / ^ \d { 1 , 30 } \. \d { 1 , 30 } \. \d { 1 , 30 } / . test ( version ) ) {
1208- const match = version . match ( / ^ \d { 1 , 30 } ( \. \d { 1 , 30 } ) * / ) ;
1209-
1210- if ( ! match ) {
1211- return undefined ;
1212- }
1213-
1214- if ( ! match [ 1 ] ) {
1215- version = version . substring ( 0 , match [ 0 ] . length ) + '.0.0' + version . substring ( match [ 0 ] . length ) ;
1216- } else if ( ! match [ 2 ] ) {
1217- version = version . substring ( 0 , match [ 0 ] . length ) + '.0' + version . substring ( match [ 0 ] . length ) ;
1218- } else {
1219- return undefined ;
1220- }
1221- }
1222-
1223- return semver . valid ( version ) ?? undefined ;
1224- }
1225-
12261031function getMigrationTitleAndDescription ( migration : MigrationSchematicDescription ) : {
12271032 title : string ;
12281033 description : string ;
0 commit comments