UNPKG

chain-able-find

Version:

find files synchronously, easily, with a chainable interface

369 lines (323 loc) 9.02 kB
const {readdirSync, lstatSync} = require('fs') const Chain = require('chain-able') const doesInclude = require('./modules/does-include') const globToRegExp = require('./modules/glob-to-regex') let cache = {} /** * @see flipfile/isDir * @since 0.0.1 * @desc uses lstatSync * @param {string} path * @return {boolean} title says it all */ function isDir(path) { var stat = lstatSync(path) return stat && stat.isDirectory() } /** * @type {Chain} */ class Christopher extends Chain { /** * @param {Chainable | *} [parent=null] */ constructor(parent = null) { super(parent) // alias this.matchFiles = this.test.bind(this) this.walkin = this.find.bind(this) /* prettier-ignore */ this .extend([ 'recursive', 'cwd', 'dir', 'ignoreDirs', 'includeFiles', 'includeDirs', // @TODO 'entry', // @TODO 'maxDepth', 'depth', 'sync', 'abs', 'cache', ]) .maxDepth(1000000) .recursive(true) .sync(true) .depth(0) .abs(false) .cache(true) } /** * @since 0.0.1 * @desc static factory * @param {Chainable | *} [parent=null] * @return {Christopher} @chainable */ static init(parent) { return new Christopher(parent) } /** * @alias matchFiles * @since 0.0.1 * @desc test these files with globs * @param {Array<string>} globs title says it all * @return {Christopher} @chainable */ test(globs) { // if string, set as arr if (typeof globs === 'string') { globs = [globs] } const regexes = globs.map(glob => { let negated = false if (glob.includes('!') === true) { negated = true // glob = glob.replace(/[!]/g, '') } return { // , {globstar: true} regex: globToRegExp(glob), negated, } }) const matcher = file => { for (let r = 0; r < regexes.length; r++) { const {negated, regex} = regexes[r] if (negated === true) { if (regex.test(file) === true) { return false } } else if (regex.test(file) === true) { return true } } return false } return this.set('matchFiles', matcher).set('matchGlobs', globs) } /** * @since 0.0.1 * @desc when matchFiles, checks the regexes to re-filter * @param {string} paths filter them all if needed * @return {string} */ filter(paths) { if (this.has('matchFiles') === false) return paths const debug = this.get('debug') || true const matcher = this.get('matchFiles') const satisfied = [] for (let i = 0; i < paths.length; i++) { const found = paths[i] if (matcher(paths[i]) === true) { satisfied.push(found) } else if (debug === true) { // console.log('not satisfied: ', found) } } return satisfied } /** * @protected * @since 0.0.1 * @see does-include * @desc check if this.ignoreDirs has it * @param {string} path chickidy check * @return {boolean} title says it all */ aintIgnored(path) { if (this.has('ignoreDirs') === false) return true const ignore = this.get('ignoreDirs') return doesInclude(path, ignore) === false } /** * @protected * @TODO @debug depth, findings, dir, satisfied, ignored * @since 0.0.1 * @return {Array<string>} directory listing */ readDir() { const dir = this.get('dir') const depth = this.get('depth') const findings = readdirSync(dir) const satisfied = [] for (let i = 0; i < findings.length; i++) { const found = findings[i] if (this.aintIgnored(found) === true) { satisfied.push(found) } } this.depth(depth + 1) return satisfied } /** * @protected * @desc terry fox the paths, #runaround * checks if depth is below max * checks ignored * walks the path * pushes satisfied to array * * @since 0.0.1 * @see does-include * @see this.ignoreDirs, * @see this.maxDepth, * @see this.depth, * @see this.dir * @param {string} path path to look through * @param {string} results push to this array if satisfied * @return {void} */ terry(path, results) { if (this.get('maxDepth') <= this.get('depth')) return const ignore = this.get('ignoreDirs') const findings = this.dir(path).walk() for (let ii = 0; ii < findings.length; ii++) { const found = findings[ii] if (ignore === undefined || doesInclude(found, ignore) === false) { results.push(found) } } } /** * @protected * @desc walk down [recursively], filter results * @since 0.0.1 * @return {Array<string>} paths found satisfying */ walk() { const dir = this.get('dir') const dirSlash = dir + '/' const recursive = this.get('recursive') const list = this.readDir() const results = [] // console.log('calling walk ', dir) for (let i = 0; i < list.length; i++) { const file = list[i] const path = dirSlash + file const isDirp = isDir(path) if (recursive === true) { if (isDirp === true) { this.terry(path, results) } else if (this.aintIgnored(path) === true) { results.push(path) } } else if (isDirp === true && this.aintIgnored(path) === true) { results.push(path) } } if (this.get('abs') === false) { return results.map(result => result.replace(dirSlash, '')) } return results } /** * @desc christopher walkin that dir * starts from a dir, * stringifies options for cache, * walks the walk, filters * * @since 0.0.1 * @param {string} [dir=process.cwd()] starting point * @return {Array<string>} results */ found(dir = null) { if (dir === null) dir = process.cwd() const chris = this.dir(dir) const sync = this.get('sync') const cacheEnabled = this.get('cache') // console.log('calling found') let hash = '' if (cacheEnabled === true) { // this takes ~7 microseconds to "hash" hash += 'recursive:' + chris.get('recursive') hash += 'dir:' + chris.get('dir') hash += 'ignoreDirs:' + chris.get('ignoreDirs') hash += 'includeFiles:' + chris.get('includeFiles') hash += 'maxDepth:' + chris.get('maxDepth') hash += 'sync:' + sync hash += 'abs:' + chris.get('abs') hash += 'matchGlobs:' + chris.get('matchGlobs').join(',') if (cache[hash] !== undefined) { if (this.get('debug') === true) { console.log('using cache') } return cache[hash] } } const result = chris.walk() const found = this.filter(result) if (cacheEnabled === true) { cache[hash] = found } // emulate async, for compatibility with promise chains if (sync === false) { return new Promise(presolve => setTimeout(() => presolve(found), 1)) } return found } find(dir = null) { this.set('cwd', dir) this.results = () => this.found(dir) return this } } /** * @desc return a new Christopher, or extract options and return results * @param {Array<string> | string | null} [globs=null] should extract the folders from the globs * @param {Object} options * @return {Christopher | Array<string>} */ function LilBunnyFooFoo(globs = null, options = {}) { const chris = Christopher.init() const isArr = Array.isArray(globs) if (!options.cwd && !options.dir) { options.cwd = process.cwd() options.dir = process.cwd() } // if using a single argument if (globs !== null && typeof globs === 'object' && !isArr) { // console.log('object globs') options = globs // eslint-disable-line globs = options.test // eslint-disable-line } else if (globs === undefined || (typeof globs !== 'string' && !isArr)) { // console.log('no globs, using class') // if passing in no options, return the instance return chris } // console.log('array globs with possible options') chris.test(globs) if (options.sync !== undefined) { chris.sync(options.sync) } if (options.ignoreDirs !== undefined) { chris.ignoreDirs(options.ignoreDirs) } if (options.abs !== undefined) { chris.abs(options.abs) } else if (options.absolute !== undefined) { chris.abs(options.absolute) } if (options.recursive !== undefined) { chris.recursive(options.recursive) } if (options.maxDepth !== undefined) { chris.maxDepth(options.maxDepth) } if (options.cache !== undefined) { chris.cache(options.cache) } return chris.find(options.cwd).results() } LilBunnyFooFoo.init = Christopher.init LilBunnyFooFoo.Christopher = Christopher LilBunnyFooFoo.fn = LilBunnyFooFoo LilBunnyFooFoo.up = () => require('./pkgup') // would want to .get module.exports = LilBunnyFooFoo