From f3d492eefb0bfde5f74025a17b806694449ae991 Mon Sep 17 00:00:00 2001 From: neverland Date: Thu, 6 Aug 2026 17:07:53 +0800 Subject: [PATCH 1/2] feat(fmt): add project cache directory --- packages/rstack/src/fmt/discoverPaths.ts | 10 +++++- packages/rstack/src/projectCache.ts | 35 +++++++++++++++++++ packages/rstack/tests/fmt/discovery.test.ts | 13 +++++++ packages/rstack/tests/projectCache.test.ts | 38 +++++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 packages/rstack/src/projectCache.ts create mode 100644 packages/rstack/tests/projectCache.test.ts diff --git a/packages/rstack/src/fmt/discoverPaths.ts b/packages/rstack/src/fmt/discoverPaths.ts index 7ed432ae..e77f12dd 100644 --- a/packages/rstack/src/fmt/discoverPaths.ts +++ b/packages/rstack/src/fmt/discoverPaths.ts @@ -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', + 'node_modules', +]); interface DiscoverFmtPathsOptions { /** Absolute directory used to resolve input paths. */ diff --git a/packages/rstack/src/projectCache.ts b/packages/rstack/src/projectCache.ts new file mode 100644 index 00000000..6217713d --- /dev/null +++ b/packages/rstack/src/projectCache.ts @@ -0,0 +1,35 @@ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +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 => { + const cachePath = getProjectCacheDir(rootPath); + const ignorePath = path.join(cachePath, '.gitignore'); + + try { + if (readFileSync(ignorePath, 'utf8') === cacheGitignore) { + return { status: 'available', path: cachePath }; + } + } catch { + // Create or repair the marker below. + } + + try { + mkdirSync(cachePath, { recursive: true }); + writeFileSync(ignorePath, cacheGitignore); + return { status: 'available', path: cachePath }; + } catch (error) { + return { status: 'unavailable', path: cachePath, error }; + } +}; + +export { ensureProjectCacheDir, getProjectCacheDir }; +export type { ProjectCacheResult }; diff --git a/packages/rstack/tests/fmt/discovery.test.ts b/packages/rstack/tests/fmt/discovery.test.ts index 9ee6443f..71b902b4 100644 --- a/packages/rstack/tests/fmt/discovery.test.ts +++ b/packages/rstack/tests/fmt/discovery.test.ts @@ -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'); diff --git a/packages/rstack/tests/projectCache.test.ts b/packages/rstack/tests/projectCache.test.ts new file mode 100644 index 00000000..eff83131 --- /dev/null +++ b/packages/rstack/tests/projectCache.test.ts @@ -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), + }); + }); +}); From 250cdcfe6a1410e083086cd10f9f0efa57b7a1a6 Mon Sep 17 00:00:00 2001 From: neverland Date: Thu, 6 Aug 2026 17:12:46 +0800 Subject: [PATCH 2/2] refactor(fmt): use async cache directory I/O --- packages/rstack/src/projectCache.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/rstack/src/projectCache.ts b/packages/rstack/src/projectCache.ts index 6217713d..5959b1ce 100644 --- a/packages/rstack/src/projectCache.ts +++ b/packages/rstack/src/projectCache.ts @@ -1,4 +1,4 @@ -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; const cacheGitignore = '*\n'; @@ -15,7 +15,7 @@ const ensureProjectCacheDir = async (rootPath: string): Promise