From baaedfbf7da98f0a5611bf28964d5ad0aa4e76fe Mon Sep 17 00:00:00 2001 From: chinesepowered Date: Thu, 23 Jul 2026 04:50:51 -0700 Subject: [PATCH] fix(core): jsonl write([]) should leave an empty file, not a stray newline (#7533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit write() built its content with join('\n') and then appended a trailing newline. For an empty array the join produces '', so the file ends up as a single '\n': one byte, no records. That makes the module's own accessors contradict each other — exists() tests size > 0 and returns true, while read() skips blank lines and returns []. Clearing a JSONL file therefore leaves something that reports as non-empty but has nothing in it. Terminate each record instead of joining with separators. The output for a non-empty array is byte-identical; an empty array now writes nothing. Co-authored-by: Shaojin Wen --- packages/core/src/utils/jsonl-utils.test.ts | 19 +++++++++++++++++++ packages/core/src/utils/jsonl-utils.ts | 7 +++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/core/src/utils/jsonl-utils.test.ts b/packages/core/src/utils/jsonl-utils.test.ts index ada232da14..257467fc3d 100644 --- a/packages/core/src/utils/jsonl-utils.test.ts +++ b/packages/core/src/utils/jsonl-utils.test.ts @@ -20,6 +20,7 @@ import { _recoverObjectsFromLine, _resetEnsuredDirsCacheForTest, countLines, + exists, parseLineTolerant, read, readLines, @@ -354,6 +355,24 @@ describe('writeLine / writeLineSync / write', () => { // branch is never exercised. A regression that dropped that branch would // make write() fail with ENOENT only when callers target a brand-new // subdirectory. + it('write() with an empty array leaves a genuinely empty file', async () => { + const file = path.join( + tmpRoot, + `we-${Math.random().toString(36).slice(2)}.jsonl`, + ); + await writeLine(file, { v: 1 }); + expect(exists(file)).toBe(true); + + // Clearing the file must not leave a stray newline behind: a 1-byte file + // makes exists() (size > 0) disagree with read() (no records). + write(file, []); + + expect(fs.readFileSync(file, 'utf8')).toBe(''); + expect(fs.statSync(file).size).toBe(0); + expect(await read(file)).toEqual([]); + expect(exists(file)).toBe(false); + }); + it('write() creates parent dirs when missing', () => { const nested = path.join(tmpRoot, 'a', 'b', 'c', 'file.jsonl'); write(nested, [{ x: 1 }]); diff --git a/packages/core/src/utils/jsonl-utils.ts b/packages/core/src/utils/jsonl-utils.ts index 1a749b747a..03203b9576 100644 --- a/packages/core/src/utils/jsonl-utils.ts +++ b/packages/core/src/utils/jsonl-utils.ts @@ -323,13 +323,16 @@ export function writeLineSync(filePath: string, data: unknown): void { * Each object will be written as a separate line. */ export function write(filePath: string, data: unknown[]): void { - const lines = data.map((item) => JSON.stringify(item)).join('\n'); + // Terminate each record rather than joining with separators: joining an + // empty array yields '' and the trailing newline then writes a 1-byte file + // that read() reports as empty but exists() reports as non-empty. + const lines = data.map((item) => `${JSON.stringify(item)}\n`).join(''); // Ensure directory exists before writing const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } - atomicWriteFileSync(filePath, `${lines}\n`, { encoding: 'utf8' }); + atomicWriteFileSync(filePath, lines, { encoding: 'utf8' }); } /**