diff --git a/.changeset/cyan-houses-call.md b/.changeset/cyan-houses-call.md new file mode 100644 index 0000000000..188069fb0f --- /dev/null +++ b/.changeset/cyan-houses-call.md @@ -0,0 +1,9 @@ +--- +"@pnpm/core": minor +"@pnpm/hooks.read-package-hook": major +"@pnpm/outdated": minor +"@pnpm/parse-overrides": minor +"pnpm": minor +--- + +Overrides now support catalogs [#8303](https://github.com/pnpm/pnpm/issues/8303). diff --git a/config/parse-overrides/package.json b/config/parse-overrides/package.json index ddade0450f..64a4576bea 100644 --- a/config/parse-overrides/package.json +++ b/config/parse-overrides/package.json @@ -29,6 +29,8 @@ "homepage": "https://github.com/pnpm/pnpm/blob/main/config/parse-overrides#readme", "funding": "https://opencollective.com/pnpm", "dependencies": { + "@pnpm/catalogs.resolver": "workspace:*", + "@pnpm/catalogs.types": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/parse-wanted-dependency": "workspace:*" }, diff --git a/config/parse-overrides/src/index.ts b/config/parse-overrides/src/index.ts index 371036861d..4eead91091 100644 --- a/config/parse-overrides/src/index.ts +++ b/config/parse-overrides/src/index.ts @@ -1,9 +1,12 @@ import { PnpmError } from '@pnpm/error' import { parseWantedDependency } from '@pnpm/parse-wanted-dependency' +import { matchCatalogResolveResult, resolveFromCatalog } from '@pnpm/catalogs.resolver' +import { type Catalogs } from '@pnpm/catalogs.types' const DELIMITER_REGEX = /[^ |@]>/ export interface VersionOverride { + selector: string parentPkg?: PackageSelector targetPkg: PackageSelector newPref: string @@ -15,28 +18,47 @@ export interface PackageSelector { } export function parseOverrides ( - overrides: Record + overrides: Record, + catalogs?: Catalogs ): VersionOverride[] { + const _resolveFromCatalog = resolveFromCatalog.bind(null, catalogs ?? {}) return Object.entries(overrides) .map(([selector, newPref]) => { - let delimiterIndex = selector.search(DELIMITER_REGEX) - if (delimiterIndex !== -1) { - delimiterIndex++ - const parentSelector = selector.substring(0, delimiterIndex) - const childSelector = selector.substring(delimiterIndex + 1) - return { - newPref, - parentPkg: parsePkgSelector(parentSelector), - targetPkg: parsePkgSelector(childSelector), - } - } + const result = parsePkgAndParentSelector(selector) + const resolvedCatalog = matchCatalogResolveResult(_resolveFromCatalog({ + pref: newPref, + alias: result.targetPkg.name, + }), { + found: ({ resolution }) => resolution.specifier, + unused: () => undefined, + misconfiguration: ({ error }) => { + throw new PnpmError('CATALOG_IN_OVERRIDES', `Could not resolve a catalog in the overrides: ${error.message}`) + }, + }) return { - newPref, - targetPkg: parsePkgSelector(selector), + selector, + newPref: resolvedCatalog ?? newPref, + ...result, } }) } +function parsePkgAndParentSelector (selector: string): Pick { + let delimiterIndex = selector.search(DELIMITER_REGEX) + if (delimiterIndex !== -1) { + delimiterIndex++ + const parentSelector = selector.substring(0, delimiterIndex) + const childSelector = selector.substring(delimiterIndex + 1) + return { + parentPkg: parsePkgSelector(parentSelector), + targetPkg: parsePkgSelector(childSelector), + } + } + return { + targetPkg: parsePkgSelector(selector), + } +} + function parsePkgSelector (selector: string): PackageSelector { const wantedDep = parseWantedDependency(selector) if (!wantedDep.alias) { diff --git a/config/parse-overrides/test/index.ts b/config/parse-overrides/test/index.ts index e075106347..d22aa6b4c5 100644 --- a/config/parse-overrides/test/index.ts +++ b/config/parse-overrides/test/index.ts @@ -3,11 +3,11 @@ import { parseOverrides } from '@pnpm/parse-overrides' test.each([ [ { foo: '1' }, - [{ newPref: '1', targetPkg: { name: 'foo' } }], + [{ selector: 'foo', newPref: '1', targetPkg: { name: 'foo' } }], ], [ { 'foo@2': '1' }, - [{ newPref: '1', targetPkg: { name: 'foo', pref: '2' } }], + [{ selector: 'foo@2', newPref: '1', targetPkg: { name: 'foo', pref: '2' } }], ], [ { @@ -15,8 +15,8 @@ test.each([ 'foo@3 || >=2': '1', }, [ - { newPref: '1', targetPkg: { name: 'foo', pref: '>2' } }, - { newPref: '1', targetPkg: { name: 'foo', pref: '3 || >=2' } }, + { selector: 'foo@>2', newPref: '1', targetPkg: { name: 'foo', pref: '>2' } }, + { selector: 'foo@3 || >=2', newPref: '1', targetPkg: { name: 'foo', pref: '3 || >=2' } }, ], ], [ @@ -27,10 +27,10 @@ test.each([ 'bar@1>foo@1': '2', }, [ - { newPref: '2', parentPkg: { name: 'bar' }, targetPkg: { name: 'foo' } }, - { newPref: '2', parentPkg: { name: 'bar', pref: '1' }, targetPkg: { name: 'foo' } }, - { newPref: '2', parentPkg: { name: 'bar' }, targetPkg: { name: 'foo', pref: '1' } }, - { newPref: '2', parentPkg: { name: 'bar', pref: '1' }, targetPkg: { name: 'foo', pref: '1' } }, + { selector: 'bar>foo', newPref: '2', parentPkg: { name: 'bar' }, targetPkg: { name: 'foo' } }, + { selector: 'bar@1>foo', newPref: '2', parentPkg: { name: 'bar', pref: '1' }, targetPkg: { name: 'foo' } }, + { selector: 'bar>foo@1', newPref: '2', parentPkg: { name: 'bar' }, targetPkg: { name: 'foo', pref: '1' } }, + { selector: 'bar@1>foo@1', newPref: '2', parentPkg: { name: 'bar', pref: '1' }, targetPkg: { name: 'foo', pref: '1' } }, ], ], [ @@ -39,14 +39,15 @@ test.each([ 'foo@3 || >=2>bar@3 || >=2': '1', }, [ - { newPref: '1', parentPkg: { name: 'foo', pref: '>2' }, targetPkg: { name: 'bar', pref: '>2' } }, - { newPref: '1', parentPkg: { name: 'foo', pref: '3 || >=2' }, targetPkg: { name: 'bar', pref: '3 || >=2' } }, + { selector: 'foo@>2>bar@>2', newPref: '1', parentPkg: { name: 'foo', pref: '>2' }, targetPkg: { name: 'bar', pref: '>2' } }, + { selector: 'foo@3 || >=2>bar@3 || >=2', newPref: '1', parentPkg: { name: 'foo', pref: '3 || >=2' }, targetPkg: { name: 'bar', pref: '3 || >=2' } }, ], ], ])('parseOverrides()', (overrides, expectedResult) => { - expect(parseOverrides(overrides)).toEqual(expectedResult) + expect(parseOverrides(overrides, {})).toEqual(expectedResult) }) test('parseOverrides() throws an exception on invalid selector', () => { - expect(() => parseOverrides({ '%': '2' })).toThrow('Cannot parse the "%" selector') + expect(() => parseOverrides({ '%': '2' }, {})).toThrow('Cannot parse the "%" selector') + expect(() => parseOverrides({ 'foo > bar': '2' }, {})).toThrow('Cannot parse the "foo > bar" selector') }) diff --git a/config/parse-overrides/tsconfig.json b/config/parse-overrides/tsconfig.json index 5872b8e958..d119a86c4f 100644 --- a/config/parse-overrides/tsconfig.json +++ b/config/parse-overrides/tsconfig.json @@ -9,6 +9,12 @@ "../../__typings__/**/*.d.ts" ], "references": [ + { + "path": "../../catalogs/resolver" + }, + { + "path": "../../catalogs/types" + }, { "path": "../../packages/error" }, diff --git a/hooks/read-package-hook/src/createReadPackageHook.ts b/hooks/read-package-hook/src/createReadPackageHook.ts index 6f72d13574..9760bbd9de 100644 --- a/hooks/read-package-hook/src/createReadPackageHook.ts +++ b/hooks/read-package-hook/src/createReadPackageHook.ts @@ -9,7 +9,7 @@ import isEmpty from 'ramda/src/isEmpty' import pipeWith from 'ramda/src/pipeWith' import { createOptionalDependenciesRemover } from './createOptionalDependenciesRemover' import { createPackageExtender } from './createPackageExtender' -import { createVersionsOverrider } from './createVersionsOverrider' +import { createVersionsOverrider, type VersionOverrideWithoutRawSelector } from './createVersionsOverrider' export function createReadPackageHook ( { @@ -22,7 +22,7 @@ export function createReadPackageHook ( }: { ignoreCompatibilityDb?: boolean lockfileDir: string - overrides?: Record + overrides?: VersionOverrideWithoutRawSelector[] ignoredOptionalDependencies?: string[] packageExtensions?: Record readPackageHook?: ReadPackageHook[] | ReadPackageHook diff --git a/hooks/read-package-hook/src/createVersionsOverrider.ts b/hooks/read-package-hook/src/createVersionsOverrider.ts index bfb67772a5..2edc9f0f05 100644 --- a/hooks/read-package-hook/src/createVersionsOverrider.ts +++ b/hooks/read-package-hook/src/createVersionsOverrider.ts @@ -2,18 +2,18 @@ import path from 'path' import semver from 'semver' import partition from 'ramda/src/partition' import { type Dependencies, type PackageManifest, type ReadPackageHook } from '@pnpm/types' -import { PnpmError } from '@pnpm/error' -import { parseOverrides, type VersionOverride as VersionOverrideBase } from '@pnpm/parse-overrides' +import { type VersionOverride as VersionOverrideBase } from '@pnpm/parse-overrides' import normalizePath from 'normalize-path' import { isIntersectingRange } from './isIntersectingRange' +export type VersionOverrideWithoutRawSelector = Omit + export function createVersionsOverrider ( - overrides: Record, + overrides: VersionOverrideWithoutRawSelector[], rootDir: string ): ReadPackageHook { - const parsedOverrides = tryParseOverrides(overrides) const [versionOverrides, genericVersionOverrides] = partition(({ parentPkg }) => parentPkg != null, - parsedOverrides + overrides .map((override) => { return { ...override, @@ -34,14 +34,6 @@ export function createVersionsOverrider ( }) as ReadPackageHook } -function tryParseOverrides (overrides: Record): VersionOverrideBase[] { - try { - return parseOverrides(overrides) - } catch (e) { - throw new PnpmError('INVALID_OVERRIDES_SELECTOR', `${(e as PnpmError).message} in pnpm.overrides`) - } -} - interface LocalTarget { protocol: LocalProtocol absolutePath: string @@ -50,7 +42,7 @@ interface LocalTarget { type LocalProtocol = 'link:' | 'file:' -function createLocalTarget (override: VersionOverrideBase, rootDir: string): LocalTarget | undefined { +function createLocalTarget (override: VersionOverrideWithoutRawSelector, rootDir: string): LocalTarget | undefined { let protocol: LocalProtocol | undefined if (override.newPref.startsWith('file:')) { protocol = 'file:' @@ -69,12 +61,7 @@ interface VersionOverride extends VersionOverrideBase { localTarget?: LocalTarget } -interface VersionOverrideWithParent extends VersionOverride { - parentPkg: { - name: string - pref?: string - } -} +type VersionOverrideWithParent = VersionOverride & Required> function overrideDepsOfPkg ( { manifest, dir }: { manifest: PackageManifest, dir: string | undefined }, diff --git a/hooks/read-package-hook/test/createReadPackageHook.ts b/hooks/read-package-hook/test/createReadPackageHook.ts index 10d58db8fb..33d7896a76 100644 --- a/hooks/read-package-hook/test/createReadPackageHook.ts +++ b/hooks/read-package-hook/test/createReadPackageHook.ts @@ -27,9 +27,14 @@ test('createReadPackageHook() runs the custom hook before the version overrider' ignoreCompatibilityDb: true, lockfileDir: '/foo', readPackageHook: [hook], - overrides: { - react: '16', - }, + overrides: [ + { + targetPkg: { + name: 'react', + }, + newPref: '16', + }, + ], }) const manifest = {} const dir = '/bar' diff --git a/hooks/read-package-hook/test/createVersionOverrider.test.ts b/hooks/read-package-hook/test/createVersionOverrider.test.ts index a777b083d2..88dcc4ea59 100644 --- a/hooks/read-package-hook/test/createVersionOverrider.test.ts +++ b/hooks/read-package-hook/test/createVersionOverrider.test.ts @@ -2,10 +2,22 @@ import path from 'path' import { createVersionsOverrider } from '../lib/createVersionsOverrider' test('createVersionsOverrider() matches sub-ranges', () => { - const overrider = createVersionsOverrider({ - 'foo@2': '2.12.0', - 'qar@>2': '1.0.0', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + targetPkg: { + name: 'foo', + pref: '2', + }, + newPref: '2.12.0', + }, + { + targetPkg: { + name: 'qar', + pref: '>2', + }, + newPref: '1.0.0', + }, + ], process.cwd()) expect( overrider({ dependencies: { foo: '^2.10.0' }, @@ -18,10 +30,22 @@ test('createVersionsOverrider() matches sub-ranges', () => { }) test('createVersionsOverrider() does not fail on non-range selectors', () => { - const overrider = createVersionsOverrider({ - 'foo@2': '2.12.0', - 'bar@github:org/bar': '2.12.0', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + targetPkg: { + name: 'foo', + pref: '2', + }, + newPref: '2.12.0', + }, + { + targetPkg: { + name: 'bar', + pref: 'github:org/bar', + }, + newPref: '2.12.0', + }, + ], process.cwd()) expect( overrider({ dependencies: { @@ -38,10 +62,30 @@ test('createVersionsOverrider() does not fail on non-range selectors', () => { }) test('createVersionsOverrider() overrides dependencies of specified packages only', () => { - const overrider = createVersionsOverrider({ - 'foo@1>bar@^1.2.0': '3.0.0', - 'qar@1>bar@>4': '3.0.0', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + parentPkg: { + name: 'foo', + pref: '1', + }, + targetPkg: { + name: 'bar', + pref: '^1.2.0', + }, + newPref: '3.0.0', + }, + { + parentPkg: { + name: 'qar', + pref: '1', + }, + targetPkg: { + name: 'bar', + pref: '>4', + }, + newPref: '3.0.0', + }, + ], process.cwd()) expect(overrider({ name: 'foo', version: '1.2.0', @@ -97,11 +141,26 @@ test('createVersionsOverrider() overrides dependencies of specified packages onl }) test('createVersionsOverrider() overrides all types of dependencies', () => { - const overrider = createVersionsOverrider({ - foo: '3.0.0', - bar: '3.0.0', - qar: '3.0.0', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + targetPkg: { + name: 'foo', + }, + newPref: '3.0.0', + }, + { + targetPkg: { + name: 'bar', + }, + newPref: '3.0.0', + }, + { + targetPkg: { + name: 'qar', + }, + newPref: '3.0.0', + }, + ], process.cwd()) expect(overrider({ name: 'foo', version: '1.2.0', @@ -130,9 +189,14 @@ test('createVersionsOverrider() overrides all types of dependencies', () => { }) test('createVersionsOverrider() overrides dependencies with links', () => { - const overrider = createVersionsOverrider({ - qar: 'link:../qar', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + targetPkg: { + name: 'qar', + }, + newPref: 'link:../qar', + }, + ], process.cwd()) expect(overrider({ name: 'foo', version: '1.2.0', @@ -150,9 +214,14 @@ test('createVersionsOverrider() overrides dependencies with links', () => { test('createVersionsOverrider() overrides dependencies with absolute links', () => { const qarAbsolutePath = path.resolve(process.cwd(), './qar') - const overrider = createVersionsOverrider({ - qar: `link:${qarAbsolutePath}`, - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + targetPkg: { + name: 'qar', + }, + newPref: `link:${qarAbsolutePath}`, + }, + ], process.cwd()) expect(overrider({ name: 'foo', @@ -170,9 +239,18 @@ test('createVersionsOverrider() overrides dependencies with absolute links', () }) test('createVersionsOverrider() overrides dependency of pkg matched by name and version', () => { - const overrider = createVersionsOverrider({ - 'yargs@^7.1.0>yargs-parser': '^20.0.0', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + parentPkg: { + name: 'yargs', + pref: '^7.1.0', + }, + targetPkg: { + name: 'yargs-parser', + }, + newPref: '^20.0.0', + }, + ], process.cwd()) expect( overrider({ name: 'yargs', @@ -191,9 +269,18 @@ test('createVersionsOverrider() overrides dependency of pkg matched by name and }) test('createVersionsOverrider() does not override dependency of pkg matched by name and version', () => { - const overrider = createVersionsOverrider({ - 'yargs@^8.1.0>yargs-parser': '^20.0.0', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + parentPkg: { + name: 'yargs', + pref: '^8.1.0', + }, + targetPkg: { + name: 'yargs-parser', + }, + newPref: '^20.0.0', + }, + ], process.cwd()) expect( overrider({ name: 'yargs', @@ -212,9 +299,17 @@ test('createVersionsOverrider() does not override dependency of pkg matched by n }) test('createVersionsOverrider() should work for scoped parent and unscoped child', () => { - const overrider = createVersionsOverrider({ - '@scoped/package>unscoped-package': 'workspace:*', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + parentPkg: { + name: '@scoped/package', + }, + targetPkg: { + name: 'unscoped-package', + }, + newPref: 'workspace:*', + }, + ], process.cwd()) expect( overrider({ name: '@scoped/package', @@ -233,9 +328,17 @@ test('createVersionsOverrider() should work for scoped parent and unscoped child }) test('createVersionsOverrider() should work for unscoped parent and scoped child', () => { - const overrider = createVersionsOverrider({ - 'unscoped-package>@scoped/package': 'workspace:*', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + parentPkg: { + name: 'unscoped-package', + }, + targetPkg: { + name: '@scoped/package', + }, + newPref: 'workspace:*', + }, + ], process.cwd()) expect( overrider({ name: 'unscoped-package', @@ -254,9 +357,17 @@ test('createVersionsOverrider() should work for unscoped parent and scoped child }) test('createVersionsOverrider() should work for scoped parent and scoped child', () => { - const overrider = createVersionsOverrider({ - '@scoped/package>@scoped/package2': 'workspace:*', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + parentPkg: { + name: '@scoped/package', + }, + targetPkg: { + name: '@scoped/package2', + }, + newPref: 'workspace:*', + }, + ], process.cwd()) expect( overrider({ name: '@scoped/package', @@ -276,9 +387,14 @@ test('createVersionsOverrider() should work for scoped parent and scoped child', test('createVersionsOverrider() overrides dependencies with file with relative path for root package', () => { const rootDir = process.cwd() - const overrider = createVersionsOverrider({ - qar: 'file:../qar', - }, rootDir) + const overrider = createVersionsOverrider([ + { + targetPkg: { + name: 'qar', + }, + newPref: 'file:../qar', + }, + ], rootDir) expect(overrider({ name: 'foo', version: '1.2.0', @@ -296,9 +412,14 @@ test('createVersionsOverrider() overrides dependencies with file with relative p test('createVersionsOverrider() overrides dependencies with file with relative path for workspace package', () => { const rootDir = process.cwd() - const overrider = createVersionsOverrider({ - qar: 'file:../qar', - }, rootDir) + const overrider = createVersionsOverrider([ + { + targetPkg: { + name: 'qar', + }, + newPref: 'file:../qar', + }, + ], rootDir) expect(overrider({ name: 'foo', version: '1.2.0', @@ -316,9 +437,14 @@ test('createVersionsOverrider() overrides dependencies with file with relative p test('createVersionsOverrider() overrides dependencies with file specified with absolute path', () => { const absolutePath = path.join(__dirname, 'qar') - const overrider = createVersionsOverrider({ - qar: `file:${absolutePath}`, - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + targetPkg: { + name: 'qar', + }, + newPref: `file:${absolutePath}`, + }, + ], process.cwd()) expect(overrider({ name: 'foo', version: '1.2.0', @@ -335,13 +461,48 @@ test('createVersionsOverrider() overrides dependencies with file specified with }) test('createVersionOverride() should use the most specific rule when both override rules match the same target', () => { - const overrider = createVersionsOverrider({ - foo: '3.0.0', - 'foo@3': '4.0.0', - 'foo@2': '2.12.0', - 'bar>foo@2': 'github:org/foo', - 'bar>foo@3': '5.0.0', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + targetPkg: { + name: 'foo', + }, + newPref: '3.0.0', + }, + { + targetPkg: { + name: 'foo', + pref: '3', + }, + newPref: '4.0.0', + }, + { + targetPkg: { + name: 'foo', + pref: '2', + }, + newPref: '2.12.0', + }, + { + parentPkg: { + name: 'bar', + }, + targetPkg: { + name: 'foo', + pref: '2', + }, + newPref: 'github:org/foo', + }, + { + parentPkg: { + name: 'bar', + }, + targetPkg: { + name: 'foo', + pref: '3', + }, + newPref: '5.0.0', + }, + ], process.cwd()) expect( overrider({ dependencies: { @@ -407,16 +568,16 @@ test('createVersionOverride() should use the most specific rule when both overri }) }) -test('createVersionOverrider() throws error when supplied an invalid selector', () => { - expect(() => createVersionsOverrider({ - 'foo > bar': '2', - }, process.cwd())).toThrowError('Cannot parse the "foo > bar" selector in pnpm.overrides') -}) - test('createVersionsOverrider() matches intersections', () => { - const overrider = createVersionsOverrider({ - 'foo@<1.2.4': '>=1.2.4', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + targetPkg: { + name: 'foo', + pref: '<1.2.4', + }, + newPref: '>=1.2.4', + }, + ], process.cwd()) expect( overrider({ dependencies: { foo: '^1.2.3' }, @@ -427,9 +588,17 @@ test('createVersionsOverrider() matches intersections', () => { }) test('createVersionsOverrider() overrides peerDependencies of another dependency', () => { - const overrider = createVersionsOverrider({ - 'react-dom>react': '18.1.0', - }, process.cwd()) + const overrider = createVersionsOverrider([ + { + parentPkg: { + name: 'react-dom', + }, + targetPkg: { + name: 'react', + }, + newPref: '18.1.0', + }, + ], process.cwd()) expect( overrider({ name: 'react-dom', diff --git a/pkg-manager/core/package.json b/pkg-manager/core/package.json index cc61b27de4..9ca444d70d 100644 --- a/pkg-manager/core/package.json +++ b/pkg-manager/core/package.json @@ -47,6 +47,7 @@ "@pnpm/normalize-registries": "workspace:*", "@pnpm/npm-package-arg": "catalog:", "@pnpm/package-requester": "workspace:*", + "@pnpm/parse-overrides": "workspace:*", "@pnpm/parse-wanted-dependency": "workspace:*", "@pnpm/pkg-manager.direct-dep-linker": "workspace:*", "@pnpm/prune-lockfile": "workspace:*", diff --git a/pkg-manager/core/src/getPeerDependencyIssues.ts b/pkg-manager/core/src/getPeerDependencyIssues.ts index 995d7b7a65..11ec071e62 100644 --- a/pkg-manager/core/src/getPeerDependencyIssues.ts +++ b/pkg-manager/core/src/getPeerDependencyIssues.ts @@ -3,8 +3,9 @@ import { resolveDependencies, getWantedDependencies } from '@pnpm/resolve-depend import { type PeerDependencyIssuesByProjects } from '@pnpm/types' import { getContext, type GetContextOptions, type ProjectOptions } from '@pnpm/get-context' import { createReadPackageHook } from '@pnpm/hooks.read-package-hook' -import { type InstallOptions } from './install/extendInstallOptions' import { DEFAULT_REGISTRIES } from '@pnpm/normalize-registries' +import { parseOverrides } from '@pnpm/parse-overrides' +import { type InstallOptions } from './install/extendInstallOptions' export type ListMissingPeersOptions = Partial & Pick manifest) ) + const overrides = parseOverrides(opts.overrides ?? {}, opts.catalogs ?? {}) const { peerDependencyIssuesByProjects, waitTillAllFetchingsFinish, @@ -71,7 +73,7 @@ export async function getPeerDependencyIssues ( readPackage: createReadPackageHook({ ignoreCompatibilityDb: opts.ignoreCompatibilityDb, lockfileDir, - overrides: opts.overrides, + overrides, packageExtensions: opts.packageExtensions, readPackageHook: opts.hooks?.readPackage, ignoredOptionalDependencies: opts.ignoredOptionalDependencies, diff --git a/pkg-manager/core/src/install/extendInstallOptions.ts b/pkg-manager/core/src/install/extendInstallOptions.ts index 1dc9d2e9b0..b891253895 100644 --- a/pkg-manager/core/src/install/extendInstallOptions.ts +++ b/pkg-manager/core/src/install/extendInstallOptions.ts @@ -17,6 +17,7 @@ import { type Registries, type PrepareExecutionEnv, } from '@pnpm/types' +import { parseOverrides, type VersionOverride } from '@pnpm/parse-overrides' import { pnpmPkgJson } from '../pnpmPkgJson' import { type ReporterFunction } from '../types' import { type PreResolutionHookContext } from '@pnpm/hooks.types' @@ -252,6 +253,7 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => { export type ProcessedInstallOptions = StrictInstallOptions & { readPackageHook?: ReadPackageHook + parsedOverrides: VersionOverride[] } export function extendOptions ( @@ -272,11 +274,12 @@ export function extendOptions ( ...defaultOpts, ...opts, storeDir: defaultOpts.storeDir, + parsedOverrides: parseOverrides(opts.overrides ?? {}, opts.catalogs ?? {}), } extendedOpts.readPackageHook = createReadPackageHook({ ignoreCompatibilityDb: extendedOpts.ignoreCompatibilityDb, readPackageHook: extendedOpts.hooks?.readPackage, - overrides: extendedOpts.overrides, + overrides: extendedOpts.parsedOverrides, lockfileDir: extendedOpts.lockfileDir, packageExtensions: extendedOpts.packageExtensions, ignoredOptionalDependencies: extendedOpts.ignoredOptionalDependencies, diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index 6112905f27..18233f4f13 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -340,12 +340,13 @@ export async function mutateModules ( const frozenLockfile = opts.frozenLockfile || opts.frozenLockfileIfExists && ctx.existsNonEmptyWantedLockfile let outdatedLockfileSettings = false + const overridesMap = Object.fromEntries((opts.parsedOverrides ?? []).map(({ selector, newPref }) => [selector, newPref])) if (!opts.ignorePackageManifest) { const outdatedLockfileSettingName = getOutdatedLockfileSetting(ctx.wantedLockfile, { autoInstallPeers: opts.autoInstallPeers, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, peersSuffixMaxLength: opts.peersSuffixMaxLength, - overrides: opts.overrides, + overrides: overridesMap, ignoredOptionalDependencies: opts.ignoredOptionalDependencies?.sort(), packageExtensionsChecksum, patchedDependencies, @@ -367,7 +368,7 @@ export async function mutateModules ( excludeLinksFromLockfile: opts.excludeLinksFromLockfile, peersSuffixMaxLength: opts.peersSuffixMaxLength, } - ctx.wantedLockfile.overrides = opts.overrides + ctx.wantedLockfile.overrides = overridesMap ctx.wantedLockfile.packageExtensionsChecksum = packageExtensionsChecksum ctx.wantedLockfile.ignoredOptionalDependencies = opts.ignoredOptionalDependencies ctx.wantedLockfile.pnpmfileChecksum = pnpmfileChecksum diff --git a/pkg-manager/core/test/catalogs.ts b/pkg-manager/core/test/catalogs.ts index 8bde8ea8ab..d5c51761c8 100644 --- a/pkg-manager/core/test/catalogs.ts +++ b/pkg-manager/core/test/catalogs.ts @@ -1,6 +1,7 @@ import { createPeersDirSuffix } from '@pnpm/dependency-path' import { type ProjectRootDir, type ProjectId, type ProjectManifest } from '@pnpm/types' import { prepareEmpty } from '@pnpm/prepare' +import { addDistTag } from '@pnpm/registry-mock' import { type MutatedProject, mutateModules, type ProjectOptions, type MutateModulesOptions, addDependenciesToPackage } from '@pnpm/core' import path from 'path' import { testDefaults } from './utils' @@ -678,3 +679,71 @@ describe('update', () => { expect(Object.keys(readLockfile().snapshots)).toEqual(['is-positive@1.0.0']) }) }) + +test('catalogs work in overrides', async () => { + await addDistTag({ package: '@pnpm.e2e/bar', version: '100.0.0', distTag: 'latest' }) + await addDistTag({ package: '@pnpm.e2e/foo', version: '100.0.0', distTag: 'latest' }) + + const overrides: Record = { + '@pnpm.e2e/foobarqar>@pnpm.e2e/foo': 'catalog:', + '@pnpm.e2e/bar@^100.0.0': 'catalog:', + '@pnpm.e2e/dep-of-pkg-with-1-dep': 'catalog:', + } + + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([ + { + name: 'project1', + dependencies: { + '@pnpm.e2e/pkg-with-1-dep': '100.0.0', + '@pnpm.e2e/foobar': '100.0.0', + '@pnpm.e2e/foobarqar': '1.0.0', + }, + }, + // Empty second project to create a multi-package workspace. + { + name: 'project2', + }, + ]) + + const catalogs = { + default: { + '@pnpm.e2e/foo': 'npm:@pnpm.e2e/qar@100.0.0', + '@pnpm.e2e/bar': '100.1.0', + '@pnpm.e2e/dep-of-pkg-with-1-dep': '101.0.0', + }, + } + await mutateModules(installProjects(projects), { + ...options, + lockfileOnly: true, + catalogs, + overrides, + }) + + let lockfile = readLockfile() + expect(lockfile.snapshots['@pnpm.e2e/foobarqar@1.0.0'].dependencies?.['@pnpm.e2e/foo']).toBe('@pnpm.e2e/qar@100.0.0') + expect(lockfile.snapshots['@pnpm.e2e/foobar@100.0.0'].dependencies?.['@pnpm.e2e/foo']).toBe('100.0.0') + expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/dep-of-pkg-with-1-dep@101.0.0']) + expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/bar@100.1.0']) + expect(lockfile.overrides).toStrictEqual({ + '@pnpm.e2e/foobarqar>@pnpm.e2e/foo': 'npm:@pnpm.e2e/qar@100.0.0', + '@pnpm.e2e/bar@^100.0.0': '100.1.0', + '@pnpm.e2e/dep-of-pkg-with-1-dep': '101.0.0', + }) + + catalogs.default['@pnpm.e2e/bar'] = '100.0.0' + await mutateModules(installProjects(projects), { + ...options, + lockfileOnly: true, + catalogs, + overrides, + }) + + lockfile = readLockfile() + expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/bar@100.0.0']) + expect(lockfile.packages).not.toHaveProperty(['@pnpm.e2e/bar@100.1.0']) + expect(lockfile.overrides).toStrictEqual({ + '@pnpm.e2e/foobarqar>@pnpm.e2e/foo': 'npm:@pnpm.e2e/qar@100.0.0', + '@pnpm.e2e/bar@^100.0.0': '100.0.0', + '@pnpm.e2e/dep-of-pkg-with-1-dep': '101.0.0', + }) +}) diff --git a/pkg-manager/core/tsconfig.json b/pkg-manager/core/tsconfig.json index 8580aa34ab..dae45f91da 100644 --- a/pkg-manager/core/tsconfig.json +++ b/pkg-manager/core/tsconfig.json @@ -36,6 +36,9 @@ { "path": "../../config/normalize-registries" }, + { + "path": "../../config/parse-overrides" + }, { "path": "../../crypto/object-hasher" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 693be05dde..b86f28c68f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1432,6 +1432,12 @@ importers: config/parse-overrides: dependencies: + '@pnpm/catalogs.resolver': + specifier: workspace:* + version: link:../../catalogs/resolver + '@pnpm/catalogs.types': + specifier: workspace:* + version: link:../../catalogs/types '@pnpm/error': specifier: workspace:* version: link:../../packages/error @@ -3848,6 +3854,9 @@ importers: '@pnpm/package-requester': specifier: workspace:* version: link:../package-requester + '@pnpm/parse-overrides': + specifier: workspace:* + version: link:../../config/parse-overrides '@pnpm/parse-wanted-dependency': specifier: workspace:* version: link:../../packages/parse-wanted-dependency @@ -6270,6 +6279,9 @@ importers: '@pnpm/npm-resolver': specifier: workspace:* version: link:../../resolving/npm-resolver + '@pnpm/parse-overrides': + specifier: workspace:* + version: link:../../config/parse-overrides '@pnpm/pick-registry-for-package': specifier: workspace:* version: link:../../config/pick-registry-for-package diff --git a/reviewing/outdated/package.json b/reviewing/outdated/package.json index e23aef8fca..ba1d8993b1 100644 --- a/reviewing/outdated/package.json +++ b/reviewing/outdated/package.json @@ -46,6 +46,7 @@ "@pnpm/matcher": "workspace:*", "@pnpm/modules-yaml": "workspace:*", "@pnpm/npm-resolver": "workspace:*", + "@pnpm/parse-overrides": "workspace:*", "@pnpm/pick-registry-for-package": "workspace:*", "@pnpm/types": "workspace:*", "ramda": "catalog:", diff --git a/reviewing/outdated/src/outdated.ts b/reviewing/outdated/src/outdated.ts index f2296fb8a1..e34fed881d 100644 --- a/reviewing/outdated/src/outdated.ts +++ b/reviewing/outdated/src/outdated.ts @@ -28,6 +28,7 @@ import * as dp from '@pnpm/dependency-path' import semver from 'semver' import { createMatcher } from '@pnpm/matcher' import { createReadPackageHook } from '@pnpm/hooks.read-package-hook' +import { parseOverrides } from '@pnpm/parse-overrides' export * from './createManifestGetter' @@ -69,7 +70,7 @@ export async function outdated ( if (overrides) { const readPackageHook = createReadPackageHook({ lockfileDir: opts.lockfileDir, - overrides, + overrides: parseOverrides(overrides, opts.catalogs ?? {}), }) const manifest = await readPackageHook?.(opts.manifest, opts.lockfileDir) if (manifest) return manifest diff --git a/reviewing/outdated/tsconfig.json b/reviewing/outdated/tsconfig.json index 16afef937d..2c2eff6042 100644 --- a/reviewing/outdated/tsconfig.json +++ b/reviewing/outdated/tsconfig.json @@ -18,6 +18,9 @@ { "path": "../../config/matcher" }, + { + "path": "../../config/parse-overrides" + }, { "path": "../../config/pick-registry-for-package" },