Skip to content

Commit 6ff1ff2

Browse files
authored
feat(fmt): add project cache directory (#221)
1 parent a4dae03 commit 6ff1ff2

4 files changed

Lines changed: 95 additions & 1 deletion

File tree

packages/rstack/src/fmt/discoverPaths.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ import {
1010
type RelativePathResolver,
1111
} from './pathHelpers.ts';
1212

13-
const defaultIgnoredDirNames = new Set(['.git', '.sl', '.svn', '.hg', '.jj', 'node_modules']);
13+
const defaultIgnoredDirNames = new Set([
14+
'.git',
15+
'.sl',
16+
'.svn',
17+
'.hg',
18+
'.jj',
19+
'.rstack',
20+
'node_modules',
21+
]);
1422

1523
interface DiscoverFmtPathsOptions {
1624
/** Absolute directory used to resolve input paths. */
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
2+
import path from 'node:path';
3+
4+
const cacheGitignore = '*\n';
5+
6+
type ProjectCacheResult =
7+
{ status: 'available'; path: string } | { status: 'unavailable'; path: string; error: unknown };
8+
9+
/** Returns the disposable cache directory for a resolved Rstack project root. */
10+
const getProjectCacheDir = (rootPath: string): string => path.join(rootPath, '.rstack', 'cache');
11+
12+
/** Creates the project cache directory without making cache failures fatal. */
13+
const ensureProjectCacheDir = async (rootPath: string): Promise<ProjectCacheResult> => {
14+
const cachePath = getProjectCacheDir(rootPath);
15+
const ignorePath = path.join(cachePath, '.gitignore');
16+
17+
try {
18+
if ((await readFile(ignorePath, 'utf8')) === cacheGitignore) {
19+
return { status: 'available', path: cachePath };
20+
}
21+
} catch {
22+
// Create or repair the marker below.
23+
}
24+
25+
try {
26+
await mkdir(cachePath, { recursive: true });
27+
await writeFile(ignorePath, cacheGitignore);
28+
return { status: 'available', path: cachePath };
29+
} catch (error) {
30+
return { status: 'unavailable', path: cachePath, error };
31+
}
32+
};
33+
34+
export { ensureProjectCacheDir, getProjectCacheDir };
35+
export type { ProjectCacheResult };

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,19 @@ test('applies config ignore patterns outside the config root', async () => {
4747
});
4848
});
4949

50+
test('excludes .rstack from discovery', async () => {
51+
await withTempProject(async (rootPath) => {
52+
const cacheFile = writeProjectFile(rootPath, '.rstack/cache/fmt-v1.json', '{}');
53+
writeProjectFile(rootPath, 'index.ts');
54+
55+
const discoveredFiles = await discover(rootPath);
56+
const explicitFile = await discover(rootPath, [cacheFile]);
57+
58+
expect(relativePaths(rootPath, discoveredFiles)).toEqual(['index.ts']);
59+
expect(explicitFile).toEqual([]);
60+
});
61+
});
62+
5063
test('keeps files re-included by a CLI ignore file during directory traversal', async () => {
5164
await withTempProject(async (rootPath) => {
5265
writeProjectFile(rootPath, '.prettierignore', 'generated/*\n!generated/keep.ts\n');
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2+
import path from 'node:path';
3+
import { expect, test } from 'rstack/test';
4+
import { ensureProjectCacheDir, getProjectCacheDir } from '../src/projectCache.ts';
5+
import { withTempProject, writeProjectFile } from './fmt/helpers.ts';
6+
7+
test('creates and repairs an ignored project cache only when requested', async () => {
8+
await withTempProject(async (rootPath) => {
9+
const cachePath = getProjectCacheDir(rootPath);
10+
const ignorePath = path.join(cachePath, '.gitignore');
11+
12+
expect(cachePath).toBe(path.join(rootPath, '.rstack', 'cache'));
13+
expect(existsSync(cachePath)).toBe(false);
14+
15+
await expect(ensureProjectCacheDir(rootPath)).resolves.toEqual({
16+
status: 'available',
17+
path: cachePath,
18+
});
19+
expect(readFileSync(ignorePath, 'utf8')).toBe('*\n');
20+
21+
writeFileSync(ignorePath, 'stale\n');
22+
await ensureProjectCacheDir(rootPath);
23+
expect(readFileSync(ignorePath, 'utf8')).toBe('*\n');
24+
});
25+
});
26+
27+
test('reports an unavailable project cache without throwing', async () => {
28+
await withTempProject(async (rootPath) => {
29+
writeProjectFile(rootPath, '.rstack', 'not a directory');
30+
31+
const result = await ensureProjectCacheDir(rootPath);
32+
33+
expect(result).toMatchObject({
34+
status: 'unavailable',
35+
path: getProjectCacheDir(rootPath),
36+
});
37+
});
38+
});

0 commit comments

Comments
 (0)