diff --git a/packages/vitest/src/node/cache/cachedirTag.ts b/packages/vitest/src/node/cache/cachedirTag.ts new file mode 100644 index 000000000..e4e5dfb60 --- /dev/null +++ b/packages/vitest/src/node/cache/cachedirTag.ts @@ -0,0 +1,39 @@ +import { writeFileSync } from 'node:fs' +import { join } from 'pathe' +import { createDebugger } from '../../utils/debugger' + +const debug = createDebugger('vitest:cache:tag') + +// https://bford.info/cachedir/ +const CACHEDIR_TAG = `Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by Vitest. +# For information about cache directory tags see https://bford.info/cachedir/ +` + +const taggedDirs = new Set() + +/** + * Marks a directory that Vitest owns as a cache, so backup tools and task runners + * can skip it. An existing tag is never overwritten. The directory must exist. + */ +export function tagCacheDir(dir: string): void { + if (taggedDirs.has(dir)) { + return + } + taggedDirs.add(dir) + try { + writeFileSync(join(dir, 'CACHEDIR.TAG'), CACHEDIR_TAG, { flag: 'wx' }) + } catch (error: any) { + // the tag is optional and must never fail the run + if (error?.code !== 'EEXIST') { + debug?.(`failed to tag ${dir} as a cache directory: ${error}`) + } + } +} + +/** + * Tags the directory again the next time it is used, after it was removed. + */ +export function forgetCacheDirTag(dir: string): void { + taggedDirs.delete(dir) +} diff --git a/packages/vitest/src/node/cache/fsModuleCache.ts b/packages/vitest/src/node/cache/fsModuleCache.ts index e20516f18..b1820f4b4 100644 --- a/packages/vitest/src/node/cache/fsModuleCache.ts +++ b/packages/vitest/src/node/cache/fsModuleCache.ts @@ -11,6 +11,7 @@ import c from 'tinyrainbow' import { searchForWorkspaceRoot } from 'vite' import { createDebugger } from '../../utils/debugger' import { hash } from '../hash' +import { forgetCacheDirTag, tagCacheDir } from './cachedirTag' const debugFs = createDebugger('vitest:cache:fs') const debugMemory = createDebugger('vitest:cache:memory') @@ -96,6 +97,7 @@ export class FileSystemModuleCache { ...this.vitest.projects.map((r) => r.config.fsModuleCachePath || this.rootCache), ] const uniquePaths = Array.from(new Set(fsCachePaths)) + uniquePaths.forEach(forgetCacheDirTag) await Promise.all( uniquePaths.map((directory) => rm(directory, { force: true, recursive: true })), ) @@ -297,6 +299,7 @@ export class FileSystemModuleCache { if (!existsSync(cacheRoot)) { mkdirSync(cacheRoot, { recursive: true }) } + tagCacheDir(cacheRoot) } const fsResultPath = join(cacheRoot, cacheKey) @@ -330,6 +333,7 @@ export class FileSystemModuleCache { if (!existsSync(this.rootCache)) { mkdirSync(this.rootCache, { recursive: true }) } + tagCacheDir(this.rootCache) await writeFile(this.metadataFilePath, JSON.stringify({ lockfileHash }, null, 2), 'utf-8') } catch (error) { // Recording the metadata is best-effort and losing the file shouldn't diff --git a/packages/vitest/src/node/cache/results.ts b/packages/vitest/src/node/cache/results.ts index 830cc9155..99b1b656d 100644 --- a/packages/vitest/src/node/cache/results.ts +++ b/packages/vitest/src/node/cache/results.ts @@ -5,6 +5,7 @@ import { existsSync } from 'node:fs' import { mkdir, readFile, rm, stat } from 'node:fs/promises' import { dirname, relative, resolve } from 'pathe' import { createDebugger } from '../../utils/debugger' +import { tagCacheDir } from './cachedirTag' import { atomicWriteFile } from './fsModuleCache' const debug = createDebugger('vitest:cache:results') @@ -199,7 +200,10 @@ export class ResultsCache { } const results: SerializedResults = { version: RESULTS_VERSION, projects } try { - await mkdir(dirname(this.path), { recursive: true }) + const cacheDir = dirname(this.path) + await mkdir(cacheDir, { recursive: true }) + // the cache is in "cacheDir/vitest/", Vitest owns the "vitest" directory + tagCacheDir(dirname(cacheDir)) await atomicWriteFile(this.path, JSON.stringify(results)) this.fileStamp = await getFileStamp(this.path) } catch (error) { diff --git a/test/e2e/test/cachedir-tag.test.ts b/test/e2e/test/cachedir-tag.test.ts new file mode 100644 index 000000000..04ad65c5f --- /dev/null +++ b/test/e2e/test/cachedir-tag.test.ts @@ -0,0 +1,76 @@ +import { readFileSync } from 'node:fs' +import { dirname, join } from 'pathe' +import { expect, test } from 'vitest' +import { runInlineTests } from '#test-utils' + +const SIGNATURE = 'Signature: 8a477f597d28d172789f06886806bc55' + +const passingTest = /* js */ ` +import { test } from 'vitest' +test('passes', () => {}) +` + +function readTag(dir: string) { + return readFileSync(join(dir, 'CACHEDIR.TAG'), 'utf-8') +} + +test('tags the results cache and the fs module caches', async () => { + const { ctx, root, stderr } = await runInlineTests( + { + 'unit/basic.test.js': passingTest, + 'api/basic.test.js': passingTest, + 'vitest.config.js': { + test: { + fsModuleCache: true, + fsModuleCachePath: './node_modules/.vitest-fs-cache', + projects: [ + { test: { name: 'unit', include: ['unit/*.test.js'] } }, + { + test: { + name: 'api', + include: ['api/*.test.js'], + fsModuleCachePath: './node_modules/.vitest-api-cache', + }, + }, + ], + }, + }, + }, + { cache: true }, + ) + expect(stderr).toBe('') + + // the results are in "cacheDir/vitest/", Vitest owns "cacheDir/vitest" + const vitestCacheDir = dirname(ctx!.viteConfig.cacheDir) + expect(vitestCacheDir).toBe(join(root, 'node_modules/.vite/vitest')) + const tag = readTag(vitestCacheDir) + expect(tag).toMatchInlineSnapshot(` + "Signature: 8a477f597d28d172789f06886806bc55 + # This file is a cache directory tag created by Vitest. + # For information about cache directory tags see https://bford.info/cachedir/ + " + `) + expect(readTag(join(root, 'node_modules/.vitest-fs-cache'))).toBe(tag) + expect(readTag(join(root, 'node_modules/.vitest-api-cache'))).toBe(tag) +}) + +test('keeps an existing tag', async () => { + const customTag = `${SIGNATURE}\n# custom\n` + const { root, stderr } = await runInlineTests( + { + 'basic.test.js': passingTest, + 'node_modules/.vite/vitest/CACHEDIR.TAG': customTag, + 'node_modules/.vitest-fs-cache/CACHEDIR.TAG': customTag, + 'vitest.config.js': { + test: { + fsModuleCache: true, + fsModuleCachePath: './node_modules/.vitest-fs-cache', + }, + }, + }, + { cache: true }, + ) + expect(stderr).toBe('') + expect(readTag(join(root, 'node_modules/.vite/vitest'))).toBe(customTag) + expect(readTag(join(root, 'node_modules/.vitest-fs-cache'))).toBe(customTag) +})