Skip to content

Commit fabe1b1

Browse files
authored
feat(fmt): cache write results (#227)
1 parent e473259 commit fabe1b1

5 files changed

Lines changed: 76 additions & 22 deletions

File tree

packages/rstack/src/fmt/runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ const runFmtFiles = async ({
183183
}: RunFmtFilesOptions): Promise<FmtRunResult> => {
184184
const shouldWrite = mode === 'write';
185185
let runCache: RunCache | undefined;
186-
if (files.length > 0 && cache && !shouldWrite) {
186+
if (files.length > 0 && cache) {
187187
runCache = {
188188
store: await loadFmtCacheStore(cache.filePath, cacheNamespace),
189189
resolveKey: createCacheKeyResolver(cache.rootPath),

packages/rstack/src/fmt/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ interface RunFmtFilesOptions {
9999
mode: FmtMode;
100100
/** Maximum number of formatting workers. */
101101
maxWorkers?: number;
102-
/** Internal persistent cache context. Currently used only by check and list modes. */
102+
/** Internal persistent cache context. */
103103
cache?: FmtCacheContext;
104104
}
105105

packages/rstack/src/fmt/worker.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface FormatFileTask {
1111
cache?: FmtFileCache;
1212
}
1313

14-
const hashContent = (content: Uint8Array): string =>
14+
const hashContent = (content: string | Uint8Array): string =>
1515
createHash('sha256').update(content).digest('hex');
1616

1717
/**
@@ -25,10 +25,9 @@ const formatFile = async ({
2525
}: FormatFileTask): Promise<FmtWorkerResult> => {
2626
let source: string | undefined;
2727
let contentHash: string | undefined;
28-
const fileCache = shouldWrite ? undefined : cache;
2928

30-
const readSource = (): string => {
31-
if (!fileCache) {
29+
const readSource = (shouldHash = !shouldWrite): string => {
30+
if (!cache || !shouldHash) {
3231
return readFileSync(file.path, 'utf8');
3332
}
3433

@@ -37,10 +36,10 @@ const formatFile = async ({
3736
return content.toString('utf8');
3837
};
3938

40-
if (fileCache?.entry && fileCache.entry[1] === fileCache.optionsHash) {
41-
source = readSource();
42-
const { entry } = fileCache;
43-
if (entry[0] === contentHash) {
39+
if (cache?.entry && cache.entry[1] === cache.optionsHash) {
40+
source = readSource(true);
41+
const { entry } = cache;
42+
if (entry[0] === contentHash && (!shouldWrite || entry[2] === 'clean')) {
4443
return { status: entry[2] === 'clean' ? 'unchanged' : 'changed' };
4544
}
4645
}
@@ -58,14 +57,18 @@ const formatFile = async ({
5857
}
5958

6059
const status = unchanged ? 'unchanged' : 'changed';
61-
if (!fileCache || contentHash === undefined) {
60+
if (!cache) {
6261
return { status };
6362
}
6463

64+
const cacheHash =
65+
shouldWrite && !unchanged
66+
? hashContent(result.formatted)
67+
: (contentHash ?? hashContent(result.source));
6568
const cacheEntry: FmtCacheEntry = [
66-
contentHash,
67-
fileCache.optionsHash,
68-
unchanged ? 'clean' : 'dirty',
69+
cacheHash,
70+
cache.optionsHash,
71+
shouldWrite || unchanged ? 'clean' : 'dirty',
6972
];
7073
return { status, cacheEntry };
7174
};

packages/rstack/tests/fmt/runnerCache.test.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, readFileSync, statSync, utimesSync, writeFileSync } from 'node:fs';
1+
import { readFileSync, statSync, utimesSync, writeFileSync } from 'node:fs';
22
import path from 'node:path';
33
import { expect, test } from 'rstack/test';
44
import { cacheNamespace, createOptionsHasher, sha256 } from '../../src/fmt/cacheIdentity.ts';
@@ -160,17 +160,67 @@ test('does not cache formatting errors', async () => {
160160
});
161161
});
162162

163-
test('does not apply the cache in write mode yet', async () => {
163+
test('write persists clean results for misses and hits', async () => {
164+
await withTempProject(async (rootPath) => {
165+
const cleanPath = path.join(rootPath, 'clean.ts');
166+
const dirtyPath = path.join(rootPath, 'dirty.ts');
167+
const cache = createCache(rootPath);
168+
writeFileSync(cleanPath, 'const clean = 1;\n');
169+
writeFileSync(dirtyPath, 'const dirty=1');
170+
171+
const files = [createRequest(cleanPath), createRequest(dirtyPath)];
172+
await expect(run(files, 'write', cache)).resolves.toMatchObject({
173+
exitCode: 0,
174+
files: [{ path: dirtyPath, status: 'written' }],
175+
processedFileCount: 2,
176+
});
177+
178+
const store = await loadFmtCacheStore(cache.filePath, cacheNamespace);
179+
expect(store.get('clean.ts')).toEqual([
180+
sha256(readFileSync(cleanPath)),
181+
expect.any(String),
182+
'clean',
183+
]);
184+
expect(store.get('dirty.ts')).toEqual([
185+
sha256(readFileSync(dirtyPath)),
186+
expect.any(String),
187+
'clean',
188+
]);
189+
190+
const timestamps = files.map((file) => statSync(file.path).mtimeMs);
191+
await expect(run(files, 'write', cache)).resolves.toMatchObject({
192+
exitCode: 0,
193+
files: [],
194+
processedFileCount: 2,
195+
});
196+
expect(files.map((file) => statSync(file.path).mtimeMs)).toEqual(timestamps);
197+
});
198+
});
199+
200+
test('write converts a dirty entry to clean', async () => {
164201
await withTempProject(async (rootPath) => {
165202
const filePath = path.join(rootPath, 'index.ts');
166203
const cache = createCache(rootPath);
204+
const file = createRequest(filePath);
167205
writeFileSync(filePath, 'const value=1');
168206

169-
await expect(run([createRequest(filePath)], 'write', cache)).resolves.toMatchObject({
207+
await run([file], 'check', cache);
208+
209+
await expect(run([file], 'write', cache)).resolves.toMatchObject({
170210
exitCode: 0,
171211
files: [{ path: filePath, status: 'written' }],
172212
});
173213
expect(readFileSync(filePath, 'utf8')).toBe('const value = 1;\n');
174-
expect(existsSync(cache.filePath)).toBe(false);
214+
215+
const store = await loadFmtCacheStore(cache.filePath, cacheNamespace);
216+
expect(store.get('index.ts')).toEqual([
217+
sha256(readFileSync(filePath)),
218+
expect.any(String),
219+
'clean',
220+
]);
221+
await expect(run([file], 'check', cache)).resolves.toMatchObject({
222+
exitCode: 0,
223+
files: [],
224+
});
175225
});
176226
});

packages/rstack/tests/fmt/worker.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ test('returns cached states before resolving the parser', async () => {
5151
const contentHash = sha256(source);
5252
const optionsHash = 'options';
5353

54-
for (const [state, status] of [
55-
['clean', 'unchanged'],
56-
['dirty', 'changed'],
54+
for (const [state, shouldWrite, status] of [
55+
['clean', false, 'unchanged'],
56+
['dirty', false, 'changed'],
57+
['clean', true, 'unchanged'],
5758
] as const) {
5859
await expect(
5960
formatFile({
@@ -63,7 +64,7 @@ test('returns cached states before resolving the parser', async () => {
6364
parser: 'unknown-parser',
6465
},
6566
},
66-
shouldWrite: false,
67+
shouldWrite,
6768
cache: {
6869
entry: [contentHash, optionsHash, state],
6970
optionsHash,

0 commit comments

Comments
 (0)