Skip to content

Commit 238419a

Browse files
fix: atomic-write generated CSS and dts files
Metro spawns multiple worker processes that each transform `components/web/metro-injected.js` and the CSS entry file. Both transforms call `buildCSS`, which writes `node_modules/uniwind/uniwind.css` via `fs.writeFileSync`. That call truncates the file before writing, so when one worker is mid-write another worker can observe a partial file as Tailwind compiles `@import 'uniwind'` from `generateCSSForThemes`/`compileVirtual`. The partial read fails to parse and surfaces as cryptic errors like: SyntaxError: .../metro-injected.js: Missing closing } at &:not(:where(.light, .light *, .dark, .dark *, ...)) This is the long-standing flake in #341 and #427: it reproduces on Linux CI (concurrent workers, low-overhead scheduling) and is effectively invisible on macOS. Enabling `EXPO_UNSTABLE_TREE_SHAKING=1` widens the race window by adding transforms and surfaces it consistently. Route both `buildCSS` and `buildDtsFile` through a new `atomicWriteFileSync` helper that stages to a unique temp file in the same directory and renames onto the target. POSIX `rename` is atomic and Windows' `MoveFileEx` (used by Node's `fs.renameSync`) does atomic replace, so concurrent readers always see either the previous file or the complete new file. Refs: #341
1 parent a2406b8 commit 238419a

3 files changed

Lines changed: 26 additions & 5 deletions

File tree

packages/uniwind/src/css/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'fs'
22
import path from 'path'
3+
import { atomicWriteFileSync } from '../utils/atomicWriteFileSync'
34
import { EXTRA_UTILITIES_CSS } from './extraUtilities'
45
import { INSETS_CSS } from './insets'
56
import { OVERWRITE_CSS } from './overwrite'
@@ -27,8 +28,5 @@ export const buildCSS = async (themes: Array<string>, input: string) => {
2728
return
2829
}
2930

30-
fs.writeFileSync(
31-
cssFilePath,
32-
newCssFile,
33-
)
31+
atomicWriteFileSync(cssFilePath, newCssFile)
3432
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import crypto from 'crypto'
2+
import fs from 'fs'
3+
4+
/**
5+
* Write a file by staging to a temp path in the same directory and renaming
6+
* onto the target. `rename` is atomic on POSIX and Windows, so concurrent
7+
* readers always observe either the previous file or the complete new file —
8+
* never a partial write. See https://github.com/uni-stack/uniwind/issues/341.
9+
*/
10+
export const atomicWriteFileSync = (filePath: string, content: string) => {
11+
const tmpPath = `${filePath}.${crypto.randomUUID()}.tmp`
12+
fs.writeFileSync(tmpPath, content)
13+
try {
14+
fs.renameSync(tmpPath, filePath)
15+
} catch (err) {
16+
// best-effort cleanup
17+
try {
18+
fs.unlinkSync(tmpPath)
19+
} catch {}
20+
throw err
21+
}
22+
}

packages/uniwind/src/utils/buildDtsFile.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'fs'
22
import { name } from '../../package.json'
3+
import { atomicWriteFileSync } from './atomicWriteFileSync'
34

45
export const buildDtsFile = (dtsPath: string, stringifiedThemes: string) => {
56
const oldDtsContent = fs.existsSync(dtsPath)
@@ -23,5 +24,5 @@ export const buildDtsFile = (dtsPath: string, stringifiedThemes: string) => {
2324
return
2425
}
2526

26-
fs.writeFileSync(dtsPath, dtsContent)
27+
atomicWriteFileSync(dtsPath, dtsContent)
2728
}

0 commit comments

Comments
 (0)