Skip to content

Commit 460ea21

Browse files
committed
fix(@angular/cli): logic which determines which temp version of the CLI is to be download during ng update
Previously, when using an older version of the Angular CLI, during `ng update`, we download the temporary `latest` version to run the update. The ensured that when running that the runner used to run the update contains the latest bug fixes and improvements. This however, can be problematic in some cases. Such as when there are API breaking changes, when running a relatively old schematic with the latest CLI can cause runtime issues, especially since those schematics were never meant to be executed on a CLI X major versions in the future. With this change, we improve the logic to determine which version of the Angular CLI should be used to run the update. Below is a summarization of this. - When using the `--next` command line argument, the `@next` version of the CLI will be used to run the update. - When updating an `@angular/` or `@nguniversal/` package, the target version will be used to run the update. Example: `ng update @angular/core@12`, the update will run on most recent patch version of `@angular/cli` of that major version `@12.2.6`. - When updating an `@angular/` or `@nguniversal/` and no target version is specified. Example: `ng update @angular/core` the update will run on most latest version of the `@angular/cli`. - When updating a third-party package, the most recent patch version of the installed `@angular/cli` will be used to run the update. Example if `13.0.0` is installed and `13.1.1` is available on NPM, the latter will be used. (cherry picked from commit 4632f1f)
1 parent 745d777 commit 460ea21

File tree

4 files changed

+188
-144
lines changed

4 files changed

+188
-144
lines changed

packages/angular/cli/commands/update-impl.ts

