diff --git a/.gitignore b/.gitignore index fdb463b..c6206d7 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ npm-debug.log .directory ._* *.iml +examples/city \ No newline at end of file diff --git a/.thought/partials/api.md.hbs b/.thought/partials/api.md.hbs new file mode 100644 index 0000000..e8a6fe1 --- /dev/null +++ b/.thought/partials/api.md.hbs @@ -0,0 +1,7 @@ +{{#if package.main}} +## API-reference + +### `require("m-io/fs")` + +{{{jsdoc 'fs.js' '#'}}} +{{/if}} diff --git a/.thought/partials/overview.md.hbs b/.thought/partials/overview.md.hbs new file mode 100644 index 0000000..a924888 --- /dev/null +++ b/.thought/partials/overview.md.hbs @@ -0,0 +1,19 @@ +This package is a replacement for the functions of {{npm 'q-io'}} that I use in my projects. I have use `q-io/fs` a lot since it has functions +like `makeTree`, `listTree` and `removeTree`. Furthermore, its `read` and `write` function work with strings by default, which makes it easier to +read text files. + +Sadly, `q-io@1` depends on {{npm 'collections'}}@1, which +[overwrites the function `Array.prototype.find` with an implementation that does not match the ES6-spec](https://github.com/montagejs/collections/issues/139). +This causes problems in {{npm 'jsdoc-parse'}}. This is another example of why [modifying objects you don’t own][zakas dont modify] +is a bad practice. + +This problem *could* be solved by using `q-io@2` instead of version 1. This version has [other problems](https://github.com/kriskowal/q-io/pull/155) which were +solved in version 1. It may be a silly feeling, but version 2 of `q-io` vseems not to receive too much care at the moment. + +Since I do not use many functions, I have decided to write a drop-in replacement for my own purposes, and this is it: `m-io`. +If you like this and want to provide more methods for your needs, please go ahead and make a PR. + + + + +[zakas dont modify]: https://www.nczonline.net/blog/2010/03/02/maintainable-javascript-dont-modify-objects-you-down-own/ \ No newline at end of file diff --git a/.thought/partials/usage.md.hbs b/.thought/partials/usage.md.hbs new file mode 100644 index 0000000..494f5cf --- /dev/null +++ b/.thought/partials/usage.md.hbs @@ -0,0 +1,16 @@ +{{#if (exists 'examples/example.js')}} + +## Usage + +The following example demonstrates how to use this module: + +{{{example 'examples/example.js'}}} + +This will generate the following output + +{{{exec 'node example.js' cwd='examples/'}}} +{{/if}} + +After deleting `city/usa`, the `city`-subtree looks liks this: + +{{dirTree 'examples' 'city/**'}} diff --git a/README.md b/README.md index 327e4d2..c7d23f0 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,27 @@ [![Coverage Status](https://img.shields.io/coveralls/nknapp/m-io.svg)](https://coveralls.io/r/nknapp/m-io) -> (Incomplete) replacement for `q-io` +> (Incomplete) replacement for q-io +This package is a replacement for the functions of [q-io](https://npmjs.com/package/q-io) that I use in my projects. I have use `q-io/fs` a lot since it has functions +like `makeTree`, `listTree` and `removeTree`. Furthermore, its `read` and `write` function work with strings by default, which makes it easier to +read text files. +Sadly, `q-io@1` depends on [collections](https://npmjs.com/package/collections)@1, which +[overwrites the function `Array.prototype.find` with an implementation that does not match the ES6-spec](https://github.com/montagejs/collections/issues/139). +This causes problems in [jsdoc-parse](https://npmjs.com/package/jsdoc-parse). This is another example of why [modifying objects you don’t own][zakas dont modify] +is a bad practice. + +This problem *could* be solved by using `q-io@2` instead of version 1. This version has [other problems](https://github.com/kriskowal/q-io/pull/155) which were +solved in version 1. It may be a silly feeling, but version 2 of `q-io` vseems not to receive too much care at the moment. + +Since I do not use many functions, I have decided to write a drop-in replacement for my own purposes, and this is it: `m-io`. +If you like this and want to provide more methods for your needs, please go ahead and make a PR. + + + + +[zakas dont modify]: https://www.nczonline.net/blog/2010/03/02/maintainable-javascript-dont-modify-objects-you-down-own/ # Installation ``` @@ -20,24 +38,111 @@ npm install m-io The following example demonstrates how to use this module: ```js - +var FS = require('m-io/fs') + +// Create some files +FS.makeTree('city/germany') + .then(() => FS.write('city/germany/darmstadt.md', 'Darmstadt is nice')) + .then(() => FS.makeTree('city/usa')) + .then(() => FS.write('city/usa/new-york.md', 'New York is huge')) + .then(() => FS.makeTree('city/france')) + .then(() => FS.write('city/france/paris.md', 'Olala')) + + // List files + .then(() => FS.listTree('city', (filename, stats) => stats.isFile())) + .then((filelist) => console.log('List files:', filelist.sort())) + + // List dirs and files + .then(() => FS.listTree('city')) + .then((list) => console.log('List dirs and files:', list.sort())) + + // Read file contents + .then(() => FS.read('city/usa/new-york.md')) + .then((nyc) => console.log('Read file contents:', nyc)) + + // Remove subdir + .then(() => FS.removeTree('city/usa')) + .done(() => console.log('Done')) ``` This will generate the following output ``` - +List files: [ 'city/france/paris.md', + 'city/germany/darmstadt.md', + 'city/usa/new-york.md' ] +List dirs and files: [ 'city', + 'city/france', + 'city/france/paris.md', + 'city/germany', + 'city/germany/darmstadt.md', + 'city/usa', + 'city/usa/new-york.md' ] +undefined +Read file contents: New York is huge +Done ``` -## API-reference +After deleting `city/usa`, the `city`-subtree looks liks this: + +

+city
+├─┬ france
+│ └── paris.md
+└─┬ germany
+  └── darmstadt.md
+
+ +## API-reference + +### `require("m-io/fs")` + + + +## fs + +* [fs](#module_fs) + * [.listTree(directoryPath, filter)](#module_fs.listTree) ⇒ Promise.<Array.<string>> + * [.makeTree(aPath, [mode])](#module_fs.makeTree) + * [.read(aPath)](#module_fs.read) + + + +### .listTree(directoryPath, filter) ⇒ Promise.<Array.<string>> +Custom implementation of [q-io/fs#listTree](http://documentup.com/kriskowal/q-io#listtreepath-guardpath-stat) +to avoid dependency on q-io + +**Kind**: static method of [fs](#module_fs) +**Returns**: Promise.<Array.<string>> - a promise for the collector, that is fulfilled after traversal + +| Param | Type | Description | +| --- | --- | --- | +| directoryPath | string | the base path | +| filter | function | a function that returns true, false or null to show that a file should be included or ignored and that a directory should be ignored completely (null) | + + + +### .makeTree(aPath, [mode]) +Replacement for [q-io/fs#makeTree](http://documentup.com/kriskowal/q-io#maketreepath-mode) + +**Kind**: static method of [fs](#module_fs) + +| Param | Type | Description | +| --- | --- | --- | +| aPath | string | the directory to be created | +| [mode] | number | (e.g. 0644) | + + + +### .read(aPath) +Replacement for [q-io/fs#read](http://documentup.com/kriskowal/q-io#readpath-options) - +**Kind**: static method of [fs](#module_fs) -## mIo() -Describe your module here +| Param | +| --- | +| aPath | -**Kind**: global function -**Access:** public diff --git a/examples/example.js b/examples/example.js index e69de29..33db0a1 100644 --- a/examples/example.js +++ b/examples/example.js @@ -0,0 +1,25 @@ +var FS = require('../fs') + +// Create some files +FS.makeTree('city/germany') + .then(() => FS.write('city/germany/darmstadt.md', 'Darmstadt is nice')) + .then(() => FS.makeTree('city/usa')) + .then(() => FS.write('city/usa/new-york.md', 'New York is huge')) + .then(() => FS.makeTree('city/france')) + .then(() => FS.write('city/france/paris.md', 'Olala')) + + // List files + .then(() => FS.listTree('city', (filename, stats) => stats.isFile())) + .then((filelist) => console.log('List files:', filelist.sort())) + + // List dirs and files + .then(() => FS.listTree('city')) + .then((list) => console.log('List dirs and files:', list.sort())) + + // Read file contents + .then(() => FS.read('city/usa/new-york.md')) + .then((nyc) => console.log('Read file contents:', nyc)) + + // Remove subdir + .then(() => FS.removeTree('city/usa')) + .done(() => console.log('Done')) diff --git a/fs.js b/fs.js index d65b9a7..512faaf 100644 --- a/fs.js +++ b/fs.js @@ -11,12 +11,16 @@ var fs = require('fs') var Q = require('q') var path = require('path') +/** + * + * @module + */ module.exports = { /** * Custom implementation of [q-io/fs#listTree](http://documentup.com/kriskowal/q-io#listtreepath-guardpath-stat) * to avoid dependency on q-io * @param {string} directoryPath the base path - * @param {function(string,fs.Stats):boolean} filter a function that returns true, false or null to show that a file + * @param {function(string,fs.Stats):boolean=} filter a function that returns true, false or null to show that a file * should be included or ignored and that a directory should be ignored completely (null) * @returns {Promise} a promise for the collector, that is fulfilled after traversal */ @@ -26,7 +30,7 @@ module.exports = { /** * Replacement for [q-io/fs#makeTree](http://documentup.com/kriskowal/q-io#maketreepath-mode) * @param {string} aPath the directory to be created - * @param {number} mode (e.g. 0644) + * @param {number=} mode (e.g. 0644) */ makeTree: function makeTree (aPath, mode) { return Q.nfcall(require('mkdirp'), aPath, {mode: mode}) @@ -40,6 +44,7 @@ module.exports = { */ read: function read (aPath, options) { const flags = optionsFrom(options).flags + console.log(flags) return Q.ninvoke(fs, 'readFile', aPath, { encoding: flags === 'b' ? null : 'utf-8' }) @@ -69,13 +74,14 @@ module.exports = { /** * Coerce a flag-string `b` into an options-object `{ flags: 'ba' }` if necessary. * @param optionsOrFlags + * @private * @returns {*} */ function optionsFrom (optionsOrFlags) { if (!optionsOrFlags) { return {} } - if (optionsOrFlags instanceof String) { + if (typeof optionsOrFlags === 'string') { return { flags: optionsOrFlags } @@ -89,6 +95,7 @@ function optionsFrom (optionsOrFlags) { * @param {function(string,fs.Stats):boolean} filter a function that returns true, false or null to show that a file * should be included or ignored and that a directory should be ignored completely (null) * @param {string[]} collector array to collect the filenames into + * @private * @returns {Promise} a promise for the collector, that is fulfilled after traversal */ function walk (directoryPath, filter, collector) { @@ -97,29 +104,28 @@ function walk (directoryPath, filter, collector) { if (err) { return defer.reject(err) } - var filterResult = filter(directoryPath, stat) + // Call filter to get result, "true" if no filter is set + var filterResult = !filter || filter(directoryPath, stat) if (filterResult) { collector.push(directoryPath) } - if (stat.isDirectory()) { - // false/true => iterate directory - if (filterResult !== null) { - fs.readdir(directoryPath, function (err, filenames) { - if (err) { - return defer.reject(err) - } - var paths = filenames.map(function (name) { - return path.join(directoryPath, name) - }) - // Walk all files/subdirs - Q.all(paths.map(function (filepath) { - return walk(filepath, filter, collector) - })) - .then(function () { - defer.fulfill(collector) - }) + // false/true => iterate directory + if (stat.isDirectory() && filterResult !== null) { + fs.readdir(directoryPath, function (err, filenames) { + if (err) { + return defer.reject(err) + } + var paths = filenames.map(function (name) { + return path.join(directoryPath, name) }) - } + // Walk all files/subdirs + Q.all(paths.map(function (filepath) { + return walk(filepath, filter, collector) + })) + .then(function () { + defer.fulfill(collector) + }) + }) } else { // No recursive call with a file defer.fulfill(collector) diff --git a/package.json b/package.json index 9cee342..5a0370e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "m-io", "version": "0.0.2", - "description": "(Incomplete) replacement for `q-io`", + "description": "(Incomplete) replacement for q-io", "repository": { "type": "git", "url": "git@github.com:nknapp/m-io.git" @@ -33,6 +33,8 @@ "rimraf": "^2.5.3" }, "devDependencies": { + "chai": "^3.5.0", + "chai-as-promised": "^5.3.0", "ghooks": "^1.0.3", "mocha": "^2.3.3", "thoughtful-release": "^0.3.0" diff --git a/test/fixtures/tree/a/b/c.txt b/test/fixtures/tree/a/b/c.txt new file mode 100644 index 0000000..3410062 --- /dev/null +++ b/test/fixtures/tree/a/b/c.txt @@ -0,0 +1 @@ +c \ No newline at end of file diff --git a/test/fixtures/tree/a/bb/cc.txt b/test/fixtures/tree/a/bb/cc.txt new file mode 100644 index 0000000..2652f5f --- /dev/null +++ b/test/fixtures/tree/a/bb/cc.txt @@ -0,0 +1 @@ +cc \ No newline at end of file diff --git a/test/fs-spec.js b/test/fs-spec.js new file mode 100644 index 0000000..10f644d --- /dev/null +++ b/test/fs-spec.js @@ -0,0 +1,137 @@ +/*! + * m-io + * + * Copyright (c) 2016 Nils Knappmeier. + * Released under the MIT license. + */ + +/* global describe */ +/* global it */ +/* global beforeEach */ +// /* global xdescribe */ +// /* global xit */ + +'use strict' + +var mfs = require('../fs.js') +var fs = require('fs') + +var chai = require('chai') +var chaiAsPromised = require('chai-as-promised') +chai.use(chaiAsPromised) +chai.should() + +describe('m-io/fs', function () { + describe('the list-tree function', function () { + const sort = (x) => x.sort() + + it('should return a file listing as array', function () { + return mfs.listTree('test/fixtures').then((x) => x.sort()).should.eventually.deep.equal([ + 'test/fixtures', + 'test/fixtures/tree', + 'test/fixtures/tree/a', + 'test/fixtures/tree/a/b', + 'test/fixtures/tree/a/b/c.txt', + 'test/fixtures/tree/a/bb', + 'test/fixtures/tree/a/bb/cc.txt' + ]) + }) + + it('should apply the filter to all entries (stats)', function () { + const filter = (name, stat) => stat.isFile() + return mfs.listTree('test/fixtures', filter).then(sort).should.eventually.deep.equal([ + 'test/fixtures/tree/a/b/c.txt', + 'test/fixtures/tree/a/bb/cc.txt' + ]) + }) + + it('should apply the filter to all entries (name)', function () { + const filter = (name, stat) => name !== 'test/fixtures/tree/a/bb' + return mfs.listTree('test/fixtures', filter).then(sort).should.eventually.deep.equal([ + 'test/fixtures', + 'test/fixtures/tree', + 'test/fixtures/tree/a', + 'test/fixtures/tree/a/b', + 'test/fixtures/tree/a/b/c.txt', + 'test/fixtures/tree/a/bb/cc.txt' + ]) + }) + + it('should not traverse dirs for which the filter returns null', function () { + const filter = (name, stat) => name === 'test/fixtures/tree/a/bb' ? null : true + return mfs.listTree('test/fixtures', filter).then(sort).should.eventually.deep.equal([ + 'test/fixtures', + 'test/fixtures/tree', + 'test/fixtures/tree/a', + 'test/fixtures/tree/a/b', + 'test/fixtures/tree/a/b/c.txt' + ]) + }) + }) + + describe('the read function', function () { + it('should read the file contents', function () { + return mfs.read('test/fixtures/tree/a/b/c.txt').should.eventually.equal('c') + }) + + it('should read the file contents as Buffer in binary mode', function () { + return mfs.read('test/fixtures/tree/a/b/c.txt', 'b').should.eventually.deep.equal(Buffer.from('c', 'utf-8')) + }) + + it('should read the file contents as Buffer in binary mode (options-object)', function () { + return mfs.read('test/fixtures/tree/a/b/c.txt', {flags: 'b'}).should.eventually.deep.equal(Buffer.from('c', 'utf-8')) + }) + }) + + describe('the write function', function () { + beforeEach(function () { + require('rimraf').sync('tmp/test') + require('mkdirp').sync('tmp/test') + }) + it('should write the file contents', function () { + return mfs.write('tmp/test/a.txt', 'a').then(() => readSync('tmp/test/a.txt')).should.eventually.equal('a') + }) + + it('should write the file contents as Buffer in binary mode', function () { + return mfs.write('tmp/test/c.txt', Buffer.from('c', 'utf-8')).then(() => readSync('tmp/test/c.txt')).should.eventually.deep.equal('c') + }) + }) + + describe('the makeTree-function', function () { + beforeEach(function () { + require('rimraf').sync('tmp/test') + require('mkdirp').sync('tmp/test') + }) + it('should create a directory with parents', function () { + return mfs.makeTree('tmp/test/make/tree/directory').then(() => fs.existsSync('tmp/test/make/tree/directory')).should.eventually.be.ok + }) + + it('should create a directory with a given mode', function () { + const result = mfs.makeTree('tmp/test/make/tree/directory700', 0o700) + .then(() => Promise.all([ + // Only the last three octals are interesting + fs.statSync('tmp/test/make').mode & 0o777, + fs.statSync('tmp/test/make/tree').mode & 0o777, + fs.statSync('tmp/test/make/tree/directory700').mode & 0o777 + ])) + return result.should.eventually.deep.equal([ 0o700, 0o700, 0o700 ]) + }) + }) + + describe('the removeTree-function', function () { + beforeEach(function () { + require('mkdirp').sync('tmp/test/remove/tree/directory') + }) + it('should create a directory with parents', function () { + return mfs.removeTree('tmp/test/remove').then(() => fs.existsSync('tmp/test/remove')).should.eventually.be.false + }) + }) +}) + +/** + * Helper to read files as string + * @param path + */ +function readSync (path) { + return fs.readFileSync(path, {encoding: 'utf-8'}) +} diff --git a/test/main-spec.js b/test/main-spec.js deleted file mode 100644 index 9f02c4f..0000000 --- a/test/main-spec.js +++ /dev/null @@ -1,20 +0,0 @@ -/*! - * m-io - * - * Copyright (c) 2016 Nils Knappmeier. - * Released under the MIT license. - */ - -/* global describe */ -// /* global it */ -// /* global xdescribe */ -// /* global xit */ - -'use strict' - -var mIo = require('../') - -describe('m-io:', function () { - // body - mIo -})