Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/rstack/src/fmt/discoverPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ import {
type RelativePathResolver,
} from './pathHelpers.ts';

const defaultIgnoredDirNames = new Set(['.git', '.sl', '.svn', '.hg', '.jj', 'node_modules']);
const defaultIgnoredDirNames = new Set([
'.git',
'.sl',
'.svn',
'.hg',
'.jj',
'.rstack',
Comment thread
chenjiahan marked this conversation as resolved.
'node_modules',
]);

interface DiscoverFmtPathsOptions {
/** Absolute directory used to resolve input paths. */
Expand Down
35 changes: 35 additions & 0 deletions packages/rstack/src/projectCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';

const cacheGitignore = '*\n';

type ProjectCacheResult =
{ status: 'available'; path: string } | { status: 'unavailable'; path: string; error: unknown };

/** Returns the disposable cache directory for a resolved Rstack project root. */
const getProjectCacheDir = (rootPath: string): string => path.join(rootPath, '.rstack', 'cache');

/** Creates the project cache directory without making cache failures fatal. */
const ensureProjectCacheDir = async (rootPath: string): Promise<ProjectCacheResult> => {
const cachePath = getProjectCacheDir(rootPath);
const ignorePath = path.join(cachePath, '.gitignore');

try {
if ((await readFile(ignorePath, 'utf8')) === cacheGitignore) {
return { status: 'available', path: cachePath };
}
} catch {
// Create or repair the marker below.
}

try {
await mkdir(cachePath, { recursive: true });
await writeFile(ignorePath, cacheGitignore);
return { status: 'available', path: cachePath };
} catch (error) {
return { status: 'unavailable', path: cachePath, error };
}
};

export { ensureProjectCacheDir, getProjectCacheDir };
export type { ProjectCacheResult };
13 changes: 13 additions & 0 deletions packages/rstack/tests/fmt/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ test('applies config ignore patterns outside the config root', async () => {
});
});

test('excludes .rstack from discovery', async () => {
await withTempProject(async (rootPath) => {
const cacheFile = writeProjectFile(rootPath, '.rstack/cache/fmt-v1.json', '{}');
writeProjectFile(rootPath, 'index.ts');

const discoveredFiles = await discover(rootPath);
const explicitFile = await discover(rootPath, [cacheFile]);

expect(relativePaths(rootPath, discoveredFiles)).toEqual(['index.ts']);
expect(explicitFile).toEqual([]);
});
});

test('keeps files re-included by a CLI ignore file during directory traversal', async () => {
await withTempProject(async (rootPath) => {
writeProjectFile(rootPath, '.prettierignore', 'generated/*\n!generated/keep.ts\n');
Expand Down
38 changes: 38 additions & 0 deletions packages/rstack/tests/projectCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { expect, test } from 'rstack/test';
import { ensureProjectCacheDir, getProjectCacheDir } from '../src/projectCache.ts';
import { withTempProject, writeProjectFile } from './fmt/helpers.ts';

test('creates and repairs an ignored project cache only when requested', async () => {
await withTempProject(async (rootPath) => {
const cachePath = getProjectCacheDir(rootPath);
const ignorePath = path.join(cachePath, '.gitignore');

expect(cachePath).toBe(path.join(rootPath, '.rstack', 'cache'));
expect(existsSync(cachePath)).toBe(false);

await expect(ensureProjectCacheDir(rootPath)).resolves.toEqual({
status: 'available',
path: cachePath,
});
expect(readFileSync(ignorePath, 'utf8')).toBe('*\n');

writeFileSync(ignorePath, 'stale\n');
await ensureProjectCacheDir(rootPath);
expect(readFileSync(ignorePath, 'utf8')).toBe('*\n');
});
});

test('reports an unavailable project cache without throwing', async () => {
await withTempProject(async (rootPath) => {
writeProjectFile(rootPath, '.rstack', 'not a directory');

const result = await ensureProjectCacheDir(rootPath);

expect(result).toMatchObject({
status: 'unavailable',
path: getProjectCacheDir(rootPath),
});
});
});