Skip to content

Commit 2c8a600

Browse files
committed
fix(@angular/cli): resolve correct registry name when using npm alias syntax during update
When a dependency uses the npm alias syntax (e.g. "@typescript/lib-dom": "npm:@types/web@^0.0.99"), ng update needs to fetch registry metadata for the actual package name (@types/web) rather than the local alias name (@typescript/lib-dom). Introduce a RegistryClient helper to intercept and translate package names before querying registry metadata or manifest info, renaming the results back to the local alias name to maintain compatibility with the rest of the resolver logic. Also update applyUpdatePlan to correctly parse and preserve alias prefixes when writing back to package.json, supporting scoped packages correctly.
1 parent b0b3a47 commit 2c8a600

2 files changed

Lines changed: 108 additions & 11 deletions

File tree

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

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,33 +25,46 @@ export class RegistryClient {
2525
private packageManager: PackageManager,
2626
private logger: logging.LoggerApi,
2727
readonly minReleaseAge: number = 0,
28+
private getRegistryName?: (name: string) => string,
2829
) {}
2930

3031
async getMetadata(packageName: string): Promise<PackageMetadata | null> {
31-
let promise = this.metadataCache.get(packageName);
32+
const registryName = this.getRegistryName ? this.getRegistryName(packageName) : packageName;
33+
let promise = this.metadataCache.get(registryName);
3234
if (!promise) {
33-
promise = this.packageManager.getRegistryMetadata(packageName).catch((e) => {
34-
this.metadataCache.delete(packageName);
35+
promise = this.packageManager.getRegistryMetadata(registryName).catch((e) => {
36+
this.metadataCache.delete(registryName);
3537
throw e;
3638
});
37-
this.metadataCache.set(packageName, promise);
39+
this.metadataCache.set(registryName, promise);
3840
}
3941

40-
return promise;
42+
const metadata = await promise;
43+
if (metadata && registryName !== packageName) {
44+
return { ...metadata, name: packageName };
45+
}
46+
47+
return metadata;
4148
}
4249

4350
async getManifest(packageName: string, version: string): Promise<PackageManifest | null> {
44-
const key = `${packageName}@${version}`;
51+
const registryName = this.getRegistryName ? this.getRegistryName(packageName) : packageName;
52+
const key = `${registryName}@${version}`;
4553
let promise = this.manifestCache.get(key);
4654
if (!promise) {
47-
promise = this.packageManager.getRegistryManifest(packageName, version).catch((e) => {
55+
promise = this.packageManager.getRegistryManifest(registryName, version).catch((e) => {
4856
this.manifestCache.delete(key);
4957
throw e;
5058
});
5159
this.manifestCache.set(key, promise);
5260
}
5361

54-
return promise;
62+
const manifest = await promise;
63+
if (manifest && registryName !== packageName) {
64+
return { ...manifest, name: packageName };
65+
}
66+
67+
return manifest;
5568
}
5669
}
5770

@@ -753,6 +766,20 @@ function _formatVersion(v?: string): string | undefined {
753766
return coerced ? coerced.toString() : undefined;
754767
}
755768

769+
function getRegistryNameAndRange(name: string, specifier: string): { name: string; range: string } {
770+
try {
771+
const result = npa.resolve(name, specifier);
772+
if (result.type === 'alias' && result.subSpec) {
773+
return {
774+
name: result.subSpec.name ?? name,
775+
range: result.subSpec.fetchSpec ?? specifier,
776+
};
777+
}
778+
} catch {}
779+
780+
return { name, range: specifier };
781+
}
782+
756783
function isPkgFromRegistry(name: string, specifier: string): boolean {
757784
const result = npa.resolve(name, specifier);
758785

@@ -812,7 +839,17 @@ export async function resolveUserUpdatePlan(
812839

813840
const packages = _buildPackageList(options, npmDeps, logger);
814841
const minReleaseAge = await packageManager.getMinimumReleaseAge();
815-
const registryClient = new RegistryClient(packageManager, logger, minReleaseAge);
842+
843+
const getRegistryName = (name: string): string => {
844+
const specifier = npmDeps.get(name);
845+
if (specifier) {
846+
return getRegistryNameAndRange(name, specifier).name;
847+
}
848+
849+
return name;
850+
};
851+
852+
const registryClient = new RegistryClient(packageManager, logger, minReleaseAge, getRegistryName);
816853

817854
const getOrFetchPackageMetadata = async (
818855
packageName: string,
@@ -1079,8 +1116,23 @@ export async function applyUpdatePlan(
10791116

10801117
const updateDependency = (deps: Record<string, string>, name: string, newVersion: string) => {
10811118
const oldVersion = deps[name];
1082-
const execResult = /^[\^~]/.exec(oldVersion);
1083-
deps[name] = `${execResult ? execResult[0] : ''}${newVersion}`;
1119+
const aliasPrefix = 'npm:';
1120+
if (oldVersion.startsWith(aliasPrefix)) {
1121+
const specifier = oldVersion.slice(aliasPrefix.length);
1122+
const lastAtIndex = specifier.lastIndexOf('@');
1123+
if (lastAtIndex !== -1) {
1124+
const registryName = specifier.slice(0, lastAtIndex);
1125+
const versionRange = specifier.slice(lastAtIndex + 1);
1126+
const execResult = /^[\^~]/.exec(versionRange);
1127+
deps[name] =
1128+
`${aliasPrefix}${registryName}@${execResult ? execResult[0] : ''}${newVersion}`;
1129+
} else {
1130+
deps[name] = oldVersion;
1131+
}
1132+
} else {
1133+
const execResult = /^[\^~]/.exec(oldVersion);
1134+
deps[name] = `${execResult ? execResult[0] : ''}${newVersion}`;
1135+
}
10841136
};
10851137

10861138
for (const [name, targetVersion] of plan.packagesToUpdate.entries()) {

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,17 @@ describe('UpdateResolver', () => {
228228
'14.0.0-next.0': { name: '@angular-devkit-tests/cdk-bug', version: '14.0.0-next.0' },
229229
},
230230
},
231+
'@types/web': {
232+
metadata: {
233+
name: '@types/web',
234+
'dist-tags': { latest: '0.0.100' },
235+
versions: ['0.0.99', '0.0.100'],
236+
},
237+
manifests: {
238+
'0.0.99': { name: '@types/web', version: '0.0.99' },
239+
'0.0.100': { name: '@types/web', version: '0.0.100' },
240+
},
241+
},
231242
};
232243

233244
async function resolvePlan(options: UpdateResolverOptions, minReleaseAge = 0) {
@@ -474,6 +485,40 @@ describe('UpdateResolver', () => {
474485
expect(plan.packagesToUpdate.get('@angular-devkit-tests/common-bug')).toBe('13.2.0');
475486
expect(plan.packagesToUpdate.get('@angular-devkit-tests/cdk-bug')).toBe('13.2.0');
476487
});
488+
489+
it('correctly resolves and updates packages using npm alias syntax', async () => {
490+
createMockWorkspace(
491+
{
492+
name: 'blah',
493+
dependencies: {
494+
'@typescript/lib-dom': 'npm:@types/web@^0.0.99',
495+
},
496+
},
497+
{
498+
'@typescript/lib-dom': {
499+
version: '0.0.99',
500+
manifest: { name: '@types/web' },
501+
},
502+
},
503+
);
504+
505+
const plan = await resolvePlan({
506+
packages: ['@typescript/lib-dom'],
507+
workspaceRoot: tempRoot,
508+
});
509+
510+
expect(plan.packagesToUpdate.get('@typescript/lib-dom')).toBe('0.0.100');
511+
512+
await applyUpdatePlan(tempRoot, plan, logger);
513+
514+
const updatedPackageJson = JSON.parse(
515+
readFileSync(path.join(tempRoot, 'package.json'), 'utf8'),
516+
) as PackageManifest;
517+
518+
expect(updatedPackageJson.dependencies?.['@typescript/lib-dom']).toBe(
519+
'npm:@types/web@^0.0.100',
520+
);
521+
});
477522
});
478523

479524
describe('RegistryClient', () => {

0 commit comments

Comments
 (0)