diff --git a/packages/angular/cli/src/commands/update/update-resolver.ts b/packages/angular/cli/src/commands/update/update-resolver.ts index 90cd04043201..6840a9a62427 100644 --- a/packages/angular/cli/src/commands/update/update-resolver.ts +++ b/packages/angular/cli/src/commands/update/update-resolver.ts @@ -25,33 +25,46 @@ export class RegistryClient { private packageManager: PackageManager, private logger: logging.LoggerApi, readonly minReleaseAge: number = 0, + private getRegistryName?: (name: string) => string, ) {} async getMetadata(packageName: string): Promise { - let promise = this.metadataCache.get(packageName); + const registryName = this.getRegistryName ? this.getRegistryName(packageName) : packageName; + let promise = this.metadataCache.get(registryName); if (!promise) { - promise = this.packageManager.getRegistryMetadata(packageName).catch((e) => { - this.metadataCache.delete(packageName); + promise = this.packageManager.getRegistryMetadata(registryName).catch((e) => { + this.metadataCache.delete(registryName); throw e; }); - this.metadataCache.set(packageName, promise); + this.metadataCache.set(registryName, promise); } - return promise; + const metadata = await promise; + if (metadata && registryName !== packageName) { + return { ...metadata, name: packageName }; + } + + return metadata; } async getManifest(packageName: string, version: string): Promise { - const key = `${packageName}@${version}`; + const registryName = this.getRegistryName ? this.getRegistryName(packageName) : packageName; + const key = `${registryName}@${version}`; let promise = this.manifestCache.get(key); if (!promise) { - promise = this.packageManager.getRegistryManifest(packageName, version).catch((e) => { + promise = this.packageManager.getRegistryManifest(registryName, version).catch((e) => { this.manifestCache.delete(key); throw e; }); this.manifestCache.set(key, promise); } - return promise; + const manifest = await promise; + if (manifest && registryName !== packageName) { + return { ...manifest, name: packageName }; + } + + return manifest; } } @@ -753,6 +766,20 @@ function _formatVersion(v?: string): string | undefined { return coerced ? coerced.toString() : undefined; } +function getRegistryNameAndRange(name: string, specifier: string): { name: string; range: string } { + try { + const result = npa.resolve(name, specifier); + if (result.type === 'alias' && result.subSpec) { + return { + name: result.subSpec.name ?? name, + range: result.subSpec.fetchSpec ?? specifier, + }; + } + } catch {} + + return { name, range: specifier }; +} + function isPkgFromRegistry(name: string, specifier: string): boolean { const result = npa.resolve(name, specifier); @@ -812,7 +839,17 @@ export async function resolveUserUpdatePlan( const packages = _buildPackageList(options, npmDeps, logger); const minReleaseAge = await packageManager.getMinimumReleaseAge(); - const registryClient = new RegistryClient(packageManager, logger, minReleaseAge); + + const getRegistryName = (name: string): string => { + const specifier = npmDeps.get(name); + if (specifier) { + return getRegistryNameAndRange(name, specifier).name; + } + + return name; + }; + + const registryClient = new RegistryClient(packageManager, logger, minReleaseAge, getRegistryName); const getOrFetchPackageMetadata = async ( packageName: string, @@ -1079,8 +1116,23 @@ export async function applyUpdatePlan( const updateDependency = (deps: Record, name: string, newVersion: string) => { const oldVersion = deps[name]; - const execResult = /^[\^~]/.exec(oldVersion); - deps[name] = `${execResult ? execResult[0] : ''}${newVersion}`; + const aliasPrefix = 'npm:'; + if (oldVersion.startsWith(aliasPrefix)) { + const specifier = oldVersion.slice(aliasPrefix.length); + const lastAtIndex = specifier.lastIndexOf('@'); + if (lastAtIndex > 0) { + const registryName = specifier.slice(0, lastAtIndex); + const versionRange = specifier.slice(lastAtIndex + 1); + const execResult = /^[\^~]/.exec(versionRange); + deps[name] = + `${aliasPrefix}${registryName}@${execResult ? execResult[0] : ''}${newVersion}`; + } else { + deps[name] = oldVersion; + } + } else { + const execResult = /^[\^~]/.exec(oldVersion); + deps[name] = `${execResult ? execResult[0] : ''}${newVersion}`; + } }; for (const [name, targetVersion] of plan.packagesToUpdate.entries()) { diff --git a/packages/angular/cli/src/commands/update/update-resolver_spec.ts b/packages/angular/cli/src/commands/update/update-resolver_spec.ts index 9cf4a4d826e1..77bf2ee816e8 100644 --- a/packages/angular/cli/src/commands/update/update-resolver_spec.ts +++ b/packages/angular/cli/src/commands/update/update-resolver_spec.ts @@ -228,6 +228,17 @@ describe('UpdateResolver', () => { '14.0.0-next.0': { name: '@angular-devkit-tests/cdk-bug', version: '14.0.0-next.0' }, }, }, + '@types/web': { + metadata: { + name: '@types/web', + 'dist-tags': { latest: '0.0.100' }, + versions: ['0.0.99', '0.0.100'], + }, + manifests: { + '0.0.99': { name: '@types/web', version: '0.0.99' }, + '0.0.100': { name: '@types/web', version: '0.0.100' }, + }, + }, }; async function resolvePlan(options: UpdateResolverOptions, minReleaseAge = 0) { @@ -474,6 +485,72 @@ describe('UpdateResolver', () => { expect(plan.packagesToUpdate.get('@angular-devkit-tests/common-bug')).toBe('13.2.0'); expect(plan.packagesToUpdate.get('@angular-devkit-tests/cdk-bug')).toBe('13.2.0'); }); + + it('correctly resolves and updates packages using npm alias syntax', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@typescript/lib-dom': 'npm:@types/web@^0.0.99', + }, + }, + { + '@typescript/lib-dom': { + version: '0.0.99', + manifest: { name: '@types/web' }, + }, + }, + ); + + const plan = await resolvePlan({ + packages: ['@typescript/lib-dom'], + workspaceRoot: tempRoot, + }); + + expect(plan.packagesToUpdate.get('@typescript/lib-dom')).toBe('0.0.100'); + + await applyUpdatePlan(tempRoot, plan, logger); + + const updatedPackageJson = JSON.parse( + readFileSync(path.join(tempRoot, 'package.json'), 'utf8'), + ) as PackageManifest; + + expect(updatedPackageJson.dependencies?.['@typescript/lib-dom']).toBe( + 'npm:@types/web@^0.0.100', + ); + }); + + it('correctly handles packages using npm alias syntax without a version range', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@typescript/lib-dom': 'npm:@types/web', + }, + }, + { + '@typescript/lib-dom': { + version: '0.0.99', + manifest: { name: '@types/web' }, + }, + }, + ); + + const plan = await resolvePlan({ + packages: ['@typescript/lib-dom'], + workspaceRoot: tempRoot, + }); + + expect(plan.packagesToUpdate.get('@typescript/lib-dom')).toBe('0.0.100'); + + await applyUpdatePlan(tempRoot, plan, logger); + + const updatedPackageJson = JSON.parse( + readFileSync(path.join(tempRoot, 'package.json'), 'utf8'), + ) as PackageManifest; + + expect(updatedPackageJson.dependencies?.['@typescript/lib-dom']).toBe('npm:@types/web'); + }); }); describe('RegistryClient', () => {