Files
vercel__next.js/test/unit/recursive-delete.test.ts
Will Binns-Smith 12bf495e0e Expand Turbopack dev cleanup (#97833)
## Summary

Builds on #97591. Webpack currently removes most of `.next` on devserver
startup. We can remove more as well.

This keeps the age-based removal introduced in #97591 and expands it to
`traces`, logs, etc. However, for any other directory that is not
`cache` or `lock`, completely remove it on startup. This includes types,
etc.

Apply age-based deletion to Turbopack-owned output that can be restored
from cache or recomputed, including route entries, chunks, assets,
service workers, middleware, and instrumentation. Completely remove
other dev output so logs, traces, generated types, diagnostics, and
future framework-owned output cannot be left as mixed generations.

Use exact-path exclusion sets in `recursiveDeleteSyncWithAsyncRetries`.
The complete pass retains `cache`, `lock`, and Turbopack-owned paths;
the age-based pass then sweeps those Turbopack paths. Both passes finish
before project creation.

## Verification

- `pnpm test-unit test/unit/recursive-delete.test.ts`
- `pnpm --filter=next types`

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Will Binns-Smith <755844+wbinnssmith@users.noreply.github.com>
2026-08-27 14:54:23 -07:00

122 lines
4.1 KiB
TypeScript

/* eslint-env jest */
import fs from 'fs-extra'
import {
recursiveDeleteSyncWithAsyncRetries,
calcBackoffMs,
} from 'next/dist/lib/recursive-delete'
import { recursiveReadDir } from 'next/dist/lib/recursive-readdir'
import { recursiveCopy } from 'next/dist/lib/recursive-copy'
import { join } from 'path'
const resolveDataDir = join(__dirname, 'isolated', '_resolvedata')
const testResolveDataDir = join(__dirname, 'isolated', 'test_resolvedata')
const testpreservefileDir = join(__dirname, 'isolated', 'preservefiles')
describe('recursiveDeleteSyncWithAsyncRetries', () => {
if (process.platform === 'win32') {
it('should skip on windows to avoid symlink issues', () => {})
return
}
it('should work', async () => {
expect.assertions(1)
try {
await recursiveCopy(resolveDataDir, testResolveDataDir)
await fs.symlink('./aa', join(testResolveDataDir, 'symlink'))
await recursiveDeleteSyncWithAsyncRetries(testResolveDataDir)
const result = await recursiveReadDir(testResolveDataDir)
expect(result.length).toBe(0)
} finally {
await recursiveDeleteSyncWithAsyncRetries(testResolveDataDir)
}
})
it('should exclude', async () => {
expect.assertions(2)
try {
await recursiveCopy(resolveDataDir, testpreservefileDir, {
overwrite: true,
})
await recursiveDeleteSyncWithAsyncRetries(
testpreservefileDir,
new Set(['cache'])
)
const result = await recursiveReadDir(testpreservefileDir)
expect(result).toEqual(['/cache/test.txt'])
} finally {
// Ensure test cleanup
await recursiveDeleteSyncWithAsyncRetries(testpreservefileDir)
const cleanupResult = await recursiveReadDir(testpreservefileDir)
expect(cleanupResult.length).toBe(0)
}
})
it('should exclude a nested path', async () => {
expect.assertions(4)
try {
await recursiveCopy(resolveDataDir, testpreservefileDir, {
overwrite: true,
})
await recursiveDeleteSyncWithAsyncRetries(
testpreservefileDir,
new Set([join('aa', 'cache.js')])
)
const result = await recursiveReadDir(testpreservefileDir)
expect(result).toEqual(['/aa/cache.js'])
expect(
await fs.pathExists(join(testpreservefileDir, 'aa', 'cache.js'))
).toBe(true)
expect(
await fs.pathExists(join(testpreservefileDir, 'aa', 'index.js'))
).toBe(false)
} finally {
// Ensure test cleanup
await recursiveDeleteSyncWithAsyncRetries(testpreservefileDir)
const cleanupResult = await recursiveReadDir(testpreservefileDir)
expect(cleanupResult.length).toBe(0)
}
})
it('should only delete files older than maxAgeMs, and empty out dirs', async () => {
const dir = join(__dirname, 'isolated', 'ttl')
try {
await fs.outputFile(join(dir, 'stale', 'old.js'), 'old')
await fs.outputFile(join(dir, 'fresh', 'new.js'), 'new')
await fs.outputFile(join(dir, 'mixed', 'old.js'), 'old')
await fs.outputFile(join(dir, 'mixed', 'new.js'), 'new')
await fs.outputFile(join(dir, 'future.js'), 'future')
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
for (const p of ['stale/old.js', 'mixed/old.js']) {
await fs.utimes(join(dir, p), weekAgo, weekAgo)
}
const twoHoursFromNow = new Date(Date.now() + 2 * 60 * 60 * 1000)
await fs.utimes(join(dir, 'future.js'), twoHoursFromNow, twoHoursFromNow)
await recursiveDeleteSyncWithAsyncRetries(dir, undefined, 60 * 60 * 1000)
expect((await recursiveReadDir(dir)).sort()).toEqual([
'/fresh/new.js',
'/mixed/new.js',
])
// `stale` held nothing but stale files, so the directory goes too
expect(await fs.pathExists(join(dir, 'stale'))).toBe(false)
} finally {
await recursiveDeleteSyncWithAsyncRetries(dir)
}
})
})
describe('calcBackoffMs', () => {
it('returns expected values', () => {
let backoffValuesMs = Array.from({ length: 6 }, (_, attempt) =>
calcBackoffMs(attempt)
)
expect(backoffValuesMs).toEqual([8, 16, 32, 64, 64, 64])
})
})