import * as fs from 'fs' import * as path from 'path' import ora from 'ora' import chalk from 'chalk' import { uid } from 'uid' import removeDir from 'del' /** * Log helper * @returns - {start, info, success, error} functions */ export const logger = () => { const logInfo = chalk.bold.blue const logSuccess = chalk.bold.green const logError = chalk.bold.red const spinner = ora(`${logInfo('Process')}`) return { spinner, start: (value: string) => spinner.start(logInfo(value)), info: (value: string) => spinner.info(logInfo(value)), success: (value: string) => spinner.succeed(logSuccess(value)), error: (value: string) => spinner.warn(logError(value)), } } /** * Check extension file string by array * @param file * @param exts * @returns regex */ export const checkFileExt = (file: string, exts: string[]) => { const regex = new RegExp(`^.*(\.({|{[a-zA-Z,]*,)|\.)(${exts.join('|')})(,[,a-zA-Z]*}|}|)$`) return regex.test(file) } /** * Copy files from input dir to output dir * @param pathIn * @param pathOut * @param exts */ export const copyFiles = (pathIn: string, pathOut: string, exts?: string[], includes?: string) => { if (!fs.existsSync(pathOut)) fs.mkdirSync(pathOut) fs.readdirSync(pathIn).forEach((file: string) => { if ((exts && checkFileExt(file, exts)) || !exts) { if ((includes && file.includes(includes)) || !includes) { if (fs.lstatSync(path.resolve(pathIn, file)).isFile()) { fs.copyFileSync(path.resolve(pathIn, file), path.resolve(pathOut, file)) } } } }) } /** * Create dir if not exist, remove file on output dir. * @param pathOut * @param exts * @param remove */ export const prepareOutputDir = (pathOut: string, exts: string[], remove?: boolean) => { if (!fs.existsSync(pathOut)) fs.mkdirSync(pathOut) if (remove) { fs.readdirSync(pathOut).forEach((file: string) => { if ((exts && checkFileExt(file, exts)) || !exts) { if (fs.lstatSync(path.resolve(pathOut, file)).isFile()) { fs.unlinkSync(path.resolve(pathOut, file)) } } }) } } /** * Get UID * @param size * @returns */ export const getUID = (size?: number) => '%temp%' + uid(size || 10) /** * Pipeline * @param [...fns] * @returns (props, options, callback) => Promise */ export const pipeline = (...fns: any) => (props: TypeToolProp, options?: any, callback?: any) => fns.reduce( ( prevFn: Promise, nextFn: (props: TypeToolProp, options?: any, callback?: any) => any, index: string | number, ) => prevFn.then((e: any) => nextFn(e, options && options[index], callback && callback[index])), Promise.resolve(props), ) /** * Watch pathIn changes and reload pipeline * @param pipeline * @param props * @param options * @param callback */ export const watchPipeline = ( pipeline: (props: TypeToolProp, options: any, callback: any) => Promise, props: TypeToolProp, options: any, callback: any, ) => { let watcher = false pipeline(props, options, callback) fs.watch(props.pathIn || props.pathOut, (_eventType, filename: any) => { const dirPath = path.resolve(props.pathIn || props.pathOut, filename) if (fs.existsSync(dirPath) && fs.statSync(dirPath).isFile()) { if (!watcher && filename) { setTimeout(() => { pipeline(props, options, callback) .then(() => { watcher = false }) .catch(e => { console.log(e) }) }, 1000) } watcher = true } }) } export const changeExtension = (file: string, extension?: string) => { if (!extension) return file const basename = path.basename(file, path.extname(file)) return path.join(path.dirname(file), basename + extension) } export interface TypeToolProp { pathIn?: string pathOut: string copy?: boolean verbose?: boolean includes?: string } export interface TypeLogger { start: ora.Ora info: ora.Ora success: ora.Ora error: ora.Ora } export interface TypeProps { exts?: string[] name?: string removePrevFile?: boolean [key: string]: any } export interface TypeOnEveryFile { pathIn?: string pathOut: string file: string props: any options: any wrapperProps: TypeProps log: any //TypeLogger next: (value: void | PromiseLike) => void } export interface TypeOnBody { pathIn?: string pathOut: string props: any options: any wrapperProps: TypeProps log: any //TypeLogger } export interface TypeToolOptions { copy?: boolean verbose?: boolean excludes?: string } export const wrapper = ( onEveryFile?: ({ pathIn, pathOut, file, props, options, wrapperProps }: TypeOnEveryFile) => void, onBody?: ({ pathIn, pathOut, props, options, wrapperProps }: TypeOnBody) => Promise | void, props?: TypeProps, ) => (_props: TypeToolProp, _options?: TypeToolOptions) => new Promise(resolve => { const uid = getUID() const pathIn = _props.pathIn || _props.pathOut const { pathOut, copy, verbose, includes } = _props const { excludes } = _options || {} const pathTemp = path.resolve(pathIn, uid) const { removePrevFile } = props || {} const exts = props?.exts || [] const name = (props && props.name) || 'Undefined' const log = logger() log.start(name) if (copy) copyFiles(pathIn, pathOut) copyFiles(pathIn, pathTemp, exts, includes) prepareOutputDir(pathOut, exts, removePrevFile) const removeDirHelper = () => { removeDir(pathTemp, { force: true }).then(() => { log.success(`${name} \n`) resolve({ pathOut }) }) } const innerLog = (value: string) => { if (!verbose) return log.info(value) } const promiseEveryFile = Promise.all( fs.readdirSync(pathTemp).map((file: string) => { if (excludes && file.includes(excludes)) return Promise.resolve() return new Promise(resolve => { if (checkFileExt(file, exts)) { if (onEveryFile) { onEveryFile({ pathIn: pathTemp, pathOut, file, props: _props, options: _options, wrapperProps: props || {}, next: resolve, log: innerLog, }) } else { resolve() } } else { resolve() } }) }), ) promiseEveryFile .then(() => { if (onBody) { const promiseBody = onBody({ pathIn: pathTemp, pathOut, props: _props, options: _options, wrapperProps: props || {}, log: innerLog, }) if (promiseBody) { promiseBody .then(() => { removeDirHelper() }) .catch(e => { log.error(e) }) } else { removeDirHelper() } } else { removeDirHelper() } }) .catch(e => { log.error(e) }) })