Skip to content

Commit e07bd05

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 30295b3 commit e07bd05

File tree

5 files changed

+93
-100
lines changed

5 files changed

+93
-100
lines changed

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

Lines changed: 67 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,6 @@ const pickManifest = require('npm-pick-manifest') as (
4242
selector: string,
4343
) => PackageManifest;
4444

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-
5045
const UPDATE_SCHEMATIC_COLLECTION = path.join(
5146
__dirname,
5247
'../src/commands/update/schematic/collection.json',
@@ -62,6 +57,8 @@ const disableVersionCheck =
6257
disableVersionCheckEnv !== '0' &&
6358
disableVersionCheckEnv.toLowerCase() !== 'false';
6459

60+
const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//;
61+
6562
export class UpdateCommand extends Command<UpdateCommandSchema> {
6663
public override readonly allowMissingWorkspace = true;
6764
private workflow!: NodeWorkflow;
@@ -277,19 +274,26 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
277274
async run(options: UpdateCommandSchema & Arguments) {
278275
await ensureCompatibleNpm(this.context.root);
279276

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

288-
return runTempPackageBin(
289-
`@angular/cli@${options.next ? 'next' : 'latest'}`,
290-
this.packageManager,
291-
process.argv.slice(2),
292-
);
285+
if (cliVersionToInstall) {
286+
this.logger.warn(
287+
'The installed Angular CLI version is outdated.\n' +
288+
`Installing a temporary Angular CLI versioned ${cliVersionToInstall} to perform the update.`,
289+
);
290+
291+
return runTempPackageBin(
292+
`@angular/cli@${cliVersionToInstall}`,
293+
this.packageManager,
294+
process.argv.slice(2),
295+
);
296+
}
293297
}
294298

295299
const logVerbose = (message: string) => {
@@ -457,8 +461,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
457461

458462
if (migrations.startsWith('../')) {
459463
this.logger.error(
460-
'Package contains an invalid migrations field. ' +
461-
'Paths outside the package root are not permitted.',
464+
'Package contains an invalid migrations field. Paths outside the package root are not permitted.',
462465
);
463466

464467
return 1;
@@ -484,9 +487,9 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
484487
}
485488
}
486489

487-
let success = false;
490+
let result: boolean;
488491
if (typeof options.migrateOnly == 'string') {
489-
success = await this.executeMigration(
492+
result = await this.executeMigration(
490493
packageName,
491494
migrations,
492495
options.migrateOnly,
@@ -500,7 +503,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
500503
return 1;
501504
}
502505

503-
success = await this.executeMigrations(
506+
result = await this.executeMigrations(
504507
packageName,
505508
migrations,
506509
from,
@@ -509,20 +512,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
509512
);
510513
}
511514

512-
if (success) {
513-
if (
514-
packageName === '@angular/core' &&
515-
options.from &&
516-
+options.from.split('.')[0] < 9 &&
517-
(options.to || packageNode.version).split('.')[0] === '9'
518-
) {
519-
this.logger.info(NG_VERSION_9_POST_MSG);
520-
}
521-
522-
return 0;
523-
}
524-
525-
return 1;
515+
return result ? 0 : 1;
526516
}
527517

528518
const requests: {
@@ -617,7 +607,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
617607
continue;
618608
}
619609

620-
if (node.package && /^@(?:angular|nguniversal)\//.test(node.package.name)) {
610+
if (node.package && ANGULAR_PACKAGES_REGEXP.test(node.package.name)) {
621611
const { name, version } = node.package;
622612
const toBeInstalledMajorVersion = +manifest.version.split('.')[0];
623613
const currentMajorVersion = +version.split('.')[0];
@@ -774,17 +764,6 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
774764
return 0;
775765
}
776766
}
777-
778-
if (
779-
migrations.some(
780-
(m) =>
781-
m.package === '@angular/core' &&
782-
m.to.split('.')[0] === '9' &&
783-
+m.from.split('.')[0] < 9,
784-
)
785-
) {
786-
this.logger.info(NG_VERSION_9_POST_MSG);
787-
}
788767
}
789768

790769
return success ? 0 : 1;
@@ -862,22 +841,55 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
862841
}
863842

864843
/**
865-
* Checks if the current installed CLI version is older than the latest version.
866-
* @returns `true` when the installed version is older.
844+
* Checks if the current installed CLI version is older or newer than a compatible version.
845+
* @returns the version to install or null when there is no update to install.
867846
*/
868-
private async checkCLILatestVersion(verbose = false, next = false): Promise<boolean> {
869-
const installedCLIVersion = VERSION.full;
870-
871-
const LatestCLIManifest = await fetchPackageManifest(
872-
`@angular/cli@${next ? 'next' : 'latest'}`,
847+
private async checkCLIVersion(
848+
packagesToUpdate: string[] | undefined,
849+
verbose = false,
850+
next = false,
851+
): Promise<string | null> {
852+
const { version } = await fetchPackageManifest(
853+
`@angular/cli@${this.getCLIUpdateRunnerVersion(packagesToUpdate, next)}`,
873854
this.logger,
874855
{
875856
verbose,
876857
usingYarn: this.packageManager === PackageManager.Yarn,
877858
},
878859
);
879860

880-
return semver.lt(installedCLIVersion, LatestCLIManifest.version);
861+
return VERSION.full === version ? null : version;
862+
}
863+
864+
private getCLIUpdateRunnerVersion(
865+
packagesToUpdate: string[] | undefined,
866+
next: boolean,
867+
): string | number {
868+
if (next) {
869+
return 'next';
870+
}
871+
872+
const updatingAngularPackage = packagesToUpdate?.find((r) => ANGULAR_PACKAGES_REGEXP.test(r));
873+
if (updatingAngularPackage) {
874+
// If we are updating any Angular package we can update the CLI to the target version because
875+
// migrations for @angular/core@13 can be executed using Angular/cli@13.
876+
// This is same behaviour as `npx @angular/cli@13 update @angular/core@13`.
877+
878+
// `@angular/cli@13` -> ['', 'angular/cli', '13']
879+
// `@angular/cli` -> ['', 'angular/cli']
880+
const tempVersion = coerceVersionNumber(updatingAngularPackage.split('@')[2]);
881+
882+
return semver.parse(tempVersion)?.major ?? 'latest';
883+
}
884+
885+
// When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in.
886+
// Typically, we can assume that the `@angular/cli` was updated previously.
887+
// Example: Angular official packages are typically updated prior to NGRX etc...
888+
// Therefore, we only update to the latest patch version of the installed major version of the Angular CLI.
889+
890+
// This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12.
891+
// We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic.
892+
return VERSION.major;
881893
}
882894
}
883895

packages/angular/cli/lib/init.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ import { isWarningEnabled } from '../utilities/config';
7373
if (isGlobalGreater) {
7474
// If using the update command and the global version is greater, use the newer update command
7575
// This allows improvements in update to be used in older versions that do not have bootstrapping
76-
if (process.argv[2] === 'update') {
76+
if (
77+
process.argv[2] === 'update' &&
78+
cli.VERSION &&
79+
cli.VERSION.major - globalVersion.major <= 1
80+
) {
7781
cli = await import('./cli');
7882
} else if (await isWarningEnabled('versionMismatch')) {
7983
// Otherwise, use local version and warn if global is newer than local

tests/legacy-cli/e2e/tests/misc/npm-7.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { rimraf, writeFile } from '../../utils/fs';
1+
import { rimraf } from '../../utils/fs';
22
import { getActivePackageManager } from '../../utils/packages';
33
import { ng, npm } from '../../utils/process';
4+
import { isPrereleaseCli } from '../../utils/project';
45
import { expectToFail } from '../../utils/utils';
56

67
const warningText = 'npm version 7.5.6 or higher is recommended';
78

8-
export default async function() {
9+
export default async function () {
910
// Only relevant with npm as a package manager
1011
if (getActivePackageManager() !== 'npm') {
1112
return;
@@ -17,12 +18,18 @@ export default async function() {
1718
}
1819

1920
const currentDirectory = process.cwd();
21+
22+
const extraArgs = [];
23+
if (isPrereleaseCli()) {
24+
extraArgs.push('--next');
25+
}
26+
2027
try {
2128
// Install version >=7.5.6
2229
await npm('install', '--global', 'npm@>=7.5.6');
2330

2431
// Ensure `ng update` does not show npm warning
25-
const { stderr: stderrUpdate1 } = await ng('update');
32+
const { stderr: stderrUpdate1 } = await ng('update', ...extraArgs);
2633
if (stderrUpdate1.includes(warningText)) {
2734
throw new Error('ng update expected to not show npm version warning.');
2835
}
@@ -37,7 +44,7 @@ export default async function() {
3744
}
3845

3946
// Ensure `ng update` shows npm warning
40-
const { stderr: stderrUpdate2 } = await ng('update');
47+
const { stderr: stderrUpdate2 } = await ng('update', ...extraArgs);
4148
if (!stderrUpdate2.includes(warningText)) {
4249
throw new Error('ng update expected to show npm version warning.');
4350
}
@@ -85,5 +92,4 @@ export default async function() {
8592
// Reset version back to 6.x
8693
await npm('install', '--global', 'npm@6');
8794
}
88-
8995
}

tests/legacy-cli/e2e/tests/update/update-multiple-versions.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
11
import { ng } from '../../utils/process';
22
import { createNpmConfigForAuthentication } from '../../utils/registry';
33
import { expectToFail } from '../../utils/utils';
4+
import { isPrereleaseCli } from '../../utils/project';
45

56
export default async function () {
67
// The environment variable has priority over the .npmrc
78
delete process.env['NPM_CONFIG_REGISTRY'];
89
const worksMessage = 'We analyzed your package.json';
910

11+
const extraArgs = [];
12+
if (isPrereleaseCli()) {
13+
extraArgs.push('--next');
14+
}
15+
1016
// Valid authentication token
1117
await createNpmConfigForAuthentication(false);
12-
const { stdout: stdout1 } = await ng('update');
18+
const { stdout: stdout1 } = await ng('update', ...extraArgs);
1319
if (!stdout1.includes(worksMessage)) {
1420
throw new Error(`Expected stdout to contain "${worksMessage}"`);
1521
}
1622

1723
await createNpmConfigForAuthentication(true);
18-
const { stdout: stdout2 } = await ng('update');
24+
const { stdout: stdout2 } = await ng('update', ...extraArgs);
1925
if (!stdout2.includes(worksMessage)) {
2026
throw new Error(`Expected stdout to contain "${worksMessage}"`);
2127
}
2228

2329
// Invalid authentication token
2430
await createNpmConfigForAuthentication(false, true);
25-
await expectToFail(() => ng('update'));
31+
await expectToFail(() => ng('update', ...extraArgs));
2632

2733
await createNpmConfigForAuthentication(true, true);
28-
await expectToFail(() => ng('update'));
34+
await expectToFail(() => ng('update', ...extraArgs));
2935
}

0 commit comments

Comments
 (0)