From 75d114f2cdd7b79bea0bd2d1dee5f0df1a8a4630 Mon Sep 17 00:00:00 2001 From: Rafa Mel Date: Sun, 28 Apr 2019 02:44:05 +0200 Subject: [PATCH] feat(exposed/file): adds remove --- src/exposed/file/index.ts | 4 ++ src/exposed/{ => file}/json.ts | 0 src/exposed/file/remove.ts | 92 ++++++++++++++++++++++++++++++++++ src/exposed/index.ts | 2 +- 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/exposed/file/index.ts rename src/exposed/{ => file}/json.ts (100%) create mode 100644 src/exposed/file/remove.ts diff --git a/src/exposed/file/index.ts b/src/exposed/file/index.ts new file mode 100644 index 0000000..635d8d5 --- /dev/null +++ b/src/exposed/file/index.ts @@ -0,0 +1,4 @@ +export { default as json } from './json'; +export { default as remove } from './remove'; + +// TODO: copy, mkdir, move, rw diff --git a/src/exposed/json.ts b/src/exposed/file/json.ts similarity index 100% rename from src/exposed/json.ts rename to src/exposed/file/json.ts diff --git a/src/exposed/file/remove.ts b/src/exposed/file/remove.ts new file mode 100644 index 0000000..99793aa --- /dev/null +++ b/src/exposed/file/remove.ts @@ -0,0 +1,92 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { rejects } from 'errorish'; +import core from '~/core'; +import { wrap } from '~/utils/errors'; +import { absolute, exists } from '~/utils/file'; +import { TScriptAsyncFn } from '~/types'; +import confirm from '../prompts/confirm'; +import { parallel } from 'promist'; +import logger from '~/utils/logger'; +import chalk from 'chalk'; + +/** + * Options taken by `remove`. + */ +export interface IRemoveOptions { + /** + * If `true`, it will require user confirmation for removal. + */ + confirm?: boolean; + /** + * If `true`, it will fail if a path doesn't exist. + */ + fail?: boolean; +} + +/** + * Removes a file, a directory -recursively-, or an array of them. + * @param paths a path for a file or directory, or an array of them. + * @param options a `IRemoveOptions` object. + * @returns An asynchronous function, as a `TScriptAsyncFn`, that won't be executed until called by `kpo` -hence, calling `remove` won't have any effect until the returned function is called. + */ +export default function remove( + paths: string | string[], + options: IRemoveOptions = {} +): TScriptAsyncFn { + return (): Promise => { + return wrap.throws(async () => { + const cwd = await core.cwd(); + paths = Array.isArray(paths) ? paths : [paths]; + paths = paths.map((path) => absolute(path, cwd)); + + const existingPaths = await parallel.filter(paths, (path) => + exists(path) + ); + const nonExistingPaths = paths.filter( + (path) => !existingPaths.includes(path) + ); + const relatives = { + existing: existingPaths.map((x) => path.relative(cwd, x)), + nonExisting: nonExistingPaths.map((x) => path.relative(cwd, x)) + }; + + if (options.fail && nonExistingPaths.length) { + throw Error( + `Path to remove doesn't exist: ${relatives.nonExisting[0]}` + ); + } + + // eslint-disable-next-line no-console + (options.confirm ? console.log : logger.debug)( + chalk.bold.yellow( + relatives.existing.length ? 'Paths to remove' : 'No paths to remove' + ) + + (relatives.existing.length + ? `\n Existing paths: "${relatives.existing.join('", "')}"` + : '') + + (relatives.nonExisting.length + ? `\n Non existing paths: "${relatives.nonExisting.join( + '", "' + )}"` + : '') + ); + + if (!existingPaths.length) return; + if (options.confirm) { + await confirm({ no: false })().then(async (x) => { + if (x === false) throw Error(`Cancelled by user`); + }); + } + + await parallel.each(existingPaths, async (absolute, i) => { + await fs.remove(absolute).catch(rejects); + + const relative = relatives.existing[i]; + logger.debug(`Removed: ${relative}`); + }); + + logger.info(`Removed: "${relatives.existing.join('", "')}"`); + }); + }; +} diff --git a/src/exposed/index.ts b/src/exposed/index.ts index 69ccdea..401f220 100644 --- a/src/exposed/index.ts +++ b/src/exposed/index.ts @@ -1,5 +1,5 @@ export * from './exec'; +export * from './file'; export * from './prompts'; export * from './tags'; -export { default as json } from './json'; export { default as options } from './options';