pnpm/pnpm#15550 made the workspace-state write non-fatal and gave pacquet's rename the Windows lock retry. pnpm v11 still wrote the file through write-file-atomic, whose rename does not retry, so a concurrent holder of .pnpm-workspace-state-v1.json on Windows still lost the write. v11 now writes a path-temp file beside the state file and renames it with renameFileWithRetryAsync, as the lockfile writer does. The temp file is removed when the write fails. write-file-atomic's fsync and same-path serialization go with it, matching pacquet's writer. pacquet gains a Windows test that holds the state file open and asserts the write waits the lock out, and checks that no temp file is left beside the state file. Refs pnpm/pnpm#14550 --------- Co-authored-by: Zoltan Kochan <z@kochan.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
52 lines
1.7 KiB
TypeScript
52 lines
1.7 KiB
TypeScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
import { expect, test } from '@jest/globals'
|
|
import { prepareEmpty } from '@pnpm/prepare'
|
|
|
|
import { getFilePath } from '../src/filePath.js'
|
|
import { updateWorkspaceState, type UpdateWorkspaceStateOptions } from '../src/index.js'
|
|
|
|
test('updateWorkspaceState() leaves no temp file beside the state file', async () => {
|
|
prepareEmpty()
|
|
const workspaceDir = process.cwd()
|
|
|
|
await updateWorkspaceState(options(workspaceDir))
|
|
await updateWorkspaceState(options(workspaceDir))
|
|
|
|
expect(fs.existsSync(getFilePath(workspaceDir))).toBe(true)
|
|
expect(cacheDirEntries(workspaceDir)).toStrictEqual([path.basename(getFilePath(workspaceDir))])
|
|
})
|
|
|
|
// A rename onto a directory fails outright on Unix. On Windows the same setup
|
|
// reports EPERM, which is retried, so the test would spend the retry budget.
|
|
const testOnUnix = process.platform === 'win32' ? test.skip : test
|
|
|
|
testOnUnix('updateWorkspaceState() leaves no temp file when the write cannot land', async () => {
|
|
prepareEmpty()
|
|
const workspaceDir = process.cwd()
|
|
const cacheFile = getFilePath(workspaceDir)
|
|
fs.mkdirSync(cacheFile, { recursive: true })
|
|
|
|
await expect(updateWorkspaceState(options(workspaceDir))).resolves.toBeUndefined()
|
|
expect(cacheDirEntries(workspaceDir)).toStrictEqual([path.basename(cacheFile)])
|
|
})
|
|
|
|
function options (workspaceDir: string): UpdateWorkspaceStateOptions {
|
|
return {
|
|
allProjects: [],
|
|
settings: {
|
|
excludeLinksFromLockfile: false,
|
|
linkWorkspacePackages: false,
|
|
preferWorkspacePackages: false,
|
|
},
|
|
workspaceDir,
|
|
pnpmfiles: [],
|
|
filteredInstall: false,
|
|
}
|
|
}
|
|
|
|
function cacheDirEntries (workspaceDir: string): string[] {
|
|
return fs.readdirSync(path.dirname(getFilePath(workspaceDir))).sort()
|
|
}
|