Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/salty-beds-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@pnpm/lockfile.settings-checker": patch
"@pnpm/lockfile.verification": patch
"@pnpm/core": patch
"@pnpm/deps.status": patch
---

Properly throw a frozen lockfile error when changing catalogs defined in `pnpm-workspace.yaml` and running `pnpm install --frozen-lockfile`. This previously passed silently as reported in [#9369](https://github.com/pnpm/pnpm/issues/9369).
6 changes: 6 additions & 0 deletions .changeset/stupid-grapes-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pnpm/fs.indexed-pkg-importer": patch
"pnpm": patch
---

Packages that don't have a `package.json` file (like Node.js) should not be reimported from the store on every install. Another file from the package should be checked in order to verify its presence in `node_modules`.
4 changes: 4 additions & 0 deletions __fixtures__/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ packages:
- '!workspace-has-shared-package-lock-json'
- '!workspace-has-shared-npm-shrinkwrap-json'
sharedWorkspaceLockfile: false

catalog:
# Used in has-outdated-deps-using-catalog-protocol fixture.
is-negative: ^1.0.0
1 change: 1 addition & 0 deletions deps/status/src/checkDepsStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ async function assertWantedLockfileUpToDate (
])

const outdatedLockfileSettingName = getOutdatedLockfileSetting(wantedLockfile, {
catalogs: config.catalogs,
autoInstallPeers: config.autoInstallPeers,
injectWorkspacePackages: config.injectWorkspacePackages,
excludeLinksFromLockfile: config.excludeLinksFromLockfile,
Expand Down
46 changes: 22 additions & 24 deletions fs/indexed-pkg-importer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,31 @@ function clonePkg (
to: string,
opts: ImportOptions
): 'clone' | undefined {
const pkgJsonPath = path.join(to, 'package.json')

if (opts.resolvedFrom !== 'store' || opts.force || !existsSync(pkgJsonPath)) {
if (opts.resolvedFrom !== 'store' || opts.force || !pkgExistsAtTargetDir(to, opts.filesMap)) {
importIndexedDir(clone, to, opts.filesMap, opts)
return 'clone'
}
return undefined
}

function pkgExistsAtTargetDir (targetDir: string, filesMap: FilesMap): boolean {
return existsSync(path.join(targetDir, pickFileFromFilesMap(filesMap)))
}

function pickFileFromFilesMap (filesMap: FilesMap): string {
// A package might not have a package.json file.
// For instance, the Node.js package.
// Or injected packages in a Bit workspace.
if (filesMap['package.json']) {
return 'package.json'
}
const files = Object.keys(filesMap)
if (files.length === 0) {
throw new Error('pickFileFromFilesMap cannot pick a file from an empty FilesMap')
}
return files[0]
}

function createCloneFunction (): CloneFunction {
// Node.js currently does not natively support reflinks on Windows and macOS.
// Hence, we use a third party solution.
Expand Down Expand Up @@ -195,24 +211,8 @@ function linkOrCopy (existingPath: string, newPath: string): void {
}
}

function pkgLinkedToStore (
filesMap: FilesMap,
to: string
): boolean {
if (filesMap['package.json']) {
if (isSameFile('package.json', to, filesMap)) {
return true
}
} else {
// An injected package might not have a package.json.
// This will probably only even happen in a Bit workspace.
const [anyFile] = Object.keys(filesMap)
if (isSameFile(anyFile, to, filesMap)) return true
}
return false
}

function isSameFile (filename: string, linkedPkgDir: string, filesMap: FilesMap): boolean {
function pkgLinkedToStore (filesMap: FilesMap, linkedPkgDir: string): boolean {
const filename = pickFileFromFilesMap(filesMap)
const linkedFile = path.join(linkedPkgDir, filename)
let stats0!: Stats
try {
Expand All @@ -230,9 +230,7 @@ export function copyPkg (
to: string,
opts: ImportOptions
): 'copy' | undefined {
const pkgJsonPath = path.join(to, 'package.json')

if (opts.resolvedFrom !== 'store' || opts.force || !existsSync(pkgJsonPath)) {
if (opts.resolvedFrom !== 'store' || opts.force || !pkgExistsAtTargetDir(to, opts.filesMap)) {
importIndexedDir(fs.copyFileSync, to, opts.filesMap, opts)
return 'copy'
}
Expand Down
2 changes: 2 additions & 0 deletions lockfile/settings-checker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
"compile": "tsc --build && pnpm run lint --fix"
},
"dependencies": {
"@pnpm/catalogs.types": "workspace:*",
"@pnpm/crypto.hash": "workspace:*",
"@pnpm/lockfile.types": "workspace:*",
"@pnpm/lockfile.verification": "workspace:*",
"@pnpm/parse-overrides": "workspace:*",
"p-map-values": "catalog:",
"ramda": "catalog:"
Expand Down
8 changes: 8 additions & 0 deletions lockfile/settings-checker/src/getOutdatedLockfileSetting.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { type Catalogs } from '@pnpm/catalogs.types'
import { type LockfileObject, type PatchFile } from '@pnpm/lockfile.types'
import { allCatalogsAreUpToDate } from '@pnpm/lockfile.verification'
import { equals } from 'ramda'

export type ChangedField =
| 'catalogs'
| 'patchedDependencies'
| 'overrides'
| 'packageExtensionsChecksum'
Expand All @@ -15,6 +18,7 @@ export type ChangedField =
export function getOutdatedLockfileSetting (
lockfile: LockfileObject,
{
catalogs,
overrides,
packageExtensionsChecksum,
ignoredOptionalDependencies,
Expand All @@ -25,6 +29,7 @@ export function getOutdatedLockfileSetting (
pnpmfileChecksum,
injectWorkspacePackages,
}: {
catalogs?: Catalogs
overrides?: Record<string, string>
packageExtensionsChecksum?: string
patchedDependencies?: Record<string, PatchFile>
Expand All @@ -36,6 +41,9 @@ export function getOutdatedLockfileSetting (
injectWorkspacePackages?: boolean
}
): ChangedField | null {
if (!allCatalogsAreUpToDate(catalogs ?? {}, lockfile.catalogs)) {
return 'catalogs'
}
if (!equals(lockfile.overrides ?? {}, overrides ?? {})) {
return 'overrides'
}
Expand Down
6 changes: 6 additions & 0 deletions lockfile/settings-checker/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
{
"path": "../../__utils__/prepare"
},
{
"path": "../../catalogs/types"
},
{
"path": "../../config/parse-overrides"
},
Expand All @@ -20,6 +23,9 @@
},
{
"path": "../types"
},
{
"path": "../verification"
}
]
}
1 change: 1 addition & 0 deletions lockfile/verification/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { allProjectsAreUpToDate } from './allProjectsAreUpToDate.js'
export { allCatalogsAreUpToDate } from './allCatalogsAreUpToDate.js'
export { getWorkspacePackagesByDirectory } from './getWorkspacePackagesByDirectory.js'
export { localTarballDepsAreUpToDate } from './localTarballDepsAreUpToDate.js'
export { linkedPackagesAreUpToDate } from './linkedPackagesAreUpToDate.js'
Expand Down
1 change: 1 addition & 0 deletions pkg-manager/core/src/install/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ export async function mutateModules (
if (!opts.ignorePackageManifest) {
const outdatedLockfileSettingName = getOutdatedLockfileSetting(ctx.wantedLockfile, {
autoInstallPeers: opts.autoInstallPeers,
catalogs: opts.catalogs,
injectWorkspacePackages: opts.injectWorkspacePackages,
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
peersSuffixMaxLength: opts.peersSuffixMaxLength,
Expand Down
43 changes: 43 additions & 0 deletions pkg-manager/core/test/catalogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,49 @@ test('lockfile is updated if catalog config changes', async () => {
})
})

test('frozen lockfile error is thrown if catalog config changes', async () => {
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([
{
name: 'project1',
dependencies: {
'is-positive': 'catalog:',
},
},
])

await mutateModules(installProjects(projects), {
...options,
lockfileOnly: true,
catalogs: {
default: {
'is-positive': '=1.0.0',
},
},
})

expect(readLockfile().importers['project1' as ProjectId]).toEqual({
dependencies: {
'is-positive': {
specifier: 'catalog:',
version: '1.0.0',
},
},
})

const frozenLockfileMutation = mutateModules(installProjects(projects), {
...options,
lockfileOnly: true,
frozenLockfile: true,
catalogs: {
default: {
'is-positive': '=3.1.0',
},
},
})

await expect(frozenLockfileMutation).rejects.toThrow('Cannot proceed with the frozen installation. The current "catalogs" configuration doesn\'t match the value found in the lockfile')
})

test('lockfile catalog snapshots retain existing entries on --filter', async () => {
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([
{
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.