diff --git a/packages/core/src/lib/util.ts b/packages/core/src/lib/util.ts index c0853b5b..0afb88e9 100644 --- a/packages/core/src/lib/util.ts +++ b/packages/core/src/lib/util.ts @@ -17,6 +17,7 @@ import * as extractZip from 'extract-zip'; import fetch from 'node-fetch'; import * as fs from 'fs'; +import {join} from 'path'; import {promisify} from 'util'; import {exec, execFile, spawn} from 'child_process'; import {x as extractTar} from 'tar'; @@ -224,3 +225,26 @@ export function validatePackageId(input: string): string | null{ } return null; } + +/** + * Removes a file or directory. If the path is a directory, recursively deletes files and + * directories inside it. + */ +export async function rmdir(path: string): Promise { + if (!fs.existsSync(path)) { + return; + } + const stat = await fs.promises.stat(path); + + // This is a regular file. Just delete it. + if (stat.isFile()) { + await fs.promises.unlink(path); + return; + } + + // This is a directory. We delete files and sub directories inside it, then delete the + // directory itself. + const entries = fs.readdirSync(path); + await Promise.all(entries.map((entry) => rmdir(join(path, entry)))); + await fs.promises.rmdir(path); +}; diff --git a/packages/core/src/spec/lib/utilSpec.ts b/packages/core/src/spec/lib/utilSpec.ts index 95751572..b3d62f1b 100644 --- a/packages/core/src/spec/lib/utilSpec.ts +++ b/packages/core/src/spec/lib/utilSpec.ts @@ -15,6 +15,8 @@ */ import * as util from '../../lib/util'; +import * as mockFs from 'mock-fs'; +import {existsSync} from 'fs'; describe('util', () => { describe('#findSuitableIcon', () => { @@ -224,4 +226,35 @@ describe('util', () => { expect(util.validatePackageId('_com.char.1twa')).not.toBeNull(); }); }); + + describe('rmdirs', () => { + it('Deletes a single file', async () => { + mockFs({'/app.txt': 'Test Content'}); + await util.rmdir('/app.txt'); + expect(existsSync('/app.txt')).toBeFalse(); + mockFs.restore(); + }); + + it('Deletes a directory', async () => { + mockFs({ + '/test': { + 'app.txt': 'Test Content', + 'subdirectory': { + 'file2.txt': 'Test Content 2', + }, + }, + '/other-file.txt': 'This should not be deleted', + }); + await util.rmdir('/test'); + expect(existsSync('/test')).toBeFalse(); + expect(existsSync('/other-file.txt')).toBeTrue(); + mockFs.restore(); + }); + + it('Skips empty directory', () => { + mockFs({}); + expectAsync(util.rmdir('/app.txt')).toBeResolved(); + mockFs.restore(); + }); + }); });