diff --git a/packages/angular/cli/src/commands/update/schematic/index.ts b/packages/angular/cli/src/commands/update/schematic/index.ts index e110480d09a5..f7b7a35cfd26 100644 --- a/packages/angular/cli/src/commands/update/schematic/index.ts +++ b/packages/angular/cli/src/commands/update/schematic/index.ts @@ -773,11 +773,31 @@ function _getAllDependencies(tree: Tree): Array '/package.json', ) as PackageManifest; - return [ + const allDeps = [ ...(Object.entries(peerDependencies || {}) as Array<[string, VersionRange]>), ...(Object.entries(devDependencies || {}) as Array<[string, VersionRange]>), ...(Object.entries(dependencies || {}) as Array<[string, VersionRange]>), ]; + + // Resolve pnpm catalog: protocol references to actual version ranges. + // The catalog: protocol is not a valid semver range and must be resolved + // before downstream processing. + return allDeps.map(([name, specifier]) => { + if (!specifier.startsWith('catalog:')) { + return [name, specifier] as const; + } + + // Fall back to the installed version from node_modules + const pkgJsonPath = `/node_modules/${name}/package.json`; + if (tree.exists(pkgJsonPath)) { + const { version } = tree.readJson(pkgJsonPath) as PackageManifest; + if (version) { + return [name, `^${version}` as VersionRange] as const; + } + } + + return [name, specifier] as const; + }); } function _formatVersion(version: string | undefined) { @@ -804,6 +824,11 @@ function _formatVersion(version: string | undefined) { * @throws When the specifier cannot be parsed. */ function isPkgFromRegistry(name: string, specifier: string): boolean { + // pnpm catalog: protocol always references registry packages + if (specifier.startsWith('catalog:')) { + return true; + } + const result = npa.resolve(name, specifier); return !!result.registry; diff --git a/packages/angular/cli/src/commands/update/schematic/index_spec.ts b/packages/angular/cli/src/commands/update/schematic/index_spec.ts index 11b2a0b5855e..978db6a4a62c 100644 --- a/packages/angular/cli/src/commands/update/schematic/index_spec.ts +++ b/packages/angular/cli/src/commands/update/schematic/index_spec.ts @@ -97,6 +97,35 @@ describe('@schematics/update', () => { expect(dependencies['@angular-devkit-tests/update-base']).toBe('1.1.0'); }); + it('should not error with pnpm catalog: protocol', async () => { + const newTree = new UnitTestTree( + new HostTree( + new virtualFs.test.TestHost({ + '/package.json': `{ + "name": "blah", + "dependencies": { + "@angular-devkit-tests/update-base": "catalog:" + } + }`, + '/node_modules/@angular-devkit-tests/update-base/package.json': `{ + "name": "@angular-devkit-tests/update-base", + "version": "1.0.0" + }`, + }), + ), + ); + + const resultTree = await schematicRunner.runSchematic( + 'update', + { + packages: ['@angular-devkit-tests/update-base'], + }, + newTree, + ); + const { dependencies } = JSON.parse(resultTree.readContent('/package.json')); + expect(dependencies['@angular-devkit-tests/update-base']).toBe('1.1.0'); + }); + it('updates Angular as compatible with Angular N-1', async () => { // Add the basic migration package. const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json')));