Skip to content

Commit 250a58b

Browse files
committedNov 23, 2021
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 1e9e890)
1 parent 6650765 commit 250a58b

File tree

4 files changed

+91
-64
lines changed

4 files changed

+91
-64
lines changed
 

Diff for: ‎packages/angular/cli/commands/update-impl.ts

+65-54
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ import {
3737
} from '../utilities/package-tree';
3838
import { Schema as UpdateCommandSchema } from './update';
3939

40-
const NG_VERSION_9_POST_MSG = colors.cyan(
41-
'\nYour project has been updated to Angular version 9!\n' +
42-
'For more info, please see: https://v9.angular.io/guide/updating-to-version-9',
43-
);
44-
4540
const UPDATE_SCHEMATIC_COLLECTION = path.join(
4641
__dirname,
4742
'../src/commands/update/schematic/collection.json',
@@ -57,6 +52,8 @@ const disableVersionCheck =
5752
disableVersionCheckEnv !== '0' &&
5853
disableVersionCheckEnv.toLowerCase() !== 'false';
5954

55+
const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//;
56+
6057
export class UpdateCommand extends Command<UpdateCommandSchema> {
6158
public override readonly allowMissingWorkspace = true;
6259
private workflow!: NodeWorkflow;
@@ -272,19 +269,26 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
272269
async run(options: UpdateCommandSchema & Arguments) {
273270
await ensureCompatibleNpm(this.context.root);
274271

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

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

290294
const logVerbose = (message: string) => {
@@ -452,8 +456,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
452456

453457
if (migrations.startsWith('../')) {
454458
this.logger.error(
455-
'Package contains an invalid migrations field. ' +
456-
'Paths outside the package root are not permitted.',
459+
'Package contains an invalid migrations field. Paths outside the package root are not permitted.',
457460
);
458461

459462
return 1;
@@ -479,9 +482,8 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
479482
}
480483
}
481484

482-
let success = false;
483485
if (typeof options.migrateOnly == 'string') {
484-
success = await this.executeMigration(
486+
await this.executeMigration(
485487
packageName,
486488
migrations,
487489
options.migrateOnly,
@@ -495,7 +497,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
495497
return 1;
496498
}
497499

498-
success = await this.executeMigrations(
500+
await this.executeMigrations(
499501
packageName,
500502
migrations,
501503
from,
@@ -504,19 +506,6 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
504506
);
505507
}
506508

507-
if (success) {
508-
if (
509-
packageName === '@angular/core' &&
510-
options.from &&
511-
+options.from.split('.')[0] < 9 &&
512-
(options.to || packageNode.version).split('.')[0] === '9'
513-
) {
514-
this.logger.info(NG_VERSION_9_POST_MSG);
515-
}
516-
517-
return 0;
518-
}
519-
520509
return 1;
521510
}
522511

@@ -612,7 +601,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
612601
continue;
613602
}
614603

615-
if (node.package && /^@(?:angular|nguniversal)\//.test(node.package.name)) {
604+
if (node.package && ANGULAR_PACKAGES_REGEXP.test(node.package.name)) {
616605
const { name, version } = node.package;
617606
const toBeInstalledMajorVersion = +manifest.version.split('.')[0];
618607
const currentMajorVersion = +version.split('.')[0];
@@ -791,17 +780,6 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
791780
return 0;
792781
}
793782
}
794-
795-
if (
796-
migrations.some(
797-
(m) =>
798-
m.package === '@angular/core' &&
799-
m.to.split('.')[0] === '9' &&
800-
+m.from.split('.')[0] < 9,
801-
)
802-
) {
803-
this.logger.info(NG_VERSION_9_POST_MSG);
804-
}
805783
}
806784

807785
return success ? 0 : 1;
@@ -879,22 +857,55 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
879857
}
880858

881859
/**
882-
* Checks if the current installed CLI version is older than the latest version.
883-
* @returns `true` when the installed version is older.
860+
* Checks if the current installed CLI version is older or newer than a compatible version.
861+
* @returns the version to install or null when there is no update to install.
884862
*/
885-
private async checkCLILatestVersion(verbose = false, next = false): Promise<boolean> {
886-
const installedCLIVersion = VERSION.full;
887-
888-
const LatestCLIManifest = await fetchPackageManifest(
889-
`@angular/cli@${next ? 'next' : 'latest'}`,
863+
private async checkCLIVersion(
864+
packagesToUpdate: string[] | undefined,
865+
verbose = false,
866+
next = false,
867+
): Promise<string | null> {
868+
const { version } = await fetchPackageManifest(
869+
`@angular/cli@${this.getCLIUpdateRunnerVersion(packagesToUpdate, next)}`,
890870
this.logger,
891871
{
892872
verbose,
893873
usingYarn: this.packageManager === PackageManager.Yarn,
894874
},
895875
);
896876

897-
return semver.lt(installedCLIVersion, LatestCLIManifest.version);
877+
return VERSION.full === version ? null : version;
878+
}
879+
880+
private getCLIUpdateRunnerVersion(
881+
packagesToUpdate: string[] | undefined,
882+
next: boolean,
883+
): string | number {
884+
if (next) {
885+
return 'next';
886+
}
887+
888+
const updatingAngularPackage = packagesToUpdate?.find((r) => ANGULAR_PACKAGES_REGEXP.test(r));
889+
if (updatingAngularPackage) {
890+
// If we are updating any Angular package we can update the CLI to the target version because
891+
// migrations for @angular/core@13 can be executed using Angular/cli@13.
892+
// This is same behaviour as `npx @angular/cli@13 update @angular/core@13`.
893+
894+
// `@angular/cli@13` -> ['', 'angular/cli', '13']
895+
// `@angular/cli` -> ['', 'angular/cli']
896+
const tempVersion = coerceVersionNumber(updatingAngularPackage.split('@')[2]);
897+
898+
return semver.parse(tempVersion)?.major ?? 'latest';
899+
}
900+
901+
// When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in.
902+
// Typically, we can assume that the `@angular/cli` was updated previously.
903+
// Example: Angular official packages are typically updated prior to NGRX etc...
904+
// Therefore, we only update to the latest patch version of the installed major version of the Angular CLI.
905+
906+
// This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12.
907+
// We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic.
908+
return VERSION.major;
898909
}
899910
}
900911

Diff for: ‎packages/angular/cli/lib/init.ts

+5-1
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

Diff for: ‎tests/legacy-cli/e2e/tests/misc/npm-7.ts

+11-5
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
}
+10-4
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)
Please sign in to comment.