├── test ├── fixtures │ ├── a.txt │ ├── b.txt │ ├── c.txt │ ├── bar │ │ ├── x.txt │ │ ├── y.txt │ │ └── z.txt │ └── foo │ │ ├── foo │ │ └── foo │ │ │ ├── one.txt │ │ │ ├── three.txt │ │ │ ├── two.txt │ │ │ └── foo │ │ │ ├── alpha.txt │ │ │ ├── beta.txt │ │ │ ├── gamma.txt │ │ │ └── foo │ │ │ ├── x.txt │ │ │ ├── y.txt │ │ │ └── z.txt │ │ ├── x.txt │ │ ├── y.txt │ │ └── z.txt ├── .eslintrc.json ├── readdir.sync.js └── readdir.js ├── .npmrc ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── index.js ├── .editorconfig ├── bench ├── fixtures │ └── package.json ├── package.json ├── bench.js └── index.js ├── .gitignore ├── examples └── readdir.js ├── lib ├── utils.js ├── sync.js └── async.js ├── LICENSE ├── package.json ├── .eslintrc.json ├── .verb.md └── README.md /test/fixtures/a.txt: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /test/fixtures/b.txt: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /test/fixtures/c.txt: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test/fixtures/bar/x.txt: -------------------------------------------------------------------------------- 1 | bar-aaa 2 | -------------------------------------------------------------------------------- /test/fixtures/bar/y.txt: -------------------------------------------------------------------------------- 1 | bar-aaa 2 | -------------------------------------------------------------------------------- /test/fixtures/bar/z.txt: -------------------------------------------------------------------------------- 1 | bar-aaa 2 | -------------------------------------------------------------------------------- /test/fixtures/foo/foo/foo/one.txt: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /test/fixtures/foo/foo/foo/three.txt: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /test/fixtures/foo/foo/foo/two.txt: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /test/fixtures/foo/x.txt: -------------------------------------------------------------------------------- 1 | foo-aaa 2 | -------------------------------------------------------------------------------- /test/fixtures/foo/y.txt: -------------------------------------------------------------------------------- 1 | foo-aaa 2 | -------------------------------------------------------------------------------- /test/fixtures/foo/z.txt: -------------------------------------------------------------------------------- 1 | foo-aaa 2 | -------------------------------------------------------------------------------- /test/fixtures/foo/foo/foo/foo/alpha.txt: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /test/fixtures/foo/foo/foo/foo/beta.txt: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /test/fixtures/foo/foo/foo/foo/gamma.txt: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /test/fixtures/foo/foo/foo/foo/foo/x.txt: -------------------------------------------------------------------------------- 1 | foo-nested-xxx 2 | -------------------------------------------------------------------------------- /test/fixtures/foo/foo/foo/foo/foo/y.txt: -------------------------------------------------------------------------------- 1 | foo-nested-yyy 2 | -------------------------------------------------------------------------------- /test/fixtures/foo/foo/foo/foo/foo/z.txt: -------------------------------------------------------------------------------- 1 | foo-nested-zzz 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [jonschlinkert, doowb] 4 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../.eslintrc.json" 4 | ], 5 | "env": { 6 | "mocha": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const readdir = require('./lib/async'); 4 | readdir.sync = require('./lib/sync'); 5 | 6 | module.exports = readdir; 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /bench/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmarks-fixtures", 3 | "description": "benchmarks-fixtures - extremely bloated monolithic lib to demonstrate readdir perf", 4 | "version": "0.0.0", 5 | "private": true, 6 | "dependencies": { 7 | "gatsby": "^3.3.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # always ignore files 2 | .DS_Store 3 | 4 | # npm 5 | node_modules 6 | npm-debug.log 7 | package-lock.json 8 | 9 | # yarn 10 | yarn.lock 11 | yarn-error.log 12 | 13 | # text editors 14 | .vscode 15 | *.sublime-* 16 | *.code-* 17 | 18 | # CI/CD 19 | .nyc_output 20 | coverage 21 | 22 | # custom 23 | attempts 24 | tmp 25 | vendor 26 | -------------------------------------------------------------------------------- /examples/readdir.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const start = Date.now(); 4 | process.on('exit', () => console.log(`Time: ${Date.now() - start}ms`)); 5 | 6 | const path = require('path'); 7 | const readdir = require('..'); 8 | 9 | const isIgnoredDir = dirent => { 10 | return false; 11 | // return ['tmp', 'vendor', '.git', 'node_modules'].includes(dirent.name); 12 | }; 13 | 14 | readdir(path.join(__dirname, '..'), { isIgnoredDir, recursive: true }) 15 | .then(console.log) 16 | .catch(console.error); 17 | -------------------------------------------------------------------------------- /bench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bench", 3 | "description": "Benchmarks for @folder/readdir", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "bench": "node index.js" 8 | }, 9 | "dependencies": { 10 | "@mrmlnc/readdir-enhanced": "^2.2.1", 11 | "composer": "^4.1.0", 12 | "fdir": "^5.0.0", 13 | "readdirp": "^3.0.0", 14 | "rimraf": "^3.0.2", 15 | "systeminformation": "^5.6.12" 16 | }, 17 | "devDependencies": { 18 | "ansi-colors": "^4.1.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: Node.js ${{ matrix.node-version }} @ ${{ matrix.os }} 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macos-latest] 12 | node-version: [10, 12, 13, 14, 15] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm install 20 | - run: npm test 21 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | 5 | exports.matcher = (value, options) => { 6 | if (Array.isArray(value)) { 7 | const matchers = value.map(val => exports.matcher(val, options)); 8 | return (file, parent) => matchers.some(fn => fn(file, parent)); 9 | } 10 | 11 | if (typeof value === 'function') { 12 | return (file, parent) => value(file, parent); 13 | } 14 | 15 | if (typeof value === 'string') { 16 | return file => file.name === value; 17 | } 18 | 19 | if (value instanceof RegExp) { 20 | return file => value.test(file.relative || file.path); 21 | } 22 | 23 | throw new TypeError(`Invalid matcher value: ${util.inspect(value)}`); 24 | }; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present, Jon Schlinkert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@folder/readdir", 3 | "version": "3.1.0", 4 | "description": "Recursively read a directory, blazing fast.", 5 | "license": "MIT", 6 | "repository": "folder/readdir", 7 | "homepage": "https://github.com/folder/readdir", 8 | "bugs": "https://github.com/folder/readdir/issues", 9 | "author": "Jon Schlinkert (https://github.com/jonschlinkert)", 10 | "funding": [ 11 | "https://github.com/sponsors/jonschlinkert", 12 | "https://paypal.me/jonathanschlinkert", 13 | "https://jonschlinkert.dev/sponsor" 14 | ], 15 | "files": [ 16 | "index.js", 17 | "lib" 18 | ], 19 | "main": "index.js", 20 | "engines": { 21 | "node": ">=10" 22 | }, 23 | "scripts": { 24 | "test": "mocha" 25 | }, 26 | "devDependencies": { 27 | "gulp-format-md": "^2.0.0", 28 | "mocha": "^8.3.2", 29 | "rimraf": "^3.0.2", 30 | "write": "^2.0.0" 31 | }, 32 | "keywords": [ 33 | "absolute", 34 | "async", 35 | "await", 36 | "commonjs", 37 | "crawl", 38 | "crawler", 39 | "deep", 40 | "dir", 41 | "directories", 42 | "directory", 43 | "dirent", 44 | "dirname", 45 | "dirs", 46 | "es6", 47 | "fast", 48 | "fdir", 49 | "file system", 50 | "file", 51 | "files", 52 | "filesystem", 53 | "filter", 54 | "find", 55 | "folder", 56 | "folders", 57 | "fs", 58 | "io", 59 | "list", 60 | "listing", 61 | "ls", 62 | "nested", 63 | "os", 64 | "promise", 65 | "read", 66 | "readdir", 67 | "readdirp", 68 | "readdirrecursive", 69 | "recurse", 70 | "recursive", 71 | "scandir", 72 | "speed", 73 | "stream", 74 | "streams", 75 | "sys", 76 | "tree", 77 | "util", 78 | "walk", 79 | "walker" 80 | ], 81 | "verb": { 82 | "toc": false, 83 | "layout": "default", 84 | "tasks": [ 85 | "readme" 86 | ], 87 | "plugins": [ 88 | "gulp-format-md" 89 | ], 90 | "related": { 91 | "links": [ 92 | "@folder/picomatch", 93 | "@folder/micromatch", 94 | "@folder/dirent", 95 | "@folder/xdg" 96 | ] 97 | }, 98 | "lint": { 99 | "reflinks": true 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /bench/bench.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const colors = require('ansi-colors'); 4 | 5 | const getTime = () => new Date().getTime(); 6 | const format = num => num.toLocaleString().replace(/\.\d+$/, ''); 7 | const cycle = (name, total, avg, finished = false) => { 8 | return process.stdout.write(`\r${name} x ${format(avg)} ops/sec (${format(total).trim()} runs sampled)`); 9 | }; 10 | 11 | class Bench { 12 | constructor(name, options) { 13 | if (typeof name !== 'string') { 14 | options = name; 15 | name = 'benchmarks'; 16 | } 17 | 18 | this.name = name; 19 | this.options = { ...options }; 20 | this.benchmarks = []; 21 | this.longest = 0; 22 | } 23 | 24 | add(name, fn) { 25 | this.longest = Math.max(this.longest, name.length); 26 | this.benchmarks.push({ name, fn }); 27 | return this; 28 | } 29 | 30 | async run(options) { 31 | const opts = { ...this.options, ...options }; 32 | const results = []; 33 | console.log(`# ${this.name}`); 34 | 35 | for (const bench of this.benchmarks) { 36 | if (opts.onRun) opts.onRun(bench); 37 | bench.name = colors.cyan(bench.name.padStart(this.longest + 1, ' ')); 38 | bench.onCycle = opts.onCycle; 39 | results.push(await this.iterate(bench)); 40 | console.log(); 41 | } 42 | 43 | return results; 44 | } 45 | 46 | async iterate(options = {}) { 47 | const bench = { max: 5000, step: 100, ...options }; 48 | 49 | const sec = 1000; 50 | const stop = bench.max; 51 | const step = bench.step; 52 | const secs = bench.secs = (stop / sec); 53 | const start = bench.start = getTime(); 54 | let elapsed = getTime() - start; 55 | let ops = 0; 56 | let last = 0; 57 | 58 | while (elapsed < stop) { 59 | await bench.fn.call(this, bench); 60 | 61 | ops++; 62 | const now = getTime(); 63 | const diff = now - last; 64 | elapsed = now - start; 65 | 66 | if (diff >= step) { 67 | if (bench.onCycle) { 68 | const state = { ops, start, last, elapsed, sec, now, stop, step }; 69 | bench.onCycle(bench, state); 70 | } 71 | cycle(bench.name, ops, ops / (elapsed / sec)); 72 | last = now; 73 | } 74 | } 75 | 76 | cycle(bench.name, ops, ops / (elapsed / sec), true); 77 | bench.secs = secs; 78 | bench.ops = ops; 79 | bench.formatted = format(ops); 80 | return bench; 81 | } 82 | } 83 | 84 | module.exports = options => new Bench(options); 85 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const util = require('util'); 6 | const rimraf = util.promisify(require('rimraf')); 7 | const Composer = require('composer'); 8 | const composer = new Composer(); 9 | const bench = require('./bench'); 10 | 11 | const { fdir: Fdir } = require('fdir'); 12 | const enhanced = require('@mrmlnc/readdir-enhanced'); 13 | const readdirp = require('readdirp'); 14 | const readdir = require('..'); 15 | 16 | const pause = (ms = 1000) => new Promise(res => setTimeout(res, ms)); 17 | 18 | const fdir = (dir, options) => { 19 | return new Fdir() 20 | .withFullPaths() 21 | .withMaxDepth(0) 22 | .withDirs() 23 | .crawl(dir) 24 | .withPromise(); 25 | }; 26 | 27 | const fdirRecursive = (dir, options) => { 28 | return new Fdir() 29 | .withFullPaths() 30 | .withDirs() 31 | .crawl(dir) 32 | .withPromise(); 33 | }; 34 | 35 | const log = files => console.log(files.length); 36 | const fixtures = path.join(__dirname, 'fixtures/node_modules'); 37 | let shouldDelete = false; 38 | 39 | composer.task('recursive-large', () => { 40 | const dir = path.join(__dirname, '..'); 41 | shouldDelete = true; 42 | 43 | return bench('recursive ~57,200 files (just gatsby!)') 44 | .add('fdir', () => fdirRecursive(dir)) 45 | .add('@folder/readdir', () => readdir(dir, { recursive: true })) 46 | .add('readdir-enhanced', () => enhanced.async(dir, { deep: true, basePath: dir })) 47 | .add('readdirp', () => readdirp.promise(dir)) 48 | .run(); 49 | }); 50 | 51 | composer.task('recursive-mid', async () => { 52 | const dir = path.join(__dirname, '..'); 53 | const files = await readdir(dir, { recursive: true }); 54 | 55 | if (shouldDelete || fs.existsSync(fixtures)) { 56 | await rimraf(fixtures, { glob: false }); 57 | await pause(1000); 58 | } 59 | 60 | return bench(`recursive ~${files.length} files`) 61 | .add('fdir', () => fdirRecursive(dir)) 62 | .add('@folder/readdir', () => readdir(dir, { recursive: true })) 63 | .add('readdir-enhanced', () => enhanced.async(dir, { deep: true, basePath: dir })) 64 | .add('readdirp', () => readdirp.promise(dir)) 65 | .run(); 66 | }); 67 | 68 | composer.task('recursive', async () => { 69 | if (shouldDelete || fs.existsSync(fixtures)) { 70 | await rimraf(fixtures, { glob: false }); 71 | await pause(1000); 72 | } 73 | 74 | return bench('recursive ~220 files') 75 | .add('fdir', () => fdirRecursive(__dirname)) 76 | .add('@folder/readdir', () => readdir(__dirname, { recursive: true })) 77 | .add('readdir-enhanced', () => enhanced.async(__dirname, { deep: true, basePath: __dirname })) 78 | .add('readdirp', () => readdirp.promise(__dirname)) 79 | .run(); 80 | }); 81 | 82 | composer.task('single', async () => { 83 | if (shouldDelete || fs.existsSync(fixtures)) { 84 | await rimraf(fixtures, { glob: false }); 85 | await pause(1000); 86 | } 87 | 88 | return bench('single directory (~5-10 files)') 89 | .add('fdir', () => fdir(__dirname)) 90 | .add('@folder/readdir', () => readdir(__dirname)) 91 | .add('readdir-enhanced', () => enhanced.async(__dirname, { basePath: __dirname })) 92 | .add('readdirp', () => readdirp.promise(__dirname, { depth: 1 })) 93 | .run(); 94 | }); 95 | 96 | composer.task('benchmarks', ['recursive-mid', 'recursive', 'single']); 97 | composer.build('benchmarks').catch(console.error); 98 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | 6 | "env": { 7 | "es2021": true, 8 | "node": true 9 | }, 10 | 11 | "parserOptions": { 12 | "ecmaVersion": 12 13 | }, 14 | 15 | "rules": { 16 | "accessor-pairs": 2, 17 | "arrow-parens": [2, "as-needed"], 18 | "arrow-spacing": [2, { "before": true, "after": true }], 19 | "block-spacing": [2, "always"], 20 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 21 | "comma-dangle": [2, "never"], 22 | "comma-spacing": [2, { "before": false, "after": true }], 23 | "comma-style": [2, "last"], 24 | "constructor-super": 2, 25 | "curly": [2, "multi-line"], 26 | "dot-location": [2, "property"], 27 | "eol-last": 2, 28 | "eqeqeq": [2, "allow-null"], 29 | "generator-star-spacing": [2, { "before": true, "after": true }], 30 | "handle-callback-err": [2, "^(err|error)$"], 31 | "indent": [2, 2, { "SwitchCase": 1 }], 32 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 33 | "keyword-spacing": [2, { "before": true, "after": true }], 34 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 35 | "new-parens": 2, 36 | "no-array-constructor": 2, 37 | "no-caller": 2, 38 | "no-class-assign": 2, 39 | "no-cond-assign": 2, 40 | "no-console": 0, 41 | "no-const-assign": 2, 42 | "no-control-regex": 2, 43 | "no-debugger": 2, 44 | "no-delete-var": 2, 45 | "no-dupe-args": 2, 46 | "no-dupe-class-members": 2, 47 | "no-dupe-keys": 2, 48 | "no-duplicate-case": 2, 49 | "no-empty-character-class": 2, 50 | "no-eval": 2, 51 | "no-ex-assign": 2, 52 | "no-extend-native": 2, 53 | "no-extra-bind": 2, 54 | "no-extra-boolean-cast": 2, 55 | "no-extra-parens": [2, "functions"], 56 | "no-fallthrough": 2, 57 | "no-floating-decimal": 2, 58 | "no-func-assign": 2, 59 | "no-implied-eval": 2, 60 | "no-implicit-coercion": 2, 61 | "no-inner-declarations": [2, "functions"], 62 | "no-invalid-regexp": 2, 63 | "no-irregular-whitespace": 2, 64 | "no-iterator": 2, 65 | "no-label-var": 2, 66 | "no-labels": 2, 67 | "no-lone-blocks": 2, 68 | "no-lonely-if": 2, 69 | "no-mixed-spaces-and-tabs": 2, 70 | "no-multi-spaces": 0, 71 | "no-multi-str": 2, 72 | "no-multiple-empty-lines": [2, { "max": 1 }], 73 | "no-native-reassign": 2, 74 | "no-negated-in-lhs": 2, 75 | "no-new": 2, 76 | "no-new-func": 2, 77 | "no-new-object": 2, 78 | "no-new-require": 2, 79 | "no-new-wrappers": 2, 80 | "no-obj-calls": 2, 81 | "no-octal": 2, 82 | "no-octal-escape": 2, 83 | "no-proto": 2, 84 | "no-redeclare": 2, 85 | "no-regex-spaces": 2, 86 | "no-return-assign": 2, 87 | "no-self-compare": 2, 88 | "no-sequences": 2, 89 | "no-shadow-restricted-names": 2, 90 | "no-spaced-func": 2, 91 | "no-sparse-arrays": 2, 92 | "no-this-before-super": 2, 93 | "no-throw-literal": 2, 94 | "no-trailing-spaces": 2, 95 | "no-undef": 2, 96 | "no-undef-init": 2, 97 | "no-unexpected-multiline": 2, 98 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 99 | "no-unreachable": 2, 100 | "no-unused-expressions": 2, 101 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 102 | "no-useless-call": 2, 103 | "no-with": 2, 104 | "object-curly-spacing": ["error", "always", { "objectsInObjects": true }], 105 | "one-var": [2, { "initialized": "never" }], 106 | "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], 107 | "padded-blocks": [0, "never"], 108 | "prefer-const": [2, { "destructuring": "all", "ignoreReadBeforeAssign": false }], 109 | "quotes": [2, "single", "avoid-escape"], 110 | "radix": 2, 111 | "semi": [2, "always"], 112 | "semi-spacing": [2, { "before": false, "after": true }], 113 | "space-before-blocks": [2, "always"], 114 | "space-before-function-paren": [2, { "anonymous": "never", "named": "never", "asyncArrow": "always" }], 115 | "space-in-parens": [2, "never"], 116 | "space-infix-ops": 2, 117 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 118 | "spaced-comment": [0, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 119 | "strict": 2, 120 | "use-isnan": 2, 121 | "valid-typeof": 2, 122 | "wrap-iife": [2, "any"], 123 | "yoda": [2, "never"] 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/sync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const utils = require('./utils'); 6 | 7 | const kPath = Symbol('path'); 8 | const kResult = Symbol('result'); 9 | const kRealPathExists = Symbol('realpath-exists'); 10 | const kUpdated = Symbol('updated'); 11 | 12 | const readdirSync = (basedir, options = {}) => { 13 | if (Array.isArray(basedir)) { 14 | return readdirsSync(basedir, options); 15 | } 16 | 17 | const seen = new Set(); 18 | const results = []; 19 | 20 | const { 21 | absolute, 22 | onDirectory, 23 | onFile, 24 | onEach, 25 | onPush, 26 | onSymbolicLink, 27 | recursive 28 | } = options; 29 | 30 | const cwd = absolute ? path.resolve(basedir) : basedir; 31 | let base = options.base || cwd; 32 | 33 | if (absolute && base !== cwd) { 34 | base = path.resolve(base); 35 | } 36 | 37 | const depth = typeof options.depth === 'number' ? options.depth : null; 38 | const dirs = options.dirs !== false && options.nodir !== true; 39 | const follow = options.follow === true || options.realpath === true || options.symlinks === true; 40 | const objects = options.objects === true || options.withFileTypes === true; 41 | const recurse = recursive === true || (depth !== null && depth > 1); 42 | const sep = options.sep || path.sep; 43 | const symlinks = (options.symlinks !== false && options.follow !== false) || options.realpath === true; 44 | 45 | const filter = options.filter ? utils.matcher(options.filter) : () => true; 46 | const isMatch = options.isMatch && utils.matcher(options.isMatch, options); 47 | const isMaxDepth = file => depth !== null && file.depth >= depth; 48 | 49 | const updatePaths = file => { 50 | if (!file[kUpdated] && file.path !== file[kPath]) { 51 | file[kUpdated] = true; 52 | file.name = path.basename(file.path); 53 | file.dirname = path.dirname(file.path); 54 | file.relative = path.relative(file.base, file.path); 55 | } 56 | }; 57 | 58 | const getReturnValue = (file, parent) => { 59 | if (file.ignore === true || file.keep === false) return; 60 | if (filter(file) === false) return; 61 | 62 | if (file.keep !== true) { 63 | if (file.isSymbolicLink() && symlinks !== true) return; 64 | if (file.isDirectory() && options.nodir === true) return; 65 | if (options.dot === false && file.name.startsWith('.')) return; 66 | if (isMatch) updatePaths(file); 67 | if (isMatch && isMatch(file, parent) === false) return; 68 | } 69 | 70 | if (absolute === true) { 71 | file.path = path.resolve(cwd, file.path); 72 | 73 | if (objects !== true) { 74 | return file.path; 75 | } 76 | } 77 | 78 | updatePaths(file); 79 | 80 | if (objects === true) { 81 | return file; 82 | } 83 | 84 | if (options.push !== false) { 85 | return file.relative; 86 | } 87 | }; 88 | 89 | const push = (file, parent) => { 90 | const value = getReturnValue(file, parent); 91 | 92 | if (value && (options.unique !== true || !seen.has(value))) { 93 | file[kResult] = value; 94 | 95 | if (options.unique === true) { 96 | seen.add(value); 97 | } 98 | 99 | if (typeof onPush === 'function') { 100 | onPush(file); 101 | } 102 | 103 | results.push(value); 104 | } 105 | }; 106 | 107 | const shouldStopRecursing = file => { 108 | return file.recurse === false || (file.recurse !== true && recurse === false); 109 | }; 110 | 111 | const walk = (dirent, parent) => { 112 | if (onEach) onEach(dirent, parent); 113 | if (onDirectory) onDirectory(dirent, parent); 114 | 115 | if (dirent.path !== cwd && !dirent.ignore && dirs) { 116 | push(dirent, parent); 117 | } 118 | 119 | if (isMaxDepth(dirent)) { 120 | dirent.recurse = false; 121 | } 122 | 123 | if (dirent.path !== cwd && shouldStopRecursing(dirent)) { 124 | return; 125 | } 126 | 127 | const dirents = fs.readdirSync(dirent.path, { withFileTypes: true }); 128 | 129 | for (const file of dirents) { 130 | file.depth = dirent.depth + 1; 131 | file.cwd = cwd; 132 | file.base = base; 133 | file.folder = dirent.name; 134 | file.dirname = dirent.path; 135 | file.path = file[kPath] = `${dirent.path}${sep}${file.name}`; 136 | 137 | let updatedRelative = false; 138 | 139 | // lazily decorate relative path if or when a user supplied function is called 140 | // if no function is passed, we can avoid calling this function 141 | const updateRelative = () => { 142 | if (updatedRelative) return; 143 | updatedRelative = true; 144 | 145 | if (file[kPath] === file.path && file.base === file.cwd) { 146 | file.relative = dirent.relative ? `${dirent.relative}${sep}${file.name}` : file.name; 147 | } else { 148 | file.relative = path.relative(file.base, file.path); 149 | } 150 | }; 151 | 152 | if (absolute !== true || objects === true || isMatch) { 153 | updateRelative(); 154 | } 155 | 156 | if (onEach) { 157 | updateRelative(); 158 | onEach(file, dirent); 159 | } 160 | 161 | if (file.isSymbolicLink()) { 162 | try { 163 | if (typeof onSymbolicLink === 'function') { 164 | updateRelative(); 165 | onSymbolicLink(file, dirent); 166 | } 167 | 168 | if (options.realpath) { 169 | file.path = fs.realpathSync(file.path); 170 | file.dirname = path.dirname(file.path); 171 | file[kRealPathExists] = true; 172 | } 173 | 174 | if (options.symlinks === true || (options.nodir !== true && follow === true) || options.stat === true) { 175 | file.stat = fs.statSync(file.path); 176 | file.isFile = () => file.stat.isFile(); 177 | file.isDirectory = () => file.stat.isDirectory(); 178 | } 179 | 180 | } catch (err) { 181 | if (err.code !== 'ENOENT') { 182 | throw err; 183 | } 184 | 185 | file[kRealPathExists] = false; 186 | } 187 | } else if (options.stat === true) { 188 | file.stat = fs.statSync(file[kPath]); 189 | } 190 | 191 | if (file.isDirectory()) { 192 | walk(file, dirent); 193 | continue; 194 | } 195 | 196 | if (typeof onFile === 'function') { 197 | updateRelative(); 198 | onFile(file, dirent); 199 | } 200 | 201 | push(file, dirent); 202 | } 203 | }; 204 | 205 | const dirent = new fs.Dirent(null, 2); 206 | dirent.depth = 0; 207 | dirent.base = base; 208 | dirent.path = cwd; 209 | dirent.cwd = cwd; 210 | dirent.relative = ''; 211 | dirent[kPath] = cwd; 212 | 213 | walk(dirent, null); 214 | return results; 215 | }; 216 | 217 | const readdirsSync = (dirs, options = {}) => { 218 | const unique = options.unique === true; 219 | const seen = new Set(); 220 | const files = []; 221 | 222 | const onPush = (file, parent) => { 223 | const result = file[kResult]; 224 | 225 | if (unique === true) { 226 | if (!seen.has(file.relative)) { 227 | seen.add(file.relative); 228 | files.push(result); 229 | } 230 | } else { 231 | files.push(result); 232 | } 233 | 234 | if (options.onPush) { 235 | options.onPush(file, parent); 236 | } 237 | }; 238 | 239 | const opts = { ...options, onPush }; 240 | 241 | for (const dir of [].concat(dirs)) { 242 | readdirSync(dir, opts); 243 | } 244 | 245 | return unique ? [...files] : files; 246 | }; 247 | 248 | readdirSync.FILE_RESULT = kResult; 249 | module.exports = readdirSync; 250 | -------------------------------------------------------------------------------- /lib/async.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const utils = require('./utils'); 6 | const { realpath, stat } = fs.promises; 7 | 8 | const kPath = Symbol('path'); 9 | const kResult = Symbol('result'); 10 | const kRealPathExists = Symbol('realpath-exists'); 11 | const kUpdated = Symbol('updated'); 12 | 13 | const readdir = async (basedir, options = {}) => { 14 | if (Array.isArray(basedir)) { 15 | return readdirs(basedir, options); 16 | } 17 | 18 | const seen = new Set(); 19 | const results = []; 20 | 21 | const { 22 | absolute, 23 | onDirectory, 24 | onFile, 25 | onEach, 26 | onPush, 27 | onSymbolicLink, 28 | recursive 29 | } = options; 30 | 31 | const cwd = absolute ? path.resolve(basedir) : basedir; 32 | let base = options.base || cwd; 33 | 34 | if (absolute && base !== cwd) { 35 | base = path.resolve(base); 36 | } 37 | 38 | const depth = typeof options.depth === 'number' ? options.depth : null; 39 | const dirs = options.dirs !== false && options.nodir !== true; 40 | const follow = options.follow === true || options.realpath === true || options.symlinks === true; 41 | const objects = options.objects === true || options.withFileTypes === true; 42 | const recurse = recursive === true || (depth !== null && depth > 1); 43 | const sep = options.sep || path.sep; 44 | const symlinks = (options.symlinks !== false && options.follow !== false) || options.realpath === true; 45 | 46 | const filter = options.filter ? utils.matcher(options.filter) : () => true; 47 | const isMatch = options.isMatch && utils.matcher(options.isMatch, options); 48 | const isMaxDepth = file => depth !== null && file.depth >= depth; 49 | 50 | const updatePaths = file => { 51 | if (!file[kUpdated] && file.path !== file[kPath]) { 52 | file[kUpdated] = true; 53 | file.name = path.basename(file.path); 54 | file.dirname = path.dirname(file.path); 55 | file.relative = path.relative(file.base, file.path); 56 | } 57 | }; 58 | 59 | const getReturnValue = (file, parent) => { 60 | if (file.ignore === true || file.keep === false) return; 61 | if (filter(file) === false) return; 62 | 63 | if (file.keep !== true) { 64 | if (symlinks !== true && follow !== true && file.isSymbolicLink()) return; 65 | if (dirs !== true && file.isDirectory()) return; 66 | if (options.nodir === true && file.isDirectory()) return; 67 | if (options.dot === false && file.name.startsWith('.')) return; 68 | if (isMatch) updatePaths(file); 69 | if (isMatch && isMatch(file, parent) === false) return; 70 | } 71 | 72 | if (absolute === true) { 73 | file.path = path.resolve(cwd, file.path); 74 | 75 | if (objects !== true) { 76 | return file.path; 77 | } 78 | } 79 | 80 | updatePaths(file); 81 | 82 | if (objects === true) { 83 | return file; 84 | } 85 | 86 | if (options.push !== false) { 87 | return file.relative; 88 | } 89 | }; 90 | 91 | const push = async (file, parent) => { 92 | const value = getReturnValue(file, parent); 93 | 94 | if (value && (options.unique !== true || !seen.has(value))) { 95 | file[kResult] = value; 96 | 97 | if (options.unique === true) { 98 | seen.add(value); 99 | } 100 | 101 | if (typeof onPush === 'function') { 102 | await onPush(file); 103 | } 104 | 105 | results.push(value); 106 | } 107 | }; 108 | 109 | const shouldStopRecursing = file => { 110 | return file.path !== cwd && (file.recurse === false || (file.recurse !== true && recurse === false)); 111 | }; 112 | 113 | const walk = async (dirent, parent, next) => { 114 | try { 115 | if (onEach) await onEach(dirent, parent); 116 | if (onDirectory) await onDirectory(dirent, parent); 117 | } catch (err) { 118 | next(err); 119 | return; 120 | } 121 | 122 | if (dirent.path !== cwd) { 123 | await push(dirent, parent); 124 | } 125 | 126 | if (isMaxDepth(dirent)) { 127 | dirent.recurse = false; 128 | } 129 | 130 | if (shouldStopRecursing(dirent)) { 131 | next(null, results); 132 | return; 133 | } 134 | 135 | fs.readdir(dirent.path, { withFileTypes: true }, async (err, files) => { 136 | if (err) { 137 | if (err.code === 'ENOENT') { 138 | next(null, results); 139 | return; 140 | } 141 | err.file = dirent; 142 | next(err); 143 | return; 144 | } 145 | 146 | let len = files.length; 147 | if (len === 0) { 148 | next(null, results); 149 | return; 150 | } 151 | 152 | for (const file of files) { 153 | file.depth = dirent.depth + 1; 154 | file.cwd = cwd; 155 | file.base = base; 156 | file.folder = dirent.name; 157 | file.dirname = dirent.path; 158 | file.path = file[kPath] = `${dirent.path}${sep}${file.name}`; 159 | 160 | let updatedRelative = false; 161 | 162 | // lazily decorate relative path if or when a user supplied function is called 163 | // if no function is passed, we can avoid calling this function 164 | const updateRelative = () => { 165 | if (updatedRelative) return; 166 | updatedRelative = true; 167 | 168 | if (file[kPath] === file.path && file.base === file.cwd) { 169 | file.relative = dirent.relative ? `${dirent.relative}${sep}${file.name}` : file.name; 170 | } else { 171 | file.relative = path.relative(file.base, file.path); 172 | } 173 | }; 174 | 175 | if (absolute !== true || objects === true || isMatch) { 176 | updateRelative(); 177 | } 178 | 179 | if (onEach) { 180 | try { 181 | updateRelative(); 182 | await onEach(file, dirent); 183 | } catch (err) { 184 | next(err); 185 | return; 186 | } 187 | } 188 | 189 | if (file.isSymbolicLink()) { 190 | 191 | try { 192 | if (typeof onSymbolicLink === 'function') { 193 | updateRelative(); 194 | await onSymbolicLink(file, dirent); 195 | } 196 | 197 | if (options.realpath) { 198 | file.path = await realpath(file.path); 199 | file.dirname = path.dirname(file.path); 200 | file[kRealPathExists] = true; 201 | } 202 | 203 | if (options.symlinks === true || (options.nodir !== true && follow === true) || options.stat === true) { 204 | file.stat = await stat(file.path); 205 | file.isFile = () => file.stat.isFile(); 206 | file.isDirectory = () => file.stat.isDirectory(); 207 | } 208 | 209 | } catch (err) { 210 | if (err.code !== 'ENOENT') { 211 | next(err); 212 | return; 213 | } 214 | 215 | file[kRealPathExists] = false; 216 | } 217 | } else if (options.stat === true) { 218 | file.stat = await stat(file[kPath]); 219 | } 220 | 221 | if (file.isDirectory()) { 222 | walk(file, dirent, (err, res) => { 223 | if (err) { 224 | err.parent = dirent; 225 | err.file = file; 226 | next(err); 227 | return; 228 | } 229 | 230 | if (--len === 0) { 231 | next(null, results); 232 | } 233 | }); 234 | continue; 235 | } 236 | 237 | if (typeof onFile === 'function') { 238 | try { 239 | updateRelative(); 240 | await onFile(file, dirent); 241 | } catch (err) { 242 | next(err); 243 | return; 244 | } 245 | } 246 | 247 | await push(file, dirent); 248 | 249 | if (--len === 0) { 250 | next(null, results); 251 | } 252 | } 253 | }); 254 | }; 255 | 256 | return new Promise((resolve, reject) => { 257 | const dirent = new fs.Dirent(null, 2); 258 | dirent.depth = 0; 259 | dirent.base = base; 260 | dirent.path = cwd; 261 | dirent.cwd = cwd; 262 | dirent.relative = ''; 263 | dirent[kPath] = cwd; 264 | 265 | let handled = false; 266 | 267 | const handleError = err => { 268 | if (!handled) { 269 | handled = true; 270 | reject(err); 271 | } 272 | }; 273 | 274 | const promise = walk(dirent, null, (err, results) => { 275 | if (err) { 276 | handleError(err); 277 | return; 278 | } 279 | 280 | if (!handled) { 281 | handled = true; 282 | resolve(results); 283 | } 284 | }); 285 | 286 | promise.catch(handleError); 287 | }); 288 | }; 289 | 290 | const readdirs = (dirs, options = {}) => { 291 | const unique = options.unique === true; 292 | const seen = new Set(); 293 | const pending = []; 294 | const files = []; 295 | 296 | const onPush = async (file, parent) => { 297 | const result = file[kResult]; 298 | 299 | if (unique === true) { 300 | if (!seen.has(file.relative)) { 301 | seen.add(file.relative); 302 | files.push(result); 303 | } 304 | } else { 305 | files.push(result); 306 | } 307 | 308 | if (options.onPush) { 309 | await options.onPush(file, parent); 310 | } 311 | }; 312 | 313 | const opts = { ...options, onPush }; 314 | 315 | for (const dir of [].concat(dirs)) { 316 | pending.push(readdir(dir, opts)); 317 | } 318 | 319 | return Promise.all(pending).then(() => unique ? [...files] : files); 320 | }; 321 | 322 | readdir.FILE_RESULT = kResult; 323 | module.exports = readdir; 324 | -------------------------------------------------------------------------------- /.verb.md: -------------------------------------------------------------------------------- 1 | ## Why use @folder/readdir and not some other lib? 2 | 3 | - It's [blazing fast](#benchmarks). 4 | - It has a simple, [straightforward API](#usage) and intuitive [options](#options) for advanced use cases. 5 | - Optionally returns an array of file objects (extends node.js native [fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent)). Returns path strings by default. 6 | - No dependencies 7 | 8 | 9 | ## Usage 10 | 11 | ```js 12 | const readdir = require('{%= name %}'); 13 | const options = {}; 14 | 15 | // async usage 16 | console.log(await readdir('somedir', options)); 17 | console.log(await readdir(['two', 'dirs'], options)); 18 | 19 | // sync usage 20 | console.log(readdir.sync('somedir', options)); 21 | console.log(readdir.sync(['two' 'dirs'], options)); 22 | ``` 23 | 24 | **params** 25 | 26 | Both the async and sync functions take the same arguments: 27 | 28 | ```js 29 | readdir(dir, options); 30 | ``` 31 | 32 | - `dir` (string|array) - one or more directories to read 33 | - `options` - see available [options](#options) 34 | 35 | 36 | ## Options 37 | 38 | ### absolute 39 | 40 | When true, absolute paths are returned. Otherwise, returned paths are relative to [options.base](#base) if defined, or the given directory. 41 | 42 | **Type**: `boolean` 43 | 44 | **Default**: `undefined` 45 | 46 | **Example** 47 | 48 | ```js 49 | console.log(await readdir('some/dir', { absolute: true })); 50 | ``` 51 | 52 | ### base 53 | 54 | The base directory from which relative paths should be created. 55 | 56 | **Type**: `string` 57 | 58 | **Default**: Defaults to the directory passed as the first argument. 59 | 60 | **Example** 61 | 62 | ```js 63 | const files = await readdir('some/dir', { base: 'dir' }); 64 | console.log(files); 65 | ``` 66 | 67 | ### basename 68 | 69 | When true, only the basename of each file is returned. 70 | 71 | **Type**: `boolean` 72 | 73 | **Default**: `undefined` 74 | 75 | **Example** 76 | 77 | ```js 78 | console.log(await readdir('some/dir', { basename: true })); 79 | ``` 80 | 81 | ### depth 82 | 83 | The maximum folder depth to recursively read directories. 84 | 85 | **Type**: `number` 86 | 87 | **Default**: `undefined` 88 | 89 | **Example** 90 | 91 | ```js 92 | const files = await readdir('some/dir', { depth: 2 }); 93 | console.log(files); 94 | ``` 95 | 96 | ### dot 97 | 98 | Dotfiles are included in the result by default. Pass `false` to ignore all dotfiles. Use [onEach][], [onFile][], [onDirectory][], or [isMatch] if you need something more granular. 99 | 100 | **Type**: `boolean` 101 | 102 | **Default**: `true` 103 | 104 | **Example** 105 | 106 | ```js 107 | const files = await readdir('.'); 108 | console.log(files); 109 | //=> ['.DS_Store', '.git', 'LICENSE', 'README.md', 'package.json'] 110 | 111 | const files = await readdir('.', { dot: false }); 112 | console.log(files); 113 | //=> ['LICENSE', 'README.md', 'package.json'] 114 | ``` 115 | 116 | ### filter 117 | 118 | **Type**: `function|string|array|regexp` 119 | 120 | **Default**: `undefined` 121 | 122 | **Example** 123 | 124 | ```js 125 | // only return file paths with "foo" somewhere in the path 126 | console.log(await readdir('some/dir', { filter: /foo/ })); 127 | 128 | // only return file paths without "foo" somewhere in the path 129 | console.log(await readdir('some/dir', { filter: file => !/foo/.test(file.path) })); 130 | ``` 131 | 132 | ### follow 133 | 134 | Follow symbolic links. 135 | 136 | **Type**: `boolean` 137 | 138 | **Default**: `undefined` 139 | 140 | **Example** 141 | 142 | ```js 143 | console.log(await readdir('some/dir', { follow: true })); 144 | ``` 145 | 146 | ### isMatch 147 | 148 | **Type**: `function|string|regex|array` 149 | 150 | **Default**: `undefined` 151 | 152 | **Example** 153 | 154 | ```js 155 | // only return file paths with "/.git/" somewhere in the path 156 | console.log(await readdir('some/dir', { isMatch: /\/\.git\// })); 157 | 158 | // only return file paths that are not inside "node_modules" 159 | console.log(await readdir('some/dir', { isMatch: file => !file.relative.includes('node_modules') })); 160 | 161 | // get all files that are not named .DS_Store 162 | console.log(await readdir('some/dir', { isMatch: file => file.name !== '.DS_Store' })); 163 | 164 | // use globs 165 | const picomatch = require('picomatch'); 166 | const isMatch = picomatch('*/*.js'); 167 | console.log(await readdir('some/dir', { isMatch: file => isMatch(file.relative) })); 168 | ``` 169 | 170 | ### nodir 171 | 172 | When `true` directories are excluded from the result. 173 | 174 | **Type**: `boolean` 175 | 176 | **Default**: `undefined` 177 | 178 | 179 | ### objects 180 | 181 | Return [fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent) objects instead of paths. 182 | 183 | **Type**: `boolean` 184 | 185 | **Default**: `undefined` 186 | 187 | ```js 188 | console.log(await readdir('some/dir', { objects: true })); 189 | ``` 190 | 191 | ### onDirectory 192 | 193 | Function to be called on all directories. 194 | 195 | **Type**: `function` 196 | 197 | **Default**: `undefined` 198 | 199 | **Example** 200 | 201 | ```js 202 | const onDirectory = file => { 203 | if (file.name === 'node_modules') { 204 | file.recurse = false; 205 | } 206 | }; 207 | console.log(await readdir('some/dir', { onDirectory })); 208 | ``` 209 | 210 | ### onEach 211 | 212 | Function to be called on all directories and files. 213 | 214 | **Type**: `function` 215 | 216 | **Default**: `undefined` 217 | 218 | **Example** 219 | 220 | ```js 221 | const onEach = file => { 222 | if (file.name === 'node_modules') { 223 | file.recurse = false; 224 | } 225 | if (file.isFile() && file.name[0] === '.') { 226 | file.keep = true; 227 | } 228 | }; 229 | console.log(await readdir('some/dir', { onEach })); 230 | ``` 231 | 232 | ### onFile 233 | 234 | Function to be called on all files. 235 | 236 | **Type**: `function` 237 | 238 | **Default**: `undefined` 239 | 240 | **Example** 241 | 242 | ```js 243 | const onFile = file => { 244 | if (file.isFile() && file.name[0] === '.') { 245 | file.keep = true; 246 | } 247 | }; 248 | console.log(await readdir('some/dir', { onFile })); 249 | ``` 250 | 251 | ### onSymbolicLink 252 | 253 | Function to be called on all symbolic links. 254 | 255 | **Type**: `function` 256 | 257 | **Default**: `undefined` 258 | 259 | **Example** 260 | 261 | ```js 262 | const onSymbolicLink = file => { 263 | // do stuff 264 | }; 265 | console.log(await readdir('some/dir', { onSymbolicLink })); 266 | ``` 267 | 268 | 269 | ### realpath 270 | 271 | When true, the realpath of the file is returned in the result. This can be used in combination with other options, like `basename` or `relative`. 272 | 273 | **Type**: `boolean` 274 | 275 | **Default**: `undefined` 276 | 277 | **Example** 278 | 279 | ```js 280 | console.log(await readdir('some/dir', { realpath: true })); 281 | ``` 282 | 283 | ### recursive 284 | 285 | **Type**: `function` 286 | **Type**: `string` 287 | **Type**: `boolean` 288 | 289 | **Default**: `undefined` 290 | 291 | **Example** 292 | 293 | ```js 294 | const files = await readdir('some/dir', { recursive: true }); 295 | console.log(files); 296 | ``` 297 | 298 | ### symlinks 299 | 300 | Returns the first directory level of symbolic links. Use [options.follow](#follow) to recursively follow symlinks. 301 | 302 | **Type**: `boolean` 303 | 304 | **Default**: `undefined` 305 | 306 | **Example** 307 | 308 | ```js 309 | console.log(await readdir('some/dir', { symlinks: true })); 310 | ``` 311 | 312 | ### unique 313 | 314 | Return only unique file paths. Only needed when [options.realpath](#realpath) is `true`. 315 | 316 | **Type**: `boolean` 317 | 318 | **Default**: `undefined` 319 | 320 | **Example** 321 | 322 | ```js 323 | console.log(await readdir('some/dir', { unique: true })); 324 | ``` 325 | 326 | 327 | ## Tips & Tricks 328 | 329 | Use the [onFile](#onFile) option to operate on files as they are read from the file system, before they are pushed onto the results array. 330 | 331 | This allows you to pricisely control which files are returned. 332 | 333 | _(Note that even when you specify that files should be returned as *paths* rather than objects, all functions passed on the options will receive files as objects, so that you may manipulate the paths that are returned however you need to)_ 334 | 335 | ```js 336 | const readdir = require('@folder/readdir'); 337 | const isMatch = file => true; 338 | 339 | module.exports = async (dir, options) => { 340 | const opts = { absolute: true, recursive: true, objects: true, ...options }; 341 | const files = []; 342 | 343 | const onFile = file => { 344 | if (isMatch(file)) { 345 | files.push(file); 346 | } 347 | }; 348 | 349 | await readdir(dir, { ...opts, onFile }); 350 | return files; 351 | }; 352 | ``` 353 | 354 | **Files and directories** 355 | 356 | The `onFile` option does not receive dir objects, only dirents (files). If you need both files and directories, you can do the following: 357 | 358 | ```js 359 | const readdir = require('@folder/readdir'); 360 | const isMatch = file => true; 361 | 362 | module.exports = async (dir, options) => { 363 | const opts = { recursive: true, objects: true, ...options }; 364 | const files = []; 365 | 366 | const onDirectory = file => { 367 | if (file.name === 'node_modules') { 368 | file.recurse = false; 369 | } 370 | }; 371 | 372 | const onFile = file => { 373 | if (isMatch(file)) { 374 | files.push(file); 375 | } 376 | }; 377 | 378 | await readdir(dir, { ...opts, onFile, onDirectory }); 379 | return files; 380 | }; 381 | ``` 382 | 383 | Or you can use [onEach](#onEach) (which gives you each file before it has been determined whether or not the file will be returned based on other criteria and options. this allows you to override default behavior in a granular way), or [onPush](#onPush) (which gives you a file that is going to be returned in the results array). 384 | 385 | Here, we only show `onEach`, since it's identical to `onPush` in terms of usage. 386 | 387 | ```js 388 | const readdir = require('@folder/readdir'); 389 | 390 | const ignore = ['node_modules', '.git']; 391 | const isIgnored = file => ignore.includes(file.nane); 392 | 393 | module.exports = async (dir, options) => { 394 | const opts = { recursive: true, objects: true, ...options }; 395 | const files = []; 396 | 397 | const onEach = file => { 398 | if (file.isDirectory()) { 399 | file.recurse = !isIgnored(file); 400 | } else { 401 | files.push(file); 402 | } 403 | }; 404 | 405 | await readdir(dir, { ...opts, onFile, onEach }); 406 | return files; 407 | }; 408 | ``` 409 | 410 | 411 | 412 | ## Benchmarks 413 | 414 | _(Note that only the benchmarks against `fdir` are included here since that library claims to be the fastest)_ 415 | 416 | To run the benchmarks yourself, you'll need to cd into the `bench` folder and run `$ npm i`. Run the `recursive-large` benchmarks last, and before you run them cd into `bench/fixtures` and do `$ npm i`. 417 | 418 | **Specs** 419 | 420 | - CPU: Intel® Core™ i9-9980HK 2.4GHz 421 | - Cores: 16 (8 Physical) 422 | - RAM: 64GB 423 | - Disk: Apple APPLE SSD AP2048N 1864GB NVMe (PCIe x4) 424 | - OS: macOS macOS Big Sur (darwin) 425 | - Kernel: 20.3.0 x64 426 | - Node: v15.14.0 427 | - V8: 8.6.395.17-node.28 428 | 429 | ``` 430 | # single directory (~5-10 files) 431 | @folder/readdir x 24,938 ops/sec (124,693 runs sampled) 432 | fdir x 24,771 ops/sec (123,858 runs sampled) 433 | 434 | # recursive ~220 files 435 | @folder/readdir x 1,915 ops/sec (9,576 runs sampled) 436 | fdir x 1,850 ops/sec (9,253 runs sampled) 437 | 438 | # recursive ~2,700 files 439 | @folder/readdir x 155 ops/sec (780 runs sampled) 440 | fdir x 145 ops/sec (730 runs sampled) 441 | 442 | # recursive ~57,200 files (just gatsby!) 443 | @folder/readdir x 11 ops/sec (57 runs sampled) 444 | fdir x 10 ops/sec (54 runs sampled) 445 | ``` 446 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @folder/readdir [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/jonathanschlinkert?locale.x=en_US) [![NPM version](https://img.shields.io/npm/v/@folder/readdir.svg?style=flat)](https://www.npmjs.com/package/@folder/readdir) [![NPM monthly downloads](https://img.shields.io/npm/dm/@folder/readdir.svg?style=flat)](https://npmjs.org/package/@folder/readdir) [![NPM total downloads](https://img.shields.io/npm/dt/@folder/readdir.svg?style=flat)](https://npmjs.org/package/@folder/readdir) 2 | 3 | > Recursively read a directory, blazing fast. 4 | 5 | Please consider following this project's author, [Jon Schlinkert](https://github.com/jonschlinkert), and consider starring the project to show your :heart: and support. 6 | 7 | ## Install 8 | 9 | Install with [npm](https://www.npmjs.com/) (requires [Node.js](https://nodejs.org/en/) >=10): 10 | 11 | ```sh 12 | $ npm install --save @folder/readdir 13 | ``` 14 | 15 | ## Why use @folder/readdir and not some other lib? 16 | 17 | * It's [blazing fast](#benchmarks). 18 | * It has a simple, [straightforward API](#usage) and intuitive [options](#options) for advanced use cases. 19 | * Optionally returns an array of file objects (extends node.js native [fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent)). Returns path strings by default. 20 | * No dependencies 21 | 22 | ## Usage 23 | 24 | ```js 25 | const readdir = require('@folder/readdir'); 26 | const options = {}; 27 | 28 | // async usage 29 | console.log(await readdir('somedir', options)); 30 | console.log(await readdir(['two', 'dirs'], options)); 31 | 32 | // sync usage 33 | console.log(readdir.sync('somedir', options)); 34 | console.log(readdir.sync(['two' 'dirs'], options)); 35 | ``` 36 | 37 | **params** 38 | 39 | Both the async and sync functions take the same arguments: 40 | 41 | ```js 42 | readdir(dir, options); 43 | ``` 44 | 45 | * `dir` (string|array) - one or more directories to read 46 | * `options` - see available [options](#options) 47 | 48 | ## Options 49 | 50 | ### absolute 51 | 52 | When true, absolute paths are returned. Otherwise, returned paths are relative to [options.base](#base) if defined, or the given directory. 53 | 54 | **Type**: `boolean` 55 | 56 | **Default**: `undefined` 57 | 58 | **Example** 59 | 60 | ```js 61 | console.log(await readdir('some/dir', { absolute: true })); 62 | ``` 63 | 64 | ### base 65 | 66 | The base directory from which relative paths should be created. 67 | 68 | **Type**: `string` 69 | 70 | **Default**: Defaults to the directory passed as the first argument. 71 | 72 | **Example** 73 | 74 | ```js 75 | const files = await readdir('some/dir', { base: 'dir' }); 76 | console.log(files); 77 | ``` 78 | 79 | ### basename 80 | 81 | When true, only the basename of each file is returned. 82 | 83 | **Type**: `boolean` 84 | 85 | **Default**: `undefined` 86 | 87 | **Example** 88 | 89 | ```js 90 | console.log(await readdir('some/dir', { basename: true })); 91 | ``` 92 | 93 | ### depth 94 | 95 | The maximum folder depth to recursively read directories. 96 | 97 | **Type**: `number` 98 | 99 | **Default**: `undefined` 100 | 101 | **Example** 102 | 103 | ```js 104 | const files = await readdir('some/dir', { depth: 2 }); 105 | console.log(files); 106 | ``` 107 | 108 | ### dot 109 | 110 | Dotfiles are included in the result by default. Pass `false` to ignore all dotfiles. Use [onEach][], [onFile][], [onDirectory][], or [isMatch] if you need something more granular. 111 | 112 | **Type**: `boolean` 113 | 114 | **Default**: `true` 115 | 116 | **Example** 117 | 118 | ```js 119 | const files = await readdir('.'); 120 | console.log(files); 121 | //=> ['.DS_Store', '.git', 'LICENSE', 'README.md', 'package.json'] 122 | 123 | const files = await readdir('.', { dot: false }); 124 | console.log(files); 125 | //=> ['LICENSE', 'README.md', 'package.json'] 126 | ``` 127 | 128 | ### filter 129 | 130 | **Type**: `function|string|array|regexp` 131 | 132 | **Default**: `undefined` 133 | 134 | **Example** 135 | 136 | ```js 137 | // only return file paths with "foo" somewhere in the path 138 | console.log(await readdir('some/dir', { filter: /foo/ })); 139 | 140 | // only return file paths without "foo" somewhere in the path 141 | console.log(await readdir('some/dir', { filter: file => !/foo/.test(file.path) })); 142 | ``` 143 | 144 | ### follow 145 | 146 | Follow symbolic links. 147 | 148 | **Type**: `boolean` 149 | 150 | **Default**: `undefined` 151 | 152 | **Example** 153 | 154 | ```js 155 | console.log(await readdir('some/dir', { follow: true })); 156 | ``` 157 | 158 | ### isMatch 159 | 160 | **Type**: `function|string|regex|array` 161 | 162 | **Default**: `undefined` 163 | 164 | **Example** 165 | 166 | ```js 167 | // only return file paths with "/.git/" somewhere in the path 168 | console.log(await readdir('some/dir', { isMatch: /\/\.git\// })); 169 | 170 | // only return file paths that are not inside "node_modules" 171 | console.log(await readdir('some/dir', { isMatch: file => !file.relative.includes('node_modules') })); 172 | 173 | // get all files that are not named .DS_Store 174 | console.log(await readdir('some/dir', { isMatch: file => file.name !== '.DS_Store' })); 175 | 176 | // use globs 177 | const picomatch = require('picomatch'); 178 | const isMatch = picomatch('*/*.js'); 179 | console.log(await readdir('some/dir', { isMatch: file => isMatch(file.relative) })); 180 | ``` 181 | 182 | ### nodir 183 | 184 | When `true` directories are excluded from the result. 185 | 186 | **Type**: `boolean` 187 | 188 | **Default**: `undefined` 189 | 190 | ### objects 191 | 192 | Return [fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent) objects instead of paths. 193 | 194 | **Type**: `boolean` 195 | 196 | **Default**: `undefined` 197 | 198 | ```js 199 | console.log(await readdir('some/dir', { objects: true })); 200 | ``` 201 | 202 | ### onDirectory 203 | 204 | Function to be called on all directories. 205 | 206 | **Type**: `function` 207 | 208 | **Default**: `undefined` 209 | 210 | **Example** 211 | 212 | ```js 213 | const onDirectory = file => { 214 | if (file.name === 'node_modules') { 215 | file.recurse = false; 216 | } 217 | }; 218 | console.log(await readdir('some/dir', { onDirectory })); 219 | ``` 220 | 221 | ### onEach 222 | 223 | Function to be called on all directories and files. 224 | 225 | **Type**: `function` 226 | 227 | **Default**: `undefined` 228 | 229 | **Example** 230 | 231 | ```js 232 | const onEach = file => { 233 | if (file.name === 'node_modules') { 234 | file.recurse = false; 235 | } 236 | if (file.isFile() && file.name[0] === '.') { 237 | file.keep = true; 238 | } 239 | }; 240 | console.log(await readdir('some/dir', { onEach })); 241 | ``` 242 | 243 | ### onFile 244 | 245 | Function to be called on all files. 246 | 247 | **Type**: `function` 248 | 249 | **Default**: `undefined` 250 | 251 | **Example** 252 | 253 | ```js 254 | const onFile = file => { 255 | if (file.isFile() && file.name[0] === '.') { 256 | file.keep = true; 257 | } 258 | }; 259 | console.log(await readdir('some/dir', { onFile })); 260 | ``` 261 | 262 | ### onSymbolicLink 263 | 264 | Function to be called on all symbolic links. 265 | 266 | **Type**: `function` 267 | 268 | **Default**: `undefined` 269 | 270 | **Example** 271 | 272 | ```js 273 | const onSymbolicLink = file => { 274 | // do stuff 275 | }; 276 | console.log(await readdir('some/dir', { onSymbolicLink })); 277 | ``` 278 | 279 | ### realpath 280 | 281 | When true, the realpath of the file is returned in the result. This can be used in combination with other options, like `basename` or `relative`. 282 | 283 | **Type**: `boolean` 284 | 285 | **Default**: `undefined` 286 | 287 | **Example** 288 | 289 | ```js 290 | console.log(await readdir('some/dir', { realpath: true })); 291 | ``` 292 | 293 | ### recursive 294 | 295 | **Type**: `function` 296 | 297 | **Type**: `string` 298 | 299 | **Type**: `boolean` 300 | 301 | **Default**: `undefined` 302 | 303 | **Example** 304 | 305 | ```js 306 | const files = await readdir('some/dir', { recursive: true }); 307 | console.log(files); 308 | ``` 309 | 310 | ### symlinks 311 | 312 | Returns the first directory level of symbolic links. Use [options.follow](#follow) to recursively follow symlinks. 313 | 314 | **Type**: `boolean` 315 | 316 | **Default**: `undefined` 317 | 318 | **Example** 319 | 320 | ```js 321 | console.log(await readdir('some/dir', { symlinks: true })); 322 | ``` 323 | 324 | ### unique 325 | 326 | Return only unique file paths. Only needed when [options.realpath](#realpath) is `true`. 327 | 328 | **Type**: `boolean` 329 | 330 | **Default**: `undefined` 331 | 332 | **Example** 333 | 334 | ```js 335 | console.log(await readdir('some/dir', { unique: true })); 336 | ``` 337 | 338 | ## Tips & Tricks 339 | 340 | Use the [onFile](#onFile) option to operate on files as they are read from the file system, before they are pushed onto the results array. 341 | 342 | This allows you to pricisely control which files are returned. 343 | 344 | _(Note that even when you specify that files should be returned as _paths_ rather than objects, all functions passed on the options will receive files as objects, so that you may manipulate the paths that are returned however you need to)_ 345 | 346 | ```js 347 | const readdir = require('@folder/readdir'); 348 | const isMatch = file => true; 349 | 350 | module.exports = async (dir, options) => { 351 | const opts = { absolute: true, recursive: true, objects: true, ...options }; 352 | const files = []; 353 | 354 | const onFile = file => { 355 | if (isMatch(file)) { 356 | files.push(file); 357 | } 358 | }; 359 | 360 | await readdir(dir, { ...opts, onFile }); 361 | return files; 362 | }; 363 | ``` 364 | 365 | **Files and directories** 366 | 367 | The `onFile` option does not receive dir objects, only dirents (files). If you need both files and directories, you can do the following: 368 | 369 | ```js 370 | const readdir = require('@folder/readdir'); 371 | const isMatch = file => true; 372 | 373 | module.exports = async (dir, options) => { 374 | const opts = { recursive: true, objects: true, ...options }; 375 | const files = []; 376 | 377 | const onDirectory = file => { 378 | if (file.name === 'node_modules') { 379 | file.recurse = false; 380 | } 381 | }; 382 | 383 | const onFile = file => { 384 | if (isMatch(file)) { 385 | files.push(file); 386 | } 387 | }; 388 | 389 | await readdir(dir, { ...opts, onFile, onDirectory }); 390 | return files; 391 | }; 392 | ``` 393 | 394 | Or you can use [onEach](#onEach) (which gives you each file before it has been determined whether or not the file will be returned based on other criteria and options. this allows you to override default behavior in a granular way), or [onPush](#onPush) (which gives you a file that is going to be returned in the results array). 395 | 396 | Here, we only show `onEach`, since it's identical to `onPush` in terms of usage. 397 | 398 | ```js 399 | const readdir = require('@folder/readdir'); 400 | 401 | const ignore = ['node_modules', '.git']; 402 | const isIgnored = file => ignore.includes(file.nane); 403 | 404 | module.exports = async (dir, options) => { 405 | const opts = { recursive: true, objects: true, ...options }; 406 | const files = []; 407 | 408 | const onEach = file => { 409 | if (file.isDirectory()) { 410 | file.recurse = !isIgnored(file); 411 | } else { 412 | files.push(file); 413 | } 414 | }; 415 | 416 | await readdir(dir, { ...opts, onFile, onEach }); 417 | return files; 418 | }; 419 | ``` 420 | 421 | ## Benchmarks 422 | 423 | _(Note that only the benchmarks against `fdir` are included here since that library claims to be the fastest)_ 424 | 425 | To run the benchmarks yourself, you'll need to cd into the `bench` folder and run `$ npm i`. Run the `recursive-large` benchmarks last, and before you run them cd into `bench/fixtures` and do `$ npm i`. 426 | 427 | **Specs** 428 | 429 | * CPU: Intel® Core™ i9-9980HK 2.4GHz 430 | * Cores: 16 (8 Physical) 431 | * RAM: 64GB 432 | * Disk: Apple APPLE SSD AP2048N 1864GB NVMe (PCIe x4) 433 | * OS: macOS macOS Big Sur (darwin) 434 | * Kernel: 20.3.0 x64 435 | * Node: v15.14.0 436 | * V8: 8.6.395.17-node.28 437 | 438 | ``` 439 | # single directory (~5-10 files) 440 | @folder/readdir x 24,938 ops/sec (124,693 runs sampled) 441 | fdir x 24,771 ops/sec (123,858 runs sampled) 442 | 443 | # recursive ~220 files 444 | @folder/readdir x 1,915 ops/sec (9,576 runs sampled) 445 | fdir x 1,850 ops/sec (9,253 runs sampled) 446 | 447 | # recursive ~2,700 files 448 | @folder/readdir x 155 ops/sec (780 runs sampled) 449 | fdir x 145 ops/sec (730 runs sampled) 450 | 451 | # recursive ~57,200 files (just gatsby!) 452 | @folder/readdir x 11 ops/sec (57 runs sampled) 453 | fdir x 10 ops/sec (54 runs sampled) 454 | ``` 455 | 456 | ## About 457 | 458 |
459 | Contributing 460 | 461 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). 462 | 463 |
464 | 465 |
466 | Running Tests 467 | 468 | Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command: 469 | 470 | ```sh 471 | $ npm install && npm test 472 | ``` 473 | 474 |
475 | 476 |
477 | Building docs 478 | 479 | _(This project's readme.md is generated by [verb](https://github.com/verbose/verb-generate-readme), please don't edit the readme directly. Any changes to the readme must be made in the [.verb.md](.verb.md) readme template.)_ 480 | 481 | To generate the readme, run the following command: 482 | 483 | ```sh 484 | $ npm install -g verbose/verb#dev verb-generate-readme && verb 485 | ``` 486 | 487 |
488 | 489 | ### Author 490 | 491 | **Jon Schlinkert** 492 | 493 | * [GitHub Profile](https://github.com/jonschlinkert) 494 | * [Twitter Profile](https://twitter.com/jonschlinkert) 495 | * [LinkedIn Profile](https://linkedin.com/in/jonschlinkert) 496 | 497 | ### License 498 | 499 | Copyright © 2021, [Jon Schlinkert](https://github.com/jonschlinkert). 500 | Released under the [MIT License](LICENSE). 501 | 502 | *** 503 | 504 | _This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.8.0, on April 19, 2021._ 505 | -------------------------------------------------------------------------------- /test/readdir.sync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const assert = require('assert').strict; 7 | const write = require('write'); 8 | const rimraf = require('rimraf'); 9 | const readdir = require('..'); 10 | 11 | const unixify = input => input.replace(/\\/g, '/'); 12 | 13 | const readdirSync = (...args) => { 14 | const files = readdir.sync(...args); 15 | 16 | return files.map(file => { 17 | return typeof file === 'string' ? unixify(file) : file; 18 | }); 19 | }; 20 | 21 | const options = { ignore: ['.DS_Store', 'Thumbs.db'] }; 22 | const temp = (...args) => unixify(path.resolve(__dirname, 'temp', ...args)); 23 | const unlinkSync = filepath => rimraf.sync(filepath, { glob: false }); 24 | let cleanup = () => {}; 25 | 26 | const createFiles = names => { 27 | if (!names) return () => {}; 28 | const paths = names.map(name => temp(name)); 29 | paths.forEach(fp => write.sync(fp, 'temp')); 30 | return () => paths.forEach(file => unlinkSync(file)); 31 | }; 32 | 33 | const cleanupTemp = () => { 34 | if (fs.existsSync(temp())) { 35 | for (const file of fs.readdirSync(temp())) { 36 | unlinkSync(temp(file)); 37 | } 38 | } 39 | }; 40 | 41 | const createSymlink = (type, name, files) => { 42 | const cleanup = createFiles(files); 43 | const dest = temp(name); 44 | const src = type === 'file' ? __filename : __dirname; 45 | fs.symlinkSync(src, dest, type); 46 | 47 | return () => { 48 | unlinkSync(dest); 49 | cleanup(); 50 | }; 51 | }; 52 | 53 | const createSymlinks = (type, names, files) => { 54 | const cleanup = createFiles(files); 55 | const fns = names.map(name => createSymlink(type, name)); 56 | 57 | return () => { 58 | fns.forEach(fn => fn()); 59 | cleanup(); 60 | }; 61 | }; 62 | 63 | describe('readdir.sync', () => { 64 | process.on('exit', cleanupTemp); 65 | beforeEach(() => cleanupTemp()); 66 | beforeEach(() => rimraf.sync(path.join(__dirname, 'symlinks'))); 67 | after(() => rimraf.sync(path.join(__dirname, 'symlinks'))); 68 | after(() => cleanupTemp()); 69 | 70 | describe('no options', () => { 71 | it('should read files in a directory and return a promise with files', () => { 72 | const files = readdirSync(__dirname); 73 | assert(files.some(file => path.basename(file) === 'readdir.js')); 74 | assert(files.some(file => path.basename(file) === 'fixtures')); 75 | }); 76 | 77 | it('should return an array of files', () => { 78 | const files = readdirSync(__dirname); 79 | assert(files.some(file => file === 'readdir.js')); 80 | assert(files.some(file => file === 'readdir.sync.js')); 81 | assert(files.some(file => file === 'fixtures')); 82 | }); 83 | 84 | it('should call options.onFile on each file', () => { 85 | const files = readdirSync(__dirname, { 86 | objects: true, 87 | onFile(file) { 88 | if (file.name === 'readdir.js') { 89 | file.stem = 'foo'; 90 | } 91 | return file; 92 | } 93 | }); 94 | 95 | assert(files.some(f => f.stem === 'foo' && f.name === 'readdir.js')); 96 | }); 97 | 98 | it('should read only one level by default', () => { 99 | cleanup = createFiles(['a/a/a', 'a/a/b', 'a/a/c']); 100 | 101 | const files = readdirSync(temp()); 102 | cleanup(); 103 | assert.equal(files.length, 1); 104 | assert.equal(files[0], 'a'); 105 | }); 106 | 107 | it('should take and array of directories', () => { 108 | cleanup = createFiles(['a/a/a', 'b/b/b']); 109 | 110 | const files = readdirSync([temp('a'), temp('b')]); 111 | cleanup(); 112 | assert.equal(files.length, 2); 113 | assert.equal(files[0], 'a'); 114 | assert.equal(files[1], 'b'); 115 | }); 116 | }); 117 | 118 | describe('options.depth', () => { 119 | it('should recursively read files (depth: 2)', () => { 120 | cleanup = createFiles(['a/b/c/d/e', 'a/a/b/c/d']); 121 | 122 | const files = readdirSync(temp(), { depth: 2 }); 123 | cleanup(); 124 | files.sort(); 125 | assert.deepEqual(files, [ 'a', 'a/a', 'a/b' ].sort()); 126 | }); 127 | 128 | it('should recursively read files (depth: 3)', () => { 129 | cleanup = createFiles(['a/b/c/d/e', 'a/a/b/c/d']); 130 | const files = readdirSync(temp(), { depth: 3 }); 131 | 132 | files.sort(); 133 | assert.deepEqual(files, [ 'a', 'a/a', 'a/a/b', 'a/b', 'a/b/c' ].sort()); 134 | }); 135 | 136 | it('should recursively read files (depth: 4)', () => { 137 | cleanup = createFiles(['a/b/c/d/e', 'a/a/b/c/d']); 138 | 139 | const files = readdirSync(temp(), { depth: 4 }); 140 | cleanup(); 141 | files.sort(); 142 | assert.deepEqual(files, [ 'a', 'a/a', 'a/a/b', 'a/a/b/c', 'a/b', 'a/b/c', 'a/b/c/d' ].sort()); 143 | }); 144 | 145 | it('should recursively read files (depth: 5)', () => { 146 | cleanup = createFiles(['a/b/c/d/e', 'a/a/b/c/d']); 147 | const expected = [ 'a', 'a/a', 'a/a/b', 'a/a/b/c', 'a/b', 'a/b/c', 'a/b/c/d', 'a/b/c/d/e', 'a/a/b/c/d' ]; 148 | 149 | const files = readdirSync(temp(), { depth: 5 }); 150 | cleanup(); 151 | files.sort(); 152 | assert.deepEqual(files, expected.sort()); 153 | }); 154 | }); 155 | 156 | describe('options.dot', () => { 157 | it('should exclude dot files when dot is false', () => { 158 | const expected = ['a', 'a/a', 'a/a/a', 'a/a/b', 'a/a/c', '.gitignore', '.DS_Store']; 159 | cleanup = createFiles(['a/a/a', 'a/a/b', 'a/a/c', '.gitignore', '.DS_Store']); 160 | 161 | const files1 = readdirSync(temp(), { recursive: true }); 162 | files1.sort(); 163 | 164 | expected.forEach(pathname => assert(files1.includes(pathname), pathname)); 165 | 166 | const files2 = readdirSync(temp(), { recursive: true, dot: false }); 167 | files2.sort(); 168 | 169 | expected.forEach(pathname => { 170 | if (pathname.startsWith('.')) { 171 | assert(!files2.includes(pathname), pathname); 172 | } else { 173 | assert(files2.includes(pathname), pathname); 174 | } 175 | }); 176 | 177 | cleanup(); 178 | }); 179 | }); 180 | 181 | describe('options.filter', () => { 182 | const opts = { ...options, relative: true, symlinks: true, base: __dirname }; 183 | 184 | it('should filter symlinks', () => { 185 | try { 186 | const names = fs.readdirSync(path.join(__dirname, '..')); 187 | cleanup = createSymlinks('file', names, ['foo.js', 'bar.js']); 188 | 189 | const filter = file => !/license/i.test(file.path); 190 | const files = readdir.sync(temp(), { ...opts, filter }); 191 | 192 | assert(files.length > 0); 193 | // symlinks 194 | assert(files.some(name => name === 'temp/README.md')); 195 | assert(!files.some(name => name === 'temp/LICENSE')); 196 | 197 | // files 198 | assert(files.some(name => name === 'temp/foo.js')); 199 | assert(files.some(name => name === 'temp/bar.js')); 200 | cleanup(); 201 | 202 | } catch (err) { 203 | cleanup(); 204 | throw err; 205 | } 206 | }); 207 | 208 | it('should filter files', () => { 209 | const filter = file => /sync/.test(file.path); 210 | 211 | const files = readdir.sync(__dirname, { ...opts, filter }); 212 | assert(files.includes('readdir.sync.js')); 213 | }); 214 | 215 | it('should filter files recursively', () => { 216 | cleanup = createFiles(['c.md', 'a/a/a/a.md', 'a/a/a/c.txt', 'a/a/a/b.md', 'a/b.txt']); 217 | 218 | const filter = file => { 219 | return file.isFile() && path.extname(file.path) === '.md'; 220 | }; 221 | 222 | const files = readdir.sync(temp(), { recursive: true, filter }); 223 | cleanup(); 224 | assert.deepEqual(files, [ 'c.md', 'a/a/a/a.md', 'a/a/a/b.md' ].sort()); 225 | }); 226 | }); 227 | 228 | describe('options.recurse', () => { 229 | it('should recursively read files', () => { 230 | cleanup = createFiles(['a/a/a', 'a/a/b', 'a/a/c']); 231 | 232 | const files = readdirSync(temp(), { recursive: true }); 233 | cleanup(); 234 | files.sort(); 235 | assert.deepEqual(files, [ 'a', 'a/a', 'a/a/a', 'a/a/b', 'a/a/c' ].sort()); 236 | }); 237 | 238 | it('should get first level symlinks by default', () => { 239 | const paths = ['a/a/a', 'a/a/b', 'a/a/c']; 240 | const links = ['b/a/a', 'b/a/b', 'b/a/c']; 241 | cleanup = createFiles(paths); 242 | 243 | for (let i = 0; i < links.length; i++) { 244 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 245 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 246 | } 247 | 248 | const files = readdirSync(temp(), { recursive: true }); 249 | cleanup(); 250 | files.sort(); 251 | assert.deepEqual(files, [ 'a', 'b', 'a/a', 'b/a', ...paths, ...links ].sort()); 252 | }); 253 | }); 254 | 255 | describe('options.objects', () => { 256 | it('should return file objects', () => { 257 | const files = readdirSync(__dirname, { objects: true }); 258 | assert(files.some(file => file.name === 'readdir.js')); 259 | assert(files.some(file => file.name === 'fixtures')); 260 | }); 261 | }); 262 | 263 | describe('options.onFile', () => { 264 | it('should call options.onFile function on each file', () => { 265 | const onFile = file => { 266 | if (file.name === 'readdir.js') { 267 | file.path = path.join(path.dirname(file.path), 'foo.js'); 268 | file.name = 'foo.js'; 269 | } 270 | return file; 271 | }; 272 | 273 | const files = readdirSync(__dirname, { onFile }); 274 | assert(files.some(file => path.basename(file) === 'foo.js')); 275 | assert(files.some(file => path.basename(file) === 'fixtures')); 276 | }); 277 | 278 | it('should not keep files when file.keep is false', () => { 279 | const paths = ['a/a/a.md', 'a/a/b.txt', 'a/a/c.md', 'a/b/c/d.txt', 'a/b/b/b.md']; 280 | cleanup = createFiles(paths); 281 | 282 | const onFile = file => { 283 | file.keep = path.extname(file.path) === '.md'; 284 | }; 285 | 286 | const files = readdirSync(temp(), { onFile, nodir: true, recursive: true }); 287 | cleanup(); 288 | assert.deepEqual(files, [ 'a/a/a.md', 'a/a/c.md', 'a/b/b/b.md' ]); 289 | }); 290 | }); 291 | 292 | describe('options.onDirectory', () => { 293 | it('should call options.onDirectory function on each directory', () => { 294 | const onDirectory = file => { 295 | if (file.name === 'fixtures') { 296 | file.path = path.join(path.dirname(file.path), 'actual'); 297 | file.name = 'actual'; 298 | } 299 | }; 300 | 301 | const files = readdirSync(__dirname, { onDirectory }); 302 | assert(files.some(file => path.basename(file) === 'readdir.js')); 303 | assert(files.some(file => path.basename(file) === 'actual')); 304 | }); 305 | 306 | it('should not recurse in a directory when file.recurse is false', () => { 307 | const paths = ['a/a/a.txt', 'a/a/b.txt', 'a/a/c.txt', 'a/b/c/d.txt', 'a/b/b/b.txt']; 308 | cleanup = createFiles(paths); 309 | 310 | const onDirectory = file => { 311 | file.recurse = file.name !== 'b'; 312 | file.keep = false; 313 | }; 314 | 315 | const files = readdirSync(temp(), { recursive: true, onDirectory }); 316 | cleanup(); 317 | assert.deepEqual(files, [ 'a/a/a.txt', 'a/a/b.txt', 'a/a/c.txt' ]); 318 | }); 319 | }); 320 | 321 | describe('options.symlinks', () => { 322 | it('should get first-level symlinks by default', () => { 323 | const link = 'temp-symlink.js'; 324 | cleanup = createSymlink('file', link, ['foo.js', 'bar.js']); 325 | 326 | const files = readdirSync(temp(), { ...options }); 327 | assert(files.length > 0); 328 | assert(files.some(name => name === link)); 329 | assert(files.some(name => name === 'foo.js')); 330 | assert(files.some(name => name === 'bar.js')); 331 | }); 332 | 333 | it('should not get first-level symlinks when disabled', () => { 334 | const paths = ['nested/a/a/a', 'nested/a/a/b', 'nested/a/a/c']; 335 | const links = ['nested/b/a/a', 'nested/b/a/b', 'nested/b/a/c']; 336 | cleanup = createFiles(paths); 337 | 338 | for (let i = 0; i < links.length; i++) { 339 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 340 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 341 | } 342 | 343 | fs.symlinkSync(temp('nested'), temp('symlinks'), 'dir'); 344 | 345 | const files = readdirSync(temp(), { recursive: true, symlinks: false }); 346 | cleanup(); 347 | unlinkSync(temp('symlinks')); 348 | 349 | assert(files.includes('nested')); 350 | assert(files.includes('nested/a')); 351 | assert(files.includes('nested/b')); 352 | assert(files.includes('nested/a/a')); 353 | assert(files.includes('nested/a/a/a')); 354 | 355 | assert(!files.includes('symlinks')); 356 | assert(!files.includes('symlinks/a')); 357 | assert(!files.includes('symlinks/b')); 358 | assert(!files.includes('symlinks/a/a')); 359 | assert(!files.includes('symlinks/a/a/a')); 360 | }); 361 | 362 | it('should return symlinked files when not disabled on options', () => { 363 | const link = 'temp-symlink.js'; 364 | cleanup = createSymlink('file', link, ['foo.js', 'bar.js']); 365 | 366 | const files = readdirSync(temp(), { ...options }); 367 | 368 | assert(files.length > 0); 369 | assert(files.some(name => name === link)); 370 | assert(files.some(name => name === 'foo.js')); 371 | assert(files.some(name => name === 'bar.js')); 372 | }); 373 | 374 | it('should return symlinked directories when not disabled on options', () => { 375 | const opts = { ...options }; 376 | 377 | const link = 'temp-symlink'; 378 | cleanup = createSymlink('dir', link, ['foo.js', 'bar.js']); 379 | 380 | const files = readdirSync(temp(), opts); 381 | 382 | assert(files.length > 0); 383 | assert(files.some(name => name === link)); 384 | assert(files.some(name => name === 'foo.js')); 385 | assert(files.some(name => name === 'bar.js')); 386 | }); 387 | 388 | it('should ignore nested symlinked files that do not exist', () => { 389 | const opts = { ...options, symlinks: true }; 390 | 391 | cleanup = createFiles(['foo.js', 'bar.js']); 392 | const tempfile = temp('tempfile.js'); 393 | const link = temp('link.js'); 394 | 395 | process.on('exit', () => unlinkSync(link)); 396 | 397 | write.sync(tempfile, 'temp'); 398 | fs.symlinkSync(tempfile, link, 'file'); 399 | unlinkSync(tempfile); 400 | 401 | const files = readdirSync(temp(), opts); 402 | 403 | assert(files.length > 0); 404 | assert(!files.some(name => name === link)); 405 | assert(files.some(name => name === 'foo.js')); 406 | assert(files.some(name => name === 'bar.js')); 407 | }); 408 | 409 | it('should ignore nested symlinked directories that do not exist', () => { 410 | const opts = { ...options, symlinks: true }; 411 | cleanup = createFiles(['foo.js', 'bar.js']); 412 | 413 | const tempdir = temp('tempdir/a/b/c'); 414 | const link = temp('link'); 415 | process.on('exit', () => unlinkSync(link)); 416 | 417 | fs.mkdirSync(tempdir, { recursive: true }); 418 | fs.symlinkSync(tempdir, link, 'dir'); 419 | rimraf.sync(tempdir); 420 | 421 | const files = readdirSync(temp(), opts); 422 | 423 | assert(files.length > 0); 424 | assert(!files.some(name => name === link)); 425 | assert(files.some(name => name === 'foo.js')); 426 | assert(files.some(name => name === 'bar.js')); 427 | }); 428 | 429 | it('should only get first-level symlinks by default', () => { 430 | const paths = ['nested/a/a/a', 'nested/a/a/b', 'nested/a/a/c']; 431 | const links = ['nested/b/a/a', 'nested/b/a/b', 'nested/b/a/c']; 432 | cleanup = createFiles(paths); 433 | 434 | for (let i = 0; i < links.length; i++) { 435 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 436 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 437 | } 438 | 439 | fs.symlinkSync(temp('nested'), temp('symlinks'), 'dir'); 440 | 441 | const files = readdirSync(temp(), { recursive: true }); 442 | cleanup(); 443 | unlinkSync(temp('symlinks')); 444 | 445 | assert(files.includes('nested')); 446 | assert(files.includes('nested/a')); 447 | assert(files.includes('nested/b')); 448 | assert(files.includes('nested/a/a')); 449 | assert(files.includes('nested/a/a/a')); 450 | 451 | assert(files.includes('symlinks')); 452 | assert(!files.includes('symlinks/a')); 453 | assert(!files.includes('symlinks/b')); 454 | assert(!files.includes('symlinks/a/a')); 455 | assert(!files.includes('symlinks/a/a/a')); 456 | }); 457 | 458 | it('should recursively get symlinks when specified', () => { 459 | const paths = ['nested/a/a/a', 'nested/a/a/b', 'nested/a/a/c']; 460 | const links = ['nested/b/a/a', 'nested/b/a/b', 'nested/b/a/c']; 461 | cleanup = createFiles(paths); 462 | 463 | for (let i = 0; i < links.length; i++) { 464 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 465 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 466 | } 467 | 468 | fs.symlinkSync(temp('nested'), temp('symlinks'), 'dir'); 469 | 470 | const files = readdirSync(temp(), { recursive: true, follow: true }); 471 | cleanup(); 472 | unlinkSync(temp('symlinks')); 473 | 474 | assert(files.includes('nested')); 475 | assert(files.includes('nested/a')); 476 | assert(files.includes('nested/b')); 477 | assert(files.includes('nested/a/a')); 478 | assert(files.includes('nested/a/a/a')); 479 | 480 | assert(files.includes('symlinks')); 481 | assert(files.includes('symlinks/a')); 482 | assert(files.includes('symlinks/b')); 483 | assert(files.includes('symlinks/a/a')); 484 | assert(files.includes('symlinks/a/a/a')); 485 | }); 486 | }); 487 | 488 | describe('options.realpath', () => { 489 | it('should return realpaths', () => { 490 | const paths = ['a/a/a', 'a/a/b', 'a/a/c']; 491 | const links = ['b/a/a', 'b/a/b', 'b/a/c']; 492 | cleanup = createFiles(paths); 493 | 494 | for (let i = 0; i < links.length; i++) { 495 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 496 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 497 | } 498 | 499 | const files = readdirSync(temp(), { recursive: true, realpath: true }); 500 | cleanup(); 501 | assert.deepEqual(files.sort(), [ 'a', 'b', 'a/a', 'b/a', ...paths, ...paths ].sort()); 502 | }); 503 | 504 | it('should return realpaths with no duplicates when options.unique is true', () => { 505 | const paths = ['a/a/a', 'a/a/b', 'a/a/c']; 506 | const links = ['b/a/a', 'b/a/b', 'b/a/c']; 507 | cleanup = createFiles(paths); 508 | 509 | for (let i = 0; i < links.length; i++) { 510 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 511 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 512 | } 513 | 514 | const files = readdirSync(temp(), { recursive: true, realpath: true, unique: true }); 515 | cleanup(); 516 | assert.deepEqual(files.sort(), [ 'a', 'b', 'a/a', 'b/a', ...paths ].sort()); 517 | }); 518 | }); 519 | 520 | describe('options.relative', () => { 521 | it('should get relative paths for symlinked files', () => { 522 | const opts = { ...options, relative: true, symlinks: true, base: __dirname }; 523 | const names = fs.readdirSync(path.join(__dirname, '..')); 524 | 525 | cleanup = createSymlinks('file', names, ['foo.js', 'bar.js']); 526 | 527 | const files = readdirSync(temp(), opts); 528 | cleanup(); 529 | assert(files.length > 0); 530 | // symlinks 531 | assert(files.some(name => name === 'temp/README.md')); 532 | assert(files.some(name => name === 'temp/LICENSE')); 533 | 534 | // files 535 | assert(files.some(name => name === 'temp/foo.js')); 536 | assert(files.some(name => name === 'temp/bar.js')); 537 | }); 538 | 539 | it('should get relative paths for symlinked files', () => { 540 | const opts = { ...options, relative: true, symlinks: true, base: __dirname }; 541 | const names = fs.readdirSync(path.join(__dirname, '..')); 542 | 543 | cleanup = createSymlinks('file', names, ['foo.js', 'bar.js']); 544 | 545 | const files = readdirSync(temp(), opts); 546 | cleanup(); 547 | assert(files.length > 0); 548 | // symlinks 549 | assert(files.some(name => name === 'temp/README.md')); 550 | assert(files.some(name => name === 'temp/LICENSE')); 551 | 552 | // files 553 | assert(files.some(name => name === 'temp/foo.js')); 554 | assert(files.some(name => name === 'temp/bar.js')); 555 | }); 556 | }); 557 | 558 | describe('options.isMatch', () => { 559 | const opts = { ...options, relative: true, symlinks: true, base: __dirname }; 560 | 561 | it('should match symlinks', () => { 562 | const names = fs.readdirSync(path.join(__dirname, '..')); 563 | cleanup = createSymlinks('file', names, ['foo.js', 'bar.js']); 564 | 565 | const isMatch = file => !/license/i.test(file.path); 566 | const files = readdirSync(temp(), { ...opts, isMatch }); 567 | 568 | assert(files.length > 0); 569 | // symlinks 570 | assert(files.some(name => name === 'temp/README.md')); 571 | assert(!files.some(name => name === 'temp/LICENSE')); 572 | 573 | // files 574 | assert(files.some(name => name === 'temp/foo.js')); 575 | assert(files.some(name => name === 'temp/bar.js')); 576 | }); 577 | 578 | it('should match files', () => { 579 | const isMatch = file => /sync/.test(file.path); 580 | 581 | const files = readdirSync(__dirname, { ...opts, isMatch }); 582 | assert(files.includes('readdir.sync.js')); 583 | }); 584 | 585 | it('should match files recursively', () => { 586 | cleanup = createFiles(['c.md', 'a/a/a/a.md', 'a/a/a/c.txt', 'a/a/a/b.md', 'a/b.txt']); 587 | 588 | const isMatch = file => { 589 | return file.isFile() && path.extname(file.path) === '.md'; 590 | }; 591 | 592 | const files = readdirSync(temp(), { recursive: true, isMatch }); 593 | cleanup(); 594 | 595 | assert.deepEqual(files, ['c.md', 'a/a/a/a.md', 'a/a/a/b.md'].sort()); 596 | }); 597 | 598 | it('should keep matching files', () => { 599 | cleanup = createFiles(['b/b/b.txt', 'a/a/a.txt', 'c/c/c.txt']); 600 | 601 | const isMatch = file => { 602 | return file.name !== 'b.txt'; 603 | }; 604 | 605 | const files = readdirSync('test/temp', { absolute: true, recursive: true, isMatch }); 606 | 607 | cleanup(); 608 | assert(files.length > 1); 609 | assert(files.includes(temp('a/a/a.txt'))); 610 | assert(!files.includes(temp('b/b/b.txt'))); 611 | assert(files.includes(temp('c/c/c.txt'))); 612 | }); 613 | 614 | it('should keep matching directories', () => { 615 | cleanup = createFiles(['bb/b/b', 'aa/a/a', 'cc/c/c']); 616 | 617 | const isMatch = file => { 618 | return !file.relative.startsWith('bb'); 619 | }; 620 | 621 | const files = readdirSync('test/temp', { absolute: true, recursive: true, isMatch }); 622 | 623 | cleanup(); 624 | assert(files.length > 1); 625 | assert(files.includes(temp('aa/a/a'))); 626 | assert(!files.includes(temp('bb/b/b'))); 627 | assert(files.includes(temp('cc/c/c'))); 628 | }); 629 | }); 630 | }); 631 | -------------------------------------------------------------------------------- /test/readdir.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const assert = require('assert').strict; 7 | const write = require('write'); 8 | const rimraf = require('rimraf'); 9 | const _readdir = require('..'); 10 | 11 | const unixify = input => input.replace(/\\/g, '/'); 12 | 13 | const readdir = async (...args) => { 14 | const files = await _readdir(...args); 15 | 16 | return files.map(file => { 17 | return typeof file === 'string' ? unixify(file) : file; 18 | }); 19 | }; 20 | 21 | const options = { ignore: ['.DS_Store', 'Thumbs.db'] }; 22 | const temp = (...args) => unixify(path.resolve(__dirname, 'temp', ...args)); 23 | const unlinkSync = filepath => rimraf.sync(filepath, { glob: false }); 24 | let cleanup = () => {}; 25 | 26 | const createFiles = names => { 27 | if (!names) return () => {}; 28 | const paths = names.map(name => temp(name)); 29 | paths.forEach(fp => write.sync(fp, 'temp')); 30 | return () => paths.forEach(file => unlinkSync(file)); 31 | }; 32 | 33 | const cleanupTemp = () => { 34 | if (fs.existsSync(temp())) { 35 | for (const file of fs.readdirSync(temp())) { 36 | unlinkSync(temp(file)); 37 | } 38 | } 39 | }; 40 | 41 | const createSymlink = (type, name, files) => { 42 | const cleanup = createFiles(files); 43 | const dest = temp(name); 44 | const src = type === 'file' ? __filename : __dirname; 45 | fs.symlinkSync(src, dest, type); 46 | 47 | return () => { 48 | unlinkSync(dest); 49 | cleanup(); 50 | }; 51 | }; 52 | 53 | const createSymlinks = (type, names, files) => { 54 | const cleanup = createFiles(files); 55 | const fns = names.map(name => createSymlink(type, name)); 56 | 57 | return () => { 58 | fns.forEach(fn => fn()); 59 | cleanup(); 60 | }; 61 | }; 62 | 63 | describe('readdir', () => { 64 | process.on('exit', cleanupTemp); 65 | beforeEach(() => cleanupTemp()); 66 | beforeEach(() => rimraf.sync(path.join(__dirname, 'symlinks'))); 67 | after(() => rimraf.sync(path.join(__dirname, 'symlinks'))); 68 | after(() => cleanupTemp()); 69 | 70 | describe('no options', () => { 71 | it('should read files in a directory and return a promise with files', cb => { 72 | readdir(__dirname) 73 | .then(files => { 74 | assert(files.some(file => path.basename(file) === 'readdir.js')); 75 | assert(files.some(file => path.basename(file) === 'fixtures')); 76 | cb(); 77 | }); 78 | }); 79 | 80 | it('should read only one level by default', () => { 81 | cleanup = createFiles(['a/a/a', 'a/a/b', 'a/a/c']); 82 | 83 | return readdir(temp()) 84 | .then(files => { 85 | cleanup(); 86 | assert.equal(files.length, 1); 87 | assert.equal(files[0], 'a'); 88 | }); 89 | }); 90 | 91 | it('should take and array of directories', () => { 92 | cleanup = createFiles(['a/a/a', 'b/b/b']); 93 | 94 | return readdir([temp('a'), temp('b')]) 95 | .then(files => { 96 | cleanup(); 97 | files.sort(); 98 | assert.equal(files.length, 2); 99 | assert.equal(files[0], 'a'); 100 | assert.equal(files[1], 'b'); 101 | }); 102 | }); 103 | }); 104 | 105 | describe('options.depth', () => { 106 | it('should recursively read files (depth: 2)', () => { 107 | cleanup = createFiles(['a/b/c/d/e', 'a/a/b/c/d']); 108 | 109 | return readdir(temp(), { depth: 2 }) 110 | .then(files => { 111 | cleanup(); 112 | files.sort(); 113 | assert.deepEqual(files, [ 'a', 'a/a', 'a/b' ].sort()); 114 | }); 115 | }); 116 | 117 | it('should recursively read files (depth: 3)', () => { 118 | cleanup = createFiles(['a/b/c/d/e', 'a/a/b/c/d']); 119 | 120 | return readdir(temp(), { depth: 3 }) 121 | .then(files => { 122 | cleanup(); 123 | files.sort(); 124 | assert.deepEqual(files, [ 'a', 'a/a', 'a/a/b', 'a/b', 'a/b/c' ].sort()); 125 | }); 126 | }); 127 | 128 | it('should recursively read files (depth: 4)', () => { 129 | cleanup = createFiles(['a/b/c/d/e', 'a/a/b/c/d']); 130 | 131 | return readdir(temp(), { depth: 4 }) 132 | .then(files => { 133 | cleanup(); 134 | files.sort(); 135 | assert.deepEqual(files, [ 'a', 'a/a', 'a/a/b', 'a/a/b/c', 'a/b', 'a/b/c', 'a/b/c/d' ].sort()); 136 | }); 137 | }); 138 | 139 | it('should recursively read files (depth: 5)', () => { 140 | cleanup = createFiles(['a/b/c/d/e', 'a/a/b/c/d']); 141 | const expected = [ 'a', 'a/a', 'a/a/b', 'a/a/b/c', 'a/b', 'a/b/c', 'a/b/c/d', 'a/b/c/d/e', 'a/a/b/c/d' ]; 142 | 143 | return readdir(temp(), { depth: 5 }) 144 | .then(files => { 145 | cleanup(); 146 | files.sort(); 147 | assert.deepEqual(files, expected.sort()); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('options.dot', () => { 153 | it('should exclude dot files when dot is false', async () => { 154 | const expected = ['a', 'a/a', 'a/a/a', 'a/a/b', 'a/a/c', '.gitignore', '.DS_Store']; 155 | cleanup = createFiles(['a/a/a', 'a/a/b', 'a/a/c', '.gitignore', '.DS_Store']); 156 | 157 | const files1 = await readdir(temp(), { recursive: true }); 158 | files1.sort(); 159 | 160 | expected.forEach(pathname => assert(files1.includes(pathname), pathname)); 161 | 162 | const files2 = await readdir(temp(), { recursive: true, dot: false }); 163 | files2.sort(); 164 | 165 | expected.forEach(pathname => { 166 | if (pathname.startsWith('.')) { 167 | assert(!files2.includes(pathname), pathname); 168 | } else { 169 | assert(files2.includes(pathname), pathname); 170 | } 171 | }); 172 | 173 | cleanup(); 174 | }); 175 | }); 176 | 177 | describe('options.filter', () => { 178 | const opts = { ...options, relative: true, symlinks: true, base: __dirname }; 179 | 180 | it('should filter symlinks with a function', async () => { 181 | try { 182 | const names = fs.readdirSync(path.join(__dirname, '..')); 183 | cleanup = createSymlinks('file', names, ['foo.js', 'bar.js']); 184 | 185 | const filter = file => !/license/i.test(file.path); 186 | const files = await readdir(temp(), { ...opts, filter }); 187 | 188 | assert(files.length > 0); 189 | 190 | // symlinks 191 | assert(files.some(name => name === 'temp/README.md')); 192 | assert(!files.some(name => name === 'temp/LICENSE')); 193 | 194 | // files 195 | assert(files.some(name => name === 'temp/foo.js')); 196 | assert(files.some(name => name === 'temp/bar.js')); 197 | 198 | } catch (err) { 199 | return Promise.reject(err); 200 | } finally { 201 | cleanup(); 202 | } 203 | }); 204 | 205 | it('should filter files with a function', () => { 206 | const filter = file => /sync/.test(file.path); 207 | 208 | return readdir(__dirname, { ...opts, filter }) 209 | .then(files => { 210 | assert(files.includes('readdir.sync.js')); 211 | assert(!files.includes('readdir.js')); 212 | }); 213 | }); 214 | 215 | it('should filter files recursively with a function', async () => { 216 | cleanup = createFiles(['c.md', 'a/a/a/a.md', 'a/a/a/c.txt', 'a/a/a/b.md', 'a/b.txt']); 217 | 218 | const filter = file => { 219 | return file.isFile() && path.extname(file.path) === '.md'; 220 | }; 221 | 222 | return readdir(temp(), { recursive: true, filter }) 223 | .then(files => { 224 | cleanup(); 225 | assert.deepEqual(files, [ 'c.md', 'a/a/a/a.md', 'a/a/a/b.md' ]); 226 | }); 227 | }); 228 | 229 | it('should filter files with a regex', () => { 230 | return readdir(__dirname, { ...opts, filter: /sync/ }) 231 | .then(files => { 232 | assert(files.includes('readdir.sync.js')); 233 | assert(!files.includes('readdir.js')); 234 | }); 235 | }); 236 | 237 | it('should filter files with an array', () => { 238 | return readdir(__dirname, { ...opts, filter: [/sync/] }) 239 | .then(files => { 240 | assert(files.includes('readdir.sync.js')); 241 | assert(!files.includes('readdir.js')); 242 | }); 243 | }); 244 | }); 245 | 246 | describe('options.recurse', () => { 247 | it('should recursively read files', () => { 248 | cleanup = createFiles(['a/a/a', 'a/a/b', 'a/a/c']); 249 | 250 | return readdir(temp(), { recursive: true }) 251 | .then(files => { 252 | cleanup(); 253 | files.sort(); 254 | assert.deepEqual(files, [ 'a', 'a/a', 'a/a/a', 'a/a/b', 'a/a/c' ].sort()); 255 | }); 256 | }); 257 | 258 | it('should get first level symlinks by default', () => { 259 | const paths = ['a/a/a', 'a/a/b', 'a/a/c']; 260 | const links = ['b/a/a', 'b/a/b', 'b/a/c']; 261 | cleanup = createFiles(paths); 262 | 263 | for (let i = 0; i < links.length; i++) { 264 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 265 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 266 | } 267 | 268 | return readdir(temp(), { recursive: true }) 269 | .then(result => { 270 | cleanup(); 271 | result.sort(); 272 | assert.deepEqual(result, [ 'a', 'b', 'a/a', 'b/a', ...paths, ...links ].sort()); 273 | }); 274 | }); 275 | }); 276 | 277 | describe('options.objects', () => { 278 | it('should return file objects', () => { 279 | return readdir(__dirname, { objects: true }) 280 | .then(files => { 281 | assert(files.some(file => file.name === 'readdir.js')); 282 | assert(files.some(file => file.name === 'fixtures')); 283 | }); 284 | }); 285 | }); 286 | 287 | describe('options.onFile', () => { 288 | it('should call options.onFile function on each file', () => { 289 | const onFile = file => { 290 | if (file.name === 'readdir.js') { 291 | file.path = path.join(path.dirname(file.path), 'foo.js'); 292 | file.name = 'foo.js'; 293 | } 294 | return file; 295 | }; 296 | 297 | return readdir(__dirname, { onFile }) 298 | .then(files => { 299 | assert(files.some(file => path.basename(file) === 'foo.js')); 300 | assert(files.some(file => path.basename(file) === 'fixtures')); 301 | }); 302 | }); 303 | 304 | it('should not keep files when file.keep is false', () => { 305 | const paths = ['a/a/a.md', 'a/a/b.txt', 'a/a/c.md', 'a/b/c/d.txt', 'a/b/b/b.md']; 306 | cleanup = createFiles(paths); 307 | 308 | const onFile = file => { 309 | file.keep = path.extname(file.path) === '.md'; 310 | }; 311 | 312 | return readdir(temp(), { onFile, nodir: true, recursive: true }) 313 | .then(files => { 314 | cleanup(); 315 | assert.deepEqual(files, [ 'a/a/a.md', 'a/a/c.md', 'a/b/b/b.md' ]); 316 | }); 317 | }); 318 | }); 319 | 320 | describe('options.onDirectory', () => { 321 | it('should call options.onDirectory function on each directory', () => { 322 | const onDirectory = file => { 323 | if (file.name === 'fixtures') { 324 | file.path = path.join(path.dirname(file.path), 'actual'); 325 | file.name = 'actual'; 326 | } 327 | }; 328 | 329 | return readdir(__dirname, { onDirectory }) 330 | .then(files => { 331 | assert(files.some(file => path.basename(file) === 'readdir.js')); 332 | assert(files.some(file => path.basename(file) === 'actual')); 333 | }); 334 | }); 335 | 336 | it('should not recurse in a directory when file.recurse is false', () => { 337 | const paths = ['a/a/a.txt', 'a/a/b.txt', 'a/a/c.txt', 'a/b/c/d.txt', 'a/b/b/b.txt']; 338 | cleanup = createFiles(paths); 339 | 340 | const onDirectory = file => { 341 | file.recurse = file.name !== 'b'; 342 | file.keep = false; 343 | }; 344 | 345 | return readdir(temp(), { recursive: true, onDirectory }) 346 | .then(files => { 347 | cleanup(); 348 | assert.deepEqual(files, [ 'a/a/a.txt', 'a/a/b.txt', 'a/a/c.txt' ]); 349 | }); 350 | }); 351 | }); 352 | 353 | describe('options.symlinks', () => { 354 | it('should get first level symlinks by default', async () => { 355 | const link = 'temp-symlink.js'; 356 | cleanup = createSymlink('file', link, ['foo.js', 'bar.js']); 357 | 358 | return readdir(temp(), { ...options, basename: true }) 359 | .then(files => { 360 | assert(files.length > 0); 361 | assert(files.some(name => name === link)); 362 | assert(files.some(name => name === 'foo.js')); 363 | assert(files.some(name => name === 'bar.js')); 364 | }); 365 | }); 366 | 367 | it('should not get first-level symlinks when disabled', () => { 368 | const paths = ['nested/a/a/a', 'nested/a/a/b', 'nested/a/a/c']; 369 | const links = ['nested/b/a/a', 'nested/b/a/b', 'nested/b/a/c']; 370 | cleanup = createFiles(paths); 371 | 372 | for (let i = 0; i < links.length; i++) { 373 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 374 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 375 | } 376 | 377 | fs.symlinkSync(temp('nested'), temp('symlinks'), 'dir'); 378 | 379 | return readdir(temp(), { recursive: true, symlinks: false }) 380 | .then(files => { 381 | cleanup(); 382 | unlinkSync(temp('symlinks')); 383 | 384 | assert(files.includes('nested')); 385 | assert(files.includes('nested/a')); 386 | assert(files.includes('nested/b')); 387 | assert(files.includes('nested/a/a')); 388 | assert(files.includes('nested/a/a/a')); 389 | 390 | assert(!files.includes('symlinks')); 391 | assert(!files.includes('symlinks/a')); 392 | assert(!files.includes('symlinks/b')); 393 | assert(!files.includes('symlinks/a/a')); 394 | assert(!files.includes('symlinks/a/a/a')); 395 | }); 396 | }); 397 | 398 | it('should return symlinked files when not disabled on options', async () => { 399 | try { 400 | const link = 'temp-symlink.js'; 401 | cleanup = createSymlink('file', link, ['foo.js', 'bar.js']); 402 | 403 | const files = await readdir(temp(), { ...options }); 404 | 405 | assert(files.length > 0); 406 | assert(files.some(name => name === link)); 407 | assert(files.some(name => name === 'foo.js')); 408 | assert(files.some(name => name === 'bar.js')); 409 | 410 | } catch (err) { 411 | return Promise.reject(err); 412 | } finally { 413 | cleanup(); 414 | } 415 | }); 416 | 417 | it('should return symlinked directories when not disabled on options', async () => { 418 | const opts = { ...options, basename: true }; 419 | 420 | try { 421 | const link = 'temp-symlink'; 422 | cleanup = createSymlink('dir', link, ['foo.js', 'bar.js']); 423 | 424 | const files = await readdir(temp(), opts); 425 | 426 | assert(files.length > 0); 427 | assert(files.some(name => name === link)); 428 | assert(files.some(name => name === 'foo.js')); 429 | assert(files.some(name => name === 'bar.js')); 430 | 431 | } catch (err) { 432 | return Promise.reject(err); 433 | } finally { 434 | cleanup(); 435 | } 436 | }); 437 | 438 | it('should ignore nested symlinked files that do not exist', async () => { 439 | const opts = { ...options, symlinks: true }; 440 | 441 | cleanup = createFiles(['foo.js', 'bar.js']); 442 | const tempfile = temp('tempfile.js'); 443 | const link = temp('link.js'); 444 | 445 | try { 446 | write.sync(tempfile, 'temp'); 447 | fs.symlinkSync(tempfile, link, 'file'); 448 | unlinkSync(tempfile); 449 | 450 | const files = await readdir(temp(), opts); 451 | 452 | assert(files.length > 0); 453 | assert(!files.some(name => name === link)); 454 | assert(files.some(name => name === 'foo.js')); 455 | assert(files.some(name => name === 'bar.js')); 456 | 457 | } catch (err) { 458 | return Promise.reject(err); 459 | } finally { 460 | cleanup(); 461 | unlinkSync(link); 462 | } 463 | }); 464 | 465 | it('should ignore nested symlinked directories that do not exist', async () => { 466 | const opts = { ...options, symlinks: true }; 467 | cleanup = createFiles(['foo.js', 'bar.js']); 468 | 469 | const tempdir = temp('tempdir/a/b/c'); 470 | const link = temp('link'); 471 | 472 | try { 473 | fs.mkdirSync(tempdir, { recursive: true }); 474 | fs.symlinkSync(tempdir, link, 'dir'); 475 | rimraf.sync(tempdir); 476 | 477 | const files = await readdir(temp(), opts); 478 | 479 | assert(files.length > 0); 480 | assert(!files.some(name => name === link)); 481 | assert(files.some(name => name === 'foo.js')); 482 | assert(files.some(name => name === 'bar.js')); 483 | 484 | } catch (err) { 485 | return Promise.reject(err); 486 | } finally { 487 | cleanup(); 488 | unlinkSync(link); 489 | } 490 | }); 491 | 492 | it('should only get first-level symlinks by default', () => { 493 | const paths = ['nested/a/a/a', 'nested/a/a/b', 'nested/a/a/c']; 494 | const links = ['nested/b/a/a', 'nested/b/a/b', 'nested/b/a/c']; 495 | cleanup = createFiles(paths); 496 | 497 | for (let i = 0; i < links.length; i++) { 498 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 499 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 500 | } 501 | 502 | fs.symlinkSync(temp('nested'), temp('symlinks'), 'dir'); 503 | 504 | return readdir(temp(), { recursive: true }) 505 | .then(files => { 506 | cleanup(); 507 | unlinkSync(temp('symlinks')); 508 | 509 | assert(files.includes('nested')); 510 | assert(files.includes('nested/a')); 511 | assert(files.includes('nested/b')); 512 | assert(files.includes('nested/a/a')); 513 | assert(files.includes('nested/a/a/a')); 514 | 515 | assert(files.includes('symlinks')); 516 | assert(!files.includes('symlinks/a')); 517 | assert(!files.includes('symlinks/b')); 518 | assert(!files.includes('symlinks/a/a')); 519 | assert(!files.includes('symlinks/a/a/a')); 520 | }); 521 | }); 522 | 523 | it('should recursively get symlinks when specified', () => { 524 | const paths = ['nested/a/a/a', 'nested/a/a/b', 'nested/a/a/c']; 525 | const links = ['nested/b/a/a', 'nested/b/a/b', 'nested/b/a/c']; 526 | cleanup = createFiles(paths); 527 | 528 | for (let i = 0; i < links.length; i++) { 529 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 530 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 531 | } 532 | 533 | fs.symlinkSync(temp('nested'), temp('symlinks'), 'dir'); 534 | 535 | return readdir(temp(), { recursive: true, follow: true }) 536 | .then(files => { 537 | cleanup(); 538 | unlinkSync(temp('symlinks')); 539 | 540 | assert(files.includes('nested'), 'should match nested'); 541 | assert(files.includes('nested/a'), 'should match nested/a'); 542 | assert(files.includes('nested/b'), 'should match nested/b'); 543 | assert(files.includes('nested/a/a'), 'should match nested/a/a'); 544 | assert(files.includes('nested/a/a/a'), 'should match nested/a/a/a'); 545 | 546 | assert(files.includes('symlinks'), 'should match symlinks'); 547 | assert(files.includes('symlinks/a'), 'should match symlinks/a'); 548 | assert(files.includes('symlinks/b'), 'should match symlinks/b'); 549 | assert(files.includes('symlinks/a/a'), 'should match symlinks/a/a'); 550 | assert(files.includes('symlinks/a/a/a'), 'should match symlinks/a/a/a'); 551 | }); 552 | }); 553 | }); 554 | 555 | describe('options.absolute', () => { 556 | it('should return absolute file paths', () => { 557 | cleanup = createFiles(['a/a/a', 'a/a/b', 'a/a/c']); 558 | 559 | return readdir('test/temp', { absolute: true, recursive: true }) 560 | .then(files => { 561 | cleanup(); 562 | 563 | assert(files.length > 1); 564 | assert(files.includes(temp('a/a/a'))); 565 | assert(files.includes(temp('a/a/b'))); 566 | assert(files.includes(temp('a/a/c'))); 567 | }); 568 | }); 569 | }); 570 | 571 | describe('options.unique', () => { 572 | it('should not return duplicates', () => { 573 | cleanup = createFiles(['a/a/a', 'a/a/b', 'a/a/c']); 574 | 575 | return readdir([temp(), temp(), temp('a'), temp(), temp(), temp('a')], { unique: true, recursive: true }) 576 | .then(files => { 577 | cleanup(); 578 | assert(files.length > 1); 579 | assert(files.includes('a/a/a')); 580 | assert(files.includes('a/a/b')); 581 | assert(files.includes('a/a/c')); 582 | files.sort(); 583 | 584 | const unique = [...new Set(files)].join(''); 585 | assert.equal(files.join(''), unique); 586 | }); 587 | }); 588 | 589 | it('should not return duplicates when "objects" is true', () => { 590 | cleanup = createFiles(['a/a/a', 'a/a/b', 'a/a/c']); 591 | 592 | return readdir([temp(), temp(), temp('a'), temp(), temp(), temp('a')], { 593 | objects: true, 594 | recursive: true, 595 | unique: true 596 | }) 597 | .then(files => { 598 | cleanup(); 599 | assert(files.length > 1); 600 | 601 | const paths = files.map(file => file.relative.replace(/\\/g, '/')); 602 | assert(paths.length > 1); 603 | assert(paths.includes('a/a/a')); 604 | assert(paths.includes('a/a/b')); 605 | assert(paths.includes('a/a/c')); 606 | paths.sort(); 607 | 608 | const unique = [...new Set(paths)].join(''); 609 | assert.equal(paths.join(''), unique); 610 | }); 611 | }); 612 | }); 613 | 614 | describe('options.realpath', () => { 615 | it('should return realpaths', () => { 616 | const paths = ['a/a/a', 'a/a/b', 'a/a/c']; 617 | const links = ['b/a/a', 'b/a/b', 'b/a/c']; 618 | cleanup = createFiles(paths); 619 | 620 | for (let i = 0; i < links.length; i++) { 621 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 622 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 623 | } 624 | 625 | return readdir(temp(), { recursive: true, realpath: true }) 626 | .then(files => { 627 | cleanup(); 628 | assert.deepEqual(files.sort(), [ 'a', 'b', 'a/a', 'b/a', ...paths, ...paths ].sort()); 629 | }); 630 | }); 631 | 632 | it('should return realpaths with no duplicates when options.unique is true', () => { 633 | const paths = ['a/a/a', 'a/a/b', 'a/a/c']; 634 | const links = ['b/a/a', 'b/a/b', 'b/a/c']; 635 | cleanup = createFiles(paths); 636 | 637 | for (let i = 0; i < links.length; i++) { 638 | fs.mkdirSync(path.dirname(temp(links[i])), { recursive: true }); 639 | fs.symlinkSync(temp(paths[i]), temp(links[i]), 'file'); 640 | } 641 | 642 | return readdir(temp(), { recursive: true, realpath: true, unique: true }) 643 | .then(files => { 644 | cleanup(); 645 | assert.deepEqual(files.sort(), [ 'a', 'b', 'a/a', 'b/a', ...paths ].sort()); 646 | }); 647 | }); 648 | }); 649 | 650 | describe('options.relative', () => { 651 | it('should get relative paths for symlinked files', async () => { 652 | const opts = { ...options, relative: true, symlinks: true, base: __dirname }; 653 | const names = fs.readdirSync(path.join(__dirname, '..')); 654 | 655 | cleanup = createSymlinks('file', names, ['foo.js', 'bar.js']); 656 | 657 | return readdir(temp(), opts) 658 | .then(files => { 659 | cleanup(); 660 | assert(files.length > 0); 661 | 662 | // symlinks 663 | assert(files.some(name => name === 'temp/README.md')); 664 | assert(files.some(name => name === 'temp/LICENSE')); 665 | 666 | // files 667 | assert(files.some(name => name === 'temp/foo.js')); 668 | assert(files.some(name => name === 'temp/bar.js')); 669 | }) 670 | .catch(err => { 671 | cleanup(); 672 | return Promise.reject(err); 673 | }); 674 | }); 675 | 676 | it('should get relative paths for symlinked files', () => { 677 | const opts = { ...options, relative: true, symlinks: true, base: __dirname }; 678 | const names = fs.readdirSync(path.join(__dirname, '..')); 679 | 680 | cleanup = createSymlinks('file', names, ['foo.js', 'bar.js']); 681 | 682 | return readdir(temp(), opts) 683 | .then(files => { 684 | cleanup(); 685 | assert(files.length > 0); 686 | // symlinks 687 | assert(files.some(name => name === 'temp/README.md')); 688 | assert(files.some(name => name === 'temp/LICENSE')); 689 | 690 | // files 691 | assert(files.some(name => name === 'temp/foo.js')); 692 | assert(files.some(name => name === 'temp/bar.js')); 693 | }) 694 | .catch(err => { 695 | cleanup(); 696 | return Promise.reject(err); 697 | }); 698 | }); 699 | }); 700 | 701 | describe('options.isMatch', () => { 702 | const opts = { ...options, relative: true, symlinks: true, base: __dirname }; 703 | 704 | it('should match symlinks with a function', async () => { 705 | try { 706 | const names = fs.readdirSync(path.join(__dirname, '..')); 707 | cleanup = createSymlinks('file', names, ['foo.js', 'bar.js']); 708 | 709 | const isMatch = file => !/license/i.test(file.path); 710 | const files = await readdir(temp(), { ...opts, isMatch }); 711 | 712 | assert(files.length > 0); 713 | // symlinks 714 | assert(files.some(name => name === 'temp/README.md')); 715 | assert(!files.some(name => name === 'temp/LICENSE')); 716 | 717 | // files 718 | assert(files.some(name => name === 'temp/foo.js')); 719 | assert(files.some(name => name === 'temp/bar.js')); 720 | 721 | } catch (err) { 722 | return Promise.reject(err); 723 | } finally { 724 | cleanup(); 725 | } 726 | }); 727 | 728 | it('should match files with a function', () => { 729 | const isMatch = file => /sync/.test(file.path); 730 | 731 | return readdir(__dirname, { ...opts, isMatch }) 732 | .then(files => { 733 | assert(files.includes('readdir.sync.js')); 734 | assert(!files.includes('readdir.js')); 735 | }); 736 | }); 737 | 738 | it('should match files recursively with a function', async () => { 739 | cleanup = createFiles(['c.md', 'a/a/a/a.md', 'a/a/a/c.txt', 'a/a/a/b.md', 'a/b.txt']); 740 | 741 | const isMatch = file => { 742 | return file.isFile() && path.extname(file.path) === '.md'; 743 | }; 744 | 745 | return readdir(temp(), { recursive: true, isMatch }) 746 | .then(files => { 747 | cleanup(); 748 | assert.deepEqual(files, [ 'c.md', 'a/a/a/a.md', 'a/a/a/b.md' ]); 749 | }); 750 | }); 751 | 752 | it('should match files with a regex', () => { 753 | return readdir(__dirname, { ...opts, isMatch: /sync/ }) 754 | .then(files => { 755 | assert(files.includes('readdir.sync.js')); 756 | assert(!files.includes('readdir.js')); 757 | }); 758 | }); 759 | 760 | it('should match files with an array', () => { 761 | return readdir(__dirname, { ...opts, isMatch: [/sync/] }) 762 | .then(files => { 763 | assert(files.includes('readdir.sync.js')); 764 | assert(!files.includes('readdir.js')); 765 | }); 766 | }); 767 | 768 | it('should keep matching files', () => { 769 | cleanup = createFiles(['b/b/b.txt', 'a/a/a.txt', 'c/c/c.txt']); 770 | 771 | const isMatch = file => { 772 | return file.isFile() && file.name !== 'b.txt'; 773 | }; 774 | 775 | return readdir('test/temp', { absolute: true, recursive: true, isMatch }) 776 | .then(files => { 777 | cleanup(); 778 | assert(files.length > 1); 779 | assert(files.includes(temp('a/a/a.txt'))); 780 | assert(!files.includes(temp('b/b/b.txt'))); 781 | assert(files.includes(temp('c/c/c.txt'))); 782 | }); 783 | }); 784 | 785 | it('should keep matching directories', () => { 786 | cleanup = createFiles(['bb/b/b', 'aa/a/a', 'cc/c/c']); 787 | 788 | const isMatch = file => { 789 | return !file.relative.startsWith('bb'); 790 | }; 791 | 792 | return readdir('test/temp', { absolute: true, recursive: true, isMatch }) 793 | .then(files => { 794 | cleanup(); 795 | assert(files.length > 1); 796 | assert(files.includes(temp('aa/a/a'))); 797 | assert(!files.includes(temp('bb/b/b'))); 798 | assert(files.includes(temp('cc/c/c'))); 799 | }); 800 | }); 801 | 802 | it('should take an array of functions', () => { 803 | cleanup = createFiles(['bb/b/b', 'aa/a/a', 'cc/c/c']); 804 | 805 | const a = file => file.relative.startsWith('aa'); 806 | const b = file => file.relative.startsWith('bb'); 807 | 808 | return readdir('test/temp', { absolute: true, recursive: true, isMatch: [a, b] }) 809 | .then(files => { 810 | cleanup(); 811 | assert(files.length > 1); 812 | assert(files.includes(temp('aa/a/a'))); 813 | assert(files.includes(temp('bb/b/b'))); 814 | assert(!files.includes(temp('cc/c/c'))); 815 | }); 816 | }); 817 | 818 | it('should take an array of regular expressions', () => { 819 | cleanup = createFiles(['bb/b/b', 'aa/a/a', 'cc/c/c']); 820 | 821 | const a = file => /^aa(\/|\\|$)/.test(file.relative); 822 | const b = file => /^bb(\/|\\|$)/.test(file.relative); 823 | 824 | return readdir('test/temp', { absolute: true, recursive: true, isMatch: [a, b] }) 825 | .then(files => { 826 | cleanup(); 827 | assert(files.length > 1); 828 | assert(files.includes(temp('aa/a/a'))); 829 | assert(files.includes(temp('bb/b/b'))); 830 | assert(!files.includes(temp('cc/c/c'))); 831 | }); 832 | }); 833 | }); 834 | }); 835 | --------------------------------------------------------------------------------