├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── docs └── index.html ├── draxt-logo.jpg ├── draxt-logo.svg ├── package-lock.json ├── package.json ├── src ├── draxt.js ├── interfaces │ ├── Directory.js │ ├── File.js │ ├── Node.js │ ├── SymbolicLink.js │ └── index.js └── util.js └── test ├── draxt.js └── interfaces ├── Directory.js ├── File.js ├── Node.js └── SymbolicLink.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | globals: { 8 | console: true, 9 | }, 10 | extends: 'eslint:recommended', 11 | rules: { 12 | indent: ['error', 4], 13 | 'no-console': 0, 14 | 'linebreak-style': ['error', 'unix'], 15 | quotes: [1, 'single'], 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .DS_Store 64 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "printWidth": 100, 4 | "tabWidth": 4, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '6' 5 | - '8' 6 | 7 | before_install: 8 | - if [[ `npm -v` == 2.* ]]; then npm install --global npm@3; fi 9 | 10 | after_success: npm run coverage 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ram Hejazi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | draxt.js logo
3 | draxt license 4 | npm-link 5 | draxt coverage status 6 |
7 |
8 | 9 | `draxt` is a utility module for selecting and manipulating filesystem objects in a Node.js environment. 10 | It uses [glob] patterns as its "selector engine". `draxt` also provides several DOM-like interfaces representing filesystem objects which build on promisified APIs for the [`fs`] and [`fs-extra`] modules. 11 | 12 | **Example directory structure:** 13 | 14 | ``` 15 | /app/ 16 | ├── controllers/ 17 | │ └── index.js 18 | ├── public/ 19 | │ ├── script.js 20 | │ └── style.css 21 | └── views/ 22 | └── index.html 23 | ``` 24 | 25 | ```js 26 | const $ = require('draxt'); 27 | 28 | (async () => { 29 | // Select `/app` directory content and create a new `draxt` collection. 30 | const $app = await $('/app/**'); 31 | $app 32 | // Let's filter js files: 33 | .filter((node) => node.extension === 'js') 34 | // Now we have a new `draxt` collection with 2 nodes. 35 | .forEach(async (node, index, allNodes) => { 36 | // `node` is instance of `File` class. Because it's a file! 37 | console.log(node.pathName); 38 | // → '/app/controllers/index.js' for the first node! 39 | 40 | console.log(node instanceof $.File); // → `true` 41 | 42 | // Let's get contents of the node. `file.read` returns a promise object. 43 | const content = await node.read('utf8'); 44 | 45 | // Let's use some synchronous methods! 46 | node.appendSync('\na new line!') 47 | .chmodSync('765') 48 | // move the file into another directory! 49 | .appendToSync('/tmp'); // or `.moveToSync('/tmp')` 50 | 51 | console.log(node.pathName); 52 | // → '/hell/index.js' for the first node in the list! 53 | 54 | // get the parent directory of the node. 55 | // returns a `Directory` instance with the pathName of '/tmp'! 56 | const parentNode = node.parentSync(); // or `await node.parent()` 57 | 58 | // is the directory empty? 59 | console.log(parentNode.isEmptySync()); // → `false` 60 | }); 61 | })(); 62 | ``` 63 | 64 | **Key notes**: 65 | 66 | - `draxt` has only 2 dependencies: [`glob`] and [`fs-extra`] modules. 67 | - `draxt` uses `glob` patterns to select filesystem objects. 68 | - Each item in a `draxt` collection is an instance of a [`File`], [`Directory`], or [`SymbolicLink`] class, which is a subclass of [`Node`]. 69 | - Every asynchronous method has a synchronous version. E.g., [`node.siblingsSync()`] for [`node.siblings()`]. 70 | - `draxt` is a simple constructor function. You can extend/overwrite its methods via its `prototype` property (or its `fn` alias) or by using the [`draxt.extend`] method. 71 | 72 | ```js 73 | const draxt = require('draxt'); 74 | // Add a method (`images`) for filtering image files. 75 | draxt.fn.images = function() { 76 | const imgExtensions = ['jpeg', 'jpg', 'png', 'git', ...]; 77 | return this.filter(node => { 78 | return node.isFile() && imgExtensions.indexOf(node.extension) > -1; 79 | }); 80 | } 81 | ``` 82 | 83 | ## Install 84 | 85 | Installing via [npm]: 86 | 87 | ```bash 88 | $ npm i draxt 89 | ``` 90 | 91 | Via [yarn]: 92 | 93 | ```bash 94 | $ yarn add draxt 95 | ``` 96 | 97 | ## Docs 98 | 99 | - [`draxt` APIs][draxt-doc] 100 | - Interfaces 101 | - [`Node`] 102 | - [`File`] 103 | - [`Directory`] 104 | - [`SymbolicLink`] 105 | 106 | ## Test 107 | 108 | In the past, mock-fs was used for mocking test file system, but since the package is not compatible with 109 | newer versions of node.js, now regular linux cmds like `mkdir` and `echo` are used for creating test files and 110 | folders. The test fs structure are created in `/tmp` directory. That being said, for now, tests only work on Linux! 111 | 112 | ```bash 113 | $ npm run test 114 | ``` 115 | 116 | ## License 117 | 118 | [Licensed under MIT.][license] 119 | 120 | [repo]: https://github.com/ramhejazi/draxt 121 | [logo]: draxt-logo.jpg 122 | [license]: https://github.com/ramhejazi/draxt/blob/master/LICENSE 123 | [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square 124 | [coverall]: https://coveralls.io/github/ramhejazi/draxt 125 | [coverall-badge]: https://img.shields.io/coveralls/github/ramhejazi/draxt.svg?style=flat-square 126 | [npm-link]: https://www.npmjs.com/package/draxt 127 | [npm-badge]: https://img.shields.io/npm/v/draxt.svg?style=flat-square 128 | [travis-link]: https://travis-ci.org/ramhejazi/draxt 129 | [travis-badge]: https://img.shields.io/travis/ramhejazi/draxt.svg?style=flat-square 130 | [deps-status-link]: https://david-dm.org/ramhejazi/draxt 131 | [deps-status-badge]: https://david-dm.org/ramhejazi/draxt.svg?style=flat-square 132 | [npm]: https://docs.npmjs.com/getting-started/what-is-npm 133 | [yarn]: https://yarnpkg.com/en/ 134 | [glob]: https://en.wikipedia.org/wiki/Glob_(programming) 135 | [`fs`]: https://nodejs.org/api/fs.html 136 | [`fs-extra`]: https://github.com/jprichardson/node-fs-extra 137 | [`glob`]: https://github.com/isaacs/node-glob 138 | [Pahlavi language]: https://en.wikipedia.org/wiki/Middle_Persian 139 | [draxt-doc]: https://ramhejazi.github.io/draxt#draxt 140 | [`Node`]: https://ramhejazi.github.io/draxt#interfaces-node 141 | [`File`]: https://ramhejazi.github.io/draxt#interfaces-file 142 | [`Directory`]: https://ramhejazi.github.io/draxt#interfaces-directory 143 | [`SymbolicLink`]: https://ramhejazi.github.io/draxt#interfaces-symboliclink 144 | [`draxt.extend`]: https://ramhejazi.github.io/draxt#draxt-extend 145 | [`node.siblingsSync()`]: https://ramhejazi.github.io/draxt#node-siblings 146 | [`node.siblings()`]: https://ramhejazi.github.io/draxt#node-siblings 147 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /draxt-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramhejazi/draxt/be2b0edd848ba338542f487812a22037b018dd3a/draxt-logo.jpg -------------------------------------------------------------------------------- /draxt-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 33 | 35 | 37 | 38 | draxt 40 | 46 | 52 | 58 | 64 | 70 | 76 | 82 | 92 | 102 | 104 | 105 | 107 | draxt 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draxt", 3 | "version": "1.3.0", 4 | "description": "jQuery/NodeList-like module for file system (nodejs)", 5 | "author": "Ram Hejazi", 6 | "main": "src/draxt.js", 7 | "scripts": { 8 | "test": "nyc --reporter=text --reporter=html ./node_modules/mocha/bin/mocha test/*" 9 | }, 10 | "homepage": "https://github.com/ramhejazi/draxt", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ramhejazi/draxt.git" 15 | }, 16 | "keywords": [ 17 | "fs", 18 | "file", 19 | "file system", 20 | "directory", 21 | "folder", 22 | "read", 23 | "write", 24 | "move", 25 | "mv", 26 | "cp", 27 | "copy", 28 | "jquery", 29 | "node", 30 | "chainable", 31 | "collection", 32 | "utility", 33 | "plugin" 34 | ], 35 | "bugs": { 36 | "url": "https://github.com/ramhejazi/draxt/issues" 37 | }, 38 | "engines": { 39 | "node": "20 || >=22" 40 | }, 41 | "dependencies": { 42 | "fs-extra": "^11.2.0", 43 | "glob": "^11.0.0" 44 | }, 45 | "devDependencies": { 46 | "chai": "^4.3.10", 47 | "mocha": "^9.2.2", 48 | "nyc": "^15.1.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/draxt.js: -------------------------------------------------------------------------------- 1 | /*eslint no-irregular-whitespace: ["error", { "skipComments": true }]*/ 2 | 3 | const {Node} = require('./interfaces'); 4 | const assign = Object.assign; 5 | const {getType} = require('./util'); 6 | const define = Object.defineProperty; 7 | const lengthProps = { 8 | get() { 9 | return this.items.length; 10 | }, 11 | enumerable: true, 12 | configurable: true, 13 | }; 14 | 15 | /** 16 | * `draxt` function is the main method of `draxt` package. The function 17 | * can be called with `new` as a constructor function (not recommended) and 18 | * without `new` as a factory function. `draxt` uses promisified `glob` package as it's 19 | * selector engine. All query results of `glob` package are converted into one of `Node`'s sub-class 20 | * (`File`, `Directory` or `SymbolicLink`) instances by analyzing pathNames' `fs.Stats` object. 21 | * The returned value of `draxt` is a `draxt` collection 22 | * which to some extent works like jQuery collections but unlike jQuery collections it's not an array-like object. 23 | * The collection items are stored as an array property (`.items`). 24 | * @prop {array} items Items of the collection. 25 | * @prop {number} length Lenght of collection's items. 26 | * @param {string|array|node|draxt} [pattern] `pattern` parameter can contain several values: 27 | * - `string` which is passed to `glob` package as `glob` pattern. 28 | * In this case `draxt` returns a `promise` object representing a `draxt` collection/instance. 29 | * - A `Node` or one it's sub-classes (`File`, `Directory` or `SymbolicLink`) instance. 30 | * In this case a `draxt` collection containing the passed `node` is returned. 31 | * - An array of `node` instances. 32 | * - A `draxt` collection to clone (shallow). 33 | * - `undefined` which returns an empty `draxt` collection. 34 | * @param {object|string} [options] Options for `glob` package. The `options` parameter 35 | * can also be a string representing a pathName which will be used as context for the query, 36 | * similar to jQuery's `$(selector, context)` syntax. 37 | * @returns {promise|draxt} 38 | * @example 39 | * // /app 40 | * // ├── controllers/ 41 | * // │   └── index.js 42 | * // ├── public/ 43 | * // │   ├── script.js 44 | * // │   └── style.css 45 | * // └── views/ 46 | * // └── index.njk/ 47 | * 48 | * const draxt = require('draxt'); 49 | * const Directory = draxt.Directory; 50 | * // Initialization with a glob pattern/selector. 51 | * // Which returns a `promise` object! 52 | * draxt('/app/**').then(draxtCollection => { 53 | * // draxtCollection: → 54 | * Draxt { 55 | * length: [Getter], 56 | * items: [ 57 | * Directory { pathName: '/app', ... } 58 | * Directory { pathName: '/app/controllers', ... } 59 | * File { pathName: '/app/controllers/index.js', ... } 60 | * ... 61 | * ] 62 | * }); 63 | * const anEmpyDraxtCollection = draxt(); 64 | * // A `draxt` collection with length of `1` 65 | * // Which has a manually created `Directory` instance. 66 | * const draxtCollection = draxt(new Directory('/app')); 67 | */ 68 | function Draxt(pattern, options = {}) { 69 | // If `this` is not a Draxt instance, create a Draxt instance. 70 | if (this instanceof Draxt !== true) { 71 | return new Draxt(...arguments); 72 | } 73 | // Define dynamic `length` property. 74 | define(this, 'length', lengthProps); 75 | // `items` refers to collection's node. 76 | this.items = []; 77 | 78 | if (getType(pattern) === 'undefined') { 79 | return this; 80 | } 81 | 82 | if (pattern instanceof Node || Array.isArray(pattern)) { 83 | this.add(pattern); 84 | return this; 85 | } 86 | 87 | if (pattern instanceof Draxt) { 88 | this.items = pattern.get().slice(); 89 | return this; 90 | } 91 | 92 | return Node.query(pattern, options).then((items) => { 93 | return this.add(items); 94 | }); 95 | } 96 | 97 | Node.Draxt = Draxt; 98 | Draxt.Node = Node; 99 | Draxt.File = Node.File; 100 | Draxt.Directory = Node.Directory; 101 | Draxt.SymbolicLink = Node.SymbolicLink; 102 | Draxt.fn = Draxt.prototype; 103 | 104 | /** 105 | * Synchronously query the file system by using `glob` package and 106 | * return a new `draxt` collection. 107 | * @param {string} pattern Glob pattern. 108 | * @param {object} [options] Options for `glob` package. 109 | * @returns {draxt} An instance of `draxt`, a.k.a. a _draxt collection_. 110 | */ 111 | Draxt.sync = function () { 112 | const items = Node.querySync(...arguments); 113 | return new Draxt(items); 114 | }; 115 | 116 | /** 117 | * Extend `draxt` by adding methods to it's `prototype`. Basically works like `jQuery.fn.extend`. 118 | * @param {object} methods 119 | */ 120 | Draxt.extend = function (methods) { 121 | assign(Draxt.fn, methods); 122 | }; 123 | 124 | /** 125 | * Add node(s) to current `draxt` collection. 126 | * Pre-exising nodes will not be added to the collection. 127 | * @param {node|array|draxt} items Instance of Node or array of nodes or a `draxt` collection. 128 | * @returns {draxt} An instance of `draxt`. 129 | * @example 130 | * const draxtCollection = draxt(); 131 | * draxtCollection.add(new Node('/pathName')); 132 | * draxtCollection.length // → 1 133 | */ 134 | Draxt.prototype.add = function (items) { 135 | if (items instanceof Draxt) { 136 | items = items.get(); 137 | } 138 | const nodes = Array.isArray(items) ? items.slice() : [items]; 139 | nodes.forEach((node) => { 140 | if (!(node instanceof Node)) { 141 | throw new Error( 142 | 'Invalid value for `items` parameter. `draxt` collection can only have Node instances. ' + 143 | 'The given value is a(n) ' + 144 | getType(node) + 145 | '!' 146 | ); 147 | } 148 | const has = this.has(node); 149 | if (!has) this.items.push(node); 150 | }); 151 | return this; 152 | }; 153 | 154 | /** 155 | * Get one or all nodes from the `draxt` collection. 156 | * With an `index` specified, `.get(index)` retrieves a single node otherwise 157 | * retrives all the nodes (if any). 158 | * @param {number} [index] - Index of node in items collection. 159 | * @returns {array|node|undefined} 160 | */ 161 | Draxt.prototype.get = function (index) { 162 | if (typeof index === 'undefined') { 163 | return this.items; 164 | } 165 | return this.items[index]; 166 | }; 167 | 168 | /** 169 | * Get the first node (if any) from the collection. 170 | * @returns {node|undefined} 171 | */ 172 | Draxt.prototype.first = function () { 173 | return this.items[0]; 174 | }; 175 | 176 | /** 177 | * Get the last node (if any) from the collection. 178 | * @returns {node|undefined} 179 | */ 180 | Draxt.prototype.last = function () { 181 | return this.items[this.items.length - 1]; 182 | }; 183 | 184 | /** 185 | * Does the `draxt` collection has a node with specified pathName? 186 | * Note that `.has()` method doesn't work by checking if collection has a specific 187 | * `Node` instance. It checks whether collection has a node with the specified 188 | * pathName. 189 | * @param {string|node} item A `Node` instance or a `pathName` 190 | * @returns {boolean} 191 | * @example 192 | * // example fs structure 193 | * // └── app 194 | * // ├── public 195 | * // │ ├── script.js 196 | * // │ └── style.css 197 | * const collection = draxtCollection.sync('/app/**'); 198 | * draxtCollection.has('/app/public/script.js') // → true 199 | * draxtCollection.has(new Node('/app/public/script.js')) // → true 200 | */ 201 | Draxt.prototype.has = function (item) { 202 | const pathName = item instanceof Node ? item.pathName : item; 203 | const found = this.items.some((node) => node.pathName === pathName); 204 | return found; 205 | }; 206 | 207 | /** 208 | * Slice the collection and return a new `Draxt` collection. 209 | * Uses `Array.prototype.slice`. 210 | * @param {integer} [begin] Zero-based index at which to begin extraction. 211 | * @param {integer} [end] Zero-based index before which to end extraction. `slice` extracts up to but not including `end`. 212 | * @returns {draxt} A new `draxt` collection which contains sliced items. 213 | */ 214 | Draxt.prototype.slice = function () { 215 | let sItems = this.items.slice(...arguments); 216 | return new Draxt(sItems); 217 | }; 218 | 219 | /** 220 | * Filter the collection's nodes and return a new `draxt` collection. 221 | * Uses `Array.prototype.filter`. 222 | * @param {function} callback A function to execute for each node. 223 | * @param {any} [thisArg] Value to use as `this` (i.e the reference Object) when executing callback. 224 | * @returns {draxt} A new `draxt` collection which contains filtered items. 225 | */ 226 | Draxt.prototype.filter = function () { 227 | let fItems = this.items.filter(...arguments); 228 | return new Draxt(fItems); 229 | }; 230 | 231 | /** 232 | * Iterate over the `draxt` collection and execute a function for each 233 | * node. Uses `Array.prototype.forEach`. 234 | * @chainable 235 | * @param {function} callback A function to execute for each node. 236 | * @param {any} [thisArg] Value to use as `this` (i.e the reference Object) when executing callback. 237 | * @returns {draxt} The current collection. 238 | */ 239 | Draxt.prototype.forEach = function () { 240 | this.items.forEach(...arguments); 241 | return this; 242 | }; 243 | 244 | /** 245 | * Alias for `draxt.forEach`. 246 | */ 247 | Draxt.prototype.each = Draxt.prototype.forEach; 248 | 249 | /** 250 | * Create an array with the results of calling a provided function on every 251 | * node in the `draxt` collection. 252 | * Uses `Array.prototype.map`. 253 | * @param {function} callback A function to execute for each node. 254 | * @param {any} [thisArg] Value to use as `this` (i.e the reference Object) when executing callback. 255 | * @returns {array} 256 | */ 257 | Draxt.prototype.map = function () { 258 | return this.items.map(...arguments); 259 | }; 260 | 261 | /** 262 | * Asynchronous version of `draxt.map`. The results of mapped array is passed 263 | * to `Promise.all` method. 264 | * @param {function} fn A function to execute for each node. 265 | * @param {any} [thisArg] Value to use as `this` (i.e the reference Object) when executing callback. 266 | * @returns {promise} 267 | */ 268 | Draxt.prototype.mapAsync = function () { 269 | return Promise.all(this.items.map(...arguments)); 270 | }; 271 | 272 | /** 273 | * Test whether at least one node in the collection passes the test implemented 274 | * by the provided function. 275 | * Uses `Array.prototype.some`. 276 | * @param {function} fn A function to execute for each node. 277 | * @returns {boolean} 278 | */ 279 | Draxt.prototype.some = function () { 280 | return this.items.some(...arguments); 281 | }; 282 | 283 | /** 284 | * Sort the nodes of collection _in place_ and return the `draxt` collection. 285 | * Uses `Array.prototype.sort`. 286 | * @param {function} callback A function that defines the sort order. 287 | * @returns {draxt} Note that the collection is sorted _in place_, and no copy is made. 288 | */ 289 | Draxt.prototype.sort = function (fn) { 290 | this.items.sort(fn); 291 | return this; 292 | }; 293 | 294 | /** 295 | * Reverse the collection's nodes _in place_. 296 | * The first array element becomes the last, and the last array element becomes the first. 297 | * @returns {draxt} 298 | */ 299 | Draxt.prototype.reverse = function () { 300 | this.items.reverse(); 301 | return this; 302 | }; 303 | 304 | /** 305 | * Filter directory nodes (instances of `Directory` class) and return a new 306 | * `draxt` collection. 307 | * @returns {draxt} 308 | */ 309 | Draxt.prototype.directories = function () { 310 | return this.filter((el) => { 311 | return el.isDirectory(); 312 | }); 313 | }; 314 | 315 | /** 316 | * Filter file nodes (instances of `File` class) and return a new `draxt` collection. 317 | * @returns {draxt} 318 | */ 319 | Draxt.prototype.files = function () { 320 | return this.filter((el) => { 321 | return el.isFile(); 322 | }); 323 | }; 324 | 325 | /** 326 | * Filter symbolic link nodes (instances of `SymbolicLink` class) and return a new `draxt` collection. 327 | * @returns {draxt} 328 | */ 329 | Draxt.prototype.symlinks = function () { 330 | return this.filter((el) => { 331 | return el.isSymbolicLink(); 332 | }); 333 | }; 334 | 335 | /** 336 | * Empty the `draxt` collection. This method doesn't affect file system! 337 | * @returns {draxt} 338 | */ 339 | Draxt.prototype.empty = function () { 340 | this.items = []; 341 | return this; 342 | }; 343 | 344 | /** 345 | * Remove node(s) from the current `draxt` collection by using `.pathName`s as the criterion. 346 | * @chainable 347 | * @param {draxt|node|array} node Accepts various paramters. 348 | * @return {draxt} 349 | */ 350 | Draxt.prototype.drop = function (node) { 351 | let nodes; 352 | if (node instanceof Node) { 353 | nodes = [node]; 354 | } else if (node instanceof Draxt) { 355 | nodes = node.get(); 356 | } else if (getType(node) === 'string') { 357 | nodes = [node]; 358 | } else if (Array.isArray(node)) { 359 | nodes = node; 360 | } else { 361 | throw new Error('Invalid paramter passed to `.drop()` method'); 362 | } 363 | const pathNames = nodes.map((item) => { 364 | if (typeof item === 'string') { 365 | return item; 366 | } 367 | return item.pathName; 368 | }); 369 | this.items = this.items.filter((node) => { 370 | return pathNames.indexOf(node.pathName) === -1; 371 | }); 372 | return this; 373 | }; 374 | 375 | module.exports = Draxt; 376 | -------------------------------------------------------------------------------- /src/interfaces/Directory.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'), 2 | {getType} = require('../util'); 3 | 4 | const nodeProps = { 5 | nodeName: {value: 'Directory', writable: false, configurable: false, enumerable: true}, 6 | NODE_TYPE: {value: 1, writable: false, configurable: false, enumerable: true}, 7 | }; 8 | 9 | /** 10 | * `Directory` class which extends the `Node` class is an interface representing pathNames 11 | * that their `fs.Stats`'s `.isDirectory()` method returns `true`. 12 | * @prop {string} nodeName Name of the node: `'Directory'`. 13 | * @prop {number} NODE_TYPE Code number for the node: `1`. 14 | */ 15 | class Directory extends Node { 16 | /** 17 | * Construct a new node 18 | * @param {string} pathName Absolute pathName of the node 19 | * @param {object} [stats] Instance of `fs.Stats` for the node 20 | */ 21 | constructor(pathName, stats) { 22 | super(pathName, stats); 23 | Object.defineProperties(this, nodeProps); 24 | } 25 | 26 | /** 27 | * Append/move passed directories into this directory node. 28 | * Uses `node.moveTo` which uses `fs-extra.move`. 29 | * @param {draxt|node|string|array} nodes Accepts various parameters: 30 | * - `draxt` collection. 31 | * - a node instance. 32 | * - pathNames of a file or directory. 33 | * - array of node instances. 34 | * - array of absolute pathNames of files/directories. 35 | * - a mixed array of nodePaths and absolute pathNames of files/directories. 36 | * @param {object} [options] Options for `fs-extra.move`. 37 | * @returns {promise} Promise representing array of moved nodes. 38 | */ 39 | append(nodes, options) { 40 | nodes = this.constructor.__normalizeAppendNodes(nodes); 41 | const mvPromices = nodes.map((node) => { 42 | node = node instanceof Node ? node : new Node(node); 43 | return node.moveTo(this, options); 44 | }); 45 | return Promise.all(mvPromices).then(() => this); 46 | } 47 | 48 | /** 49 | * Synchronous version of `directory.append`. 50 | * @chainable 51 | * @param {draxt|array|string} nodes 52 | * @param {object} [options] Options for `fs-extra.move`. 53 | * @returns {node} The `directory` node. 54 | */ 55 | appendSync(nodes, options) { 56 | nodes = this.constructor.__normalizeAppendNodes(nodes); 57 | nodes.forEach((node) => { 58 | node = node instanceof Node ? node : new Node(node); 59 | return node.moveToSync(this, options); 60 | }); 61 | return this; 62 | } 63 | 64 | /** 65 | * Asynchronously select children of the directory by using `glob` package and 66 | * return a `draxt` collection. 67 | * @param {string} [pattern='*'] Glob pattern relative to the directory. The pattern 68 | * is used against `baseName` of directory child nodes. 69 | * @param {object} [options] Options for `glob` package. 70 | * @returns {promise} 71 | */ 72 | children() { 73 | const {rawQuery, toNodes, Draxt} = Node; 74 | let [options, filterFn] = this.__normalizeRelativeGlobOptions(...arguments); 75 | return rawQuery('*', options).then((items) => { 76 | if (filterFn) { 77 | items = items.filter(filterFn); 78 | } 79 | return toNodes(items).then(Draxt); 80 | }); 81 | } 82 | 83 | /** 84 | * Synchronous version of `directory.children`. 85 | * @param {string} [selector] Optional selector 86 | * @param {object} [options] Options for glob package 87 | * @returns {draxt} 88 | */ 89 | childrenSync() { 90 | const {rawQuerySync, toNodesSync, Draxt} = Node; 91 | let [_options, filterFn] = this.__normalizeRelativeGlobOptions(...arguments); 92 | let items = rawQuerySync('*', _options); 93 | if (filterFn) { 94 | items = items.filter(filterFn); 95 | } 96 | return new Draxt(toNodesSync(items)); 97 | } 98 | 99 | /** 100 | * Ensures that a directory is empty. Deletes directory contents if the directory 101 | * is not empty. If the directory does not exist, it is created. 102 | * The directory itself is not deleted. 103 | * Wrapper for `fs-extra.emptyDir`. 104 | * @returns {promise} 105 | */ 106 | empty() { 107 | return this.fs.emptyDir(this.pathName); 108 | } 109 | 110 | /** 111 | * Synchronous version of `directory.empty` method. 112 | * Wrapper for `fs-extra.emptyDirSync`. 113 | * @returns {node} 114 | */ 115 | emptySync() { 116 | this.fs.emptyDirSync(this.pathName); 117 | return this; 118 | } 119 | 120 | /** 121 | * Asynchronously ensure directory exists. 122 | * Wrapper for `fs-extra.ensureDir`. 123 | *@returns {promise} 124 | */ 125 | ensure() { 126 | return this.fs.ensureDir(this.pathName); 127 | } 128 | 129 | /** 130 | * Synchronously ensure directory exists. 131 | * Wrapper for `fs-extra.ensureDirSync`. 132 | * @returns {node} 133 | */ 134 | ensureSync() { 135 | this.fs.ensureDirSync(this.pathName); 136 | return this; 137 | } 138 | 139 | /** 140 | * Is directory empty? 141 | * @returns {promise} 142 | */ 143 | isEmpty() { 144 | return this.readdir().then((files) => { 145 | return files.length === 0; 146 | }); 147 | } 148 | 149 | /** 150 | * Synchronous version of `directory.isEmpty` method. 151 | * @returns {boolean} 152 | */ 153 | isEmptySync() { 154 | return this.readdirSync().length === 0; 155 | } 156 | 157 | /** 158 | * Find matching decendants of the directory node. 159 | * Uses `glob` package. 160 | * @param {string} pattern Glob pattern. 161 | * @param {object} options Options for `glob` package. 162 | * @returns {Promise} 163 | */ 164 | find(selector, options) { 165 | const {Draxt} = Node; 166 | options = Node.__normalizeGlobOptions(options); 167 | options.cwd = this.pathName; 168 | return Node.query(selector, options).then((items) => { 169 | return new Draxt(items); 170 | }); 171 | } 172 | 173 | /** 174 | * Synchronous version of `directory.find` method. 175 | * @param {string} selector 176 | * @param {object} options Options for `glob` package. 177 | * @returns {draxt} 178 | */ 179 | findSync(selector, options) { 180 | const {Draxt} = Node; 181 | options = Node.__normalizeGlobOptions(options); 182 | options.cwd = this.pathName; 183 | const items = Node.querySync(selector, options); 184 | return new Draxt(items); 185 | } 186 | 187 | /** 188 | * Wrapper for promisified `fs.readdir`. 189 | * @param {string|object} options 190 | * @returns {promise} 191 | */ 192 | readdir() { 193 | return this.fs.readdir(this.pathName, ...arguments); 194 | } 195 | 196 | /** 197 | * Wrapper for `fs.readdirSync`. 198 | * @param {string|object} options 199 | * @returns {array} 200 | */ 201 | readdirSync() { 202 | return this.fs.readdirSync(this.pathName, ...arguments); 203 | } 204 | 205 | /** 206 | * Alias for `directory.readdir` method. 207 | * @param {string|object} options 208 | * @returns {promise} 209 | */ 210 | read() { 211 | return this.readdir(...arguments); 212 | } 213 | 214 | /** 215 | * Alias for `directory.readdirSync` method. 216 | * @param {string|object} options 217 | * @returns {array} 218 | */ 219 | readSync() { 220 | return this.readdirSync(...arguments); 221 | } 222 | 223 | /** 224 | * Wrapper for promisified `fs.rmdir`. 225 | * Deletes the directory, which must be empty. 226 | * @returns {promise} 227 | */ 228 | rmdir() { 229 | return this.fs.rmdir(this.pathName); 230 | } 231 | 232 | /** 233 | * Wrapper for `fs.rmdirSync`. 234 | * Deletes the directory, which must be empty. 235 | * @chainable 236 | * @returns {node} 237 | */ 238 | rmdirSync() { 239 | this.fs.rmdirSync(this.pathName); 240 | return this; 241 | } 242 | 243 | static __normalizeAppendNodes(nodes) { 244 | const {Draxt} = Node; 245 | if (nodes instanceof Draxt) { 246 | nodes = nodes.get(); 247 | } else if (nodes instanceof Node) { 248 | nodes = [nodes]; 249 | } else if (getType(nodes) === 'string') { 250 | nodes = [nodes]; 251 | } else if (getType(nodes) !== 'array') { 252 | throw new Error(`Invalid parameter for \`nodes\` parameter: ${nodes}`); 253 | } 254 | return nodes; 255 | } 256 | } 257 | 258 | module.exports = Node.Directory = Directory; 259 | -------------------------------------------------------------------------------- /src/interfaces/File.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | const nodeProps = { 4 | nodeName: {value: 'File', writable: false, configurable: false, enumerable: true}, 5 | NODE_TYPE: {value: 2, writable: false, configurable: false}, 6 | }; 7 | 8 | /** 9 | * `File` class which extends the `Node` class is an interface representing pathNames 10 | * that their `fs.Stats`'s `.isFile()` method returns `true`. 11 | * @prop {string} nodeName Name of the node: `'File'`. 12 | * @prop {number} NODE_TYPE Code number for the node: `2`. 13 | */ 14 | class File extends Node { 15 | /** 16 | * Construct a new file. 17 | * @param {string} pathName Absolute pathName of the node 18 | * @param {object} [stats] Instance of `fs.Stats` for the node 19 | */ 20 | constructor(pathName, stats) { 21 | super(pathName, stats); 22 | Object.defineProperties(this, nodeProps); 23 | } 24 | 25 | /** 26 | * Ensure the file node exists on file system. 27 | * Wrapper for `fs-extra.ensureFile`. 28 | * @returns {promise} 29 | */ 30 | ensure() { 31 | return this.fs.ensureFile(this.pathName); 32 | } 33 | 34 | /** 35 | * Ensure the file node exists on file system synchronously. 36 | * Wrapper for `fs.ensureFileSync`. 37 | * @returns {node} 38 | */ 39 | ensureSync() { 40 | this.fs.ensureFileSync(this.pathName); 41 | return this; 42 | } 43 | 44 | /** 45 | * Asynchronously append data to a file, creating the file if it does not yet exist. `data` can be a string or a Buffer. 46 | * Wrapper for `fs.appendFile`. 47 | * @returns {promise} 48 | */ 49 | append() { 50 | return this.fs.appendFile(this.pathName, ...arguments); 51 | } 52 | 53 | /** 54 | * Wrapper for `fs.appendFileSync`. 55 | * @returns {node} 56 | */ 57 | appendSync() { 58 | this.fs.appendFileSync(this.pathName, ...arguments); 59 | return this; 60 | } 61 | 62 | /** 63 | * Promisified wrapper for `fs.readFile`. 64 | * @returns {promise} Promise object representing contents of the file. 65 | */ 66 | read() { 67 | return this.fs.readFile(this.pathName, ...arguments); 68 | } 69 | 70 | /** 71 | * Wrapper for `fs.readFileSync`. 72 | * @returns {any} 73 | */ 74 | readSync() { 75 | return this.fs.readFileSync(this.pathName, ...arguments); 76 | } 77 | 78 | /** 79 | * Promisified wrapper for `fs.truncate` 80 | * @returns {promise} 81 | */ 82 | truncate() { 83 | return this.fs.truncate(this.pathName, ...arguments); 84 | } 85 | 86 | /** 87 | * Wrapper for `fs.truncateSync`. 88 | * @returns {node} 89 | */ 90 | truncateSync() { 91 | this.fs.truncateSync(this.pathName, ...arguments); 92 | return this; 93 | } 94 | 95 | /** 96 | * Promisified `fs.writeFile` 97 | * @returns {promise} 98 | */ 99 | write() { 100 | return this.fs.writeFile(this.pathName, ...arguments); 101 | } 102 | 103 | /** 104 | * Wrapper for `fs.writeFileSync`. 105 | * @chainable 106 | * @returns {node} 107 | */ 108 | writeSync() { 109 | this.fs.writeFileSync(this.pathName, ...arguments); 110 | return this; 111 | } 112 | } 113 | 114 | module.exports = Node.File = File; 115 | -------------------------------------------------------------------------------- /src/interfaces/Node.js: -------------------------------------------------------------------------------- 1 | const path = require('path'), 2 | {glob, globSync} = require('glob'), 3 | fs = require('fs-extra'), 4 | minimatch = require('minimatch'), 5 | {getType} = require('../util'), 6 | assign = Object.assign; 7 | // Default properies which are used for detecting node type. 8 | const defaultNodeProps = { 9 | nodeName: {value: 'Node', writable: false, configurable: true, enumerable: true}, 10 | NODE_TYPE: {value: 0, writable: false, configurable: true, enumerable: true}, 11 | }; 12 | 13 | /** 14 | * Node class is an interface that other classes representing 15 | * file system's nodes (like `File`, `Directory`, `SymbolicLink`, ...) inherit. 16 | * @prop {string} [nodeName='Node'] Default: `'Node'`. The name of constructor of the current node. 17 | * @prop {number} [NODE_TYPE=0] Default: `0`. Code number for the node. 18 | * @prop {string} pathName Absolute pathName of the node. Example: `'/app/readme.md'`. 19 | * @prop {string} baseName baseName of the node. Example: `'readme.md'`. 20 | * @prop {string} name Name of the node without the possible extension. Example `'readme'`. 21 | * @prop {string|undefined} extension Extension of the node without `.`. Example: `'js'`. 22 | * @prop {string} parentPath pathName of the parent directory of the node. 23 | * @prop {string} rootPath Root path of the file system. 24 | * @prop {object|undefined} _stats Cached instance of `fs.Stats` for the node. 25 | * @prop {object} fs Refers to `fs-extra` package. 26 | * @prop {object} glob Refers to `glob` package. 27 | */ 28 | class Node { 29 | /** 30 | * Construct a new node 31 | * @param {string} pathName Absolute pathName of the node 32 | * @param {object} [stats] Instance of `fs.Stats` for the node 33 | */ 34 | constructor(pathName, stats) { 35 | this._stats = stats; 36 | Object.defineProperties(this, defaultNodeProps); 37 | this._setPathParams(pathName); 38 | } 39 | 40 | /** 41 | * Parse the node's pathName by using `path.parse()` method 42 | * and set the corresponsing node's properties. 43 | * @returns {undefined} 44 | */ 45 | _setPathParams(nodePathname) { 46 | const {root, dir, name, ext, base} = path.parse(nodePathname); 47 | this.pathName = nodePathname; 48 | this.baseName = base; 49 | this.name = name; 50 | this.extension = ext.slice(1); 51 | this.rootPath = root; 52 | this.parentPath = dir; 53 | } 54 | 55 | /** 56 | * Get the node's pathName. 57 | * @returns {string} 58 | */ 59 | getPathName() { 60 | return this.pathName; 61 | } 62 | 63 | /** 64 | * Get the node's baseName. 65 | * @returns {string} 66 | */ 67 | getBaseName() { 68 | return this.baseName; 69 | } 70 | 71 | /** 72 | * Get the node's extension. 73 | * @returns {string} 74 | */ 75 | getExtension() { 76 | return this.extension; 77 | } 78 | 79 | /** 80 | * Get name of the node. 81 | * For `File` nodes the `name` property is the name of file without possible extension. 82 | * @returns {string} 83 | */ 84 | getName() { 85 | return this.name; 86 | } 87 | 88 | /** 89 | * Get the node's parent directory pathName. 90 | * @returns {string} 91 | */ 92 | getParentPath() { 93 | return this.parentPath; 94 | } 95 | 96 | /** 97 | * Get cached `fs.Stats` instance for the node. Returns `undefined` when there 98 | * is no cached stats for the node. This happens only when the node is created 99 | * manually by user without passing a stats object. 100 | * @returns {object|undefined} 101 | */ 102 | getCachedStats() { 103 | return this._stats; 104 | } 105 | 106 | /** 107 | * Get a stat property's value from cached `fs.Stats` for the node. 108 | * The method returns `undefined` when there is no cached stats. 109 | * @param {string} propName 110 | * @example 111 | * // Get `blksize` property of fs.Stats instance cached for the node. 112 | * const node_ctime = node.getStatProp('blksize'); 113 | * @returns {any} 114 | */ 115 | getStatProp(propName) { 116 | return (this._stats || {})[propName]; 117 | } 118 | 119 | /** 120 | * Get "access time" of the node. Returns `atime` property of the cached stats. 121 | * @returns {date} 122 | */ 123 | getAccessTime() { 124 | return this.getStatProp('atime'); 125 | } 126 | 127 | /** 128 | * Get "modified time" of the node. Returns `mtime` property of the cached stats. 129 | * @returns {date} 130 | */ 131 | getModifiedTime() { 132 | return this.getStatProp('mtime'); 133 | } 134 | 135 | /** 136 | * Get "birthday time" of the node. Returns `birthtime` property of the cached stats. 137 | * @returns {date} 138 | */ 139 | getBirthTime() { 140 | return this.getStatProp('birthtime'); 141 | } 142 | 143 | /** 144 | * Get "change time" of the node. Returns `ctime` property of the cached stats. 145 | * @returns {date} 146 | */ 147 | getChangeTime() { 148 | return this.getStatProp('ctime'); 149 | } 150 | 151 | /** 152 | * Get size of the node. 153 | * Size is simply the `size` property of the cached `fs.Stats` instance. 154 | * @returns {number} 155 | */ 156 | getSize() { 157 | return this.getStatProp('size'); 158 | } 159 | 160 | /** 161 | * Is the node a directory? 162 | * @returns {boolean} 163 | */ 164 | isDirectory() { 165 | return this.nodeName === 'Directory'; 166 | } 167 | 168 | /** 169 | * Is the node a file? 170 | * @returns {boolean} 171 | */ 172 | isFile() { 173 | return this.nodeName === 'File'; 174 | } 175 | 176 | /** 177 | * Is the node a symbolic link? 178 | * @returns {boolean} 179 | */ 180 | isSymbolicLink() { 181 | return this.nodeName === 'SymbolicLink'; 182 | } 183 | 184 | /** 185 | * Is the node a dot file? i.e. does the node's name begin with dot character. 186 | * @returns {boolean} 187 | */ 188 | isDotFile() { 189 | return this.baseName[0] === '.'; 190 | } 191 | 192 | /** 193 | * Asynchronously renew stats of the node. Uses `fs.lstat`. 194 | * @returns {promise} A fresh `fs.Stats` instance for the node. 195 | */ 196 | renewStats() { 197 | return this.fs.lstat(this.pathName).then((stats) => { 198 | this._stats = stats; 199 | return stats; 200 | }); 201 | } 202 | 203 | /** 204 | * Synchronously renew stats of the node. Uses `fs.lstatSync`. 205 | * @chainable 206 | * @returns {node} 207 | */ 208 | renewStatsSync() { 209 | const stat = this.lstatSync(this.pathName); 210 | this._stats = stat; 211 | return this; 212 | } 213 | 214 | /** 215 | * Get octal representation of the node's permissions. 216 | * @returns {string} 217 | * @example 218 | * node.getOctalPermissions() // → "755" 219 | */ 220 | getOctalPermissions() { 221 | return (this._stats.mode & 0o777).toString(8); 222 | } 223 | 224 | /** 225 | * Get permissions of the node for owner, group and others by converting `mode` 226 | * property of cached stats into an object. 227 | * @example 228 | * node.getPermissions() 229 | * // → 230 | * { 231 | * read: { owner: true, group: true, others: false }, 232 | * write: { owner: true, group: true, others: false }, 233 | * execute: { owner: true, group: true, others: false } 234 | * } 235 | * @returns {object} 236 | */ 237 | getPermissions() { 238 | if (getType(this._stats) !== 'object') { 239 | throw new Error( 240 | 'No valid cached stats ofr this node. Run `.renewStats()` before calling this function!' 241 | ); 242 | } 243 | // Logic taken from npm `mode-to-permissions` module 244 | const mode = this._stats.mode, 245 | owner = mode >> 6, 246 | group = (mode << 3) >> 6, 247 | others = (mode << 6) >> 6; 248 | 249 | return { 250 | read: { 251 | owner: !!(owner & 4), 252 | group: !!(group & 4), 253 | others: !!(others & 4), 254 | }, 255 | write: { 256 | owner: !!(owner & 2), 257 | group: !!(group & 2), 258 | others: !!(others & 2), 259 | }, 260 | execute: { 261 | owner: !!(owner & 1), 262 | group: !!(group & 1), 263 | others: !!(others & 1), 264 | }, 265 | }; 266 | } 267 | 268 | /** 269 | * Asynchronously tests a user's permissions for the file or directory. 270 | * Wrapper for promisified `fs.access`. 271 | * @param {integer} [mode=fs.constants.F_OK] 272 | * @returns {promise} 273 | * @example 274 | * // Check if the node is readable. 275 | * node.access(node.fs.constants.R_OK).then(() => { 276 | * // node is readable 277 | * }).catch(e => { 278 | * // node is not readable 279 | * }); 280 | */ 281 | access(mode) { 282 | return this.fs.access(this.pathName, mode); 283 | } 284 | 285 | /** 286 | * Wrapper for `fs.accessSync`. 287 | * @chainable 288 | * @param {integer} [mode=fs.constants.F_OK] 289 | * @returns {node} this 290 | */ 291 | accessSync(mode) { 292 | this.fs.accessSync(this.pathName, mode); 293 | return this; 294 | } 295 | 296 | /** 297 | * Wrapper for promisified `fs.chmod`. 298 | * @param {integer} mode 299 | * @returns {promise} 300 | */ 301 | chmod(mode) { 302 | return this.fs.chmod(this.pathName, mode); 303 | } 304 | 305 | /** 306 | * Wrapper for `fs.chmodSync`. 307 | * @chainable 308 | * @param {integer} mode 309 | * @returns {node} this 310 | */ 311 | chmodSync(mode) { 312 | this.fs.chmodSync(this.pathName, mode); 313 | return this; 314 | } 315 | 316 | /** 317 | * Wrapper for promisified `fs.lchmod`. 318 | * @param {integer} mode 319 | * @returns {promise} 320 | */ 321 | lchmod(mode) { 322 | return this.fs.lchmod(this.pathName, mode); 323 | } 324 | 325 | /** 326 | * Wrapper for `fs.lchmodSync`. 327 | * @chainable 328 | * @param {integer} mode 329 | * @returns {node} 330 | */ 331 | lchmodSync(mode) { 332 | this.fs.lchmodSync(this.pathName, mode); 333 | return this; 334 | } 335 | 336 | /** 337 | * Wrapper for promisified `fs.chown`. 338 | * @param {integer} uid The user id 339 | * @param {integer} gid The group id 340 | * @returns {promise} 341 | */ 342 | chown(uid, gid) { 343 | return this.fs.chown(this.pathName, uid, gid); 344 | } 345 | 346 | /** 347 | * Wrapper for `fs.chownSync`. 348 | * @chainable 349 | * @param {integer} uid The user id 350 | * @param {integer} gid The group id 351 | * @returns {node} The file node 352 | */ 353 | chownSync(uid, gid) { 354 | this.fs.chownSync(this.pathName, uid, gid); 355 | return this; 356 | } 357 | 358 | /** 359 | * Wrapper for promisified `fs.lchown`. 360 | * @param {integer} uid The user id 361 | * @param {integer} gid The group id 362 | * @returns {promise} 363 | */ 364 | lchown(uid, gid) { 365 | return this.fs.lchown(this.pathName, uid, gid); 366 | } 367 | 368 | /** 369 | * Wrapper for `fs.lchownSync`. 370 | * @chainable 371 | * @param {integer} uid The user id 372 | * @param {integer} gid The group id 373 | * @returns {node} The file node 374 | */ 375 | lchownSync(uid, gid) { 376 | this.fs.lchownSync(this.pathName, uid, gid); 377 | return this; 378 | } 379 | 380 | /** 381 | * Does node exist on file system? 382 | * Uses `fs.access` instead of the deprecated `fs.exists` method. 383 | * @returns {promise} 384 | */ 385 | exists() { 386 | return this.access(this.fs.constants.F_OK) 387 | .then(() => { 388 | return true; 389 | }) 390 | .catch(() => false); 391 | } 392 | 393 | /** 394 | * Does node exist on file system? 395 | * Wrapper for `fs.existsSync`. 396 | * @returns {boolean} 397 | */ 398 | existsSync() { 399 | return this.fs.existsSync(this.pathName); 400 | } 401 | 402 | /** 403 | * Wrapper for promisified `fs.stat`. 404 | * @returns {promise} Promise representing instance of `fs.Stats` for the node. 405 | */ 406 | stat() { 407 | return this.fs.stat(this.pathName, ...arguments); 408 | } 409 | 410 | /** 411 | * Wrapper for `fs.statSync`. 412 | * @returns {object} Instance of `fs.Stats` for the node. 413 | */ 414 | statSync() { 415 | return this.fs.statSync(this.pathName, ...arguments); 416 | } 417 | 418 | /** 419 | * Wrapper for promisified `fs.lstat`. 420 | * @returns {promise} Promise representing instance of `fs.Stats` for the node. 421 | */ 422 | lstat() { 423 | return this.fs.lstat(this.pathName, ...arguments); 424 | } 425 | 426 | /** 427 | * Wrapper for `fs.lstatSync`. 428 | * @returns {object} Instance of `fs.Stats` for the node. 429 | */ 430 | lstatSync() { 431 | return this.fs.lstatSync(this.pathName, ...arguments); 432 | } 433 | 434 | /** 435 | * Wrapper for promisified `fs.link`. 436 | * @param {string|Buffer|URL} newPath 437 | * @returns {Promise} 438 | */ 439 | link(newPath) { 440 | return this.fs.link(this.pathName, newPath); 441 | } 442 | 443 | /** 444 | * Wrapper for `fs.linkSync`. 445 | * @chainable 446 | * @param {string|buffer|URL} newPath 447 | * @returns {node} 448 | */ 449 | linkSync(newPath) { 450 | this.fs.linkSync(this.pathName, newPath); 451 | return this; 452 | } 453 | 454 | /** 455 | * Asynchronously rename node to the pathname provided as newPath. 456 | * In the case that `newPath` already exists, it will be overwritten. 457 | * Wrapper for promisified `fs.rename`. 458 | * @param newPath {string|Buffer|URL} 459 | * @returns {promise} 460 | */ 461 | rename(newPath) { 462 | return this.fs.rename(this.pathName, ...arguments).then(() => { 463 | this._setPathParams(newPath); 464 | }); 465 | } 466 | 467 | /** 468 | * Wrapper for `fs.renameSync`. 469 | * @chainable 470 | * @param newPath {string|Buffer|URL} 471 | * @returns {node} 472 | */ 473 | renameSync(newPath) { 474 | this.fs.renameSync(this.pathName, newPath); 475 | this._setPathParams(newPath); 476 | return this; 477 | } 478 | 479 | /** 480 | * Wrapper for promisified `fs.utimes`. 481 | * @param atime {number|string|Date} 482 | * @param mtime {number|string|Date} 483 | * @returns {promise} 484 | */ 485 | utimes() { 486 | return this.fs.utimes(this.pathName, ...arguments); 487 | } 488 | 489 | /** 490 | * Wrapper for `fs.utimesSync`. 491 | * @chainable 492 | * @param atime {number|string|Date} 493 | * @param mtime {number|string|Date} 494 | * @returns {node} 495 | */ 496 | utimesSync() { 497 | this.fs.utimesSync(this.pathName, ...arguments); 498 | return this; 499 | } 500 | 501 | /** 502 | * Asynchronously copy the node. `Directory` instances can have contents. Like `cp -r`. 503 | * When directory doesn't exist, it's created! 504 | * Wrapper for `fs-extra.copy`. 505 | * @param {string} destPath Destination path. 506 | * @param {object} options Options for `fs-extra.copy`. 507 | * @returns {promise} 508 | * @example 509 | * // creating a `File` instance. `File` class extends the `Node` class! 510 | * const file = new File('/app/resources/style.css'); 511 | * file.copy('/app/backup/backup_style.css').then(() => { 512 | * // file has been copied successfully! 513 | * }).catch(e => { 514 | * // There was an error! 515 | * }); 516 | */ 517 | copy() { 518 | return this.fs.copy(this.pathName, ...arguments); 519 | } 520 | 521 | /** 522 | * Wrapper for `fs-extra.copySync`. 523 | * @chainable 524 | * @param {string} destPath Destination path. 525 | * @param {object} options Options for `fs-extra.copySync`. 526 | * @returns {node} 527 | */ 528 | copySync() { 529 | this.fs.copySync(this.pathName, ...arguments); 530 | return this; 531 | } 532 | 533 | /** 534 | * Move node to another location. `baseName` property of the node is joined 535 | * with `targetDir` param for resolving the final path for the node. 536 | * The method on success updates path-related properties of the node, 537 | * but node's cached stats (if any) is not refreshed! 538 | * For updating node's stats, user can call `node.renewStats()` or `node.renewStatsSync()` 539 | * methods after moving the node. 540 | * Uses `fs-extra.move`. 541 | * @param {object|string} targetDir `Directory` instance or absolute path of the target directory. 542 | * @param {object} options Options for `fs-extra.move`. 543 | * @return {promise} 544 | * @example 545 | * const node = new File('/app/resources/style.css'); 546 | * const dir = new Directory('/app/target_dir'); 547 | * node.moveTo(dir || '/app/target_dir').then(() => { 548 | * // node has been moved into '/app/target_dir' directory! 549 | * node.getPathName(); // → '/app/target_dir/style.css' 550 | * }); 551 | */ 552 | moveTo(targetDir, options) { 553 | const targetPath = this.__resolvePath(targetDir); 554 | if (typeof options === 'function') { 555 | throw new Error('`node.moveTo` doesn not accept a callback function!'); 556 | } 557 | // Fix `fs.move` broken handling of optional parameters! 558 | const args = [this.pathName, targetPath]; 559 | if (options) { 560 | args.push(options); 561 | } 562 | return this.fs.move(...args).then(() => { 563 | this._setPathParams(targetPath); 564 | return this; 565 | }); 566 | } 567 | 568 | /** 569 | * Synchronous version of `node.moveTo`. 570 | * @chainable 571 | * @param {object|string} targetDir `Directory` instance or absolute path of the target directory. 572 | * @param {object} options Options for `fs-extra.move`. 573 | * @returns {node} 574 | */ 575 | moveToSync(targetDir, options) { 576 | const targetPath = this.__resolvePath(targetDir); 577 | this.fs.moveSync(this.pathName, targetPath, options); 578 | this._setPathParams(targetPath); 579 | return this; 580 | } 581 | 582 | /** 583 | * Alias for `node.moveTo`. 584 | */ 585 | appendTo() { 586 | return this.moveTo(...arguments); 587 | } 588 | 589 | /** 590 | * Alias for `node.moveToSync`. 591 | * @chainable 592 | * @returns {node} 593 | */ 594 | appendToSync() { 595 | return this.moveToSync(...arguments); 596 | } 597 | 598 | /** 599 | * Asynchronously select siblings of the node. 600 | * Uses `glob` package. 601 | * @param {string} [patten='*'] Optional `glob` pattern. 602 | * @param {object} [options] Options for `glob` package. 603 | * @return {promise} Promise representing a `draxt` collection. 604 | */ 605 | siblings() { 606 | const {rawQuery, toNodes, Draxt} = Node; 607 | let [_options, filterFn] = this.__normalizeRelativeGlobOptions(...arguments); 608 | _options.ignore.push(this.pathName); 609 | return rawQuery('*', _options).then((items) => { 610 | if (filterFn) { 611 | items = items.filter(filterFn); 612 | } 613 | return toNodes(items).then(Draxt); 614 | }); 615 | } 616 | 617 | /** 618 | * Synchronously select siblings of the node. 619 | * Uses `glob` package. 620 | * @param {string} [pattern='*'] Optional `glob` pattern. 621 | * @param {object} [options] Options for `glob` package. 622 | * @return {draxt} A `draxt` collection. 623 | */ 624 | siblingsSync() { 625 | const {rawQuerySync, toNodesSync, Draxt} = Node; 626 | let [_options, filterFn] = this.__normalizeRelativeGlobOptions(...arguments); 627 | _options.ignore.push(this.pathName); 628 | let items = rawQuerySync('*', _options); 629 | if (filterFn) { 630 | items = items.filter(filterFn); 631 | } 632 | return new Draxt(toNodesSync(items)); 633 | } 634 | 635 | /** 636 | * Remove the node from file system! `Directory` nodes can have contents. Like `rm -rf`. 637 | * Wrapper for `fs-extra.remove`. 638 | * @returns {promise} 639 | */ 640 | remove() { 641 | return this.fs.remove(this.pathName, ...arguments); 642 | } 643 | 644 | /** 645 | * Wrapper for `fs-extra.removeSync`. 646 | * @chainable 647 | * @returns {node} 648 | */ 649 | removeSync() { 650 | this.fs.removeSync(this.pathName, ...arguments); 651 | return this; 652 | } 653 | 654 | /** 655 | * Asynchronously get parent directory node of the node. It's an async method 656 | * as it gets an instance of `fs.Stats` for the parent node asynchronously! 657 | * @returns {promise} Promise representing a `Directory` instance. 658 | * @example 659 | * const file = new File('/app/resources/style.css'); 660 | * file.parent().then(dir => { 661 | * dir.isDirectory(); // → true 662 | * dir.getPathName(); // → '/app/resources' 663 | * }); 664 | */ 665 | parent() { 666 | const {Directory} = Node; 667 | return this.fs.lstat(this.parentPath).then((stats) => { 668 | return new Directory(this.parentPath, stats); 669 | }); 670 | } 671 | 672 | /** 673 | * Synchronously get parent directory node of the node. 674 | * @returns {node} A `Directory` instance. 675 | */ 676 | parentSync() { 677 | const {Directory} = Node; 678 | const stats = this.fs.lstatSync(this.parentPath); 679 | return new Directory(this.parentPath, stats); 680 | } 681 | 682 | /** 683 | * Asynchronously query the file system by using `glob` package. 684 | * @param {string} pattern Pattern for `glob` package. 685 | * @param {object} [options] Options for `glob` package. 686 | * @returns {promise} An array of pathNames. 687 | */ 688 | static rawQuery(pattern, options) { 689 | options = Node.__normalizeGlobOptions(options); 690 | return glob(pattern, options); 691 | } 692 | 693 | /** 694 | * Synchronously query the file system by using `glob` package. 695 | * @param {string} pattern Pattern for `glob` package. 696 | * @param {object} [options] Options for `glob` package. 697 | * @returns {array} An array of pathNames. 698 | */ 699 | static rawQuerySync(pattern, options) { 700 | options = Node.__normalizeGlobOptions(options); 701 | return globSync(pattern, options); 702 | } 703 | 704 | /** 705 | * Convert array of paths to array of node instances asynchronously! 706 | * A node instance can be an instance of `File`, `Directory` or `SymbolicLink`. 707 | * @param {array} pathNames Array of pathNames. 708 | * @returns {promise} Array of node instances. 709 | * @example 710 | * const pathNames = [ 711 | * '/app/resources', 712 | * '/app/resources/style.css' 713 | * ]; 714 | * Node.toNodes(pathNames).then(result => { 715 | * // result: 716 | * [ 717 | * Directory { pathName: '/app/resources', ... }, 718 | * File { pathName: '/app/resources/style.css', ... } 719 | * ] 720 | * }); 721 | */ 722 | static toNodes(pathNames) { 723 | const nItems = []; 724 | const ps = pathNames.map((item, i) => { 725 | return fs.lstat(item).then((stats) => { 726 | nItems[i] = Node.__statsToNode(item, stats); 727 | }); 728 | }); 729 | return Promise.all(ps).then(() => nItems); 730 | } 731 | 732 | /** 733 | * Convert array of paths to array of nodes synchronously! 734 | * A node instance can be instance of `File`, `Directory` or `SymbolicLink`. 735 | * @param {array} pathNames Array of paths 736 | * @returns {array} Array of node instances. 737 | */ 738 | static toNodesSync(pathNames) { 739 | return pathNames.map((item) => { 740 | const stats = fs.lstatSync(item); 741 | return Node.__statsToNode(item, stats); 742 | }); 743 | } 744 | 745 | /** 746 | * Create a node object by analyzing `fs.Stats` of a pathName. 747 | * @param {string} pathName Absolute pathName of the node. 748 | * @param {object} stats Instance of `fs.Stats` for the pathName. 749 | * @returns {node} 750 | */ 751 | static __statsToNode(pathName, stats) { 752 | const {File, Directory, SymbolicLink} = Node; 753 | if (stats.isFile()) { 754 | return new File(pathName, stats); 755 | } else if (stats.isDirectory()) { 756 | return new Directory(pathName, stats); 757 | } else if (stats.isSymbolicLink()) { 758 | return new SymbolicLink(pathName, stats); 759 | } else { 760 | return new Node(pathName, stats); 761 | } 762 | } 763 | 764 | /** 765 | * Asynchronously query the file system by using `glob` package. 766 | * @param {string} pattern A `glob` pattern. 767 | * @param {object} [options] Options for `glob` package. 768 | * @returns {promise} Array of nodes. 769 | */ 770 | static query() { 771 | const {rawQuery, toNodes} = Node; 772 | return rawQuery(...arguments).then(toNodes); 773 | } 774 | 775 | /** 776 | * Synchronously query the file system by using `glob` package. 777 | * @param {string} pattern 778 | * @param {object} [options] Options for `glob` package. 779 | * @returns {array} Array of nodes. 780 | */ 781 | static querySync() { 782 | const {rawQuerySync, toNodesSync} = Node; 783 | return toNodesSync(rawQuerySync(...arguments)); 784 | } 785 | 786 | /** 787 | * Normalize glob options. This function overrides some possible user-set 788 | * params like the `absolute` parameter. 789 | * @param {object} options 790 | * @returns {object} Normalized object 791 | */ 792 | static __normalizeGlobOptions(options = {}) { 793 | const type = getType(options); 794 | if (type !== 'undefined' && ['string', 'object'].indexOf(type) === -1) { 795 | throw new Error('Optional `options` parameter must be either a string or an object!'); 796 | } 797 | // query(pattern, context) syntax 798 | if (type === 'string') { 799 | options = { 800 | cwd: options, 801 | }; 802 | } 803 | 804 | assign(options, { 805 | absolute: true, 806 | }); 807 | 808 | return options; 809 | } 810 | 811 | /** 812 | * Normalize glob options for `Node#siblings` and `Node#siblingsSync` methods. 813 | * @param {string} [pattern] Optional pattern 814 | * @param {object} [options] Optional options 815 | * @return {array} 816 | */ 817 | __normalizeRelativeGlobOptions(pattern, options = {}) { 818 | let filterFn; 819 | if (arguments.length === 1 && getType(pattern) === 'object') { 820 | options = pattern; 821 | pattern = undefined; 822 | } 823 | if (pattern && getType(pattern) !== 'string') { 824 | throw new Error('`pattern` parameter should be a string!'); 825 | } 826 | if (getType(options) === 'string') { 827 | throw new Error('Relational queries do not accept `context` paramter!'); 828 | } 829 | if (options && getType(options) !== 'object') { 830 | throw new Error('Invalid type for `options` parameter!'); 831 | } 832 | 833 | // Convert `options.ignore` into an array (if it's not) 834 | let ignore = options.ignore; 835 | if (ignore) { 836 | if (!Array.isArray(ignore)) { 837 | ignore = [ignore]; 838 | } 839 | } else { 840 | ignore = []; 841 | } 842 | options.ignore = ignore; 843 | 844 | // If `pattern` exists create a minimatch filter function 845 | if (pattern) { 846 | filterFn = minimatch.filter(pattern, { 847 | matchBase: true, 848 | dot: !!options.dot, 849 | }); 850 | } 851 | 852 | const dirPath = this.nodeName === 'Directory' ? this.pathName : this.parentPath; 853 | 854 | assign(options, { 855 | cwd: dirPath, 856 | }); 857 | 858 | return [options, filterFn]; 859 | } 860 | 861 | /** 862 | * Make a pathName by joining `dir` parameter with node's `baseName` 863 | * @param {node|string} dir Instance of `Directory` class or a string pathName 864 | * @returns {string} 865 | */ 866 | __resolvePath(dir) { 867 | const dirType = typeof dir; 868 | if (dirType === 'undefined') { 869 | throw new Error('`dir` parameter is required!'); 870 | } 871 | const isDirectory = dir.nodeName === 'Directory'; 872 | if (!isDirectory && dirType !== 'string') { 873 | throw new Error('`dir` parameter must be a string or instance of Directory class!'); 874 | } 875 | const dirPath = isDirectory ? dir.pathName : dir; 876 | 877 | if (!path.isAbsolute(dirPath)) { 878 | throw new Error('`dir` must be an absolute path!'); 879 | } 880 | return path.join(dirPath, this.baseName); 881 | } 882 | } 883 | 884 | Node.prototype.fs = fs; 885 | Node.prototype.glob = glob; 886 | 887 | module.exports = Node; 888 | -------------------------------------------------------------------------------- /src/interfaces/SymbolicLink.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'), 2 | nodeProps = { 3 | nodeName: {value: 'SymbolicLink', writable: false, configurable: false, enumerable: true}, 4 | NODE_TYPE: {value: 3, writable: false, configurable: false, enumerable: true}, 5 | }; 6 | 7 | /** 8 | * `SymbolicLink` class which extends the `Node` class is an interface representing pathNames 9 | * that their `fs.Stats`'s `.isSymbolicLink()` method returns `true`. 10 | * @prop {string} nodeName Name of the node: `'SymbolicLink'`. 11 | * @prop {number} NODE_TYPE Code number for the node: `3`. 12 | */ 13 | class SymbolicLink extends Node { 14 | /** 15 | * Construct a new symbolic link. 16 | * @param {string} pathName Absolute pathName of the node 17 | * @param {object} [stats] Instance of `fs.Stats` for the node 18 | */ 19 | constructor(nodePath, stats) { 20 | super(nodePath, stats); 21 | Object.defineProperties(this, nodeProps); 22 | } 23 | 24 | /** 25 | * Is the symlink broken? 26 | * @returns {promise} 27 | */ 28 | isBroken() { 29 | return this.readlink().then((linkPath) => { 30 | return this.exists(linkPath).then((ret) => !ret); 31 | }); 32 | } 33 | 34 | /** 35 | * Synchronous version of `symbolicLink.isBroken` method. 36 | * @returns {boolean} 37 | */ 38 | isBrokenSync() { 39 | return !this.existsSync(this.readlinkSync()); 40 | } 41 | 42 | /** 43 | * Asynchronously read the value of the symbolic link. 44 | * Wrapper for `fs.readlink`. 45 | * @param {string|object} options Options for `fs.readlinkSync`. 46 | * @returns {promise} 47 | */ 48 | readlink(options) { 49 | return this.fs.readlink(this.pathName, options); 50 | } 51 | 52 | /** 53 | * Synchronously read the value of the symbolic link. 54 | * Wrapper for `fs.readlinkSync`. 55 | * @param {string|object} options Options for `fs.readlinkSync`. 56 | * @returns {string|buffer} 57 | */ 58 | readlinkSync(options) { 59 | return this.fs.readlinkSync(this.pathName, options); 60 | } 61 | } 62 | 63 | module.exports = Node.SymbolicLink = SymbolicLink; 64 | -------------------------------------------------------------------------------- /src/interfaces/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Node: require('./Node'), 3 | File: require('./File'), 4 | Directory: require('./Directory'), 5 | SymbolicLink: require('./SymbolicLink'), 6 | }; 7 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple utility object 3 | */ 4 | module.exports = { 5 | /** 6 | * Get type of a variable 7 | * @param {any} value 8 | * @returns {string} 9 | */ 10 | getType(value) { 11 | return Object.prototype 12 | .toString 13 | .apply(value) 14 | .match(/\[object (\w+)\]/)[1] 15 | .toLowerCase(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/draxt.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach, afterEach*/ 2 | const draxt = require('../src/draxt'), 3 | {expect} = require('chai'), 4 | {execSync} = require('child_process'), 5 | {Node} = draxt; 6 | 7 | const contentList = ['a.js', 'another_dir', 'another_example_file.md', 'b.md', 'example_file.md']; 8 | const mkfs = () => { 9 | const pre = ` 10 | rm -r /tmp/draxt_test_dir 11 | mkdir /tmp/draxt_test_dir 12 | echo 'example content' > /tmp/draxt_test_dir/example_file.md 13 | echo 'example content' > /tmp/draxt_test_dir/another_example_file.md 14 | mkdir /tmp/draxt_test_dir/another_dir 15 | echo '...' > /tmp/draxt_test_dir/a.js 16 | ln -s /tmp/draxt_test_dir/another_example_file /tmp/draxt_test_dir/b.md 17 | `; 18 | execSync(pre); 19 | }; 20 | describe('draxt', function () { 21 | beforeEach(function () { 22 | mkfs(); 23 | }); 24 | 25 | afterEach(function () { 26 | mkfs(); 27 | }); 28 | 29 | it('basic initialization', function () { 30 | expect(draxt()).to.be.instanceof(draxt); 31 | expect(draxt().length).to.eql(0); 32 | expect(() => draxt(['str'])).to.throw('Invalid value for `items` parameter'); 33 | const items_one = [new Node('pathName')]; 34 | const d_one = draxt(items_one); 35 | expect(d_one.length).to.eql(1); 36 | expect(d_one.items).to.eql(items_one); 37 | expect(draxt.fn).to.eql(draxt.prototype); 38 | }); 39 | 40 | it('initialization with query', function () { 41 | return draxt('/tmp/draxt_test_dir/*').then((d) => { 42 | expect(d).to.be.instanceof(draxt); 43 | expect(d.length).to.eql(5); 44 | expect(draxt(d).items).to.eql(d.items); 45 | expect(draxt(d).length).to.eql(d.length); 46 | // make sure the `items` parameter has been cloned! 47 | expect(draxt(d).items === d.items).to.eql(false); 48 | }); 49 | }); 50 | 51 | it('.sync()', function () { 52 | const result = draxt.sync('/tmp/draxt_test_dir'); 53 | expect(result).to.be.instanceof(draxt); 54 | expect(result.length).to.eql(1); 55 | expect(draxt.sync('/tmp/draxt_test_dir/*').length).to.eql(5); 56 | }); 57 | 58 | it('.extend()', function () { 59 | const exampleFunc = function () { 60 | return 'exampleFunc'; 61 | }; 62 | draxt.extend({ 63 | exampleFunc, 64 | }); 65 | expect(draxt.prototype.exampleFunc).to.eql(exampleFunc); 66 | expect(draxt.fn.exampleFunc).to.eql(exampleFunc); 67 | }); 68 | 69 | it('.add()', function () { 70 | const d = draxt(); 71 | expect(d.length).to.eql(0); 72 | const node = new Node('pathName'); 73 | d.add(node); 74 | expect(d.length).to.eql(1); 75 | // should not add a duplicate item! 76 | d.add(node); 77 | expect(d.length).to.eql(1); 78 | d.add(new Node('pathName')); 79 | expect(d.length).to.eql(1); 80 | d.add(new Node('pathName2')); 81 | expect(d.length).to.eql(2); 82 | const d2 = draxt([new Node('pathName3')]); 83 | d.add(d2); 84 | expect(d.length).to.eql(3); 85 | }); 86 | 87 | it('.get()', function () { 88 | const d = draxt.sync('/tmp/draxt_test_dir/*'); 89 | const node = d.get(); 90 | expect(node).to.be.an('array'); 91 | expect(node).to.be.eql(d.items); 92 | expect(d.get(0)).to.be.eql(d.items[0]); 93 | }); 94 | 95 | it('.first() && .last()', function () { 96 | const d = draxt.sync('/tmp/draxt_test_dir/*'); 97 | expect(d.first()).to.eql(d.items[0]); 98 | expect(d.last()).to.eql(d.items.pop()); 99 | }); 100 | 101 | it('.has()', function () { 102 | const d = draxt.sync('/tmp/draxt_test_dir/*'); 103 | expect(d.has('/tmp/draxt_test_dir/another_dir')).to.eql(true); 104 | expect(d.has(new Node('/tmp/draxt_test_dir/another_dir'))).to.eql(true); 105 | expect(d.has('/tmp/draxt_test_dir/non_existent')).to.eql(false); 106 | }); 107 | 108 | it('.slice()', function () { 109 | const d = draxt.sync('/tmp/draxt_test_dir/**'); 110 | expect(d.length).to.eql(6); 111 | const d2 = d.slice(0, 4); 112 | expect(d2).to.be.instanceof(draxt); 113 | expect(d2.length).to.be.eql(4); 114 | const d3 = d.slice(-1); 115 | expect(d3.length).to.eql(1); 116 | expect(d3.get(0)).to.be.instanceof(Node); 117 | }); 118 | 119 | it('.filter()', function () { 120 | const d = draxt.sync('/tmp/draxt_test_dir/**'); 121 | const d2 = d.filter((node) => { 122 | return node.isFile(); 123 | }); 124 | expect(d2).to.be.instanceof(draxt); 125 | expect(d === d2).to.eql(false); 126 | expect(d2.length).to.eql(3); 127 | }); 128 | 129 | it('.map()', function () { 130 | const d = draxt.sync('/tmp/draxt_test_dir/*'); 131 | const res = d.map((node) => node.baseName); 132 | res.sort(); 133 | expect(res.length).to.eql(d.length); 134 | expect(res).to.be.an('array'); 135 | expect(res).to.eql(contentList); 136 | }); 137 | 138 | it('.mapAsync', function () { 139 | const d = draxt.sync('/tmp/draxt_test_dir/*'); 140 | const res = d.mapAsync((node) => { 141 | return new Promise((res) => { 142 | setTimeout(() => res(node.baseName), 30); 143 | }); 144 | }); 145 | expect(res).to.be.instanceof(Promise); 146 | return res.then((baseNames) => { 147 | baseNames.sort(); 148 | expect(baseNames).to.eql(contentList); 149 | }); 150 | }); 151 | 152 | it('.each() && .forEach()', function () { 153 | const d = draxt.sync('/tmp/draxt_test_dir/*'); 154 | expect(d.each).to.be.eql(d.forEach); 155 | const res = []; 156 | expect(d.forEach((node) => res.push(node.baseName))).to.eql(d); 157 | res.sort(); 158 | expect(res).to.eql(contentList); 159 | }); 160 | 161 | it('.some', function () { 162 | const d = draxt.sync('/tmp/draxt_test_dir/*'); 163 | const res = d.some((node) => node.baseName === 'another_dir'); 164 | const res2 = d.some((node) => node.baseName === 'non_existent'); 165 | expect(res).to.eql(true); 166 | expect(res2).to.eql(false); 167 | }); 168 | 169 | it('.sort() && .reverse()', function () { 170 | const d = draxt.sync('/tmp/draxt_test_dir/*'); 171 | const originalNodesBackup = d.get().slice(); 172 | expect(d.reverse().get()).to.eql(originalNodesBackup.reverse()); 173 | }); 174 | 175 | it('.directories()', function () { 176 | const d = draxt.sync('/tmp/draxt_test_dir/**'); 177 | const dirs = d.directories(); 178 | expect(dirs.length).to.eql(2); 179 | expect(dirs === d).to.eql(false); 180 | }); 181 | 182 | it('.files()', function () { 183 | const d = draxt.sync('/tmp/draxt_test_dir/**'); 184 | const files = d.files(); 185 | expect(files.length).to.eql(3); 186 | expect(files === d).to.eql(false); 187 | }); 188 | 189 | it('.symlinks()', function () { 190 | const d = draxt.sync('/tmp/draxt_test_dir/**'); 191 | const symlinks = d.symlinks(); 192 | expect(symlinks.length).to.eql(1); 193 | expect(symlinks).to.be.instanceof(draxt); 194 | }); 195 | 196 | it('.empty()', function () { 197 | const d = draxt.sync('/tmp/draxt_test_dir/**'); 198 | expect(d.length).to.eql(6); 199 | expect(d.empty()).to.eql(d); 200 | expect(d.length).to.eql(0); 201 | }); 202 | 203 | it('.drop()', function () { 204 | const d = draxt.sync('/tmp/draxt_test_dir/**'); 205 | expect(d.length).to.eql(6); 206 | d.drop('/tmp/draxt_test_dir/example_file.md'); 207 | expect(d.length).to.eql(5); 208 | d.drop([new Node('/tmp/draxt_test_dir/another_example_file.md'), '/non_existent']); 209 | expect(d.length).to.eql(4); 210 | const d2 = draxt.sync('/tmp/draxt_test_dir/another_dir/*'); 211 | d.drop(d2); 212 | expect(d.length).to.eql(4); 213 | d.drop(new Node('/tmp/draxt_test_dir')); 214 | expect(d.length).to.eql(3); 215 | expect(() => d.drop(new Date())).to.throw('Invalid paramter passed to'); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /test/interfaces/Directory.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, it*/ 2 | const Draxt = require('../../src/draxt'); 3 | const {expect} = require('chai'); 4 | const {execSync} = require('child_process'); 5 | const {Directory, File} = Draxt.Node; 6 | const shouldNotPass = function () { 7 | throw new Error('should not pass!'); 8 | }; 9 | 10 | describe('Directory', function () { 11 | describe('initialization and basic methods', function () { 12 | const nodePath = '/fake/_fakepath'; 13 | const stats = {}; 14 | it('`new`', function () { 15 | const node = new Directory(nodePath, stats); 16 | expect(node.pathName).to.eql(nodePath); 17 | expect(node.extension).to.eql(''); 18 | expect(node.name).to.eql('_fakepath'); 19 | expect(node.baseName).to.eql('_fakepath'); 20 | expect(node.parentPath).to.eql('/fake'); 21 | expect(node.getCachedStats() === stats).to.eql(true); 22 | }); 23 | 24 | it('path.parse methods', function () { 25 | const node = new Directory(nodePath, stats); 26 | expect(node.getPathName()).to.eql(nodePath); 27 | expect(node.getExtension()).to.eql(''); 28 | expect(node.getName()).to.eql('_fakepath'); 29 | expect(node.getBaseName()).to.eql('_fakepath'); 30 | expect(node.getParentPath()).to.eql('/fake'); 31 | }); 32 | }); 33 | 34 | describe('fs methods', function () { 35 | beforeEach(function () { 36 | const pre = ` 37 | rm -r /tmp/fake_test_dir 38 | mkdir /tmp/fake_test_dir 39 | mkdir /tmp/fake_test_dir/empty_dir 40 | mkdir /tmp/fake_test_dir/empty_dir2 41 | mkdir /tmp/fake_test_dir/non_empty_dir 42 | mkdir /tmp/fake_test_dir/non_empty_dir/foo 43 | mkdir /tmp/fake_test_dir/non_empty_dir/.git 44 | mkdir /tmp/fake_test_dir/non_empty_dir2 45 | echo 'example content' > /tmp/fake_test_dir/example_file.md 46 | echo 'content' > /tmp/fake_test_dir/non_empty_dir/file.rb 47 | echo 'content' > /tmp/fake_test_dir/non_empty_dir2/file.md 48 | `; 49 | execSync(pre); 50 | }); 51 | 52 | it('.rmdir() & .rmdirSync() & .ensure() & .ensureSync()', function () { 53 | const dir = new Directory('/tmp/fake_test_dir/empty_dir'); 54 | const dir2 = new Directory('/tmp/fake_test_dir/non_empty_dir'); 55 | expect(dir.existsSync()).to.eql(true); 56 | expect(dir.rmdirSync()).to.eql(dir); 57 | expect(dir.existsSync()).to.eql(false); 58 | // recreate the dir 59 | expect(dir.ensureSync()).to.eql(dir); 60 | // make sure the generated node is a directory 61 | expect(dir.renewStatsSync().isDirectory()).to.eql(true); 62 | expect(dir.existsSync()).to.eql(true); 63 | 64 | // rmdir should throw en error for non-empty dirs 65 | expect(function () { 66 | dir2.rmdirSync(); 67 | }).to.throw('ENOTEMPTY'); 68 | 69 | return dir.rmdir().then(function () { 70 | expect(dir.existsSync()).to.eql(false); 71 | return dir.ensure().then(function () { 72 | expect(dir.existsSync()).to.eql(true); 73 | expect(dir.renewStatsSync().isDirectory()).to.eql(true); 74 | return dir2 75 | .rmdir() 76 | .then(shouldNotPass) 77 | .catch((e) => { 78 | expect(e.message).to.have.string('ENOTEMPTY'); 79 | }); 80 | }); 81 | }); 82 | }); 83 | 84 | it('.readdir() & .readdirSync() & .read() & .readSync()', function () { 85 | const dir = new Directory('/tmp/fake_test_dir/non_empty_dir'); 86 | const expected = ['.git', 'file.rb', 'foo']; 87 | expect(dir.readSync()).to.eql(expected); 88 | return dir.read().then((ret) => { 89 | expect(ret).to.eql(expected); 90 | }); 91 | }); 92 | 93 | it('isEmpty() & .isEmptySync()', function () { 94 | const dir = new Directory('/tmp/fake_test_dir/empty_dir'); 95 | const dir2 = new Directory('/tmp/fake_test_dir/non_empty_dir'); 96 | expect(dir.isEmptySync()).to.eql(true); 97 | expect(dir2.isEmptySync()).to.eql(false); 98 | return dir.isEmpty().then((empty) => { 99 | expect(empty).to.eql(true); 100 | return dir2.isEmpty().then((empty) => { 101 | expect(empty).to.eql(false); 102 | }); 103 | }); 104 | }); 105 | 106 | it('.empty() & .emptySync()', function () { 107 | const expected = ['.git', 'file.rb', 'foo']; 108 | const dir = new Directory('/tmp/fake_test_dir/non_empty_dir'); 109 | const dir2 = new Directory('/tmp/fake_test_dir/non_empty_dir2'); 110 | expect(dir.readSync()).to.eql(expected); 111 | expect(dir.emptySync()).to.eql(dir); 112 | expect(dir.readSync()).to.eql([]); 113 | expect(dir2.readSync()).to.eql(['file.md']); 114 | return dir2.empty().then(function () { 115 | expect(dir2.readSync()).to.eql([]); 116 | }); 117 | }); 118 | 119 | it('__normalizeAppendNodes', function () { 120 | const method = Directory.__normalizeAppendNodes; 121 | const d = Draxt([new File('file.ext'), new Directory('dirname')]); 122 | expect(method(d)).to.eql(d.items); 123 | const d2 = ['str', new File('doo')]; 124 | expect(method(d2)).to.eql(d2); 125 | const d3 = new File(''); 126 | expect(method(d3)).to.eql([d3]); 127 | expect(method('path')).to.eql(['path']); 128 | expect(method(d.items)).to.eql(d.items); 129 | expect(() => method(new Date())).to.throw(); 130 | }); 131 | 132 | it('.append() & .appendSync()', function () { 133 | const col = [ 134 | new File('/tmp/fake_test_dir/non_empty_dir/file.rb'), 135 | '/tmp/fake_test_dir/non_empty_dir/foo', 136 | ]; 137 | // the directory will be created! 138 | const dir = new Directory('/tmp/fake_test_dir/empty_dir'); 139 | expect(dir.appendSync(col)).to.eql(dir); 140 | // expect node to have it's new path! 141 | expect(col[0].pathName).to.eql('/tmp/fake_test_dir/empty_dir/file.rb'); 142 | expect(dir.readSync()).to.eql(['file.rb', 'foo']); 143 | expect(new Directory('/tmp/fake_test_dir/non_empty_dir').readSync()).to.eql(['.git']); 144 | // col2 145 | const col2 = Draxt([new File('/tmp/fake_test_dir/example_file.md')]); 146 | return dir.append(col2).then(function () { 147 | expect(col2.get(0).pathName).eql('/tmp/fake_test_dir/empty_dir/example_file.md'); 148 | return dir.append('/tmp/fake_test_dir/non_empty_dir2'); 149 | }); 150 | }); 151 | 152 | it('.children() & .childrenSync()', function () { 153 | const dir = new Directory('/tmp/fake_test_dir/non_empty_dir'); 154 | const ret = dir.childrenSync(); 155 | expect(ret).to.be.instanceof(Draxt); 156 | expect(ret.length).to.eql(2); 157 | const ret2 = dir.childrenSync('*.rb'); 158 | expect(ret2.length).to.eql(1); 159 | expect(ret2.get(0)).to.be.instanceof(File); 160 | expect(ret2.get(0).baseName).to.eql('file.rb'); 161 | const ret3 = dir.childrenSync({dot: true}); 162 | expect(ret3.length).to.eql(3); 163 | 164 | return dir.children().then((set) => { 165 | expect(set).to.be.instanceof(Draxt); 166 | expect(set.length).to.eql(2); 167 | expect(set.get(0).baseName).to.eql('foo'); 168 | return dir.children('f*').then((set2) => { 169 | expect(set2.length).to.eql(2); 170 | }); 171 | }); 172 | }); 173 | 174 | it('.find() & .findSync()', function () { 175 | const dir = new Directory('/tmp/fake_test_dir'); 176 | const ret = dir.findSync('*'); 177 | expect(ret).to.be.instanceof(Draxt); 178 | expect(ret.length).to.eql(5); 179 | const ret2 = dir.findSync('**'); 180 | expect(ret2.length).to.eql(9); 181 | const ret3 = dir.findSync('**', {dot: true}); 182 | expect(ret3.length).to.eql(10); 183 | return dir.find('*').then((set) => { 184 | expect(set).to.be.instanceof(Draxt); 185 | expect(set.length).to.eql(5); 186 | return dir.find('**', {dot: true}).then((set2) => { 187 | expect(set2.length).to.eql(10); 188 | }); 189 | }); 190 | }); 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /test/interfaces/File.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, afterEach, it*/ 2 | const {File} = require('../../src/draxt').Node; 3 | const {expect} = require('chai'); 4 | 5 | describe('File', () => { 6 | describe('initialization', () => { 7 | it('new and .isFile() method', () => { 8 | const file = new File('/fake/fakepath/module.md', {}); 9 | expect(file.isFile()).to.eql(true); 10 | expect(file.isDirectory()).to.eql(false); 11 | expect(file.isSymbolicLink()).to.eql(false); 12 | expect(file.NODE_TYPE).to.eql(2); 13 | expect(file.nodeName).to.eql('File'); 14 | }); 15 | }); 16 | 17 | describe('fs methods', () => { 18 | beforeEach(() => { 19 | const {execSync} = require('child_process'); 20 | const pre = ` 21 | rm -r /tmp/fake_dir 22 | mkdir /tmp/fake_dir 23 | echo 'example content.' > /tmp/fake_dir/example_file.md 24 | echo 'example content.' > /tmp/fake_dir/another_example_file.md 25 | `; 26 | execSync(pre); 27 | }); 28 | 29 | afterEach(() => { 30 | // vol.reset(); 31 | }); 32 | 33 | it('.read() && .readSync()', () => { 34 | const file = new File('/tmp/fake_dir/example_file.md', {}); 35 | const content = file.readSync('utf8'); 36 | const expectContent = 'example content.\n'; 37 | expect(content).to.eql(expectContent); 38 | return file.read('utf8').then((content) => { 39 | expect(content).to.eql(expectContent); 40 | }); 41 | }); 42 | 43 | it('.append() && .appendSync()', () => { 44 | const file = new File('/tmp/fake_dir/example_file.md', {}); 45 | const ret = file.appendSync(' appended content'); 46 | expect(ret).to.eql(file); 47 | expect(file.readSync('utf8')).to.eql('example content.\n appended content'); 48 | return file.append('. string').then(() => { 49 | return file.read('utf8').then((content) => { 50 | expect(content).to.eql('example content.\n appended content. string'); 51 | }); 52 | }); 53 | }); 54 | 55 | it('.write() && .writeSync()', () => { 56 | const file = new File('/tmp/fake_dir/example_file.md', {}); 57 | const ret = file.writeSync('new content'); 58 | expect(ret).to.eql(file); 59 | expect(file.readSync('utf8')).to.eql('new content'); 60 | return file.write('new async written content', () => { 61 | return file.read('utf8').then((content) => { 62 | return expect(content).to.eql('new async written content'); 63 | }); 64 | }); 65 | }); 66 | 67 | it('.truncate() && .truncateSync()', () => { 68 | const file = new File('/tmp/fake_dir/example_file.md', {}); 69 | const ret = file.truncateSync(4); 70 | expect(ret).to.eql(file); 71 | expect(file.readSync('utf8')).to.eql('exam'); 72 | return file.truncate().then(() => { 73 | return file.read('utf8').then((content) => { 74 | return expect(content).to.eql(''); 75 | }); 76 | }); 77 | }); 78 | 79 | it('.ensure() && .ensureSync()', () => { 80 | const file = new File('/tmp/fake_dir/non_existent.md'); 81 | const file2 = new File('/tmp/fake_dir/non_existent2.md'); 82 | expect(file.existsSync()).to.eql(false); 83 | expect(file.ensureSync()).to.eql(file); 84 | expect(file.existsSync()).to.eql(true); 85 | // async example 86 | expect(file2.existsSync()).to.eql(false); 87 | return file2.ensure().then(() => { 88 | expect(file2.existsSync()).to.eql(true); 89 | }); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/interfaces/Node.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach, afterEach*/ 2 | const draxt = require('../../src/draxt'), 3 | {Node} = draxt, 4 | {Directory, File} = Node, 5 | {expect} = require('chai'), 6 | fs = require('fs-extra'), 7 | path = require('path'), 8 | isTravis = 'TRAVIS' in process.env && 'CI' in process.env, 9 | shouldNotPass = function () { 10 | throw new Error('should not pass!'); 11 | }; 12 | 13 | const mkfs = () => { 14 | const {execSync} = require('child_process'); 15 | const pre = ` 16 | rm -r /tmp/node_test_dir 17 | mkdir /tmp/node_test_dir 18 | mkdir /tmp/node_test_dir/another_dir 19 | mkdir /tmp/node_test_dir/another_dir/.dir 20 | echo 'example content.' > /tmp/node_test_dir/example_file.md 21 | echo 'example content.' > /tmp/node_test_dir/another_example_file.md 22 | echo '...' > /tmp/node_test_dir/another_dir/a.js 23 | echo '...' > /tmp/node_test_dir/another_dir/b.js 24 | echo '...' > /tmp/node_test_dir/another_dir/c.php 25 | echo '...' > /tmp/node_test_dir/another_dir/k.php 26 | echo '...' > /tmp/node_test_dir/another_dir/d.html 27 | echo '...' > /tmp/node_test_dir/another_dir/README.md 28 | echo '...' > /tmp/node_test_dir/another_dir/foo.rb 29 | echo '...' > /tmp/node_test_dir/another_dir/document.txt 30 | ln -s /tmp/node_test_dir/example_file.md /tmp/node_test_dir/another_dir/g.md 31 | `; 32 | execSync(pre); 33 | }; 34 | 35 | describe('Node', function () { 36 | describe('initialization and basic methods', function () { 37 | const nodePath = '/fake/_fakepath/module.js'; 38 | const stats = {}; 39 | it('`new`', function () { 40 | const node = new Node(nodePath, stats); 41 | expect(node.pathName).to.eql(nodePath); 42 | expect(node.extension).to.eql('js'); 43 | expect(node.name).to.eql('module'); 44 | expect(node.baseName).to.eql('module.js'); 45 | expect(node.parentPath).to.eql('/fake/_fakepath'); 46 | expect(node.rootPath).to.eql('/'); 47 | expect(node.getCachedStats() === stats).to.eql(true); 48 | }); 49 | 50 | it('path.parse methods', function () { 51 | const node = new Node(nodePath, stats); 52 | expect(node.getPathName()).to.eql(nodePath); 53 | expect(node.getExtension()).to.eql('js'); 54 | expect(node.getName()).to.eql('module'); 55 | expect(node.getBaseName()).to.eql('module.js'); 56 | expect(node.getParentPath()).to.eql('/fake/_fakepath'); 57 | expect(node.isDotFile()).to.eql(false); 58 | node.baseName = '.git'; 59 | expect(node.isDotFile()).to.eql(true); 60 | node._stats = undefined; 61 | expect(node.getStatProp('foo')).to.eql(undefined); 62 | }); 63 | }); 64 | 65 | describe('fs module methods', function () { 66 | beforeEach(function () { 67 | mkfs(); 68 | }); 69 | 70 | // afterEach(function () { 71 | // mockFs.restore(); 72 | // }); 73 | 74 | it('.getPermissions()', function () { 75 | const node = new Node('/tmp/node_test_dir/example_file.md'); 76 | expect(() => node.getPermissions()).to.throw('renewStats'); 77 | const perms = node.chmodSync('700').renewStatsSync().getPermissions(); 78 | expect(perms).to.eql({ 79 | read: {owner: true, group: false, others: false}, 80 | write: {owner: true, group: false, others: false}, 81 | execute: {owner: true, group: false, others: false}, 82 | }); 83 | const perms2 = node.chmodSync('777').renewStatsSync().getPermissions(); 84 | expect(perms2).to.eql({ 85 | read: {owner: true, group: true, others: true}, 86 | write: {owner: true, group: true, others: true}, 87 | execute: {owner: true, group: true, others: true}, 88 | }); 89 | }); 90 | 91 | it('.getAccessTime() && .getBirthTime() && .getModifiedTime() && .getChangeTime()', function () { 92 | const node = new Node('/tmp/node_test_dir/example_file.md'); 93 | node.renewStatsSync(); 94 | expect(node.getBirthTime()).to.eql(node._stats.birthtime); 95 | expect(node.getAccessTime()).to.eql(node._stats.atime); 96 | expect(node.getChangeTime()).to.eql(node._stats.ctime); 97 | expect(node.getModifiedTime()).to.eql(node._stats.mtime); 98 | }); 99 | 100 | it('.__resolvePath()', function () { 101 | const node = new Node('/tmp/node_test_dir/example_file.md'); 102 | expect(node.__resolvePath('/foo/bar')).to.eql('/foo/bar/example_file.md'); 103 | expect(node.__resolvePath('/foo/bar/')).to.eql('/foo/bar/example_file.md'); 104 | expect(node.__resolvePath(new Directory('/foo/bar/'))).to.eql( 105 | '/foo/bar/example_file.md' 106 | ); 107 | expect(() => node.__resolvePath('../bar/')).to.throw('absolute'); 108 | expect(() => node.__resolvePath(new File('/foo/bar.js'))).to.throw('Directory'); 109 | expect(() => node.__resolvePath()).to.throw('required'); 110 | }); 111 | 112 | it('.renewStats() && .renewStatsSync()', function () { 113 | const node = new Node('/tmp/node_test_dir/example_file.md'); 114 | expect(node._stats).to.eql(); 115 | expect(node.renewStatsSync()).to.eql(node); 116 | expect(node._stats).to.be.an('object'); 117 | const oldCache = node._stats; 118 | node._stats = null; 119 | return node.renewStats().then(function () { 120 | expect(node._stats).to.be.an('object'); 121 | expect(node._stats === oldCache).to.eql(false); 122 | expect(node.getSize()).to.eql(node._stats.size); 123 | }); 124 | }); 125 | 126 | it('.access() && .accessSync()', function () { 127 | const node = new Node('/tmp/node_test_dir/example_file.md', {}); 128 | const node2 = new Node('/tmp/node_test_dir/does_not_exist.md', {}); 129 | expect(node.accessSync()).to.eql(node); 130 | expect(() => node2.accessSync()).to.throw(); 131 | return node.access().then(function () { 132 | return node2 133 | .access() 134 | .then(shouldNotPass) 135 | .catch((e) => { 136 | expect(e.message).to.include('ENOENT'); 137 | }); 138 | }); 139 | }); 140 | 141 | it('.chmod() && .chmodSync() && .lchmod() && .lchmodSync()', function () { 142 | const node = new Node('/tmp/node_test_dir/another_dir/a.js', {}); 143 | expect(node.chmodSync('700')).to.eql(node); 144 | node.renewStatsSync(); 145 | expect(node.getOctalPermissions()).to.eql('700'); 146 | expect(node.lchmodSync('755')).to.eql(node); 147 | node.renewStatsSync(); 148 | return node.chmod('755').then(function () { 149 | node.renewStatsSync(); 150 | if (!isTravis) expect(node.getOctalPermissions()).to.eql('755'); 151 | return node.lchmod('711').then(function () { 152 | node.renewStatsSync(); 153 | }); 154 | }); 155 | }); 156 | 157 | it('.chown() && .chownSync() && .lchown() && .lchownSync()', function () { 158 | const node = new Node('/tmp/node_test_dir/another_dir/g.md', {}); 159 | expect(node.chownSync(1000, 1000)).to.eql(node); 160 | node.renewStatsSync(); 161 | // Temporarily comment some tests that fail on travis environment! 162 | // 163 | // expect(node._stats.uid).to.eql(10); 164 | // expect(node._stats.gid).to.eql(11); 165 | // expect(node.lchownSync(1000, 1000)).to.eql(node); 166 | // node.renewStatsSync(); 167 | // expect(node._stats.uid).to.eql(20); 168 | // expect(node._stats.gid).to.eql(22); 169 | return node.chown(1000, 1000).then(function () { 170 | // node.renewStatsSync(); 171 | // expect(node._stats.uid).to.eql(30); 172 | // expect(node._stats.gid).to.eql(33); 173 | // This test fails probably because of the mockFs 174 | // return node.lchown(1000, 1000).then(function() { 175 | // node.renewStatsSync(); 176 | // expect(node._stats.uid).to.eql(40); 177 | // expect(node._stats.gid).to.eql(44); 178 | // }); 179 | }); 180 | }); 181 | 182 | it('.exists() && .existsSync()', function () { 183 | const node = new Node('/tmp/node_test_dir/example_file.md', {}); 184 | const node2 = new Node('/tmp/node_test_dir/does_not_exist.md', {}); 185 | expect(node.existsSync()).to.eql(true); 186 | expect(node2.existsSync()).to.eql(false); 187 | return node.exists().then((ret) => { 188 | expect(ret).to.eql(true); 189 | return node2.exists().then((ret2) => { 190 | expect(ret2).to.eql(false); 191 | }); 192 | }); 193 | }); 194 | 195 | it('.stat() && .statSync() && .lstat() && .lstatSync() ', function () { 196 | const node = new Node('/tmp/node_test_dir/example_file.md', {}); 197 | expect(node.statSync()).to.be.an('object'); 198 | expect(node.lstatSync()).to.be.an('object'); 199 | return node.stat().then(function (stats) { 200 | expect(stats).to.be.an('object'); 201 | return node.lstat().then((stats2) => { 202 | expect(stats2).to.be.an('object'); 203 | }); 204 | }); 205 | }); 206 | 207 | it('.link() && .linkSync()', function () { 208 | const node = new Node('/tmp/node_test_dir/example_file.md', {}); 209 | const nodeLink = new Node('/tmp/node_test_dir/example_file_link.md'); 210 | const nodeLink2 = new Node('/tmp/node_test_dir/example_file_link2.md'); 211 | // make sure new name for the file doesn't exist before linking 212 | expect(nodeLink.existsSync()).to.eql(false); 213 | expect(nodeLink2.existsSync()).to.eql(false); 214 | expect(node.linkSync(nodeLink.pathName)).to.eql(node); 215 | expect(nodeLink.existsSync()).to.eql(true); 216 | return node.link(nodeLink2.pathName).then(function () { 217 | expect(nodeLink2.existsSync()).to.eql(true); 218 | }); 219 | }); 220 | 221 | it('.rename() && .renameSync()', function () { 222 | const node = new Node('/tmp/node_test_dir/example_file.md', {}); 223 | const renameSampleNode = new Node('/tmp/node_test_dir/another_dir/example_file.js', {}); 224 | const renameSampleNodeAsync = new Node( 225 | '/tmp/node_test_dir/another_dir/new_name.md', 226 | {} 227 | ); 228 | expect(renameSampleNode.existsSync()).to.eql(false); 229 | expect(renameSampleNodeAsync.existsSync()).to.eql(false); 230 | expect(node.renameSync(renameSampleNode.pathName)).to.eql(node); 231 | expect(renameSampleNode.existsSync()).to.eql(true); 232 | expect(node.pathName).to.eql(renameSampleNode.pathName); 233 | expect(node.getExtension()).to.eql('js'); 234 | return node.rename(renameSampleNodeAsync.pathName).then(function () { 235 | expect(renameSampleNodeAsync.existsSync()).to.eql(true); 236 | expect(node.pathName).to.eql(renameSampleNodeAsync.pathName); 237 | }); 238 | }); 239 | 240 | it('.utimes() && .utimesSync()', function () { 241 | const node = new Node('/tmp/node_test_dir/example_file.md'); 242 | const atime = 1529607246; // Thursday, June 21, 2018 6:54:06 PM 243 | const mtime = 1529520846; // Wednesday, June 20, 2018 6:54:06 PM 244 | expect(node.utimesSync(atime, mtime)).to.eql(node); 245 | node.renewStatsSync(); 246 | if (!isTravis) { 247 | // for some reason these fails on travis! Probably because of mock-fs 248 | expect(node.getStatProp('atimeMs') / 1000).to.eql(atime); 249 | expect(node.getStatProp('mtimeMs') / 1000).to.eql(mtime); 250 | } 251 | return node.utimes(atime - 2000, mtime - 2000).then(function () { 252 | node.renewStatsSync(); 253 | if (!isTravis) { 254 | // for some reason these fails on travis! Probably because of mock-fs 255 | expect(node.getStatProp('atimeMs') / 1000).to.eql(atime - 2000); 256 | expect(node.getStatProp('mtimeMs') / 1000).to.eql(mtime - 2000); 257 | } 258 | }); 259 | }); 260 | }); 261 | 262 | describe('Utility methods', function () { 263 | beforeEach(function () { 264 | mkfs(); 265 | }); 266 | 267 | it('.__normalizeGlobOptions()', function () { 268 | const o1 = Node.__normalizeGlobOptions(); 269 | expect(o1).to.eql({ 270 | absolute: true, 271 | }); 272 | const o2 = Node.__normalizeGlobOptions({absolute: false}); 273 | expect(o2).to.eql({ 274 | absolute: true, 275 | }); 276 | expect(() => Node.__normalizeGlobOptions([])).to.throw( 277 | 'must be either a string or an object' 278 | ); 279 | const o3 = Node.__normalizeGlobOptions('/path'); 280 | expect(o3).to.eql({ 281 | cwd: '/path', 282 | absolute: true, 283 | }); 284 | }); 285 | 286 | it('.__statsToNode()', function () { 287 | const mockStats = (type) => { 288 | return { 289 | isFile() { 290 | return type === 'File'; 291 | }, 292 | isSymbolicLink() { 293 | return type === 'SymbolicLink'; 294 | }, 295 | isDirectory() { 296 | return type === 'Directory'; 297 | }, 298 | }; 299 | }; 300 | [ 301 | {path: '/node', type: 'Node', nodeType: 0}, 302 | {path: '/directory', type: 'Directory', nodeType: 1}, 303 | {path: '/file', type: 'File', nodeType: 2}, 304 | {path: '/symlink', type: 'SymbolicLink', nodeType: 3}, 305 | ].forEach((item) => { 306 | const node = Node.__statsToNode(item.path, mockStats(item.type)); 307 | expect(node.pathName).to.eql(item.path); 308 | expect(node.nodeName).to.eql(item.type); 309 | expect(node.NODE_TYPE).to.eql(item.nodeType); 310 | }); 311 | }); 312 | 313 | it('.toNodes() && .toNodesSync()', function () { 314 | const paths = [ 315 | '/tmp/node_test_dir/another_dir', 316 | '/tmp/node_test_dir/another_dir/g.md', 317 | '/tmp/node_test_dir/example_file.md', 318 | ]; 319 | const nodes1 = Node.toNodesSync(paths); 320 | expect(nodes1).to.be.an('array'); 321 | expect(nodes1[0]).to.be.instanceof(Node.Directory); 322 | expect(nodes1[1]).to.be.instanceof(Node.SymbolicLink); 323 | expect(nodes1[2]).to.be.instanceof(Node.File); 324 | nodes1.forEach((el, index) => { 325 | expect(el.pathName).to.eql(paths[index]); 326 | }); 327 | 328 | return Node.toNodes(paths).then((nodes2) => { 329 | expect(nodes2[0]).to.be.instanceof(Node.Directory); 330 | expect(nodes2[1]).to.be.instanceof(Node.SymbolicLink); 331 | expect(nodes2[2]).to.be.instanceof(Node.File); 332 | nodes2.forEach((el, index) => { 333 | expect(el.pathName).to.eql(paths[index]); 334 | }); 335 | }); 336 | }); 337 | 338 | (function () { 339 | const rawExpected1 = [ 340 | '/tmp/node_test_dir/another_dir', 341 | '/tmp/node_test_dir/another_example_file.md', 342 | '/tmp/node_test_dir/example_file.md', 343 | ].sort(); 344 | const rawExpected2 = [ 345 | '/tmp/node_test_dir', 346 | '/tmp/node_test_dir/another_dir', 347 | '/tmp/node_test_dir/another_dir/a.js', 348 | '/tmp/node_test_dir/another_dir/b.js', 349 | '/tmp/node_test_dir/another_dir/c.php', 350 | '/tmp/node_test_dir/another_dir/d.html', 351 | '/tmp/node_test_dir/another_dir/document.txt', 352 | '/tmp/node_test_dir/another_dir/foo.rb', 353 | '/tmp/node_test_dir/another_dir/g.md', 354 | '/tmp/node_test_dir/another_dir/k.php', 355 | '/tmp/node_test_dir/another_dir/README.md', 356 | '/tmp/node_test_dir/another_example_file.md', 357 | '/tmp/node_test_dir/example_file.md', 358 | ].sort(); 359 | it('.rawQuery() && .rawQuerySync()', function () { 360 | // result seem to be sorted by default 361 | let items1 = Node.rawQuerySync('*', '/tmp/node_test_dir'); 362 | expect(items1.sort()).to.eql(rawExpected1); 363 | const items2 = Node.rawQuerySync('**', { 364 | cwd: '/tmp/node_test_dir', 365 | }); 366 | expect(items2.sort()).to.eql(rawExpected2); 367 | return Node.rawQuery('*', '/tmp/node_test_dir').then((res1) => { 368 | expect(res1.sort()).to.eql(rawExpected1); 369 | return Node.rawQuery('**', '/tmp/node_test_dir').then((res2) => { 370 | expect(res2.sort()).to.eql(rawExpected2); 371 | }); 372 | }); 373 | }); 374 | 375 | it('.query() && .querySync()', function () { 376 | const result1 = Node.querySync('*', '/tmp/node_test_dir'); 377 | expect(result1).to.be.an('array'); 378 | expect(result1.length).to.eql(rawExpected1.length); 379 | const result2 = Node.querySync('**', { 380 | cwd: '/tmp/node_test_dir', 381 | }); 382 | expect(result2.length).to.eql(rawExpected2.length); 383 | expect(result2[0].pathName).to.eql(rawExpected2[0]); 384 | return Node.query('*', '/tmp/node_test_dir').then((res1) => { 385 | expect(res1.length).to.eql(res1.length); 386 | return Node.query('**', '/tmp/node_test_dir').then((res2) => { 387 | expect(res2.length).to.eql(rawExpected2.length); 388 | }); 389 | }); 390 | }); 391 | })(); 392 | 393 | it('.remove() && .removeSync()', function () { 394 | const node = new Node('/tmp/node_test_dir/example_file.md'); 395 | const node2 = new Node('/tmp/node_test_dir/another_example_file.md'); 396 | expect(node.existsSync()).to.eql(true); 397 | expect(node.removeSync()).to.eql(node); 398 | expect(node.existsSync()).to.eql(false); 399 | 400 | expect(node2.existsSync()).to.eql(true); 401 | return node2.remove().then(function () { 402 | expect(node2.existsSync()).to.eql(false); 403 | }); 404 | }); 405 | 406 | it('.parent() && .parentSync()', function () { 407 | const node = new Node('/tmp/node_test_dir/example_file.md'); 408 | const parent = node.parentSync(); 409 | expect(parent).to.be.instanceof(Directory); 410 | expect(parent.pathName).to.eql('/tmp/node_test_dir'); 411 | return node.parent().then((parentAsync) => { 412 | expect(parentAsync).to.be.instanceof(Directory); 413 | expect(parentAsync.pathName).to.eql('/tmp/node_test_dir'); 414 | }); 415 | }); 416 | 417 | it('.siblings() && .siblingsSync()', function () { 418 | const node = new Node('/tmp/node_test_dir/another_dir/c.php'); 419 | const s1 = node.siblingsSync(); 420 | expect(s1.length).to.eql(8); 421 | const exists = s1.some((el) => el.path === node.pathName); 422 | expect(exists).to.eql(false); 423 | const s2 = node.siblingsSync('*.php'); 424 | expect(s2.length).to.eql(1); 425 | // Note: `*` doesn't 426 | const s3 = node.siblingsSync('*', { 427 | ignore: '*.php', 428 | dot: true, 429 | }); 430 | expect(s3.length).to.eql(8); 431 | const s4 = node.siblingsSync({ 432 | ignore: ['*.php'], 433 | }); 434 | expect(s4.length).to.eql(7); 435 | 436 | // errors 437 | expect(() => node.siblingsSync([], {})).to.throw('should be a string'); 438 | expect(() => node.siblingsSync('*', 'context')).to.throw('context'); 439 | expect(() => node.siblingsSync('*', [])).to.throw('options'); 440 | return node.siblings().then((ss1) => { 441 | expect(ss1.length).to.eql(8); 442 | return node 443 | .siblings('*.md', { 444 | ignore: 'README.md', 445 | }) 446 | .then((ss2) => { 447 | expect(ss2.length).to.eql(1); 448 | expect(ss2).to.be.instanceof(draxt); 449 | expect(ss2.get(0).baseName).to.eql('g.md'); 450 | }); 451 | }); 452 | }); 453 | 454 | it('.moveTo() && .moveToSync() && .appendTo() && .appendToSync()', function () { 455 | const node = new Node('/tmp/node_test_dir/another_dir/c.php'); 456 | expect(node.moveToSync('/tmp/node_test_dir/another_dir/.git')).to.eql(node); 457 | expect(node.pathName).to.eql('/tmp/node_test_dir/another_dir/.git/c.php'); 458 | return node.moveTo('/tmp/node_test_dir').then(function () { 459 | expect(node.pathName).to.eql('/tmp/node_test_dir/c.php'); 460 | expect(() => node.moveTo('/tmp/node_test_dir/', function () {})).to.throw( 461 | 'callback' 462 | ); 463 | // @TODO: rewrite, test content had been changed. 464 | // expect(() => node.appendToSync('/tmp/node_test_dir/store')).to.throw( 465 | // 'dest already exists.' 466 | // ); 467 | // return node 468 | // .appendTo('/tmp/node_test_dir/store', {overwrite: true}) 469 | // .then(function () { 470 | // expect(node.pathName).to.eql('/tmp/node_test_dir/store/c.php'); 471 | // expect(node.fs.readFileSync(node.pathName, 'utf8')).to.eql( 472 | // 'c.php content!' 473 | // ); 474 | // }); 475 | }); 476 | }); 477 | }); 478 | 479 | /** 480 | * Exclude copy from others tests for creating example test files and directories 481 | * Reason: fs-extra is not fully compatible with mock-fs module! 482 | */ 483 | describe('.copy() && .copySync()', function () { 484 | const nodePath = path.join(__dirname, '/test_dir/exmaple.node'); 485 | const copyPath = path.join(__dirname, '/test_dir/non_existent/copied.php'); 486 | const copyPath2 = path.join(__dirname, '/test_dir/copied.php'); 487 | beforeEach(function () { 488 | fs.removeSync(path.join(__dirname, 'test_dir')); 489 | fs.ensureFileSync(nodePath); 490 | fs.writeFileSync(nodePath, 'example content!', 'utf8'); 491 | }); 492 | it('.copy() && .copySync', function () { 493 | const node = new Node(nodePath); 494 | expect(node.existsSync()).to.eql(true); 495 | expect(node.copySync(copyPath)).to.eql(node); 496 | expect(node.fs.readFileSync(copyPath, 'utf8')).to.eql('example content!'); 497 | return node.copy(copyPath2).then(function () { 498 | expect(node.fs.readFileSync(copyPath2, 'utf8')).to.eql('example content!'); 499 | }); 500 | }); 501 | afterEach(function () { 502 | fs.removeSync(path.join(__dirname, 'test_dir')); 503 | }); 504 | }); 505 | }); 506 | -------------------------------------------------------------------------------- /test/interfaces/SymbolicLink.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, it*/ 2 | const {SymbolicLink} = require('../../src/draxt').Node; 3 | const {expect} = require('chai'); 4 | 5 | describe('SymbolicLink', () => { 6 | describe('initialization and basic methods', () => { 7 | const nodePath = '/fake/_fakepath'; 8 | const stats = {}; 9 | it('`new`', () => { 10 | const node = new SymbolicLink(nodePath, stats); 11 | expect(node.pathName).to.eql(nodePath); 12 | expect(node.extension).to.eql(''); 13 | expect(node.name).to.eql('_fakepath'); 14 | expect(node.baseName).to.eql('_fakepath'); 15 | expect(node.parentPath).to.eql('/fake'); 16 | expect(node.getCachedStats() === stats).to.eql(true); 17 | }); 18 | 19 | it('path.parse methods', () => { 20 | const node = new SymbolicLink(nodePath, stats); 21 | expect(node.getPathName()).to.eql(nodePath); 22 | expect(node.getExtension()).to.eql(''); 23 | expect(node.getName()).to.eql('_fakepath'); 24 | expect(node.getBaseName()).to.eql('_fakepath'); 25 | expect(node.getParentPath()).to.eql('/fake'); 26 | }); 27 | }); 28 | 29 | describe('fs methods', () => { 30 | beforeEach(() => { 31 | const {execSync} = require('child_process'); 32 | const pre = ` 33 | rm -r /tmp/fake_dir 34 | mkdir /tmp/fake_dir 35 | mkdir /tmp/fake_dir/childdir 36 | echo 'example content.' > /tmp/fake_dir/example_file.md 37 | ln -s /tmp/fake_dir/example_file.md /tmp/fake_dir/childdir/sym1.md 38 | ln -s /tmp/fake_dir/non_existent.md /tmp/fake_dir/childdir/sym2 39 | `; 40 | execSync(pre); 41 | }); 42 | 43 | // afterEach(() => { 44 | // vol.reset(); 45 | // }); 46 | 47 | it('.readlink() && .readlink()', () => { 48 | const sym1 = new SymbolicLink('/tmp/fake_dir/childdir/sym1.md'); 49 | const sym2 = new SymbolicLink('/tmp/fake_dir/childdir/sym2'); 50 | expect(sym1.readlinkSync()).to.eql('/tmp/fake_dir/example_file.md'); 51 | return sym2.readlink().then((pathName) => { 52 | expect(pathName).to.eql('/tmp/fake_dir/non_existent.md'); 53 | }); 54 | }); 55 | 56 | it('.isBroken() && .isBrokenSync()', () => { 57 | const sym1 = new SymbolicLink('/tmp/fake_dir/childdir/sym1.md'); 58 | const sym2 = new SymbolicLink('/tmp/fake_dir/childdir/sym2'); 59 | expect(sym1.isBrokenSync()).to.eql(false); 60 | return sym2.isBroken().then((ret) => { 61 | expect(ret).to.eql(true); 62 | }); 63 | }); 64 | }); 65 | }); 66 | --------------------------------------------------------------------------------