@@ -3,8 +3,8 @@ import type { RuntimeRPC } from '../../types/rpc'
3
3
import type { TestProject } from '../project'
4
4
import type { ResolveSnapshotPathHandlerContext } from '../types/config'
5
5
import { mkdirSync } from 'node:fs'
6
- import { writeFile } from 'node:fs/promises'
7
- import { join } from 'pathe'
6
+ import { rename , stat , unlink , writeFile } from 'node:fs/promises'
7
+ import { dirname , join } from 'pathe'
8
8
import { hash } from '../hash'
9
9
10
10
const created = new Set ( )
@@ -65,7 +65,11 @@ export function createMethodsRPC(project: TestProject, options: MethodsOptions =
65
65
}
66
66
promises . set (
67
67
tmp ,
68
- writeFile ( tmp , code , 'utf-8' ) . finally ( ( ) => promises . delete ( tmp ) ) ,
68
+
69
+ atomicWriteFile ( tmp , code )
70
+ // Fallback to non-atomic write for windows case where file already exists:
71
+ . catch ( ( ) => writeFile ( tmp , code , 'utf-8' ) )
72
+ . finally ( ( ) => promises . delete ( tmp ) ) ,
69
73
)
70
74
await promises . get ( tmp )
71
75
Object . assign ( result , { id : tmp } )
@@ -146,3 +150,35 @@ function handleRollupError(e: unknown): never {
146
150
}
147
151
throw e
148
152
}
153
+
154
+ /**
155
+ * Performs an atomic write operation using the write-then-rename pattern.
156
+ *
157
+ * Why we need this:
158
+ * - Ensures file integrity by never leaving partially written files on disk
159
+ * - Prevents other processes from reading incomplete data during writes
160
+ * - Particularly important for test files where incomplete writes could cause test failures
161
+ *
162
+ * The implementation writes to a temporary file first, then renames it to the target path.
163
+ * This rename operation is atomic on most filesystems (including POSIX-compliant ones),
164
+ * guaranteeing that other processes will only ever see the complete file.
165
+ *
166
+ * Added in https://github.com/vitest-dev/vitest/pull/7531
167
+ */
168
+ async function atomicWriteFile ( realFilePath : string , data : string ) : Promise < void > {
169
+ const dir = dirname ( realFilePath )
170
+ const tmpFilePath = join ( dir , `.tmp-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` )
171
+
172
+ try {
173
+ await writeFile ( tmpFilePath , data , 'utf-8' )
174
+ await rename ( tmpFilePath , realFilePath )
175
+ }
176
+ finally {
177
+ try {
178
+ if ( await stat ( tmpFilePath ) ) {
179
+ await unlink ( tmpFilePath )
180
+ }
181
+ }
182
+ catch { }
183
+ }
184
+ }
0 commit comments