1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";
4import { resolveSymlinkTarget } from "@t3tools/shared/symlink";
5
6/**
7 * Replaces a file's contents via a sibling temp file and rename. A symlinked
8 * target is resolved first so the link survives and its destination is
9 * rewritten, since renaming over the link itself would swap it for a regular file.
10 */
11export const writeFileStringAtomically = (input: {
12 readonly filePath: string;
13 readonly contents: string;
14}) =>
15 Effect.scoped(
16 Effect.gen(function* () {
17 const fs = yield* FileSystem.FileSystem;
18 const path = yield* Path.Path;
19 const targetPath = yield* resolveSymlinkTarget(input.filePath);
20 const targetDirectory = path.dirname(targetPath);
21
22 yield* fs.makeDirectory(targetDirectory, { recursive: true });
23 // The temp directory is cleanup, not part of the write: failing to remove
24 // it (a virus scanner holding it on Windows) must not fail a write that
25 // already landed.
26 const tempDirectory = yield* Effect.acquireRelease(
27 fs.makeTempDirectory({
28 directory: targetDirectory,
29 prefix: `${path.basename(targetPath)}.`,
30 }),
31 (directory) => fs.remove(directory, { recursive: true }).pipe(Effect.ignore({ log: true })),
32 );
33 const tempPath = path.join(tempDirectory, "contents.tmp");
34
35 yield* fs.writeFileString(tempPath, input.contents);
36 yield* fs.rename(tempPath, targetPath);
37 }),
38 );