Lines changed: 97 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { execSync } from 'child_process';
1313
import * as fs from 'fs';
1414
import * as path from 'path';
1515
import * as semver from 'semver';
16+
import { VERSION } from '../lib/cli';
1617
import { PackageManager } from '../lib/config/schema';
1718
import { Command } from '../models/command';
1819
import { Arguments } from '../models/interface';
@@ -38,11 +39,6 @@ const pickManifest = require('npm-pick-manifest') as (
3839

3940
const oldConfigFileNames = ['.angular-cli.json', 'angular-cli.json'];
4041

41-
const NG_VERSION_9_POST_MSG = colors.cyan(
42-
'\nYour project has been updated to Angular version 9!\n' +
43-
'For more info, please see: https://v9.angular.io/guide/updating-to-version-9',
44-
);
45-
4642
/**
4743
* Disable CLI version mismatch checks and forces usage of the invoked CLI
4844
* instead of invoking the local installed version.
@@ -53,6 +49,8 @@ const disableVersionCheck =
5349
disableVersionCheckEnv !== '0' &&
5450
disableVersionCheckEnv.toLowerCase() !== 'false';
5551

52+
const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//;
53+
5654
export class UpdateCommand extends Command<UpdateCommandSchema> {
5755
public readonly allowMissingWorkspace = true;
5856
private workflow!: NodeWorkflow;
@@ -84,7 +82,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
8482
let logs: string[] = [];
8583
const files = new Set<string>();
8684

87-
const reporterSubscription = this.workflow.reporter.subscribe(event => {
85+
const reporterSubscription = this.workflow.reporter.subscribe((event) => {
8886
// Strip leading slash to prevent confusion.
8987
const eventPath = event.path.startsWith('/') ? event.path.substr(1) : event.path;
9088

@@ -114,11 +112,11 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
114112
}
115113
});
116114

117-
const lifecycleSubscription = this.workflow.lifeCycle.subscribe(event => {
115+
const lifecycleSubscription = this.workflow.lifeCycle.subscribe((event) => {
118116
if (event.kind == 'end' || event.kind == 'post-tasks-start') {
119117
if (!error) {
120118
// Output the logging queue, no error happened.
121-
logs.forEach(log => this.logger.info(log));
119+
logs.forEach((log) => this.logger.info(log));
122120
logs = [];
123121
}
124122
}
@@ -141,12 +139,14 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
141139
return { success: !error, files };
142140
} catch (e) {
143141
if (e instanceof UnsuccessfulWorkflowExecution) {
144-
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+
);
145145
} else {
146146
const logPath = writeErrorToLogFile(e);
147147
this.logger.fatal(
148148
`${colors.symbols.cross} Migration failed: ${e.message}\n` +
149-
` See "${logPath}" for further details.\n`,
149+
` See "${logPath}" for further details.\n`,
150150
);
151151
}
152152

@@ -164,7 +164,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
164164
commit?: boolean,
165165
): Promise<boolean> {
166166
const collection = this.workflow.engine.createCollection(collectionPath);
167-
const name = collection.listSchematicNames().find(name => name === migrationName);
167+
const name = collection.listSchematicNames().find((name) => name === migrationName);
168168
if (!name) {
169169
this.logger.error(`Cannot find migration '${migrationName}' in '${packageName}'.`);
170170

@@ -213,20 +213,20 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
213213
return true;
214214
}
215215

216-
this.logger.info(
217-
colors.cyan(`** Executing migrations of package '${packageName}' **\n`),
218-
);
216+
this.logger.info(colors.cyan(`** Executing migrations of package '${packageName}' **\n`));
219217

220218
return this.executePackageMigrations(migrations, packageName, commit);
221219
}
222220

223221
private async executePackageMigrations(
224-
migrations: Iterable<{ name: string; description: string; collection: { name: string }}>,
222+
migrations: Iterable<{ name: string; description: string; collection: { name: string } }>,
225223
packageName: string,
226224
commit = false,
227225
): Promise<boolean> {
228226
for (const migration of migrations) {
229-
this.logger.info(`${colors.symbols.pointer} ${migration.description.replace(/\. /g, '.\n ')}`);
227+
this.logger.info(
228+
`${colors.symbols.pointer} ${migration.description.replace(/\. /g, '.\n ')}`,
229+
);
230230

231231
const result = await this.executeSchematic(migration.collection.name, migration.name);
232232
if (!result.success) {
@@ -280,19 +280,27 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
280280
throw e;
281281
}
282282

283-
// Check if the current installed CLI version is older than the latest version.
284-
if (!disableVersionCheck && await this.checkCLILatestVersion(options.verbose, options.next)) {
285-
this.logger.warn(
286-
`The installed local Angular CLI version is older than the latest ${options.next ? 'pre-release' : 'stable'} version.\n` +
287-
'Installing a temporary version to perform the update.',
283+
// Check if the current installed CLI version is older than the latest compatible version.
284+
if (!disableVersionCheck) {
285+
const cliVersionToInstall = await this.checkCLIVersion(
286+
options['--'],
287+
options.verbose,
288+
options.next,
288289
);
289290

290-
return runTempPackageBin(
291-
`@angular/cli@${options.next ? 'next' : 'latest'}`,
292-
this.logger,
293-
this.packageManager,
294-
process.argv.slice(2),
295-
);
291+
if (cliVersionToInstall) {
292+
this.logger.warn(
293+
'The installed Angular CLI version is outdated.\n' +
294+
`Installing a temporary Angular CLI versioned ${cliVersionToInstall} to perform the update.`,
295+
);
296+
297+
return runTempPackageBin(
298+
`@angular/cli@${cliVersionToInstall}`,
299+
this.logger,
300+
this.packageManager,
301+
process.argv.slice(2),
302+
);
303+
}
296304
}
297305

298306
const packages: PackageIdentifier[] = [];
@@ -307,7 +315,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
307315
return 1;
308316
}
309317

310-
if (packages.some(v => v.name === packageIdentifier.name)) {
318+
if (packages.some((v) => v.name === packageIdentifier.name)) {
311319
this.logger.error(`Duplicate package '${packageIdentifier.name}' specified.`);
312320

313321
return 1;
@@ -410,7 +418,9 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
410418

411419
if (options.migrateOnly) {
412420
if (!options.from && typeof options.migrateOnly !== 'string') {
413-
this.logger.error('"from" option is required when using the "migrate-only" option without a migration name.');
421+
this.logger.error(
422+
'"from" option is required when using the "migrate-only" option without a migration name.',
423+
);
414424

415425
return 1;
416426
} else if (packages.length !== 1) {
@@ -436,7 +446,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
436446
// Allow running migrations on transitively installed dependencies
437447
// There can technically be nested multiple versions
438448
// TODO: If multiple, this should find all versions and ask which one to use
439-
const child = packageTree.children.find(c => c.name === packageName);
449+
const child = packageTree.children.find((c) => c.name === packageName);
440450
if (child) {
441451
packageNode = child;
442452
}
@@ -471,8 +481,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
471481

472482
if (migrations.startsWith('../')) {
473483
this.logger.error(
474-
'Package contains an invalid migrations field. ' +
475-
'Paths outside the package root are not permitted.',
484+
'Package contains an invalid migrations field. Paths outside the package root are not permitted.',
476485
);
477486

478487
return 1;
@@ -498,14 +507,15 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
498507
}
499508
}
500509

501-
let success = false;
502510
if (typeof options.migrateOnly == 'string') {
503-
success = await this.executeMigration(
511+
await this.executeMigration(
504512
packageName,
505513
migrations,
506514
options.migrateOnly,
507515
options.createCommits,
508516
);
517+
518+
return 0;
509519
} else {
510520
const from = coerceVersionNumber(options.from);
511521
if (!from) {
@@ -518,28 +528,15 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
518528
'>' + from + ' <=' + (options.to || packageNode.package.version),
519529
);
520530

521-
success = await this.executeMigrations(
531+
await this.executeMigrations(
522532
packageName,
523533
migrations,
524534
migrationRange,
525535
options.createCommits,
526536
);
527-
}
528-
529-
if (success) {
530-
if (
531-
packageName === '@angular/core'
532-
&& options.from
533-
&& +options.from.split('.')[0] < 9
534-
&& (options.to || packageNode.package.version).split('.')[0] === '9'
535-
) {
536-
this.logger.info(NG_VERSION_9_POST_MSG);
537-
}
538537

539538
return 0;
540539
}
541-
542-
return 1;
543540
}
544541

545542
const requests: {
@@ -634,7 +631,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
634631
continue;
635632
}
636633

637-
if (node.package && /^@(?:angular|nguniversal)\//.test(node.package.name)) {
634+
if (node.package && ANGULAR_PACKAGES_REGEXP.test(node.package.name)) {
638635
const { name, version } = node.package;
639636
const toBeInstalledMajorVersion = +manifest.version.split('.')[0];
640637
const currentMajorVersion = +version.split('.')[0];
@@ -681,7 +678,8 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
681678

682679
if (success && options.createCommits) {
683680
const committed = this.commit(
684-
`Angular CLI update for packages - ${packagesToUpdate.join(', ')}`);
681+
`Angular CLI update for packages - ${packagesToUpdate.join(', ')}`,
682+
);
685683
if (!committed) {
686684
return 1;
687685
}
@@ -711,10 +709,6 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
711709
return 0;
712710
}
713711
}
714-
715-
if (migrations.some(m => m.package === '@angular/core' && m.to.split('.')[0] === '9' && +m.from.split('.')[0] < 9)) {
716-
this.logger.info(NG_VERSION_9_POST_MSG);
717-
}
718712
}
719713

720714
return success ? 0 : 1;
@@ -744,8 +738,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
744738
try {
745739
createCommit(message);
746740
} catch (err) {
747-
this.logger.error(
748-
`Failed to commit update (${message}):\n${err.stderr}`);
741+
this.logger.error(`Failed to commit update (${message}):\n${err.stderr}`);
749742

750743
return false;
751744
}
@@ -754,8 +747,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
754747
const hash = findCurrentGitSha();
755748
const shortMessage = message.split('\n')[0];
756749
if (hash) {
757-
this.logger.info(` Committed migration step (${getShortHash(hash)}): ${
758-
shortMessage}.`);
750+
this.logger.info(` Committed migration step (${getShortHash(hash)}): ${shortMessage}.`);
759751
} else {
760752
// Commit was successful, but reading the hash was not. Something weird happened,
761753
// but nothing that would stop the update. Just log the weirdness and continue.
@@ -768,7 +760,10 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
768760

769761
private checkCleanGit(): boolean {
770762
try {
771-
const topLevel = execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: 'pipe' });
763+
const topLevel = execSync('git rev-parse --show-toplevel', {
764+
encoding: 'utf8',
765+
stdio: 'pipe',
766+
});
772767
const result = execSync('git status --porcelain', { encoding: 'utf8', stdio: 'pipe' });
773768
if (result.trim().length === 0) {
774769
return true;
@@ -791,22 +786,55 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
791786
}
792787

793788
/**
794-
* Checks if the current installed CLI version is older than the latest version.
795-
* @returns `true` when the installed version is older.
796-
*/
797-
private async checkCLILatestVersion(verbose = false, next = false): Promise<boolean> {
798-
const { version: installedCLIVersion } = require('../package.json');
799-
800-
const LatestCLIManifest = await fetchPackageManifest(
801-
`@angular/cli@${next ? 'next' : 'latest'}`,
789+
* Checks if the current installed CLI version is older or newer than a compatible version.
790+
* @returns the version to install or null when there is no update to install.
791+
*/
792+
private async checkCLIVersion(
793+
packagesToUpdate: string[] | undefined,
794+
verbose = false,
795+
next = false,
796+
): Promise<string | null> {
797+
const { version } = await fetchPackageManifest(
798+
`@angular/cli@${this.getCLIUpdateRunnerVersion(packagesToUpdate, next)}`,
802799
this.logger,
803800
{
804801
verbose,
805802
usingYarn: this.packageManager === PackageManager.Yarn,
806803
},
807804
);
808805

809-
return semver.lt(installedCLIVersion, LatestCLIManifest.version);
806+
return VERSION.full === version ? null : version;
807+
}
808+
809+
private getCLIUpdateRunnerVersion(
810+
packagesToUpdate: string[] | undefined,
811+
next: boolean,
812+
): string | number {
813+
if (next) {
814+
return 'next';
815+
}
816+
817+
const updatingAngularPackage = packagesToUpdate?.find((r) => ANGULAR_PACKAGES_REGEXP.test(r));
818+
if (updatingAngularPackage) {
819+
// If we are updating any Angular package we can update the CLI to the target version because
820+
// migrations for @angular/core@13 can be executed using Angular/cli@13.
821+
// This is same behaviour as `npx @angular/cli@13 update @angular/core@13`.
822+
823+
// `@angular/cli@13` -> ['', 'angular/cli', '13']
824+
// `@angular/cli` -> ['', 'angular/cli']
825+
const tempVersion = coerceVersionNumber(updatingAngularPackage.split('@')[2]);
826+
827+
return semver.parse(tempVersion)?.major ?? 'latest';
828+
}
829+
830+
// When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in.
831+
// Typically, we can assume that the `@angular/cli` was updated previously.
832+
// Example: Angular official packages are typically updated prior to NGRX etc...
833+
// Therefore, we only update to the latest patch version of the installed major version of the Angular CLI.
834+
835+
// This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12.
836+
// We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic.
837+
return VERSION.major;
810838
}
811839
}
812840

@@ -839,7 +867,7 @@ function createCommit(message: string) {
839867
*/
840868
function findCurrentGitSha(): string | null {
841869
try {
842-
const hash = execSync('git rev-parse HEAD', {encoding: 'utf8', stdio: 'pipe'});
870+
const hash = execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: 'pipe' });
843871

844872
return hash.trim();
845873
} catch {

packages/angular/cli/lib/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { getWorkspaceRaw } from '../../utilities/config';
1313
import { writeErrorToLogFile } from '../../utilities/log-file';
1414
import { getWorkspaceDetails } from '../../utilities/project';
1515

16+
export { VERSION, Version } from '../../models/version';
17+
1618
const debugEnv = process.env['NG_DEBUG'];
1719
const isDebug =
1820
debugEnv !== undefined &&

0 commit comments

Comments
 (0)