fix(lockfile): support CRLF line endings in env lockfiles (#11654)

* fix(lockfile): support CRLF line endings in env lockfiles

Normalize CRLF line endings before parsing YAML document
separators in streamed env lockfile reads.

Previously the parser assumed LF-only separators (`\n---\n`),
which caused pnpm to report ERR_PNPM_BROKEN_LOCKFILE or outdated
lockfile errors when configDependencies lockfiles were checked
out with CRLF line endings on Windows.

Fixes #11612

* test(lockfile): cover CRLF normalization and clean up yamlDocuments

Add CRLF-handling tests for streamReadFirstYamlDocument (CRLF and
BOM+CRLF) and extractMainDocument (CRLF in combined file and CRLF
in content without separator). Hoist the duplicated CRLF replace
in Phase 1 out of the if/else, drop two stray semicolons and a
couple of blank lines.

* chore: include pnpm in changeset

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
Dipan Chakraborty
2026-05-15 13:45:42 +05:30
committed by GitHub
parent b6e2c8c5ac
commit 6e93f350a9
3 changed files with 41 additions and 1 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/lockfile.fs": patch
"pnpm": patch
---
Fix lockfile parsing failures when `pnpm-lock.yaml` contains CRLF line endings and multiple YAML documents [#11612](https://github.com/pnpm/pnpm/issues/11612).

View File

@@ -27,6 +27,8 @@ export async function streamReadFirstYamlDocument (filePath: string): Promise<st
} else {
buffer += chunk.value
}
// Normalize CRLF (Windows) to LF so document separator detection works.
buffer = buffer.replace(/\r\n/g, '\n')
if (buffer.length >= YAML_DOCUMENT_START.length) break
}
if (!buffer.startsWith(YAML_DOCUMENT_START)) {
@@ -42,7 +44,8 @@ export async function streamReadFirstYamlDocument (filePath: string): Promise<st
}
const chunk = await chunks.next() // eslint-disable-line no-await-in-loop
if (chunk.done) break
buffer += chunk.value
// Normalize CRLF (Windows) to LF so the separator search matches on Windows-checked-out files.
buffer = (buffer + chunk.value).replace(/\r\n/g, '\n')
}
} catch (err: unknown) {
if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
@@ -61,6 +64,7 @@ export async function streamReadFirstYamlDocument (filePath: string): Promise<st
* Otherwise returns the entire content (no env document present).
*/
export function extractMainDocument (content: string): string {
content = content.replace(/\r\n/g, '\n')
if (!content.startsWith(YAML_DOCUMENT_START)) return content
const sep = content.indexOf(YAML_DOCUMENT_SEPARATOR, YAML_DOCUMENT_START.length)
if (sep === -1) return ''

View File

@@ -64,6 +64,25 @@ describe('streamReadFirstYamlDocument', () => {
const result = await streamReadFirstYamlDocument(filePath)
expect(result).toBe(envContent)
})
test('handles CRLF line endings (Windows)', async () => {
const dir = temporaryDirectory()
const filePath = path.join(dir, 'test.yaml')
const envContent = 'lockfileVersion: env-1.0\nimporters:\n .:\n foo: bar'
const content = `---\n${envContent}\n---\nlockfileVersion: 9.0\n`.replace(/\n/g, '\r\n')
fs.writeFileSync(filePath, content)
const result = await streamReadFirstYamlDocument(filePath)
expect(result).toBe(envContent)
})
test('handles BOM with CRLF line endings', async () => {
const dir = temporaryDirectory()
const filePath = path.join(dir, 'test.yaml')
const content = '---\r\nfoo: bar\r\n---\r\nlockfileVersion: 9.0\r\n'
fs.writeFileSync(filePath, content)
const result = await streamReadFirstYamlDocument(filePath)
expect(result).toBe('foo: bar')
})
})
describe('extractMainDocument', () => {
@@ -82,4 +101,15 @@ describe('extractMainDocument', () => {
const combined = `---\nfoo: bar\n---\n${mainContent}`
expect(extractMainDocument(combined)).toBe(mainContent)
})
test('handles CRLF line endings in combined file', () => {
const mainContent = 'lockfileVersion: 9.0\npackages: {}\n'
const combined = `---\nfoo: bar\n---\n${mainContent}`.replace(/\n/g, '\r\n')
expect(extractMainDocument(combined)).toBe(mainContent)
})
test('normalizes CRLF to LF for content without document separator', () => {
const content = 'lockfileVersion: 9.0\r\npackages: {}\r\n'
expect(extractMainDocument(content)).toBe('lockfileVersion: 9.0\npackages: {}\n')
})
})