Update a package.json (or the prepack save file) in place, without ever leaving it
at zero length.
Fs.writeFile opens the target with O_TRUNC and then writes. A process killed
between those two steps leaves a zero byte manifest, and because nothing fails
loudly the run can still exit 0 (FPM-66: a postinstall child killed during test
teardown emptied a tracked package.json).
The fix cannot be temp-file + rename: fyn hardlinks package files into node_modules,
and rename swaps the directory entry, so the installed copies would silently keep the
old content on the old inode. Instead this opens "r+" (no O_TRUNC), writes the new
bytes first, and only then truncates to the new length. Shorter replacements are
padded with JSON whitespace in the same write, so readers between the write and
truncate do not see old trailing bytes. The inode - and therefore every hardlink
to it - is preserved, and the file never passes through empty.
The write is skipped entirely when the content already matches, which is the common
case for postinstall - it used to rewrite the manifest byte for byte on every install.
Parameters
file: string
path to update
content: string|Buffer<ArrayBufferLike>
new content
Returns Promise<boolean>
true if the file was written, false if it already had this content
Update a package.json (or the prepack save file) in place, without ever leaving it at zero length.
Fs.writeFileopens the target with O_TRUNC and then writes. A process killed between those two steps leaves a zero byte manifest, and because nothing fails loudly the run can still exit 0 (FPM-66: a postinstall child killed during test teardown emptied a tracked package.json).The fix cannot be temp-file + rename: fyn hardlinks package files into node_modules, and rename swaps the directory entry, so the installed copies would silently keep the old content on the old inode. Instead this opens "r+" (no O_TRUNC), writes the new bytes first, and only then truncates to the new length. Shorter replacements are padded with JSON whitespace in the same write, so readers between the write and truncate do not see old trailing bytes. The inode - and therefore every hardlink to it - is preserved, and the file never passes through empty.
The write is skipped entirely when the content already matches, which is the common case for postinstall - it used to rewrite the manifest byte for byte on every install.