Skip to content
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
106 changes: 33 additions & 73 deletions packages/angular/cli/src/commands/update/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { existsSync, promises as fs } from 'node:fs';
import { createRequire } from 'node:module';
import * as path from 'node:path';
import npa from 'npm-package-arg';
import * as semver from 'semver';
import { Argv } from 'yargs';
import {
CommandModule,
Expand All @@ -21,14 +20,10 @@ import {
Options,
} from '../../command-builder/command-module';
import { SchematicEngineHost } from '../../command-builder/utilities/schematic-engine-host';
import { PackageManager, PackageManifest, createPackageManager } from '../../package-managers';
import { colors } from '../../utilities/color';
import { disableVersionCheck } from '../../utilities/environment-options';
import { assertIsError } from '../../utilities/error';
import {
PackageIdentifier,
PackageManifest,
fetchPackageMetadata,
} from '../../utilities/package-metadata';
import {
PackageTreeNode,
findPackageJson,
Expand Down Expand Up @@ -174,7 +169,13 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
}

async run(options: Options<UpdateCommandArgs>): Promise<number | void> {
const { logger, packageManager } = this.context;
const { logger } = this.context;
// Instantiate the package manager
const packageManager = await createPackageManager({
cwd: this.context.root,
logger,
configuredPackageManager: this.context.packageManager.name,
});

// Check if the current installed CLI version is older than the latest compatible version.
// Skip when running `ng update` without a package name as this will not trigger an actual update.
Expand All @@ -183,7 +184,6 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
options.packages,
logger,
packageManager,
options.verbose,
options.next,
);

Expand All @@ -201,7 +201,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
}
}

