Skip to content

v12 ng update fixes #22223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 94 additions & 77 deletions packages/angular/cli/commands/update-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@ const pickManifest = require('npm-pick-manifest') as (
selector: string,
) => PackageManifest;

const NG_VERSION_9_POST_MSG = colors.cyan(
'\nYour project has been updated to Angular version 9!\n' +
'For more info, please see: https://v9.angular.io/guide/updating-to-version-9',
);

const UPDATE_SCHEMATIC_COLLECTION = path.join(
__dirname,
'../src/commands/update/schematic/collection.json',
Expand All @@ -62,6 +57,8 @@ const disableVersionCheck =
disableVersionCheckEnv !== '0' &&
disableVersionCheckEnv.toLowerCase() !== 'false';

const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//;

export class UpdateCommand extends Command<UpdateCommandSchema> {
public override readonly allowMissingWorkspace = true;
private workflow!: NodeWorkflow;
Expand Down Expand Up @@ -277,19 +274,26 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
async run(options: UpdateCommandSchema & Arguments) {
await ensureCompatibleNpm(this.context.root);

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

return runTempPackageBin(
`@angular/cli@${options.next ? 'next' : 'latest'}`,
this.packageManager,
process.argv.slice(2),
);
if (cliVersionToInstall) {
this.logger.warn(
'The installed Angular CLI version is outdated.\n' +
`Installing a temporary Angular CLI versioned ${cliVersionToInstall} to perform the update.`,
);

return runTempPackageBin(
`@angular/cli@${cliVersionToInstall}`,
this.packageManager,
process.argv.slice(2),
);
}
}

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

if (migrations.startsWith('../')) {
this.logger.error(
'Package contains an invalid migrations field. ' +
'Paths outside the package root are not permitted.',
'Package contains an invalid migrations field. Paths outside the package root are not permitted.',
);

return 1;
Expand All @@ -484,9 +487,9 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
}
}

let success = false;
let result: boolean;
if (typeof options.migrateOnly == 'string') {
success = await this.executeMigration(
result = await this.executeMigration(
packageName,
migrations,
options.migrateOnly,
Expand All @@ -500,7 +503,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
return 1;
}

success = await this.executeMigrations(
result = await this.executeMigrations(
packageName,
migrations,
from,
Expand All @@ -509,20 +512,7 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
);
}

if (success) {
if (
packageName === '@angular/core' &&
options.from &&
+options.from.split('.')[0] < 9 &&
(options.to || packageNode.version).split('.')[0] === '9'
) {
this.logger.info(NG_VERSION_9_POST_MSG);
}

return 0;
}

return 1;
return result ? 0 : 1;
}

const requests: {
Expand Down Expand Up @@ -612,35 +602,40 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
return 1;
}

if (node.package?.name === '@angular/cli') {
// Migrations for non LTS versions of Angular CLI are no longer included in @schematics/angular v12.
if (manifest.version === node.package?.version) {
this.logger.info(`Package '${packageName}' is already up to date.`);
continue;
}

if (node.package && ANGULAR_PACKAGES_REGEXP.test(node.package.name)) {
const { name, version } = node.package;
const toBeInstalledMajorVersion = +manifest.version.split('.')[0];
const currentMajorVersion = +node.package.version.split('.')[0];
if (currentMajorVersion < 9 && toBeInstalledMajorVersion >= 12) {
const updateVersions: Record<number, number> = {
1: 6,
6: 7,
7: 8,
8: 9,
};

const updateTo = updateVersions[currentMajorVersion];
this.logger.error(
'Updating multiple major versions at once is not recommended. ' +
`Run 'ng update @angular/cli@${updateTo}' in your workspace directory ` +
`to update to latest '${updateTo}.x' version of '@angular/cli'.\n\n` +
'For more information about the update process, see https://update.angular.io/.',
);
const currentMajorVersion = +version.split('.')[0];

if (toBeInstalledMajorVersion - currentMajorVersion > 1) {
// Only allow updating a single version at a time.
if (currentMajorVersion < 6) {
// Before version 6, the major versions were not always sequential.
// Example @angular/core skipped version 3, @angular/cli skipped versions 2-5.
this.logger.error(
`Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` +
`For more information about the update process, see https://update.angular.io/.`,
);
} else {
const nextMajorVersionFromCurrent = currentMajorVersion + 1;

this.logger.error(
`Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` +
`Run 'ng update ${name}@${nextMajorVersionFromCurrent}' in your workspace directory ` +
`to update to latest '${nextMajorVersionFromCurrent}.x' version of '${name}'.\n\n` +
`For more information about the update process, see https://update.angular.io/?v=${currentMajorVersion}.0-${nextMajorVersionFromCurrent}.0`,
);
}

return 1;
}
}

if (manifest.version === node.package?.version) {
this.logger.info(`Package '${packageName}' is already up to date.`);
continue;
}

packagesToUpdate.push(requestIdentifier.toString());
}

Expand Down Expand Up @@ -769,17 +764,6 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
return 0;
}
}

if (
migrations.some(
(m) =>
m.package === '@angular/core' &&
m.to.split('.')[0] === '9' &&
+m.from.split('.')[0] < 9,
)
) {
this.logger.info(NG_VERSION_9_POST_MSG);
}
}

return success ? 0 : 1;
Expand Down Expand Up @@ -857,22 +841,55 @@ export class UpdateCommand extends Command<UpdateCommandSchema> {
}

