Skip to content

Commit a8ea3c8

Browse files
feat: tag cache directories with CACHEDIR.TAG (#11461)
1 parent 28ef93d commit a8ea3c8

4 files changed

Lines changed: 124 additions & 1 deletion

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { writeFileSync } from 'node:fs'
2+
import { join } from 'pathe'
3+
import { createDebugger } from '../../utils/debugger'
4+
5+
const debug = createDebugger('vitest:cache:tag')
6+
7+
// https://bford.info/cachedir/
8+
const CACHEDIR_TAG = `Signature: 8a477f597d28d172789f06886806bc55
9+
# This file is a cache directory tag created by Vitest.
10+
# For information about cache directory tags see https://bford.info/cachedir/
11+
`
12+
13+
const taggedDirs = new Set<string>()
14+
15+
/**
16+
* Marks a directory that Vitest owns as a cache, so backup tools and task runners
17+
* can skip it. An existing tag is never overwritten. The directory must exist.
18+
*/
19+
export function tagCacheDir(dir: string): void {
20+
if (taggedDirs.has(dir)) {
21+
return
22+
}
23+
taggedDirs.add(dir)
24+
try {
25+
writeFileSync(join(dir, 'CACHEDIR.TAG'), CACHEDIR_TAG, { flag: 'wx' })
26+
} catch (error: any) {
27+
// the tag is optional and must never fail the run
28+
if (error?.code !== 'EEXIST') {
29+
debug?.(`failed to tag ${dir} as a cache directory: ${error}`)
30+
}
31+
}
32+
}
33+
34+
/**
35+
* Tags the directory again the next time it is used, after it was removed.
36+
*/
37+
export function forgetCacheDirTag(dir: string): void {
38+
taggedDirs.delete(dir)
39+
}

‎packages/vitest/src/node/cache/fsModuleCache.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import c from 'tinyrainbow'
1111
import { searchForWorkspaceRoot } from 'vite'
1212
import { createDebugger } from '../../utils/debugger'
1313
import { hash } from '../hash'
14+
import { forgetCacheDirTag, tagCacheDir } from './cachedirTag'
1415

1516
const debugFs = createDebugger('vitest:cache:fs')
1617
const debugMemory = createDebugger('vitest:cache:memory')
@@ -96,6 +97,7 @@ export class FileSystemModuleCache {
9697
...this.vitest.projects.map((r) => r.config.fsModuleCachePath || this.rootCache),
9798
]
9899
const uniquePaths = Array.from(new Set(fsCachePaths))
100+
uniquePaths.forEach(forgetCacheDirTag)
99101
await Promise.all(
100102
uniquePaths.map((directory) => rm(directory, { force: true, recursive: true })),
101103
)
@@ -297,6 +299,7 @@ export class FileSystemModuleCache {
297299
if (!existsSync(cacheRoot)) {
298300
mkdirSync(cacheRoot, { recursive: true })
299301
}
302+
tagCacheDir(cacheRoot)
300303
}
301304

302305
const fsResultPath = join(cacheRoot, cacheKey)
@@ -330,6 +333,7 @@ export class FileSystemModuleCache {
330333
if (!existsSync(this.rootCache)) {
331334
mkdirSync(this.rootCache, { recursive: true })
332335
}
336+
tagCacheDir(this.rootCache)
333337
await writeFile(this.metadataFilePath, JSON.stringify({ lockfileHash }, null, 2), 'utf-8')
334338
} catch (error) {
335339
// Recording the metadata is best-effort and losing the file shouldn't

‎packages/vitest/src/node/cache/results.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { existsSync } from 'node:fs'
55
import { mkdir, readFile, rm, stat } from 'node:fs/promises'
66
import { dirname, relative, resolve } from 'pathe'
77
import { createDebugger } from '../../utils/debugger'
8+
import { tagCacheDir } from './cachedirTag'
89
import { atomicWriteFile } from './fsModuleCache'
910

1011
const debug = createDebugger('vitest:cache:results')
@@ -199,7 +200,10 @@ export class ResultsCache {
199200
}
200201
const results: SerializedResults = { version: RESULTS_VERSION, projects }
201202
try {
202-
await mkdir(dirname(this.path), { recursive: true })
203+
const cacheDir = dirname(this.path)
204+
await mkdir(cacheDir, { recursive: true })
205+
// the cache is in "cacheDir/vitest/<hash>", Vitest owns the "vitest" directory
206+
tagCacheDir(dirname(cacheDir))
203207
await atomicWriteFile(this.path, JSON.stringify(results))
204208
this.fileStamp = await getFileStamp(this.path)
205209
} catch (error) {

‎test/e2e/test/cachedir-tag.test.ts‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { readFileSync } from 'node:fs'
2+
import { dirname, join } from 'pathe'
3+
import { expect, test } from 'vitest'
4+
import { runInlineTests } from '#test-utils'
5+
6+
const SIGNATURE = 'Signature: 8a477f597d28d172789f06886806bc55'
7+
8+
const passingTest = /* js */ `
9+
import { test } from 'vitest'
10+
test('passes', () => {})
11+
`
12+
13+
function readTag(dir: string) {
14+
return readFileSync(join(dir, 'CACHEDIR.TAG'), 'utf-8')
15+
}
16+
17+
test('tags the results cache and the fs module caches', async () => {
18+
const { ctx, root, stderr } = await runInlineTests(
19+
{
20+
'unit/basic.test.js': passingTest,
21+
'api/basic.test.js': passingTest,
22+
'vitest.config.js': {
23+
test: {
24+
fsModuleCache: true,
25+
fsModuleCachePath: './node_modules/.vitest-fs-cache',
26+
projects: [
27+
{ test: { name: 'unit', include: ['unit/*.test.js'] } },
28+
{
29+
test: {
30+
name: 'api',
31+
include: ['api/*.test.js'],
32+
fsModuleCachePath: './node_modules/.vitest-api-cache',
33+
},
34+
},
35+
],
36+
},
37+
},
38+
},
39+
{ cache: true },
40+
)
41+
expect(stderr).toBe('')
42+
43+
// the results are in "cacheDir/vitest/<hash>", Vitest owns "cacheDir/vitest"
44+
const vitestCacheDir = dirname(ctx!.viteConfig.cacheDir)
45+
expect(vitestCacheDir).toBe(join(root, 'node_modules/.vite/vitest'))
46+
const tag = readTag(vitestCacheDir)
47+
expect(tag).toMatchInlineSnapshot(`
48+
"Signature: 8a477f597d28d172789f06886806bc55
49+
# This file is a cache directory tag created by Vitest.
50+
# For information about cache directory tags see https://bford.info/cachedir/
51+
"
52+
`)
53+
expect(readTag(join(root, 'node_modules/.vitest-fs-cache'))).toBe(tag)
54+
expect(readTag(join(root, 'node_modules/.vitest-api-cache'))).toBe(tag)
55+
})
56+
57+
test('keeps an existing tag', async () => {
58+
const customTag = `${SIGNATURE}\n# custom\n`
59+
const { root, stderr } = await runInlineTests(
60+
{
61+
'basic.test.js': passingTest,
62+
'node_modules/.vite/vitest/CACHEDIR.TAG': customTag,
63+
'node_modules/.vitest-fs-cache/CACHEDIR.TAG': customTag,
64+
'vitest.config.js': {
65+
test: {
66+
fsModuleCache: true,
67+
fsModuleCachePath: './node_modules/.vitest-fs-cache',
68+
},
69+
},
70+
},
71+
{ cache: true },
72+
)
73+
expect(stderr).toBe('')
74+
expect(readTag(join(root, 'node_modules/.vite/vitest'))).toBe(customTag)
75+
expect(readTag(join(root, 'node_modules/.vitest-fs-cache'))).toBe(customTag)
76+
})

0 commit comments

Comments
 (0)