From 10644193506c1a08dbc1ca900673cff585e3e0a8 Mon Sep 17 00:00:00 2001 From: hoyeon Date: Tue, 4 Nov 2025 14:46:52 +0900 Subject: [PATCH 1/4] drop protocol after normalization --- packages/plugin-catalog/sources/index.ts | 51 ++++++++++++++++++++---- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/packages/plugin-catalog/sources/index.ts b/packages/plugin-catalog/sources/index.ts index 40ce89676b8..b4e3ad899ee 100644 --- a/packages/plugin-catalog/sources/index.ts +++ b/packages/plugin-catalog/sources/index.ts @@ -1,8 +1,44 @@ -import {type Descriptor, type Locator, type Plugin, type Project, type Resolver, type ResolveOptions, type Workspace, SettingsType, structUtils, Manifest, ThrowReport} from '@yarnpkg/core'; -import {Hooks as CoreHooks} from '@yarnpkg/core'; -import {Hooks as PackHooks} from '@yarnpkg/plugin-pack'; +import {type Descriptor, type Locator, type Plugin, type Project, type Resolver, type ResolveOptions, type Workspace, SettingsType, structUtils, Manifest, ThrowReport, Configuration} from '@yarnpkg/core'; +import {Hooks as CoreHooks} from '@yarnpkg/core'; +import {Hooks as PackHooks} from '@yarnpkg/plugin-pack'; -import {isCatalogReference, resolveDescriptorFromCatalog} from './utils'; +import {isCatalogReference, resolveDescriptorFromCatalog} from './utils'; + +const SAFE_PROTOCOLS_TO_ALWAYS_KEEP = new Set([`patch:`, `portal:`, `link:`]); + +function tryRoundTripRange(range: string, configuration: Configuration): string { + try { + const parsed = structUtils.parseRange(range); + let {protocol, source, params, selector} = parsed; + + const defaultProtocol = configuration.get(`defaultProtocol`); + + // only drop protocol when it's exactly the default npm protocol and no special semantics needed + const canDropProtocol = + protocol != null && + protocol === defaultProtocol && + protocol === `npm:` && // be explicit + !SAFE_PROTOCOLS_TO_ALWAYS_KEEP.has(protocol); + + if (canDropProtocol) + protocol = null; + + // Replace the catalog reference with the resolved range + const normalized = structUtils.makeRange({protocol, source, params, selector}); + + // idempotency check: if normalization changes meaningfully, keep original + const reparsed = structUtils.parseRange(normalized); + const sameShape = + reparsed.protocol === (canDropProtocol ? null : parsed.protocol) && + reparsed.source === parsed.source && + JSON.stringify(reparsed.params) === JSON.stringify(parsed.params) && + reparsed.selector === parsed.selector; + + return sameShape ? normalized : range; + } catch { + return range; + } +} declare module '@yarnpkg/core' { interface ConfigurationValueMap { @@ -85,12 +121,11 @@ const plugin: Plugin = { // Resolve the catalog reference to get the actual version range const resolvedDescriptor = resolveDescriptorFromCatalog(project, descriptor, resolver, resolveOptions); - let {protocol, source, params, selector} = structUtils.parseRange(structUtils.convertToManifestRange(resolvedDescriptor.range)); - if (protocol === workspace.project.configuration.get(`defaultProtocol`)) - protocol = null; + // Convert to manifest range to strip internal params + const resolvedRange = structUtils.convertToManifestRange(resolvedDescriptor.range); // Replace the catalog reference with the resolved range - dependencies[identStr] = structUtils.makeRange({protocol, source, params, selector}); + dependencies[identStr] = tryRoundTripRange(resolvedRange, workspace.project.configuration); } } }, From 9bb69dc2fb10390a53a1385abde88d70d9d2e81c Mon Sep 17 00:00:00 2001 From: hoyeon Date: Tue, 4 Nov 2025 14:48:23 +0900 Subject: [PATCH 2/4] add test --- packages/plugin-catalog/tests/utils.test.ts | 70 +++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/plugin-catalog/tests/utils.test.ts b/packages/plugin-catalog/tests/utils.test.ts index 3072b2b2897..9a48adaa88c 100644 --- a/packages/plugin-catalog/tests/utils.test.ts +++ b/packages/plugin-catalog/tests/utils.test.ts @@ -437,5 +437,75 @@ describe(`utils`, () => { expect(result).toBe(modifiedDescriptor); expect(result.range).toBe(`npm:^18.0.0-modified`); }); + + it(`should preserve patch: protocol when resolving catalog reference`, () => { + const catalog = new Map(); + catalog.set(`typescript`, `patch:typescript@npm%3A^5.9.3#optional!builtin`); + configuration.values.set(`catalog`, catalog); + + const dependency = structUtils.makeDescriptor( + structUtils.makeIdent(null, `typescript`), + `catalog:`, + ); + + // Mock the resolver to return the patch descriptor + const patchRange = `patch:typescript@npm%3A^5.9.3#optional!builtin`; + const patchDescriptor = structUtils.makeDescriptor( + structUtils.makeIdent(null, `typescript`), + patchRange, + ); + mockResolver.bindDescriptor.mockReturnValue(patchDescriptor); + + const result = resolveDescriptorFromCatalog(project, dependency, mockResolver, resolveOptions); + + expect(result.range).toBe(patchRange); + expect(result.range).toMatch(/^patch:/); + }); + + it(`should preserve portal: protocol when resolving catalog reference`, () => { + const catalog = new Map(); + catalog.set(`my-package`, `portal:../local-package`); + configuration.values.set(`catalog`, catalog); + + const dependency = structUtils.makeDescriptor( + structUtils.makeIdent(null, `my-package`), + `catalog:`, + ); + + const portalRange = `portal:../local-package`; + const portalDescriptor = structUtils.makeDescriptor( + structUtils.makeIdent(null, `my-package`), + portalRange, + ); + mockResolver.bindDescriptor.mockReturnValue(portalDescriptor); + + const result = resolveDescriptorFromCatalog(project, dependency, mockResolver, resolveOptions); + + expect(result.range).toBe(portalRange); + expect(result.range).toMatch(/^portal:/); + }); + + it(`should preserve link: protocol when resolving catalog reference`, () => { + const catalog = new Map(); + catalog.set(`my-package`, `link:../linked-package`); + configuration.values.set(`catalog`, catalog); + + const dependency = structUtils.makeDescriptor( + structUtils.makeIdent(null, `my-package`), + `catalog:`, + ); + + const linkRange = `link:../linked-package`; + const linkDescriptor = structUtils.makeDescriptor( + structUtils.makeIdent(null, `my-package`), + linkRange, + ); + mockResolver.bindDescriptor.mockReturnValue(linkDescriptor); + + const result = resolveDescriptorFromCatalog(project, dependency, mockResolver, resolveOptions); + + expect(result.range).toBe(linkRange); + expect(result.range).toMatch(/^link:/); + }); }); }); From 05855e57e5007fb93a321df57700e80d54572293 Mon Sep 17 00:00:00 2001 From: hoyeon Date: Tue, 4 Nov 2025 17:14:52 +0900 Subject: [PATCH 3/4] add version --- .yarn/versions/33ccb772.yml | 24 +++++++++++++++++++++ packages/plugin-catalog/tests/utils.test.ts | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .yarn/versions/33ccb772.yml diff --git a/.yarn/versions/33ccb772.yml b/.yarn/versions/33ccb772.yml new file mode 100644 index 00000000000..0a460931acc --- /dev/null +++ b/.yarn/versions/33ccb772.yml @@ -0,0 +1,24 @@ +releases: + "@yarnpkg/cli": patch + "@yarnpkg/plugin-catalog": patch + +declined: + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@yarnpkg/plugin-essentials" + - "@yarnpkg/plugin-init" + - "@yarnpkg/plugin-interactive-tools" + - "@yarnpkg/plugin-nm" + - "@yarnpkg/plugin-npm-cli" + - "@yarnpkg/plugin-pack" + - "@yarnpkg/plugin-patch" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-pnpm" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-version" + - "@yarnpkg/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/core" + - "@yarnpkg/doctor" diff --git a/packages/plugin-catalog/tests/utils.test.ts b/packages/plugin-catalog/tests/utils.test.ts index 9a48adaa88c..86dd92ce6ae 100644 --- a/packages/plugin-catalog/tests/utils.test.ts +++ b/packages/plugin-catalog/tests/utils.test.ts @@ -438,7 +438,7 @@ describe(`utils`, () => { expect(result.range).toBe(`npm:^18.0.0-modified`); }); - it(`should preserve patch: protocol when resolving catalog reference`, () => { + it(`should preserve patch: protocol when resolving catalog reference.`, () => { const catalog = new Map(); catalog.set(`typescript`, `patch:typescript@npm%3A^5.9.3#optional!builtin`); configuration.values.set(`catalog`, catalog); From 7c75092e888ed3a65e0392e88c198fed761ca06f Mon Sep 17 00:00:00 2001 From: hoyeon Date: Wed, 5 Nov 2025 07:45:32 +0900 Subject: [PATCH 4/4] update format --- .../pkg-tests-specs/sources/commands/patchCommit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/patchCommit.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/commands/patchCommit.test.ts index cff830889e8..6b78617de63 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/commands/patchCommit.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/patchCommit.test.ts @@ -114,7 +114,7 @@ describe(`Commands`, () => { }); expect(manifest.resolutions).toEqual({ - [`no-deps@npm:1.0.0`]: expect.stringMatching(/^patch:no-deps/), + [`no-deps@npm:1.0.0`]: expect.stringMatching(/^patch:no-deps/), }); }), );