Skip to content

Commit 9e3b1cb

Browse files
committed
fix(cleanup): make directory removal safe and reliable
1 parent 23a5a6c commit 9e3b1cb

3 files changed

Lines changed: 92 additions & 24 deletions

File tree

‎packages/nuxt-cli/src/utils/fs.ts‎

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import{existsSync,promisesasfsp}from'node:fs'
22
import{join}from'pathe'
3-
import{debug}from'../utils/logger'
43

54
exportasyncfunctionclearDir(path: string,exclude?: string[]){
65
if(!exclude){
@@ -22,14 +21,3 @@ export async function clearDir(path: string, exclude?: string[]) {
2221
exportfunctionclearBuildDir(path: string){
2322
returnclearDir(path,['cache','analyze','nuxt.json','nuxt.lock'])
2423
}
25-
26-
exportasyncfunctionrmRecursive(paths: string[]){
27-
awaitPromise.all(
28-
paths
29-
.filter(p=>typeofp==='string')
30-
.map(async(path)=>{
31-
debug(`Removing recursive path: ${path}`)
32-
awaitfsp.rm(path,{recursive: true,force: true}).catch(()=>{})
33-
}),
34-
)
35-
}

‎packages/nuxt-cli/src/utils/nuxt.ts‎

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { promises as fsp } from 'node:fs'
55
import{hash}from'ohash'
66
import{dirname,resolve}from'pathe'
77

8-
import{logger}from'../utils/logger'
9-
import{rmRecursive}from'./fs'
8+
import{debug,logger}from'../utils/logger'
109

1110
constGIT_ID_RE=/\.([0-9a-f]{7,8})$/
1211

@@ -20,21 +19,29 @@ interface NuxtProjectManifest {
2019
}
2120
}
2221

23-
/** `silent` is for callers that already report progress themselves. */
2422
exportasyncfunctioncleanupNuxtDirs(rootDir: string,buildDir: string,options: {silent?: boolean}={}){
23+
constroot=resolve(rootDir)
24+
constbuild=resolve(root,buildDir)
25+
if(build===root||root.startsWith(build.endsWith('/') ? build : `${build}/`)){
26+
thrownewError('Cannot clean a build directory that contains the project root.')
27+
}
28+
2529
if(!options.silent){
2630
logger.info('Cleaning up generated Nuxt files and caches...')
2731
}
2832

29-
awaitrmRecursive(
30-
[
31-
buildDir,
32-
'.output',
33-
'dist',
34-
'node_modules/.vite',
35-
'node_modules/.cache',
36-
].map(dir=>resolve(rootDir,dir)),
37-
)
33+
constpaths=newSet([
34+
build,
35+
'.output',
36+
'dist',
37+
'node_modules/.vite',
38+
'node_modules/.cache',
39+
].map(dir=>resolve(root,dir)))
40+
41+
awaitPromise.all([...paths].map((path)=>{
42+
debug(`Removing recursive path: ${path}`)
43+
returnfsp.rm(path,{recursive: true,force: true})
44+
}))
3845
}
3946

4047
exportfunctionnuxtVersionToGitIdentifier(version: string){
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import{existsSync}from'node:fs'
2+
import{mkdir,mkdtemp,rm,writeFile}from'node:fs/promises'
3+
import{tmpdir}from'node:os'
4+
5+
import{runCommand}from'citty'
6+
import{join}from'pathe'
7+
import{afterEach,beforeEach,describe,expect,it,vi}from'vitest'
8+
9+
importcleanupfrom'../../../src/commands/cleanup'
10+
11+
const{ loadNuxtConfig }=vi.hoisted(()=>({
12+
loadNuxtConfig: vi.fn(),
13+
}))
14+
15+
vi.mock('../../../src/utils/kit',()=>({
16+
loadKit: ()=>Promise.resolve({ loadNuxtConfig }),
17+
}))
18+
19+
letcwd: string
20+
21+
asyncfunctioncreateFile(path: string){
22+
awaitmkdir(join(path,'..'),{recursive: true})
23+
awaitwriteFile(path,'')
24+
}
25+
26+
describe('cleanup',()=>{
27+
beforeEach(async()=>{
28+
vi.clearAllMocks()
29+
cwd=awaitmkdtemp(join(tmpdir(),'nuxt-cleanup-'))
30+
loadNuxtConfig.mockResolvedValue({rootDir: cwd,buildDir: join(cwd,'.nuxt')})
31+
})
32+
33+
afterEach(async()=>{
34+
awaitrm(cwd,{recursive: true,force: true})
35+
})
36+
37+
it('loads the development config and removes generated directories',async()=>{
38+
constgenerated=[
39+
'.nuxt/nuxt.json',
40+
'.output/server/index.mjs',
41+
'dist/index.html',
42+
'node_modules/.vite/cache',
43+
'node_modules/.cache/nuxt/client.json',
44+
]
45+
awaitPromise.all(generated.map(path=>createFile(join(cwd,path))))
46+
awaitcreateFile(join(cwd,'node_modules/nuxt/package.json'))
47+
48+
awaitrunCommand(cleanup,{rawArgs: [cwd]})
49+
50+
expect(loadNuxtConfig).toHaveBeenCalledWith({ cwd,overrides: {dev: true}})
51+
expect(generated.every(path=>!existsSync(join(cwd,path)))).toBe(true)
52+
expect(existsSync(join(cwd,'node_modules/nuxt/package.json'))).toBe(true)
53+
})
54+
55+
it('removes a custom build directory only once when it overlaps a cache directory',async()=>{
56+
constbuildDir=join(cwd,'node_modules/.cache')
57+
loadNuxtConfig.mockResolvedValue({rootDir: cwd, buildDir })
58+
awaitcreateFile(join(buildDir,'nuxt/client.json'))
59+
60+
awaitexpect(runCommand(cleanup,{rawArgs: [cwd]})).resolves.toBeDefined()
61+
})
62+
63+
it.each([
64+
['the project root',()=>cwd],
65+
['a parent of the project root',()=>join(cwd,'..')],
66+
])('refuses to remove %s',async(_,getBuildDir)=>{
67+
loadNuxtConfig.mockResolvedValue({rootDir: cwd,buildDir: getBuildDir()})
68+
awaitcreateFile(join(cwd,'package.json'))
69+
70+
awaitexpect(runCommand(cleanup,{rawArgs: [cwd]})).rejects.toThrow('Cannot clean a build directory that contains the project root.')
71+
expect(existsSync(join(cwd,'package.json'))).toBe(true)
72+
})
73+
})

0 commit comments

Comments
 (0)