/**
* Checks if the current installed CLI version is older than the latest version.
* @returns `true` when the installed version is older.
* Checks if the current installed CLI version is older or newer than a compatible version.
* @returns the version to install or null when there is no update to install.
*/
private async checkCLILatestVersion(verbose = false, next = false): Promise<boolean> {
const installedCLIVersion = VERSION.full;

const LatestCLIManifest = await fetchPackageManifest(
`@angular/cli@${next ? 'next' : 'latest'}`,
private async checkCLIVersion(
packagesToUpdate: string[] | undefined,
verbose = false,
next = false,
): Promise<string | null> {
const { version } = await fetchPackageManifest(
`@angular/cli@${this.getCLIUpdateRunnerVersion(packagesToUpdate, next)}`,
this.logger,
{
verbose,
usingYarn: this.packageManager === PackageManager.Yarn,
},
);

return semver.lt(installedCLIVersion, LatestCLIManifest.version);
return VERSION.full === version ? null : version;
}

private getCLIUpdateRunnerVersion(
packagesToUpdate: string[] | undefined,
next: boolean,
): string | number {
if (next) {
return 'next';
}

const updatingAngularPackage = packagesToUpdate?.find((r) => ANGULAR_PACKAGES_REGEXP.test(r));
if (updatingAngularPackage) {
// If we are updating any Angular package we can update the CLI to the target version because
// migrations for @angular/core@13 can be executed using Angular/cli@13.
// This is same behaviour as `npx @angular/cli@13 update @angular/core@13`.

// `@angular/cli@13` -> ['', 'angular/cli', '13']
// `@angular/cli` -> ['', 'angular/cli']
const tempVersion = coerceVersionNumber(updatingAngularPackage.split('@')[2]);

return semver.parse(tempVersion)?.major ?? 'latest';
}

// When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in.
// Typically, we can assume that the `@angular/cli` was updated previously.
// Example: Angular official packages are typically updated prior to NGRX etc...
// Therefore, we only update to the latest patch version of the installed major version of the Angular CLI.

// This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12.
// We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic.
return VERSION.major;
}
}

Expand Down
6 changes: 5 additions & 1 deletion packages/angular/cli/lib/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ import { isWarningEnabled } from '../utilities/config';
if (isGlobalGreater) {
// If using the update command and the global version is greater, use the newer update command
// This allows improvements in update to be used in older versions that do not have bootstrapping
if (process.argv[2] === 'update') {
if (
process.argv[2] === 'update' &&
cli.VERSION &&
cli.VERSION.major - globalVersion.major <= 1
) {
cli = await import('./cli');
} else if (await isWarningEnabled('versionMismatch')) {
// Otherwise, use local version and warn if global is newer than local
Expand Down
36 changes: 32 additions & 4 deletions packages/angular/cli/src/commands/update/schematic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,13 +422,39 @@ function _usageMessage(
const packageGroups = new Map<string, string>();
const packagesToUpdate = [...infoMap.entries()]
.map(([name, info]) => {
const tag = options.next
let tag = options.next
? info.npmPackageJson['dist-tags']['next']
? 'next'
: 'latest'
: 'latest';
const version = info.npmPackageJson['dist-tags'][tag];
const target = info.npmPackageJson.versions[version];
let version = info.npmPackageJson['dist-tags'][tag];
let target = info.npmPackageJson.versions[version];

const versionDiff = semver.diff(info.installed.version, version);
if (
versionDiff !== 'patch' &&
versionDiff !== 'minor' &&
/^@(?:angular|nguniversal)\//.test(name)
) {
const installedMajorVersion = semver.parse(info.installed.version)?.major;
const toInstallMajorVersion = semver.parse(version)?.major;
if (
installedMajorVersion !== undefined &&
toInstallMajorVersion !== undefined &&
installedMajorVersion < toInstallMajorVersion - 1
) {
const nextMajorVersion = `${installedMajorVersion + 1}.`;
const nextMajorVersions = Object.keys(info.npmPackageJson.versions)
.filter((v) => v.startsWith(nextMajorVersion))
.sort((a, b) => (a > b ? -1 : 1));

if (nextMajorVersions.length) {
version = nextMajorVersions[0];
target = info.npmPackageJson.versions[version];
tag = '';
}
}
}

return {
name,
Expand Down Expand Up @@ -462,7 +488,9 @@ function _usageMessage(
}

let command = `ng update ${name}`;
if (tag == 'next') {
if (!tag) {
command += `@${semver.parse(version)?.major || version}`;
} else if (tag == 'next') {
command += ' --next';
}

Expand Down
16 changes: 11 additions & 5 deletions tests/legacy-cli/e2e/tests/misc/npm-7.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { rimraf, writeFile } from '../../utils/fs';
import { rimraf } from '../../utils/fs';
import { getActivePackageManager } from '../../utils/packages';
import { ng, npm } from '../../utils/process';
import { isPrereleaseCli } from '../../utils/project';
import { expectToFail } from '../../utils/utils';

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

export default async function() {
export default async function () {
// Only relevant with npm as a package manager
if (getActivePackageManager() !== 'npm') {
return;
Expand All @@ -17,12 +18,18 @@ export default async function() {
}

const currentDirectory = process.cwd();

const extraArgs = [];
if (isPrereleaseCli()) {
extraArgs.push('--next');
}

try {
// Install version >=7.5.6
await npm('install', '--global', 'npm@>=7.5.6');

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

// Ensure `ng update` shows npm warning
const { stderr: stderrUpdate2 } = await ng('update');
const { stderr: stderrUpdate2 } = await ng('update', ...extraArgs);
if (!stderrUpdate2.includes(warningText)) {
throw new Error('ng update expected to show npm version warning.');
}
Expand Down Expand Up @@ -85,5 +92,4 @@ export default async function() {
// Reset version back to 6.x
await npm('install', '--global', 'npm@6');
}

}
Loading