diff --git a/src/public/fs/copy.ts b/src/public/fs/copy.ts deleted file mode 100644 index af955a2..0000000 --- a/src/public/fs/copy.ts +++ /dev/null @@ -1,103 +0,0 @@ -import path from 'path'; -import fs from 'fs-extra'; -import { absolute, exists } from '~/utils/file'; -import { IFsUpdateOptions, TSource } from './types'; -import expose, { TExposedOverload } from '~/utils/expose'; -import confirm from '~/utils/confirm'; -import logger from '~/utils/logger'; -import { open } from '~/utils/errors'; - -export type TCopyFilterFn = - | ((src: string, dest: string) => boolean) - | ((src: string, dest: string) => Promise); - -export default expose(copy) as TExposedOverload< - typeof copy, - | [TSource, string] - | [TSource, string, IFsUpdateOptions] - | [TSource, string, TCopyFilterFn] - | [TSource, string, IFsUpdateOptions | undefined, TCopyFilterFn] ->; - -// TODO allow to take an option to duplicate folder structure on dest from a base + don't allow it when src is upwards instead of nested in that folder -function copy( - src: TSource, - dest: string, - filter?: TCopyFilterFn -): () => Promise; -function copy( - src: TSource, - dest: string, - options?: IFsUpdateOptions, - filter?: TCopyFilterFn -): () => Promise; -/** - * Recursive copy. If an array of paths is passed as `src`, `dest` will be expected to be a directory. - * It is an *exposed* function: call `copy.fn()`, which takes the same arguments, in order to execute on call. - * @returns An asynchronous function -hence, calling `copy` won't have any effect until the returned function is called. - */ -function copy(src: TSource, dest: string, ...args: any[]): () => Promise { - return async () => { - src = typeof src === 'function' ? await src() : await src; - - if (Array.isArray(src)) { - // Check dest is a folder - if (await exists(dest)) { - const stat = await fs.stat(dest); - if (!stat.isDirectory()) { - throw Error('Destination must be a folder for an array of sources'); - } - } - for (let source of src) { - await trunk(source, path.join(dest, path.parse(source).base), args); - } - } else { - await trunk(src, dest, args); - } - }; -} - -/** @hidden */ -export async function trunk( - src: string, - dest: string, - args: any[] -): Promise { - const options: IFsUpdateOptions = Object.assign( - { overwrite: true }, - args.find((x) => typeof x === 'object') || {} - ); - let filter: TCopyFilterFn = - args.find((x) => typeof x === 'function') || (() => true); - - const cwd = process.cwd(); - src = absolute({ path: src, cwd }); - dest = absolute({ path: dest, cwd }); - - const relatives = { - src: './' + path.relative(cwd, src), - dest: './' + path.relative(cwd, dest) - }; - - const srcExist = await exists(src, { fail: options.fail }); - if (!srcExist) { - logger.info(`Copy skipped: "${relatives.src}" to "${relatives.dest}"`); - return; - } - - const msg = `Copy "${relatives.src}" to "${relatives.dest}"?`; - if (!(await confirm(msg, options))) return; - - await fs.copy(src, dest, { - overwrite: options.overwrite, - errorOnExist: options.fail, - async filter(src: string, dest: string): Promise { - try { - return await filter(src, dest); - } catch (err) { - throw open(err); - } - } - }); - logger.info(`Copied: "${relatives.src}" to "${relatives.dest}"`); -} diff --git a/src/public/fs/copy/copy.ts b/src/public/fs/copy/copy.ts new file mode 100644 index 0000000..bc261d6 --- /dev/null +++ b/src/public/fs/copy/copy.ts @@ -0,0 +1,76 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { absolute, exists } from '~/utils/file'; +import { IFsUpdateOptions, TCopyFilterFn } from '../types'; +import confirm from '~/utils/confirm'; +import logger from '~/utils/logger'; +import { open } from '~/utils/errors'; + +// TODO allow to take an option to duplicate folder structure on dest from a base + don't allow it when src is upwards instead of nested in that folder + +export default async function copy( + src: string | string[], + dest: string, + options: IFsUpdateOptions = {}, + filter: TCopyFilterFn = () => true +): Promise { + options = Object.assign({ overwrite: true }, options); + + if (Array.isArray(src)) { + // Check dest is a folder + if (await exists(dest)) { + const stat = await fs.stat(dest); + if (!stat.isDirectory()) { + throw Error('Destination must be a folder for an array of sources'); + } + } + for (let source of src) { + await each( + source, + path.join(dest, path.parse(source).base), + options, + filter + ); + } + } else { + await each(src, dest, options, filter); + } +} + +export async function each( + src: string, + dest: string, + options: IFsUpdateOptions, + filter: TCopyFilterFn +): Promise { + const cwd = process.cwd(); + src = absolute({ path: src, cwd }); + dest = absolute({ path: dest, cwd }); + + const relatives = { + src: './' + path.relative(cwd, src), + dest: './' + path.relative(cwd, dest) + }; + + const srcExist = await exists(src, { fail: options.fail }); + if (!srcExist) { + logger.info(`Copy skipped: "${relatives.src}" to "${relatives.dest}"`); + return; + } + + const msg = `Copy "${relatives.src}" to "${relatives.dest}"?`; + if (!(await confirm(msg, options))) return; + + await fs.copy(src, dest, { + overwrite: options.overwrite, + errorOnExist: options.fail, + async filter(src: string, dest: string): Promise { + try { + return await filter(src, dest); + } catch (err) { + throw open(err); + } + } + }); + logger.info(`Copied: "${relatives.src}" to "${relatives.dest}"`); +} diff --git a/src/public/fs/copy/index.ts b/src/public/fs/copy/index.ts new file mode 100644 index 0000000..5441585 --- /dev/null +++ b/src/public/fs/copy/index.ts @@ -0,0 +1,39 @@ +import { TSource, IFsUpdateOptions, TCopyFilterFn } from '../types'; +import expose, { TExposedOverload } from '~/utils/expose'; +import trunk from './copy'; + +export default expose(copy) as TExposedOverload< + typeof copy, + | [TSource, string] + | [TSource, string, IFsUpdateOptions] + | [TSource, string, TCopyFilterFn] + | [TSource, string, IFsUpdateOptions | undefined, TCopyFilterFn] +>; + +function copy( + src: TSource, + dest: string, + filter?: TCopyFilterFn +): () => Promise; +function copy( + src: TSource, + dest: string, + options?: IFsUpdateOptions, + filter?: TCopyFilterFn +): () => Promise; +/** + * Recursive copy. If an array of paths is passed as `src`, `dest` will be expected to be a directory. + * It is an *exposed* function: call `copy.fn()`, which takes the same arguments, in order to execute on call. + * @returns An asynchronous function -hence, calling `copy` won't have any effect until the returned function is called. + */ +function copy(src: TSource, dest: string, ...args: any[]): () => Promise { + return async () => { + const hasOptions = typeof args[0] !== 'function'; + return trunk( + typeof src === 'function' ? await src() : await src, + dest, + hasOptions ? args[0] : undefined, + hasOptions ? args[1] : args[0] + ); + }; +} diff --git a/src/public/fs/json.ts b/src/public/fs/json.ts index 87e70cc..21bd3d2 100644 --- a/src/public/fs/json.ts +++ b/src/public/fs/json.ts @@ -1,7 +1,7 @@ import { IOfType } from '~/types'; import expose from '~/utils/expose'; import rw from './rw'; -import { IFsUpdateOptions } from './types'; +import { IFsUpdateOptions, TContentFn, TSource } from './types'; export default expose(json); @@ -11,14 +11,15 @@ export default expose(json); * @returns An asynchronous function -hence, calling `json` won't have any effect until the returned function is called. */ function json( - file: string, + file: TSource, fn: ( + file: string, json?: IOfType ) => IOfType | void | Promise | void>, - options: IFsUpdateOptions = {} + options?: IFsUpdateOptions ): () => Promise { return async () => { - const _fn = async (raw?: string): Promise => { + const _fn: TContentFn = async (file, raw) => { const json = await fn(raw ? JSON.parse(raw) : undefined); return json ? JSON.stringify(json, null, 2) : undefined; }; diff --git a/src/public/fs/mkdir.ts b/src/public/fs/mkdir.ts deleted file mode 100644 index 32c1ae8..0000000 --- a/src/public/fs/mkdir.ts +++ /dev/null @@ -1,75 +0,0 @@ -import path from 'path'; -import fs from 'fs-extra'; -import { absolute, exists } from '~/utils/file'; -import confirm from '~/utils/confirm'; -import { parallel } from 'promist'; -import logger from '~/utils/logger'; -import chalk from 'chalk'; -import expose from '~/utils/expose'; -import { IFsCreateDeleteOptions, TSource } from './types'; - -export default expose(mkdir); -/** - * Deep creates a directory or an array of them. - * It is an *exposed* function: call `mkdir.fn()`, which takes the same arguments, in order to execute on call. - * @param paths a path for a directory, or an array of them. - * @param options an `IFsCreateDeleteOptions` object. - * @returns An asynchronous function -hence, calling `mkdir` won't have any effect until the returned function is called. - */ -function mkdir( - paths: TSource, - options: IFsCreateDeleteOptions = {} -): () => Promise { - return async () => { - const cwd = process.cwd(); - paths = typeof paths === 'function' ? await paths() : await paths; - 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 && existingPaths.length) { - throw Error(`Directory already exists: ${relatives.existing[0]}`); - } - - // eslint-disable-next-line no-console - (options.confirm ? console.log : logger.debug)( - chalk.bold.yellow( - relatives.existing.length - ? 'Directories to create' - : 'No directories to create' - ) + - (relatives.existing.length - ? `\n Existing paths: "${relatives.existing.join('", "')}"` - : '') + - (relatives.nonExisting.length - ? `\n Non existing paths: "${relatives.nonExisting.join('", "')}"` - : '') - ); - - if (!nonExistingPaths.length) { - logger.info( - `Create skipped: "${relatives.existing - .concat(relatives.nonExisting) - .join('", "')}"` - ); - return; - } - if (!(await confirm('Create?', options))) return; - - await parallel.each(nonExistingPaths, async (absolute, i) => { - await fs.ensureDir(absolute); - - const relative = relatives.nonExisting[i]; - logger.debug(`Created: ${relative}`); - }); - logger.info(`Created: "${relatives.nonExisting.join('", "')}"`); - }; -} diff --git a/src/public/fs/mkdir/index.ts b/src/public/fs/mkdir/index.ts new file mode 100644 index 0000000..29bf21f --- /dev/null +++ b/src/public/fs/mkdir/index.ts @@ -0,0 +1,24 @@ +import expose from '~/utils/expose'; +import trunk from './mkdir'; +import { TSource, IFsCreateDeleteOptions } from '../types'; + +export default expose(mkdir); + +/** + * Deep creates a directory or an array of them. + * It is an *exposed* function: call `mkdir.fn()`, which takes the same arguments, in order to execute on call. + * @param paths a path for a directory, or an array of them. + * @param options an `IFsCreateDeleteOptions` object. + * @returns An asynchronous function -hence, calling `mkdir` won't have any effect until the returned function is called. + */ +function mkdir( + paths: TSource, + options?: IFsCreateDeleteOptions +): () => Promise { + return async () => { + return trunk( + typeof paths === 'function' ? await paths() : await paths, + options + ); + }; +} diff --git a/src/public/fs/mkdir/mkdir.ts b/src/public/fs/mkdir/mkdir.ts new file mode 100644 index 0000000..75c1bd0 --- /dev/null +++ b/src/public/fs/mkdir/mkdir.ts @@ -0,0 +1,63 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { absolute, exists } from '~/utils/file'; +import confirm from '~/utils/confirm'; +import { parallel } from 'promist'; +import logger from '~/utils/logger'; +import chalk from 'chalk'; +import { IFsCreateDeleteOptions } from '../types'; + +export default async function mkdir( + paths: string | string[], + options: IFsCreateDeleteOptions = {} +): Promise { + const cwd = process.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 && existingPaths.length) { + throw Error(`Directory already exists: ${relatives.existing[0]}`); + } + + // eslint-disable-next-line no-console + (options.confirm ? console.log : logger.debug)( + chalk.bold.yellow( + relatives.existing.length + ? 'Directories to create' + : 'No directories to create' + ) + + (relatives.existing.length + ? `\n Existing paths: "${relatives.existing.join('", "')}"` + : '') + + (relatives.nonExisting.length + ? `\n Non existing paths: "${relatives.nonExisting.join('", "')}"` + : '') + ); + + if (!nonExistingPaths.length) { + logger.info( + `Create skipped: "${relatives.existing + .concat(relatives.nonExisting) + .join('", "')}"` + ); + return; + } + if (!(await confirm('Create?', options))) return; + + await parallel.each(nonExistingPaths, async (absolute, i) => { + await fs.ensureDir(absolute); + + const relative = relatives.nonExisting[i]; + logger.debug(`Created: ${relative}`); + }); + logger.info(`Created: "${relatives.nonExisting.join('", "')}"`); +} diff --git a/src/public/fs/move/index.ts b/src/public/fs/move/index.ts new file mode 100644 index 0000000..3ba267e --- /dev/null +++ b/src/public/fs/move/index.ts @@ -0,0 +1,24 @@ +import { TSource, IFsUpdateOptions } from '../types'; +import expose from '~/utils/expose'; +import trunk from './move'; + +export default expose(move); + +/** + * Move files or directories. If an array of paths is passed as `src`, `dest` will be expected to be a directory. + * It is an *exposed* function: call `move.fn()`, which takes the same arguments, in order to execute on call. + * @returns An asynchronous function -hence, calling `move` won't have any effect until the returned function is called. + */ +function move( + src: TSource, + dest: string, + options?: IFsUpdateOptions +): () => Promise { + return async () => { + return trunk( + typeof src === 'function' ? await src() : await src, + dest, + options + ); + }; +} diff --git a/src/public/fs/move.ts b/src/public/fs/move/move.ts similarity index 54% rename from src/public/fs/move.ts rename to src/public/fs/move/move.ts index df50450..70a65a4 100644 --- a/src/public/fs/move.ts +++ b/src/public/fs/move/move.ts @@ -1,51 +1,38 @@ import path from 'path'; import fs from 'fs-extra'; import { absolute, exists } from '~/utils/file'; -import { IFsUpdateOptions, TSource } from './types'; -import expose from '~/utils/expose'; +import { IFsUpdateOptions } from '../types'; import confirm from '~/utils/confirm'; import logger from '~/utils/logger'; -export default expose(move); - -/** - * Move files or directories. If an array of paths is passed as `src`, `dest` will be expected to be a directory. - * It is an *exposed* function: call `move.fn()`, which takes the same arguments, in order to execute on call. - * @returns An asynchronous function -hence, calling `move` won't have any effect until the returned function is called. - */ -function move( - src: TSource, +export default async function move( + src: string | string[], dest: string, options: IFsUpdateOptions = {} -): () => Promise { - return async () => { - src = typeof src === 'function' ? await src() : await src; +): Promise { + options = Object.assign({ overwrite: true }, options); - if (Array.isArray(src)) { - // Check dest is a folder - if (await exists(dest)) { - const stat = await fs.stat(dest); - if (!stat.isDirectory()) { - throw Error('Destination must be a folder for an array of sources'); - } + if (Array.isArray(src)) { + // Check dest is a folder + if (await exists(dest)) { + const stat = await fs.stat(dest); + if (!stat.isDirectory()) { + throw Error('Destination must be a folder for an array of sources'); } - for (let source of src) { - await trunk(source, path.join(dest, path.parse(source).base), options); - } - } else { - await trunk(src, dest, options); } - }; + for (let source of src) { + await each(source, path.join(dest, path.parse(source).base), options); + } + } else { + await each(src, dest, options); + } } -/** @hidden */ -export async function trunk( +export async function each( src: string, dest: string, options: IFsUpdateOptions ): Promise { - options = Object.assign({ overwrite: true }, options); - const cwd = process.cwd(); src = absolute({ path: src, cwd }); dest = absolute({ path: dest, cwd }); diff --git a/src/public/fs/read.ts b/src/public/fs/read/index.ts similarity index 57% rename from src/public/fs/read.ts rename to src/public/fs/read/index.ts index 889f5a5..1d2abc4 100644 --- a/src/public/fs/read.ts +++ b/src/public/fs/read/index.ts @@ -1,8 +1,7 @@ -import fs from 'fs-extra'; -import { exists, absolute } from '~/utils/file'; import expose from '~/utils/expose'; -import { IFsReadOptions } from './types'; import { TScript } from '~/types'; +import { TSource, IFsReadOptions } from '../types'; +import trunk from './read'; export default expose(read); @@ -12,16 +11,15 @@ export default expose(read); * @returns An asynchronous function -hence, calling `read` won't have any effect until the returned function is called. */ function read( - file: string, + file: TSource, fn: (raw?: string) => TScript, - options: IFsReadOptions = {} + options?: IFsReadOptions ): () => Promise { return async () => { - const cwd = process.cwd(); - file = absolute({ path: file, cwd }); - const doesExist = await exists(file, { fail: options.fail }); - const raw = doesExist ? await fs.readFile(file).then(String) : undefined; - - return fn(raw); + return trunk( + typeof file === 'function' ? await file() : await file, + fn, + options + ); }; } diff --git a/src/public/fs/read/read.ts b/src/public/fs/read/read.ts new file mode 100644 index 0000000..c09bfc3 --- /dev/null +++ b/src/public/fs/read/read.ts @@ -0,0 +1,27 @@ +import fs from 'fs-extra'; +import { exists, absolute } from '~/utils/file'; +import { IFsReadOptions } from '../types'; +import { TScript } from '~/types'; + +export default async function read( + file: string | string[], + fn: (raw?: string) => TScript, + options: IFsReadOptions = {} +): Promise { + return Array.isArray(file) + ? Promise.all(file.map((item) => each(item, fn, options))) + : each(file, fn, options); +} + +export async function each( + file: string, + fn: (raw?: string) => TScript, + options: IFsReadOptions +): Promise { + const cwd = process.cwd(); + file = absolute({ path: file, cwd }); + const doesExist = await exists(file, { fail: options.fail }); + const raw = doesExist ? await fs.readFile(file).then(String) : undefined; + + return fn(raw); +} diff --git a/src/public/fs/remove.ts b/src/public/fs/remove.ts deleted file mode 100644 index b27bf9d..0000000 --- a/src/public/fs/remove.ts +++ /dev/null @@ -1,73 +0,0 @@ -import path from 'path'; -import fs from 'fs-extra'; -import { absolute, exists } from '~/utils/file'; -import confirm from '~/utils/confirm'; -import { parallel } from 'promist'; -import logger from '~/utils/logger'; -import chalk from 'chalk'; -import expose from '~/utils/expose'; -import { IFsCreateDeleteOptions, TSource } from './types'; - -export default expose(remove); -/** - * Removes a file, a directory -recursively-, or an array of them. - * It is an *exposed* function: call `remove.fn()`, which takes the same arguments, in order to execute on call. - * @param paths a path for a file or directory, or an array of them. - * @param options an `IFsCreateDeleteOptions` object. - * @returns An asynchronous function -hence, calling `remove` won't have any effect until the returned function is called. - */ -function remove( - paths: TSource, - options: IFsCreateDeleteOptions = {} -): () => Promise { - return async () => { - const cwd = process.cwd(); - paths = typeof paths === 'function' ? await paths() : await paths; - 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) { - logger.info( - `Remove skipped: "${relatives.existing - .concat(relatives.nonExisting) - .join('", "')}"` - ); - return; - } - if (!(await confirm('Remove?', options))) return; - - await parallel.each(existingPaths, async (absolute, i) => { - await fs.remove(absolute); - - const relative = relatives.existing[i]; - logger.debug(`Removed: ${relative}`); - }); - logger.info(`Removed: "${relatives.existing.join('", "')}"`); - }; -} diff --git a/src/public/fs/remove/index.ts b/src/public/fs/remove/index.ts new file mode 100644 index 0000000..18402fd --- /dev/null +++ b/src/public/fs/remove/index.ts @@ -0,0 +1,24 @@ +import expose from '~/utils/expose'; +import { IFsCreateDeleteOptions, TSource } from '../types'; +import trunk from './remove'; + +export default expose(remove); + +/** + * Removes a file, a directory -recursively-, or an array of them. + * It is an *exposed* function: call `remove.fn()`, which takes the same arguments, in order to execute on call. + * @param paths a path for a file or directory, or an array of them. + * @param options an `IFsCreateDeleteOptions` object. + * @returns An asynchronous function -hence, calling `remove` won't have any effect until the returned function is called. + */ +function remove( + paths: TSource, + options?: IFsCreateDeleteOptions +): () => Promise { + return async () => { + return trunk( + typeof paths === 'function' ? await paths() : await paths, + options + ); + }; +} diff --git a/src/public/fs/remove/remove.ts b/src/public/fs/remove/remove.ts new file mode 100644 index 0000000..916ef55 --- /dev/null +++ b/src/public/fs/remove/remove.ts @@ -0,0 +1,61 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { absolute, exists } from '~/utils/file'; +import confirm from '~/utils/confirm'; +import { parallel } from 'promist'; +import logger from '~/utils/logger'; +import chalk from 'chalk'; +import { IFsCreateDeleteOptions } from '../types'; + +export default async function remove( + paths: string | string[], + options: IFsCreateDeleteOptions = {} +): Promise { + const cwd = process.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) { + logger.info( + `Remove skipped: "${relatives.existing + .concat(relatives.nonExisting) + .join('", "')}"` + ); + return; + } + if (!(await confirm('Remove?', options))) return; + + await parallel.each(existingPaths, async (absolute, i) => { + await fs.remove(absolute); + + const relative = relatives.existing[i]; + logger.debug(`Removed: ${relative}`); + }); + logger.info(`Removed: "${relatives.existing.join('", "')}"`); +} diff --git a/src/public/fs/rw.ts b/src/public/fs/rw.ts deleted file mode 100644 index bcf0d16..0000000 --- a/src/public/fs/rw.ts +++ /dev/null @@ -1,51 +0,0 @@ -import path from 'path'; -import fs from 'fs-extra'; -import { exists, absolute } from '~/utils/file'; -import expose from '~/utils/expose'; -import confirm from '~/utils/confirm'; -import { IFsUpdateOptions } from './types'; -import logger from '~/utils/logger'; -import { open } from '~/utils/errors'; - -export default expose(rw); - -/** - * Reads a `file` and passes it as an argument to a callback `fn`. If the callback returns other than `undefined`, **`file` will be overwritten** with its contents. `file` can be relative to the project's directory. - * It is an *exposed* function: call `rw.fn()`, which takes the same arguments, in order to execute on call. - * @returns An asynchronous function -hence, calling `rw` won't have any effect until the returned function is called. - */ -function rw( - file: string, - fn: (raw?: string) => string | void | Promise, - options: IFsUpdateOptions = {} -): () => Promise { - return async () => { - options = Object.assign({ overwrite: true }, options); - - const cwd = process.cwd(); - file = absolute({ path: file, cwd }); - const relative = './' + path.relative(cwd, file); - - const doesExist = await exists(file, { fail: options.fail }); - - const raw = doesExist ? await fs.readFile(file).then(String) : undefined; - - let response: string | void; - try { - response = await fn(raw); - } catch (e) { - throw open(e); - } - - if (response === undefined || (doesExist && !options.overwrite)) { - logger.info(`Write skipped: ${relative}`); - return; - } - - if (!(await confirm(`Write "${relative}"?`, options))) return; - - await fs.ensureDir(path.parse(file).dir); - await fs.writeFile(file, String(response)); - logger.info(`Written: ${relative}`); - }; -} diff --git a/src/public/fs/rw/index.ts b/src/public/fs/rw/index.ts new file mode 100644 index 0000000..e1dfc64 --- /dev/null +++ b/src/public/fs/rw/index.ts @@ -0,0 +1,24 @@ +import expose from '~/utils/expose'; +import { IFsUpdateOptions, TSource, TContentFn } from '../types'; +import trunk from './rw'; + +export default expose(rw); + +/** + * Reads a `file` and passes it as an argument to a callback `fn`. If the callback returns other than `undefined`, **`file` will be overwritten** with its contents. `file` can be relative to the project's directory. + * It is an *exposed* function: call `rw.fn()`, which takes the same arguments, in order to execute on call. + * @returns An asynchronous function -hence, calling `rw` won't have any effect until the returned function is called. + */ +function rw( + file: TSource, + fn: TContentFn, + options?: IFsUpdateOptions +): () => Promise { + return async () => { + return trunk( + typeof file === 'function' ? await file() : await file, + fn, + options + ); + }; +} diff --git a/src/public/fs/rw/rw.ts b/src/public/fs/rw/rw.ts new file mode 100644 index 0000000..87bb46c --- /dev/null +++ b/src/public/fs/rw/rw.ts @@ -0,0 +1,51 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { exists, absolute } from '~/utils/file'; +import confirm from '~/utils/confirm'; +import { IFsUpdateOptions, TContentFn } from '../types'; +import logger from '~/utils/logger'; +import { open } from '~/utils/errors'; + +export default async function rw( + file: string | string[], + fn: TContentFn, + options: IFsUpdateOptions = {} +): Promise { + options = Object.assign({ overwrite: true }, options); + + Array.isArray(file) + ? await Promise.all(file.map((item) => each(item, fn, options))) + : await each(file, fn, options); +} + +export async function each( + file: string, + fn: TContentFn, + options: IFsUpdateOptions +): Promise { + const cwd = process.cwd(); + file = absolute({ path: file, cwd }); + const relative = './' + path.relative(cwd, file); + + const doesExist = await exists(file, { fail: options.fail }); + + const raw = doesExist ? await fs.readFile(file).then(String) : undefined; + + let response: string | void; + try { + response = await fn(file, raw); + } catch (e) { + throw open(e); + } + + if (response === undefined || (doesExist && !options.overwrite)) { + logger.info(`Write skipped: ${relative}`); + return; + } + + if (!(await confirm(`Write "${relative}"?`, options))) return; + + await fs.ensureDir(path.parse(file).dir); + await fs.writeFile(file, String(response)); + logger.info(`Written: ${relative}`); +} diff --git a/src/public/fs/types.ts b/src/public/fs/types.ts index a3c79a9..b6a79b5 100644 --- a/src/public/fs/types.ts +++ b/src/public/fs/types.ts @@ -4,6 +4,15 @@ export type TSource = | Promise | (() => string[] | Promise); +export type TCopyFilterFn = + | ((src: string, dest: string) => boolean) + | ((src: string, dest: string) => Promise); + +export type TContentFn = ( + file: string, + raw?: string +) => string | void | Promise; + /** * Options taken by read *fs* functions. */ diff --git a/src/public/fs/write.ts b/src/public/fs/write.ts deleted file mode 100644 index cfa4dcc..0000000 --- a/src/public/fs/write.ts +++ /dev/null @@ -1,69 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; -import expose, { TExposedOverload } from '~/utils/expose'; -import { IFsUpdateOptions } from './types'; -import { exists, absolute } from '~/utils/file'; -import confirm from '~/utils/confirm'; -import logger from '~/utils/logger'; -import { open } from '~/utils/errors'; - -export default expose(write) as TExposedOverload< - typeof write, - | [string] - | [string, string | (() => string | Promise)] - | [string, IFsUpdateOptions] - | [string, string | (() => string | Promise), IFsUpdateOptions] ->; - -function write( - file: string, - raw?: string | (() => string | Promise) -): () => Promise; -function write(file: string, options?: IFsUpdateOptions): () => Promise; -function write( - file: string, - raw: string | (() => string | Promise), - options?: IFsUpdateOptions -): () => Promise; -/** - * Writes a `file` with `raw`. If no `raw` content is passed, it will simply ensure it does exist. - * It is an *exposed* function: call `write.fn()`, which takes the same arguments, in order to execute on call. - * @returns An asynchronous function -hence, calling `write` won't have any effect until the returned function is called. - */ -function write(file: string, ...args: any[]): () => Promise { - return async () => { - const hasRaw = typeof args[0] === 'string' || typeof args[0] === 'function'; - const options: IFsUpdateOptions = Object.assign( - { overwrite: true }, - (hasRaw ? args[1] : args[0]) || {} - ); - let raw: string | (() => string | Promise) = hasRaw ? args[0] : ''; - if (typeof raw === 'function') { - try { - raw = await raw(); - } catch (err) { - throw open(err); - } - } - - const cwd = process.cwd(); - file = absolute({ path: file, cwd }); - const relative = './' + path.relative(process.cwd(), file); - - const doesExist = await exists(file); - if (options.fail && doesExist) { - throw Error(`File already exists: ${relative}`); - } - - if (doesExist && !options.overwrite) { - logger.info(`Write skipped: ${relative}`); - return; - } - - if (!(await confirm(`Write "${relative}"?`, options))) return; - - await fs.ensureDir(path.parse(file).dir); - await fs.writeFile(file, String(raw)); - logger.info(`Written: ${relative}`); - }; -} diff --git a/src/public/fs/write/index.ts b/src/public/fs/write/index.ts new file mode 100644 index 0000000..83f9481 --- /dev/null +++ b/src/public/fs/write/index.ts @@ -0,0 +1,38 @@ +import expose, { TExposedOverload } from '~/utils/expose'; +import { IFsUpdateOptions, TContentFn, TSource } from '../types'; +import trunk from './write'; + +export default expose(write) as TExposedOverload< + typeof write, + | [TSource] + | [TSource, undefined | string | TContentFn] + | [TSource, IFsUpdateOptions] + | [TSource, undefined | string | TContentFn, IFsUpdateOptions] +>; + +function write(file: TSource, raw?: string | TContentFn): () => Promise; +function write(file: TSource, options?: IFsUpdateOptions): () => Promise; +function write( + file: TSource, + raw?: string | TContentFn, + options?: IFsUpdateOptions +): () => Promise; +/** + * Writes a `file` with `raw`. If no `raw` content is passed, it will overwrite with no content. + * It is an *exposed* function: call `write.fn()`, which takes the same arguments, in order to execute on call. + * @returns An asynchronous function -hence, calling `write` won't have any effect until the returned function is called. + */ +function write(file: TSource, ...args: any[]): () => Promise { + return async () => { + const hasRaw = + typeof args[0] === 'string' || + typeof args[0] === 'function' || + typeof args[0] === 'undefined'; + + return trunk( + typeof file === 'function' ? await file() : await file, + hasRaw ? args[0] : undefined, + hasRaw ? args[1] : args[0] + ); + }; +} diff --git a/src/public/fs/write/write.ts b/src/public/fs/write/write.ts new file mode 100644 index 0000000..fc4076a --- /dev/null +++ b/src/public/fs/write/write.ts @@ -0,0 +1,53 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { IFsUpdateOptions, TContentFn } from '../types'; +import { exists, absolute } from '~/utils/file'; +import confirm from '~/utils/confirm'; +import logger from '~/utils/logger'; +import { open } from '~/utils/errors'; + +export default async function write( + file: string | string[], + raw?: string | TContentFn, + options: IFsUpdateOptions = {} +): Promise { + options = Object.assign({ overwrite: true }, options); + + Array.isArray(file) + ? await Promise.all(file.map((item) => each(item, raw, options))) + : await each(file, raw, options); +} + +export async function each( + file: string, + raw: void | string | TContentFn, + options: IFsUpdateOptions +): Promise { + const cwd = process.cwd(); + file = absolute({ path: file, cwd }); + const relative = './' + path.relative(process.cwd(), file); + + if (typeof raw === 'function') { + try { + raw = await raw(file); + } catch (err) { + throw open(err); + } + } + + const doesExist = await exists(file); + if (options.fail && doesExist) { + throw Error(`File already exists: ${relative}`); + } + + if (doesExist && !options.overwrite) { + logger.info(`Write skipped: ${relative}`); + return; + } + + if (!(await confirm(`Write "${relative}"?`, options))) return; + + await fs.ensureDir(path.parse(file).dir); + await fs.writeFile(file, raw ? String(raw) : ''); + logger.info(`Written: ${relative}`); +}