mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-29 18:35:18 -04:00
fix(lockfile): wait for stream close before returning (#12408)
This commit is contained in:
6
.changeset/slow-windows-lockfile-streams.md
Normal file
6
.changeset/slow-windows-lockfile-streams.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/lockfile.fs": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Wait for early-exited lockfile read streams to close before rewriting lockfiles.
|
||||
@@ -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.
|
||||
|
||||
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user