From 8c8ba32c7a41627d06f44ed5d990dd68bdd241e3 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:18:05 -0500 Subject: [PATCH] refactor(@angular/cli): integrate new package manager abstraction in update command Integrates the new `PackageManager` abstraction into the `ng update` command. This refactoring replaces the direct `pacote` package calls with the unified `PackageManager` API, improving consistency and maintainability. This does not yet change the internal update schematic which still does use the `pacote` package. --- .../angular/cli/src/commands/update/cli.ts | 106 +++++--------- .../commands/update/utilities/cli-version.ts | 133 +++++++++--------- 2 files changed, 96 insertions(+), 143 deletions(-) diff --git a/packages/angular/cli/src/commands/update/cli.ts b/packages/angular/cli/src/commands/update/cli.ts index 2ce76d30fece..3de979b481e6 100644 --- a/packages/angular/cli/src/commands/update/cli.ts +++ b/packages/angular/cli/src/commands/update/cli.ts @@ -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, @@ -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, @@ -174,7 +169,13 @@ export default class UpdateCommandModule extends CommandModule): Promise { - 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. @@ -183,7 +184,6 @@ export default class UpdateCommandModule extends CommandModule favor @schematics/update from this package // Otherwise, use packages from the active workspace (migrations) resolvePaths: this.resolvePaths, @@ -276,7 +276,13 @@ export default class UpdateCommandModule extends CommandModule, options: Options, - packages: PackageIdentifier[], + packages: npa.Result[], + packageManager: PackageManager, ): Promise { const { logger } = this.context; @@ -406,13 +413,14 @@ export default class UpdateCommandModule extends CommandModule { - 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; } @@ -120,52 +119,53 @@ export function getCLIUpdateRunnerVersion( */ export async function runTempBinary( packageName: string, - packageManager: PackageManagerUtils, + packageManager: PackageManager, args: string[] = [], ): Promise { - 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 }; - 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 }; + 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(); + } } /** @@ -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 { // 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;