Skip to content

Commit 8d4ecae

Browse files
committed
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.
1 parent 114cddd commit 8d4ecae

File tree

2 files changed

+95
-143
lines changed

2 files changed

+95
-143
lines changed

packages/angular/cli/src/commands/update/cli.ts

Lines changed: 32 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { existsSync, promises as fs } from 'node:fs';
1212
import { createRequire } from 'node:module';
1313
import * as path from 'node:path';
1414
import npa from 'npm-package-arg';
15-
import * as semver from 'semver';
1615
import { Argv } from 'yargs';
1716
import {
1817
CommandModule,
@@ -21,14 +20,10 @@ import {
2120
Options,
2221
} from '../../command-builder/command-module';
2322
import { SchematicEngineHost } from '../../command-builder/utilities/schematic-engine-host';
23+
import { PackageManager, PackageManifest, createPackageManager } from '../../package-managers';
2424
import { colors } from '../../utilities/color';
2525
import { disableVersionCheck } from '../../utilities/environment-options';
2626
import { assertIsError } from '../../utilities/error';
27-
import {
28-
PackageIdentifier,
29-
PackageManifest,
30-
fetchPackageMetadata,
31-
} from '../../utilities/package-metadata';
3227
import {
3328
PackageTreeNode,
3429
findPackageJson,
@@ -174,7 +169,12 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
174169
}
175170

176171
async run(options: Options<UpdateCommandArgs>): Promise<number | void> {
177-
const { logger, packageManager } = this.context;
172+
const { logger } = this.context;
173+
// Instantiate the package manager
174+
const packageManager = await createPackageManager({
175+
cwd: this.context.root,
176+
logger,
177+
});
178178

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

@@ -201,7 +200,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
201200
}
202201
}
203202

204-
const packages: PackageIdentifier[] = [];
203+
const packages: npa.Result[] = [];
205204
for (const request of options.packages ?? []) {
206205
try {
207206
const packageIdentifier = npa(request);
@@ -230,7 +229,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
230229
packageIdentifier.type = 'tag';
231230
}
232231

233-
packages.push(packageIdentifier as PackageIdentifier);
232+
packages.push(packageIdentifier);
234233
} catch (e) {
235234
assertIsError(e);
236235
logger.error(e.message);
@@ -247,7 +246,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
247246

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

277276
return options.migrateOnly
278277
? this.migrateOnly(workflow, (options.packages ?? [])[0], rootDependencies, options)
279-
: this.updatePackagesAndMigrate(workflow, rootDependencies, options, packages);
278+
: this.updatePackagesAndMigrate(
279+
workflow,
280+
rootDependencies,
281+
options,
282+
packages,
283+
packageManager,
284+
);
280285
}
281286

282287
private async migrateOnly(
@@ -395,7 +400,8 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
395400
workflow: NodeWorkflow,
396401
rootDependencies: Map<string, PackageTreeNode>,
397402
options: Options<UpdateCommandArgs>,
398-
packages: PackageIdentifier[],
403+
packages: npa.Result[],
404+
packageManager: PackageManager,
399405
): Promise<number> {
400406
const { logger } = this.context;
401407

@@ -406,13 +412,14 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
406412
};
407413

408414
const requests: {
409-
identifier: PackageIdentifier;
415+
identifier: npa.Result;
410416
node: PackageTreeNode;
411417
}[] = [];
412418

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

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

441-
let metadata;
448+
let manifest: PackageManifest | null = null;
442449
try {
443-
// Metadata requests are internally cached; multiple requests for same name
444-
// does not result in additional network traffic
445-
metadata = await fetchPackageMetadata(packageName, logger, {
446-
verbose: options.verbose,
447-
});
450+
manifest = await packageManager.getManifest(requestIdentifier);
448451
} catch (e) {
449452
assertIsError(e);
450-
logger.error(`Error fetching metadata for '${packageName}': ` + e.message);
453+
logger.error(`Error fetching manifest for '${packageName}': ` + e.message);
451454

452455
return 1;
453456
}
454457

