├── .editorconfig ├── .eslintrc.yml ├── .gitattributes ├── .github └── workflows │ └── CI-CD.yaml ├── .gitignore ├── .mocharc.yaml ├── .nycrc.yml ├── .vscode ├── launch.json └── tasks.json ├── 404.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _config.yml ├── dist ├── index.d.ts ├── index.js └── package.json ├── package-lock.json ├── package.json ├── src ├── async │ ├── for-each.ts │ └── index.ts ├── call.ts ├── directory-reader.ts ├── index.ts ├── iterator │ ├── index.ts │ └── pending.ts ├── normalize-options.ts ├── stat.ts ├── stream │ └── index.ts ├── sync │ ├── for-each.ts │ ├── fs.ts │ └── index.ts ├── types-internal.ts └── types-public.ts ├── test ├── dir │ ├── .dotfile │ ├── empty.txt │ ├── file.json │ ├── file.txt │ └── subdir │ │ ├── .dotdir │ │ └── .dotfile │ │ ├── file.txt │ │ └── subsubdir │ │ ├── empty.txt │ │ ├── file.json │ │ └── file.txt ├── fixtures │ └── init.js ├── specs │ ├── basePath.spec.js │ ├── deep.spec.js │ ├── default.spec.js │ ├── errors.spec.js │ ├── exports.spec.js │ ├── filter.spec.js │ ├── fs.spec.js │ ├── sep.spec.js │ ├── stats.spec.js │ ├── stream.spec.js │ └── typescript-definition.spec.ts └── utils │ ├── dir.js │ ├── for-each-api.js │ └── is-stats.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor config 2 | # http://EditorConfig.org 3 | 4 | # This EditorConfig overrides any parent EditorConfigs 5 | root = true 6 | 7 | # Default rules applied to all file types 8 | [*] 9 | 10 | # No trailing spaces, newline at EOF 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | end_of_line = lf 15 | 16 | # 2 space indentation 17 | indent_style = space 18 | indent_size = 2 19 | 20 | # JavaScript-specific settings 21 | [*.{js,ts}] 22 | quote_type = double 23 | continuation_indent_size = 2 24 | curly_bracket_next_line = false 25 | indent_brace_style = BSD 26 | spaces_around_operators = true 27 | spaces_around_brackets = none 28 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | # ESLint config 2 | # http://eslint.org/docs/user-guide/configuring 3 | # https://jstools.dev/eslint-config/ 4 | 5 | root: true 6 | extends: "@jsdevtools" 7 | env: 8 | node: true 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Git attributes 2 | # https://git-scm.com/docs/gitattributes 3 | # https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes 4 | 5 | # Normalize line endings for all files that git determines to be text. 6 | # https://git-scm.com/docs/gitattributes#gitattributes-Settostringvalueauto 7 | * text=auto 8 | 9 | # Normalize line endings to LF on checkin, and do NOT convert to CRLF when checking-out on Windows. 10 | # https://git-scm.com/docs/gitattributes#gitattributes-Settostringvaluelf 11 | *.txt text eol=lf 12 | *.html text eol=lf 13 | *.md text eol=lf 14 | *.css text eol=lf 15 | *.scss text eol=lf 16 | *.map text eol=lf 17 | *.js text eol=lf 18 | *.jsx text eol=lf 19 | *.ts text eol=lf 20 | *.tsx text eol=lf 21 | *.json text eol=lf 22 | *.yml text eol=lf 23 | *.yaml text eol=lf 24 | *.xml text eol=lf 25 | *.svg text eol=lf 26 | -------------------------------------------------------------------------------- /.github/workflows/CI-CD.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow 2 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions 3 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions 4 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions 5 | 6 | name: CI-CD 7 | 8 | on: 9 | push: 10 | branches: 11 | - "*" 12 | tags-ignore: 13 | - "*" 14 | 15 | schedule: 16 | - cron: "0 0 1 * *" 17 | 18 | jobs: 19 | test: 20 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 21 | runs-on: ${{ matrix.os }} 22 | timeout-minutes: 10 23 | strategy: 24 | fail-fast: true 25 | matrix: 26 | os: 27 | - ubuntu-latest 28 | - macos-latest 29 | - windows-latest 30 | node: 31 | - 10 32 | - 12 33 | 34 | steps: 35 | - name: Checkout source 36 | uses: actions/checkout@v2 37 | 38 | - name: Install Node ${{ matrix.node }} 39 | uses: actions/setup-node@v1 40 | with: 41 | node-version: ${{ matrix.node }} 42 | 43 | - name: Install dependencies 44 | run: npm ci 45 | 46 | - name: Run linters 47 | run: npm run lint 48 | 49 | - name: Build the code 50 | run: npm run build 51 | 52 | - name: Run TypeScript tests 53 | run: npm run test:typescript 54 | 55 | - name: Run JavaScript tests 56 | run: npm run coverage 57 | 58 | - name: Send code coverage results to Coveralls 59 | uses: coverallsapp/github-action@v1.1.0 60 | with: 61 | github-token: ${{ secrets.GITHUB_TOKEN }} 62 | parallel: true 63 | 64 | coverage: 65 | name: Code Coverage 66 | runs-on: ubuntu-latest 67 | timeout-minutes: 10 68 | needs: test 69 | steps: 70 | - name: Let Coveralls know that all tests have finished 71 | uses: coverallsapp/github-action@v1.1.0 72 | with: 73 | github-token: ${{ secrets.GITHUB_TOKEN }} 74 | parallel-finished: true 75 | 76 | deploy: 77 | name: Publish to NPM 78 | if: github.ref == 'refs/heads/master' 79 | runs-on: ubuntu-latest 80 | timeout-minutes: 10 81 | needs: test 82 | 83 | steps: 84 | - name: Checkout source 85 | uses: actions/checkout@v2 86 | 87 | - name: Install Node 88 | uses: actions/setup-node@v1 89 | 90 | - name: Install dependencies 91 | run: npm ci 92 | 93 | - name: Build the code 94 | run: npm run build 95 | 96 | - name: Publish to NPM 97 | uses: JS-DevTools/npm-publish@v1 98 | with: 99 | token: ${{ secrets.NPM_TOKEN }} 100 | 101 | - name: Prepare the non-scoped packaged 102 | run: | 103 | cp LICENSE *.md dist 104 | VERSION=$(node -e "console.log(require('./package.json').version)") 105 | sed -i "s/X.X.X/${VERSION}/g" dist/package.json 106 | 107 | - name: Publish the non-scoped package to NPM 108 | uses: JS-DevTools/npm-publish@v1 109 | with: 110 | token: ${{ secrets.NPM_TOKEN }} 111 | package: dist/package.json 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Git ignore 2 | # https://git-scm.com/docs/gitignore 3 | 4 | # Miscellaneous 5 | *~ 6 | *# 7 | .DS_STORE 8 | Thumbs.db 9 | .netbeans 10 | nbproject 11 | .node_history 12 | 13 | # IDEs & Text Editors 14 | .idea 15 | .sublime-* 16 | .vscode/settings.json 17 | .netbeans 18 | nbproject 19 | 20 | # Temporary files 21 | .tmp 22 | .temp 23 | .grunt 24 | .lock-wscript 25 | 26 | # Logs 27 | /logs 28 | *.log 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | 35 | # Dependencies 36 | node_modules 37 | 38 | # Build output 39 | /lib 40 | 41 | # Test output 42 | /.nyc_output 43 | /coverage 44 | /test/dir/**/*symlink* 45 | -------------------------------------------------------------------------------- /.mocharc.yaml: -------------------------------------------------------------------------------- 1 | # Mocha options 2 | # https://mochajs.org/#configuring-mocha-nodejs 3 | # https://github.com/mochajs/mocha/blob/master/example/config/.mocharc.yml 4 | 5 | 6 | spec: 7 | # Test fixtures 8 | - test/fixtures/*.js 9 | 10 | # Test specs 11 | - test/specs/**/*.spec.js 12 | 13 | # Abort after first test failure 14 | bail: true 15 | async-only: true 16 | -------------------------------------------------------------------------------- /.nycrc.yml: -------------------------------------------------------------------------------- 1 | # NYC config 2 | # https://github.com/istanbuljs/nyc#configuration-files 3 | 4 | extension: 5 | - .js 6 | - .ts 7 | 8 | reporter: 9 | - text 10 | - lcov 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // VSCode Launch Configuration 2 | // https://code.visualstudio.com/docs/editor/debugging#_launch-configurations 3 | 4 | // Available variables which can be used inside of strings. 5 | // ${workspaceRoot}: the root folder of the team 6 | // ${file}: the current opened file 7 | // ${fileBasename}: the current opened file's basename 8 | // ${fileDirname}: the current opened file's dirname 9 | // ${fileExtname}: the current opened file's extension 10 | // ${cwd}: the current working directory of the spawned process 11 | 12 | { 13 | "version": "0.2.0", 14 | "configurations": [ 15 | { 16 | "type": "node", 17 | "request": "launch", 18 | "name": "Run Mocha", 19 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 20 | "args": [ 21 | "--timeout=60000", 22 | "--retries=0", 23 | ], 24 | "outFiles": [ 25 | "${workspaceFolder}/lib/**/*.js" 26 | ], 27 | "smartStep": true, 28 | "skipFiles": [ 29 | "/**/*.js" 30 | ], 31 | }, 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // VSCode Tasks 2 | // https://code.visualstudio.com/docs/editor/tasks 3 | 4 | // Available variables which can be used inside of strings. 5 | // ${workspaceRoot}: the root folder of the team 6 | // ${file}: the current opened file 7 | // ${fileBasename}: the current opened file's basename 8 | // ${fileDirname}: the current opened file's dirname 9 | // ${fileExtname}: the current opened file's extension 10 | // ${cwd}: the current working directory of the spawned process 11 | 12 | { 13 | "version": "2.0.0", 14 | "command": "npm", 15 | "tasks": [ 16 | { 17 | "type": "npm", 18 | "script": "build", 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | }, 23 | "problemMatcher": "$tsc" 24 | }, 25 | 26 | 27 | { 28 | "type": "npm", 29 | "script": "test", 30 | "group": { 31 | "kind": "test", 32 | "isDefault": true 33 | }, 34 | }, 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /404.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: 404 3 | --- 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ==================================================================================================== 3 | All notable changes will be documented in this file. 4 | Readdir Enhanced adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | 7 | [v6.0.0](https://github.com/JS-DevTools/readdir-enhanced/tree/v6.0.0) (2020-02-17) 8 | ---------------------------------------------------------------------------------------------------- 9 | 10 | - Moved Readdir Enhanced to the [@JSDevTools scope](https://www.npmjs.com/org/jsdevtools) on NPM 11 | 12 | - The "readdir-enhanced" NPM package is now just a wrapper around the scoped "@jsdevtools/readdir-enhanced" package 13 | 14 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v5.1.1...v6.0.0) 15 | 16 | 17 | [v5.1.0](https://github.com/JS-DevTools/readdir-enhanced/tree/v5.1.0) (2019-11-07) 18 | ---------------------------------------------------------------------------------------------------- 19 | 20 | - The [`filter` option](README.md#filter) can now be set to a boolean to include/exclude everything. 21 | 22 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v5.0.1...v5.1.0) 23 | 24 | 25 | [v5.0.0](https://github.com/JS-DevTools/readdir-enhanced/tree/v5.0.0) (2019-11-03) 26 | ---------------------------------------------------------------------------------------------------- 27 | 28 | #### Breaking Changes 29 | 30 | - Previously there were alternative versions of each function that returned [`fs.Stats` objects](https://nodejs.org/api/fs.html#fs_class_fs_stats) rather than path strings. These functions have been replaced by [the `stats` option](README.md#stats). 31 | 32 | #### Other Changes 33 | 34 | - Completely rewritten in TypeScript 35 | 36 | - Added an [async iterable interface](README.md#pick-your-api) so you can now crawl directories using convenient [`for await...of` syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of). This is faster and more efficient that the normal sync or async interfaces, since it doesn't require buffering all results in memory. 37 | 38 | 39 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v4.0.3...v5.0.0) 40 | 41 | 42 | [v4.0.0](https://github.com/JS-DevTools/readdir-enhanced/tree/v4.0.0) (2019-08-19) 43 | ---------------------------------------------------------------------------------------------------- 44 | #### Breaking Changes 45 | 46 | - Removed [code](https://github.com/JS-DevTools/readdir-enhanced/commit/a35044d3399697d47ff20aee6f59bb48c355986d) that was stripping the drive letters from Windows paths when using glob filters. 47 | 48 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v3.0.1...v4.0.0) 49 | 50 | 51 | [v3.0.0](https://github.com/JS-DevTools/readdir-enhanced/tree/v3.0.0) (2019-06-13) 52 | ---------------------------------------------------------------------------------------------------- 53 | #### Breaking Changes 54 | 55 | - Dropped support for Node 6 56 | 57 | - Updated all code to ES6+ syntax (async/await, template literals, arrow functions, etc.) 58 | 59 | #### Other Changes 60 | 61 | - Added [TypeScript definitions](lib/index.d.ts) 62 | 63 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v2.2.4...v3.0.0) 64 | 65 | 66 | [v2.2.0](https://github.com/JS-DevTools/readdir-enhanced/tree/v2.2.0) (2018-01-09) 67 | ---------------------------------------------------------------------------------------------------- 68 | - Refactored the codebase to use ES6 syntax (Node v4.x compatible) 69 | 70 | - You can now provide [your own implementation](https://github.com/JS-DevTools/readdir-enhanced#custom-fs-methods) for the [filesystem module](https://nodejs.org/api/fs.html) that's used by `readdir-enhanced`. Just set the `fs` option to your implementation. Thanks to [@mrmlnc](https://github.com/mrmlnc) for the idea and [the PR](https://github.com/JS-DevTools/readdir-enhanced/pull/10)! 71 | 72 | - [Better error handling](https://github.com/JS-DevTools/readdir-enhanced/commit/0d330b68524bafbdeae11566a3e8af1bc3f184bf), especially around user-specified logic, such as `options.deep`, `options.filter`, and `options.fs` 73 | 74 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v2.1.0...v2.2.0) 75 | 76 | 77 | [v2.1.0](https://github.com/JS-DevTools/readdir-enhanced/tree/v2.1.0) (2017-12-01) 78 | ---------------------------------------------------------------------------------------------------- 79 | - The `fs.Stats` objects now include a `depth` property, which indicates the number of subdirectories beneath the base path. Thanks to [@mrmlnc](https://github.com/mrmlnc) for [the PR](https://github.com/JS-DevTools/readdir-enhanced/pull/8)! 80 | 81 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v2.0.0...v2.1.0) 82 | 83 | 84 | [v2.0.0](https://github.com/JS-DevTools/readdir-enhanced/tree/v2.0.0) (2017-11-15) 85 | ---------------------------------------------------------------------------------------------------- 86 | - Dropped support for Node v0.x, which is no longer actively maintained. Please upgrade to Node 4 or newer. 87 | 88 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v1.5.0...v2.0.0) 89 | 90 | 91 | [v1.5.0](https://github.com/JS-DevTools/readdir-enhanced/tree/v1.5.0) (2017-04-10) 92 | ---------------------------------------------------------------------------------------------------- 93 | The [`deep` option](README.md#deep) can now be set to a [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp), a [glob pattern](https://github.com/isaacs/node-glob#glob-primer), or a function, which allows you to customize which subdirectories get crawled. Of course, you can also still still set the `deep` option to `true` to crawl _all_ subdirectories, or a number if you just want to limit the recursion depth. 94 | 95 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v1.4.0...v1.5.0) 96 | 97 | 98 | [v1.4.0](https://github.com/JS-DevTools/readdir-enhanced/tree/v1.4.0) (2016-08-26) 99 | ---------------------------------------------------------------------------------------------------- 100 | The [`filter` option](README.md#filter) can now be set to a regular expression or a glob pattern string, which simplifies filtering based on file names. Of course, you can still set the `filter` option to a function if you need to perform more advanced filtering based on the [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) of each file. 101 | 102 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v1.3.4...v1.4.0) 103 | 104 | 105 | [v1.3.4](https://github.com/JS-DevTools/readdir-enhanced/tree/v1.3.4) (2016-08-26) 106 | ---------------------------------------------------------------------------------------------------- 107 | As of this release, `readdir-enhanced` is fully tested on all major Node versions (0.x, 4.x, 5.x, 6.x) on [linux](https://travis-ci.org/JS-DevTools/readdir-enhanced) and [Windows](https://ci.appveyor.com/project/JamesMessinger/readdir-enhanced/branch/master), with [nearly 100% code coverage](https://coveralls.io/github/JS-DevTools/readdir-enhanced?branch=master). I do all of my local development and testing on MacOS, so that's covered too. 108 | 109 | [Full Changelog](https://github.com/JS-DevTools/readdir-enhanced/compare/v1.0.1...v1.3.4) 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 James Messinger 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Enhanced `fs.readdir()` 2 | ======================= 3 | 4 | [![Cross-Platform Compatibility](https://jstools.dev/img/badges/os-badges.svg)](https://github.com/JS-DevTools/readdir-enhanced/actions) 5 | [![Build Status](https://github.com/JS-DevTools/readdir-enhanced/workflows/CI-CD/badge.svg)](https://github.com/JS-DevTools/readdir-enhanced/actions) 6 | 7 | [![Coverage Status](https://coveralls.io/repos/github/JS-DevTools/readdir-enhanced/badge.svg?branch=master)](https://coveralls.io/github/JS-DevTools/readdir-enhanced?branch=master) 8 | [![Dependencies](https://david-dm.org/JS-DevTools/readdir-enhanced.svg)](https://david-dm.org/JS-DevTools/readdir-enhanced) 9 | 10 | [![npm](https://img.shields.io/npm/v/@jsdevtools/readdir-enhanced.svg)](https://www.npmjs.com/package/@jsdevtools/readdir-enhanced) 11 | [![License](https://img.shields.io/npm/l/@jsdevtools/readdir-enhanced.svg)](LICENSE) 12 | [![Buy us a tree](https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-lightgreen)](https://plant.treeware.earth/JS-DevTools/readdir-enhanced) 13 | 14 | 15 | 16 | Features 17 | ---------------------------------- 18 | - Fully [**backward-compatible**](#backward-compatible) drop-in replacement for [`fs.readdir()`](https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback) and [`fs.readdirSync()`](https://nodejs.org/api/fs.html#fs_fs_readdirsync_path_options) 19 | 20 | - Can [crawl sub-directories](#deep) - you can even control which ones 21 | 22 | - Supports [filtering results](#filter) using globs, regular expressions, or custom logic 23 | 24 | - Can return [absolute paths](#basepath) 25 | 26 | - Can return [`fs.Stats` objects](#stats) rather than just paths 27 | 28 | - Exposes additional APIs: [Promise, Stream, EventEmitter, and Async Iterator](#pick-your-api). 29 | 30 | 31 | 32 | Example 33 | ---------------------------------- 34 | 35 | ```javascript 36 | import readdir from "@jsdevtools/readdir-enhanced"; 37 | import through2 from "through2"; 38 | 39 | // Synchronous API 40 | let files = readdir.sync("my/directory"); 41 | 42 | // Callback API 43 | readdir.async("my/directory", (err, files) => { ... }); 44 | 45 | // Promises API 46 | readdir.async("my/directory") 47 | .then((files) => { ... }) 48 | .catch((err) => { ... }); 49 | 50 | // Async/Await API 51 | let files = await readdir.async("my/directory"); 52 | 53 | // Async Iterator API 54 | for await (let item of readdir.iterator("my/directory")) { 55 | ... 56 | } 57 | 58 | // EventEmitter API 59 | readdir.stream("my/directory") 60 | .on("data", (path) => { ... }) 61 | .on("file", (path) => { ... }) 62 | .on("directory", (path) => { ... }) 63 | .on("symlink", (path) => { ... }) 64 | .on("error", (err) => { ... }); 65 | 66 | // Streaming API 67 | let stream = readdir.stream("my/directory") 68 | .pipe(through2.obj(function(data, enc, next) { 69 | console.log(data); 70 | this.push(data); 71 | next(); 72 | }); 73 | ``` 74 | 75 | 76 | 77 | Installation 78 | -------------------------- 79 | Install using [npm](https://docs.npmjs.com/about-npm/): 80 | 81 | ```bash 82 | npm install @jsdevtools/readdir-enhanced 83 | ``` 84 | 85 | 86 | 87 | Pick Your API 88 | ---------------------------------- 89 | Readdir Enhanced has multiple APIs, so you can pick whichever one you prefer. Here are some things to consider about each API: 90 | 91 | |Function|Returns|Syntax|[Blocks the thread?](#blocking-the-thread)|[Buffers results?](#buffered-results)| 92 | |---|---|---|---|---| 93 | |`readdirSync()`
`readdir.sync()`|Array|Synchronous|yes|yes| 94 | |`readdir()`
`readdir.async()`
`readdirAsync()`|[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises)|[`async/await`](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await)
[`Promise.then()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises)
[callback](https://nodejs.org/en/knowledge/getting-started/control-flow/what-are-callbacks/)|no|yes| 95 | |`readdir.iterator()`
`readdirIterator()`|[Iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)|[`for await...of`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of)|no|no| 96 | |`readdir.stream()`
`readdirStream()`|[Readable Stream](https://nodejs.org/api/stream.html#stream_readable_streams)|[`stream.on("data")`](https://nodejs.org/api/stream.html#stream_event_data)
[`stream.read()`](https://nodejs.org/api/stream.html#stream_readable_read_size)
[`stream.pipe()`](https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options)|no|no| 97 | 98 | ### Blocking the Thread 99 | The synchronous API blocks the thread until all results have been read. Only use this if you know the directory does not contain many items, or if your program needs the results before it can do anything else. 100 | 101 | ### Buffered Results 102 | Some APIs buffer the results, which means you get all the results at once (as an array). This can be more convenient to work with, but it can also consume a significant amount of memory, depending on how many results there are. The non-buffered APIs return each result to you one-by-one, which means you can start processing the results even while the directory is still being read. 103 | 104 | 105 | 106 | Alias Exports 107 | ---------------------------------- 108 | The [example above](#example) imported the `readdir` default export and used its properties, such as `readdir.sync` or `readdir.async` to call specific APIs. For convenience, each of the different APIs is exported as a named function that you can import directly. 109 | 110 | - `readdir.sync()` is also exported as `readdirSync()` 111 | - `readdir.async()` is also exported as `readdirAsync()` 112 | - `readdir.iterator()` is also exported as `readdirIterator()` 113 | - `readdir.stream()` is also exported as `readdirStream()` 114 | 115 | Here's how to import named exports rather than the default export: 116 | 117 | ```javascript 118 | import { readdirSync, readdirAsync, readdirIterator, readdirStream } from "@jsdevtools/readdir-enhanced"; 119 | ``` 120 | 121 | 122 | 123 | 124 | Enhanced Features 125 | ---------------------------------- 126 | Readdir Enhanced adds several features to the built-in `fs.readdir()` function. All of the enhanced features are opt-in, which makes Readdir Enhanced [fully backward compatible by default](#backward-compatible). You can enable any of the features by passing-in an `options` argument as the second parameter. 127 | 128 | 129 | 130 | 131 | Crawl Subdirectories 132 | ---------------------------------- 133 | By default, Readdir Enhanced will only return the top-level contents of the starting directory. But you can set the `deep` option to recursively traverse the subdirectories and return their contents as well. 134 | 135 | ### Crawl ALL subdirectories 136 | 137 | The `deep` option can be set to `true` to traverse the entire directory structure. 138 | 139 | ```javascript 140 | import readdir from "@jsdevtools/readdir-enhanced"; 141 | 142 | readdir("my/directory", {deep: true}, (err, files) => { 143 | console.log(files); 144 | // => subdir1 145 | // => subdir1/file.txt 146 | // => subdir1/subdir2 147 | // => subdir1/subdir2/file.txt 148 | // => subdir1/subdir2/subdir3 149 | // => subdir1/subdir2/subdir3/file.txt 150 | }); 151 | ``` 152 | 153 | ### Crawl to a specific depth 154 | The `deep` option can be set to a number to only traverse that many levels deep. For example, calling `readdir("my/directory", {deep: 2})` will return `subdir1/file.txt` and `subdir1/subdir2/file.txt`, but it _won't_ return `subdir1/subdir2/subdir3/file.txt`. 155 | 156 | ```javascript 157 | import readdir from "@jsdevtools/readdir-enhanced"; 158 | 159 | readdir("my/directory", {deep: 2}, (err, files) => { 160 | console.log(files); 161 | // => subdir1 162 | // => subdir1/file.txt 163 | // => subdir1/subdir2 164 | // => subdir1/subdir2/file.txt 165 | // => subdir1/subdir2/subdir3 166 | }); 167 | ``` 168 | 169 | ### Crawl subdirectories by name 170 | For simple use-cases, you can use a [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) or a [glob pattern](https://github.com/isaacs/node-glob#glob-primer) to crawl only the directories whose path matches the pattern. The path is relative to the starting directory by default, but you can customize this via [`options.basePath`](#basepath). 171 | 172 | > **NOTE:** Glob patterns [_always_ use forward-slashes](https://github.com/isaacs/node-glob#windows), even on Windows. This _does not_ apply to regular expressions though. Regular expressions should use the appropraite path separator for the environment. Or, you can match both types of separators using `[\\/]`. 173 | 174 | ```javascript 175 | import readdir from "@jsdevtools/readdir-enhanced"; 176 | 177 | // Only crawl the "lib" and "bin" subdirectories 178 | // (notice that the "node_modules" subdirectory does NOT get crawled) 179 | readdir("my/directory", {deep: /lib|bin/}, (err, files) => { 180 | console.log(files); 181 | // => bin 182 | // => bin/cli.js 183 | // => lib 184 | // => lib/index.js 185 | // => node_modules 186 | // => package.json 187 | }); 188 | ``` 189 | 190 | ### Custom recursion logic 191 | For more advanced recursion, you can set the `deep` option to a function that accepts an [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object and returns a truthy value if the starting directory should be crawled. 192 | 193 | > **NOTE:** The [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object that's passed to the function has additional `path` and `depth` properties. The `path` is relative to the starting directory by default, but you can customize this via [`options.basePath`](#basepath). The `depth` is the number of subdirectories beneath the base path (see [`options.deep`](#deep)). 194 | 195 | ```javascript 196 | import readdir from "@jsdevtools/readdir-enhanced"; 197 | 198 | // Crawl all subdirectories, except "node_modules" 199 | function ignoreNodeModules (stats) { 200 | return stats.path.indexOf("node_modules") === -1; 201 | } 202 | 203 | readdir("my/directory", {deep: ignoreNodeModules}, (err, files) => { 204 | console.log(files); 205 | // => bin 206 | // => bin/cli.js 207 | // => lib 208 | // => lib/index.js 209 | // => node_modules 210 | // => package.json 211 | }); 212 | ``` 213 | 214 | 215 | 216 | 217 | Filtering 218 | ---------------------------------- 219 | The `filter` option lets you limit the results based on any criteria you want. 220 | 221 | ### Filter by name 222 | For simple use-cases, you can use a [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) or a [glob pattern](https://github.com/isaacs/node-glob#glob-primer) to filter items by their path. The path is relative to the starting directory by default, but you can customize this via [`options.basePath`](#basepath). 223 | 224 | > **NOTE:** Glob patterns [_always_ use forward-slashes](https://github.com/isaacs/node-glob#windows), even on Windows. This _does not_ apply to regular expressions though. Regular expressions should use the appropraite path separator for the environment. Or, you can match both types of separators using `[\\/]`. 225 | 226 | ```javascript 227 | import readdir from "@jsdevtools/readdir-enhanced"; 228 | 229 | // Find all .txt files 230 | readdir("my/directory", {filter: "*.txt"}); 231 | 232 | // Find all package.json files 233 | readdir("my/directory", {filter: "**/package.json", deep: true}); 234 | 235 | // Find everything with at least one number in the name 236 | readdir("my/directory", {filter: /\d+/}); 237 | ``` 238 | 239 | ### Custom filtering logic 240 | For more advanced filtering, you can specify a filter function that accepts an [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object and returns a truthy value if the item should be included in the results. 241 | 242 | > **NOTE:** The [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object that's passed to the filter function has additional `path` and `depth` properties. The `path` is relative to the starting directory by default, but you can customize this via [`options.basePath`](#basepath). The `depth` is the number of subdirectories beneath the base path (see [`options.deep`](#deep)). 243 | 244 | ```javascript 245 | import readdir from "@jsdevtools/readdir-enhanced"; 246 | 247 | // Only return file names containing an underscore 248 | function myFilter(stats) { 249 | return stats.isFile() && stats.path.indexOf("_") >= 0; 250 | } 251 | 252 | readdir("my/directory", {filter: myFilter}, (err, files) => { 253 | console.log(files); 254 | // => __myFile.txt 255 | // => my_other_file.txt 256 | // => img_1.jpg 257 | // => node_modules 258 | }); 259 | ``` 260 | 261 | 262 | 263 | 264 | Get `fs.Stats` objects instead of strings 265 | ------------------------------------------------------------ 266 | All of the Readdir Enhanced functions listed above return an array of strings (paths). But in some situations, the path isn't enough information. Setting the `stats` option returns an array of [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) objects instead of path strings. The `fs.Stats` object contains all sorts of useful information, such as the size, the creation date/time, and helper methods such as `isFile()`, `isDirectory()`, `isSymbolicLink()`, etc. 267 | 268 | > **NOTE:** The [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) objects that are returned also have additional `path` and `depth` properties. The `path` is relative to the starting directory by default, but you can customize this via [`options.basePath`](#basepath). The `depth` is the number of subdirectories beneath the base path (see [`options.deep`](#deep)). 269 | 270 | ```javascript 271 | import readdir from "@jsdevtools/readdir-enhanced"; 272 | 273 | readdir("my/directory", { stats: true }, (err, stats) => { 274 | for (let stat of stats) { 275 | console.log(`${stat.path} was created at ${stat.birthtime}`); 276 | } 277 | }); 278 | ``` 279 | 280 | 281 | 282 | 283 | Base Path 284 | ---------------------------------- 285 | By default all Readdir Enhanced functions return paths that are relative to the starting directory. But you can use the `basePath` option to customize this. The `basePath` will be prepended to all of the returned paths. One common use-case for this is to set `basePath` to the absolute path of the starting directory, so that all of the returned paths will be absolute. 286 | 287 | ```javascript 288 | import readdir from "@jsdevtools/readdir-enhanced"; 289 | import { resolve } from "path"; 290 | 291 | // Get absolute paths 292 | let absPath = resolve("my/dir"); 293 | readdir("my/directory", {basePath: absPath}, (err, files) => { 294 | console.log(files); 295 | // => /absolute/path/to/my/directory/file1.txt 296 | // => /absolute/path/to/my/directory/file2.txt 297 | // => /absolute/path/to/my/directory/subdir 298 | }); 299 | 300 | // Get paths relative to the working directory 301 | readdir("my/directory", {basePath: "my/directory"}, (err, files) => { 302 | console.log(files); 303 | // => my/directory/file1.txt 304 | // => my/directory/file2.txt 305 | // => my/directory/subdir 306 | }); 307 | ``` 308 | 309 | 310 | 311 | 312 | Path Separator 313 | ---------------------------------- 314 | By default, Readdir Enhanced uses the correct path separator for your OS (`\` on Windows, `/` on Linux & MacOS). But you can set the `sep` option to any separator character(s) that you want to use instead. This is usually used to ensure consistent path separators across different OSes. 315 | 316 | ```javascript 317 | import readdir from "@jsdevtools/readdir-enhanced"; 318 | 319 | // Always use Windows path separators 320 | readdir("my/directory", {sep: "\\", deep: true}, (err, files) => { 321 | console.log(files); 322 | // => subdir1 323 | // => subdir1\file.txt 324 | // => subdir1\subdir2 325 | // => subdir1\subdir2\file.txt 326 | // => subdir1\subdir2\subdir3 327 | // => subdir1\subdir2\subdir3\file.txt 328 | }); 329 | ``` 330 | 331 | 332 | 333 | 334 | Custom FS methods 335 | ---------------------------------- 336 | By default, Readdir Enhanced uses the default [Node.js FileSystem module](https://nodejs.org/api/fs.html) for methods like `fs.stat`, `fs.readdir` and `fs.lstat`. But in some situations, you can want to use your own FS methods (FTP, SSH, remote drive and etc). So you can provide your own implementation of FS methods by setting `options.fs` or specific methods, such as `options.fs.stat`. 337 | 338 | ```javascript 339 | import readdir from "@jsdevtools/readdir-enhanced"; 340 | 341 | function myCustomReaddirMethod(dir, callback) { 342 | callback(null, ["__myFile.txt"]); 343 | } 344 | 345 | let options = { 346 | fs: { 347 | readdir: myCustomReaddirMethod 348 | } 349 | }; 350 | 351 | readdir("my/directory", options, (err, files) => { 352 | console.log(files); 353 | // => __myFile.txt 354 | }); 355 | ``` 356 | 357 | 358 | 359 | 360 | Backward Compatible 361 | ------------------------------------- 362 | Readdir Enhanced is fully backward-compatible with Node.js' built-in `fs.readdir()` and `fs.readdirSync()` functions, so you can use it as a drop-in replacement in existing projects without affecting existing functionality, while still being able to use the enhanced features as needed. 363 | 364 | ```javascript 365 | import { readdir, readdirSync } from "@jsdevtools/readdir-enhanced"; 366 | 367 | // Use it just like Node's built-in fs.readdir function 368 | readdir("my/directory", (er, files) => { ... }); 369 | 370 | // Use it just like Node's built-in fs.readdirSync function 371 | let files = readdirSync("my/directory"); 372 | ``` 373 | 374 | 375 | 376 | A Note on Streams 377 | ---------------------------------- 378 | The Readdir Enhanced streaming API follows the Node.js streaming API. A lot of questions around the streaming API can be answered by reading the [Node.js documentation.](https://nodejs.org/api/stream.html). However, we've tried to answer the most common questions here. 379 | 380 | ### Stream Events 381 | 382 | All events in the Node.js streaming API are supported by Readdir Enhanced. These events include "end", "close", "drain", "error", plus more. [An exhaustive list of events is available in the Node.js documentation.](https://nodejs.org/api/stream.html#stream_class_stream_readable) 383 | 384 | #### Detect when the Stream has finished 385 | 386 | Using these events, we can detect when the stream has finished reading files. 387 | 388 | ```javascript 389 | import readdir from "@jsdevtools/readdir-enhanced"; 390 | 391 | // Build the stream using the Streaming API 392 | let stream = readdir.stream("my/directory") 393 | .on("data", (path) => { ... }); 394 | 395 | // Listen to the end event to detect the end of the stream 396 | stream.on("end", () => { 397 | console.log("Stream finished!"); 398 | }); 399 | ``` 400 | 401 | ### Paused Streams vs. Flowing Streams 402 | 403 | As with all Node.js streams, a Readdir Enhanced stream starts in "paused mode". For the stream to start emitting files, you'll need to switch it to "flowing mode". 404 | 405 | There are many ways to trigger flowing mode, such as adding a `stream.data()` handler, using `stream.pipe()` or calling `stream.resume()`. 406 | 407 | Unless you trigger flowing mode, your stream will stay paused and you won't receive any file events. 408 | 409 | [More information on paused vs. flowing mode can be found in the Node.js documentation.](https://nodejs.org/api/stream.html#stream_two_reading_modes) 410 | 411 | 412 | 413 | Contributing 414 | -------------------------- 415 | Contributions, enhancements, and bug-fixes are welcome! [Open an issue](https://github.com/JS-DevTools/readdir-enhanced/issues) on GitHub and [submit a pull request](https://github.com/JS-DevTools/readdir-enhanced/pulls). 416 | 417 | #### Building 418 | To build the project locally on your computer: 419 | 420 | 1. __Clone this repo__
421 | `git clone https://github.com/JS-DevTools/readdir-enhanced.git` 422 | 423 | 2. __Install dependencies__
424 | `npm install` 425 | 426 | 3. __Run the tests__
427 | `npm test` 428 | 429 | 430 | 431 | License 432 | -------------------------- 433 | Readdir Enhanced is 100% free and open-source, under the [MIT license](LICENSE). Use it however you want. 434 | 435 | This package is [Treeware](http://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**](https://plant.treeware.earth/JS-DevTools/readdir-enhanced) to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats. 436 | 437 | 438 | 439 | Big Thanks To 440 | -------------------------- 441 | Thanks to these awesome companies for their support of Open Source developers ❤ 442 | 443 | [![Travis CI](https://jstools.dev/img/badges/travis-ci.svg)](https://travis-ci.com) 444 | [![SauceLabs](https://jstools.dev/img/badges/sauce-labs.svg)](https://saucelabs.com) 445 | [![Coveralls](https://jstools.dev/img/badges/coveralls.svg)](https://coveralls.io) 446 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: JS-DevTools/gh-pages-theme 2 | 3 | title: Enhanced fs.readdir 4 | logo: https://jstools.dev/img/logos/logo.png 5 | 6 | author: 7 | twitter: JSDevTools 8 | 9 | google_analytics: UA-68102273-3 10 | 11 | twitter: 12 | username: JSDevTools 13 | card: summary 14 | 15 | defaults: 16 | - scope: 17 | path: "" 18 | values: 19 | image: https://jstools.dev/img/logos/card.png 20 | - scope: 21 | path: "test/**/*" 22 | values: 23 | sitemap: false 24 | 25 | plugins: 26 | - jekyll-sitemap 27 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import readdir from "@jsdevtools/readdir-enhanced"; 2 | export * from "@jsdevtools/readdir-enhanced"; 3 | export default readdir; 4 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = require("@jsdevtools/readdir-enhanced"); 3 | -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "readdir-enhanced", 3 | "version": "X.X.X", 4 | "description": "fs.readdir with sync, async, streaming, and async iterator APIs + filtering, recursion, absolute paths, etc.", 5 | "keywords": [ 6 | "fs", 7 | "readdir", 8 | "async", 9 | "promise", 10 | "iterator", 11 | "generator", 12 | "async-iterator", 13 | "stream", 14 | "event", 15 | "event-emitter", 16 | "recursive", 17 | "deep", 18 | "walk", 19 | "crawl", 20 | "filter", 21 | "absolute" 22 | ], 23 | "author": { 24 | "name": "James Messinger", 25 | "url": "https://jamesmessinger.com" 26 | }, 27 | "license": "MIT", 28 | "homepage": "https://jstools.dev/readdir-enhanced", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/JS-DevTools/readdir-enhanced.git" 32 | }, 33 | "main": "index.js", 34 | "types": "index.d.ts", 35 | "files": [ 36 | "index.js", 37 | "index.d.ts" 38 | ], 39 | "engines": { 40 | "node": ">=10" 41 | }, 42 | "dependencies": { 43 | "@jsdevtools/readdir-enhanced": "X.X.X" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jsdevtools/readdir-enhanced", 3 | "version": "6.0.4", 4 | "description": "fs.readdir with sync, async, streaming, and async iterator APIs + filtering, recursion, absolute paths, etc.", 5 | "keywords": [ 6 | "fs", 7 | "readdir", 8 | "async", 9 | "promise", 10 | "iterator", 11 | "generator", 12 | "async-iterator", 13 | "stream", 14 | "event", 15 | "event-emitter", 16 | "recursive", 17 | "deep", 18 | "walk", 19 | "crawl", 20 | "filter", 21 | "absolute" 22 | ], 23 | "author": { 24 | "name": "James Messinger", 25 | "url": "https://jamesmessinger.com" 26 | }, 27 | "license": "MIT", 28 | "homepage": "https://jstools.dev/readdir-enhanced", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/JS-DevTools/readdir-enhanced.git" 32 | }, 33 | "main": "lib/index.js", 34 | "types": "lib/index.d.ts", 35 | "files": [ 36 | "lib" 37 | ], 38 | "scripts": { 39 | "clean": "shx rm -rf .nyc_output coverage lib", 40 | "lint": "eslint src test", 41 | "build": "tsc", 42 | "watch": "tsc --watch", 43 | "test": "npm run test:node && npm run test:typescript && npm run lint", 44 | "test:node": "mocha", 45 | "test:typescript": "tsc --noEmit --strict --lib esnext test/specs/typescript-definition.spec.ts", 46 | "coverage": "nyc node_modules/mocha/bin/mocha", 47 | "upgrade": "npm-check -u && npm audit fix", 48 | "bump": "bump --tag --push --all", 49 | "release": "npm run upgrade && npm run clean && npm run build && npm test && npm run bump" 50 | }, 51 | "engines": { 52 | "node": ">=10" 53 | }, 54 | "devDependencies": { 55 | "@jsdevtools/eslint-config": "^1.0.4", 56 | "@jsdevtools/version-bump-prompt": "^6.0.5", 57 | "@types/chai": "^4.2.11", 58 | "@types/mocha": "^8.0.0", 59 | "@types/node": "^14.0.23", 60 | "chai": "^4.2.0", 61 | "del": "^5.1.0", 62 | "eslint": "^7.4.0", 63 | "mocha": "^8.0.1", 64 | "npm-check": "^5.9.2", 65 | "nyc": "^15.1.0", 66 | "shx": "^0.3.2", 67 | "through2": "^4.0.2", 68 | "typescript": "^3.9.7" 69 | }, 70 | "dependencies": { 71 | "@jsdevtools/file-path-filter": "^3.0.2" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/async/for-each.ts: -------------------------------------------------------------------------------- 1 | import { Iterator, VoidCallback } from "../types-internal"; 2 | 3 | /** 4 | * Simultaneously processes all items in the given array. 5 | * 6 | * @param array - The array to iterate over 7 | * @param iterator - The function to call for each item in the array 8 | * @param done - The function to call when all iterators have completed 9 | * 10 | * @internal 11 | */ 12 | export function asyncForEach(array: T[], iterator: Iterator, done: VoidCallback): void { 13 | if (!Array.isArray(array)) { 14 | throw new TypeError(`${array} is not an array`); 15 | } 16 | 17 | if (array.length === 0) { 18 | // NOTE: Normally a bad idea to mix sync and async, but it's safe here because 19 | // of the way that this method is currently used by DirectoryReader. 20 | done(); 21 | return; 22 | } 23 | 24 | // Simultaneously process all items in the array. 25 | let pending = array.length; 26 | for (let item of array) { 27 | iterator(item, callback); 28 | } 29 | 30 | function callback() { 31 | if (--pending === 0) { 32 | done(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/async/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { DirectoryReader } from "../directory-reader"; 3 | import { Callback, Options, Stats } from "../types-public"; 4 | import { asyncForEach as forEach } from "./for-each"; 5 | 6 | const asyncFacade = { fs, forEach }; 7 | 8 | /** 9 | * A backward-compatible drop-in replacement for Node's built-in `fs.readdir()` function 10 | * that adds support for additional features like filtering, recursion, absolute paths, etc. 11 | */ 12 | export function readdirAsync(dir: string, callback: Callback): void; 13 | 14 | /** 15 | * A backward-compatible drop-in replacement for Node's built-in `fs.readdir()` function 16 | * that adds support for additional features like filtering, recursion, absolute paths, etc. 17 | */ 18 | export function readdirAsync(dir: string, options: undefined, callback: Callback): void; 19 | 20 | /** 21 | * A backward-compatible drop-in replacement for Node's built-in `fs.readdir()` function 22 | * that adds support for additional features like filtering, recursion, absolute paths, etc. 23 | */ 24 | export function readdirAsync(dir: string, options: Options & { stats?: false }, callback: Callback): void; 25 | 26 | /** 27 | * Asynchronous `readdir()` that returns an array of `Stats` objects via a callback. 28 | */ 29 | export function readdirAsync(dir: string, options: Options & { stats: true }, callback: Callback): void; 30 | 31 | /** 32 | * Asynchronous `readdir()` that returns its results via a Promise. 33 | */ 34 | export function readdirAsync(dir: string, options?: Options & { stats?: false }): Promise; 35 | 36 | /** 37 | * Asynchronous `readdir()` that returns an array of `Stats` objects via a Promise. 38 | */ 39 | export function readdirAsync(dir: string, options: Options & { stats: true }): Promise; 40 | 41 | export function readdirAsync(dir: string, options: Options | Callback | undefined, callback?: Callback): Promise | void { 42 | if (typeof options === "function") { 43 | callback = options; 44 | options = undefined; 45 | } 46 | 47 | let promise = new Promise((resolve, reject) => { 48 | let results: T[] = []; 49 | let reader = new DirectoryReader(dir, options as Options, asyncFacade); 50 | let stream = reader.stream; 51 | 52 | stream.on("error", (err: Error) => { 53 | reject(err); 54 | stream.pause(); 55 | }); 56 | stream.on("data", (result: T) => { 57 | results.push(result); 58 | }); 59 | stream.on("end", () => { 60 | resolve(results); 61 | }); 62 | }); 63 | 64 | if (callback) { 65 | promise.then( 66 | (results: T[]) => callback!(null, results), 67 | (err: Error) => callback!(err, undefined as unknown as T[]) 68 | ); 69 | } 70 | else { 71 | return promise; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/call.ts: -------------------------------------------------------------------------------- 1 | import { Callback } from "./types-public"; 2 | 3 | /** 4 | * A function that accepts an input value and returns an output value via a callback. 5 | * @internal 6 | */ 7 | export type Fn = (input: TInput, callback: Callback) => void; 8 | 9 | 10 | /** 11 | * Calls a function with the given arguments, and ensures that the error-first callback is _always_ 12 | * invoked exactly once, even if the function throws an error. 13 | * 14 | * @param fn - The function to invoke 15 | * @param args - The arguments to pass to the function. The final argument must be a callback function. 16 | * 17 | * @internal 18 | */ 19 | export function safeCall(fn: Fn, input: TInput, callback: Callback): void { 20 | // Replace the callback function with a wrapper that ensures it will only be called once 21 | callback = callOnce(callback); 22 | 23 | try { 24 | fn(input, callback); 25 | } 26 | catch (err) { 27 | callback(err as Error, undefined as unknown as TOutput); 28 | } 29 | } 30 | 31 | 32 | /** 33 | * Returns a wrapper function that ensures the given callback function is only called once. 34 | * Subsequent calls are ignored, unless the first argument is an Error, in which case the 35 | * error is thrown. 36 | * 37 | * @param callback - The function that should only be called once 38 | * 39 | * @internal 40 | */ 41 | export function callOnce(callback: Callback): Callback { 42 | let fulfilled = false; 43 | 44 | return function onceWrapper(this: unknown, err: Error | null, result: T) { 45 | if (!fulfilled) { 46 | fulfilled = true; 47 | callback.call(this, err as Error, result); 48 | } 49 | else if (err) { 50 | // The callback has already been called, but now an error has occurred 51 | // (most likely inside the callback function). So re-throw the error, 52 | // so it gets handled further up the call stack 53 | throw err; 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/directory-reader.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { Readable } from "stream"; 3 | import { safeCall } from "./call"; 4 | import { NormalizedOptions, normalizeOptions } from "./normalize-options"; 5 | import { stat } from "./stat"; 6 | import { Facade, VoidCallback } from "./types-internal"; 7 | import { EventName, Options, Stats } from "./types-public"; 8 | 9 | interface Directory { 10 | path: string; 11 | basePath: string; 12 | depth: number; 13 | } 14 | 15 | interface Chunk { 16 | data: Stats | string; 17 | file?: boolean; 18 | directory?: boolean; 19 | symlink?: boolean; 20 | } 21 | 22 | /** 23 | * Asynchronously reads the contents of a directory and streams the results 24 | * via a `ReadableStream`. 25 | * 26 | * @internal 27 | */ 28 | export class DirectoryReader { 29 | public stream: Readable; 30 | private options: NormalizedOptions; 31 | private shouldRead: boolean; 32 | private pending: number; 33 | private buffer: Chunk[]; 34 | private queue: Directory[]; 35 | 36 | /** 37 | * @param dir - The absolute or relative directory path to read 38 | * @param [options] - User-specified options, if any (see `normalizeOptions()`) 39 | * @param facade - sync or async function implementations 40 | * @param emit - Indicates whether the reader should emit "file", "directory", and "symlink" events. 41 | */ 42 | public constructor(dir: string, options: Options | undefined, facade: Facade, emit = false) { 43 | this.options = normalizeOptions(options, facade, emit); 44 | 45 | // Indicates whether we should keep reading 46 | // This is set false if stream.Readable.push() returns false. 47 | this.shouldRead = true; 48 | 49 | // The directories to read 50 | // (initialized with the top-level directory) 51 | this.queue = [{ 52 | path: dir, 53 | basePath: this.options.basePath, 54 | depth: 0 55 | }]; 56 | 57 | // The number of directories that are currently being processed 58 | this.pending = 0; 59 | 60 | // The data that has been read, but not yet emitted 61 | this.buffer = []; 62 | 63 | this.stream = new Readable({ objectMode: true }); 64 | this.stream._read = () => { 65 | // Start (or resume) reading 66 | this.shouldRead = true; 67 | 68 | // If we have data in the buffer, then send the next chunk 69 | if (this.buffer.length > 0) { 70 | this.pushFromBuffer(); 71 | } 72 | 73 | // If we have directories queued, then start processing the next one 74 | if (this.queue.length > 0) { 75 | this.readNextDirectory(); 76 | } 77 | 78 | this.checkForEOF(); 79 | }; 80 | } 81 | 82 | /** 83 | * Reads the next directory in the queue 84 | */ 85 | private readNextDirectory() { 86 | let { facade } = this.options; 87 | let dir = this.queue.shift()!; 88 | this.pending++; 89 | 90 | // Read the directory listing 91 | safeCall(facade.fs.readdir, dir.path, (err: Error | null, items: string[]) => { 92 | if (err) { 93 | // fs.readdir threw an error 94 | this.emit("error", err); 95 | return this.finishedReadingDirectory(); 96 | } 97 | 98 | try { 99 | // Process each item in the directory (simultaneously, if async) 100 | facade.forEach( 101 | items, 102 | this.processItem.bind(this, dir), 103 | this.finishedReadingDirectory.bind(this, dir) 104 | ); 105 | } 106 | catch (err2) { 107 | // facade.forEach threw an error 108 | // (probably because fs.readdir returned an invalid result) 109 | this.emit("error", err2); 110 | this.finishedReadingDirectory(); 111 | } 112 | }); 113 | } 114 | 115 | /** 116 | * This method is called after all items in a directory have been processed. 117 | * 118 | * NOTE: This does not necessarily mean that the reader is finished, since there may still 119 | * be other directories queued or pending. 120 | */ 121 | private finishedReadingDirectory() { 122 | this.pending--; 123 | 124 | if (this.shouldRead) { 125 | // If we have directories queued, then start processing the next one 126 | if (this.queue.length > 0) { 127 | this.readNextDirectory(); 128 | } 129 | 130 | this.checkForEOF(); 131 | } 132 | } 133 | 134 | /** 135 | * Determines whether the reader has finished processing all items in all directories. 136 | * If so, then the "end" event is fired (via {@Readable#push}) 137 | */ 138 | private checkForEOF() { 139 | if (this.buffer.length === 0 && // The stuff we've already read 140 | this.pending === 0 && // The stuff we're currently reading 141 | this.queue.length === 0) { // The stuff we haven't read yet 142 | // There's no more stuff! 143 | this.stream.push(null); 144 | } 145 | } 146 | 147 | /** 148 | * Processes a single item in a directory. 149 | * 150 | * If the item is a directory, and `option.deep` is enabled, then the item will be added 151 | * to the directory queue. 152 | * 153 | * If the item meets the filter criteria, then it will be emitted to the reader's stream. 154 | * 155 | * @param dir - A directory object from the queue 156 | * @param item - The name of the item (name only, no path) 157 | * @param done - A callback function that is called after the item has been processed 158 | */ 159 | private processItem(dir: Directory, item: string, done: VoidCallback) { 160 | let stream = this.stream; 161 | let options = this.options; 162 | 163 | let itemPath = dir.basePath + item; 164 | let fullPath = path.join(dir.path, item); 165 | 166 | // If `options.deep` is a number, and we've already recursed to the max depth, 167 | // then there's no need to check fs.Stats to know if it's a directory. 168 | // If `options.deep` is a function, then we'll need fs.Stats 169 | let maxDepthReached = dir.depth >= options.recurseDepth; 170 | 171 | // Do we need to call `fs.stat`? 172 | let needStats = 173 | !maxDepthReached || // we need the fs.Stats to know if it's a directory 174 | options.stats || // the user wants fs.Stats objects returned 175 | options.recurseFnNeedsStats || // we need fs.Stats for the recurse function 176 | options.filterFnNeedsStats || // we need fs.Stats for the filter function 177 | stream.listenerCount("file") || // we need the fs.Stats to know if it's a file 178 | stream.listenerCount("directory") || // we need the fs.Stats to know if it's a directory 179 | stream.listenerCount("symlink"); // we need the fs.Stats to know if it's a symlink 180 | 181 | // If we don't need stats, then exit early 182 | if (!needStats) { 183 | if (this.filter({ path: itemPath } as unknown as Stats)) { 184 | this.pushOrBuffer({ data: itemPath }); 185 | } 186 | return done(); 187 | } 188 | 189 | // Get the fs.Stats object for this path 190 | stat(options.facade.fs, fullPath, (err: Error | null, stats: Stats) => { 191 | if (err) { 192 | // fs.stat threw an error 193 | this.emit("error", err); 194 | return done(); 195 | } 196 | 197 | try { 198 | // Add the item's path to the fs.Stats object 199 | // The base of this path, and its separators are determined by the options 200 | // (i.e. options.basePath and options.sep) 201 | stats.path = itemPath; 202 | 203 | // Add depth of the path to the fs.Stats object for use this in the filter function 204 | stats.depth = dir.depth; 205 | 206 | if (this.shouldRecurse(stats, maxDepthReached)) { 207 | // Add this subdirectory to the queue 208 | this.queue.push({ 209 | path: fullPath, 210 | basePath: itemPath + options.sep, 211 | depth: dir.depth + 1, 212 | }); 213 | } 214 | 215 | // Determine whether this item matches the filter criteria 216 | if (this.filter(stats)) { 217 | this.pushOrBuffer({ 218 | data: options.stats ? stats : itemPath, 219 | file: stats.isFile(), 220 | directory: stats.isDirectory(), 221 | symlink: stats.isSymbolicLink(), 222 | }); 223 | } 224 | 225 | done(); 226 | } 227 | catch (err2) { 228 | // An error occurred while processing the item 229 | // (probably during a user-specified function, such as options.deep, options.filter, etc.) 230 | this.emit("error", err2); 231 | done(); 232 | } 233 | }); 234 | } 235 | 236 | /** 237 | * Pushes the given chunk of data to the stream, or adds it to the buffer, 238 | * depending on the state of the stream. 239 | */ 240 | private pushOrBuffer(chunk: Chunk) { 241 | // Add the chunk to the buffer 242 | this.buffer.push(chunk); 243 | 244 | // If we're still reading, then immediately emit the next chunk in the buffer 245 | // (which may or may not be the chunk that we just added) 246 | if (this.shouldRead) { 247 | this.pushFromBuffer(); 248 | } 249 | } 250 | 251 | /** 252 | * Immediately pushes the next chunk in the buffer to the reader's stream. 253 | * The "data" event will always be fired (via `Readable.push()`). 254 | * In addition, the "file", "directory", and/or "symlink" events may be fired, 255 | * depending on the type of properties of the chunk. 256 | */ 257 | private pushFromBuffer() { 258 | let stream = this.stream; 259 | let chunk = this.buffer.shift()!; 260 | 261 | // Stream the data 262 | try { 263 | this.shouldRead = stream.push(chunk.data); 264 | } 265 | catch (err) { 266 | this.emit("error", err); 267 | } 268 | 269 | if (this.options.emit) { 270 | // Also emit specific events, based on the type of chunk 271 | chunk.file && this.emit("file", chunk.data); 272 | chunk.symlink && this.emit("symlink", chunk.data); 273 | chunk.directory && this.emit("directory", chunk.data); 274 | } 275 | } 276 | 277 | /** 278 | * Determines whether the given directory meets the user-specified recursion criteria. 279 | * If the user didn't specify recursion criteria, then this function will default to true. 280 | * 281 | * @param stats - The directory's `Stats` object 282 | * @param maxDepthReached - Whether we've already crawled the user-specified depth 283 | */ 284 | private shouldRecurse(stats: Stats, maxDepthReached: boolean): boolean | undefined { 285 | let { recurseFn } = this.options; 286 | 287 | if (maxDepthReached) { 288 | // We've already crawled to the maximum depth. So no more recursion. 289 | return false; 290 | } 291 | else if (!stats.isDirectory()) { 292 | // It's not a directory. So don't try to crawl it. 293 | return false; 294 | } 295 | else if (recurseFn) { 296 | try { 297 | // Run the user-specified recursion criteria 298 | return !!recurseFn(stats); 299 | } 300 | catch (err) { 301 | // An error occurred in the user's code. 302 | // In Sync and Async modes, this will return an error. 303 | // In Streaming mode, we emit an "error" event, but continue processing 304 | this.emit("error", err); 305 | } 306 | } 307 | else { 308 | // No recursion function was specified, and we're within the maximum depth. 309 | // So crawl this directory. 310 | return true; 311 | } 312 | } 313 | 314 | /** 315 | * Determines whether the given item meets the user-specified filter criteria. 316 | * If the user didn't specify a filter, then this function will always return true. 317 | * 318 | * @param stats - The item's `Stats` object, or an object with just a `path` property 319 | */ 320 | private filter(stats: Stats): boolean | undefined { 321 | let { filterFn } = this.options; 322 | 323 | if (filterFn) { 324 | try { 325 | // Run the user-specified filter function 326 | return !!filterFn(stats); 327 | } 328 | catch (err) { 329 | // An error occurred in the user's code. 330 | // In Sync and Async modes, this will return an error. 331 | // In Streaming mode, we emit an "error" event, but continue processing 332 | this.emit("error", err); 333 | } 334 | } 335 | else { 336 | // No filter was specified, so match everything 337 | return true; 338 | } 339 | } 340 | 341 | /** 342 | * Emits an event. If one of the event listeners throws an error, 343 | * then an "error" event is emitted. 344 | */ 345 | private emit(eventName: EventName, data: unknown): void { 346 | let stream = this.stream; 347 | 348 | try { 349 | stream.emit(eventName, data); 350 | } 351 | catch (err) { 352 | if (eventName === "error") { 353 | // Don't recursively emit "error" events. 354 | // If the first one fails, then just throw 355 | throw err; 356 | } 357 | else { 358 | stream.emit("error", err); 359 | } 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { readdirAsync } from "./async"; 2 | import { readdirIterator } from "./iterator"; 3 | import { readdirStream } from "./stream"; 4 | import { readdirSync } from "./sync"; 5 | 6 | /** 7 | * Enhanced `fs.readdir()` 8 | */ 9 | export type Readdir = typeof readdirAsync & { 10 | sync: typeof readdirSync; 11 | async: typeof readdirAsync; 12 | stream: typeof readdirStream; 13 | iterator: typeof readdirIterator; 14 | }; 15 | 16 | const readdir = readdirAsync as Readdir; 17 | readdir.sync = readdirSync; 18 | readdir.async = readdirAsync; 19 | readdir.stream = readdirStream; 20 | readdir.iterator = readdirIterator; 21 | 22 | export { readdirAsync } from "./async"; 23 | export { readdirIterator } from "./iterator"; 24 | export { readdirStream } from "./stream"; 25 | export { readdirSync } from "./sync"; 26 | export * from "./types-public"; 27 | export { readdir }; 28 | export default readdir; 29 | 30 | // CommonJS default export hack 31 | /* eslint-env commonjs */ 32 | if (typeof module === "object" && typeof module.exports === "object") { 33 | module.exports = Object.assign(module.exports.default, module.exports); 34 | } 35 | -------------------------------------------------------------------------------- /src/iterator/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { asyncForEach as forEach } from "../async/for-each"; 3 | import { DirectoryReader } from "../directory-reader"; 4 | import { Options, Stats } from "../types-public"; 5 | import { Pending, pending } from "./pending"; 6 | 7 | const iteratorFacade = { fs, forEach }; 8 | 9 | /** 10 | * Aynchronous `readdir()` that returns an `AsyncIterableIterator` (an object that implements 11 | * both the `AsyncIterable` and `AsyncIterator` interfaces) that yields path strings. 12 | * 13 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators 14 | */ 15 | export function readdirIterator(dir: string, options?: Options & { stats?: false }): AsyncIterableIterator; 16 | 17 | /** 18 | * Aynchronous `readdir()` that returns an `AsyncIterableIterator` (an object that implements 19 | * both the `AsyncIterable` and `AsyncIterator` interfaces) that yields `Stats` objects. 20 | * 21 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators 22 | */ 23 | export function readdirIterator(dir: string, options: Options & { stats: true }): AsyncIterableIterator; 24 | 25 | export function readdirIterator(dir: string, options?: Options): AsyncIterableIterator { 26 | let reader = new DirectoryReader(dir, options, iteratorFacade); 27 | let stream = reader.stream; 28 | let pendingValues: T[] = []; 29 | let pendingReads: Array>> = []; 30 | let error: Error | undefined; 31 | let readable = false; 32 | let done = false; 33 | 34 | stream.on("error", function streamError(err: Error) { 35 | error = err; 36 | stream.pause(); 37 | fulfillPendingReads(); 38 | }); 39 | 40 | stream.on("end", function streamEnd() { 41 | done = true; 42 | fulfillPendingReads(); 43 | }); 44 | 45 | stream.on("readable", function streamReadable() { 46 | readable = true; 47 | fulfillPendingReads(); 48 | }); 49 | 50 | return { 51 | [Symbol.asyncIterator]() { 52 | return this; 53 | }, 54 | 55 | next() { 56 | let pendingRead = pending>(); 57 | pendingReads.push(pendingRead); 58 | 59 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 60 | Promise.resolve().then(fulfillPendingReads); 61 | 62 | return pendingRead.promise; 63 | } 64 | }; 65 | 66 | function fulfillPendingReads() { 67 | if (error) { 68 | while (pendingReads.length > 0) { 69 | let pendingRead = pendingReads.shift()!; 70 | pendingRead.reject(error); 71 | } 72 | } 73 | else if (pendingReads.length > 0) { 74 | while (pendingReads.length > 0) { 75 | let pendingRead = pendingReads.shift()!; 76 | let value = getNextValue(); 77 | 78 | if (value) { 79 | pendingRead.resolve({ value }); 80 | } 81 | else if (done) { 82 | pendingRead.resolve({ done, value }); 83 | } 84 | else { 85 | pendingReads.unshift(pendingRead); 86 | break; 87 | } 88 | } 89 | } 90 | } 91 | 92 | function getNextValue(): T | undefined { 93 | let value = pendingValues.shift(); 94 | if (value) { 95 | return value; 96 | } 97 | else if (readable) { 98 | readable = false; 99 | 100 | while (true) { 101 | value = stream.read() as T | undefined; 102 | if (value) { 103 | pendingValues.push(value); 104 | } 105 | else { 106 | break; 107 | } 108 | } 109 | 110 | return pendingValues.shift(); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/iterator/pending.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a `Promise` and the functions to resolve or reject it. 3 | * @internal 4 | */ 5 | export function pending(): Pending { 6 | let resolve: Resolve, reject: Reject; 7 | 8 | let promise = new Promise((res, rej) => { 9 | resolve = res; 10 | reject = rej; 11 | }); 12 | 13 | return { 14 | promise, 15 | resolve(result) { 16 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 17 | Promise.resolve(result).then(resolve); 18 | }, 19 | reject(reason: Error) { 20 | Promise.reject(reason).catch(reject); 21 | } 22 | }; 23 | } 24 | 25 | /** 26 | * A pending `Promise`, and the functions to resolve or reject it. 27 | * @internal 28 | */ 29 | export interface Pending { 30 | promise: Promise; 31 | 32 | /** 33 | * Resolves the promise with the given value. 34 | */ 35 | resolve(result: T | PromiseLike): void; 36 | 37 | /** 38 | * Rejects the promise with the given reason. 39 | */ 40 | reject(reason: Error): void; 41 | } 42 | 43 | type Resolve = (result: T | PromiseLike) => void; 44 | type Reject = (error: Error) => void; 45 | -------------------------------------------------------------------------------- /src/normalize-options.ts: -------------------------------------------------------------------------------- 1 | import { createFilter } from "@jsdevtools/file-path-filter"; 2 | import * as path from "path"; 3 | import { Facade } from "./types-internal"; 4 | import { FilterFunction, Options, Stats } from "./types-public"; 5 | 6 | /** 7 | * Normalized and sanitized options. 8 | * @internal 9 | */ 10 | export interface NormalizedOptions { 11 | recurseDepth: number; 12 | recurseFn?: FilterFunction; 13 | recurseFnNeedsStats: boolean; 14 | filterFn?: FilterFunction; 15 | filterFnNeedsStats: boolean; 16 | sep: string; 17 | basePath: string; 18 | facade: Facade; 19 | emit: boolean; 20 | stats: boolean; 21 | } 22 | 23 | /** 24 | * Validates and normalizes the options argument 25 | * 26 | * @param [options] - User-specified options, if any 27 | * @param facade - sync or async function implementations 28 | * @param emit - Indicates whether the reader should emit "file", "directory", and "symlink" events. 29 | * 30 | * @internal 31 | */ 32 | export function normalizeOptions(options: Options | undefined, facade: Facade, emit: boolean): NormalizedOptions { 33 | if (options === null || options === undefined) { 34 | options = {}; 35 | } 36 | else if (typeof options !== "object") { 37 | throw new TypeError("options must be an object"); 38 | } 39 | 40 | let sep = options.sep; 41 | if (sep === null || sep === undefined) { 42 | sep = path.sep; 43 | } 44 | else if (typeof sep !== "string") { 45 | throw new TypeError("options.sep must be a string"); 46 | } 47 | 48 | let stats = Boolean(options.stats || options.withFileTypes); 49 | 50 | let recurseDepth, recurseFn, recurseFnNeedsStats = false, deep = options.deep; 51 | if (deep === null || deep === undefined) { 52 | recurseDepth = 0; 53 | } 54 | else if (typeof deep === "boolean") { 55 | recurseDepth = deep ? Infinity : 0; 56 | } 57 | else if (typeof deep === "number") { 58 | if (deep < 0 || isNaN(deep)) { 59 | throw new Error("options.deep must be a positive number"); 60 | } 61 | else if (Math.floor(deep) !== deep) { 62 | throw new Error("options.deep must be an integer"); 63 | } 64 | else { 65 | recurseDepth = deep; 66 | } 67 | } 68 | else if (typeof deep === "function") { 69 | // Recursion functions require a Stats object 70 | recurseFnNeedsStats = true; 71 | recurseDepth = Infinity; 72 | recurseFn = deep; 73 | } 74 | else if (deep instanceof RegExp || (typeof deep === "string" && deep.length > 0)) { 75 | recurseDepth = Infinity; 76 | recurseFn = createFilter({ map, sep }, deep); 77 | } 78 | else { 79 | throw new TypeError("options.deep must be a boolean, number, function, regular expression, or glob pattern"); 80 | } 81 | 82 | let filterFn, filterFnNeedsStats = false, filter = options.filter; 83 | if (filter !== null && filter !== undefined) { 84 | if (typeof filter === "function") { 85 | // Filter functions requres a Stats object 86 | filterFnNeedsStats = true; 87 | filterFn = filter; 88 | } 89 | else if ( 90 | filter instanceof RegExp || 91 | typeof filter === "boolean" || 92 | (typeof filter === "string" && filter.length > 0)) { 93 | filterFn = createFilter({ map, sep }, filter); 94 | } 95 | else { 96 | throw new TypeError("options.filter must be a boolean, function, regular expression, or glob pattern"); 97 | } 98 | } 99 | 100 | let basePath = options.basePath; 101 | if (basePath === null || basePath === undefined) { 102 | basePath = ""; 103 | } 104 | else if (typeof basePath === "string") { 105 | // Append a path separator to the basePath, if necessary 106 | if (basePath && basePath.substr(-1) !== sep) { 107 | basePath += sep; 108 | } 109 | } 110 | else { 111 | throw new TypeError("options.basePath must be a string"); 112 | } 113 | 114 | // Determine which facade methods to use 115 | if (options.fs === null || options.fs === undefined) { 116 | // The user didn't provide their own facades, so use our internal ones 117 | } 118 | else if (typeof options.fs === "object") { 119 | // Merge the internal facade methods with the user-provided `fs` facades 120 | facade = Object.assign({}, facade); 121 | facade.fs = Object.assign({}, facade.fs, options.fs); 122 | } 123 | else { 124 | throw new TypeError("options.fs must be an object"); 125 | } 126 | 127 | return { 128 | recurseDepth, 129 | recurseFn, 130 | recurseFnNeedsStats, 131 | filterFn, 132 | filterFnNeedsStats, 133 | stats, 134 | sep, 135 | basePath, 136 | facade, 137 | emit, 138 | }; 139 | } 140 | 141 | /** 142 | * Maps our modified fs.Stats objects to file paths 143 | */ 144 | function map(stats: Stats) { 145 | return stats.path; 146 | } 147 | -------------------------------------------------------------------------------- /src/stat.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from "fs"; 2 | import { safeCall } from "./call"; 3 | import { Callback, FileSystem } from "./types-public"; 4 | 5 | /** 6 | * Retrieves the `Stats` for the given path. If the path is a symbolic link, 7 | * then the Stats of the symlink's target are returned instead. If the symlink is broken, 8 | * then the Stats of the symlink itself are returned. 9 | * 10 | * @param fs - Synchronous or Asynchronouse facade for the "fs" module 11 | * @param path - The path to return stats for 12 | * 13 | * @internal 14 | */ 15 | export function stat(fs: FileSystem, path: string, callback: Callback): void { 16 | let isSymLink = false; 17 | 18 | safeCall(fs.lstat, path, (err: Error | null, lstats: Stats) => { 19 | if (err) { 20 | // fs.lstat threw an eror 21 | return callback(err, undefined as unknown as Stats); 22 | } 23 | 24 | try { 25 | isSymLink = lstats.isSymbolicLink(); 26 | } 27 | catch (err2) { 28 | // lstats.isSymbolicLink() threw an error 29 | // (probably because fs.lstat returned an invalid result) 30 | return callback(err2 as Error, undefined as unknown as Stats); 31 | } 32 | 33 | if (isSymLink) { 34 | // Try to resolve the symlink 35 | symlinkStat(fs, path, lstats, callback); 36 | } 37 | else { 38 | // It's not a symlink, so return the stats as-is 39 | callback(null, lstats); 40 | } 41 | }); 42 | } 43 | 44 | /** 45 | * Retrieves the `Stats` for the target of the given symlink. 46 | * If the symlink is broken, then the Stats of the symlink itself are returned. 47 | * 48 | * @param fs - Synchronous or Asynchronouse facade for the "fs" module 49 | * @param path - The path of the symlink to return stats for 50 | * @param lstats - The stats of the symlink 51 | */ 52 | function symlinkStat(fs: FileSystem, path: string, lstats: Stats, callback: Callback): void { 53 | safeCall(fs.stat, path, (err: Error | null, stats: Stats) => { 54 | if (err) { 55 | // The symlink is broken, so return the stats for the link itself 56 | return callback(null, lstats); 57 | } 58 | 59 | try { 60 | // Return the stats for the resolved symlink target, 61 | // and override the `isSymbolicLink` method to indicate that it's a symlink 62 | stats.isSymbolicLink = () => true; 63 | } 64 | catch (err2) { 65 | // Setting stats.isSymbolicLink threw an error 66 | // (probably because fs.stat returned an invalid result) 67 | return callback(err2 as Error, undefined as unknown as Stats); 68 | } 69 | 70 | callback(null, stats); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /src/stream/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { Readable } from "stream"; 3 | import { asyncForEach as forEach } from "../async/for-each"; 4 | import { DirectoryReader } from "../directory-reader"; 5 | import { Options } from "../types-public"; 6 | 7 | const streamFacade = { fs, forEach }; 8 | 9 | /** 10 | * Aynchronous `readdir()` that returns a `ReadableStream` (which is also an `EventEmitter`). 11 | * All stream data events ("data", "file", "directory", "symlink") are passed a path string. 12 | */ 13 | export function readdirStream(dir: string, options?: Options & { stats?: false }): Readable; 14 | 15 | /** 16 | * Aynchronous `readdir()` that returns a `ReadableStream` (which is also an `EventEmitter`). 17 | * All stream data events ("data", "file", "directory", "symlink") are passed a `Stats` object. 18 | */ 19 | export function readdirStream(dir: string, options: Options & { stats: true }): Readable; 20 | 21 | export function readdirStream(dir: string, options?: Options): Readable { 22 | let reader = new DirectoryReader(dir, options, streamFacade, true); 23 | return reader.stream; 24 | } 25 | -------------------------------------------------------------------------------- /src/sync/for-each.ts: -------------------------------------------------------------------------------- 1 | import { Iterator, VoidCallback } from "../types-internal"; 2 | 3 | /** 4 | * A facade that allows `Array.forEach()` to be called as though it were asynchronous. 5 | * 6 | * @param array - The array to iterate over 7 | * @param iterator - The function to call for each item in the array 8 | * @param done - The function to call when all iterators have completed 9 | * 10 | * @internal 11 | */ 12 | export function syncForEach(array: T[], iterator: Iterator, done: VoidCallback): void { 13 | if (!Array.isArray(array)) { 14 | throw new TypeError(`${array} is not an array`); 15 | } 16 | 17 | for (let item of array) { 18 | iterator(item, () => { 19 | // Note: No error-handling here because this is currently only ever called 20 | // by DirectoryReader, which never passes an `error` parameter to the callback. 21 | // Instead, DirectoryReader emits an "error" event if an error occurs. 22 | }); 23 | } 24 | 25 | done(); 26 | } 27 | -------------------------------------------------------------------------------- /src/sync/fs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { callOnce } from "../call"; 3 | import { Callback, FileSystem } from "../types-public"; 4 | 5 | /** 6 | * Synchronous versions of `fs` methods. 7 | * 8 | * @internal 9 | */ 10 | export const syncFS: FileSystem = { 11 | /** 12 | * A facade around `fs.readdirSync()` that allows it to be called 13 | * the same way as `fs.readdir()`. 14 | */ 15 | readdir(dir: string, callback: Callback): void { 16 | // Make sure the callback is only called once 17 | callback = callOnce(callback); 18 | 19 | try { 20 | let items = fs.readdirSync(dir); 21 | callback(null, items); 22 | } 23 | catch (err) { 24 | callback(err as Error, undefined as unknown as string[]); 25 | } 26 | }, 27 | 28 | /** 29 | * A facade around `fs.statSync()` that allows it to be called 30 | * the same way as `fs.stat()`. 31 | */ 32 | stat(path: string, callback: Callback): void { 33 | // Make sure the callback is only called once 34 | callback = callOnce(callback); 35 | 36 | try { 37 | let stats = fs.statSync(path); 38 | callback(null, stats); 39 | } 40 | catch (err) { 41 | callback(err as Error, undefined as unknown as fs.Stats); 42 | } 43 | }, 44 | 45 | /** 46 | * A facade around `fs.lstatSync()` that allows it to be called 47 | * the same way as `fs.lstat()`. 48 | */ 49 | lstat(path: string, callback: Callback): void { 50 | // Make sure the callback is only called once 51 | callback = callOnce(callback); 52 | 53 | try { 54 | let stats = fs.lstatSync(path); 55 | callback(null, stats); 56 | } 57 | catch (err) { 58 | callback(err as Error, undefined as unknown as fs.Stats); 59 | } 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /src/sync/index.ts: -------------------------------------------------------------------------------- 1 | import { DirectoryReader } from "../directory-reader"; 2 | import { Options, Stats } from "../types-public"; 3 | import { syncForEach as forEach } from "./for-each"; 4 | import { syncFS as fs } from "./fs"; 5 | 6 | const syncFacade = { fs, forEach }; 7 | 8 | /** 9 | * A backward-compatible drop-in replacement for Node's built-in `fs.readdirSync()` function 10 | * that adds support for additional features like filtering, recursion, absolute paths, etc. 11 | */ 12 | export function readdirSync(dir: string, options?: Options & { stats?: false }): string[]; 13 | 14 | /** 15 | * Synchronous `readdir()` that returns results as an array of `Stats` objects 16 | */ 17 | export function readdirSync(dir: string, options: Options & { stats: true }): Stats[]; 18 | 19 | export function readdirSync(dir: string, options?: Options): T[] { 20 | let reader = new DirectoryReader(dir, options, syncFacade); 21 | let stream = reader.stream; 22 | 23 | let results: T[] = []; 24 | let data: T = stream.read() as T; 25 | while (data !== null) { 26 | results.push(data); 27 | data = stream.read() as T; 28 | } 29 | 30 | return results; 31 | } 32 | -------------------------------------------------------------------------------- /src/types-internal.ts: -------------------------------------------------------------------------------- 1 | import { FileSystem } from "./types-public"; 2 | 3 | 4 | /** 5 | * Synchronous or asynchronous facades for various methods, including for the Node.js File System module 6 | * @internal 7 | */ 8 | export interface Facade { 9 | /** 10 | * Custom implementations of filesystem methods. 11 | */ 12 | fs: FileSystem; 13 | 14 | /** 15 | * Calls the specified iterator function for each value in the array. 16 | * This method may be synchronous or asynchronous. 17 | */ 18 | forEach(array: T[], iterator: Iterator, done: VoidCallback): void; 19 | } 20 | 21 | /** 22 | * A function that is called for each item in the array 23 | * @internal 24 | */ 25 | export type Iterator = (item: T, callback: VoidCallback) => void; 26 | 27 | /** 28 | * A callback function without any parameters. 29 | */ 30 | export type VoidCallback = () => void; 31 | -------------------------------------------------------------------------------- /src/types-public.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | /** 4 | * Enhanced `fs.readdir()` options 5 | */ 6 | export interface Options { 7 | /** 8 | * Filter critiera. Can be a glob pattern, a regular expression, or a filter function. 9 | * 10 | * Defaults to returning all files. 11 | */ 12 | filter?: boolean | string | RegExp | FilterFunction; 13 | 14 | /** 15 | * The depth to crawl. Can be `true` to crawl indefinitely, a number to crawl only to that 16 | * depth, or a filter (see the `filter` option) to crawl only directories that match the filter. 17 | * 18 | * Defaults to zero, which will not crawl subdirectories. 19 | */ 20 | deep?: boolean | number | string | RegExp | FilterFunction; 21 | 22 | /** 23 | * Return `Stats` objects instead of just path strings. 24 | * 25 | * Defaults to `false`. 26 | */ 27 | stats?: boolean; 28 | 29 | /** 30 | * Alias for the `stats` option. This property is supported for compatibility with the Node.js 31 | * built-in `fs.readdir()` function. 32 | */ 33 | withFileTypes?: boolean; 34 | 35 | /** 36 | * The path separator to use. 37 | * 38 | * Defaults to "\" on Windows and "/" on other platforms. 39 | */ 40 | sep?: string; 41 | 42 | /** 43 | * The baase path to prefix results with. 44 | * 45 | * Defaults to an empty string, which means results will be relative to the directory path. 46 | */ 47 | basePath?: string; 48 | 49 | /** 50 | * Custom implementations of filesystem methods. 51 | * 52 | * Defaults to the Node "fs" module. 53 | */ 54 | fs?: Partial; 55 | } 56 | 57 | /** 58 | * Custom implementations of filesystem methods. 59 | */ 60 | export interface FileSystem { 61 | /** 62 | * Returns the names of files in a directory. 63 | */ 64 | readdir(path: string, callback: Callback): void; 65 | 66 | /** 67 | * Returns filesystem information about a directory entry. 68 | */ 69 | stat(path: string, callback: Callback): void; 70 | 71 | /** 72 | * Returns filesystem information about a symlink. 73 | */ 74 | lstat(path: string, callback: Callback): void; 75 | } 76 | 77 | /** 78 | * An `fs.Stats` object with additional information. 79 | */ 80 | export interface Stats extends fs.Stats { 81 | /** 82 | * The relative path of the file. 83 | * 84 | * NOTE: The value is affected by the `basePath` and `sep` options. 85 | */ 86 | path: string; 87 | 88 | /** 89 | * The depth of this entry, relative to the original directory. 90 | */ 91 | depth: number; 92 | } 93 | 94 | /** 95 | * A function that determines whether a path should be included or not. 96 | */ 97 | export type FilterFunction = (stat: Stats) => unknown; 98 | 99 | /** 100 | * An error-first callback function. 101 | */ 102 | export type Callback = (err: Error | null, result: T) => void; 103 | 104 | /** 105 | * The events that can be emitted by the stream interface. 106 | */ 107 | export type EventName = "error" | "file" | "directory" | "symlink"; 108 | -------------------------------------------------------------------------------- /test/dir/.dotfile: -------------------------------------------------------------------------------- 1 | Dotfile Tango Victor 2 | -------------------------------------------------------------------------------- /test/dir/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-DevTools/readdir-enhanced/247dabdd17d42b7b166f837a5d5b5c36085f8cb1/test/dir/empty.txt -------------------------------------------------------------------------------- /test/dir/file.json: -------------------------------------------------------------------------------- 1 | { 2 | "alpha": [3, 2, 1, 0] 3 | } -------------------------------------------------------------------------------- /test/dir/file.txt: -------------------------------------------------------------------------------- 1 | Alpha Foo -------------------------------------------------------------------------------- /test/dir/subdir/.dotdir/.dotfile: -------------------------------------------------------------------------------- 1 | Dotfile Tango Victor 2 | -------------------------------------------------------------------------------- /test/dir/subdir/file.txt: -------------------------------------------------------------------------------- 1 | Beta Beer Bar -------------------------------------------------------------------------------- /test/dir/subdir/subsubdir/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-DevTools/readdir-enhanced/247dabdd17d42b7b166f837a5d5b5c36085f8cb1/test/dir/subdir/subsubdir/empty.txt -------------------------------------------------------------------------------- /test/dir/subdir/subsubdir/file.json: -------------------------------------------------------------------------------- 1 | { 2 | "alpha": [3, 2, 1, 0] 3 | } -------------------------------------------------------------------------------- /test/dir/subdir/subsubdir/file.txt: -------------------------------------------------------------------------------- 1 | Beta Beer Bar -------------------------------------------------------------------------------- /test/fixtures/init.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const del = require("del"); 4 | const path = require("path"); 5 | const fs = require("fs"); 6 | 7 | before(() => { 8 | console.log("Initializing test directory"); 9 | 10 | // create some empty dirs (cannot check-in empty dirs to git) 11 | fs.mkdirSync("test/dir/.dotdir", { recursive: true }); 12 | fs.mkdirSync("test/dir/empty", { recursive: true }); 13 | fs.mkdirSync("test/dir/subdir/.dotdir/empty", { recursive: true }); 14 | 15 | // create symlinks (checking symlinks into git is problematic cross-platform) 16 | symlink("test/dir/file.txt", "test/dir/file-symlink.txt", "file"); 17 | symlink("test/dir/subdir/subsubdir/file.txt", "test/dir/subdir/subsubdir/file-symlink.txt", "file"); 18 | symlink("test/dir/subdir", "test/dir/subdir-symlink", "dir"); 19 | symlink("test/dir/subdir/subsubdir", "test/dir/subsubdir-symlink", "dir"); 20 | 21 | // create broken symlinks (checking broken symlinks into git is problematic) 22 | brokenSymlink("test/dir/broken-symlink.txt", "file"); 23 | brokenSymlink("test/dir/subdir/subsubdir/broken-symlink.txt", "file"); 24 | brokenSymlink("test/dir/broken-dir-symlink", "dir"); 25 | 26 | // delete files that get created automatically by the OS 27 | del.sync("test/dir/**/.DS_Store", { dot: true }); 28 | del.sync("test/dir/**/Thumbs.db", { dot: true }); 29 | }); 30 | 31 | /** 32 | * Creates (or re-creates) a symbolic link. 33 | * If the symlink already exists, it is re-created, in case paths or permissions have changed. 34 | */ 35 | function symlink (targetPath, linkPath, type) { 36 | try { 37 | fs.unlinkSync(linkPath); 38 | } 39 | catch (e) { 40 | if (e.code !== "ENOENT") { 41 | throw e; 42 | } 43 | } 44 | 45 | targetPath = path.resolve(targetPath); 46 | fs.symlinkSync(targetPath, linkPath, type); 47 | } 48 | 49 | /** 50 | * Creates (or re-creates) a broken symbolic link. 51 | */ 52 | function brokenSymlink (linkPath, type) { 53 | let tmp = path.join(path.dirname(linkPath), Date.now() + type); 54 | 55 | if (type === "file") { 56 | fs.writeFileSync(tmp, ""); 57 | symlink(tmp, linkPath, type); 58 | fs.unlinkSync(tmp); 59 | } 60 | else { 61 | fs.mkdirSync(tmp); 62 | symlink(tmp, linkPath, type); 63 | fs.rmdirSync(tmp); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/specs/basePath.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const forEachApi = require("../utils/for-each-api"); 4 | const dir = require("../utils/dir"); 5 | const { expect } = require("chai"); 6 | const path = require("path"); 7 | 8 | let testDirAbsPath = path.resolve("test/dir"); 9 | 10 | describe("options.basePath", () => { 11 | forEachApi([ 12 | { 13 | it: 'should return relative paths if basePath === "" (empty string)', 14 | args: ["test/dir", { basePath: "" }], 15 | assert (error, data) { 16 | expect(error).to.equal(null); 17 | expect(data).to.have.same.members(dir.shallow.data); 18 | }, 19 | streamAssert (errors, data, files, dirs, symlinks) { 20 | expect(errors).to.have.lengthOf(0); 21 | expect(data).to.have.same.members(dir.shallow.data); 22 | expect(files).to.have.same.members(dir.shallow.files); 23 | expect(dirs).to.have.same.members(dir.shallow.dirs); 24 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 25 | }, 26 | }, 27 | { 28 | it: 'should return relative paths if basePath === "."', 29 | args: ["test/dir", { basePath: "." }], 30 | assert (error, data) { 31 | expect(error).to.equal(null); 32 | assertPathsMatch(data, dir.shallow.data, "."); 33 | }, 34 | streamAssert (errors, data, files, dirs, symlinks) { 35 | expect(errors).to.have.lengthOf(0); 36 | assertPathsMatch(data, dir.shallow.data, "."); 37 | assertPathsMatch(files, dir.shallow.files, "."); 38 | assertPathsMatch(dirs, dir.shallow.dirs, "."); 39 | assertPathsMatch(symlinks, dir.shallow.symlinks, "."); 40 | }, 41 | }, 42 | { 43 | it: "should return absolute paths if basePath === absolute path", 44 | args: ["test/dir", { basePath: testDirAbsPath }], 45 | assert (error, data) { 46 | expect(error).to.equal(null); 47 | assertPathsMatch(data, dir.shallow.data, testDirAbsPath); 48 | }, 49 | streamAssert (errors, data, files, dirs, symlinks) { 50 | expect(errors).to.have.lengthOf(0); 51 | assertPathsMatch(data, dir.shallow.data, testDirAbsPath); 52 | assertPathsMatch(files, dir.shallow.files, testDirAbsPath); 53 | assertPathsMatch(dirs, dir.shallow.dirs, testDirAbsPath); 54 | assertPathsMatch(symlinks, dir.shallow.symlinks, testDirAbsPath); 55 | }, 56 | }, 57 | { 58 | it: "should return relative paths to process.cwd() if basePath === path", 59 | args: ["test/dir", { basePath: "test/dir" }], 60 | assert (error, data) { 61 | expect(error).to.equal(null); 62 | assertPathsMatch(data, dir.shallow.data, "test/dir"); 63 | }, 64 | streamAssert (errors, data, files, dirs, symlinks) { 65 | expect(errors).to.have.lengthOf(0); 66 | assertPathsMatch(data, dir.shallow.data, "test/dir"); 67 | assertPathsMatch(files, dir.shallow.files, "test/dir"); 68 | assertPathsMatch(dirs, dir.shallow.dirs, "test/dir"); 69 | assertPathsMatch(symlinks, dir.shallow.symlinks, "test/dir"); 70 | }, 71 | }, 72 | ]); 73 | 74 | function assertPathsMatch (actual, expected, basePath) { 75 | let expectedAbsolutePaths = expected.map(relPath => { 76 | return basePath + path.sep + relPath; 77 | }); 78 | expect(actual).to.have.same.members(expectedAbsolutePaths); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /test/specs/deep.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const forEachApi = require("../utils/for-each-api"); 4 | const dir = require("../utils/dir"); 5 | const { expect } = require("chai"); 6 | const path = require("path"); 7 | 8 | describe("options.deep", () => { 9 | forEachApi([ 10 | { 11 | it: "should return all deep contents", 12 | args: ["test/dir", { deep: true }], 13 | assert (error, data) { 14 | expect(error).to.equal(null); 15 | expect(data).to.have.same.members(dir.deep.data); 16 | }, 17 | streamAssert (errors, data, files, dirs, symlinks) { 18 | expect(errors).to.have.lengthOf(0); 19 | expect(data).to.have.same.members(dir.deep.data); 20 | expect(files).to.have.same.members(dir.deep.files); 21 | expect(dirs).to.have.same.members(dir.deep.dirs); 22 | expect(symlinks).to.have.same.members(dir.deep.symlinks); 23 | }, 24 | }, 25 | { 26 | it: "should only return top-level contents if deep === false", 27 | args: ["test/dir", { deep: false }], 28 | assert (error, data) { 29 | expect(error).to.equal(null); 30 | expect(data).to.have.same.members(dir.shallow.data); 31 | }, 32 | streamAssert (errors, data, files, dirs, symlinks) { 33 | expect(errors).to.have.lengthOf(0); 34 | expect(data).to.have.same.members(dir.shallow.data); 35 | expect(files).to.have.same.members(dir.shallow.files); 36 | expect(dirs).to.have.same.members(dir.shallow.dirs); 37 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 38 | }, 39 | }, 40 | { 41 | it: "should only return top-level contents if deep === 0", 42 | args: ["test/dir", { deep: 0 }], 43 | assert (error, data) { 44 | expect(error).to.equal(null); 45 | expect(data).to.have.same.members(dir.shallow.data); 46 | }, 47 | streamAssert (errors, data, files, dirs, symlinks) { 48 | expect(errors).to.have.lengthOf(0); 49 | expect(data).to.have.same.members(dir.shallow.data); 50 | expect(files).to.have.same.members(dir.shallow.files); 51 | expect(dirs).to.have.same.members(dir.shallow.dirs); 52 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 53 | }, 54 | }, 55 | { 56 | it: "should return 1-level deep contents", 57 | args: ["test/dir", { deep: 1 }], 58 | assert (error, data) { 59 | expect(error).to.equal(null); 60 | expect(data).to.have.same.members(dir.deep.oneLevel.data); 61 | }, 62 | streamAssert (errors, data, files, dirs, symlinks) { 63 | expect(errors).to.have.lengthOf(0); 64 | expect(data).to.have.same.members(dir.deep.oneLevel.data); 65 | expect(files).to.have.same.members(dir.deep.oneLevel.files); 66 | expect(dirs).to.have.same.members(dir.deep.oneLevel.dirs); 67 | expect(symlinks).to.have.same.members(dir.deep.oneLevel.symlinks); 68 | }, 69 | }, 70 | { 71 | it: "should return all deep contents if deep is a number greater than the number of dirs", 72 | args: ["test/dir", { deep: 25 }], 73 | assert (error, data) { 74 | expect(error).to.equal(null); 75 | expect(data).to.have.same.members(dir.deep.data); 76 | }, 77 | streamAssert (errors, data, files, dirs, symlinks) { 78 | expect(errors).to.have.lengthOf(0); 79 | expect(data).to.have.same.members(dir.deep.data); 80 | expect(files).to.have.same.members(dir.deep.files); 81 | expect(dirs).to.have.same.members(dir.deep.dirs); 82 | expect(symlinks).to.have.same.members(dir.deep.symlinks); 83 | }, 84 | }, 85 | { 86 | it: "should return all deep contents if deep === Infinity", 87 | args: ["test/dir", { deep: Infinity }], 88 | assert (error, data) { 89 | expect(error).to.equal(null); 90 | expect(data).to.have.same.members(dir.deep.data); 91 | }, 92 | streamAssert (errors, data, files, dirs, symlinks) { 93 | expect(errors).to.have.lengthOf(0); 94 | expect(data).to.have.same.members(dir.deep.data); 95 | expect(files).to.have.same.members(dir.deep.files); 96 | expect(dirs).to.have.same.members(dir.deep.dirs); 97 | expect(symlinks).to.have.same.members(dir.deep.symlinks); 98 | }, 99 | }, 100 | { 101 | it: "should recurse based on a regular expression", 102 | args: ["test/dir", { 103 | deep: /^((?!\-symlink).)*$/, 104 | }], 105 | assert (error, data) { 106 | expect(error).to.equal(null); 107 | expect(data).to.have.same.members(this.omitSymlinkDirs(dir.deep.data)); 108 | }, 109 | streamAssert (errors, data, files, dirs, symlinks) { 110 | expect(errors).to.have.lengthOf(0); 111 | expect(data).to.have.same.members(this.omitSymlinkDirs(dir.deep.data)); 112 | expect(files).to.have.same.members(this.omitSymlinkDirs(dir.deep.files)); 113 | expect(dirs).to.have.same.members(this.omitSymlinkDirs(dir.deep.dirs)); 114 | expect(symlinks).to.have.same.members(this.omitSymlinkDirs(dir.deep.symlinks)); 115 | }, 116 | 117 | // Omits the contents of the "-symlink" directories 118 | omitSymlinkDirs (paths) { 119 | return paths.filter(p => { 120 | return p.indexOf("-symlink" + path.sep) === -1; 121 | }); 122 | } 123 | }, 124 | { 125 | it: "should recurse based on a glob pattern", 126 | args: ["test/dir", { 127 | deep: "subdir", 128 | }], 129 | assert (error, data) { 130 | expect(error).to.equal(null); 131 | expect(data).to.have.same.members(this.shallowPlusSubdir("data")); 132 | }, 133 | streamAssert (errors, data, files, dirs, symlinks) { 134 | expect(errors).to.have.lengthOf(0); 135 | expect(data).to.have.same.members(this.shallowPlusSubdir("data")); 136 | expect(files).to.have.same.members(this.shallowPlusSubdir("files")); 137 | expect(dirs).to.have.same.members(this.shallowPlusSubdir("dirs")); 138 | expect(symlinks).to.have.same.members(this.shallowPlusSubdir("symlinks")); 139 | }, 140 | 141 | // Returns the shallow contents of the root directory and the "subdir" directory 142 | shallowPlusSubdir (type) { 143 | return dir.shallow[type].concat( 144 | dir.subdir.shallow[type].map(file => { 145 | return path.join("subdir", file); 146 | }) 147 | ); 148 | } 149 | }, 150 | { 151 | it: "should recurse based on a custom function", 152 | args: ["test/dir", { 153 | deep (stats) { 154 | return stats.path !== "subdir"; 155 | }, 156 | }], 157 | assert (error, data) { 158 | expect(error).to.equal(null); 159 | expect(data).to.have.same.members(this.omitSubdir(dir.deep.data)); 160 | }, 161 | streamAssert (errors, data, files, dirs, symlinks) { 162 | expect(errors).to.have.lengthOf(0); 163 | expect(data).to.have.same.members(this.omitSubdir(dir.deep.data)); 164 | expect(files).to.have.same.members(this.omitSubdir(dir.deep.files)); 165 | expect(dirs).to.have.same.members(this.omitSubdir(dir.deep.dirs)); 166 | expect(symlinks).to.have.same.members(this.omitSubdir(dir.deep.symlinks)); 167 | }, 168 | 169 | // Omits the contents of the "subdir" directory 170 | omitSubdir (paths) { 171 | return paths.filter(p => { 172 | return p.substr(0, 7) !== "subdir" + path.sep; 173 | }); 174 | } 175 | }, 176 | { 177 | it: "should recurse based on a custom function with depth property", 178 | args: ["test/dir", { 179 | deep (stats) { 180 | return stats.depth !== 1; 181 | }, 182 | }], 183 | assert (error, data) { 184 | expect(error).to.equal(null); 185 | expect(data).to.have.same.members(this.omitSubdir(dir.deep.data)); 186 | }, 187 | streamAssert (errors, data, files, dirs, symlinks) { 188 | expect(errors).to.have.lengthOf(0); 189 | expect(data).to.have.same.members(this.omitSubdir(dir.deep.data)); 190 | expect(files).to.have.same.members(this.omitSubdir(dir.deep.files)); 191 | expect(dirs).to.have.same.members(this.omitSubdir(dir.deep.dirs)); 192 | expect(symlinks).to.have.same.members(this.omitSubdir(dir.deep.symlinks)); 193 | }, 194 | 195 | // Omits the contents of the "subdir" directory 196 | omitSubdir (paths) { 197 | return paths.filter(p => { 198 | return p.split(path.sep).length <= 2; 199 | }); 200 | } 201 | }, 202 | { 203 | it: "should return all deep contents if custom deep function always returns true", 204 | args: ["test/dir", { 205 | deep () { 206 | return true; 207 | }, 208 | }], 209 | assert (error, data) { 210 | expect(error).to.equal(null); 211 | expect(data).to.have.same.members(dir.deep.data); 212 | }, 213 | streamAssert (errors, data, files, dirs, symlinks) { 214 | expect(errors).to.have.lengthOf(0); 215 | expect(data).to.have.same.members(dir.deep.data); 216 | expect(files).to.have.same.members(dir.deep.files); 217 | expect(dirs).to.have.same.members(dir.deep.dirs); 218 | expect(symlinks).to.have.same.members(dir.deep.symlinks); 219 | }, 220 | }, 221 | { 222 | it: "should return shallow contents if custom deep function always returns false", 223 | args: ["test/dir", { 224 | deep () { 225 | return false; 226 | }, 227 | }], 228 | assert (error, data) { 229 | expect(error).to.equal(null); 230 | expect(data).to.have.same.members(dir.shallow.data); 231 | }, 232 | streamAssert (errors, data, files, dirs, symlinks) { 233 | expect(errors).to.have.lengthOf(0); 234 | expect(data).to.have.same.members(dir.shallow.data); 235 | expect(files).to.have.same.members(dir.shallow.files); 236 | expect(dirs).to.have.same.members(dir.shallow.dirs); 237 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 238 | }, 239 | }, 240 | { 241 | it: "should handle errors that occur in the deep function", 242 | args: ["test/dir", { 243 | deep (stats) { 244 | if (stats.isSymbolicLink()) { 245 | throw new Error("Boooooom!"); 246 | } 247 | return false; 248 | } 249 | }], 250 | assert (error, data) { 251 | // The sync & async APIs abort after the first error and don't return any data 252 | expect(error).to.be.an.instanceOf(Error); 253 | expect(error.message).to.equal("Boooooom!"); 254 | expect(data).to.equal(undefined); 255 | }, 256 | streamAssert (errors, data, files, dirs, symlinks) { 257 | // The streaming API emits errors and data separately 258 | expect(errors).to.have.lengthOf(2); 259 | expect(data).to.have.same.members([ 260 | ".dotdir", "empty", "subdir", ".dotfile", "empty.txt", "file.txt", "file.json", 261 | "broken-dir-symlink", "broken-symlink.txt", "file-symlink.txt", "subdir-symlink", 262 | "subsubdir-symlink" 263 | ]); 264 | expect(files).to.have.same.members([ 265 | ".dotfile", "empty.txt", "file.txt", "file.json", "file-symlink.txt" 266 | ]); 267 | expect(dirs).to.have.same.members([ 268 | ".dotdir", "empty", "subdir", "subdir-symlink", "subsubdir-symlink" 269 | ]); 270 | expect(symlinks).to.have.same.members([ 271 | "broken-dir-symlink", "broken-symlink.txt", "file-symlink.txt", "subdir-symlink", 272 | "subsubdir-symlink" 273 | ]); 274 | }, 275 | }, 276 | ]); 277 | }); 278 | -------------------------------------------------------------------------------- /test/specs/default.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const forEachApi = require("../utils/for-each-api"); 4 | const dir = require("../utils/dir"); 5 | const { expect } = require("chai"); 6 | const fs = require("fs"); 7 | const path = require("path"); 8 | 9 | describe("default behavior", () => { 10 | forEachApi([ 11 | { 12 | it: "should return the same results as `fs.readdir`", 13 | args: ["test/dir"], 14 | assert (error, data) { 15 | let fsResults = fs.readdirSync("test/dir"); 16 | expect(error).to.equal(null); 17 | expect(data).to.have.same.members(fsResults); 18 | }, 19 | }, 20 | { 21 | it: "should return an empty array for an empty dir", 22 | args: ["test/dir/empty"], 23 | assert (error, data) { 24 | expect(error).to.equal(null); 25 | expect(data).to.be.an("array").with.lengthOf(0); 26 | }, 27 | streamAssert (errors, data, files, dirs, symlinks) { 28 | expect(errors).to.have.lengthOf(0); 29 | expect(data).to.have.lengthOf(0); 30 | expect(files).to.have.lengthOf(0); 31 | expect(dirs).to.have.lengthOf(0); 32 | expect(symlinks).to.have.lengthOf(0); 33 | }, 34 | }, 35 | { 36 | it: "should return all top-level contents", 37 | args: ["test/dir"], 38 | assert (error, data) { 39 | expect(error).to.equal(null); 40 | expect(data).to.have.same.members(dir.shallow.data); 41 | }, 42 | streamAssert (errors, data, files, dirs, symlinks) { 43 | expect(errors).to.have.lengthOf(0); 44 | expect(data).to.have.same.members(dir.shallow.data); 45 | expect(files).to.have.same.members(dir.shallow.files); 46 | expect(dirs).to.have.same.members(dir.shallow.dirs); 47 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 48 | }, 49 | }, 50 | { 51 | it: "should return the same results if the path is absolute", 52 | args: [path.resolve("test/dir")], 53 | assert (error, data) { 54 | expect(error).to.equal(null); 55 | expect(data).to.have.same.members(dir.shallow.data); 56 | }, 57 | streamAssert (errors, data, files, dirs, symlinks) { 58 | expect(errors).to.have.lengthOf(0); 59 | expect(data).to.have.same.members(dir.shallow.data); 60 | expect(files).to.have.same.members(dir.shallow.files); 61 | expect(dirs).to.have.same.members(dir.shallow.dirs); 62 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 63 | }, 64 | }, 65 | { 66 | it: "should return all top-level contents of a directory symlink", 67 | args: ["test/dir/subdir-symlink"], 68 | assert (error, data) { 69 | expect(error).to.equal(null); 70 | expect(data).to.have.same.members(dir.subdir.shallow.data); 71 | }, 72 | streamAssert (errors, data, files, dirs, symlinks) { 73 | expect(errors).to.have.lengthOf(0); 74 | expect(data).to.have.same.members(dir.subdir.shallow.data); 75 | expect(files).to.have.same.members(dir.subdir.shallow.files); 76 | expect(dirs).to.have.same.members(dir.subdir.shallow.dirs); 77 | expect(symlinks).to.have.same.members(dir.subdir.shallow.symlinks); 78 | }, 79 | }, 80 | { 81 | it: "should return relative paths", 82 | args: ["test/dir"], 83 | assert (error, data) { 84 | expect(error).to.equal(null); 85 | for (let item of data) { 86 | expect(item).not.to.contain("/"); 87 | expect(item).not.to.contain("\\"); 88 | } 89 | }, 90 | streamAssert (errors, data, files, dirs, symlinks) { 91 | expect(errors).to.have.lengthOf(0); 92 | expect(data).to.have.same.members(dir.shallow.data); 93 | expect(files).to.have.same.members(dir.shallow.files); 94 | expect(dirs).to.have.same.members(dir.shallow.dirs); 95 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 96 | }, 97 | }, 98 | ]); 99 | }); 100 | -------------------------------------------------------------------------------- /test/specs/errors.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const forEachApi = require("../utils/for-each-api"); 4 | const { expect } = require("chai"); 5 | 6 | describe("error handling", () => { 7 | forEachApi([ 8 | { 9 | it: "should throw an error if no arguments are passed", 10 | args: [], 11 | assert (error, data) { 12 | expect(error).to.be.an.instanceOf(TypeError); 13 | expect(error.message).to.match(/must be a string|must be one of type string|must be of type string/); 14 | expect(data).to.equal(undefined); 15 | }, 16 | streamAssert (errors, data, files, dirs, symlinks) { 17 | expect(errors).to.have.lengthOf(1); 18 | expect(data).to.have.lengthOf(0); 19 | expect(files).to.have.lengthOf(0); 20 | expect(dirs).to.have.lengthOf(0); 21 | expect(symlinks).to.have.lengthOf(0); 22 | }, 23 | }, 24 | { 25 | it: "should throw an error if the path is not a string", 26 | args: [55555], 27 | assert (error, data) { 28 | expect(error).to.be.an.instanceOf(TypeError); 29 | expect(error.message).to.match(/must be a string|must be one of type string|must be of type string/); 30 | expect(data).to.equal(undefined); 31 | }, 32 | streamAssert (errors, data, files, dirs, symlinks) { 33 | expect(errors).to.have.lengthOf(1); 34 | expect(data).to.have.lengthOf(0); 35 | expect(files).to.have.lengthOf(0); 36 | expect(dirs).to.have.lengthOf(0); 37 | expect(symlinks).to.have.lengthOf(0); 38 | }, 39 | }, 40 | { 41 | it: "should throw an error if options are invalid", 42 | args: ["test/dir", "invalid options"], 43 | assert (error, data) { 44 | expect(error).to.be.an.instanceOf(TypeError); 45 | expect(error.message).to.equal("options must be an object"); 46 | expect(data).to.equal(undefined); 47 | }, 48 | streamAssert (errors, data, files, dirs, symlinks) { 49 | expect(errors).to.have.lengthOf(1); 50 | expect(data).to.have.lengthOf(0); 51 | expect(files).to.have.lengthOf(0); 52 | expect(dirs).to.have.lengthOf(0); 53 | expect(symlinks).to.have.lengthOf(0); 54 | }, 55 | }, 56 | { 57 | it: "should throw an error if options.deep is invalid", 58 | args: ["test/dir", { deep: { foo: "bar" }}], 59 | assert (error, data) { 60 | expect(error).to.be.an.instanceOf(TypeError); 61 | expect(error.message).to.equal("options.deep must be a boolean, number, function, regular expression, or glob pattern"); 62 | expect(data).to.equal(undefined); 63 | }, 64 | streamAssert (errors, data, files, dirs, symlinks) { 65 | expect(errors).to.have.lengthOf(1); 66 | expect(data).to.have.lengthOf(0); 67 | expect(files).to.have.lengthOf(0); 68 | expect(dirs).to.have.lengthOf(0); 69 | expect(symlinks).to.have.lengthOf(0); 70 | }, 71 | }, 72 | { 73 | it: "should throw an error if options.deep is negative", 74 | args: ["test/dir", { deep: -5 }], 75 | assert (error, data) { 76 | expect(error).to.be.an.instanceOf(Error); 77 | expect(error.message).to.equal("options.deep must be a positive number"); 78 | expect(data).to.equal(undefined); 79 | }, 80 | streamAssert (errors, data, files, dirs, symlinks) { 81 | expect(errors).to.have.lengthOf(1); 82 | expect(data).to.have.lengthOf(0); 83 | expect(files).to.have.lengthOf(0); 84 | expect(dirs).to.have.lengthOf(0); 85 | expect(symlinks).to.have.lengthOf(0); 86 | }, 87 | }, 88 | { 89 | it: "should throw an error if options.deep is NaN", 90 | args: ["test/dir", { deep: NaN }], 91 | assert (error, data) { 92 | expect(error).to.be.an.instanceOf(Error); 93 | expect(error.message).to.equal("options.deep must be a positive number"); 94 | expect(data).to.equal(undefined); 95 | }, 96 | streamAssert (errors, data, files, dirs, symlinks) { 97 | expect(errors).to.have.lengthOf(1); 98 | expect(data).to.have.lengthOf(0); 99 | expect(files).to.have.lengthOf(0); 100 | expect(dirs).to.have.lengthOf(0); 101 | expect(symlinks).to.have.lengthOf(0); 102 | }, 103 | }, 104 | { 105 | it: "should throw an error if options.deep is not an integer", 106 | args: ["test/dir", { deep: 5.4 }], 107 | assert (error, data) { 108 | expect(error).to.be.an.instanceOf(Error); 109 | expect(error.message).to.equal("options.deep must be an integer"); 110 | expect(data).to.equal(undefined); 111 | }, 112 | streamAssert (errors, data, files, dirs, symlinks) { 113 | expect(errors).to.have.lengthOf(1); 114 | expect(data).to.have.lengthOf(0); 115 | expect(files).to.have.lengthOf(0); 116 | expect(dirs).to.have.lengthOf(0); 117 | expect(symlinks).to.have.lengthOf(0); 118 | }, 119 | }, 120 | { 121 | it: "should throw an error if options.filter is invalid", 122 | args: ["test/dir", { filter: 12345 }], 123 | assert (error, data) { 124 | expect(error).to.be.an.instanceOf(TypeError); 125 | expect(error.message).to.equal( 126 | "options.filter must be a boolean, function, regular expression, or glob pattern"); 127 | expect(data).to.equal(undefined); 128 | }, 129 | streamAssert (errors, data, files, dirs, symlinks) { 130 | expect(errors).to.have.lengthOf(1); 131 | expect(data).to.have.lengthOf(0); 132 | expect(files).to.have.lengthOf(0); 133 | expect(dirs).to.have.lengthOf(0); 134 | expect(symlinks).to.have.lengthOf(0); 135 | }, 136 | }, 137 | { 138 | it: "should throw an error if options.sep is invalid", 139 | args: ["test/dir", { sep: 57 }], 140 | assert (error, data) { 141 | expect(error).to.be.an.instanceOf(TypeError); 142 | expect(error.message).to.equal("options.sep must be a string"); 143 | expect(data).to.equal(undefined); 144 | }, 145 | streamAssert (errors, data, files, dirs, symlinks) { 146 | expect(errors).to.have.lengthOf(1); 147 | expect(data).to.have.lengthOf(0); 148 | expect(files).to.have.lengthOf(0); 149 | expect(dirs).to.have.lengthOf(0); 150 | expect(symlinks).to.have.lengthOf(0); 151 | }, 152 | }, 153 | { 154 | it: "should throw an error if options.basePath is invalid", 155 | args: ["test/dir", { basePath: 57 }], 156 | assert (error, data) { 157 | expect(error).to.be.an.instanceOf(TypeError); 158 | expect(error.message).to.equal("options.basePath must be a string"); 159 | expect(data).to.equal(undefined); 160 | }, 161 | streamAssert (errors, data, files, dirs, symlinks) { 162 | expect(errors).to.have.lengthOf(1); 163 | expect(data).to.have.lengthOf(0); 164 | expect(files).to.have.lengthOf(0); 165 | expect(dirs).to.have.lengthOf(0); 166 | expect(symlinks).to.have.lengthOf(0); 167 | }, 168 | }, 169 | { 170 | it: "should throw an error if the directory does not exist", 171 | args: ["test/dir/does-not-exist"], 172 | assert (error, data) { 173 | expect(error).to.be.an.instanceOf(Error); 174 | expect(error.code).to.equal("ENOENT"); 175 | expect(data).to.equal(undefined); 176 | }, 177 | streamAssert (errors, data, files, dirs, symlinks) { 178 | expect(errors).to.have.lengthOf(1); 179 | expect(data).to.have.lengthOf(0); 180 | expect(files).to.have.lengthOf(0); 181 | expect(dirs).to.have.lengthOf(0); 182 | expect(symlinks).to.have.lengthOf(0); 183 | }, 184 | }, 185 | { 186 | it: "should throw an error if the path is not a directory", 187 | args: ["test/dir/file.txt"], 188 | assert (error, data) { 189 | expect(error).to.be.an.instanceOf(Error); 190 | expect(error.code).to.equal("ENOTDIR"); 191 | expect(data).to.equal(undefined); 192 | }, 193 | streamAssert (errors, data, files, dirs, symlinks) { 194 | expect(errors).to.have.lengthOf(1); 195 | expect(data).to.have.lengthOf(0); 196 | expect(files).to.have.lengthOf(0); 197 | expect(dirs).to.have.lengthOf(0); 198 | expect(symlinks).to.have.lengthOf(0); 199 | }, 200 | }, 201 | { 202 | it: "should throw an error if the path is a broken symlink", 203 | args: ["test/dir/broken-dir-symlink"], 204 | assert (error, data) { 205 | expect(error).to.be.an.instanceOf(Error); 206 | expect(error.code).to.equal("ENOENT"); 207 | expect(data).to.equal(undefined); 208 | }, 209 | streamAssert (errors, data, files, dirs, symlinks) { 210 | expect(errors).to.have.lengthOf(1); 211 | expect(data).to.have.lengthOf(0); 212 | expect(files).to.have.lengthOf(0); 213 | expect(dirs).to.have.lengthOf(0); 214 | expect(symlinks).to.have.lengthOf(0); 215 | }, 216 | }, 217 | { 218 | it: "should throw an error if `options.fs` is invalid", 219 | args: ["test/dir", { fs: "Hello, World" }], 220 | assert (error, data) { 221 | expect(error).to.be.an.instanceOf(TypeError); 222 | expect(error.message).to.equal("options.fs must be an object"); 223 | expect(data).to.equal(undefined); 224 | }, 225 | streamAssert (errors, data, files, dirs, symlinks) { 226 | expect(errors).to.have.lengthOf(1); 227 | expect(data).to.have.lengthOf(0); 228 | expect(files).to.have.lengthOf(0); 229 | expect(dirs).to.have.lengthOf(0); 230 | expect(symlinks).to.have.lengthOf(0); 231 | }, 232 | }, 233 | ]); 234 | }); 235 | -------------------------------------------------------------------------------- /test/specs/exports.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { default: defaultExport, readdir, readdirSync, readdirAsync, readdirIterator, readdirStream } = require("../../"); 4 | const { expect } = require("chai"); 5 | 6 | describe("exports", () => { 7 | describe("Synchronous API", () => { 8 | it("should export the `readdirSync` function as `readdirSync`", done => { 9 | expect(readdirSync).to.be.a("function"); 10 | expect(readdirSync.name).to.equal("readdirSync"); 11 | done(); 12 | }); 13 | 14 | it("should alias `readdirSync` as `readdir.sync`", done => { 15 | expect(readdir.sync).to.be.a("function"); 16 | expect(readdir.sync).to.equal(readdirSync); 17 | done(); 18 | }); 19 | }); 20 | 21 | describe("Asynchronous API (callback/Promise)", () => { 22 | it("should export the `readdirAsync` function as the default export", done => { 23 | expect(defaultExport).to.be.a("function"); 24 | expect(defaultExport).to.equal(readdirAsync); 25 | done(); 26 | }); 27 | 28 | it("should export the `readdirAsync` function as `readdir`", done => { 29 | expect(readdir).to.be.a("function"); 30 | expect(readdir).to.equal(readdirAsync); 31 | done(); 32 | }); 33 | 34 | it("should alias `readdirAsync` as `readdirAync`", done => { 35 | expect(readdirAsync).to.be.a("function"); 36 | expect(readdir.name).to.equal("readdirAsync"); 37 | done(); 38 | }); 39 | 40 | it("should alias `readdirAsync` as `readdir.async`", done => { 41 | expect(readdir.async).to.be.a("function"); 42 | expect(readdir.async).to.equal(readdirAsync); 43 | done(); 44 | }); 45 | }); 46 | 47 | describe("Iterator API", () => { 48 | it("should export the `readdirIterator` function as `readdirIterator`", done => { 49 | expect(readdirIterator).to.be.a("function"); 50 | expect(readdirIterator.name).to.equal("readdirIterator"); 51 | done(); 52 | }); 53 | 54 | it("should alias `readdirIterator` as `readdir.iterator`", done => { 55 | expect(readdir.iterator).to.be.a("function"); 56 | expect(readdir.iterator).to.equal(readdirIterator); 57 | done(); 58 | }); 59 | }); 60 | 61 | describe("Asynchronous API (Stream/EventEmitter)", () => { 62 | it("should export the `readdirStream` function as `readdirStream`", done => { 63 | expect(readdirStream).to.be.a("function"); 64 | expect(readdirStream.name).to.equal("readdirStream"); 65 | done(); 66 | }); 67 | 68 | it("should alias `readdirStream` as `readdir.stream`", done => { 69 | expect(readdir.stream).to.be.a("function"); 70 | expect(readdir.stream).to.equal(readdirStream); 71 | done(); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/specs/filter.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const forEachApi = require("../utils/for-each-api"); 4 | const dir = require("../utils/dir"); 5 | const { expect } = require("chai"); 6 | 7 | describe("options.filter", () => { 8 | forEachApi([ 9 | { 10 | it: "should return filtered top-level contents", 11 | args: ["test/dir", { 12 | filter (stats) { 13 | return stats.path.indexOf("empty") >= 0; 14 | } 15 | }], 16 | assert (error, data) { 17 | expect(error).to.equal(null); 18 | expect(data).to.have.same.members(["empty", "empty.txt"]); 19 | }, 20 | streamAssert (errors, data, files, dirs, symlinks) { 21 | expect(errors).to.have.lengthOf(0); 22 | expect(data).to.have.same.members(["empty", "empty.txt"]); 23 | expect(files).to.have.same.members(["empty.txt"]); 24 | expect(dirs).to.have.same.members(["empty"]); 25 | expect(symlinks).to.have.lengthOf(0); 26 | }, 27 | }, 28 | { 29 | it: "should return filtered deep contents", 30 | args: ["test/dir", { 31 | deep: true, 32 | filter (stats) { 33 | return stats.path.indexOf("empty") >= 0; 34 | } 35 | }], 36 | assert (error, data) { 37 | expect(error).to.equal(null); 38 | expect(data).to.have.same.members(dir.empties.deep.data); 39 | }, 40 | streamAssert (errors, data, files, dirs, symlinks) { 41 | expect(errors).to.have.lengthOf(0); 42 | expect(data).to.have.same.members(dir.empties.deep.data); 43 | expect(files).to.have.same.members(dir.empties.deep.files); 44 | expect(dirs).to.have.same.members(dir.empties.deep.dirs); 45 | expect(symlinks).to.have.same.members(dir.empties.deep.symlinks); 46 | }, 47 | }, 48 | { 49 | it: "should filter by files", 50 | args: ["test/dir", { 51 | deep: true, 52 | filter (stats) { 53 | return stats.isFile(); 54 | } 55 | }], 56 | assert (error, data) { 57 | expect(error).to.equal(null); 58 | expect(data).to.have.same.members(dir.deep.files); 59 | }, 60 | streamAssert (errors, data, files, dirs, symlinks) { 61 | expect(errors).to.have.lengthOf(0); 62 | expect(data).to.have.same.members(dir.deep.files); 63 | expect(files).to.have.same.members(dir.deep.files); 64 | expect(dirs).to.have.lengthOf(0); 65 | expect(symlinks).to.have.same.members(dir.symlinks.deep.files); 66 | }, 67 | }, 68 | { 69 | it: "should filter by directories", 70 | args: ["test/dir", { 71 | deep: true, 72 | filter (stats) { 73 | return stats.isDirectory(); 74 | } 75 | }], 76 | assert (error, data) { 77 | expect(error).to.equal(null); 78 | expect(data).to.have.same.members(dir.deep.dirs); 79 | }, 80 | streamAssert (errors, data, files, dirs, symlinks) { 81 | expect(errors).to.have.lengthOf(0); 82 | expect(data).to.have.same.members(dir.deep.dirs); 83 | expect(files).to.have.lengthOf(0); 84 | expect(dirs).to.have.same.members(dir.deep.dirs); 85 | expect(symlinks).to.have.same.members(dir.symlinks.deep.dirs); 86 | }, 87 | }, 88 | { 89 | it: "should filter by symlinks", 90 | args: ["test/dir", { 91 | deep: true, 92 | filter (stats) { 93 | return stats.isSymbolicLink(); 94 | } 95 | }], 96 | assert (error, data) { 97 | expect(error).to.equal(null); 98 | expect(data).to.have.same.members(dir.deep.symlinks); 99 | }, 100 | streamAssert (errors, data, files, dirs, symlinks) { 101 | expect(errors).to.have.lengthOf(0); 102 | expect(data).to.have.same.members(dir.deep.symlinks); 103 | expect(files).to.have.same.members(dir.symlinks.deep.files); 104 | expect(dirs).to.have.same.members(dir.symlinks.deep.dirs); 105 | expect(symlinks).to.have.same.members(dir.deep.symlinks); 106 | }, 107 | }, 108 | { 109 | it: "should return everything if the filter is true", 110 | args: ["test/dir", { 111 | filter: true, 112 | }], 113 | assert (error, data) { 114 | expect(error).to.equal(null); 115 | expect(data).to.have.same.members(dir.shallow.data); 116 | }, 117 | streamAssert (errors, data, files, dirs, symlinks) { 118 | expect(errors).to.have.lengthOf(0); 119 | expect(data).to.have.same.members(dir.shallow.data); 120 | expect(files).to.have.same.members(dir.shallow.files); 121 | expect(dirs).to.have.same.members(dir.shallow.dirs); 122 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 123 | }, 124 | }, 125 | { 126 | it: "should return nothing if the filter is false", 127 | args: ["test/dir", { 128 | filter: false, 129 | }], 130 | assert (error, data) { 131 | expect(error).to.equal(null); 132 | expect(data).to.have.lengthOf(0); 133 | }, 134 | streamAssert (errors, data, files, dirs, symlinks) { 135 | expect(errors).to.have.lengthOf(0); 136 | expect(data).to.have.lengthOf(0); 137 | expect(files).to.have.lengthOf(0); 138 | expect(dirs).to.have.lengthOf(0); 139 | expect(symlinks).to.have.lengthOf(0); 140 | }, 141 | }, 142 | { 143 | it: "should filter by a regular expression", 144 | args: ["test/dir", { 145 | filter: /.*empt[^aeiou]/, 146 | }], 147 | assert (error, data) { 148 | expect(error).to.equal(null); 149 | expect(data).to.have.same.members(dir.empties.shallow.data); 150 | }, 151 | streamAssert (errors, data, files, dirs, symlinks) { 152 | expect(errors).to.have.lengthOf(0); 153 | expect(data).to.have.same.members(dir.empties.shallow.data); 154 | expect(files).to.have.same.members(dir.empties.shallow.files); 155 | expect(dirs).to.have.same.members(dir.empties.shallow.dirs); 156 | expect(symlinks).to.have.same.members(dir.empties.shallow.symlinks); 157 | }, 158 | }, 159 | { 160 | it: "should use appropriate path separators when filtering by a regular expression", 161 | args: ["test/dir", { 162 | deep: true, 163 | sep: "\\", 164 | filter: /subdir\\[^\\]*\\.*\.txt/, 165 | }], 166 | assert (error, data) { 167 | expect(error).to.equal(null); 168 | expect(data).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromDir.data); 169 | }, 170 | streamAssert (errors, data, files, dirs, symlinks) { 171 | expect(errors).to.have.lengthOf(0); 172 | expect(data).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromDir.data); 173 | expect(files).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromDir.files); 174 | expect(dirs).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromDir.dirs); 175 | expect(symlinks).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromDir.symlinks); 176 | }, 177 | }, 178 | { 179 | it: "should filter by a glob pattern", 180 | args: ["test/dir", { 181 | filter: "empty*" 182 | }], 183 | assert (error, data) { 184 | expect(error).to.equal(null); 185 | expect(data).to.have.same.members(dir.empties.shallow.data); 186 | }, 187 | streamAssert (errors, data, files, dirs, symlinks) { 188 | expect(errors).to.have.lengthOf(0); 189 | expect(data).to.have.same.members(dir.empties.shallow.data); 190 | expect(files).to.have.same.members(dir.empties.shallow.files); 191 | expect(dirs).to.have.same.members(dir.empties.shallow.dirs); 192 | expect(symlinks).to.have.same.members(dir.empties.shallow.symlinks); 193 | }, 194 | }, 195 | { 196 | it: "should use POSIX paths when filtering by a glob pattern", 197 | args: ["test/dir", { 198 | deep: true, 199 | sep: "\\", 200 | filter: "subdir/*/*.txt", 201 | }], 202 | assert (error, data) { 203 | expect(error).to.equal(null); 204 | expect(data).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromDir.data); 205 | }, 206 | streamAssert (errors, data, files, dirs, symlinks) { 207 | expect(errors).to.have.lengthOf(0); 208 | expect(data).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromDir.data); 209 | expect(files).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromDir.files); 210 | expect(dirs).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromDir.dirs); 211 | expect(symlinks).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromDir.symlinks); 212 | }, 213 | }, 214 | { 215 | it: "should prepend a POSIX version of the basePath when filtering by a glob pattern", 216 | args: ["test/dir", { 217 | deep: true, 218 | basePath: dir.windowsBasePath, 219 | sep: "\\", 220 | filter: "C:/Windows/**/subdir/*/*.txt", 221 | }], 222 | assert (error, data) { 223 | expect(error).to.equal(null); 224 | expect(data).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromRoot.data); 225 | }, 226 | streamAssert (errors, data, files, dirs, symlinks) { 227 | expect(errors).to.have.lengthOf(0); 228 | expect(data).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromRoot.data); 229 | expect(files).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromRoot.files); 230 | expect(dirs).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromRoot.dirs); 231 | expect(symlinks).to.have.same.members(dir.subdir.subsubdir.txt.windowsStyle.fromRoot.symlinks); 232 | }, 233 | }, 234 | { 235 | it: "should filter by a file extension pattern", 236 | args: ["test/dir", { 237 | deep: true, 238 | filter: "**/*.txt", 239 | }], 240 | assert (error, data) { 241 | expect(error).to.equal(null); 242 | expect(data).to.have.same.members(dir.txt.deep.data); 243 | }, 244 | streamAssert (errors, data, files, dirs, symlinks) { 245 | expect(errors).to.have.lengthOf(0); 246 | expect(data).to.have.same.members(dir.txt.deep.data); 247 | expect(files).to.have.same.members(dir.txt.deep.files); 248 | expect(dirs).to.have.same.members(dir.txt.deep.dirs); 249 | expect(symlinks).to.have.same.members(dir.txt.deep.symlinks); 250 | }, 251 | }, 252 | { 253 | it: "should handle errors that occur in the filter function", 254 | args: ["test/dir", { 255 | filter (stats) { 256 | if (stats.isSymbolicLink()) { 257 | throw new Error("Boooooom!"); 258 | } 259 | return true; 260 | } 261 | }], 262 | assert (error, data) { 263 | // The sync & async APIs abort after the first error and don't return any data 264 | expect(error).to.be.an.instanceOf(Error); 265 | expect(error.message).to.equal("Boooooom!"); 266 | expect(data).to.equal(undefined); 267 | }, 268 | streamAssert (errors, data, files, dirs, symlinks) { 269 | // The streaming API emits errors and data separately 270 | expect(errors).to.have.lengthOf(5); 271 | expect(data).to.have.same.members([ 272 | ".dotdir", "empty", "subdir", ".dotfile", "empty.txt", "file.txt", "file.json", 273 | ]); 274 | expect(files).to.have.same.members([ 275 | ".dotfile", "empty.txt", "file.txt", "file.json", 276 | ]); 277 | expect(dirs).to.have.same.members([ 278 | ".dotdir", "empty", "subdir", 279 | ]); 280 | expect(symlinks).to.have.lengthOf(0); 281 | }, 282 | }, 283 | ]); 284 | }); 285 | -------------------------------------------------------------------------------- /test/specs/fs.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const forEachApi = require("../utils/for-each-api"); 4 | const dir = require("../utils/dir"); 5 | const path = require("path"); 6 | const fs = require("fs"); 7 | const { expect } = require("chai"); 8 | 9 | describe("options.fs", () => { 10 | forEachApi([ 11 | { 12 | it: "should have no effect if `options.fs` is null", 13 | args: ["test/dir", { fs: null }], 14 | assert (error, data) { 15 | expect(error).to.equal(null); 16 | expect(data).to.have.same.members(dir.shallow.data); 17 | }, 18 | streamAssert (errors, data, files, dirs, symlinks) { 19 | expect(errors).to.have.lengthOf(0); 20 | expect(data).to.have.same.members(dir.shallow.data); 21 | expect(files).to.have.same.members(dir.shallow.files); 22 | expect(dirs).to.have.same.members(dir.shallow.dirs); 23 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 24 | }, 25 | }, 26 | { 27 | it: "should have no effect if `options.fs` is empty", 28 | args: ["test/dir", { fs: {}}], 29 | assert (error, data) { 30 | expect(error).to.equal(null); 31 | expect(data).to.have.same.members(dir.shallow.data); 32 | }, 33 | streamAssert (errors, data, files, dirs, symlinks) { 34 | expect(errors).to.have.lengthOf(0); 35 | expect(data).to.have.same.members(dir.shallow.data); 36 | expect(files).to.have.same.members(dir.shallow.files); 37 | expect(dirs).to.have.same.members(dir.shallow.dirs); 38 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 39 | }, 40 | }, 41 | 42 | 43 | /************************************************************ 44 | * fs.readdir 45 | ************************************************************/ 46 | { 47 | it: "should use a custom `fs.readdir` method", 48 | args: ["test/dir", { 49 | fs: { 50 | readdir (dirPath, callback) { 51 | callback(null, dir.txt.shallow.data); 52 | }, 53 | } 54 | }], 55 | assert (error, data) { 56 | expect(error).to.equal(null); 57 | expect(data).to.have.same.members(dir.txt.shallow.data); 58 | }, 59 | streamAssert (errors, data, files, dirs, symlinks) { 60 | expect(errors).to.have.lengthOf(0); 61 | expect(data).to.have.same.members(dir.txt.shallow.data); 62 | expect(files).to.have.same.members(dir.txt.shallow.files); 63 | expect(dirs).to.have.same.members(dir.txt.shallow.dirs); 64 | expect(symlinks).to.have.same.members(dir.txt.shallow.symlinks); 65 | }, 66 | }, 67 | { 68 | it: "should handle an invalid file name from a custom `fs.readdir` method", 69 | args: ["test/dir", { 70 | deep: true, 71 | fs: { 72 | readdir (dirPath, callback) { 73 | callback(null, ["empty.txt", "invalid.txt", "file.txt"]); 74 | }, 75 | } 76 | }], 77 | assert (error, data) { 78 | // The sync & async APIs abort after the first error and don't return any data 79 | expect(error).to.be.an.instanceOf(Error); 80 | expect(error.code).to.equal("ENOENT"); 81 | expect(data).to.equal(undefined); 82 | }, 83 | streamAssert (errors, data, files, dirs, symlinks) { 84 | // The streaming API emits errors and data separately 85 | expect(errors).to.have.lengthOf(1); 86 | expect(errors[0].code).to.equal("ENOENT"); 87 | expect(data).to.have.same.members(["empty.txt", "file.txt"]); 88 | expect(files).to.have.same.members(["empty.txt", "file.txt"]); 89 | expect(dirs).to.have.lengthOf(0); 90 | expect(symlinks).to.have.lengthOf(0); 91 | }, 92 | }, 93 | { 94 | it: "should handle a null result from a custom `fs.readdir` method", 95 | args: ["test/dir", { 96 | deep: true, 97 | fs: { 98 | readdir (dirPath, callback) { 99 | callback(null, null); 100 | }, 101 | } 102 | }], 103 | assert (error, data) { 104 | // The sync & async APIs abort after the first error and don't return any data 105 | expect(error).to.be.an.instanceOf(TypeError); 106 | expect(error.message).to.match(/null is not an array/); 107 | expect(data).to.equal(undefined); 108 | }, 109 | streamAssert (errors, data, files, dirs, symlinks) { 110 | // The streaming API emits errors and data separately 111 | expect(errors).to.have.lengthOf(1); 112 | expect(errors[0].message).to.match(/null is not an array/); 113 | expect(data).to.have.lengthOf(0); 114 | expect(files).to.have.lengthOf(0); 115 | expect(dirs).to.have.lengthOf(0); 116 | expect(symlinks).to.have.lengthOf(0); 117 | }, 118 | }, 119 | { 120 | it: "should handle an invalid result from a custom `fs.readdir` method", 121 | args: ["test/dir", { 122 | deep: true, 123 | fs: { 124 | readdir (dirPath, callback) { 125 | callback(null, 12345); 126 | }, 127 | } 128 | }], 129 | assert (error, data) { 130 | // The sync & async APIs abort after the first error and don't return any data 131 | expect(error).to.be.an.instanceOf(TypeError); 132 | expect(error.message).to.equal("12345 is not an array"); 133 | expect(data).to.equal(undefined); 134 | }, 135 | streamAssert (errors, data, files, dirs, symlinks) { 136 | // The streaming API emits errors and data separately 137 | expect(errors).to.have.lengthOf(1); 138 | expect(errors[0].message).to.equal("12345 is not an array"); 139 | expect(data).to.have.lengthOf(0); 140 | expect(files).to.have.lengthOf(0); 141 | expect(dirs).to.have.lengthOf(0); 142 | expect(symlinks).to.have.lengthOf(0); 143 | }, 144 | }, 145 | { 146 | it: "should handle an error from a custom `fs.readdir` method", 147 | args: ["test/dir", { 148 | deep: true, 149 | fs: { 150 | readdir (dirPath, callback) { 151 | // Simulate a sporadic error 152 | if (dirPath === dir.path("test/dir/subdir/subsubdir")) { 153 | callback(new Error("Boooooom!")); 154 | } 155 | else { 156 | let files = fs.readdirSync(dirPath); 157 | callback(null, files); 158 | } 159 | }, 160 | } 161 | }], 162 | assert (error, data) { 163 | // The sync & async APIs abort after the first error and don't return any data 164 | expect(error).to.be.an.instanceOf(Error); 165 | expect(error.message).to.equal("Boooooom!"); 166 | expect(data).to.equal(undefined); 167 | }, 168 | streamAssert (errors, data, files, dirs, symlinks) { 169 | // The streaming API emits errors and data separately 170 | expect(errors).to.have.lengthOf(1); 171 | expect(data).to.have.same.members(this.omitSubdir(dir.deep.data)); 172 | expect(files).to.have.same.members(this.omitSubdir(dir.deep.files)); 173 | expect(dirs).to.have.same.members(this.omitSubdir(dir.deep.dirs)); 174 | expect(symlinks).to.have.same.members(this.omitSubdir(dir.deep.symlinks)); 175 | }, 176 | 177 | // Omits the contents of the "subsubdir" directory 178 | omitSubdir (paths) { 179 | return paths.filter(p => { 180 | return p.substr(7, 10) !== "subsubdir" + path.sep; 181 | }); 182 | } 183 | }, 184 | { 185 | it: "should handle an error thrown by a custom `fs.readdir` method", 186 | args: ["test/dir", { 187 | deep: true, 188 | fs: { 189 | readdir (dirPath, callback) { 190 | // Simulate an error being thrown (rather than returned to the callback) 191 | if (dirPath === dir.path("test/dir/subdir/subsubdir")) { 192 | throw new Error("Boooooom!"); 193 | } 194 | else { 195 | let files = fs.readdirSync(dirPath); 196 | callback(null, files); 197 | } 198 | }, 199 | } 200 | }], 201 | assert (error, data) { 202 | // The sync & async APIs abort after the first error and don't return any data 203 | expect(error).to.be.an.instanceOf(Error); 204 | expect(error.message).to.equal("Boooooom!"); 205 | expect(data).to.equal(undefined); 206 | }, 207 | streamAssert (errors, data, files, dirs, symlinks) { 208 | // The streaming API emits errors and data separately 209 | expect(errors).to.have.lengthOf(1); 210 | expect(data).to.have.same.members(this.omitSubdir(dir.deep.data)); 211 | expect(files).to.have.same.members(this.omitSubdir(dir.deep.files)); 212 | expect(dirs).to.have.same.members(this.omitSubdir(dir.deep.dirs)); 213 | expect(symlinks).to.have.same.members(this.omitSubdir(dir.deep.symlinks)); 214 | }, 215 | 216 | // Omits the contents of the "subsubdir" directory 217 | omitSubdir (paths) { 218 | return paths.filter(p => { 219 | return p.substr(7, 10) !== "subsubdir" + path.sep; 220 | }); 221 | } 222 | }, 223 | 224 | 225 | /************************************************************ 226 | * fs.stat 227 | ************************************************************/ 228 | { 229 | it: "should use a custom `fs.stat` method", 230 | args: ["test/dir", { 231 | deep: true, 232 | fs: { 233 | stat (dirPath, callback) { 234 | callback(null, { 235 | isFile () { return true; }, 236 | isDirectory () { return false; }, 237 | isSymbolicLink () { return false; }, 238 | }); 239 | }, 240 | } 241 | }], 242 | assert (error, data) { 243 | expect(error).to.equal(null); 244 | expect(data).to.have.same.members(this.omitSymlinkDirs(dir.deep.data)); 245 | }, 246 | streamAssert (errors, data, files, dirs, symlinks) { 247 | expect(errors).to.have.lengthOf(0); 248 | expect(data).to.have.same.members(this.omitSymlinkDirs(dir.deep.data)); 249 | expect(files).to.have.same.members( 250 | Array.from(new Set( 251 | this.omitSymlinkDirs(dir.deep.files) 252 | .concat(this.omitSymlinkDirs(dir.deep.symlinks)))) 253 | ); 254 | expect(dirs).to.have.same.members(this.omitSymlinks(dir.deep.dirs)); 255 | expect(symlinks).to.have.same.members(this.omitSymlinkDirs(dir.deep.symlinks)); 256 | }, 257 | 258 | // Omits symlink directories 259 | omitSymlinks (files) { 260 | return files.filter(file => !file.includes("symlink")); 261 | }, 262 | 263 | // Omits symlink directories 264 | omitSymlinkDirs (files) { 265 | return files.filter(file => !file.includes("symlink" + path.sep)); 266 | }, 267 | }, 268 | { 269 | it: "should handle a null result from a custom `fs.stat` method", 270 | args: ["test/dir", { 271 | deep: true, 272 | fs: { 273 | stat (dirPath, callback) { 274 | callback(null, null); 275 | }, 276 | } 277 | }], 278 | assert (error, data) { 279 | // The sync & async APIs abort after the first error and don't return any data 280 | expect(error).to.be.an.instanceOf(TypeError); 281 | expect(error.message).to.match(/Cannot \w+ property 'isSymbolicLink' of null/); 282 | expect(data).to.equal(undefined); 283 | }, 284 | streamAssert (errors, data, files, dirs, symlinks) { 285 | // The streaming API emits errors and data separately 286 | expect(errors).to.have.lengthOf(7); 287 | expect(errors[0].message).to.match(/Cannot \w+ property 'isSymbolicLink' of null/); 288 | expect(data).to.have.same.members(this.omitSymlinks(dir.deep.data)); 289 | expect(files).to.have.same.members(this.omitSymlinks(dir.deep.files)); 290 | expect(dirs).to.have.same.members(this.omitSymlinks(dir.deep.dirs)); 291 | expect(symlinks).to.have.same.members(this.omitSymlinks(dir.deep.symlinks)); 292 | }, 293 | 294 | // Omits symlinks 295 | omitSymlinks (files) { 296 | return files.filter(file => !file.includes("symlink")); 297 | } 298 | }, 299 | { 300 | it: "should handle an invalid result from a custom `fs.stat` method", 301 | args: ["test/dir", { 302 | deep: true, 303 | fs: { 304 | stat (dirPath, callback) { 305 | callback(null, "Hello, world"); 306 | }, 307 | } 308 | }], 309 | assert (error, data) { 310 | // The sync & async APIs abort after the first error and don't return any data 311 | expect(error).to.be.an.instanceOf(TypeError); 312 | expect(error.message).to.match(/Cannot .* property 'isSymbolicLink'/); 313 | expect(data).to.equal(undefined); 314 | }, 315 | streamAssert (errors, data, files, dirs, symlinks) { 316 | // The streaming API emits errors and data separately 317 | expect(errors).to.have.lengthOf(7); 318 | expect(errors[0].message).to.match(/Cannot .* property 'isSymbolicLink'/); 319 | expect(data).to.have.same.members(this.omitSymlinks(dir.deep.data)); 320 | expect(files).to.have.same.members(this.omitSymlinks(dir.deep.files)); 321 | expect(dirs).to.have.same.members(this.omitSymlinks(dir.deep.dirs)); 322 | expect(symlinks).to.have.same.members(this.omitSymlinks(dir.deep.symlinks)); 323 | }, 324 | 325 | // Omits symlinks 326 | omitSymlinks (files) { 327 | return files.filter(file => !file.includes("symlink")); 328 | } 329 | }, 330 | { 331 | it: "should handle an error from a custom `fs.stat` method", 332 | args: ["test/dir", { 333 | fs: { 334 | stat (filePath, callback) { 335 | // Simulate a sporadic error 336 | if (filePath === dir.path("test/dir/subsubdir-symlink")) { 337 | callback(new Error("Boooooom!")); 338 | } 339 | else { 340 | let stats = fs.statSync(filePath); 341 | callback(null, stats); 342 | } 343 | }, 344 | } 345 | }], 346 | assert (error, data) { 347 | // An error in fs.stat is handled internally, so no error is thrown 348 | expect(error).to.equal(null); 349 | expect(data).to.have.same.members(dir.shallow.data); 350 | }, 351 | streamAssert (errors, data, files, dirs, symlinks) { 352 | expect(errors).to.have.lengthOf(0); 353 | expect(data).to.have.same.members(dir.shallow.data); 354 | expect(files).to.have.same.members(dir.shallow.files); 355 | expect(dirs).to.have.same.members( 356 | dir.shallow.dirs.filter(file => file !== "subsubdir-symlink") 357 | ); 358 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 359 | }, 360 | }, 361 | { 362 | it: "should handle an error thrown by a custom `fs.stat` method", 363 | args: ["test/dir", { 364 | fs: { 365 | stat (filePath, callback) { 366 | // Simulate an error being thrown (rather than returned to the callback) 367 | if (filePath === dir.path("test/dir/subsubdir-symlink")) { 368 | throw new Error("Boooooom!"); 369 | } 370 | else { 371 | let stats = fs.statSync(filePath); 372 | callback(null, stats); 373 | } 374 | }, 375 | } 376 | }], 377 | assert (error, data) { 378 | // An error in fs.stat is handled internally, so no error is thrown 379 | expect(error).to.equal(null); 380 | expect(data).to.have.same.members(dir.shallow.data); 381 | }, 382 | streamAssert (errors, data, files, dirs, symlinks) { 383 | expect(errors).to.have.lengthOf(0); 384 | expect(data).to.have.same.members(dir.shallow.data); 385 | expect(files).to.have.same.members(dir.shallow.files); 386 | expect(dirs).to.have.same.members( 387 | dir.shallow.dirs.filter(file => file !== "subsubdir-symlink") 388 | ); 389 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 390 | }, 391 | }, 392 | 393 | 394 | /************************************************************ 395 | * fs.lstat 396 | ************************************************************/ 397 | { 398 | it: "should use a custom `fs.lstat` method", 399 | args: ["test/dir", { 400 | deep: true, 401 | fs: { 402 | lstat (dirPath, callback) { 403 | callback(null, { 404 | isFile () { return true; }, 405 | isDirectory () { return false; }, 406 | isSymbolicLink () { return false; }, 407 | }); 408 | }, 409 | } 410 | }], 411 | assert (error, data) { 412 | expect(error).to.equal(null); 413 | expect(data).to.have.same.members(dir.shallow.data); 414 | }, 415 | streamAssert (errors, data, files, dirs, symlinks) { 416 | expect(errors).to.have.lengthOf(0); 417 | expect(data).to.have.same.members(dir.shallow.data); 418 | expect(files).to.have.same.members(dir.shallow.data); 419 | expect(dirs).to.have.lengthOf(0); 420 | expect(symlinks).to.have.lengthOf(0); 421 | }, 422 | }, 423 | { 424 | it: "should handle a null result from a custom `fs.lstat` method", 425 | args: ["test/dir", { 426 | deep: true, 427 | fs: { 428 | lstat (dirPath, callback) { 429 | callback(null, null); 430 | }, 431 | } 432 | }], 433 | assert (error, data) { 434 | // The sync & async APIs abort after the first error and don't return any data 435 | expect(error).to.be.an.instanceOf(TypeError); 436 | expect(error.message).to.match(/Cannot \w+ property 'isSymbolicLink' of null/); 437 | expect(data).to.equal(undefined); 438 | }, 439 | streamAssert (errors, data, files, dirs, symlinks) { 440 | // The streaming API emits errors and data separately 441 | expect(errors).to.have.lengthOf(12); 442 | expect(errors[0].message).to.match(/Cannot \w+ property 'isSymbolicLink' of null/); 443 | expect(data).to.have.lengthOf(0); 444 | expect(files).to.have.lengthOf(0); 445 | expect(dirs).to.have.lengthOf(0); 446 | expect(symlinks).to.have.lengthOf(0); 447 | }, 448 | }, 449 | { 450 | it: "should handle an invalid result from a custom `fs.lstat` method", 451 | args: ["test/dir", { 452 | deep: true, 453 | fs: { 454 | lstat (dirPath, callback) { 455 | callback(null, "Hello, world"); 456 | }, 457 | } 458 | }], 459 | assert (error, data) { 460 | // The sync & async APIs abort after the first error and don't return any data 461 | expect(error).to.be.an.instanceOf(TypeError); 462 | expect(error.message).to.match(/lstats.isSymbolicLink is not a function/); 463 | expect(data).to.equal(undefined); 464 | }, 465 | streamAssert (errors, data, files, dirs, symlinks) { 466 | // The streaming API emits errors and data separately 467 | expect(errors).to.have.lengthOf(12); 468 | expect(errors[0].message).to.match(/lstats.isSymbolicLink is not a function/); 469 | expect(data).to.have.lengthOf(0); 470 | expect(files).to.have.lengthOf(0); 471 | expect(dirs).to.have.lengthOf(0); 472 | expect(symlinks).to.have.lengthOf(0); 473 | }, 474 | }, 475 | { 476 | it: "should handle an error from a custom `fs.lstat` method", 477 | args: ["test/dir", { 478 | deep: true, 479 | fs: { 480 | lstat (filePath, callback) { 481 | // Simulate a sporadic error 482 | if (filePath === dir.path("test/dir/subsubdir-symlink")) { 483 | callback(new Error("Boooooom!")); 484 | } 485 | else { 486 | let lstats = fs.lstatSync(filePath); 487 | callback(null, lstats); 488 | } 489 | }, 490 | } 491 | }], 492 | assert (error, data) { 493 | // An error in fs.lstat is handled internally, so no error is thrown 494 | expect(error).to.be.an.instanceOf(Error); 495 | expect(data).to.equal(undefined); 496 | }, 497 | streamAssert (errors, data, files, dirs, symlinks) { 498 | expect(errors).to.have.lengthOf(1); 499 | expect(data).to.have.same.members(this.omitSubdirSymlink(dir.deep.data)); 500 | expect(files).to.have.same.members(this.omitSubdirSymlink(dir.deep.files)); 501 | expect(dirs).to.have.same.members(this.omitSubdirSymlink(dir.deep.dirs)); 502 | expect(symlinks).to.have.same.members(this.omitSubdirSymlink(dir.deep.symlinks)); 503 | }, 504 | 505 | // Omits the "subsubdir-symlink" directory and its children 506 | omitSubdirSymlink (files) { 507 | return files.filter(file => !file.includes("subsubdir-symlink")); 508 | } 509 | }, 510 | { 511 | it: "should handle an error thrown by a custom `fs.lstat` method", 512 | args: ["test/dir", { 513 | deep: true, 514 | fs: { 515 | lstat (filePath, callback) { 516 | // Simulate an error being thrown (rather than returned to the callback) 517 | if (filePath === dir.path("test/dir/subsubdir-symlink")) { 518 | throw new Error("Boooooom!"); 519 | } 520 | else { 521 | let lstats = fs.lstatSync(filePath); 522 | callback(null, lstats); 523 | } 524 | }, 525 | } 526 | }], 527 | assert (error, data) { 528 | // An error in fs.lstat is handled internally, so no error is thrown 529 | expect(error).to.be.an.instanceOf(Error); 530 | expect(data).to.equal(undefined); 531 | }, 532 | streamAssert (errors, data, files, dirs, symlinks) { 533 | expect(errors).to.have.lengthOf(1); 534 | expect(data).to.have.same.members(this.omitSubdirSymlink(dir.deep.data)); 535 | expect(files).to.have.same.members(this.omitSubdirSymlink(dir.deep.files)); 536 | expect(dirs).to.have.same.members(this.omitSubdirSymlink(dir.deep.dirs)); 537 | expect(symlinks).to.have.same.members(this.omitSubdirSymlink(dir.deep.symlinks)); 538 | }, 539 | 540 | // Omits the "subsubdir-symlink" directory and its children 541 | omitSubdirSymlink (files) { 542 | return files.filter(file => !file.includes("subsubdir-symlink")); 543 | } 544 | }, 545 | ]); 546 | }); 547 | -------------------------------------------------------------------------------- /test/specs/sep.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const forEachApi = require("../utils/for-each-api"); 4 | const dir = require("../utils/dir"); 5 | const { expect } = require("chai"); 6 | const path = require("path"); 7 | 8 | describe("options.sep", () => { 9 | forEachApi([ 10 | { 11 | it: "should have no effect if `options.deep` is not set", 12 | args: ["test/dir", { sep: "_" }], 13 | assert (error, data) { 14 | expect(error).to.equal(null); 15 | expect(data).to.have.same.members(dir.shallow.data); 16 | }, 17 | streamAssert (errors, data, files, dirs, symlinks) { 18 | expect(errors).to.have.lengthOf(0); 19 | expect(data).to.have.same.members(dir.shallow.data); 20 | expect(files).to.have.same.members(dir.shallow.files); 21 | expect(dirs).to.have.same.members(dir.shallow.dirs); 22 | expect(symlinks).to.have.same.members(dir.shallow.symlinks); 23 | }, 24 | }, 25 | { 26 | it: 'should return POSIX paths if sep === "/"', 27 | args: ["test/dir", { deep: true, sep: "/" }], 28 | assert (error, data) { 29 | expect(error).to.equal(null); 30 | assertPathsMatch(data, dir.deep.data, "/"); 31 | }, 32 | streamAssert (errors, data, files, dirs, symlinks) { 33 | expect(errors).to.have.lengthOf(0); 34 | assertPathsMatch(data, dir.deep.data, "/"); 35 | assertPathsMatch(files, dir.deep.files, "/"); 36 | assertPathsMatch(dirs, dir.deep.dirs, "/"); 37 | assertPathsMatch(symlinks, dir.deep.symlinks, "/"); 38 | }, 39 | }, 40 | { 41 | it: 'should return Windows paths if sep === "\\"', 42 | args: ["test/dir", { deep: true, sep: "\\" }], 43 | assert (error, data) { 44 | expect(error).to.equal(null); 45 | assertPathsMatch(data, dir.deep.data, "\\"); 46 | }, 47 | streamAssert (errors, data, files, dirs, symlinks) { 48 | expect(errors).to.have.lengthOf(0); 49 | assertPathsMatch(data, dir.deep.data, "\\"); 50 | assertPathsMatch(files, dir.deep.files, "\\"); 51 | assertPathsMatch(dirs, dir.deep.dirs, "\\"); 52 | assertPathsMatch(symlinks, dir.deep.symlinks, "\\"); 53 | }, 54 | }, 55 | { 56 | it: "should allow sep to be an empty string", 57 | args: ["test/dir", { deep: true, sep: "" }], 58 | assert (error, data) { 59 | expect(error).to.equal(null); 60 | assertPathsMatch(data, dir.deep.data, ""); 61 | }, 62 | streamAssert (errors, data, files, dirs, symlinks) { 63 | expect(errors).to.have.lengthOf(0); 64 | assertPathsMatch(data, dir.deep.data, ""); 65 | assertPathsMatch(files, dir.deep.files, ""); 66 | assertPathsMatch(dirs, dir.deep.dirs, ""); 67 | assertPathsMatch(symlinks, dir.deep.symlinks, ""); 68 | }, 69 | }, 70 | { 71 | it: "should allow sep to be multiple characters", 72 | args: ["test/dir", { deep: true, sep: "-----" }], 73 | assert (error, data) { 74 | expect(error).to.equal(null); 75 | assertPathsMatch(data, dir.deep.data, "-----"); 76 | }, 77 | streamAssert (errors, data, files, dirs, symlinks) { 78 | expect(errors).to.have.lengthOf(0); 79 | assertPathsMatch(data, dir.deep.data, "-----"); 80 | assertPathsMatch(files, dir.deep.files, "-----"); 81 | assertPathsMatch(dirs, dir.deep.dirs, "-----"); 82 | assertPathsMatch(symlinks, dir.deep.symlinks, "-----"); 83 | }, 84 | }, 85 | ]); 86 | 87 | function assertPathsMatch (actual, expected, sep) { 88 | let regExp = new RegExp("\\" + path.sep, "g"); 89 | let expectedPaths = expected.map(expectedPath => { 90 | return expectedPath.replace(regExp, sep); 91 | }); 92 | expect(actual).to.have.same.members(expectedPaths); 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /test/specs/stats.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const readdir = require("../../"); 4 | const dir = require("../utils/dir"); 5 | const isStats = require("../utils/is-stats"); 6 | const { expect } = require("chai"); 7 | 8 | describe("options.stats", () => { 9 | describe("Synchronous API", () => { 10 | it("should return stats instead of paths", done => { 11 | let data = readdir.sync("test/dir", { stats: true }); 12 | assertStats(data, dir.shallow.data, done); 13 | }); 14 | }); 15 | 16 | describe("Asynchronous API (callback/Promise)", () => { 17 | it("should return stats instead of paths", done => { 18 | readdir.async("test/dir", { stats: true }, (err, data) => { 19 | expect(err).to.equal(null); 20 | assertStats(data, dir.shallow.data, done); 21 | }); 22 | }); 23 | }); 24 | 25 | describe("Stream/EventEmitter API", () => { 26 | it("should return stats instead of paths", done => { 27 | let error, data = [], files = [], dirs = [], symlinks = []; 28 | let stream = readdir.stream("test/dir", { stats: true }); 29 | 30 | stream.on("error", done); 31 | stream.on("data", dataInfo => { 32 | data.push(dataInfo); 33 | }); 34 | stream.on("file", fileInfo => { 35 | files.push(fileInfo); 36 | }); 37 | stream.on("directory", dirInfo => { 38 | dirs.push(dirInfo); 39 | }); 40 | stream.on("symlink", symlinkInfo => { 41 | symlinks.push(symlinkInfo); 42 | }); 43 | stream.on("end", () => { 44 | assertStats(data, dir.shallow.data, errorHandler); 45 | assertStats(files, dir.shallow.files, errorHandler); 46 | assertStats(dirs, dir.shallow.dirs, errorHandler); 47 | assertStats(symlinks, dir.shallow.symlinks, errorHandler); 48 | done(error); 49 | 50 | function errorHandler (e) { error = error || e; } 51 | }); 52 | }); 53 | }); 54 | 55 | describe("Iterator API", () => { 56 | it("should return stats instead of paths", done => { 57 | Promise.resolve() 58 | .then(async () => { 59 | let data = []; 60 | 61 | for await (let stat of readdir.iterator("test/dir", { stats: true })) { 62 | data.push(stat); 63 | } 64 | 65 | assertStats(data, dir.shallow.data, done); 66 | }) 67 | .catch(done); 68 | }); 69 | }); 70 | 71 | function assertStats (data, expected, done) { 72 | try { 73 | // Should return an array of the correct length 74 | expect(data).to.be.an("array").with.lengthOf(expected.length); 75 | 76 | // Should return the expected paths 77 | let paths = data.map(stat => { return stat.path; }); 78 | expect(paths).to.have.same.members(expected); 79 | 80 | // Each item should be a valid fs.Stats object 81 | for (let stat of data) { 82 | expect(stat).to.satisfy(isStats); 83 | } 84 | 85 | done(); 86 | } 87 | catch (error) { 88 | done(error); 89 | } 90 | } 91 | }); 92 | -------------------------------------------------------------------------------- /test/specs/stream.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const readdir = require("../../"); 4 | const dir = require("../utils/dir"); 5 | const { expect } = require("chai"); 6 | const through2 = require("through2"); 7 | const fs = require("fs"); 8 | 9 | let nodeVersion = parseFloat(process.version.substr(1)); 10 | 11 | describe("Stream API", () => { 12 | it("should be able to pipe to other streams as a Buffer", done => { 13 | let allData = []; 14 | 15 | readdir.stream("test/dir") 16 | .pipe(through2((data, enc, next) => { 17 | try { 18 | // By default, the data is streamed as a Buffer 19 | expect(data).to.be.an.instanceOf(Buffer); 20 | 21 | // Buffer.toString() returns the file name 22 | allData.push(data.toString()); 23 | 24 | next(null, data); 25 | } 26 | catch (e) { 27 | next(e); 28 | } 29 | })) 30 | .on("finish", () => { 31 | try { 32 | expect(allData).to.have.same.members(dir.shallow.data); 33 | done(); 34 | } 35 | catch (e) { 36 | done(e); 37 | } 38 | }) 39 | .on("error", err => { 40 | done(err); 41 | }); 42 | }); 43 | 44 | it('should be able to pipe to other streams in "object mode"', done => { 45 | let allData = []; 46 | 47 | readdir.stream("test/dir") 48 | .pipe(through2({ objectMode: true }, (data, enc, next) => { 49 | try { 50 | // In "object mode", the data is a string 51 | expect(data).to.be.a("string"); 52 | 53 | allData.push(data); 54 | next(null, data); 55 | } 56 | catch (e) { 57 | next(e); 58 | } 59 | })) 60 | .on("finish", () => { 61 | try { 62 | expect(allData).to.have.same.members(dir.shallow.data); 63 | done(); 64 | } 65 | catch (e) { 66 | done(e); 67 | } 68 | }) 69 | .on("error", err => { 70 | done(err); 71 | }); 72 | }); 73 | 74 | it('should be able to pipe fs.Stats to other streams in "object mode"', done => { 75 | let allData = []; 76 | 77 | readdir.stream("test/dir", { stats: true }) 78 | .pipe(through2({ objectMode: true }, (data, enc, next) => { 79 | try { 80 | // The data is an fs.Stats object 81 | expect(data).to.be.an("object"); 82 | expect(data).to.be.an.instanceOf(fs.Stats); 83 | 84 | allData.push(data.path); 85 | next(null, data); 86 | } 87 | catch (e) { 88 | next(e); 89 | } 90 | })) 91 | .on("finish", () => { 92 | try { 93 | expect(allData).to.have.same.members(dir.shallow.data); 94 | done(); 95 | } 96 | catch (e) { 97 | done(e); 98 | } 99 | }) 100 | .on("error", done); 101 | }); 102 | 103 | it("should be able to pause & resume the stream", done => { 104 | let allData = []; 105 | 106 | let stream = readdir.stream("test/dir") 107 | .on("data", data => { 108 | allData.push(data); 109 | 110 | // The stream should not be paused 111 | expect(stream.isPaused()).to.equal(false); 112 | 113 | if (allData.length === 3) { 114 | // Pause for one second 115 | stream.pause(); 116 | setTimeout(() => { 117 | try { 118 | // The stream should still be paused 119 | expect(stream.isPaused()).to.equal(true); 120 | 121 | // The array should still only contain 3 items 122 | expect(allData).to.have.lengthOf(3); 123 | 124 | // Read the rest of the stream 125 | stream.resume(); 126 | } 127 | catch (e) { 128 | done(e); 129 | } 130 | }, 1000); 131 | } 132 | }) 133 | .on("end", () => { 134 | expect(allData).to.have.same.members(dir.shallow.data); 135 | done(); 136 | }) 137 | .on("error", done); 138 | }); 139 | 140 | it('should be able to use "readable" and "read"', done => { 141 | let allData = []; 142 | let nullCount = 0; 143 | 144 | let stream = readdir.stream("test/dir") 145 | .on("readable", () => { 146 | // Manually read the next chunk of data 147 | let data = stream.read(); 148 | 149 | while (true) { // eslint-disable-line 150 | if (data === null) { 151 | // The stream is done 152 | nullCount++; 153 | break; 154 | } 155 | else { 156 | // The data should be a string (the file name) 157 | expect(data).to.be.a("string").with.length.of.at.least(1); 158 | allData.push(data); 159 | 160 | data = stream.read(); 161 | } 162 | } 163 | }) 164 | .on("end", () => { 165 | if (nodeVersion >= 12) { 166 | // In Node >= 12, the "readable" event fires twice, 167 | // and stream.read() returns null twice 168 | expect(nullCount).to.equal(2); 169 | } 170 | else if (nodeVersion >= 10) { 171 | // In Node >= 10, the "readable" event only fires once, 172 | // and stream.read() only returns null once 173 | expect(nullCount).to.equal(1); 174 | } 175 | else { 176 | // In Node < 10, the "readable" event fires 13 times (once per file), 177 | // and stream.read() returns null each time 178 | expect(nullCount).to.equal(13); 179 | } 180 | 181 | expect(allData).to.have.same.members(dir.shallow.data); 182 | done(); 183 | }) 184 | .on("error", done); 185 | }); 186 | 187 | it('should be able to subscribe to custom events instead of "data"', done => { 188 | let allFiles = []; 189 | let allSubdirs = []; 190 | 191 | let stream = readdir.stream("test/dir"); 192 | 193 | // Calling "resume" is required, since we're not handling the "data" event 194 | stream.resume(); 195 | 196 | stream 197 | .on("file", filename => { 198 | expect(filename).to.be.a("string").with.length.of.at.least(1); 199 | allFiles.push(filename); 200 | }) 201 | .on("directory", subdir => { 202 | expect(subdir).to.be.a("string").with.length.of.at.least(1); 203 | allSubdirs.push(subdir); 204 | }) 205 | .on("end", () => { 206 | expect(allFiles).to.have.same.members(dir.shallow.files); 207 | expect(allSubdirs).to.have.same.members(dir.shallow.dirs); 208 | done(); 209 | }) 210 | .on("error", done); 211 | }); 212 | 213 | it('should handle errors that occur in the "data" event listener', done => { 214 | testErrorHandling("data", dir.shallow.data, 7, done); 215 | }); 216 | 217 | it('should handle errors that occur in the "file" event listener', done => { 218 | testErrorHandling("file", dir.shallow.files, 3, done); 219 | }); 220 | 221 | it('should handle errors that occur in the "directory" event listener', done => { 222 | testErrorHandling("directory", dir.shallow.dirs, 2, done); 223 | }); 224 | 225 | it('should handle errors that occur in the "symlink" event listener', done => { 226 | testErrorHandling("symlink", dir.shallow.symlinks, 5, done); 227 | }); 228 | 229 | function testErrorHandling (eventName, expected, expectedErrors, done) { 230 | let errors = [], data = []; 231 | let stream = readdir.stream("test/dir"); 232 | 233 | // Capture all errors 234 | stream.on("error", error => { 235 | errors.push(error); 236 | }); 237 | 238 | stream.on(eventName, path => { 239 | data.push(path); 240 | 241 | if (path.indexOf(".txt") >= 0 || path.indexOf("dir-") >= 0) { 242 | throw new Error("Epic Fail!!!"); 243 | } 244 | else { 245 | return true; 246 | } 247 | }); 248 | 249 | stream.on("end", () => { 250 | try { 251 | // Make sure the correct number of errors were thrown 252 | expect(errors).to.have.lengthOf(expectedErrors); 253 | for (let error of errors) { 254 | expect(error.message).to.equal("Epic Fail!!!"); 255 | } 256 | 257 | // All of the events should have still been emitted, despite the errors 258 | expect(data).to.have.same.members(expected); 259 | 260 | done(); 261 | } 262 | catch (e) { 263 | done(e); 264 | } 265 | }); 266 | 267 | stream.resume(); 268 | } 269 | }); 270 | -------------------------------------------------------------------------------- /test/specs/typescript-definition.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-style, @typescript-eslint/no-unused-vars, @typescript-eslint/no-unused-vars-experimental */ 2 | import readdir, { readdirAsync, readdirIterator, readdirStream, readdirSync, Stats } from "../../"; 3 | 4 | const root = "path/to/some/directory"; 5 | const options = {}; 6 | const pathsCallback = (err: Error | null, paths: string[]) => undefined; 7 | const statsCallback = (err: Error | null, stats: Stats[]) => undefined; 8 | const writableStream = {} as NodeJS.WritableStream; 9 | const pathHandler = (path: string) => undefined; 10 | const pathsHandler = (paths: string[]) => undefined; 11 | const statsHandler = (stats: Stats[]) => undefined; 12 | const errorHandler = (err: Error) => undefined; 13 | const statsFilter = (stats: Stats) => true; 14 | 15 | export function testSyncApi() { 16 | let paths: string[]; 17 | paths = readdir.sync(root); 18 | paths = readdirSync(root); 19 | paths = readdir.sync(root, options); 20 | paths = readdirSync(root, options); 21 | } 22 | 23 | export function testCallbackApi() { 24 | readdir(root, pathsCallback); 25 | readdir.async(root, pathsCallback); 26 | readdirAsync(root, pathsCallback); 27 | readdir(root, options, pathsCallback); 28 | readdir.async(root, options, pathsCallback); 29 | readdirAsync(root, options, pathsCallback); 30 | } 31 | 32 | export function testPromiseApi() { 33 | readdir(root).then(pathsHandler).catch(errorHandler); 34 | readdir.async(root).then(pathsHandler).catch(errorHandler); 35 | readdirAsync(root).then(pathsHandler).catch(errorHandler); 36 | readdir(root, options).then(pathsHandler).catch(errorHandler); 37 | readdir.async(root, options).then(pathsHandler).catch(errorHandler); 38 | readdirAsync(root, options).then(pathsHandler).catch(errorHandler); 39 | } 40 | 41 | export function testEventEmitterApi() { 42 | readdir.stream(root).on("data", pathHandler).on("error", errorHandler); 43 | readdirStream(root).on("data", pathHandler).on("error", errorHandler); 44 | readdir.stream(root, options).on("data", pathHandler).on("error", errorHandler); 45 | readdirStream(root, options).on("data", pathHandler).on("error", errorHandler); 46 | } 47 | 48 | export function testStreamingApi() { 49 | readdir.stream(root).pipe(writableStream); 50 | readdirStream(root).pipe(writableStream); 51 | readdir.stream(root, options).pipe(writableStream); 52 | readdirStream(root, options).pipe(writableStream); 53 | } 54 | 55 | export async function testIteratorApi() { 56 | for await (let path of readdir.iterator(root)) { path = ""; } 57 | for await (let path of readdirIterator(root)) { path = ""; } 58 | for await (let path of readdir.iterator(root, options)) { path = ""; } 59 | for await (let path of readdirIterator(root, options)) { path = ""; } 60 | } 61 | 62 | export async function testDeepOption() { 63 | readdirSync(root, { deep: true }); 64 | readdirAsync(root, { deep: true }, pathsCallback); 65 | readdirStream(root, { deep: true }).on("data", pathHandler); 66 | for await (let path of readdirIterator(root, { deep: true })) { path = ""; } 67 | 68 | readdirSync(root, { deep: 5 }); 69 | readdirAsync(root, { deep: 5 }, pathsCallback); 70 | readdirStream(root, { deep: 5 }).on("data", pathHandler); 71 | for await (let path of readdirIterator(root, { deep: 5 })) { path = ""; } 72 | 73 | readdirSync(root, { deep: "subdir/**" }); 74 | readdirAsync(root, { deep: "subdir/**" }, pathsCallback); 75 | readdirStream(root, { deep: "subdir/**" }).on("data", pathHandler); 76 | for await (let path of readdirIterator(root, { deep: "subdir/**" })) { path = ""; } 77 | 78 | readdirSync(root, { deep: /subdir|subdir2/ }); 79 | readdirAsync(root, { deep: /subdir|subdir2/ }, pathsCallback); 80 | readdirStream(root, { deep: /subdir|subdir2/ }).on("data", pathHandler); 81 | for await (let path of readdirIterator(root, { deep: /subdir|subdir2/ })) { path = ""; } 82 | 83 | readdirSync(root, { deep: statsFilter }); 84 | readdirAsync(root, { deep: statsFilter }, pathsCallback); 85 | readdirStream(root, { deep: statsFilter }).on("data", pathHandler); 86 | for await (let path of readdirIterator(root, { deep: statsFilter })) { path = ""; } 87 | } 88 | 89 | export async function testFilterOption() { 90 | readdirSync(root, { filter: true }); 91 | readdirAsync(root, { filter: true }, pathsCallback); 92 | readdirStream(root, { filter: true }).on("data", pathHandler); 93 | for await (let path of readdirIterator(root, { filter: true })) { path = ""; } 94 | 95 | readdirSync(root, { filter: false }); 96 | readdirAsync(root, { filter: false }, pathsCallback); 97 | readdirStream(root, { filter: false }).on("data", pathHandler); 98 | for await (let path of readdirIterator(root, { filter: false })) { path = ""; } 99 | 100 | readdirSync(root, { filter: "*.txt" }); 101 | readdirAsync(root, { filter: "*.txt" }, pathsCallback); 102 | readdirStream(root, { filter: "*.txt" }).on("data", pathHandler); 103 | for await (let path of readdirIterator(root, { filter: "*.txt" })) { path = ""; } 104 | 105 | readdirSync(root, { filter: /\.txt$/ }); 106 | readdirAsync(root, { filter: /\.txt$/ }, pathsCallback); 107 | readdirStream(root, { filter: /\.txt$/ }).on("data", pathHandler); 108 | for await (let path of readdirIterator(root, { filter: /\.txt$/ })) { path = ""; } 109 | 110 | readdirSync(root, { filter: statsFilter }); 111 | readdirAsync(root, { filter: statsFilter }, pathsCallback); 112 | readdirStream(root, { filter: statsFilter }).on("data", pathHandler); 113 | for await (let path of readdirIterator(root, { filter: statsFilter })) { path = ""; } 114 | } 115 | 116 | export async function testStatsOption() { 117 | let stats: Stats[]; 118 | stats = readdir.sync(root, { stats: true }); 119 | stats = readdirSync(root, { stats: true }); 120 | 121 | readdir.async(root, { stats: true }, statsCallback); 122 | readdirAsync(root, { stats: true }, statsCallback); 123 | 124 | readdir.async(root, { stats: true }).then(statsHandler).catch(errorHandler); 125 | readdirAsync(root, { stats: true }).then(statsHandler).catch(errorHandler); 126 | 127 | readdir.stream(root, { stats: true }).on("data", statsHandler).on("error", errorHandler); 128 | readdirStream(root, { stats: true }).on("data", statsHandler).on("error", errorHandler); 129 | 130 | readdir.stream(root, { stats: true }).pipe(writableStream); 131 | readdirStream(root, { stats: true }).pipe(writableStream); 132 | 133 | for await (let stat of readdir.iterator(root, { stats: true })) { stat.path = ""; } 134 | for await (let stat of readdirIterator(root, { stats: true })) { stat.path = ""; } 135 | } 136 | 137 | export async function testBasePathOption() { 138 | readdirSync(root, { basePath: "/base/path" }); 139 | readdirAsync(root, { basePath: "/base/path" }, pathsCallback); 140 | readdirStream(root, { basePath: "/base/path" }).on("data", pathHandler); 141 | for await (let path of readdirIterator(root, { basePath: "/base/path" })) { path = ""; } 142 | } 143 | 144 | export async function testSepOption() { 145 | readdirSync(root, { sep: "/" }); 146 | readdirAsync(root, { sep: "/" }, pathsCallback); 147 | readdirStream(root, { sep: "/" }).on("data", pathHandler); 148 | for await (let path of readdirIterator(root, { sep: "/" })) { path = ""; } 149 | } 150 | 151 | export async function testFSOption() { 152 | const customFS = { 153 | readdir(dir: string, cb: (err: Error | null, names: string[]) => void): void { 154 | cb(null, [dir]); 155 | } 156 | }; 157 | 158 | readdirSync(root, { fs: customFS }); 159 | readdirAsync(root, { fs: customFS }, pathsCallback); 160 | readdirStream(root, { fs: customFS }).on("data", pathHandler); 161 | for await (let path of readdirIterator(root, { fs: customFS })) { path = ""; } 162 | } 163 | -------------------------------------------------------------------------------- /test/utils/dir.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | 5 | // This fake basePath is used to make sure Windows paths are handled properly. 6 | let windowsBasePath = "C:\\Windows\\Users\\Desktop"; 7 | 8 | let dir = module.exports = { 9 | windowsBasePath, 10 | 11 | path: changePathSeparators, 12 | 13 | shallow: { 14 | data: [ 15 | ".dotdir", 16 | ".dotfile", 17 | "broken-dir-symlink", 18 | "broken-symlink.txt", 19 | "empty", 20 | "empty.txt", 21 | "file-symlink.txt", 22 | "file.json", 23 | "file.txt", 24 | "subdir", 25 | "subdir-symlink", 26 | "subsubdir-symlink", 27 | ], 28 | dirs: [ 29 | ".dotdir", 30 | "empty", 31 | "subdir", 32 | "subdir-symlink", 33 | "subsubdir-symlink", 34 | ], 35 | files: [ 36 | ".dotfile", 37 | "empty.txt", 38 | "file-symlink.txt", 39 | "file.json", 40 | "file.txt", 41 | ], 42 | symlinks: [ 43 | "broken-dir-symlink", 44 | "broken-symlink.txt", 45 | "file-symlink.txt", 46 | "subdir-symlink", 47 | "subsubdir-symlink", 48 | ], 49 | }, 50 | 51 | deep: { 52 | data: [ 53 | ".dotdir", 54 | ".dotfile", 55 | "broken-dir-symlink", 56 | "broken-symlink.txt", 57 | "empty", 58 | "empty.txt", 59 | "file-symlink.txt", 60 | "file.json", 61 | "file.txt", 62 | "subdir", 63 | "subdir-symlink", 64 | "subdir-symlink/.dotdir", 65 | "subdir-symlink/.dotdir/.dotfile", 66 | "subdir-symlink/.dotdir/empty", 67 | "subdir-symlink/file.txt", 68 | "subdir-symlink/subsubdir", 69 | "subdir-symlink/subsubdir/broken-symlink.txt", 70 | "subdir-symlink/subsubdir/empty.txt", 71 | "subdir-symlink/subsubdir/file-symlink.txt", 72 | "subdir-symlink/subsubdir/file.json", 73 | "subdir-symlink/subsubdir/file.txt", 74 | "subdir/.dotdir", 75 | "subdir/.dotdir/.dotfile", 76 | "subdir/.dotdir/empty", 77 | "subdir/file.txt", 78 | "subdir/subsubdir", 79 | "subdir/subsubdir/broken-symlink.txt", 80 | "subdir/subsubdir/empty.txt", 81 | "subdir/subsubdir/file-symlink.txt", 82 | "subdir/subsubdir/file.json", 83 | "subdir/subsubdir/file.txt", 84 | "subsubdir-symlink", 85 | "subsubdir-symlink/broken-symlink.txt", 86 | "subsubdir-symlink/empty.txt", 87 | "subsubdir-symlink/file-symlink.txt", 88 | "subsubdir-symlink/file.json", 89 | "subsubdir-symlink/file.txt", 90 | ], 91 | dirs: [ 92 | ".dotdir", 93 | "empty", 94 | "subdir", 95 | "subdir-symlink", 96 | "subdir-symlink/.dotdir", 97 | "subdir-symlink/.dotdir/empty", 98 | "subdir-symlink/subsubdir", 99 | "subdir/.dotdir", 100 | "subdir/.dotdir/empty", 101 | "subdir/subsubdir", 102 | "subsubdir-symlink", 103 | ], 104 | files: [ 105 | ".dotfile", 106 | "empty.txt", 107 | "file-symlink.txt", 108 | "file.json", 109 | "file.txt", 110 | "subdir-symlink/.dotdir/.dotfile", 111 | "subdir-symlink/file.txt", 112 | "subdir-symlink/subsubdir/empty.txt", 113 | "subdir-symlink/subsubdir/file-symlink.txt", 114 | "subdir-symlink/subsubdir/file.json", 115 | "subdir-symlink/subsubdir/file.txt", 116 | "subdir/.dotdir/.dotfile", 117 | "subdir/file.txt", 118 | "subdir/subsubdir/empty.txt", 119 | "subdir/subsubdir/file-symlink.txt", 120 | "subdir/subsubdir/file.json", 121 | "subdir/subsubdir/file.txt", 122 | "subsubdir-symlink/empty.txt", 123 | "subsubdir-symlink/file-symlink.txt", 124 | "subsubdir-symlink/file.json", 125 | "subsubdir-symlink/file.txt", 126 | ], 127 | symlinks: [ 128 | "broken-dir-symlink", 129 | "broken-symlink.txt", 130 | "file-symlink.txt", 131 | "subdir-symlink", 132 | "subdir-symlink/subsubdir/broken-symlink.txt", 133 | "subdir-symlink/subsubdir/file-symlink.txt", 134 | "subdir/subsubdir/broken-symlink.txt", 135 | "subdir/subsubdir/file-symlink.txt", 136 | "subsubdir-symlink", 137 | "subsubdir-symlink/broken-symlink.txt", 138 | "subsubdir-symlink/file-symlink.txt", 139 | ], 140 | 141 | oneLevel: { 142 | data: [ 143 | ".dotdir", 144 | ".dotfile", 145 | "broken-dir-symlink", 146 | "broken-symlink.txt", 147 | "empty", 148 | "empty.txt", 149 | "file-symlink.txt", 150 | "file.json", 151 | "file.txt", 152 | "subdir", 153 | "subdir-symlink", 154 | "subdir-symlink/.dotdir", 155 | "subdir-symlink/file.txt", 156 | "subdir-symlink/subsubdir", 157 | "subdir/.dotdir", 158 | "subdir/file.txt", 159 | "subdir/subsubdir", 160 | "subsubdir-symlink", 161 | "subsubdir-symlink/broken-symlink.txt", 162 | "subsubdir-symlink/empty.txt", 163 | "subsubdir-symlink/file-symlink.txt", 164 | "subsubdir-symlink/file.json", 165 | "subsubdir-symlink/file.txt", 166 | ], 167 | dirs: [ 168 | ".dotdir", 169 | "empty", 170 | "subdir", 171 | "subdir-symlink", 172 | "subdir-symlink/.dotdir", 173 | "subdir-symlink/subsubdir", 174 | "subdir/.dotdir", 175 | "subdir/subsubdir", 176 | "subsubdir-symlink", 177 | ], 178 | files: [ 179 | ".dotfile", 180 | "empty.txt", 181 | "file-symlink.txt", 182 | "file.json", 183 | "file.txt", 184 | "subdir-symlink/file.txt", 185 | "subdir/file.txt", 186 | "subsubdir-symlink/empty.txt", 187 | "subsubdir-symlink/file-symlink.txt", 188 | "subsubdir-symlink/file.json", 189 | "subsubdir-symlink/file.txt", 190 | ], 191 | symlinks: [ 192 | "broken-dir-symlink", 193 | "broken-symlink.txt", 194 | "file-symlink.txt", 195 | "subdir-symlink", 196 | "subsubdir-symlink", 197 | "subsubdir-symlink/broken-symlink.txt", 198 | "subsubdir-symlink/file-symlink.txt", 199 | ], 200 | }, 201 | }, 202 | 203 | subdir: { 204 | shallow: { 205 | data: [ 206 | ".dotdir", 207 | "file.txt", 208 | "subsubdir", 209 | ], 210 | dirs: [ 211 | ".dotdir", 212 | "subsubdir", 213 | ], 214 | files: [ 215 | "file.txt", 216 | ], 217 | symlinks: [], 218 | }, 219 | 220 | deep: { 221 | data: [ 222 | ".dotdir", 223 | ".dotdir/.dotfile", 224 | ".dotdir/empty", 225 | "file.txt", 226 | "subsubdir", 227 | "subsubdir/broken-symlink.txt", 228 | "subsubdir/empty.txt", 229 | "subsubdir/file-symlink.txt", 230 | "subsubdir/file.json", 231 | "subsubdir/file.txt", 232 | ], 233 | dirs: [ 234 | ".dotdir", 235 | ".dotdir/empty", 236 | "subsubdir", 237 | ], 238 | files: [ 239 | ".dotdir/.dotfile", 240 | "file.txt", 241 | "subsubdir/empty.txt", 242 | "subsubdir/file-symlink.txt", 243 | "subsubdir/file.json", 244 | "subsubdir/file.txt", 245 | ], 246 | symlinks: [ 247 | "subsubdir/broken-symlink.txt", 248 | "subsubdir/file-symlink.txt", 249 | ], 250 | }, 251 | 252 | txt: { 253 | shallow: { 254 | data: [ 255 | "file.txt", 256 | ], 257 | dirs: [], 258 | files: [ 259 | "file.txt", 260 | ], 261 | symlinks: [], 262 | }, 263 | 264 | deep: { 265 | data: [ 266 | "file.txt", 267 | "subsubdir/broken-symlink.txt", 268 | "subsubdir/empty.txt", 269 | "subsubdir/file-symlink.txt", 270 | "subsubdir/file.txt", 271 | ], 272 | dirs: [], 273 | files: [ 274 | "file.txt", 275 | "subsubdir/empty.txt", 276 | "subsubdir/file-symlink.txt", 277 | "subsubdir/file.txt", 278 | ], 279 | symlinks: [ 280 | "subsubdir/broken-symlink.txt", 281 | "subsubdir/file-symlink.txt", 282 | ], 283 | }, 284 | }, 285 | 286 | subsubdir: { 287 | data: [ 288 | "broken-symlink.txt", 289 | "empty.txt", 290 | "file-symlink.txt", 291 | "file.json", 292 | "file.txt", 293 | ], 294 | dirs: [], 295 | files: [ 296 | "empty.txt", 297 | "file-symlink.txt", 298 | "file.json", 299 | "file.txt", 300 | ], 301 | symlinks: [ 302 | "broken-symlink.txt", 303 | "file-symlink.txt", 304 | ], 305 | 306 | txt: { 307 | data: [ 308 | "broken-symlink.txt", 309 | "empty.txt", 310 | "file-symlink.txt", 311 | "file.txt", 312 | ], 313 | dirs: [], 314 | files: [ 315 | "empty.txt", 316 | "file-symlink.txt", 317 | "file.txt", 318 | ], 319 | symlinks: [ 320 | "broken-symlink.txt", 321 | "file-symlink.txt", 322 | ], 323 | 324 | windowsStyle: { 325 | fromDir: { 326 | data: [ 327 | "subdir\\subsubdir\\broken-symlink.txt", 328 | "subdir\\subsubdir\\empty.txt", 329 | "subdir\\subsubdir\\file-symlink.txt", 330 | "subdir\\subsubdir\\file.txt", 331 | ], 332 | dirs: [], 333 | files: [ 334 | "subdir\\subsubdir\\empty.txt", 335 | "subdir\\subsubdir\\file-symlink.txt", 336 | "subdir\\subsubdir\\file.txt", 337 | ], 338 | symlinks: [ 339 | "subdir\\subsubdir\\broken-symlink.txt", 340 | "subdir\\subsubdir\\file-symlink.txt", 341 | ], 342 | }, 343 | 344 | fromRoot: { 345 | data: [ 346 | windowsBasePath + "\\subdir\\subsubdir\\broken-symlink.txt", 347 | windowsBasePath + "\\subdir\\subsubdir\\empty.txt", 348 | windowsBasePath + "\\subdir\\subsubdir\\file-symlink.txt", 349 | windowsBasePath + "\\subdir\\subsubdir\\file.txt", 350 | ], 351 | dirs: [], 352 | files: [ 353 | windowsBasePath + "\\subdir\\subsubdir\\empty.txt", 354 | windowsBasePath + "\\subdir\\subsubdir\\file-symlink.txt", 355 | windowsBasePath + "\\subdir\\subsubdir\\file.txt", 356 | ], 357 | symlinks: [ 358 | windowsBasePath + "\\subdir\\subsubdir\\broken-symlink.txt", 359 | windowsBasePath + "\\subdir\\subsubdir\\file-symlink.txt", 360 | ], 361 | }, 362 | } 363 | }, 364 | } 365 | }, 366 | 367 | txt: { 368 | shallow: { 369 | data: [ 370 | "broken-symlink.txt", 371 | "empty.txt", 372 | "file.txt", 373 | "file-symlink.txt", 374 | ], 375 | dirs: [], 376 | files: [ 377 | "empty.txt", 378 | "file.txt", 379 | "file-symlink.txt", 380 | ], 381 | symlinks: [ 382 | "broken-symlink.txt", 383 | "file-symlink.txt", 384 | ], 385 | }, 386 | 387 | deep: { 388 | data: [ 389 | "broken-symlink.txt", 390 | "empty.txt", 391 | "file-symlink.txt", 392 | "file.txt", 393 | "subdir/file.txt", 394 | "subdir/subsubdir/broken-symlink.txt", 395 | "subdir/subsubdir/empty.txt", 396 | "subdir/subsubdir/file-symlink.txt", 397 | "subdir/subsubdir/file.txt", 398 | "subdir-symlink/file.txt", 399 | "subdir-symlink/subsubdir/broken-symlink.txt", 400 | "subdir-symlink/subsubdir/empty.txt", 401 | "subdir-symlink/subsubdir/file-symlink.txt", 402 | "subdir-symlink/subsubdir/file.txt", 403 | "subsubdir-symlink/broken-symlink.txt", 404 | "subsubdir-symlink/empty.txt", 405 | "subsubdir-symlink/file-symlink.txt", 406 | "subsubdir-symlink/file.txt", 407 | ], 408 | dirs: [], 409 | files: [ 410 | "empty.txt", 411 | "file.txt", 412 | "file-symlink.txt", 413 | "subdir/file.txt", 414 | "subdir/subsubdir/empty.txt", 415 | "subdir/subsubdir/file.txt", 416 | "subdir/subsubdir/file-symlink.txt", 417 | "subdir-symlink/file.txt", 418 | "subdir-symlink/subsubdir/empty.txt", 419 | "subdir-symlink/subsubdir/file.txt", 420 | "subdir-symlink/subsubdir/file-symlink.txt", 421 | "subsubdir-symlink/empty.txt", 422 | "subsubdir-symlink/file.txt", 423 | "subsubdir-symlink/file-symlink.txt", 424 | ], 425 | symlinks: [ 426 | "broken-symlink.txt", 427 | "file-symlink.txt", 428 | "subdir/subsubdir/broken-symlink.txt", 429 | "subdir/subsubdir/file-symlink.txt", 430 | "subdir-symlink/subsubdir/broken-symlink.txt", 431 | "subdir-symlink/subsubdir/file-symlink.txt", 432 | "subsubdir-symlink/broken-symlink.txt", 433 | "subsubdir-symlink/file-symlink.txt", 434 | ], 435 | }, 436 | }, 437 | 438 | empties: { 439 | shallow: { 440 | data: [ 441 | "empty", 442 | "empty.txt", 443 | ], 444 | dirs: [ 445 | "empty", 446 | ], 447 | files: [ 448 | "empty.txt", 449 | ], 450 | symlinks: [], 451 | }, 452 | 453 | deep: { 454 | data: [ 455 | "empty", 456 | "empty.txt", 457 | "subdir/.dotdir/empty", 458 | "subdir/subsubdir/empty.txt", 459 | "subdir-symlink/.dotdir/empty", 460 | "subdir-symlink/subsubdir/empty.txt", 461 | "subsubdir-symlink/empty.txt", 462 | ], 463 | dirs: [ 464 | "empty", 465 | "subdir/.dotdir/empty", 466 | "subdir-symlink/.dotdir/empty", 467 | ], 468 | files: [ 469 | "empty.txt", 470 | "subdir/subsubdir/empty.txt", 471 | "subdir-symlink/subsubdir/empty.txt", 472 | "subsubdir-symlink/empty.txt", 473 | ], 474 | symlinks: [], 475 | }, 476 | }, 477 | 478 | symlinks: { 479 | deep: { 480 | files: [ 481 | "file-symlink.txt", 482 | "subdir/subsubdir/file-symlink.txt", 483 | "subdir-symlink/subsubdir/file-symlink.txt", 484 | "subsubdir-symlink/file-symlink.txt", 485 | ], 486 | dirs: [ 487 | "subdir-symlink", 488 | "subsubdir-symlink", 489 | ], 490 | }, 491 | }, 492 | }; 493 | 494 | // Change all the path separators to "\" on Windows 495 | if (path.sep !== "/") { 496 | changePathSeparatorsRecursive(dir); 497 | } 498 | 499 | function changePathSeparatorsRecursive (obj) { 500 | for (let key of Object.keys(obj)) { 501 | let value = obj[key]; 502 | if (Array.isArray(value)) { 503 | obj[key] = value.map(changePathSeparators); 504 | } 505 | else if (typeof value === "object") { 506 | changePathSeparatorsRecursive(value); 507 | } 508 | } 509 | } 510 | 511 | function changePathSeparators (p) { 512 | return p.replace(/\//g, path.sep); 513 | } 514 | -------------------------------------------------------------------------------- /test/utils/for-each-api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const readdir = require("../../"); 4 | 5 | module.exports = forEachApi; 6 | 7 | /** 8 | * Runs an array of tests tests against each of the readdir-enhanced APIs 9 | */ 10 | function forEachApi (tests) { 11 | describe("Synchronous API", () => { 12 | for (let test of tests) { 13 | testApi(test, "sync", done => { 14 | try { 15 | let data = readdir.sync.apply(null, test.args); 16 | done(null, data); 17 | } 18 | catch (error) { 19 | done(error); 20 | } 21 | }); 22 | } 23 | }); 24 | 25 | describe("Promise API", () => { 26 | for (let test of tests) { 27 | testApi(test, "async", done => { 28 | readdir.async.apply(null, test.args) 29 | .then( 30 | data => { 31 | done(null, data); 32 | }, 33 | error => { 34 | done(error); 35 | } 36 | ); 37 | }); 38 | } 39 | }); 40 | 41 | describe("Callback API", () => { 42 | for (let test of tests) { 43 | testApi(test, "callback", done => { 44 | let args = test.args.length === 0 ? [undefined, done] : [...test.args, done]; 45 | readdir.async.apply(null, args); 46 | }); 47 | } 48 | }); 49 | 50 | describe("Stream/EventEmitter API", () => { 51 | for (let test of tests) { 52 | testApi(test, "stream", done => { 53 | let stream, errors = [], data = [], files = [], dirs = [], symlinks = []; 54 | 55 | try { 56 | stream = readdir.stream.apply(null, test.args); 57 | } 58 | catch (error) { 59 | return done([error], data, files, dirs, symlinks); 60 | } 61 | 62 | stream.on("error", error => { 63 | errors.push(error); 64 | }); 65 | stream.on("file", file => { 66 | files.push(file); 67 | }); 68 | stream.on("directory", dir => { 69 | dirs.push(dir); 70 | }); 71 | stream.on("symlink", symlink => { 72 | symlinks.push(symlink); 73 | }); 74 | stream.on("data", datum => { 75 | data.push(datum); 76 | }); 77 | stream.on("end", () => { 78 | done(errors, data, files, dirs, symlinks); 79 | }); 80 | }); 81 | } 82 | }); 83 | 84 | describe("Iterator API (for await...of)", () => { 85 | for (let test of tests) { 86 | testApi(test, "iterator", async (done) => { 87 | try { 88 | let data = []; 89 | for await (let datum of readdir.iterator.apply(null, test.args)) { 90 | data.push(datum); 91 | } 92 | 93 | done(null, data); 94 | } 95 | catch (error) { 96 | done(error); 97 | } 98 | }); 99 | } 100 | }); 101 | 102 | describe("Iterator API (await iterator.next())", () => { 103 | for (let test of tests) { 104 | testApi(test, "await next", async (done) => { 105 | try { 106 | let iterator = readdir.iterator.apply(null, test.args); 107 | let data = []; 108 | 109 | // eslint-disable-next-line no-constant-condition 110 | while (true) { 111 | let result = await iterator.next(); 112 | 113 | if (result.done) { 114 | break; 115 | } 116 | else { 117 | data.push(result.value); 118 | } 119 | } 120 | 121 | done(null, data); 122 | } 123 | catch (error) { 124 | done(error); 125 | } 126 | }); 127 | } 128 | }); 129 | 130 | describe("Iterator API (iterator.next() without await)", () => { 131 | for (let test of tests) { 132 | testApi(test, "next", async (done) => { 133 | try { 134 | let iterator = readdir.iterator.apply(null, test.args); 135 | let promises = []; 136 | 137 | for (let i = 0; i < 50; i++) { 138 | let promise = iterator.next(); 139 | promises.push(promise); 140 | } 141 | 142 | let results = await Promise.all(promises); 143 | let data = []; 144 | 145 | for (let result of results) { 146 | if (!result.done) { 147 | data.push(result.value); 148 | } 149 | } 150 | 151 | done(null, data); 152 | } 153 | catch (error) { 154 | done(error); 155 | } 156 | }); 157 | } 158 | }); 159 | } 160 | 161 | /** 162 | * Runs a single test against a single readdir-enhanced API. 163 | * 164 | * @param {object} test - An object containing test info, parameters, and assertions 165 | * @param {string} apiName - The name of the API being tested ("sync", "async", "stream", etc.) 166 | * @param {function} api - A function that calls the readdir-enhanced API and returns its results 167 | */ 168 | function testApi (test, apiName, api) { 169 | if (test.only === true || test.only === apiName) { 170 | // Only run this one test (useful for debugging) 171 | it.only(test.it, runTest); 172 | } 173 | else if (test.skip) { 174 | // Skip this test (useful for temporarily disabling a failing test) 175 | it.skip(test.it, runTest); 176 | } 177 | else { 178 | // Run this test normally 179 | it(test.it, runTest); 180 | } 181 | 182 | function runTest (done) { 183 | // Call the readdir-enhanced API and get the results 184 | api((errors, data, files, dirs, symlinks) => { 185 | try { 186 | data && data.sort(); 187 | files && files.sort(); 188 | dirs && dirs.sort(); 189 | symlinks && symlinks.sort(); 190 | 191 | if (apiName === "stream") { 192 | // Perform assertions that are specific to the streaming API 193 | if (test.streamAssert) { 194 | test.streamAssert(errors, data, files, dirs, symlinks); 195 | } 196 | 197 | // Modify the results to match the sync/callback/promise API results 198 | if (errors.length === 0) { 199 | errors = null; 200 | } 201 | else { 202 | errors = errors[0]; 203 | data = undefined; 204 | } 205 | } 206 | 207 | // Perform assertions that are common to ALL of the APIs (including streaming) 208 | test.assert(errors, data); 209 | 210 | done(); 211 | } 212 | catch (e) { 213 | // An assertion failed, so fail the test 214 | done(e); 215 | 216 | console.error( 217 | "==================== ACTUAL RESULTS ====================\n" + 218 | "errors: " + JSON.stringify(errors) + "\n\n" + 219 | "data: " + JSON.stringify(data) + "\n\n" + 220 | "files: " + JSON.stringify(files) + "\n\n" + 221 | "dirs: " + JSON.stringify(dirs) + "\n\n" + 222 | "symlinks: " + JSON.stringify(symlinks) + "\n" + 223 | "========================================================" 224 | ); 225 | } 226 | }); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /test/utils/is-stats.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const { expect } = require("chai"); 5 | 6 | module.exports = isStats; 7 | 8 | /** 9 | * Assets that the given objects is a Readdir Enhanced Stats object. 10 | */ 11 | function isStats (stats) { 12 | expect(stats).to.be.an("object").and.instanceOf(fs.Stats); 13 | expect(stats.atime).to.be.an.instanceOf(Date); 14 | expect(stats.atimeMs).to.be.a("number").above(0); 15 | expect(stats.birthtime).to.be.an.instanceOf(Date); 16 | expect(stats.birthtimeMs).to.be.a("number").above(0); 17 | expect(stats.ctime).to.be.an.instanceOf(Date); 18 | expect(stats.ctimeMs).to.be.a("number").above(0); 19 | expect(stats.depth).to.be.a("number").at.least(0); 20 | expect(stats.dev).to.be.a("number").at.least(0); 21 | expect(stats.gid).to.be.a("number").at.least(0); 22 | expect(stats.ino).to.be.a("number").at.least(0); 23 | expect(stats.mode).to.be.a("number").above(0); 24 | expect(stats.mtime).to.be.an.instanceOf(Date); 25 | expect(stats.mtimeMs).to.be.a("number").above(0); 26 | expect(stats.nlink).to.be.a("number").above(0); 27 | expect(stats.path).to.be.a("string").with.length.above(0); 28 | expect(stats.rdev).to.be.a("number").at.least(0); 29 | expect(stats.size).to.be.a("number").at.least(0); 30 | expect(stats.uid).to.be.a("number").at.least(0); 31 | 32 | return true; 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | 7 | "outDir": "lib", 8 | "sourceMap": true, 9 | "declaration": true, 10 | 11 | "newLine": "LF", 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": true, 15 | "noImplicitThis": true, 16 | "strictBindCallApply": true, 17 | "strictNullChecks": true, 18 | "strictPropertyInitialization": true, 19 | "stripInternal": true, 20 | 21 | "typeRoots": [ 22 | "node_modules/@types", 23 | "src/typings" 24 | ] 25 | }, 26 | "include": [ 27 | "src/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | --------------------------------------------------------------------------------