fix(lockfile): wait for stream close before returning (#12408)

This commit is contained in:
Zoltan Kochan
2026-06-14 23:37:22 +02:00
committed by GitHub
parent 18cba025ed
commit d50d691e5a
3 changed files with 38 additions and 5 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/lockfile.fs": patch
"pnpm": patch
---
Wait for early-exited lockfile read streams to close before rewriting lockfiles.

View File

@@ -1,4 +1,4 @@
import { createReadStream } from 'node:fs'
import { createReadStream, type ReadStream } from 'node:fs'
import util from 'node:util'
import stripBom from 'strip-bom'
@@ -32,31 +32,45 @@ export async function streamReadFirstYamlDocument (filePath: string): Promise<st
if (buffer.length >= YAML_DOCUMENT_START.length) break
}
if (!buffer.startsWith(YAML_DOCUMENT_START)) {
stream.destroy()
await closeStream(stream)
return null
}
// Phase 2: find the second "---" separator
let firstDocument: string | undefined
while (true) {
const sep = buffer.indexOf(YAML_DOCUMENT_SEPARATOR, YAML_DOCUMENT_START.length)
if (sep !== -1) {
stream.destroy()
return buffer.slice(YAML_DOCUMENT_START.length, sep)
firstDocument = buffer.slice(YAML_DOCUMENT_START.length, sep)
break
}
const chunk = await chunks.next() // eslint-disable-line no-await-in-loop
if (chunk.done) break
// Normalize CRLF (Windows) to LF so the separator search matches on Windows-checked-out files.
buffer = (buffer + chunk.value).replace(/\r\n/g, '\n')
}
if (firstDocument != null) {
await closeStream(stream)
return firstDocument
}
} catch (err: unknown) {
await closeStream(stream)
if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
return null
}
throw err
}
stream.destroy()
await closeStream(stream)
return null
}
async function closeStream (stream: ReadStream): Promise<void> {
if (stream.closed) return
await new Promise<void>((resolve) => {
stream.once('close', resolve)
stream.destroy()
})
}
/**
* Extracts the main lockfile content (second YAML document) from a combined string.
* If the file starts with "---\n", returns the content after the separator.

View File

@@ -26,6 +26,19 @@ describe('streamReadFirstYamlDocument', () => {
expect(result).toBeNull()
})
test('closes a non-env lockfile before returning null', async () => {
const dir = temporaryDirectory()
const filePath = path.join(dir, 'test.yaml')
const tempFilePath = `${filePath}.tmp`
fs.writeFileSync(filePath, 'lockfileVersion: 9.0\n')
fs.writeFileSync(tempFilePath, 'lockfileVersion: 9.0\nimporters: {}\n')
const result = await streamReadFirstYamlDocument(filePath)
expect(result).toBeNull()
fs.renameSync(tempFilePath, filePath)
})
test('returns null for a non-existent file', async () => {
const dir = temporaryDirectory()
const result = await streamReadFirstYamlDocument(path.join(dir, 'nonexistent.yaml'))