455-
// Try to find a package version based on the user requested package specifier
456-
// registry specifier types are either version, range, or tag
457-
let manifest: PackageManifest | undefined;
458-
switch (requestIdentifier.type) {
459-
case 'tag':
460-
manifest = metadata.tags[requestIdentifier.fetchSpec];
461-
// If not found and next option was used and user did not provide a specifier, try latest.
462-
// Package may not have a next tag.
463-
if (
464-
!manifest &&
465-
requestIdentifier.fetchSpec === 'next' &&
466-
requestIdentifier.rawSpec === '*'
467-
) {
468-
manifest = metadata.tags['latest'];
469-
}
470-
break;
471-
case 'version':
472-
manifest = metadata.versions[requestIdentifier.fetchSpec];
473-
break;
474-
case 'range':
475-
for (const potentialManifest of Object.values(metadata.versions)) {
476-
// Ignore deprecated package versions
477-
if (potentialManifest.deprecated) {
478-
continue;
479-
}
480-
// Only consider versions that are within the range
481-
if (
482-
!semver.satisfies(potentialManifest.version, requestIdentifier.fetchSpec, {
483-
loose: true,
484-
})
485-
) {
486-
continue;
487-
}
488-
// Update the used manifest if current potential is newer than existing or there is not one yet
489-
if (
490-
!manifest ||
491-
semver.gt(potentialManifest.version, manifest.version, { loose: true })
492-
) {
493-
manifest = potentialManifest;
494-
}
495-
}
496-
break;
497-
}
498-
499458
if (!manifest) {
500459
logger.error(
501460
`Package specified by '${requestIdentifier.raw}' does not exist within the registry.`,
@@ -560,10 +519,8 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
560519
);
561520

562521
if (success) {
563-
const { root: commandRoot, packageManager } = this.context;
564-
const installArgs = shouldForcePackageManager(packageManager, logger, options.verbose)
565-
? ['--force']
566-
: [];
522+
const { root: commandRoot } = this.context;
523+
const force = await shouldForcePackageManager(packageManager, logger, options.verbose);
567524
const tasks = new Listr([
568525
{
569526
title: 'Cleaning node modules directory',
@@ -585,9 +542,11 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
585542
{
586543
title: 'Installing packages',
587544
async task() {
588-
const installationSuccess = await packageManager.installAll(installArgs, commandRoot);
589-
590-
if (!installationSuccess) {
545+
try {
546+
await packageManager.install({
547+
force,
548+
});
549+
} catch (e) {
591550
throw new CommandError('Unable to install packages');
592551
}
593552
},

packages/angular/cli/src/commands/update/utilities/cli-version.ts

Lines changed: 63 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ import { spawnSync } from 'node:child_process';
1111
import { existsSync, promises as fs } from 'node:fs';
1212
import { join, resolve } from 'node:path';
1313
import * as semver from 'semver';
14-
import { PackageManager } from '../../../../lib/config/workspace-schema';
15-
import { PackageManagerUtils } from '../../../utilities/package-manager';
16-
import { fetchPackageManifest } from '../../../utilities/package-metadata';
14+
import { PackageManager } from '../../../package-managers';
1715
import { VERSION } from '../../../utilities/version';
1816
import { ANGULAR_PACKAGES_REGEXP } from './constants';
1917

@@ -58,18 +56,19 @@ export function coerceVersionNumber(version: string | undefined): string | undef
5856
export async function checkCLIVersion(
5957
packagesToUpdate: string[],
6058
logger: logging.LoggerApi,
61-
packageManager: PackageManagerUtils,
62-
verbose = false,
59+
packageManager: PackageManager,
6360
next = false,
6461
): Promise<string | null> {
65-
const { version } = await fetchPackageManifest(
66-
`@angular/cli@${getCLIUpdateRunnerVersion(packagesToUpdate, next)}`,
67-
logger,
68-
{
69-
verbose,
70-
usingYarn: packageManager.name === PackageManager.Yarn,
71-
},
72-
);
62+
const runnerVersion = getCLIUpdateRunnerVersion(packagesToUpdate, next);
63+
const manifest = await packageManager.getManifest(`@angular/cli@${runnerVersion}`);
64+
65+
if (!manifest) {
66+
logger.warn(`Could not find @angular/cli version '${runnerVersion}'.`);
67+
68+
return null;
69+
}
70+
71+
const version = manifest.version;
7372

7473
return VERSION.full === version ? null : version;
7574
}
@@ -120,52 +119,53 @@ export function getCLIUpdateRunnerVersion(
120119
*/
121120
export async function runTempBinary(
122121
packageName: string,
123-
packageManager: PackageManagerUtils,
122+
packageManager: PackageManager,
124123
args: string[] = [],
125124
): Promise<number> {
126-
const { success, tempNodeModules } = await packageManager.installTemp(packageName);
127-
if (!success) {
128-
return 1;
129-
}
130-
131-
// Remove version/tag etc... from package name
132-
// Ex: @angular/cli@latest -> @angular/cli
133-
const packageNameNoVersion = packageName.substring(0, packageName.lastIndexOf('@'));
134-
const pkgLocation = join(tempNodeModules, packageNameNoVersion);
135-
const packageJsonPath = join(pkgLocation, 'package.json');
136-
137-
// Get a binary location for this package
138-
let binPath: string | undefined;
139-
if (existsSync(packageJsonPath)) {
140-
const content = await fs.readFile(packageJsonPath, 'utf-8');
141-
if (content) {
142-
const { bin = {} } = JSON.parse(content) as { bin: Record<string, string> };
143-
const binKeys = Object.keys(bin);
144-
145-
if (binKeys.length) {
146-
binPath = resolve(pkgLocation, bin[binKeys[0]]);
125+
const { workingDirectory, cleanup } = await packageManager.acquireTempPackage(packageName);
126+
127+
try {
128+
// Remove version/tag etc... from package name
129+
// Ex: @angular/cli@latest -> @angular/cli
130+
const packageNameNoVersion = packageName.substring(0, packageName.lastIndexOf('@'));
131+
const pkgLocation = join(workingDirectory, 'node_modules', packageNameNoVersion);
132+
const packageJsonPath = join(pkgLocation, 'package.json');
133+
134+
// Get a binary location for this package
135+
let binPath: string | undefined;
136+
if (existsSync(packageJsonPath)) {
137+
const content = await fs.readFile(packageJsonPath, 'utf-8');
138+
if (content) {
139+
const { bin = {} } = JSON.parse(content) as { bin: Record<string, string> };
140+
const binKeys = Object.keys(bin);
141+
142+
if (binKeys.length) {
143+
binPath = resolve(pkgLocation, bin[binKeys[0]]);
144+
}
147145
}
148146
}
149-
}
150147

151-
if (!binPath) {
152-
throw new Error(`Cannot locate bin for temporary package: ${packageNameNoVersion}.`);
153-
}
148+
if (!binPath) {
149+
throw new Error(`Cannot locate bin for temporary package: ${packageNameNoVersion}.`);
150+
}
154151

155-
const { status, error } = spawnSync(process.execPath, [binPath, ...args], {
156-
stdio: 'inherit',
157-
env: {
158-
...process.env,
159-
NG_DISABLE_VERSION_CHECK: 'true',
160-
NG_CLI_ANALYTICS: 'false',
161-
},
162-
});
163-
164-
if (status === null && error) {
165-
throw error;
166-
}
152+
const { status, error } = spawnSync(process.execPath, [binPath, ...args], {
153+
stdio: 'inherit',
154+
env: {
155+
...process.env,
156+
NG_DISABLE_VERSION_CHECK: 'true',
157+
NG_CLI_ANALYTICS: 'false',
158+
},
159+
});
160+
161+
if (status === null && error) {
162+
throw error;
163+
}
167164

168-
return status ?? 0;
165+
return status ?? 0;
166+
} finally {
167+
await cleanup();
168+
}
169169
}
170170

171171
/**
@@ -175,30 +175,23 @@ export async function runTempBinary(
175175
* @param verbose Whether to log verbose output.
176176
* @returns True if the package manager should be forced, false otherwise.
177177
*/
178-
export function shouldForcePackageManager(
179-
packageManager: PackageManagerUtils,
178+
export async function shouldForcePackageManager(
179+
packageManager: PackageManager,
180180
logger: logging.LoggerApi,
181181
verbose: boolean,
182-
): boolean {
182+
): Promise<boolean> {
183183
// npm 7+ can fail due to it incorrectly resolving peer dependencies that have valid SemVer
184184
// ranges during an update. Update will set correct versions of dependencies within the
185185
// package.json file. The force option is set to workaround these errors.
186-
// Example error:
187-
// npm ERR! Conflicting peer dependency: @angular/[email protected]
188-
// npm ERR! node_modules/@angular/compiler-cli
189-
// npm ERR! peer @angular/compiler-cli@"^14.0.0 || ^14.0.0-rc" from @angular-devkit/[email protected]
190-
// npm ERR! node_modules/@angular-devkit/build-angular
191-
// npm ERR! dev @angular-devkit/build-angular@"~14.0.0-rc.0" from the root project
192-
if (
193-
packageManager.name === PackageManager.Npm &&
194-
packageManager.version &&
195-
semver.gte(packageManager.version, '7.0.0')
196-
) {
197-
if (verbose) {
198-
logger.info('NPM 7+ detected -- enabling force option for package installation');
199-
}
186+
if (packageManager.name === 'npm') {
187+
const version = await packageManager.getVersion();
188+
if (semver.gte(version, '7.0.0')) {
189+
if (verbose) {
190+
logger.info('NPM 7+ detected -- enabling force option for package installation');
191+
}
200192

201-
return true;
193+
return true;
194+
}
202195
}
203196

204197
return false;

0 commit comments

Comments
 (0)