Skip to content

Commit 1bbbac6

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 917393a commit 1bbbac6

2 files changed

Lines changed: 140 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

@@ -760,6 +773,20 @@ function _formatVersion(v?: string): string | undefined {
760773
return coerced ? coerced.toString() : undefined;
761774
}
762775

776+
function getRegistryNameAndRange(name: string, specifier: string): { name: string; range: string } {
777+
try {
778+
const result = npa.resolve(name, specifier);
779+
if (result.type === 'alias' && result.subSpec) {
780+
return {
781+
name: result.subSpec.name ?? name,
782+
range: result.subSpec.fetchSpec ?? specifier,
783+
};
784+
}
785+
} catch {}
786+
787+
return { name, range: specifier };
788+
}
789+
763790
function isPkgFromRegistry(name: string, specifier: string): boolean {
764791
const result = npa.resolve(name, specifier);
765792

@@ -886,7 +913,17 @@ export async function resolveUserUpdatePlan(
886913
const usingYarn = options.packageManager === 'yarn';
887914

888915
const minReleaseAge = await packageManager.getMinimumReleaseAge();
889-
const registryClient = new RegistryClient(packageManager, logger, minReleaseAge);
916+
917+
const getRegistryName = (name: string): string => {
918+
const specifier = npmDeps.get(name);
919+
if (specifier) {
920+
return getRegistryNameAndRange(name, specifier).name;
921+
}
922+
923+
return name;
924+
};
925+
926+
const registryClient = new RegistryClient(packageManager, logger, minReleaseAge, getRegistryName);
890927

891928
await checkCatalogUpdates(
892929
normalizedPackages,
@@ -1163,8 +1200,23 @@ export async function applyUpdatePlan(
11631200

11641201
const updateDependency = (deps: Record<string, string>, name: string, newVersion: string) => {
11651202
const oldVersion = deps[name];
1166-
const execResult = /^[\^~]/.exec(oldVersion);
1167-
deps[name] = `${execResult ? execResult[0] : ''}${newVersion}`;
1203+
const aliasPrefix = 'npm:';
1204+
if (oldVersion.startsWith(aliasPrefix)) {
1205+
const specifier = oldVersion.slice(aliasPrefix.length);
1206+
const lastAtIndex = specifier.lastIndexOf('@');
1207+
if (lastAtIndex > 0) {
1208+
const registryName = specifier.slice(0, lastAtIndex);
1209+
const versionRange = specifier.slice(lastAtIndex + 1);
1210+
const execResult = /^[\^~]/.exec(versionRange);
1211+
deps[name] =
1212+
`${aliasPrefix}${registryName}@${execResult ? execResult[0] : ''}${newVersion}`;
1213+
} else {
1214+
deps[name] = oldVersion;
1215+
}
1216+
} else {
1217+
const execResult = /^[\^~]/.exec(oldVersion);
1218+
deps[name] = `${execResult ? execResult[0] : ''}${newVersion}`;
1219+
}
11681220
};
11691221

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

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

Lines changed: 77 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) {
@@ -573,6 +584,72 @@ Please perform the following steps to update:
573584
}),
574585
).toBeRejectedWithError(new RegExp(expectedError));
575586
});
587+
588+
it('correctly resolves and updates packages using npm alias syntax', async () => {
589+
createMockWorkspace(
590+
{
591+
name: 'blah',
592+
dependencies: {
593+
'@typescript/lib-dom': 'npm:@types/web@^0.0.99',
594+
},
595+
},
596+
{
597+
'@typescript/lib-dom': {
598+
version: '0.0.99',
599+
manifest: { name: '@types/web' },
600+
},
601+
},
602+
);
603+
604+
const plan = await resolvePlan({
605+
packages: ['@typescript/lib-dom'],
606+
workspaceRoot: tempRoot,
607+
});
608+
609+
expect(plan.packagesToUpdate.get('@typescript/lib-dom')).toBe('0.0.100');
610+
611+
await applyUpdatePlan(tempRoot, plan, logger);
612+
613+
const updatedPackageJson = JSON.parse(
614+
readFileSync(path.join(tempRoot, 'package.json'), 'utf8'),
615+
) as PackageManifest;
616+
617+
expect(updatedPackageJson.dependencies?.['@typescript/lib-dom']).toBe(
618+
'npm:@types/web@^0.0.100',
619+
);
620+
});
621+
622+
it('correctly handles packages using npm alias syntax without a version range', async () => {
623+
createMockWorkspace(
624+
{
625+
name: 'blah',
626+
dependencies: {
627+
'@typescript/lib-dom': 'npm:@types/web',
628+
},
629+
},
630+
{
631+
'@typescript/lib-dom': {
632+
version: '0.0.99',
633+
manifest: { name: '@types/web' },
634+
},
635+
},
636+
);
637+
638+
const plan = await resolvePlan({
639+
packages: ['@typescript/lib-dom'],
640+
workspaceRoot: tempRoot,
641+
});
642+
643+
expect(plan.packagesToUpdate.get('@typescript/lib-dom')).toBe('0.0.100');
644+
645+
await applyUpdatePlan(tempRoot, plan, logger);
646+
647+
const updatedPackageJson = JSON.parse(
648+
readFileSync(path.join(tempRoot, 'package.json'), 'utf8'),
649+
) as PackageManifest;
650+
651+
expect(updatedPackageJson.dependencies?.['@typescript/lib-dom']).toBe('npm:@types/web');
652+
});
576653
});
577654

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

0 commit comments

Comments
 (0)