diff --git a/.gitignore b/.gitignore index 5902c15..f7ce2bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /coverage/ /lib/ /node_modules/ -/npm-debug.log +/npm-debug.* diff --git a/.npmignore b/.npmignore index d71ab34..d80ec38 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,6 @@ /coverage/ /src/ +/test/ /.babelrc /.editorconfig /.eslintrc.yml diff --git a/README.md b/README.md index e4007bb..a10b108 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,6 @@ [Gulp][Gulp link] plugin for applying arbitrary transformations to the contents of files. -* **Simple**. Just pass a callback function that takes the current file -contents and returns the desired contents. -* **Flexible**. Receive file contents as a Buffer or a string. Compatible with -pipelines in both buffer mode and streaming mode. -* **Economical**. Reduce the need for gulp-specific plugins by pairing -gulp-transform with ordinary node packages and functions. - ## Install Install via [npm][NPM link]: @@ -89,9 +82,10 @@ gulp.task('cheerio', function() { ##### transformFn `function` -The callback responsible for the transformation. The return value must be a -string or a Buffer, which will replace the file's contents. The callback -is invoked once per file with the following arguments: +The callback responsible for the transformation, whose return value will replace +the file's contents. The return value may be a string, a Buffer, or a Promise +resolvable to a string or Buffer. The callback is invoked once per file with the +following arguments: * **contents** `Buffer` | `string`
The initial contents of the file. Contents are passed as a Buffer unless the diff --git a/package.json b/package.json index 2ec675e..947e459 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "gulp": "3.x" }, "dependencies": { + "es6-promise": "^4.0.5", "gulp-util": "^3.0.7", "lodash": "^4.13.1" }, @@ -49,10 +50,10 @@ "chai": "^3.5.0", "coffee-script": "^1.10.0", "coveralls": "^2.11.9", - "eslint": "^2.11.1", + "eslint": "^2.13.1", "event-stream": "^3.3.2", - "istanbul": "1.0.0-alpha.2", - "mocha": "^2.5.3", + "istanbul": "1.1.0-alpha.1", + "mocha": "^3.1.2", "rimraf": "^2.5.2", "sinon": "^1.17.4", "sinon-chai": "^2.8.0" diff --git a/src/file-stream.js b/src/file-stream.js index d2250d6..e2da43b 100644 --- a/src/file-stream.js +++ b/src/file-stream.js @@ -19,9 +19,13 @@ export class FileStream extends Transform { _flush(done) { let contents = Buffer.concat(this.data); - this.push(transform(this.fn, contents, this.file, this.opts)); - done(); + transform(this.fn, contents, this.file, this.opts).then((result) => { + this.push(result); + done(); + }).catch((err) => { + done(err); + }); } } diff --git a/src/index.js b/src/index.js index c7ef43f..662fe5b 100644 --- a/src/index.js +++ b/src/index.js @@ -5,13 +5,10 @@ import {err} from './err'; export default function gulpTransform(transformFn, options) { if (isNil(transformFn)) { err('transformFn must be defined'); - } else if (!isFunction(transformFn)) { err('transformFn must be a function'); - } else if (!isNil(options) && !isObject(options)) { err('options must be an object'); - } else { return new PluginStream(transformFn, options); } diff --git a/src/plugin-stream.js b/src/plugin-stream.js index 2fc649e..1de084c 100644 --- a/src/plugin-stream.js +++ b/src/plugin-stream.js @@ -15,14 +15,17 @@ export class PluginStream extends Transform { let {fn, opts} = this; if (file.isBuffer()) { - file.contents = transform(fn, file.contents, file, opts); - } - - if (file.isStream()) { + transform(fn, file.contents, file, opts).then((result) => { + file.contents = result; + next(null, file); + }).catch((err) => { + next(err); + }); + } else if (file.isStream()) { file.contents = file.contents.pipe(new FileStream(fn, file, opts)); + next(null, file); + } else { + next(null, file); } - - next(null, file); } - } diff --git a/src/transform.js b/src/transform.js index fe09458..f4bad6d 100644 --- a/src/transform.js +++ b/src/transform.js @@ -1,11 +1,20 @@ -import {isBuffer, isString} from 'lodash'; +import {Promise} from 'es6-promise'; +import {isBuffer} from 'lodash'; import {err} from './err'; -export function transform(fn, contents, file, opts) { - let encoded = opts.encoding ? contents.toString(opts.encoding) : contents; - let transformed = fn.call(opts.thisArg, encoded, file); +export function transform(fn, contents, file, {encoding, thisArg}) { + let decoded = encoding ? contents.toString(encoding) : contents; + let transformed = fn.call(thisArg, decoded, file); - return isBuffer(transformed) ? transformed : - isString(transformed) ? new Buffer(transformed) : - err('transformFn must return a string or a Buffer'); + return Promise.resolve(transformed).then(toBuffer); +} + +function toBuffer(contents) { + if (isBuffer(contents)) { + return contents; + } else if (contents != null) { + return new Buffer(String(contents)); + } else { + err('transformFn may not return or resolve to null or undefined'); + } } diff --git a/test/fixtures/fn.coffee b/test/fixtures/fn.coffee index a9c5a6c..cf925c9 100644 --- a/test/fixtures/fn.coffee +++ b/test/fixtures/fn.coffee @@ -1,3 +1,4 @@ +{Promise} = require 'es6-promise'; {spy} = require 'sinon' re = /one|two|three/g @@ -7,11 +8,17 @@ dict = two: 'deux' three: 'trois' +translate = (content) -> + content.replace(re, (match) -> dict[match]) + exports.stringFn = -> spy (content) -> - content.replace re, (match) -> - dict[match] + translate(content) exports.bufferFn = -> spy (content) -> Buffer.concat([content, content]) + +exports.asyncFn = -> + spy (content) -> + return Promise.resolve(translate(content)) diff --git a/test/index.coffee b/test/index.coffee index 17cb3cb..c3285c6 100644 --- a/test/index.coffee +++ b/test/index.coffee @@ -1,11 +1,11 @@ chai = require 'chai' sinonChai = require 'sinon-chai' {match: {any, instanceOf}} = require 'sinon' -{File: {isVinyl}} = require 'gulp-util' +{File: {isVinyl}, PluginError} = require 'gulp-util' {wait} = require 'event-stream' {buffer, string} = require './fixtures/content' {buffered, streaming} = require './fixtures/file' -{bufferFn, stringFn} = require './fixtures/fn' +{bufferFn, stringFn, asyncFn} = require './fixtures/fn' err = require './helpers/err' transform = require '../src' @@ -30,10 +30,14 @@ describe 'plugin: gulp-transform', -> it 'throws PluginError', -> err -> transform 42 - context 'returns neither a string nor a Buffer', -> + context 'returns null or undefined', -> - it 'throws PluginError', -> - err -> transform((content) -> 42).write buffered() + it 'emits PluginError', (done) -> + stream = transform((content) -> null) + stream.write buffered() + stream.on 'error', (err) -> + err.should.be.instanceOf PluginError + done() context 'returns a Buffer or string', -> [fn, file] = [null, null] @@ -52,6 +56,23 @@ describe 'plugin: gulp-transform', -> it 'is called with vinyl File as second argument', -> fn.should.have.been.calledWith any, file + context 'returns a Promise that resolves to a string or Buffer', -> + [fn, file] = [null, null] + + beforeEach -> + file = buffered() + fn = asyncFn() + transform(fn, {encoding: 'utf8'}).write(file) + + it 'is called once per file', -> + fn.should.have.been.calledOnce + + it 'is called with contents as first argument', -> + fn.should.have.been.calledWith string + + it 'is called with vinyl File as second argument', -> + fn.should.have.been.calledWith any, file + describe 'param: options', -> context 'not an object', -> @@ -80,40 +101,83 @@ describe 'plugin: gulp-transform', -> fn.should.have.been.calledOn undefined describe 'mode: buffer', -> - file = null - beforeEach (done) -> - transform(bufferFn()).once('data', (_file) -> - file = _file - done() - ).write buffered() + context 'synchronous', -> + file = null + + beforeEach (done) -> + transform(bufferFn()).once('data', (_file) -> + file = _file + done() + ).write buffered() + + it 'returns a stream of vinyl Files', -> + isVinyl(file).should.be.true + + it 'files are in buffer mode', -> + file.isBuffer().should.be.true; - it 'returns a stream of vinyl Files', -> - isVinyl(file).should.be.true + it 'transforms file contents', -> + file.contents.should.deep.equal Buffer.concat([buffer, buffer]) - it 'files are in buffer mode', -> - file.isBuffer().should.be.true; + context 'async', -> + file = null - it 'transforms file contents', -> - file.contents.should.deep.equal Buffer.concat([buffer, buffer]) + beforeEach (done) -> + transform(asyncFn(), {encoding: 'utf8'}).once('data', (_file) -> + file = _file + done() + ).write buffered() + + it 'returns a stream of vinyl Files', -> + isVinyl(file).should.be.true + + it 'files are in buffer mode', -> + file.isBuffer().should.be.true; + + it 'transforms file contents', -> + file.contents.should.deep.equal new Buffer('un deux trois') describe 'mode: streaming', -> - file = null - beforeEach (done) -> - transform(stringFn(), {encoding: 'utf8'}).once('data', (_file) -> - file = _file - done() - ).write streaming() + context 'synchronous', -> + file = null - it 'returns a stream of vinyl Files', -> - isVinyl(file).should.be.true + beforeEach (done) -> + transform(stringFn(), {encoding: 'utf8'}).once('data', (_file) -> + file = _file + done() + ).write streaming() - it 'files are in streaming mode', -> - file.isStream().should.be.true + it 'returns a stream of vinyl Files', -> + isVinyl(file).should.be.true - it 'transforms file contents', (done) -> - file.pipe(wait((err, data) -> - data.should.deep.equal new Buffer('un deux trois') - done() - )) + it 'files are in streaming mode', -> + file.isStream().should.be.true + + it 'transforms file contents', (done) -> + file.pipe(wait((err, data) -> + data.should.deep.equal new Buffer('un deux trois') + done() + )) + + context 'async', -> + file = null + + beforeEach (done) -> + transform(asyncFn(), {encoding: 'utf8'}).once('data', (_file) -> + file = _file + done() + ).write streaming() + + it 'returns a stream of vinyl Files', -> + isVinyl(file).should.be.true + + it 'files are in streaming mode', -> + file.isStream().should.be.true + + it 'transforms file contents', (done) -> + file.pipe(wait((err, data) -> + data.should.deep.equal new Buffer('un deux trois') + done() + ))