Repository navigation
Description
Description
Immediately after rs fmt formats and writes a file:
rs fmt --checkexits with code 0 and reports that formatting is correct.rs fmt --check --no-cacheexits with code 1 and reports a formatting issue for exactly the same file.- Running cached
rs fmtagain leaves the file unchanged. - Running formatting again without cache changes the file.
This reproduces with a fresh cache directory and does not require an existing project or CI environment.
Environment
- Linux x64
- Node.js 24.19.0
- pnpm 12.3.4
- Rstack 0.7.3
- Prettier 3.9.6 (Rstack dependency)
- yuku-parser 0.9.3 (Rstack dependency)
Minimal reproduction
Create the following three files in an empty directory.
package.json
{
"private": true,
"type": "module",
"devDependencies": {
"rstack": "0.7.3"
}
}rstack.config.mjs
import { define } from 'rstack';
define.fmt({
singleQuote: true,
trailingComma: 'all',
});example.ts
Please preserve the exact formatting of this input:
const fetch = rs.fn<typeof globalThis.fetch>().mockImplementation(() => Promise.resolve(
new Response('cached pixels', { headers: { 'content-type': 'image/webp' } }),
));Here, rs is only an identifier in the formatting input. The code is never executed, so no test framework is required.
Install dependencies:
pnpm installStarting with a nonexistent repro-cache directory, run each command separately, in order:
pnpm exec rs fmt --cache-location ./repro-cache example.ts
# Exits with 0 and writes the first formatted output, B.
pnpm exec rs fmt --check --cache-location ./repro-cache example.ts
# Exits with 0: Format check passed.
pnpm exec rs fmt --check --no-cache example.ts
# Exits with 1: Formatting issues found in 1 file.
pnpm exec rs fmt --cache-location ./repro-cache example.ts
# Exits with 0, but output B remains unchanged.
pnpm exec rs fmt --no-cache example.ts
# Exits with 0 and changes B into a different output, C.
pnpm exec rs fmt --check --no-cache example.ts
# Exits with 0: Format check passed.Expected behavior
Cached and uncached checks should produce consistent results for identical file contents, configuration, and tool versions.
Formatting should be idempotent: formatting its output again should not change it.
Additional investigation
Two observable behaviors combine to produce this result:
- For this input, formatting the first output B again produces a different output C.
- Rstack caches the first written output B as clean. Subsequent cache hits skip formatting, so they do not detect that B would still be changed by another formatting pass.
In the installed Rstack 0.7.3 package, formatFile in dist/335.js stores the hash of result.formatted after writing and classifies the cache entry using:
shouldWrite || unchanged ? 'clean' : 'dirty'As a result, a written output is marked clean.
Separately, directly calling Prettier 3.9.6's format() with parser: 'typescript', singleQuote: true, and trailingComma: 'all' also produces the same B and C.
Therefore, the non-idempotent formatting is not specific to Rstack or its Yuku integration. This report concerns how caching the first written output as clean masks that behavior, causing cached and uncached checks to disagree. There is no evidence that this is a cache-hash collision.
Activity
elecmonkey commentedon Sep 13, 2026
The root cause appears to be that Prettier does not guarantee idempotent output. For this particular input, Prettier is not idempotent: format(A) = B, but format(B) = C. Rstack CLI caches B as clean immediately after writing it, so the subsequent cached check skips formatting even though B would change again.
Prettier still seems to have quite a few open issues around idempotency, some of which have remained unresolved for years. So while this behavior is not caused by Rstack CLI itself, we should be careful about whether it is reasonable for Rstack CLI to assume that Prettier is always idempotent. The tricky part, though, is that any approach that breaks this assumption would come with a performance cost.
elecmonkey commentedon Sep 13, 2026
One thing to add: the same code also takes two formatting passes with Biome, while Oxfmt gets it right in a single pass.