const packages: PackageIdentifier[] = [];
const packages: npa.Result[] = [];
for (const request of options.packages ?? []) {
try {
const packageIdentifier = npa(request);
Expand Down Expand Up @@ -230,7 +230,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
packageIdentifier.type = 'tag';
}

packages.push(packageIdentifier as PackageIdentifier);
packages.push(packageIdentifier);
} catch (e) {
assertIsError(e);
logger.error(e.message);
Expand All @@ -247,7 +247,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs

const workflow = new NodeWorkflow(this.context.root, {
packageManager: packageManager.name,
packageManagerForce: shouldForcePackageManager(packageManager, logger, options.verbose),
packageManagerForce: await shouldForcePackageManager(packageManager, logger, options.verbose),
// __dirname -> favor @schematics/update from this package
// Otherwise, use packages from the active workspace (migrations)
resolvePaths: this.resolvePaths,
Expand Down Expand Up @@ -276,7 +276,13 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs

return options.migrateOnly
? this.migrateOnly(workflow, (options.packages ?? [])[0], rootDependencies, options)
: this.updatePackagesAndMigrate(workflow, rootDependencies, options, packages);
: this.updatePackagesAndMigrate(
workflow,
rootDependencies,
options,
packages,
packageManager,
);
}

private async migrateOnly(
Expand Down Expand Up @@ -395,7 +401,8 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
workflow: NodeWorkflow,
rootDependencies: Map<string, PackageTreeNode>,
options: Options<UpdateCommandArgs>,
packages: PackageIdentifier[],
packages: npa.Result[],
packageManager: PackageManager,
): Promise<number> {
const { logger } = this.context;

Expand All @@ -406,13 +413,14 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
};

const requests: {
identifier: PackageIdentifier;
identifier: npa.Result;
node: PackageTreeNode;
}[] = [];

// Validate packages actually are part of the workspace
for (const pkg of packages) {
const node = rootDependencies.get(pkg.name);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const node = rootDependencies.get(pkg.name!);
if (!node?.package) {
logger.error(`Package '${pkg.name}' is not a dependency.`);

Expand All @@ -438,64 +446,16 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
for (const { identifier: requestIdentifier, node } of requests) {
const packageName = requestIdentifier.name;

let metadata;
let manifest: PackageManifest | null = null;
try {
// Metadata requests are internally cached; multiple requests for same name
// does not result in additional network traffic
metadata = await fetchPackageMetadata(packageName, logger, {
verbose: options.verbose,
});
manifest = await packageManager.getManifest(requestIdentifier);
} catch (e) {
assertIsError(e);
logger.error(`Error fetching metadata for '${packageName}': ` + e.message);
logger.error(`Error fetching manifest for '${packageName}': ` + e.message);

return 1;
}

// Try to find a package version based on the user requested package specifier
// registry specifier types are either version, range, or tag
let manifest: PackageManifest | undefined;
switch (requestIdentifier.type) {
case 'tag':
manifest = metadata.tags[requestIdentifier.fetchSpec];
// If not found and next option was used and user did not provide a specifier, try latest.
// Package may not have a next tag.
if (
!manifest &&
requestIdentifier.fetchSpec === 'next' &&
requestIdentifier.rawSpec === '*'
) {
manifest = metadata.tags['latest'];
}
break;
case 'version':
manifest = metadata.versions[requestIdentifier.fetchSpec];
break;
case 'range':
for (const potentialManifest of Object.values(metadata.versions)) {
// Ignore deprecated package versions
if (potentialManifest.deprecated) {
continue;
}
// Only consider versions that are within the range
if (
!semver.satisfies(potentialManifest.version, requestIdentifier.fetchSpec, {
loose: true,
})
) {
continue;
}
// Update the used manifest if current potential is newer than existing or there is not one yet
if (
!manifest ||
semver.gt(potentialManifest.version, manifest.version, { loose: true })
) {
manifest = potentialManifest;
}
}
break;
}

if (!manifest) {
logger.error(
`Package specified by '${requestIdentifier.raw}' does not exist within the registry.`,
Expand Down Expand Up @@ -560,10 +520,8 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
);

if (success) {
const { root: commandRoot, packageManager } = this.context;
const installArgs = shouldForcePackageManager(packageManager, logger, options.verbose)
? ['--force']
: [];
const { root: commandRoot } = this.context;
const force = await shouldForcePackageManager(packageManager, logger, options.verbose);
const tasks = new Listr([
{
title: 'Cleaning node modules directory',
Expand All @@ -585,9 +543,11 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
{
title: 'Installing packages',
async task() {
const installationSuccess = await packageManager.installAll(installArgs, commandRoot);

if (!installationSuccess) {
try {
await packageManager.install({
force,
});
} catch (e) {
throw new CommandError('Unable to install packages');
}
},
Expand Down
133 changes: 63 additions & 70 deletions packages/angular/cli/src/commands/update/utilities/cli-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import { spawnSync } from 'node:child_process';
import { existsSync, promises as fs } from 'node:fs';
import { join, resolve } from 'node:path';
import * as semver from 'semver';
import { PackageManager } from '../../../../lib/config/workspace-schema';
import { PackageManagerUtils } from '../../../utilities/package-manager';
import { fetchPackageManifest } from '../../../utilities/package-metadata';
import { PackageManager } from '../../../package-managers';
import { VERSION } from '../../../utilities/version';
import { ANGULAR_PACKAGES_REGEXP } from './constants';

Expand Down Expand Up @@ -58,18 +56,19 @@ export function coerceVersionNumber(version: string | undefined): string | undef
export async function checkCLIVersion(
packagesToUpdate: string[],
logger: logging.LoggerApi,
packageManager: PackageManagerUtils,
verbose = false,
packageManager: PackageManager,
next = false,
): Promise<string | null> {
const { version } = await fetchPackageManifest(
`@angular/cli@${getCLIUpdateRunnerVersion(packagesToUpdate, next)}`,
logger,
{
verbose,
usingYarn: packageManager.name === PackageManager.Yarn,
},
);
const runnerVersion = getCLIUpdateRunnerVersion(packagesToUpdate, next);
const manifest = await packageManager.getManifest(`@angular/cli@${runnerVersion}`);

if (!manifest) {
logger.warn(`Could not find @angular/cli version '${runnerVersion}'.`);

return null;
}

const version = manifest.version;

return VERSION.full === version ? null : version;
}
Expand Down Expand Up @@ -120,52 +119,53 @@ export function getCLIUpdateRunnerVersion(
*/
export async function runTempBinary(
packageName: string,
packageManager: PackageManagerUtils,
packageManager: PackageManager,
args: string[] = [],
): Promise<number> {
const { success, tempNodeModules } = await packageManager.installTemp(packageName);
if (!success) {
return 1;
}

// Remove version/tag etc... from package name
// Ex: @angular/cli@latest -> @angular/cli
const packageNameNoVersion = packageName.substring(0, packageName.lastIndexOf('@'));
const pkgLocation = join(tempNodeModules, packageNameNoVersion);
const packageJsonPath = join(pkgLocation, 'package.json');

// Get a binary location for this package
let binPath: string | undefined;
if (existsSync(packageJsonPath)) {
const content = await fs.readFile(packageJsonPath, 'utf-8');
if (content) {
const { bin = {} } = JSON.parse(content) as { bin: Record<string, string> };
const binKeys = Object.keys(bin);

if (binKeys.length) {
binPath = resolve(pkgLocation, bin[binKeys[0]]);
const { workingDirectory, cleanup } = await packageManager.acquireTempPackage(packageName);

try {
// Remove version/tag etc... from package name
// Ex: @angular/cli@latest -> @angular/cli
const packageNameNoVersion = packageName.substring(0, packageName.lastIndexOf('@'));
const pkgLocation = join(workingDirectory, 'node_modules', packageNameNoVersion);
const packageJsonPath = join(pkgLocation, 'package.json');

// Get a binary location for this package
let binPath: string | undefined;
if (existsSync(packageJsonPath)) {
const content = await fs.readFile(packageJsonPath, 'utf-8');
if (content) {
const { bin = {} } = JSON.parse(content) as { bin: Record<string, string> };
const binKeys = Object.keys(bin);

if (binKeys.length) {
binPath = resolve(pkgLocation, bin[binKeys[0]]);
}
}
}
}

if (!binPath) {
throw new Error(`Cannot locate bin for temporary package: ${packageNameNoVersion}.`);
}
if (!binPath) {
throw new Error(`Cannot locate bin for temporary package: ${packageNameNoVersion}.`);
}

const { status, error } = spawnSync(process.execPath, [binPath, ...args], {
stdio: 'inherit',
env: {
...process.env,
NG_DISABLE_VERSION_CHECK: 'true',
NG_CLI_ANALYTICS: 'false',
},
});

if (status === null && error) {
throw error;
}
const { status, error } = spawnSync(process.execPath, [binPath, ...args], {
stdio: 'inherit',
env: {
...process.env,
NG_DISABLE_VERSION_CHECK: 'true',
NG_CLI_ANALYTICS: 'false',
},
});

if (status === null && error) {
throw error;
}

return status ?? 0;
return status ?? 0;
} finally {
await cleanup();
}
}

/**
Expand All @@ -175,30 +175,23 @@ export async function runTempBinary(
* @param verbose Whether to log verbose output.
* @returns True if the package manager should be forced, false otherwise.
*/
export function shouldForcePackageManager(
packageManager: PackageManagerUtils,
export async function shouldForcePackageManager(
packageManager: PackageManager,
logger: logging.LoggerApi,
verbose: boolean,
): boolean {
): Promise<boolean> {
// npm 7+ can fail due to it incorrectly resolving peer dependencies that have valid SemVer
// ranges during an update. Update will set correct versions of dependencies within the
// package.json file. The force option is set to workaround these errors.
// Example error:
// npm ERR! Conflicting peer dependency: @angular/compiler-cli@14.0.0-rc.0
// npm ERR! node_modules/@angular/compiler-cli
// npm ERR! peer @angular/compiler-cli@"^14.0.0 || ^14.0.0-rc" from @angular-devkit/build-angular@14.0.0-rc.0
// npm ERR! node_modules/@angular-devkit/build-angular
// npm ERR! dev @angular-devkit/build-angular@"~14.0.0-rc.0" from the root project
if (
packageManager.name === PackageManager.Npm &&
packageManager.version &&
semver.gte(packageManager.version, '7.0.0')
) {
if (verbose) {
logger.info('NPM 7+ detected -- enabling force option for package installation');
}
if (packageManager.name === 'npm') {
const version = await packageManager.getVersion();
if (semver.gte(version, '7.0.0')) {
if (verbose) {
logger.info('NPM 7+ detected -- enabling force option for package installation');
}

return true;
return true;
}
}

return false;
Expand Down