mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-19 14:20:36 -04:00
fix: preserve catalog protocol when updating project manifest (#8285)
* fix: preserve catalog protocol when updating project manifest
This makes `pnpm add foo@catalog:` preserve the catalog protocol when
updating `package.json`.
This also fixes a quirk with `pnpm update --latest <name>` when <name>
is a catalog'ed dep. The `updateSpec` field is set on all deps if
`wantedDependencies` filters to 0.
341656f9b3/pkg-manager/resolve-dependencies/src/toResolveImporter.ts (L44-L48)
* test: ensure pnpm update does not update catalog dependencies (yet)
* test: `pnpm add is-positive@catalog:`
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { createPeersDirSuffix } from '@pnpm/dependency-path'
|
||||
import { type ProjectRootDir, type ProjectId, type ProjectManifest } from '@pnpm/types'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { type MutatedProject, mutateModules, type ProjectOptions, type MutateModulesOptions } from '@pnpm/core'
|
||||
import { type MutatedProject, mutateModules, type ProjectOptions, type MutateModulesOptions, addDependenciesToPackage } from '@pnpm/core'
|
||||
import path from 'path'
|
||||
import { testDefaults } from './utils'
|
||||
|
||||
@@ -511,3 +511,123 @@ test('lockfile catalog snapshots should remove unused entries', async () => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('add', () => {
|
||||
test('adding is-positive@catalog: works', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {},
|
||||
}])
|
||||
|
||||
const updatedManifest = await addDependenciesToPackage(
|
||||
projects['project1' as ProjectId],
|
||||
['is-positive@catalog:'],
|
||||
{
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
allowNew: true,
|
||||
catalogs: {
|
||||
default: { 'is-positive': '1.0.0' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(updatedManifest).toEqual({
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
})
|
||||
expect(readLockfile()).toEqual(expect.objectContaining({
|
||||
catalogs: { default: { 'is-positive': { specifier: '1.0.0', version: '1.0.0' } } },
|
||||
packages: { 'is-positive@1.0.0': expect.objectContaining({}) },
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
// The 'pnpm update' command should eventually support updates of dependencies
|
||||
// in the catalog. This is a more involved feature since pnpm-workspace.yaml
|
||||
// needs to be edited. Until the catalog update feature is implemented, ensure
|
||||
// pnpm update does not touch or rewrite dependencies using the catalog
|
||||
// protocol.
|
||||
describe('update', () => {
|
||||
test('update does not modify catalog: protocol', async () => {
|
||||
const { options, projects } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
}])
|
||||
|
||||
const updatedManifest = await addDependenciesToPackage(
|
||||
projects['project1' as ProjectId],
|
||||
['is-positive'],
|
||||
{
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
allowNew: false,
|
||||
update: true,
|
||||
catalogs: {
|
||||
default: { 'is-positive': '^1.0.0' },
|
||||
},
|
||||
})
|
||||
|
||||
// Expecting the manifest to remain unchanged.
|
||||
expect(updatedManifest).toEqual({
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('update does not upgrade cataloged dependency', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
}])
|
||||
|
||||
const catalogs = {
|
||||
default: { 'is-positive': '3.0.0' },
|
||||
}
|
||||
const mutateOpts = {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs,
|
||||
}
|
||||
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
// Updating the catalog from 3.0.0 to ^3.0.0. This should still lock to the
|
||||
// existing 3.0.0 version despite version 3.1.0 existing.
|
||||
catalogs.default['is-positive'] = '^3.0.0'
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
expect(readLockfile().catalogs.default).toEqual({
|
||||
'is-positive': { specifier: '^3.0.0', version: '3.0.0' },
|
||||
})
|
||||
|
||||
// Expecting the manifest to remain unchanged after running an update.
|
||||
const updatedManifest = await addDependenciesToPackage(
|
||||
projects['project1' as ProjectId],
|
||||
['is-positive'],
|
||||
{
|
||||
...mutateOpts,
|
||||
update: true,
|
||||
})
|
||||
|
||||
expect(updatedManifest).toEqual({
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
})
|
||||
|
||||
// The lockfile should only contain 3.0.0 and not 3.1.0 (or a later version).
|
||||
expect(readLockfile()).toEqual(expect.objectContaining({
|
||||
catalogs: { default: { 'is-positive': { specifier: '^3.0.0', version: '3.0.0' } } },
|
||||
packages: { 'is-positive@3.0.0': expect.objectContaining({}) },
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -530,6 +530,7 @@ async function resolveDependenciesOfImporterDependency (
|
||||
throw result.error
|
||||
},
|
||||
})
|
||||
const originalPref = extendedWantedDep.wantedDependency.pref
|
||||
|
||||
if (catalogLookup != null) {
|
||||
// The lockfile from a previous installation may have already resolved this
|
||||
@@ -560,7 +561,10 @@ async function resolveDependenciesOfImporterDependency (
|
||||
// If the catalog protocol was used, store metadata about the catalog
|
||||
// lookup to use in the lockfile.
|
||||
if (result.resolveDependencyResult != null && catalogLookup != null) {
|
||||
result.resolveDependencyResult.catalogLookup = catalogLookup
|
||||
result.resolveDependencyResult.catalogLookup = {
|
||||
...catalogLookup,
|
||||
userSpecifiedPref: originalPref,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -62,6 +62,17 @@ export interface ResolvedDirectDependency {
|
||||
export interface CatalogLookupMetadata {
|
||||
readonly catalogName: string
|
||||
readonly specifier: string
|
||||
|
||||
/**
|
||||
* The catalog protocol pref the user wrote in package.json files or as a
|
||||
* parameter to pnpm add. Ex: pnpm add foo@catalog:
|
||||
*
|
||||
* This will usually be 'catalog:<name>', but can simply be 'catalog:' if
|
||||
* users wrote the default catalog shorthand. This is different than the
|
||||
* catalogName field, which would be 'default' regardless of whether users
|
||||
* originally requested 'catalog:' or 'catalog:default'.
|
||||
*/
|
||||
readonly userSpecifiedPref: string
|
||||
}
|
||||
|
||||
export interface Importer<WantedDepExtraProps> {
|
||||
|
||||
@@ -72,6 +72,7 @@ export async function updateProjectManifest (
|
||||
function resolvedDirectDepToSpecObject (
|
||||
{
|
||||
alias,
|
||||
catalogLookup,
|
||||
isNew,
|
||||
name,
|
||||
normalizedPref,
|
||||
@@ -89,7 +90,9 @@ function resolvedDirectDepToSpecObject (
|
||||
}
|
||||
): PackageSpecObject {
|
||||
let pref!: string
|
||||
if (normalizedPref) {
|
||||
if (catalogLookup) {
|
||||
pref = catalogLookup.userSpecifiedPref
|
||||
} else if (normalizedPref) {
|
||||
pref = normalizedPref
|
||||
} else {
|
||||
const shouldUseWorkspaceProtocol = resolution.type === 'directory' &&
|
||||
|
||||
Reference in New Issue
Block a user