{"version":3,"file":"writeFileIfChanged.cjs","names":["readFile","writeFile","stat","mkdir","basename","randomBytes","join","chmod","rename","dirname","rm"],"sources":["../../src/writeFileIfChanged.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { rmSync } from 'node:fs';\nimport {\n  chmod,\n  mkdir,\n  readFile,\n  rename,\n  rm,\n  stat,\n  writeFile,\n} from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\n\nconst activeTempFiles = new Set<string>();\n\n// Synchronous cleanup on process exit\nprocess.on('exit', () => {\n  for (const file of activeTempFiles) {\n    try {\n      rmSync(file, { force: true });\n    } catch {}\n  }\n});\n\n/**\n * Permission bits a freshly created file receives on this system. Detected once\n * from the first temp file we actually write (`process.umask()` with no\n * argument is deprecated and thread-unsafe) and cached, so the atomic path can\n * skip restoring the mode whenever the source file already uses the default\n * mode — which is the case for essentially every generated file.\n */\nlet defaultFileMode: number | undefined;\n\n/**\n * Options for {@link writeFileIfChanged}.\n */\nexport type WriteFileIfChangedOptions = {\n  /** Encoding used to turn `data` into bytes. Defaults to `'utf8'`. */\n  encoding?: BufferEncoding;\n  /** Directory to hold the temporary file used for the atomic swap. */\n  tempDir?: string;\n  /**\n   * Write atomically via a temp file + `rename` so readers never observe a\n   * half-written file. This creates a fresh inode on every write, which roughly\n   * doubles the cost of a changed write compared to overwriting in place. Set to\n   * `false` for callers whose consumers tolerate torn reads to write directly.\n   * Defaults to `true`.\n   */\n  atomic?: boolean;\n};\n\n/**\n * Write `data` to `path` only when it differs from the file already on disk,\n * preserving the existing file's permission mode.\n *\n * The whole file is read back and byte-compared because the files written here\n * (dictionaries, entry points, types) are small, so reading beats streaming a\n * hash. When the content is unchanged the write is skipped entirely, which both\n * avoids inode churn and prevents downstream watchers from rebuilding.\n *\n * @returns `true` when the file was written, `false` when it was already up to date.\n */\nexport const writeFileIfChanged = async (\n  path: string,\n  data: string,\n  { encoding = 'utf8', tempDir, atomic = true }: WriteFileIfChangedOptions = {}\n): Promise<boolean> => {\n  const newData = Buffer.from(data, encoding);\n\n  // Read the current content first: the common case is an unchanged file, and\n  // this single read is the whole fast path (no stat/chmod needed to bail out).\n  let existingData: Buffer | null = null;\n  try {\n    existingData = await readFile(path);\n  } catch {}\n\n  if (existingData?.equals(newData)) {\n    return false;\n  }\n\n  // Fast path: overwrite in place. Truncating an existing file keeps its inode,\n  // so its permission mode is preserved automatically (no chmod needed). This\n  // skips the temp-file inode churn at the cost of exposing readers to a\n  // partially written file.\n  if (!atomic) {\n    await writeFile(path, newData);\n    return true;\n  }\n\n  // The atomic swap replaces the inode, so the existing mode is not preserved\n  // and must be reapplied. Only a file that already exists has a mode to keep.\n  let modeToRestore: number | undefined;\n  if (existingData !== null) {\n    try {\n      modeToRestore = (await stat(path)).mode & 0o777;\n    } catch {}\n  }\n\n  if (tempDir) {\n    await mkdir(tempDir, { recursive: true });\n  }\n\n  const tempFileName = `${basename(path)}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`;\n  const tempPath = tempDir\n    ? join(tempDir, tempFileName)\n    : `${path}.${tempFileName}`;\n  activeTempFiles.add(tempPath);\n\n  try {\n    for (let attempt = 0; ; attempt++) {\n      try {\n        await writeFile(tempPath, newData);\n\n        if (modeToRestore !== undefined) {\n          // Learn the ambient default mode once from a real temp file, then only\n          // chmod when the source file used a non-default mode.\n          if (defaultFileMode === undefined) {\n            try {\n              defaultFileMode = (await stat(tempPath)).mode & 0o777;\n            } catch {}\n          }\n          if (modeToRestore !== defaultFileMode) {\n            await chmod(tempPath, modeToRestore);\n          }\n        }\n\n        await rename(tempPath, path);\n        break;\n      } catch (error) {\n        const isMissingDirectory =\n          (error as NodeJS.ErrnoException).code === 'ENOENT';\n\n        if (!isMissingDirectory || attempt > 0) throw error;\n\n        await mkdir(dirname(path), { recursive: true });\n        if (tempDir) await mkdir(tempDir, { recursive: true });\n      }\n    }\n  } catch (error) {\n    try {\n      await rm(tempPath, { force: true });\n    } catch {}\n    throw error;\n  } finally {\n    activeTempFiles.delete(tempPath);\n  }\n\n  return true;\n};\n"],"mappings":";;;;;;;AAaA,MAAM,kCAAkB,IAAI,IAAY;AAGxC,QAAQ,GAAG,cAAc;CACvB,KAAK,MAAM,QAAQ,iBACjB,IAAI;EACF,oBAAO,MAAM,EAAE,OAAO,KAAK,CAAC;CAC9B,QAAQ,CAAC;AAEb,CAAC;;;;;;;;AASD,IAAI;;;;;;;;;;;;AA+BJ,MAAa,qBAAqB,OAChC,MACA,MACA,EAAE,WAAW,QAAQ,SAAS,SAAS,SAAoC,CAAC,MACvD;CACrB,MAAM,UAAU,OAAO,KAAK,MAAM,QAAQ;CAI1C,IAAI,eAA8B;CAClC,IAAI;EACF,eAAe,UAAMA,2BAAS,IAAI;CACpC,QAAQ,CAAC;CAET,IAAI,cAAc,OAAO,OAAO,GAC9B,OAAO;CAOT,IAAI,CAAC,QAAQ;EACX,UAAMC,4BAAU,MAAM,OAAO;EAC7B,OAAO;CACT;CAIA,IAAI;CACJ,IAAI,iBAAiB,MACnB,IAAI;EACF,iBAAiB,UAAMC,uBAAK,IAAI,EAAC,CAAE,OAAO;CAC5C,QAAQ,CAAC;CAGX,IAAI,SACF,UAAMC,wBAAM,SAAS,EAAE,WAAW,KAAK,CAAC;CAG1C,MAAM,eAAe,OAAGC,oBAAS,IAAI,EAAE,GAAG,KAAK,IAAI,EAAE,OAAGC,yBAAY,CAAC,CAAC,CAAC,SAAS,KAAK,EAAE;CACvF,MAAM,WAAW,cACbC,gBAAK,SAAS,YAAY,IAC1B,GAAG,KAAK,GAAG;CACf,gBAAgB,IAAI,QAAQ;CAE5B,IAAI;EACF,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;GACF,UAAML,4BAAU,UAAU,OAAO;GAEjC,IAAI,kBAAkB,QAAW;IAG/B,IAAI,oBAAoB,QACtB,IAAI;KACF,mBAAmB,UAAMC,uBAAK,QAAQ,EAAC,CAAE,OAAO;IAClD,QAAQ,CAAC;IAEX,IAAI,kBAAkB,iBACpB,UAAMK,wBAAM,UAAU,aAAa;GAEvC;GAEA,UAAMC,yBAAO,UAAU,IAAI;GAC3B;EACF,SAAS,OAAO;GAId,IAAI,EAFD,MAAgC,SAAS,aAEjB,UAAU,GAAG,MAAM;GAE9C,UAAML,4BAAMM,mBAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;GAC9C,IAAI,SAAS,UAAMN,wBAAM,SAAS,EAAE,WAAW,KAAK,CAAC;EACvD;CAEJ,SAAS,OAAO;EACd,IAAI;GACF,UAAMO,qBAAG,UAAU,EAAE,OAAO,KAAK,CAAC;EACpC,QAAQ,CAAC;EACT,MAAM;CACR,UAAU;EACR,gBAAgB,OAAO,QAAQ;CACjC;CAEA,OAAO;AACT"}