├── test ├── .eslintrc.json ├── normalize.js ├── Dirent.js └── Vinyl.js ├── .editorconfig ├── .github ├── workflows │ └── test.yml ├── contributing.md └── code-of-conduct.md ├── .gitignore ├── example.js ├── package.json ├── .verb.md ├── utils.js ├── .eslintrc.json ├── README.md └── index.js /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../.eslintrc.json" 4 | ], 5 | "env": { 6 | "mocha": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [test/{actual/expected,fixtures}/*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /test/normalize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const assert = require('assert').strict; 5 | const utils = require('../utils'); 6 | 7 | describe('normalize()', function() { 8 | it('leaves empty strings unmodified', () => { 9 | const result = utils.normalize(''); 10 | assert.equal(result, ''); 11 | }); 12 | 13 | it('applies path.normalize for everything else', () => { 14 | const str = '/foo//../bar/baz'; 15 | const result = utils.normalize(str); 16 | assert.equal(result, path.normalize(str)); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: [push, pull_request] 3 | 4 | env: 5 | CI: true 6 | 7 | jobs: 8 | test: 9 | name: Node.js ${{ matrix.node-version }} @ ${{ matrix.os }} 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-latest, windows-latest, macos-latest] 15 | node-version: [10, 12, 13, 14, 15] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm install 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.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 | # Custom ignore patterns 64 | tmp/ 65 | temp/ 66 | vendor/ 67 | .DS_Store 68 | *.sublime-* 69 | *lock.* 70 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const Dirent = require('.'); 5 | 6 | console.log(new Dirent(__filename)); 7 | 8 | const dirent = new Dirent({ path: __filename }); 9 | console.log(dirent); 10 | console.log(dirent.name); 11 | console.log(dirent.basename); 12 | console.log(dirent.path); 13 | 14 | const file = new Dirent({ path: 'example.js', stat: fs.statSync(__filename) }); 15 | console.log(file); 16 | console.log('stem', [file.stem]); 17 | console.log('name', [file.name]); 18 | console.log('basename', [file.basename]); 19 | console.log('path', [file.path]); 20 | console.log('absolute', [file.absolute]); 21 | console.log('base', [file.base]); 22 | console.log('relative', [file.relative]); 23 | console.log('isDirectory', [file.isDirectory()]); 24 | console.log('isFile', [file.isFile()]); 25 | console.log('isSymbolicLink', [file.isSymbolicLink()]); 26 | 27 | console.log(new Dirent('some/file/path.txt')); 28 | console.log(new Dirent({ path: 'some/file/path.txt' })); 29 | 30 | // assuming __filename is /Users/jonschlinkert/dev/dirent/example.js 31 | const file2 = new Dirent(__filename); 32 | console.log(file2.dirname); 33 | //=> /Users/jonschlinkert/dev//dirent 34 | console.log(file2.basename); 35 | //=> example.js 36 | console.log(file2.stem); 37 | //=> example 38 | console.log(file2.extname); 39 | //=> .js 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dirent", 3 | "version": "2.0.2", 4 | "description": "Virtual file class. Extends the native fs.Dirent class with methods to simplify path handling. Similar to Vinyl, but lightweight.", 5 | "license": "MIT", 6 | "repository": "folder/dirent", 7 | "homepage": "https://github.com/folder/dirent", 8 | "author": "Jon Schlinkert (https://github.com/jonschlinkert)", 9 | "engines": { 10 | "node": ">=10" 11 | }, 12 | "scripts": { 13 | "test": "mocha", 14 | "test:ci": "npm run test" 15 | }, 16 | "files": [ 17 | "index.js", 18 | "utils.js" 19 | ], 20 | "keywords": [ 21 | "dir", 22 | "directory entity", 23 | "directory", 24 | "dirent", 25 | "entity", 26 | "file system", 27 | "file", 28 | "file-system", 29 | "fs", 30 | "fs.Dirent", 31 | "path", 32 | "readdir", 33 | "readdirsync", 34 | "vfile", 35 | "vfs", 36 | "vinyl", 37 | "virtual file" 38 | ], 39 | "dependencies": { 40 | "clone-deep": "^4.0.1" 41 | }, 42 | "devDependencies": { 43 | "cloneable-readable": "^2.1.0", 44 | "concat-stream": "^2.0.0", 45 | "from2": "^2.3.0", 46 | "gulp-format-md": "^2.0.0", 47 | "mocha": "^8.3.2", 48 | "pump": "^3.0.0" 49 | }, 50 | "verb": { 51 | "toc": false, 52 | "layout": "default", 53 | "tasks": [ 54 | "readme" 55 | ], 56 | "related": { 57 | "links": [ 58 | "@folder/readdir", 59 | "@folder/xdg" 60 | ] 61 | }, 62 | "plugins": [ 63 | "gulp-format-md" 64 | ], 65 | "lint": { 66 | "reflinks": true 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.verb.md: -------------------------------------------------------------------------------- 1 | ## Credit 2 | 3 | This library is very heavily based on [Vinyl][vinyl]. Thank you to the [gulp.js][gulp] team for the inspiration and great ideas behind vinyl. This work is based on that. 4 | 5 | 6 | ## Comparison to Vinyl 7 | 8 | The API for path handling is close enough that you can use Vinyl's docs as a reference. There are only a couple of minor differences: 9 | 10 | 1. Streams are not supported on `file.contents` by default. See how to [add stream support](#how-to-add-stream-support). 11 | 2. Dirent extends Node's native [fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent) class. 12 | 3. You can pass a string or object as the first argument, and optionally a [file type](https://nodejs.org/api/fs.html#fs_file_type_constants) as the second argument. 13 | 14 | 15 | ## Usage 16 | 17 | ```js 18 | const Dirent = require('{%= name %}'); 19 | 20 | // Pass a file path as a string, or an object with properties to add to the dirent 21 | // The following examples are equivalent. When the path is a string, it will 22 | // be converted to an object and set on the dirent.path property. 23 | console.log(new Dirent('some/file/path.txt')); 24 | //=> 25 | console.log(new Dirent({ path: 'some/file/path.txt' })); 26 | //=> 27 | 28 | // assuming __filename is /jonschlinkert/dev/dirent/index.js 29 | const file = new Dirent(__filename); 30 | console.log(file.dirname); //=> /Users/jonschlinkert/dev//dirent 31 | console.log(file.basename); //=> example.js 32 | console.log(file.stem); //=> example 33 | console.log(file.extname); //=> .js 34 | ``` 35 | 36 | ## How to add stream support 37 | 38 | Stream support is added using the [cloneable-readable][] library (this is the same library used by Vinyl). To add support for streams, do the following: 39 | 40 | ```js 41 | const clonable = require('cloneable-readable'); 42 | const Dirent = require('dirent'); 43 | const File = Dirent.create(cloneable); 44 | 45 | const file = new File({ path: 'foo.js' }); 46 | ``` 47 | 48 | 49 | 50 | [gulp]: https://github.com/gulpjs 51 | [vinyl]: https://github.com/gulpjs/vinyl 52 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { getOwnPropertyDescriptor } = Reflect; 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const isObject = value => value !== null && typeof value === 'object'; 8 | const isWindows = process.platform === 'win32'; 9 | 10 | exports.handlers = (Dirent, dirent) => ({ 11 | get(file, prop) { 12 | if (exports.builtinProperties.has(prop)) return file[prop]; 13 | 14 | // use "dirent.constructor" for subclassing 15 | const ctorProtoDesc = getOwnPropertyDescriptor(dirent.constructor.prototype, prop); 16 | if (ctorProtoDesc && typeof ctorProtoDesc.value === 'function' && !(dirent instanceof fs.Dirent)) { 17 | return ctorProtoDesc[prop]; 18 | } 19 | 20 | // then check Dirent 21 | const protoDesc = getOwnPropertyDescriptor(Dirent.prototype, prop); 22 | if (protoDesc && typeof protoDesc.value === 'function') { 23 | return file[prop]; 24 | } 25 | 26 | // then "file" 27 | const fileDesc = getOwnPropertyDescriptor(file.constructor.prototype, prop); 28 | if (fileDesc && typeof fileDesc.value !== 'function') { 29 | return file[prop]; 30 | } 31 | 32 | // then "stat" and instance properties 33 | const objects = [file.stat, dirent, dirent.stat]; 34 | const obj = objects.find(obj => obj && prop in obj) || file; 35 | return obj[prop]; 36 | } 37 | }); 38 | 39 | exports.builtinProperties = new Set(['constructor', 'contents', 'stat', 'history', 'path', 'base', 'cwd']); 40 | 41 | exports.isBuffer = value => { 42 | if (isObject(value) && value.constructor && typeof value.constructor.isBuffer === 'function') { 43 | return value.constructor.isBuffer(value); 44 | } 45 | return false; 46 | }; 47 | 48 | exports.isStream = value => { 49 | if (isObject(value) && typeof value.pipe === 'function') { 50 | return typeof value.on === 'function'; 51 | } 52 | return false; 53 | }; 54 | 55 | exports.removeTrailingSep = str => { 56 | let i = str.length; 57 | while (i > 1 && (str[i - 1] === '/' || (isWindows && str[i - 1] === '\\'))) i--; 58 | return str.slice(0, i); 59 | }; 60 | 61 | exports.normalize = input => { 62 | return input ? path.normalize(input) : ''; 63 | }; 64 | 65 | exports.replaceExtname = (filepath, extname) => { 66 | if (typeof filepath !== 'string') return filepath; 67 | if (filepath === '') return filepath; 68 | 69 | const { dir, name } = path.parse(filepath); 70 | 71 | if (!extname.startsWith('.')) { 72 | extname = `.${extname}`; 73 | } 74 | 75 | return path.join(dir, `${name}${extname}`); 76 | }; 77 | 78 | exports.pathError = (prop, key = 'set') => { 79 | return `Expected "file.path" to be a string, cannot ${key} "file.${prop}".`; 80 | }; 81 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to dirent 2 | 3 | First and foremost, thank you! We appreciate that you want to contribute to dirent, your time is valuable, and your contributions mean a lot to us. 4 | 5 | **What does "contributing" mean?** 6 | 7 | Creating an issue is the simplest form of contributing to a project. But there are many ways to contribute, including the following: 8 | 9 | - Updating or correcting documentation 10 | - Feature requests 11 | - Bug reports 12 | 13 | If you'd like to learn more about contributing in general, the [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing) has a lot of useful information. 14 | 15 | **Showing support for dirent** 16 | 17 | Please keep in mind that open source software is built by people like you, who spend their free time creating things the rest the community can use. 18 | 19 | Don't have time to contribute? No worries, here are some other ways to show your support for dirent: 20 | 21 | - star the [project](https://github.com/folder/dirent) 22 | - tweet your support for dirent 23 | 24 | ## Issues 25 | 26 | ### Before creating an issue 27 | 28 | Please try to determine if the issue is caused by an underlying library, and if so, create the issue there. Sometimes this is difficult to know. We only ask that you attempt to give a reasonable attempt to find out. Oftentimes the readme will have advice about where to go to create issues. 29 | 30 | Try to follow these guidelines 31 | 32 | - **Investigate the issue**: 33 | - **Check the readme** - oftentimes you will find notes about creating issues, and where to go depending on the type of issue. 34 | - Create the issue in the appropriate repository. 35 | 36 | ### Creating an issue 37 | 38 | Please be as descriptive as possible when creating an issue. Give us the information we need to successfully answer your question or address your issue by answering the following in your issue: 39 | 40 | - **version**: please note the version of dirent are you using 41 | - **extensions, plugins, helpers, etc** (if applicable): please list any extensions you're using 42 | - **error messages**: please paste any error messages into the issue, or a [gist](https://gist.github.com/) 43 | 44 | ## Above and beyond 45 | 46 | Here are some tips for creating idiomatic issues. Taking just a little bit extra time will make your issue easier to read, easier to resolve, more likely to be found by others who have the same or similar issue in the future. 47 | 48 | - read the [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing) 49 | - take some time to learn basic markdown. This [markdown cheatsheet](https://gist.github.com/jonschlinkert/5854601) is super helpful, as is the GitHub guide to [basic markdown](https://help.github.com/articles/markdown-basics/). 50 | - Learn about [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). And if you want to really go above and beyond, read [mastering markdown](https://guides.github.com/features/mastering-markdown/). 51 | - use backticks to wrap code. This ensures that code will retain its format, making it much more readable to others 52 | - use syntax highlighting by adding the correct language name after the first "code fence" 53 | 54 | 55 | [so]: http://stackoverflow.com/questions/tagged/dirent 56 | -------------------------------------------------------------------------------- /.github/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | 6 | "env": { 7 | "es2021": true, 8 | "node": true 9 | }, 10 | 11 | "parserOptions": { 12 | "ecmaVersion": 12 13 | }, 14 | 15 | "rules": { 16 | "accessor-pairs": 2, 17 | "arrow-parens": [2, "as-needed"], 18 | "arrow-spacing": [2, { "before": true, "after": true }], 19 | "block-spacing": [2, "always"], 20 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 21 | "comma-dangle": [2, "never"], 22 | "comma-spacing": [2, { "before": false, "after": true }], 23 | "comma-style": [2, "last"], 24 | "constructor-super": 2, 25 | "curly": [2, "multi-line"], 26 | "dot-location": [2, "property"], 27 | "eol-last": 2, 28 | "eqeqeq": [2, "allow-null"], 29 | "generator-star-spacing": [2, { "before": true, "after": true }], 30 | "handle-callback-err": [2, "^(err|error)$"], 31 | "indent": [2, 2, { "SwitchCase": 1 }], 32 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 33 | "keyword-spacing": [2, { "before": true, "after": true }], 34 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 35 | "new-parens": 2, 36 | "no-array-constructor": 2, 37 | "no-caller": 2, 38 | "no-class-assign": 2, 39 | "no-cond-assign": 2, 40 | "no-const-assign": 2, 41 | "no-control-regex": 2, 42 | "no-debugger": 2, 43 | "no-delete-var": 2, 44 | "no-dupe-args": 2, 45 | "no-dupe-class-members": 2, 46 | "no-dupe-keys": 2, 47 | "no-duplicate-case": 2, 48 | "no-empty-character-class": 2, 49 | "no-eval": 2, 50 | "no-ex-assign": 2, 51 | "no-extend-native": 2, 52 | "no-extra-bind": 2, 53 | "no-extra-boolean-cast": 2, 54 | "no-extra-parens": [2, "functions"], 55 | "no-fallthrough": 2, 56 | "no-floating-decimal": 2, 57 | "no-func-assign": 2, 58 | "no-implied-eval": 2, 59 | "no-implicit-coercion": 2, 60 | "no-inner-declarations": [2, "functions"], 61 | "no-invalid-regexp": 2, 62 | "no-irregular-whitespace": 2, 63 | "no-iterator": 2, 64 | "no-label-var": 2, 65 | "no-labels": 2, 66 | "no-lone-blocks": 2, 67 | "no-lonely-if": 2, 68 | "no-mixed-spaces-and-tabs": 2, 69 | "no-multi-spaces": 0, 70 | "no-multi-str": 2, 71 | "no-multiple-empty-lines": [2, { "max": 1 }], 72 | "no-native-reassign": 2, 73 | "no-negated-in-lhs": 2, 74 | "no-new": 2, 75 | "no-new-func": 2, 76 | "no-new-object": 2, 77 | "no-new-require": 2, 78 | "no-new-wrappers": 2, 79 | "no-obj-calls": 2, 80 | "no-octal": 2, 81 | "no-octal-escape": 2, 82 | "no-proto": 2, 83 | "no-redeclare": 2, 84 | "no-regex-spaces": 2, 85 | "no-return-assign": 2, 86 | "no-self-compare": 2, 87 | "no-sequences": 2, 88 | "no-shadow-restricted-names": 2, 89 | "no-spaced-func": 2, 90 | "no-sparse-arrays": 2, 91 | "no-this-before-super": 2, 92 | "no-throw-literal": 2, 93 | "no-trailing-spaces": 2, 94 | "no-undef": 2, 95 | "no-undef-init": 2, 96 | "no-unexpected-multiline": 2, 97 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 98 | "no-unreachable": 2, 99 | "no-unused-expressions": 2, 100 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 101 | "no-useless-call": 2, 102 | "no-with": 2, 103 | "object-curly-spacing": ["error", "always", { "objectsInObjects": true }], 104 | "one-var": [2, { "initialized": "never" }], 105 | "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], 106 | "padded-blocks": [0, "never"], 107 | "prefer-const": [2, { "destructuring": "all", "ignoreReadBeforeAssign": false }], 108 | "quotes": [2, "single", "avoid-escape"], 109 | "radix": 2, 110 | "semi": [2, "always"], 111 | "semi-spacing": [2, { "before": false, "after": true }], 112 | "space-before-blocks": [2, "always"], 113 | "space-before-function-paren": [2, { "anonymous": "never", "named": "never", "asyncArrow": "always" }], 114 | "space-in-parens": [2, "never"], 115 | "space-infix-ops": 2, 116 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 117 | "spaced-comment": [0, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 118 | "strict": 2, 119 | "use-isnan": 2, 120 | "valid-typeof": 2, 121 | "wrap-iife": [2, "any"], 122 | "yoda": [2, "never"] 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/Dirent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const fs = require('fs'); 5 | const assert = require('assert').strict; 6 | const cloneable = require('cloneable-readable'); 7 | const concat = require('concat-stream'); 8 | const from = require('from2'); 9 | const pipe = require('pump'); 10 | const Dirent = require('..'); 11 | 12 | describe('Dirent', () => { 13 | it('should create a Dirent instance', () => { 14 | assert(new Dirent() instanceof Dirent); 15 | }); 16 | 17 | it('should add stream support', cb => { 18 | const options = { 19 | cwd: '/', 20 | base: '/test/', 21 | path: '/test/test.coffee', 22 | contents: from(['wa', 'dup']) 23 | }; 24 | 25 | const File = Dirent.create(cloneable); 26 | const file = new File(options); 27 | assert.equal(file.isStream(), true); 28 | 29 | const file2 = file.clone(); 30 | 31 | assert(file2 !== file); 32 | assert.equal(file2.cwd, file.cwd); 33 | assert.equal(file2.base, file.base); 34 | assert.equal(file2.path, file.path); 35 | assert(file2.contents !== file.contents); 36 | 37 | let ends = 2; 38 | let data = null; 39 | let output = null; 40 | 41 | function compare(err) { 42 | if (err) { 43 | cb(err); 44 | return; 45 | } 46 | 47 | if (--ends === 0) { 48 | assert(data !== output); 49 | assert.equal(data.toString(), output.toString()); 50 | cb(); 51 | } 52 | } 53 | 54 | pipe([ 55 | file.contents, 56 | concat(function(d) { 57 | data = d; 58 | }) 59 | ], compare); 60 | 61 | pipe([ 62 | file2.contents, 63 | concat(function(d) { 64 | output = d; 65 | }) 66 | ], compare); 67 | }); 68 | 69 | it('should set dirent.path when value is an object', () => { 70 | const dirent = new Dirent({ path: __filename }); 71 | assert.equal(dirent.path, __filename); 72 | }); 73 | 74 | it('should return false on all `is*` methods by default', () => { 75 | const dirent = new Dirent({ path: __filename }); 76 | assert.equal(dirent.isDirectory(), false); 77 | assert.equal(dirent.isFile(), false); 78 | assert.equal(dirent.isBlockDevice(), false); 79 | assert.equal(dirent.isCharacterDevice(), false); 80 | assert.equal(dirent.isSymbolicLink(), false); 81 | assert.equal(dirent.isFIFO(), false); 82 | assert.equal(dirent.isSocket(), false); 83 | }); 84 | 85 | it('should accept `type` integer as the second argument', () => { 86 | assert.equal(new Dirent({ path: __dirname }, 2).isFile(), false); 87 | assert.equal(new Dirent({ path: __dirname }, 2).isDirectory(), true); 88 | assert.equal(new Dirent({ path: __filename }, 1).isDirectory(), false); 89 | assert.equal(new Dirent({ path: __filename }, 1).isFile(), true); 90 | }); 91 | 92 | it('should get type from dirent.stat object passed on constructor', () => { 93 | const fileStat = fs.statSync(__filename); 94 | const dirStat = fs.statSync(__dirname); 95 | 96 | assert.equal(new Dirent({ path: __dirname }).isDirectory(), false); 97 | assert.equal(new Dirent({ path: __filename }).isFile(), false); 98 | 99 | assert.equal(new Dirent({ path: __filename, stat: dirStat }).isDirectory(), true); 100 | assert.equal(new Dirent({ path: __dirname, stat: fileStat }).isFile(), true); 101 | }); 102 | 103 | it('should call type function from dirent.stat object when set directly', () => { 104 | const dirStat = fs.statSync(__dirname); 105 | const fileStat = fs.statSync(__filename); 106 | 107 | const dir = new Dirent({ path: __dirname }); 108 | const file = new Dirent({ path: __filename }); 109 | 110 | assert.equal(dir.isDirectory(), false); 111 | assert.equal(file.isFile(), false); 112 | 113 | dir.stat = dirStat; 114 | file.stat = fileStat; 115 | 116 | assert.equal(dir.isDirectory(), true); 117 | assert.equal(file.isFile(), true); 118 | }); 119 | 120 | it('should get `mode` from dirent.stat object when set directly', () => { 121 | const dirStat = fs.statSync(__dirname); 122 | const fileStat = fs.statSync(__filename); 123 | 124 | const dir = new Dirent({ path: __dirname }); 125 | const file = new Dirent({ path: __filename }); 126 | 127 | assert.equal(dir.mode, undefined); 128 | assert.equal(file.mode, undefined); 129 | 130 | dir.stat = dirStat; 131 | file.stat = fileStat; 132 | 133 | assert.equal(dir.mode, dir.stat.mode); 134 | assert.equal(file.mode, file.stat.mode); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dirent [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/jonathanschlinkert?locale.x=en_US) [![NPM version](https://img.shields.io/npm/v/dirent.svg?style=flat)](https://www.npmjs.com/package/dirent) [![NPM monthly downloads](https://img.shields.io/npm/dm/dirent.svg?style=flat)](https://npmjs.org/package/dirent) [![NPM total downloads](https://img.shields.io/npm/dt/dirent.svg?style=flat)](https://npmjs.org/package/dirent) [![Tests](https://github.com/folder/dirent/actions/workflows/test.yml/badge.svg)](https://github.com/folder/dirent/actions/workflows/test.yml) 2 | 3 | > Virtual file class. Extends the native fs.Dirent class with methods to simplify path handling. Similar to Vinyl, but lightweight. 4 | 5 | Please consider following this project's author, [Jon Schlinkert](https://github.com/jonschlinkert), and consider starring the project to show your :heart: and support. 6 | 7 | ## Install 8 | 9 | Install with [npm](https://www.npmjs.com/) (requires [Node.js](https://nodejs.org/en/) >=10): 10 | 11 | ```sh 12 | $ npm install --save dirent 13 | ``` 14 | 15 | ## Credit 16 | 17 | This library is very heavily based on [Vinyl](https://github.com/gulpjs/vinyl). Thank you to the [gulp.js](https://github.com/gulpjs) team for the inspiration and great ideas behind vinyl. This work is based on that. 18 | 19 | ## Comparison to Vinyl 20 | 21 | The API for path handling is close enough that you can use Vinyl's docs as a reference. There are only a couple of minor differences: 22 | 23 | 1. Streams are not supported on `file.contents` by default. See how to [add stream support](#how-to-add-stream-support). 24 | 2. Dirent extends Node's native [fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent) class. 25 | 3. You can pass a string or object as the first argument, and optionally a [file type](https://nodejs.org/api/fs.html#fs_file_type_constants) as the second argument. 26 | 27 | ## Usage 28 | 29 | ```js 30 | const Dirent = require('dirent'); 31 | 32 | // Pass a file path as a string, or an object with properties to add to the dirent 33 | // The following examples are equivalent. When the path is a string, it will 34 | // be converted to an object and set on the dirent.path property. 35 | console.log(new Dirent('some/file/path.txt')); 36 | //=> 37 | console.log(new Dirent({ path: 'some/file/path.txt' })); 38 | //=> 39 | 40 | // assuming __filename is /jonschlinkert/dev/dirent/index.js 41 | const file = new Dirent(__filename); 42 | console.log(file.dirname); //=> /Users/jonschlinkert/dev//dirent 43 | console.log(file.basename); //=> example.js 44 | console.log(file.stem); //=> example 45 | console.log(file.extname); //=> .js 46 | ``` 47 | 48 | ## How to add stream support 49 | 50 | Stream support is added using the [cloneable-readable](https://github.com/mcollina/cloneable-readable) library (this is the same library used by Vinyl). To add support for streams, do the following: 51 | 52 | ```js 53 | const clonable = require('cloneable-readable'); 54 | const Dirent = require('dirent'); 55 | const File = Dirent.create(cloneable); 56 | 57 | const file = new File({ path: 'foo.js' }); 58 | ``` 59 | 60 | ## About 61 | 62 |
63 | Contributing 64 | 65 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). 66 | 67 | Please read the [contributing guide](.github/contributing.md) for advice on opening issues, pull requests, and coding standards. 68 | 69 |
70 | 71 |
72 | Running Tests 73 | 74 | Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command: 75 | 76 | ```sh 77 | $ npm install && npm test 78 | ``` 79 | 80 |
81 | 82 |
83 | Building docs 84 | 85 | _(This project's readme.md is generated by [verb](https://github.com/verbose/verb-generate-readme), please don't edit the readme directly. Any changes to the readme must be made in the [.verb.md](.verb.md) readme template.)_ 86 | 87 | To generate the readme, run the following command: 88 | 89 | ```sh 90 | $ npm install -g verbose/verb#dev verb-generate-readme && verb 91 | ``` 92 | 93 |
94 | 95 | ### Contributors 96 | 97 | | **Commits** | **Contributor** | 98 | | --- | --- | 99 | | 25 | [jonschlinkert](https://github.com/jonschlinkert) | 100 | | 2 | [doowb](https://github.com/doowb) | 101 | 102 | ### Author 103 | 104 | **Jon Schlinkert** 105 | 106 | * [GitHub Profile](https://github.com/jonschlinkert) 107 | * [Twitter Profile](https://twitter.com/jonschlinkert) 108 | * [LinkedIn Profile](https://linkedin.com/in/jonschlinkert) 109 | 110 | ### License 111 | 112 | Copyright © 2021, [Jon Schlinkert](https://github.com/jonschlinkert). 113 | Released under the MIT License. 114 | 115 | *** 116 | 117 | _This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.8.0, on April 18, 2021._ -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const kBase = Symbol('base'); 4 | const kCwd = Symbol('cwd'); 5 | const kContents = Symbol('contents'); 6 | const kSymlink = Symbol('symlink'); 7 | const kDirent = Symbol('dirent'); 8 | 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | const util = require('util'); 12 | const assert = require('assert'); 13 | const clone = require('clone-deep'); 14 | 15 | const { 16 | builtinProperties, 17 | handlers, 18 | isBuffer, 19 | isStream, 20 | normalize, 21 | pathError, 22 | removeTrailingSep, 23 | replaceExtname 24 | } = require('./utils'); 25 | 26 | /** 27 | * Create the Dirent class by optionally passing a clonable stream. 28 | */ 29 | 30 | const create = clonableStream => { 31 | class Dirent extends fs.Dirent { 32 | constructor(dirent = {}, type = '') { 33 | super(null, type, ''); 34 | 35 | if (typeof dirent === 'string') { 36 | dirent = { path: dirent }; 37 | } 38 | 39 | this[kDirent] = dirent; 40 | this.contents = dirent.contents || null; 41 | this.stat = dirent.stat || null; 42 | 43 | // replay history to get path normalization 44 | const history = [].concat(dirent.history || []).concat(dirent.path || []); 45 | this.history = []; 46 | history.forEach(value => { this.path = value; }); 47 | 48 | this.cwd = dirent.cwd || process.cwd(); 49 | this.base = dirent.base; 50 | 51 | for (const key of Object.keys(dirent)) { 52 | if (!builtinProperties.has(key)) { 53 | this[key] = dirent[key]; 54 | } 55 | } 56 | 57 | return new Proxy(this, handlers(Dirent, dirent)); 58 | } 59 | 60 | isDirectory() { 61 | if (!this.isNull()) return false; 62 | if (typeof this[kDirent].isDirectory === 'function') { 63 | return this[kDirent].isDirectory(); 64 | } 65 | if (this.stat && typeof this.stat.isDirectory === 'function') { 66 | return this.stat.isDirectory(); 67 | } 68 | return super.isDirectory(); 69 | } 70 | 71 | // gulp compat 72 | isSymbolic() { 73 | return this.isNull() && this.isSymbolicLink(); 74 | } 75 | // alias for isSymbolicLink 76 | isSymlink() { 77 | return this.isSymbolicLink(); 78 | } 79 | 80 | isBuffer() { 81 | return isBuffer(this.contents); 82 | } 83 | 84 | isStream() { 85 | return isStream(this.contents); 86 | } 87 | 88 | isNull() { 89 | return this.contents === null; 90 | } 91 | 92 | inspect() { 93 | return this[util.inspect.custom](); 94 | } 95 | 96 | clone(options) { 97 | if (typeof options === 'boolean') options = { deep: options }; 98 | const opts = { deep: true, contents: true, ...options }; 99 | 100 | const { base, cwd } = this; 101 | const history = this.history.slice(); 102 | const stat = this.stat ? Object.assign(new fs.Stats(), this.stat) : null; 103 | 104 | const contents = (opts.contents && this.isBuffer()) 105 | ? Buffer.from(this.contents) 106 | : ((this.isStream() && this.contents.clone) ? this.contents.clone() : this.contents); 107 | 108 | const file = new this.constructor({ cwd, base, stat, history, contents }); 109 | 110 | for (const key of Object.keys(this)) { 111 | if (!builtinProperties.has(key)) { 112 | file[key] = opts.deep ? clone(this[key], true) : this[key]; 113 | } 114 | } 115 | 116 | return file; 117 | } 118 | 119 | set contents(value) { 120 | assert(this.constructor.isValidContents(value), 'Expected file.contents to be a Buffer, Stream, or null.'); 121 | if (clonableStream && isStream(value) && !clonableStream.isCloneable(value)) { 122 | value = clonableStream(value); 123 | } 124 | this[kContents] = value; 125 | } 126 | get contents() { 127 | return this[kContents]; 128 | } 129 | 130 | set cwd(cwd) { 131 | assert(cwd && typeof cwd === 'string', 'file.cwd should be a non-empty string'); 132 | this[kCwd] = removeTrailingSep(normalize(cwd)); 133 | } 134 | get cwd() { 135 | return this[kCwd]; 136 | } 137 | 138 | set base(value) { 139 | if (value == null) { 140 | delete this[kBase]; 141 | return; 142 | } 143 | assert(value && typeof value === 'string', 'file.base should be a non-empty string, null, or undefined'); 144 | value = removeTrailingSep(normalize(value)); 145 | if (value === this[kCwd]) { 146 | delete this[kBase]; 147 | return; 148 | } 149 | this[kBase] = value; 150 | } 151 | get base() { 152 | return this[kBase] || this[kCwd]; 153 | } 154 | 155 | set dirname(value) { 156 | assert(this.path, pathError('dirname', 'set')); 157 | this.path = path.join(value, this.basename); 158 | } 159 | get dirname() { 160 | assert(this.path, pathError('dirname', 'get')); 161 | return path.dirname(this.path); 162 | } 163 | 164 | set basename(value) { 165 | assert(this.path, pathError('basename', 'set')); 166 | this.path = path.join(this.dirname, value); 167 | } 168 | get basename() { 169 | assert(this.path, pathError('basename', 'get')); 170 | return path.basename(this.path); 171 | } 172 | 173 | set name(value) { 174 | if (value && typeof value === 'string') { 175 | this.basename = value; 176 | } 177 | } 178 | get name() { 179 | return this.basename; 180 | } 181 | 182 | set stem(value) { 183 | assert(this.path, pathError('stem', 'set')); 184 | this.path = path.join(this.dirname, value + this.extname); 185 | } 186 | get stem() { 187 | assert(this.path, pathError('stem', 'get')); 188 | return path.basename(this.path, this.extname); 189 | } 190 | 191 | set extname(value) { 192 | assert(this.path, pathError('extname', 'set')); 193 | this.path = replaceExtname(this.path, value); 194 | } 195 | get extname() { 196 | assert(this.path, pathError('extname', 'get')); 197 | return path.extname(this.path); 198 | } 199 | 200 | set symlink(value) { 201 | assert(typeof value === 'string', 'Expected "file.symlink" to be a string.'); 202 | this[kSymlink] = removeTrailingSep(normalize(value)); 203 | } 204 | get symlink() { 205 | return this[kSymlink] || null; 206 | } 207 | 208 | set path(value) { 209 | assert(typeof value === 'string', 'Expected "file.path" to be a string.'); 210 | value = removeTrailingSep(normalize(value)); 211 | if (value && value !== this.path) { 212 | this.history.push(value); 213 | } 214 | } 215 | get path() { 216 | return this.history[this.history.length - 1] || null; 217 | } 218 | 219 | set absolute(value) { 220 | throw new Error('"file.absolute" is a getter and may not be defined.'); 221 | } 222 | get absolute() { 223 | assert(this.path, pathError('absolute', 'get')); 224 | return path.resolve(this.cwd, this.path); 225 | } 226 | 227 | set relative(value) { 228 | throw new Error('"file.relative" is a getter and may not be defined.'); 229 | } 230 | get relative() { 231 | assert(this.path, pathError('relative', 'get')); 232 | return path.relative(this.base, this.absolute); 233 | } 234 | 235 | set realpath(value) { 236 | throw new Error('"file.realpath" is a getter and may not be defined.'); 237 | } 238 | get realpath() { 239 | assert(this.path, pathError('realpath', 'get')); 240 | try { 241 | return fs.realpathSync(this.absolute); 242 | } catch (err) { 243 | /* ignore error */ 244 | return null; 245 | } 246 | } 247 | 248 | [util.inspect.custom]() { 249 | const filepath = this.path ? (!this.relative.includes('../') ? this.relative : this.absolute) : null; 250 | const inspect = filepath ? [`"${filepath}"`] : []; 251 | if (this.isBuffer()) { 252 | inspect.push(this.contents.inspect()); 253 | } 254 | if (this.isStream()) { 255 | const name = this.contents.constructor.name; 256 | const suffix = name.endsWith('Stream') ? '' : 'Stream'; 257 | inspect.push(`<${name === 'Class' ? '' : name}${suffix}>`); 258 | } 259 | return ``; 260 | } 261 | 262 | static isValidContents(value) { 263 | return value === null || isBuffer(value) || isStream(value); 264 | } 265 | 266 | static create(clonableStream) { 267 | return create(clonableStream); 268 | } 269 | } 270 | 271 | return Dirent; 272 | }; 273 | 274 | module.exports = create(/*clonableStream*/); 275 | -------------------------------------------------------------------------------- /test/Vinyl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { isPrototypeOf } = Object; 4 | const isWin = process.platform === 'win32'; 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const util = require('util'); 8 | const assert = require('assert').strict; 9 | const concat = require('concat-stream'); 10 | const from = require('from2'); 11 | const pipe = require('pump'); 12 | const File = require('..').create(require('cloneable-readable')); 13 | 14 | /** 15 | * Tests from Vinyl. Most stream tests were removed. 16 | */ 17 | 18 | describe('File', () => { 19 | describe('defaults', () => { 20 | it('defaults cwd to process.cwd', () => { 21 | const file = new File(); 22 | assert.equal(file.cwd, process.cwd()); 23 | }); 24 | 25 | it('defaults base to process.cwd', () => { 26 | const file = new File(); 27 | assert.equal(file.base, process.cwd()); 28 | }); 29 | 30 | it('defaults base to cwd property', () => { 31 | const cwd = path.normalize('/'); 32 | const file = new File({ cwd: cwd }); 33 | assert.equal(file.base, cwd); 34 | }); 35 | 36 | it('defaults path to null', () => { 37 | const file = new File(); 38 | assert.equal(file.path, null); 39 | }); 40 | 41 | it('defaults history to an empty array', () => { 42 | const file = new File(); 43 | assert.equal(file.history.length, 0); 44 | }); 45 | 46 | it('defaults stat to null', () => { 47 | const file = new File(); 48 | assert.equal(file.stat, null); 49 | }); 50 | 51 | it('defaults contents to null', () => { 52 | const file = new File(); 53 | assert.equal(file.contents, null); 54 | }); 55 | }); 56 | 57 | describe('constructor()', () => { 58 | it('sets base', () => { 59 | const val = path.normalize('/'); 60 | const file = new File({ base: val }); 61 | assert.equal(file.base, val); 62 | }); 63 | 64 | it('sets cwd', () => { 65 | const val = path.normalize('/'); 66 | const file = new File({ cwd: val }); 67 | assert.equal(file.cwd, val); 68 | }); 69 | 70 | it('sets path (and history)', () => { 71 | const filepath = path.normalize('/test.coffee'); 72 | const file = new File({ path: filepath }); 73 | assert.equal(file.path, filepath); 74 | assert.equal(file.history[0], filepath); 75 | }); 76 | 77 | it('sets history (and path)', () => { 78 | const val = path.normalize('/test.coffee'); 79 | const file = new File({ history: [val] }); 80 | assert.equal(file.path, val); 81 | assert.equal(file.history[0], val); 82 | }); 83 | 84 | it('sets stat', () => { 85 | const val = {}; 86 | const file = new File({ stat: val }); 87 | assert.equal(file.stat, val); 88 | }); 89 | 90 | it('sets contents', () => { 91 | const val = Buffer.from('test'); 92 | const file = new File({ contents: val }); 93 | assert.equal(file.contents, val); 94 | }); 95 | 96 | it('sets custom properties', () => { 97 | const sourceMap = {}; 98 | const file = new File({ sourceMap }); 99 | assert.equal(file.sourceMap, sourceMap); 100 | }); 101 | 102 | it('normalizes path', () => { 103 | const val = '/test/foo/../test.coffee'; 104 | const expected = path.normalize(val); 105 | const file = new File({ path: val }); 106 | 107 | assert.equal(file.path, expected); 108 | assert.equal(file.history[0], expected); 109 | }); 110 | 111 | it('normalizes and removes trailing separator from path', () => { 112 | const val = '/test/foo/../foo/'; 113 | const expected = path.normalize(val.slice(0, -1)); 114 | const file = new File({ path: val }); 115 | assert.equal(file.path, expected); 116 | }); 117 | 118 | it('normalizes history', () => { 119 | const val = [ 120 | '/test/bar/../bar/test.coffee', 121 | '/test/foo/../test.coffee' 122 | ]; 123 | 124 | const expected = val.map(fp => path.normalize(fp)); 125 | const file = new File({ history: val }); 126 | 127 | assert.equal(file.path, expected[1]); 128 | assert.equal(file.history[0], expected[0]); 129 | }); 130 | 131 | it('normalizes and removes trailing separator from history', () => { 132 | const val = [ 133 | '/test/foo/../foo/', 134 | '/test/bar/../bar/' 135 | ]; 136 | 137 | const expected = val.map(fp => path.normalize(fp.slice(0, -1))); 138 | const file = new File({ history: val }); 139 | assert.equal(file.history.join(''), expected.join('')); 140 | }); 141 | 142 | it('appends path to history if both exist and different from last', () => { 143 | const val = path.normalize('/test/baz/test.coffee'); 144 | const history = [ 145 | path.normalize('/test/bar/test.coffee'), 146 | path.normalize('/test/foo/test.coffee') 147 | ]; 148 | const file = new File({ path: val, history: history }); 149 | 150 | const expectedHistory = history.concat(val); 151 | 152 | assert.equal(file.path, val); 153 | assert.equal(file.history.join(''), expectedHistory.join('')); 154 | }); 155 | 156 | it('does not append path to history if both exist and same as last', () => { 157 | const val = path.normalize('/test/baz/test.coffee'); 158 | const history = [ 159 | path.normalize('/test/bar/test.coffee'), 160 | path.normalize('/test/foo/test.coffee'), 161 | val 162 | ]; 163 | 164 | const file = new File({ path: val, history: history }); 165 | assert.equal(file.path, val); 166 | assert.equal(file.history.join(''), history.join('')); 167 | }); 168 | 169 | it('does not mutate history array passed in', () => { 170 | const val = path.normalize('/test/baz/test.coffee'); 171 | const history = [ 172 | path.normalize('/test/bar/test.coffee'), 173 | path.normalize('/test/foo/test.coffee') 174 | ]; 175 | 176 | const historyCopy = [].concat(history); 177 | const file = new File({ path: val, history }); 178 | 179 | const expectedHistory = history.concat(val); 180 | 181 | assert.equal(file.path, val); 182 | assert.equal(file.history.join(''), expectedHistory.join('')); 183 | assert.equal(history.join(''), historyCopy.join('')); 184 | }); 185 | }); 186 | 187 | describe('isBuffer()', () => { 188 | it('returns true when the contents are a Buffer', () => { 189 | const val = Buffer.from('test'); 190 | const file = new File({ contents: val }); 191 | assert.equal(file.isBuffer(), true); 192 | }); 193 | 194 | it('returns false when the contents are a Stream', () => { 195 | const val = from([]); 196 | const file = new File({ contents: val }); 197 | assert.equal(file.isBuffer(), false); 198 | }); 199 | 200 | it('returns false when the contents are null', () => { 201 | const file = new File({ contents: null }); 202 | assert.equal(file.isBuffer(), false); 203 | }); 204 | }); 205 | 206 | describe('isStream()', () => { 207 | it('returns false when the contents are a Buffer', () => { 208 | const val = Buffer.from('test'); 209 | const file = new File({ contents: val }); 210 | assert.equal(file.isStream(), false); 211 | }); 212 | 213 | it('returns true when the contents are a Stream', () => { 214 | const val = from([]); 215 | const file = new File({ contents: val }); 216 | assert.equal(file.isStream(), true); 217 | }); 218 | 219 | it('returns false when the contents are null', () => { 220 | const file = new File({ contents: null }); 221 | assert.equal(file.isStream(), false); 222 | }); 223 | }); 224 | 225 | describe('isNull()', () => { 226 | it('returns false when the contents are a Buffer', () => { 227 | const val = Buffer.from('test'); 228 | const file = new File({ contents: val }); 229 | assert.equal(file.isNull(), false); 230 | }); 231 | 232 | it('returns false when the contents are a Stream', () => { 233 | const val = from([]); 234 | const file = new File({ contents: val }); 235 | assert.equal(file.isNull(), false); 236 | }); 237 | 238 | it('returns true when the contents are null', () => { 239 | const file = new File({ contents: null }); 240 | assert.equal(file.isNull(), true); 241 | }); 242 | }); 243 | 244 | describe('isDirectory()', () => { 245 | const fakeStat = { 246 | isDirectory() { 247 | return true; 248 | } 249 | }; 250 | 251 | it('returns false when the contents are a Buffer', () => { 252 | const contents = Buffer.from('test'); 253 | const file = new File({ contents, stat: fakeStat }); 254 | assert.equal(file.isDirectory(), false); 255 | }); 256 | 257 | it('returns false when the contents are a Stream', () => { 258 | const file = new File({ contents: from([]), stat: fakeStat }); 259 | assert.equal(file.isDirectory(), false); 260 | }); 261 | 262 | it('returns true when the contents are null & stat.isDirectory is true', () => { 263 | const file = new File({ contents: null, stat: fakeStat }); 264 | assert.equal(file.isDirectory(), true); 265 | }); 266 | 267 | it('returns false when stat exists but does not contain an isDirectory method', () => { 268 | const file = new File({ contents: null, stat: {} }); 269 | assert.equal(file.isDirectory(), false); 270 | }); 271 | 272 | it('returns false when stat does not exist', () => { 273 | const file = new File({ contents: null }); 274 | assert.equal(file.isDirectory(), false); 275 | }); 276 | }); 277 | 278 | describe('isSymbolic()', () => { 279 | const fakeStat = { 280 | isSymbolicLink() { 281 | return true; 282 | } 283 | }; 284 | 285 | it('returns false when the contents are a Buffer', () => { 286 | const val = Buffer.from('test'); 287 | const file = new File({ contents: val, stat: fakeStat }); 288 | assert.equal(file.isSymbolic(), false); 289 | }); 290 | 291 | it('returns false when the contents are a Stream', () => { 292 | const val = from([]); 293 | const file = new File({ contents: val, stat: fakeStat }); 294 | assert.equal(file.isSymbolic(), false); 295 | }); 296 | 297 | it('returns true when the contents are null & stat.isSymbolicLink is true', () => { 298 | const file = new File({ contents: null, stat: fakeStat }); 299 | assert.equal(file.isSymbolic(), true); 300 | }); 301 | 302 | it('returns false when stat exists but does not contain an isSymbolicLink method', () => { 303 | const file = new File({ contents: null, stat: {} }); 304 | assert.equal(file.isSymbolic(), false); 305 | }); 306 | 307 | it('returns false when stat does not exist', () => { 308 | const file = new File({ contents: null }); 309 | assert.equal(file.isSymbolic(), false); 310 | }); 311 | }); 312 | 313 | describe('inspect()', () => { 314 | it('returns correct format when no contents and no path', () => { 315 | const file = new File(); 316 | const expectation = ''; 317 | assert.equal(file.inspect(), expectation); 318 | assert.equal(util.inspect(file), expectation); 319 | if (util.inspect.custom) { 320 | assert.equal(file[util.inspect.custom](), expectation); 321 | } 322 | }); 323 | 324 | it('returns correct format when Buffer contents and no path', () => { 325 | const val = Buffer.from('test'); 326 | const file = new File({ contents: val }); 327 | assert.equal(file.inspect(), '>'); 328 | }); 329 | 330 | it('returns correct format when Buffer contents and relative path', () => { 331 | const val = Buffer.from('test'); 332 | const file = new File({ 333 | cwd: '/', 334 | base: '/test/', 335 | path: '/test/test.coffee', 336 | contents: val 337 | }); 338 | assert.equal(file.inspect(), '>'); 339 | }); 340 | 341 | it('returns correct format when Stream contents and relative path', () => { 342 | const file = new File({ 343 | cwd: '/', 344 | base: '/test/', 345 | path: '/test/test.coffee', 346 | contents: from([]) 347 | }); 348 | 349 | assert.equal(file.inspect(), '>'); 350 | }); 351 | 352 | it('returns correct format when null contents and relative path', () => { 353 | const file = new File({ 354 | cwd: '/', 355 | base: '/test/', 356 | path: '/test/test.coffee', 357 | contents: null 358 | }); 359 | 360 | assert.equal(file.inspect(), ''); 361 | }); 362 | }); 363 | 364 | describe('cwd get/set', () => { 365 | it('gets and sets cwd', () => { 366 | const val = '/test'; 367 | const file = new File(); 368 | file.cwd = val; 369 | assert.equal(file.cwd, isWin ? '\\test' : val); 370 | }); 371 | 372 | it('normalizes and removes trailing separator on set', () => { 373 | const val = '/test/foo/../foo/'; 374 | const expected = path.normalize(val.slice(0, -1)); 375 | const file = new File(); 376 | 377 | file.cwd = val; 378 | 379 | assert.equal(file.cwd, expected); 380 | 381 | const val2 = '\\test\\foo\\..\\foo\\'; 382 | const expected2 = path.normalize(isWin ? val2.slice(0, -1) : val2); 383 | 384 | file.cwd = val2; 385 | assert.equal(file.cwd, expected2); 386 | }); 387 | 388 | it('throws on set with invalid values', () => { 389 | const invalidValues = [ 390 | '', 391 | null, 392 | undefined, 393 | true, 394 | false, 395 | 0, 396 | Infinity, 397 | NaN, 398 | {}, 399 | [] 400 | ]; 401 | const file = new File(); 402 | 403 | invalidValues.forEach(function(val) { 404 | function invalid() { 405 | file.cwd = val; 406 | } 407 | assert.throws(invalid, 'cwd must be a non-empty string.'); 408 | }); 409 | 410 | }); 411 | }); 412 | 413 | describe('base get/set', () => { 414 | 415 | it('proxies cwd when omitted', () => { 416 | const file = new File({ cwd: '/test' }); 417 | assert.equal(file.base, file.cwd); 418 | }); 419 | 420 | it('proxies cwd when same', () => { 421 | const file = new File({ 422 | cwd: '/test', 423 | base: '/test' 424 | }); 425 | file.cwd = '/foo/'; 426 | assert.equal(file.base, file.cwd); 427 | 428 | const file2 = new File({ 429 | cwd: '/test' 430 | }); 431 | file2.base = '/test/'; 432 | file2.cwd = '/foo/'; 433 | assert.equal(file2.base, file.cwd); 434 | }); 435 | 436 | it('proxies to cwd when set to same value', () => { 437 | const file = new File({ 438 | cwd: '/foo', 439 | base: '/bar' 440 | }); 441 | assert(file.base !== file.cwd); 442 | file.base = file.cwd; 443 | assert.equal(file.base, file.cwd); 444 | }); 445 | 446 | it('proxies to cwd when null or undefined', () => { 447 | const file = new File({ 448 | cwd: '/foo', 449 | base: '/bar' 450 | }); 451 | assert(file.base !== file.cwd); 452 | file.base = null; 453 | assert.equal(file.base, file.cwd); 454 | file.base = '/bar/'; 455 | assert(file.base !== file.cwd); 456 | file.base = undefined; 457 | assert.equal(file.base, file.cwd); 458 | }); 459 | 460 | it('returns base', () => { 461 | const base = '/test/'; 462 | const file = new File(); 463 | file.base = base; 464 | assert.equal(file.base, isWin ? '\\test' : '/test'); 465 | }); 466 | 467 | it('sets base', () => { 468 | const val = '/test/foo'; 469 | const file = new File(); 470 | file.base = val; 471 | assert.equal(file.base, path.normalize(val)); 472 | }); 473 | 474 | it('normalizes and removes trailing separator on set', () => { 475 | const val = '/test/foo/../foo/'; 476 | const expected = path.normalize(val.slice(0, -1)); 477 | const file = new File(); 478 | 479 | file.base = val; 480 | 481 | assert.equal(file.base, expected); 482 | 483 | const val2 = '\\test\\foo\\..\\foo\\'; 484 | const expected2 = path.normalize(isWin ? val2.slice(0, -1) : val2); 485 | 486 | file.base = val2; 487 | 488 | assert.equal(file.base, expected2); 489 | }); 490 | 491 | it('throws on set with invalid values', () => { 492 | const invalidValues = [ 493 | true, 494 | false, 495 | 1, 496 | 0, 497 | Infinity, 498 | NaN, 499 | '', 500 | {}, 501 | [] 502 | ]; 503 | const file = new File(); 504 | 505 | invalidValues.forEach(function(val) { 506 | function invalid() { 507 | file.base = val; 508 | } 509 | assert.throws(invalid, 'base must be a non-empty string, or null/undefined.'); 510 | }); 511 | 512 | }); 513 | }); 514 | 515 | describe('clone()', () => { 516 | it('copies all attributes over with Buffer contents', cb => { 517 | const options = { 518 | cwd: '/', 519 | base: '/test/', 520 | path: '/test/test.coffee', 521 | contents: Buffer.from('test') 522 | }; 523 | const file = new File(options); 524 | const file2 = file.clone(); 525 | 526 | assert(file2 !== file); 527 | assert.equal(file2.cwd, file.cwd); 528 | assert.equal(file2.base, file.base); 529 | assert.equal(file2.path, file.path); 530 | assert(file2.contents !== file.contents); 531 | assert.equal(file2.contents.toString(), file.contents.toString()); 532 | cb(); 533 | }); 534 | 535 | it('assigns Buffer content reference when contents option is false', cb => { 536 | const options = { 537 | cwd: '/', 538 | base: '/test/', 539 | path: '/test/test.js', 540 | contents: Buffer.from('test') 541 | }; 542 | const file = new File(options); 543 | 544 | const copy1 = file.clone({ contents: false }); 545 | assert(copy1.contents, file.contents); 546 | 547 | const copy2 = file.clone(); 548 | assert(copy2.contents !== file.contents); 549 | 550 | const copy3 = file.clone({ contents: 'invalid' }); 551 | assert(copy3.contents !== file.contents); 552 | cb(); 553 | }); 554 | 555 | it('copies all attributes over with Stream contents', cb => { 556 | const options = { 557 | cwd: '/', 558 | base: '/test/', 559 | path: '/test/test.coffee', 560 | contents: from(['wa', 'dup']) 561 | }; 562 | 563 | const file = new File(options); 564 | const file2 = file.clone(); 565 | 566 | assert(file2 !== file); 567 | assert.equal(file2.cwd, file.cwd); 568 | assert.equal(file2.base, file.base); 569 | assert.equal(file2.path, file.path); 570 | assert(file2.contents !== file.contents); 571 | 572 | let ends = 2; 573 | let data = null; 574 | let output = null; 575 | 576 | function compare(err) { 577 | if (err) { 578 | cb(err); 579 | return; 580 | } 581 | 582 | if (--ends === 0) { 583 | assert(data !== output); 584 | assert.equal(data.toString(), output.toString()); 585 | cb(); 586 | } 587 | } 588 | 589 | pipe([ 590 | file.contents, 591 | concat(function(d) { 592 | data = d; 593 | }) 594 | ], compare); 595 | 596 | pipe([ 597 | file2.contents, 598 | concat(function(d) { 599 | output = d; 600 | }) 601 | ], compare); 602 | }); 603 | 604 | it('does not start flowing until all clones flows (data)', cb => { 605 | const options = { 606 | cwd: '/', 607 | base: '/test/', 608 | path: '/test/test.coffee', 609 | contents: from(['wa', 'dup']) 610 | }; 611 | const file = new File(options); 612 | const file2 = file.clone(); 613 | let ends = 2; 614 | 615 | let data = ''; 616 | let output = ''; 617 | 618 | function compare() { 619 | if (--ends === 0) { 620 | assert.equal(data, output); 621 | cb(); 622 | } 623 | } 624 | 625 | // Start flowing file2 626 | file2.contents.on('data', function(chunk) { 627 | output += chunk.toString(); 628 | }); 629 | 630 | process.nextTick(() => { 631 | // Nothing was written yet 632 | assert.equal(data, ''); 633 | assert.equal(output, ''); 634 | 635 | // Starts flowing file 636 | file.contents.on('data', function(chunk) { 637 | data += chunk.toString(); 638 | }); 639 | }); 640 | 641 | file2.contents.on('end', compare); 642 | file.contents.on('end', compare); 643 | }); 644 | 645 | it('does not start flowing until all clones flow (readable)', cb => { 646 | const options = { 647 | cwd: '/', 648 | base: '/test/', 649 | path: '/test/test.coffee', 650 | contents: from(['wa', 'dup']) 651 | }; 652 | const file = new File(options); 653 | const file2 = file.clone(); 654 | 655 | let output = ''; 656 | 657 | function compare(data) { 658 | assert.equal(data.toString(), output); 659 | } 660 | 661 | // Start flowing file2 662 | file2.contents.on('readable', function() { 663 | let chunk; 664 | while ((chunk = this.read()) !== null) { 665 | output += chunk.toString(); 666 | } 667 | }); 668 | 669 | pipe([file.contents, concat(compare)], cb); 670 | }); 671 | 672 | it('copies all attributes over with null contents', cb => { 673 | const options = { 674 | cwd: '/', 675 | base: '/test/', 676 | path: '/test/test.coffee', 677 | contents: null 678 | }; 679 | const file = new File(options); 680 | const file2 = file.clone(); 681 | 682 | assert(file2 !== file); 683 | assert.equal(file2.cwd, file.cwd); 684 | assert.equal(file2.base, file.base); 685 | assert.equal(file2.path, file.path); 686 | assert.equal(file2.contents, null); 687 | cb(); 688 | }); 689 | 690 | it('properly clones the `stat` property', cb => { 691 | const options = { 692 | cwd: '/', 693 | base: '/test/', 694 | path: '/test/test.js', 695 | contents: Buffer.from('test'), 696 | stat: fs.statSync(__filename) 697 | }; 698 | 699 | const file = new File(options); 700 | const copy = file.clone(); 701 | 702 | assert.equal(copy.stat.isFile(), true); 703 | assert.equal(copy.stat.isDirectory(), false); 704 | assert(file.stat instanceof fs.Stats); 705 | assert(copy.stat instanceof fs.Stats); 706 | cb(); 707 | }); 708 | 709 | it('properly clones the `history` property', () => { 710 | const options = { 711 | cwd: path.normalize('/'), 712 | base: path.normalize('/test/'), 713 | path: path.normalize('/test/test.js'), 714 | contents: Buffer.from('test') 715 | }; 716 | 717 | const file = new File(options); 718 | const copy = file.clone(); 719 | 720 | assert.equal(copy.history[0], options.path); 721 | copy.path = 'lol'; 722 | assert(file.path !== copy.path); 723 | }); 724 | 725 | it('copies custom properties', cb => { 726 | const options = { 727 | cwd: '/', 728 | base: '/test/', 729 | path: '/test/test.coffee', 730 | contents: null, 731 | custom: { meta: {} } 732 | }; 733 | 734 | const file = new File(options); 735 | const file2 = file.clone(); 736 | 737 | assert(file2 !== file); 738 | assert.equal(file2.cwd, file.cwd); 739 | assert.equal(file2.base, file.base); 740 | assert.equal(file2.path, file.path); 741 | assert(file2.custom !== file.custom); 742 | assert(file2.custom.meta !== file.custom.meta); 743 | assert.deepEqual(file2.custom, file.custom); 744 | cb(); 745 | }); 746 | 747 | it('copies history', cb => { 748 | const options = { 749 | cwd: '/', 750 | base: '/test/', 751 | path: '/test/test.coffee', 752 | contents: null 753 | }; 754 | const history = [ 755 | path.normalize('/test/test.coffee'), 756 | path.normalize('/test/test.js'), 757 | path.normalize('/test/test-938di2s.js') 758 | ]; 759 | 760 | const file = new File(options); 761 | file.path = history[1]; 762 | file.path = history[2]; 763 | const file2 = file.clone(); 764 | 765 | assert.deepEqual(file2.history, history); 766 | assert(file2.history !== file.history); 767 | assert.equal(file2.path, history[2]); 768 | cb(); 769 | }); 770 | 771 | it('supports deep & shallow copy of all attributes', cb => { 772 | const options = { 773 | cwd: '/', 774 | base: '/test/', 775 | path: '/test/test.coffee', 776 | contents: null, 777 | custom: { meta: {} } 778 | }; 779 | 780 | const file = new File(options); 781 | 782 | const file2 = file.clone(); 783 | assert.equal(file2.custom.toString(), file.custom.toString()); 784 | assert(file2.custom !== file.custom); 785 | assert.deepEqual(file2.custom.meta, file.custom.meta); 786 | assert(file2.custom.meta !== file.custom.meta); 787 | 788 | const file3 = file.clone(true); 789 | assert.deepEqual(file3.custom, file.custom); 790 | assert(file3.custom !== file.custom); 791 | assert.deepEqual(file3.custom.meta, file.custom.meta); 792 | assert(file3.custom.meta !== file.custom.meta); 793 | 794 | const file4 = file.clone({ deep: true }); 795 | assert.deepEqual(file4.custom, file.custom); 796 | assert(file4.custom !== file.custom); 797 | assert.deepEqual(file4.custom.meta, file.custom.meta); 798 | assert(file4.custom.meta !== file.custom.meta); 799 | 800 | const file5 = file.clone(false); 801 | assert.deepEqual(file5.custom, file.custom); 802 | assert(file5.custom, file.custom); 803 | assert.deepEqual(file5.custom.meta, file.custom.meta); 804 | assert(file.custom.meta, file.custom.meta); 805 | 806 | const file6 = file.clone({ deep: false }); 807 | assert.deepEqual(file6.custom, file.custom); 808 | assert(file6.custom, file.custom); 809 | assert.deepEqual(file6.custom.meta, file.custom.meta); 810 | assert(file.custom.meta, file.custom.meta); 811 | 812 | cb(); 813 | }); 814 | 815 | it('supports inheritance', () => { 816 | class ExtendedFile extends File {} 817 | 818 | const file = new ExtendedFile(); 819 | const file2 = file.clone(); 820 | 821 | assert(file2 !== file); 822 | assert(file2.constructor, ExtendedFile); 823 | assert(file2 instanceof ExtendedFile); 824 | assert(file2 instanceof File); 825 | assert.equal(isPrototypeOf.call(ExtendedFile.prototype, file2), true); 826 | assert.equal(isPrototypeOf.call(File.prototype, file2), true); 827 | }); 828 | }); 829 | 830 | describe('relative get/set', () => { 831 | it('throws on set', () => { 832 | const file = new File(); 833 | 834 | function invalid() { 835 | file.relative = 'test'; 836 | } 837 | 838 | assert.throws(invalid, /may not be defined/); 839 | }); 840 | 841 | it('throws on get with no path', () => { 842 | const file = new File(); 843 | 844 | function invalid() { 845 | return file.relative; 846 | } 847 | 848 | assert.throws(invalid, 'No path specified! Cannot get relative.'); 849 | }); 850 | 851 | it('returns a relative path from base', () => { 852 | const file = new File({ 853 | base: '/test/', 854 | path: '/test/test.coffee' 855 | }); 856 | 857 | assert.equal(file.relative, 'test.coffee'); 858 | }); 859 | 860 | it('returns a relative path from cwd', () => { 861 | const file = new File({ 862 | cwd: '/', 863 | path: '/test/test.coffee' 864 | }); 865 | 866 | assert.equal(file.relative, path.normalize('test/test.coffee')); 867 | }); 868 | 869 | it('does not append separator when directory', () => { 870 | const file = new File({ 871 | base: '/test', 872 | path: '/test/foo/bar', 873 | stat: { 874 | isDirectory() { 875 | return true; 876 | } 877 | } 878 | }); 879 | 880 | assert.equal(file.relative, path.normalize('foo/bar')); 881 | }); 882 | 883 | it('does not append separator when symlink', () => { 884 | const file = new File({ 885 | base: '/test', 886 | path: '/test/foo/bar', 887 | stat: { 888 | isSymbolicLink() { 889 | return true; 890 | } 891 | } 892 | }); 893 | 894 | assert.equal(file.relative, path.normalize('foo/bar')); 895 | }); 896 | 897 | it('does not append separator when directory & symlink', () => { 898 | const file = new File({ 899 | base: '/test', 900 | path: '/test/foo/bar', 901 | stat: { 902 | isDirectory() { 903 | return true; 904 | }, 905 | isSymbolicLink() { 906 | return true; 907 | } 908 | } 909 | }); 910 | 911 | assert.equal(file.relative, path.normalize('foo/bar')); 912 | }); 913 | }); 914 | 915 | describe('dirname get/set', () => { 916 | 917 | it('throws on get with no path', () => { 918 | const file = new File(); 919 | 920 | function invalid() { 921 | return file.dirname; 922 | } 923 | 924 | assert.throws(invalid, 'No path specified! Can not get dirname.'); 925 | }); 926 | 927 | it('returns the dirname without trailing separator', () => { 928 | const file = new File({ 929 | cwd: '/', 930 | base: '/test', 931 | path: '/test/test.coffee' 932 | }); 933 | 934 | assert.equal(file.dirname, path.normalize('/test')); 935 | }); 936 | 937 | it('throws on set with no path', () => { 938 | const file = new File(); 939 | 940 | function invalid() { 941 | file.dirname = '/test'; 942 | } 943 | 944 | assert.throws(invalid, 'No path specified! Can not set dirname.'); 945 | }); 946 | 947 | it('replaces the dirname of the path', () => { 948 | const file = new File({ 949 | cwd: '/', 950 | base: '/test/', 951 | path: '/test/test.coffee' 952 | }); 953 | 954 | file.dirname = '/test/foo'; 955 | assert.equal(file.path, path.normalize('/test/foo/test.coffee')); 956 | }); 957 | }); 958 | 959 | describe('basename get/set', () => { 960 | 961 | it('throws on get with no path', () => { 962 | const file = new File(); 963 | 964 | function invalid() { 965 | return file.basename; 966 | } 967 | 968 | assert.throws(invalid, 'No path specified! Can not get basename.'); 969 | }); 970 | 971 | it('returns the basename of the path', () => { 972 | const file = new File({ 973 | cwd: '/', 974 | base: '/test/', 975 | path: '/test/test.coffee' 976 | }); 977 | 978 | assert.equal(file.basename, 'test.coffee'); 979 | }); 980 | 981 | it('does not append trailing separator when directory', () => { 982 | const file = new File({ 983 | path: '/test/foo', 984 | stat: { 985 | isDirectory() { 986 | return true; 987 | } 988 | } 989 | }); 990 | 991 | assert.equal(file.basename, 'foo'); 992 | }); 993 | 994 | it('does not append trailing separator when symlink', () => { 995 | const file = new File({ 996 | path: '/test/foo', 997 | stat: { 998 | isSymbolicLink() { 999 | return true; 1000 | } 1001 | } 1002 | }); 1003 | 1004 | assert.equal(file.basename, 'foo'); 1005 | }); 1006 | 1007 | it('does not append trailing separator when directory & symlink', () => { 1008 | const file = new File({ 1009 | path: '/test/foo', 1010 | stat: { 1011 | isDirectory() { 1012 | return true; 1013 | }, 1014 | isSymbolicLink() { 1015 | return true; 1016 | } 1017 | } 1018 | }); 1019 | 1020 | assert.equal(file.basename, 'foo'); 1021 | }); 1022 | 1023 | it('removes trailing separator', () => { 1024 | const file = new File({ 1025 | path: '/test/foo/' 1026 | }); 1027 | 1028 | assert.equal(file.basename, 'foo'); 1029 | }); 1030 | 1031 | it('removes trailing separator when directory', () => { 1032 | const file = new File({ 1033 | path: '/test/foo/', 1034 | stat: { 1035 | isDirectory() { 1036 | return true; 1037 | } 1038 | } 1039 | }); 1040 | 1041 | assert.equal(file.basename, 'foo'); 1042 | }); 1043 | 1044 | it('removes trailing separator when symlink', () => { 1045 | const file = new File({ 1046 | path: '/test/foo/', 1047 | stat: { 1048 | isSymbolicLink() { 1049 | return true; 1050 | } 1051 | } 1052 | }); 1053 | 1054 | assert.equal(file.basename, 'foo'); 1055 | }); 1056 | 1057 | it('removes trailing separator when directory & symlink', () => { 1058 | const file = new File({ 1059 | path: '/test/foo/', 1060 | stat: { 1061 | isDirectory() { 1062 | return true; 1063 | }, 1064 | isSymbolicLink() { 1065 | return true; 1066 | } 1067 | } 1068 | }); 1069 | 1070 | assert.equal(file.basename, 'foo'); 1071 | }); 1072 | 1073 | it('throws on set with no path', () => { 1074 | const file = new File(); 1075 | 1076 | function invalid() { 1077 | file.basename = 'test.coffee'; 1078 | } 1079 | 1080 | assert.throws(invalid, 'No path specified! Can not set basename.'); 1081 | }); 1082 | 1083 | it('replaces the basename of the path', () => { 1084 | const file = new File({ 1085 | cwd: '/', 1086 | base: '/test/', 1087 | path: '/test/test.coffee' 1088 | }); 1089 | 1090 | file.basename = 'foo.png'; 1091 | assert.equal(file.path, path.normalize('/test/foo.png')); 1092 | }); 1093 | }); 1094 | 1095 | describe('stem get/set', () => { 1096 | 1097 | it('throws on get with no path', () => { 1098 | const file = new File(); 1099 | 1100 | function invalid() { 1101 | return file.stem; 1102 | } 1103 | 1104 | assert.throws(invalid, 'No path specified! Can not get stem.'); 1105 | }); 1106 | 1107 | it('returns the stem of the path', () => { 1108 | const file = new File({ 1109 | cwd: '/', 1110 | base: '/test/', 1111 | path: '/test/test.coffee' 1112 | }); 1113 | 1114 | assert.equal(file.stem, 'test'); 1115 | }); 1116 | 1117 | it('throws on set with no path', () => { 1118 | const file = new File(); 1119 | 1120 | function invalid() { 1121 | file.stem = 'test.coffee'; 1122 | } 1123 | 1124 | assert.throws(invalid, 'No path specified! Can not set stem.'); 1125 | }); 1126 | 1127 | it('replaces the stem of the path', () => { 1128 | const file = new File({ 1129 | cwd: '/', 1130 | base: '/test/', 1131 | path: '/test/test.coffee' 1132 | }); 1133 | 1134 | file.stem = 'foo'; 1135 | assert.equal(file.path, path.normalize('/test/foo.coffee')); 1136 | }); 1137 | }); 1138 | 1139 | describe('extname get/set', () => { 1140 | 1141 | it('throws on get with no path', () => { 1142 | const file = new File(); 1143 | 1144 | function invalid() { 1145 | return file.extname; 1146 | } 1147 | 1148 | assert.throws(invalid, 'No path specified! Can not get extname.'); 1149 | }); 1150 | 1151 | it('returns the extname of the path', () => { 1152 | const file = new File({ 1153 | cwd: '/', 1154 | base: '/test/', 1155 | path: '/test/test.coffee' 1156 | }); 1157 | 1158 | assert.equal(file.extname, '.coffee'); 1159 | }); 1160 | 1161 | it('throws on set with no path', () => { 1162 | const file = new File(); 1163 | 1164 | function invalid() { 1165 | file.extname = '.coffee'; 1166 | } 1167 | 1168 | assert.throws(invalid, 'No path specified! Can not set extname.'); 1169 | }); 1170 | 1171 | it('replaces the extname of the path', () => { 1172 | const file = new File({ 1173 | cwd: '/', 1174 | base: '/test/', 1175 | path: '/test/test.coffee' 1176 | }); 1177 | 1178 | file.extname = '.png'; 1179 | assert.equal(file.path, path.normalize('/test/test.png')); 1180 | }); 1181 | }); 1182 | 1183 | describe('path get/set', () => { 1184 | it('records path in history upon instantiation', () => { 1185 | const file = new File({ 1186 | cwd: '/', 1187 | path: '/test/test.coffee' 1188 | }); 1189 | 1190 | const history = [ 1191 | path.normalize('/test/test.coffee') 1192 | ]; 1193 | 1194 | assert.equal(file.path, history[0]); 1195 | assert.equal(file.history.join(''), history.join('')); 1196 | }); 1197 | 1198 | it('records path in history when set', () => { 1199 | const val = path.normalize('/test/test.js'); 1200 | const file = new File({ 1201 | cwd: '/', 1202 | path: '/test/test.coffee' 1203 | }); 1204 | const history = [ 1205 | path.normalize('/test/test.coffee'), 1206 | val 1207 | ]; 1208 | 1209 | file.path = val; 1210 | assert.equal(file.path, val); 1211 | assert.equal(file.history.join(''), history.join('')); 1212 | 1213 | const val2 = path.normalize('/test/test.es6'); 1214 | history.push(val2); 1215 | 1216 | file.path = val2; 1217 | assert.equal(file.path, val2); 1218 | assert.equal(file.history.join(''), history.join('')); 1219 | }); 1220 | 1221 | it('does not record path in history when set to the current path', () => { 1222 | const val = path.normalize('/test/test.coffee'); 1223 | const file = new File({ 1224 | cwd: '/', 1225 | path: val 1226 | }); 1227 | const history = [ 1228 | val 1229 | ]; 1230 | 1231 | file.path = val; 1232 | file.path = val; 1233 | assert.equal(file.path, val); 1234 | assert.equal(file.history.join(''), history.join('')); 1235 | }); 1236 | 1237 | it('does not record path in history when set to empty string', () => { 1238 | const val = path.normalize('/test/test.coffee'); 1239 | const file = new File({ 1240 | cwd: '/', 1241 | path: val 1242 | }); 1243 | 1244 | const history = [ 1245 | val 1246 | ]; 1247 | 1248 | file.path = ''; 1249 | assert.equal(file.path, val); 1250 | assert.equal(file.history.join(''), history.join('')); 1251 | }); 1252 | 1253 | it('throws on set with null path', () => { 1254 | const file = new File(); 1255 | 1256 | assert.equal(file.path, null); 1257 | assert.equal(file.history.length, 0); 1258 | 1259 | function invalid() { 1260 | file.path = null; 1261 | } 1262 | 1263 | assert.throws(invalid, 'path should be a string.'); 1264 | }); 1265 | 1266 | it('normalizes the path upon set', () => { 1267 | const val = '/test/foo/../test.coffee'; 1268 | const expected = path.normalize(val); 1269 | const file = new File(); 1270 | 1271 | file.path = val; 1272 | 1273 | assert.equal(file.path, expected); 1274 | assert.equal(file.history.join(''), expected); 1275 | }); 1276 | 1277 | it('removes the trailing separator upon set', () => { 1278 | const file = new File(); 1279 | file.path = '/test/'; 1280 | 1281 | assert.equal(file.path, path.normalize('/test')); 1282 | assert.equal(file.history.join(''), path.normalize('/test')); 1283 | }); 1284 | 1285 | it('removes the trailing separator upon set when directory', () => { 1286 | const file = new File({ 1287 | stat: { 1288 | isDirectory() { 1289 | return true; 1290 | } 1291 | } 1292 | }); 1293 | file.path = '/test/'; 1294 | 1295 | assert.equal(file.path, path.normalize('/test')); 1296 | assert.equal(file.history.join(''), path.normalize('/test')); 1297 | }); 1298 | 1299 | it('removes the trailing separator upon set when symlink', () => { 1300 | const file = new File({ 1301 | stat: { 1302 | isSymbolicLink() { 1303 | return true; 1304 | } 1305 | } 1306 | }); 1307 | file.path = '/test/'; 1308 | 1309 | assert.equal(file.path, path.normalize('/test')); 1310 | assert.equal(file.history.join(''), path.normalize('/test')); 1311 | }); 1312 | 1313 | it('removes the trailing separator upon set when directory & symlink', () => { 1314 | const file = new File({ 1315 | stat: { 1316 | isDirectory() { 1317 | return true; 1318 | }, 1319 | isSymbolicLink() { 1320 | return true; 1321 | } 1322 | } 1323 | }); 1324 | file.path = '/test/'; 1325 | 1326 | assert.equal(file.path, path.normalize('/test')); 1327 | assert.equal(file.history.join(''), path.normalize('/test')); 1328 | }); 1329 | }); 1330 | 1331 | describe('symlink get/set', () => { 1332 | it('return null on get with no symlink', () => { 1333 | const file = new File(); 1334 | assert.equal(file.symlink, null); 1335 | }); 1336 | 1337 | it('returns symlink', () => { 1338 | const val = '/test/test.coffee'; 1339 | const file = new File(); 1340 | file.symlink = val; 1341 | 1342 | assert.equal(file.symlink, isWin ? '\\test\\test.coffee' : val); 1343 | }); 1344 | 1345 | it('throws on set with non-string', () => { 1346 | const file = new File(); 1347 | 1348 | function invalid() { 1349 | file.symlink = null; 1350 | } 1351 | 1352 | assert.throws(invalid, 'symlink should be a string'); 1353 | }); 1354 | 1355 | it('sets symlink', () => { 1356 | const val = '/test/test.coffee'; 1357 | const expected = path.normalize(val); 1358 | const file = new File(); 1359 | file.symlink = val; 1360 | 1361 | assert.equal(file.symlink, expected); 1362 | }); 1363 | 1364 | it('allows relative symlink', () => { 1365 | const val = 'test.coffee'; 1366 | const file = new File(); 1367 | file.symlink = val; 1368 | 1369 | assert.equal(file.symlink, val); 1370 | }); 1371 | 1372 | it('normalizes and removes trailing separator upon set', () => { 1373 | const val = '/test/foo/../bar/'; 1374 | const expected = path.normalize(val.slice(0, -1)); 1375 | const file = new File(); 1376 | file.symlink = val; 1377 | 1378 | assert.equal(file.symlink, expected); 1379 | }); 1380 | }); 1381 | }); 1382 | --------------------------------------------------------------------------------