├── .gitignore ├── .jshintrc ├── .travis.yml ├── README.md ├── gulpfile.js ├── index.js ├── package.json ├── proj.sublime-project └── test ├── gulp-chug-integrate.js ├── gulp-chug-spec.js ├── mocha.opts └── subproj ├── gulpfile-custom-name.js ├── gulpfile.js └── subdir └── gulpfile.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.sublime-workspace 3 | npm-debug.log 4 | .DS_Store 5 | coverage 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "expr": true, 3 | "lastsemic": true, 4 | "node": true, 5 | "globals": { 6 | "describe": true, 7 | "it": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-chug [![NPM version][npm-badge-img]][npm-url] [![Build Status](https://travis-ci.org/robatron/gulp-chug.png?branch=master)](https://travis-ci.org/robatron/gulp-chug) [![Dependency Status](https://david-dm.org/robatron/gulp-chug.png)](https://david-dm.org/robatron/gulp-chug) 2 | 3 | > A [gulp][gulp-url] plugin for running external gulpfiles as part of a gulp task inside another gulpfile. 4 | 5 | gulp-chug is *non-modifying*, i.e., gulp-chug will return the same stream it 6 | receives. See [Use with other plugins](#use-with-other-plugins) for an example. 7 | 8 | Requires [node](//nodejs.org) >= 0.10 9 | 10 | Inspired by [shama](https://github.com/shama)'s [grunt-hub](https://github.com/shama/grunt-hub). 11 | 12 | **Note:** This plugin has been [blacklisted](https://github.com/gulpjs/plugins/issues/93), however I have yet to find an example of a pattern that allows for the execution of child gulpfiles without task name collisions. If you have an example, let me know! 13 | 14 | ## Install 15 | 16 | Install with [npm](https://npmjs.org/package/gulp-chug): 17 | 18 | ```sh 19 | npm install gulp-chug 20 | ``` 21 | 22 | 23 | ## Usage 24 | 25 | ### Run external gulpfiles 26 | 27 | Run one external gulpfile: 28 | 29 | ```js 30 | var gulp = require( 'gulp' ); 31 | var chug = require( 'gulp-chug' ); 32 | 33 | gulp.task( 'default', function () { 34 | gulp.src( './subproj/gulpfile.js' ) 35 | .pipe( chug() ) 36 | } ); 37 | ``` 38 | 39 | Run multiple external gulpfiles: 40 | 41 | ```js 42 | var gulp = require( 'gulp' ); 43 | var chug = require( 'gulp-chug' ); 44 | 45 | gulp.task( 'default', function () { 46 | 47 | // Find and run all gulpfiles under all subdirectories 48 | gulp.src( './**/gulpfile.js' ) 49 | .pipe( chug() ) 50 | } ); 51 | ``` 52 | 53 | ### Use with other plugins 54 | 55 | grunt-chug will not modify streams passed to it, but will happily accept 56 | streams modified by other plugins: 57 | 58 | ```js 59 | var gulp = require( 'gulp' ); 60 | var chug = require( 'gulp-chug' ); 61 | var replace = require( 'gulp-replace' ); 62 | 63 | gulp.task( 'default', function () { 64 | gulp.src( './subproj/gulpfile.js' ) 65 | 66 | // Transform stream with gulp-replace 67 | .pipe( replace( 'Hello', 'Goodbye' ) ) 68 | 69 | // Run modified stream with gulp-chug 70 | .pipe( chug() ) 71 | } ); 72 | ``` 73 | 74 | ### Make gulp-chug faster by ignoring file contents 75 | 76 | If gulp-chug is the only plugin in the stream, there's no need to actually read 77 | the contents of the gulpfiles. Set `{ read: false }` in `gulp.src` to speed 78 | things up: 79 | 80 | ```js 81 | var gulp = require( 'gulp' ); 82 | var chug = require( 'gulp-chug' ); 83 | 84 | gulp.task( 'default', function () { 85 | gulp.src( './subproj/gulpfile.js', { read: false } ) 86 | .pipe( chug() ) 87 | } ); 88 | ``` 89 | ## Options 90 | 91 | Gulp chug supports several options, all of which are **optional**, e.g., 92 | 93 | ```js 94 | var gulp = require( 'gulp' ); 95 | var chug = require( 'gulp-chug' ); 96 | 97 | gulp.task( 'default', function () { 98 | gulp.src( './subproj/gulpfile.js' ) 99 | .pipe( chug( { 100 | nodeCmd: 'node', 101 | tasks: [ 'default' ], 102 | args: [ '--my-arg-1', '--my-arg-2' ] 103 | } ) ); 104 | } ); 105 | ``` 106 | 107 | ### tasks 108 | 109 | The tasks to run from each gulpfile. Default is `default`. 110 | 111 | ```js 112 | chug( { 113 | tasks: [ 'my-task-1', 'my-task-2' ] 114 | } ) 115 | ``` 116 | 117 | ### nodeCmd 118 | 119 | The node command to spawn when running gulpfiles. Default is `node`. 120 | 121 | ```js 122 | chug( { 123 | nodeCmd: './my-node-bin' 124 | } ) 125 | ``` 126 | 127 | ### args 128 | 129 | Additional command-line arguments to pass to each spawned process. Default is 130 | none. 131 | 132 | ```js 133 | chug( { 134 | args: [ '--my-arg-1', '--my-arg-2' ] 135 | } ) 136 | ``` 137 | 138 | ## Callback 139 | 140 | You can pass a callback function to gulp chug that will be executed after the 141 | external gulpfile has finished running. 142 | 143 | ```js 144 | var gulp = require( 'gulp' ); 145 | var chug = require( 'gulp-chug' ); 146 | 147 | gulp.task( 'default', function () { 148 | 149 | gulp.src( './subproj/gulpfile.js' ) 150 | .pipe( chug( function () { 151 | console.log( 'Done' ); 152 | } ) ) 153 | } ); 154 | ``` 155 | 156 | In combination with gulp-chug options: 157 | 158 | ```js 159 | var gulp = require( 'gulp' ); 160 | var chug = require( 'gulp-chug' ); 161 | 162 | gulp.task( 'default', function () { 163 | 164 | gulp.src( './subproj/gulpfile.js' ) 165 | .pipe( chug( { 166 | tasks: [ 'my-task-1', 'my-task-2' ] 167 | }, function () { 168 | console.log( 'Done' ); 169 | } ) ) 170 | } ); 171 | ``` 172 | 173 | ## See also 174 | 175 | - [gulp-hub](https://github.com/frankwallis/gulp-hub) - Load tasks from other gulpfiles 176 | 177 | ## Changelog 178 | 179 | ### 0.4 180 | 181 | - Add option to pass additional command-line arguments to each gulpfile 182 | - Update deps 183 | 184 | ### 0.3 185 | 186 | - Add `nodeCmd` option to choose one's own node binary to spawn 187 | - Add error handling for spawned processes 188 | - Update deps 189 | 190 | ### 0.2 191 | 192 | - Use `child_process.spawn` instead of `child_process.exec` for real-time child gulpfile output (see [exec vs spawn](http://www.hacksparrow.com/difference-between-spawn-and-exec-of-node-js-child_process.html)) 193 | - Implement proper unit tests 194 | - Fix bug where temp gulpfile would be written to the glob base instead of as a sibling to the original gulpfile 195 | 196 | ### 0.1 197 | 198 | - Initial release 199 | 200 | ## License 201 | 202 | The MIT License (MIT) 203 | 204 | Copyright (c) 2014 Rob McGuire-Dale 205 | 206 | Permission is hereby granted, free of charge, to any person obtaining a copy 207 | of this software and associated documentation files (the "Software"), to deal 208 | in the Software without restriction, including without limitation the rights 209 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 210 | copies of the Software, and to permit persons to whom the Software is 211 | furnished to do so, subject to the following conditions: 212 | 213 | The above copyright notice and this permission notice shall be included in 214 | all copies or substantial portions of the Software. 215 | 216 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 217 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 218 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 219 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 220 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 221 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 222 | THE SOFTWARE. 223 | 224 | [npm-badge-img]: https://badge.fury.io/js/gulp-chug.png 225 | [npm-url]: https://npmjs.org/package/gulp-chug 226 | [gulp-url]: https://github.com/wearefractal/gulp 227 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require( 'gulp' ); 2 | var istanbul = require( 'gulp-istanbul' ); 3 | var mocha = require( 'gulp-mocha' ); 4 | 5 | gulp.task( 'test', function ( cb ) { 6 | gulp.src( 'index.js' ) 7 | .pipe( istanbul() ) 8 | .pipe( istanbul.hookRequire() ) 9 | .on( 'finish', function () { 10 | gulp.src( 'test/*-spec.js' ) 11 | .pipe( mocha() ) 12 | .pipe( istanbul.writeReports() ) 13 | .on( 'end', cb ); 14 | } ); 15 | } ); 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require( 'fs' ); 4 | var path = require( 'path' ); 5 | var util = require( 'util' ); 6 | var spawn = require( 'child_process' ).spawn; 7 | 8 | var _ = require( 'lodash' ); 9 | var through = require( 'through2' ); 10 | var resolve = require( 'resolve' ); 11 | var gutil = require( 'gulp-util' ); 12 | var PluginError = gutil.PluginError; 13 | 14 | var PKG = require( './package.json' ); 15 | 16 | // Primary gulp function 17 | module.exports = function ( options, userCallback ) { 18 | 19 | // Consider `options` the callback if it's a function 20 | if ( _.isFunction( options ) ) { 21 | userCallback = options; 22 | options = {}; 23 | } 24 | 25 | // Set default options 26 | var opts = _.assign( { 27 | nodeCmd: 'node', 28 | tasks: [ 'default' ] 29 | }, options ); 30 | 31 | // Set the callback to a noop if it's not a function 32 | userCallback = _.isFunction( userCallback ) ? userCallback : _.noop 33 | 34 | // Create a stream through which each file will pass 35 | return through.obj( function ( file, enc, callback ) { 36 | 37 | // Grab reference to this through object 38 | var self = this; 39 | 40 | // Since we're not modifying the gulpfile, always push it back on the 41 | // stream. 42 | self.push( file ); 43 | 44 | // Configure logging and errors 45 | var say = function( msg, noNewLine ) { 46 | if ( !noNewLine ) { 47 | return console.log( 48 | util.format( '[%s]', gutil.colors.green( PKG.name ) ), msg 49 | ); 50 | } 51 | process.stdout.write( util.format( '[%s]', gutil.colors.green( PKG.name ) ) + ' ' + msg ) 52 | }; 53 | 54 | var sayErr = function( errMsg ) { 55 | self.emit( 'error', new PluginError( PKG.name, errMsg ) ); 56 | }; 57 | 58 | // Error if file contents is stream ( { buffer: false } in gulp.src ) 59 | // TODO: Add support for a streams 60 | if ( file.isStream() ) { 61 | sayErr( 'Streams are not supported yet. Pull requests welcome :)' ); 62 | return callback(); 63 | } 64 | 65 | // Gather target gulpfile info 66 | var gulpfile = {}; 67 | gulpfile.path = file.path; 68 | gulpfile.relPath = path.relative( process.cwd(), gulpfile.path ); 69 | gulpfile.base = path.dirname( file.path ); 70 | gulpfile.relBase = path.relative( process.cwd(), gulpfile.base ); 71 | gulpfile.name = path.basename( gulpfile.path ); 72 | gulpfile.ext = path.extname( gulpfile.name ); 73 | 74 | // If file contents is null, { read: false }, just execute file as-is 75 | // on disk 76 | if( file.isNull() ){ 77 | say( util.format( 78 | 'Gulpfile, %s, contents is empty. Reading directly from disk...', 79 | gulpfile.name 80 | ) ); 81 | } 82 | 83 | // If file contents is a buffer, write a temp file and run that instead 84 | if( file.isBuffer() ) { 85 | 86 | say( 'File is a buffer. Need to write buffer to temp file...' ); 87 | 88 | var tmpGulpfileName = util.format( 89 | '%s.tmp.%s%s', 90 | path.basename( gulpfile.name, gulpfile.ext ), 91 | new Date().getTime(), 92 | gulpfile.ext 93 | ); 94 | 95 | // Tweak gulpfile info to account for temp file 96 | gulpfile.origPath = gulpfile.path; 97 | gulpfile.path = path.join( gulpfile.base, tmpGulpfileName ); 98 | gulpfile.tmpPath = gulpfile.path; 99 | gulpfile.origRelPath = gulpfile.relPath; 100 | gulpfile.relPath = path.relative( process.cwd(), gulpfile.path ); 101 | gulpfile.name = tmpGulpfileName; 102 | 103 | say( util.format( 104 | 'Writing buffer to %s...', 105 | gutil.colors.magenta( gulpfile.relPath ) 106 | ) ); 107 | 108 | // Write tmp file to disk 109 | fs.writeFileSync( gulpfile.path, file.contents ); 110 | } 111 | 112 | // Find local gulp cli script 113 | var localGulpPackage = null; 114 | var localGulpPackageBase = null; 115 | var localGulpCliPath = null; 116 | try { 117 | localGulpPackageBase = path.dirname( resolve.sync( 'gulp', { basedir: gulpfile.base } ) ); 118 | localGulpPackage = require( path.join( localGulpPackageBase, 'package.json' ) ); 119 | localGulpCliPath = path.resolve( path.join( localGulpPackageBase, localGulpPackage.bin.gulp ) ); 120 | } catch( err ) { 121 | sayErr( util.format( 122 | 'Problem finding locally-installed `gulp` for gulpfile %s. ' + 123 | '(Try running `npm install gulp` from %s to install a local ' + 124 | 'gulp for said gulpfile.)\n\n%s', 125 | gutil.colors.magenta( gulpfile.origPath ), 126 | gutil.colors.magenta( gulpfile.base ), 127 | err 128 | ) ); 129 | return callback(); 130 | } 131 | 132 | // Construct command and args 133 | var cmd = opts.nodeCmd; 134 | 135 | var args = [ 136 | localGulpCliPath, '--gulpfile', gulpfile.name 137 | ].concat(opts.tasks); 138 | 139 | // Concatinate additional command-line arguments if provided 140 | if ( _.isArray( opts.args ) || _.isString( opts.args ) ) { 141 | args = args.concat( opts.args ); 142 | } 143 | 144 | say( 145 | 'Spawning process ' + gutil.colors.magenta( localGulpCliPath ) + 146 | ' with args ' + gutil.colors.magenta( args.join( ' ' ) ) + 147 | ' from directory ' + gutil.colors.magenta( gulpfile.base ) + '...' 148 | ); 149 | 150 | // Execute local gulpfile cli script 151 | var spawnedGulp = spawn( cmd, args, { cwd: gulpfile.base } ); 152 | 153 | // Log output coming from gulpfile stdout and stderr 154 | var logGulpfileOutput = ( function () { 155 | 156 | // Should prefix next output. 157 | var newline = true; 158 | 159 | return function ( data ) { 160 | var output = data.toString(); 161 | if ( newline ) { 162 | say( util.format( '(%s) %s', 163 | gutil.colors.magenta( gulpfile.relPath ), 164 | output 165 | ), true ); 166 | } else { 167 | process.stdout.write( output ); 168 | } 169 | 170 | // If this batch of gulpfile output didn't terminate a 171 | // line, we'll just pass the next batch through. 172 | // This allows tasks that show progress (such as tests) to 173 | // write partial lines without us splitting them up. 174 | newline = output.substr( -1 ) === '\n'; 175 | } 176 | })(); 177 | 178 | // Remove temp file if one exists 179 | var cleanupTmpFile = function () { 180 | try { 181 | if( gulpfile.tmpPath ) { 182 | say( util.format( 'Removing temp file %s', gulpfile.tmpPath ) ); 183 | fs.unlinkSync( gulpfile.tmpPath ); 184 | } 185 | } catch ( e ) { 186 | // Wrap in try/catch because when executed due to ctrl+c, 187 | // we can't unlink the file 188 | } 189 | }; 190 | 191 | // Handle errors in gulpfile 192 | spawnedGulp.on( 'error', function ( error ) { 193 | sayErr( util.format( 194 | 'Error executing gulpfile %s:\n\n%s', 195 | gutil.colors.magenta( gulpfile.path ), 196 | error 197 | ) ); 198 | } ); 199 | 200 | // Handle gulpfile stdout and stderr 201 | spawnedGulp.stdout.on( 'data', logGulpfileOutput ); 202 | spawnedGulp.stderr.on( 'data', logGulpfileOutput ); 203 | 204 | // Clean up temp gulpfile exit 205 | spawnedGulp.on( 'exit', function ( exitCode ) { 206 | cleanupTmpFile(); 207 | 208 | if ( exitCode === 0 ) { 209 | say( 'Returning to parent gulpfile...' ); 210 | } else { 211 | sayErr( util.format( 212 | 'Gulpfile %s exited with an error :(', 213 | gutil.colors.magenta( gulpfile.path ) 214 | ) ); 215 | } 216 | 217 | // Run the stream callback 218 | callback(); 219 | 220 | // Run user callback 221 | userCallback(); 222 | } ); 223 | 224 | // Clean up temp gulpfile if on ctrl + c 225 | process.on( 'SIGINT', cleanupTmpFile ); 226 | } ); 227 | }; 228 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-chug", 3 | "version": "0.5.1", 4 | "description": "Run external gulpfiles as part of a gulp task inside another gulpfile", 5 | "repository": "https://github.com/robatron/gulp-chug.git", 6 | "main": "index.js", 7 | "keywords": [ 8 | "gulpplugin", 9 | "hub", 10 | "nest", 11 | "inception", 12 | "cascade", 13 | "domino", 14 | "grunt-hub", 15 | "gulp-hub" 16 | ], 17 | "author": "Rob McGuire-Dale ", 18 | "contributors": [ 19 | "Derek Peterson ", 20 | "Harry Wolff ", 21 | "Evgenus ", 22 | "Mike O'Brien ", 23 | "Phillip Green II " 24 | ], 25 | "license": "MIT", 26 | "dependencies": { 27 | "gulp-util": "^3.0.7", 28 | "lodash": "^4.0.0", 29 | "resolve": "^1.1.6", 30 | "through2": "^2.0.0" 31 | }, 32 | "devDependencies": { 33 | "gulp": "^3.9.0", 34 | "gulp-istanbul": "^0.10.3", 35 | "gulp-mocha": "^2.2.0", 36 | "gulp-replace": "^0.5.4", 37 | "gulp-util": "^3.0.7", 38 | "mocha": "^2.3.4", 39 | "proxyquire": "^1.7.3", 40 | "should": "^8.1.1", 41 | "sinon": "^1.17.2" 42 | }, 43 | "scripts": { 44 | "test": "gulp --gulpfile test/gulp-chug-integrate.js && gulp test", 45 | "publish-patch": "npm version patch && git push origin master --tags && npm publish" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /proj.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": "." 7 | } 8 | ], 9 | "settings": 10 | { 11 | "default_line_ending": "unix", 12 | "translate_tabs_to_spaces": true, 13 | "trim_trailing_white_space_on_save": true, 14 | "tab_size": 4, 15 | "ensure_newline_at_eof_on_save": true, 16 | "trim_trailing_white_space_on_save": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/gulp-chug-integrate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * gulp-chug integration "tests" to assure a working state in a less-contrived 5 | * environment. 6 | * 7 | * This is basically a normal gulpfile that runs gulp-chug through some common 8 | * scenarios. 9 | * 10 | * Integration "tests" pass if this gulpfile runs without error. 11 | */ 12 | 13 | var gulp = require( 'gulp' ); 14 | var replace = require( 'gulp-replace' ); 15 | var chug = require( '../index.js' ); 16 | 17 | // Happy path 18 | gulp.task( 'happy', function () { 19 | return gulp.src( './subproj/gulpfile.js' ) 20 | .pipe( chug() ); 21 | } ); 22 | 23 | // Custom gulpfile file name 24 | gulp.task( 'custom-filename', function () { 25 | return gulp.src( './subproj/gulpfile-custom-name.js' ) 26 | .pipe( chug() ); 27 | } ); 28 | 29 | // Custom gulpfile file name 30 | gulp.task( 'custom-filename-multiple-tasks', function () { 31 | return gulp.src( './subproj/gulpfile-custom-name.js' ) 32 | .pipe( chug({tasks:['task1', 'task2']}) ); 33 | } ); 34 | 35 | // Nested gulpfile 36 | gulp.task( 'deep-nest', function () { 37 | return gulp.src( './subproj/subdir/gulpfile.js' ) 38 | .pipe( chug() ); 39 | } ); 40 | 41 | gulp.task( 'deep-nest-multiple-tasks', function () { 42 | return gulp.src( './subproj/subdir/gulpfile.js' ) 43 | .pipe( chug({tasks:['task1', 'task2']}) ); 44 | } ); 45 | 46 | // Glob multiple gulpfiles 47 | gulp.task( 'glob', function () { 48 | return gulp.src( './subproj/**/gulpfile*.js' ) 49 | .pipe( chug() ); 50 | } ); 51 | 52 | gulp.task( 'glob-multiple-tasks', function () { 53 | return gulp.src( './subproj/**/gulpfile*.js' ) 54 | .pipe( chug({tasks:['task1', 'task2']}) ); 55 | } ); 56 | 57 | // Non-existant gulpfile 58 | gulp.task( 'non-existant', function () { 59 | return gulp.src( './subproj/non-existant-file.js' ) 60 | .pipe( chug() ); 61 | } ); 62 | 63 | // No-read option 64 | gulp.task( 'no-read', function () { 65 | return gulp.src( './subproj/gulpfile.js', { read: false } ) 66 | .pipe( chug() ); 67 | } ); 68 | 69 | // Mess with the gulpfile before running 70 | gulp.task( 'modify-before', function () { 71 | return gulp.src( './subproj/gulpfile.js' ) 72 | .pipe( replace( 'Hello', 'Goodbye' ) ) 73 | .pipe( chug() ); 74 | } ); 75 | 76 | gulp.task( 'default', [ 77 | 'happy', 78 | 'custom-filename', 79 | 'custom-filename-multiple-tasks', 80 | 'deep-nest', 81 | 'deep-nest-multiple-tasks', 82 | 'glob', 83 | 'glob-multiple-tasks', 84 | 'non-existant', 85 | 'no-read', 86 | 'modify-before' 87 | ] ); 88 | -------------------------------------------------------------------------------- /test/gulp-chug-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * gulp-chug test spec file 5 | * 6 | * TODO: Use sinon sandboxes to suppress console output from gulp-chug 7 | */ 8 | 9 | var util = require( 'util' ); 10 | var path = require( 'path' ); 11 | var _ = require( 'lodash' ); 12 | var pequire = require( 'proxyquire' ).noCallThru(); 13 | var sinon = require( 'sinon' ); 14 | var should = require( 'should' ); 15 | var gutil = require( 'gulp-util' ); 16 | 17 | var CHUG_PATH = '../index.js'; 18 | 19 | // Happy-path proxy dependencies 20 | var proxyDeps = { 21 | fs: { 22 | writeFileSync: _.noop 23 | }, 24 | path: { 25 | relative: _.noop, 26 | dirname: function () { return 'path-dirname-return' }, 27 | basename: function () { return 'path-basename-return' }, 28 | extname: _.noop, 29 | join: function () { return 'path-join-return' }, 30 | resolve: function () { return 'path-resolve-return' } 31 | }, 32 | resolve: { 33 | sync: _.noop 34 | }, 35 | 'path-join-return': { 36 | bin: { 37 | gulp: 'gulp-cli-bin' 38 | } 39 | }, 40 | child_process: { 41 | spawn: function () { 42 | return { 43 | on: _.noop, 44 | stdout: { on: _.noop }, 45 | stderr: { on: _.noop } 46 | }; 47 | } 48 | }, 49 | './package.json': { 50 | name: 'gulp-chug-proxy' 51 | } 52 | }; 53 | 54 | // Return proxy dependencies with optional overrides 55 | var getProxyDeps = function ( overrides ) { 56 | return _.assign( {}, proxyDeps, overrides || {} ); 57 | }; 58 | 59 | describe( 'gulp-chug', function () { 60 | 61 | it( 'emits an error if supplied a stream', function ( done ) { 62 | var chug = require( CHUG_PATH ); 63 | var stream = chug(); 64 | var streamFile = { 65 | isNull: function () { return false }, 66 | isStream: function () { return true } 67 | }; 68 | stream.on( 'error', function ( err ) { 69 | err.message.should.equal( 'Streams are not supported yet. Pull requests welcome :)' ); 70 | done(); 71 | } ); 72 | stream.write( streamFile ); 73 | } ); 74 | 75 | it( 'creates a temporary gulpfile if supplied a buffer', function () { 76 | var pdeps = getProxyDeps( { 77 | fs: { 78 | writeFileSync: sinon.spy() 79 | } 80 | } ); 81 | var chug = pequire( CHUG_PATH, pdeps ); 82 | var stream = chug(); 83 | var streamFile = { 84 | isNull: function () { return false }, 85 | isStream: function () { return false }, 86 | isBuffer: function () { return true }, 87 | contents: 'file-contents' 88 | }; 89 | stream.write( streamFile ); 90 | 91 | pdeps.fs.writeFileSync.calledOnce.should.be.true; 92 | pdeps.fs.writeFileSync.calledWith( 'path-join-return', streamFile.contents ).should.be.true; 93 | } ); 94 | 95 | describe( 'during failures to find a local gulp', function () { 96 | 97 | var ERR_MSG_BEGIN = 'Problem finding locally-installed `gulp` for gulpfile'; 98 | 99 | var streamFile = { 100 | isNull: function () { return false }, 101 | isStream: function () { return false }, 102 | isBuffer: function () { return false } 103 | }; 104 | 105 | it( 'emits an error if a local gulp cannot be found', function ( done ) { 106 | var pdeps = getProxyDeps( { 107 | resolve: { sync: function () { throw new Error() } } 108 | } ); 109 | var chug = pequire( CHUG_PATH, pdeps ); 110 | var stream = chug(); 111 | stream.on( 'error', function ( err ) { 112 | err.message.should.startWith( ERR_MSG_BEGIN ); 113 | done(); 114 | } ); 115 | stream.write( streamFile ); 116 | } ); 117 | 118 | it( 'emits an error if the local gulp\'s package file cannot be parsed', function ( done ) { 119 | var pdeps = getProxyDeps( { 120 | path: _.assign( {}, getProxyDeps().path, { 121 | dirname: function () { return 'dirname-return' }, 122 | join: function ( a, b ) { 123 | if( a === 'dirname-return' && b === 'package.json' ) { 124 | throw new Error(); 125 | } 126 | } 127 | } ) 128 | } ); 129 | var chug = pequire( CHUG_PATH, pdeps ); 130 | var stream = chug(); 131 | stream.on( 'error', function ( err ) { 132 | err.message.should.startWith( ERR_MSG_BEGIN ); 133 | done(); 134 | } ); 135 | stream.write( streamFile ); 136 | } ); 137 | 138 | it( 'emits an error if the local gulp\'s package file does not contain a binary path', function ( done ) { 139 | var pdeps = getProxyDeps( { 'path-join-return': {} } ); 140 | var chug = pequire( CHUG_PATH, pdeps ); 141 | var stream = chug(); 142 | stream.on( 'error', function ( err ) { 143 | err.message.should.startWith( ERR_MSG_BEGIN ); 144 | done(); 145 | } ); 146 | stream.write( streamFile ); 147 | } ); 148 | } ); 149 | 150 | it( 'spawns a process to execute the target gulpfile', function () { 151 | var pdeps = getProxyDeps( { 152 | child_process: { 153 | spawn: sinon.spy( function () { 154 | return { 155 | on: _.noop, 156 | stdout: { on: _.noop }, 157 | stderr: { on: _.noop } 158 | }; 159 | } ) 160 | } 161 | } ); 162 | var chug = pequire( CHUG_PATH, pdeps ); 163 | var stream = chug(); 164 | var streamFile = { 165 | isNull: function () { return false }, 166 | isStream: function () { return false }, 167 | isBuffer: function () { return false } 168 | }; 169 | stream.write( streamFile ); 170 | 171 | pdeps.child_process.spawn.calledOnce.should.be.true; 172 | pdeps.child_process.spawn.calledWith( 173 | 'node', 174 | [ 'path-resolve-return', '--gulpfile', 'path-basename-return', 'default' ], 175 | { cwd: 'path-dirname-return' } 176 | ).should.be.true; 177 | } ); 178 | 179 | it( 'supports multiple tasks if provided', function () { 180 | 181 | // Additional string argument 182 | var pdeps = getProxyDeps( { 183 | child_process: { 184 | spawn: sinon.spy( function () { 185 | return { 186 | on: _.noop, 187 | stdout: { on: _.noop }, 188 | stderr: { on: _.noop } 189 | }; 190 | } ) 191 | } 192 | } ); 193 | var chug = pequire( CHUG_PATH, pdeps ); 194 | var stream = chug( { 195 | tasks: ['task1', 'task2'] 196 | } ); 197 | var streamFile = { 198 | isNull: function () { return false }, 199 | isStream: function () { return false }, 200 | isBuffer: function () { return false } 201 | }; 202 | stream.write( streamFile ); 203 | 204 | pdeps.child_process.spawn.calledOnce.should.be.true; 205 | pdeps.child_process.spawn.calledWith( 206 | 'node', 207 | [ 208 | 'path-resolve-return', '--gulpfile', 'path-basename-return', 209 | 'task1', 'task2' 210 | ], 211 | { cwd: 'path-dirname-return' } 212 | ).should.be.true; 213 | } ); 214 | 215 | it( 'supports additional command-line arguments if provided', function () { 216 | 217 | // Additional string argument 218 | var pdeps = getProxyDeps( { 219 | child_process: { 220 | spawn: sinon.spy( function () { 221 | return { 222 | on: _.noop, 223 | stdout: { on: _.noop }, 224 | stderr: { on: _.noop } 225 | }; 226 | } ) 227 | } 228 | } ); 229 | var chug = pequire( CHUG_PATH, pdeps ); 230 | var stream = chug( { 231 | args: 'fake-arg-1' 232 | } ); 233 | var streamFile = { 234 | isNull: function () { return false }, 235 | isStream: function () { return false }, 236 | isBuffer: function () { return false } 237 | }; 238 | stream.write( streamFile ); 239 | 240 | pdeps.child_process.spawn.calledOnce.should.be.true; 241 | pdeps.child_process.spawn.calledWith( 242 | 'node', 243 | [ 244 | 'path-resolve-return', '--gulpfile', 'path-basename-return', 245 | 'default', 'fake-arg-1' 246 | ], 247 | { cwd: 'path-dirname-return' } 248 | ).should.be.true; 249 | 250 | // Additinal array arguments 251 | pdeps.child_process.spawn.reset(); 252 | chug = pequire( CHUG_PATH, pdeps ); 253 | stream = chug( { 254 | args: [ 'fake-arg-2', 'fake-arg-3' ] 255 | } ); 256 | streamFile = { 257 | isNull: function () { return false }, 258 | isStream: function () { return false }, 259 | isBuffer: function () { return false } 260 | }; 261 | stream.write( streamFile ); 262 | 263 | pdeps.child_process.spawn.calledOnce.should.be.true; 264 | pdeps.child_process.spawn.calledWith( 265 | 'node', 266 | [ 267 | 'path-resolve-return', '--gulpfile', 'path-basename-return', 268 | 'default', 'fake-arg-2', 'fake-arg-3' 269 | ], 270 | { cwd: 'path-dirname-return' } 271 | ).should.be.true(); 272 | } ); 273 | 274 | it( 'supports a callback to be executed after the target gulpfile has completed executing', function () { 275 | var cbSpy = sinon.spy(); 276 | 277 | var pdeps = getProxyDeps( { 278 | child_process: { 279 | spawn: function () { 280 | return { 281 | on: function ( event, callback ) { 282 | if ( event === 'exit' ) { 283 | callback( 0 ); 284 | } 285 | }, 286 | stdout: { on: _.noop }, 287 | stderr: { on: _.noop } 288 | }; 289 | } 290 | } 291 | } ); 292 | var chug = pequire( CHUG_PATH, pdeps ); 293 | 294 | var stream = chug( cbSpy ); 295 | var streamFile = { 296 | isNull: function () { return false }, 297 | isStream: function () { return false }, 298 | isBuffer: function () { return false } 299 | }; 300 | stream.write( streamFile ); 301 | cbSpy.calledOnce.should.be.true(); 302 | 303 | cbSpy.reset() 304 | stream = chug( {}, cbSpy ); 305 | var streamFile = { 306 | isNull: function () { return false }, 307 | isStream: function () { return false }, 308 | isBuffer: function () { return false } 309 | }; 310 | stream.write( streamFile ); 311 | cbSpy.calledOnce.should.be.true(); 312 | } ); 313 | 314 | it( 'handles spawn errors', function ( done ) { 315 | var ERR_MSG_BEGIN = 'Error executing gulpfile'; 316 | var pdeps = getProxyDeps( { 317 | child_process: { 318 | spawn: function () { 319 | return { 320 | on: function ( event, fn ) { if ( event === 'error' ) fn() }, 321 | stdout: { on: _.noop }, 322 | stderr: { on: _.noop } 323 | }; 324 | } 325 | } 326 | } ); 327 | var chug = pequire( CHUG_PATH, pdeps ); 328 | var stream = chug(); 329 | stream.on( 'error', function ( err ) { 330 | err.message.should.startWith( ERR_MSG_BEGIN ); 331 | done(); 332 | } ); 333 | var streamFile = { 334 | isNull: function () { return false }, 335 | isStream: function () { return false }, 336 | isBuffer: function () { return false } 337 | }; 338 | stream.write( streamFile ); 339 | } ); 340 | 341 | it( 'handles non-zero exit codes from spawned child gulpfiles', function ( done ) { 342 | var ERR_MSG_PATTERN = /Gulpfile .* exited with an error :\(/; 343 | var pdeps = getProxyDeps( { 344 | child_process: { 345 | spawn: function () { 346 | return { 347 | on: function ( event, fn ) { if ( event === 'exit' ) fn( 1 ) }, 348 | stdout: { on: _.noop }, 349 | stderr: { on: _.noop } 350 | }; 351 | } 352 | } 353 | } ); 354 | var chug = pequire( CHUG_PATH, pdeps ); 355 | var stream = chug(); 356 | var streamFile = { 357 | isNull: function () { return false }, 358 | isStream: function () { return false }, 359 | isBuffer: function () { return false } 360 | }; 361 | stream.on( 'error', function ( err ) { 362 | err.message.should.match( ERR_MSG_PATTERN ); 363 | done(); 364 | } ); 365 | stream.write( streamFile ); 366 | } ); 367 | 368 | it( 'outputs stdout and stderr of the target gulpfile during execution', function () { 369 | var stdoutSpy = new sinon.spy(); 370 | var stderrSpy = new sinon.spy(); 371 | var pdeps = getProxyDeps( { 372 | child_process: { 373 | spawn: function () { 374 | return { 375 | on: _.noop, 376 | stdout: { on: stdoutSpy }, 377 | stderr: { on: stderrSpy } 378 | }; 379 | } 380 | } 381 | } ); 382 | var chug = pequire( CHUG_PATH, pdeps ); 383 | var stream = chug(); 384 | var streamFile = { 385 | isNull: function () { return false }, 386 | isStream: function () { return false }, 387 | isBuffer: function () { return false } 388 | }; 389 | stream.write( streamFile ); 390 | 391 | stdoutSpy.calledOnce.should.be.true(); 392 | stdoutSpy.calledWith( 'data' ).should.be.true(); 393 | stderrSpy.calledOnce.should.be.true(); 394 | stderrSpy.calledWith( 'data' ).should.be.true(); 395 | } ); 396 | 397 | it( 'cleans up any temporary gulpfiles on exit', function () { 398 | var pdeps = getProxyDeps( { 399 | fs: { 400 | writeFileSync: _.noop, 401 | unlinkSync: sinon.spy() 402 | }, 403 | child_process: { 404 | spawn: function () { 405 | return { 406 | on: function ( event, fn ) { 407 | if ( event === 'exit' ) fn( 0 ); 408 | }, 409 | stdout: { on: _.noop }, 410 | stderr: { on: _.noop } 411 | }; 412 | } 413 | } 414 | } ); 415 | var chug = pequire( CHUG_PATH, pdeps ); 416 | var stream = chug(); 417 | var streamFile = { 418 | isNull: function () { return false }, 419 | isStream: function () { return false }, 420 | isBuffer: function () { return true } 421 | }; 422 | stream.write( streamFile ); 423 | pdeps.fs.unlinkSync.calledOnce.should.be.true(); 424 | pdeps.fs.unlinkSync.calledWith( 'path-join-return' ).should.be.true(); 425 | } ); 426 | } ); 427 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --ui bdd 3 | -------------------------------------------------------------------------------- /test/subproj/gulpfile-custom-name.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require( 'util' ); 4 | var gulp = require( 'gulp' ); 5 | var gutil = require( 'gulp-util' ); 6 | 7 | gulp.task( 'default', function () { 8 | gutil.log( util.format( 'Hello from %s!', __filename ) ); 9 | } ); 10 | 11 | gulp.task('task1', function() { 12 | gutil.log( util.format( 'Task1 from %s!', __filename ) ); 13 | }); 14 | 15 | gulp.task('task2', function() { 16 | gutil.log( util.format( 'Task1 from %s!', __filename ) ); 17 | }); 18 | -------------------------------------------------------------------------------- /test/subproj/gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require( 'util' ); 4 | var gulp = require( 'gulp' ); 5 | var gutil = require( 'gulp-util' ); 6 | 7 | gulp.task( 'default', function () { 8 | gutil.log( util.format( 'Hello from %s!', __filename ) ); 9 | } ); 10 | 11 | gulp.task('task1', function() { 12 | gutil.log( util.format( 'Task1 from %s!', __filename ) ); 13 | }); 14 | 15 | gulp.task('task2', function() { 16 | gutil.log( util.format( 'Task1 from %s!', __filename ) ); 17 | }); 18 | -------------------------------------------------------------------------------- /test/subproj/subdir/gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require( 'util' ); 4 | var gulp = require( 'gulp' ); 5 | var gutil = require( 'gulp-util' ); 6 | 7 | gulp.task( 'default', function () { 8 | gutil.log( util.format( 'Hello from %s!', __filename ) ); 9 | } ); 10 | 11 | gulp.task('task1', function() { 12 | gutil.log( util.format( 'Task1 from %s!', __filename ) ); 13 | }); 14 | 15 | gulp.task('task2', function() { 16 | gutil.log( util.format( 'Task1 from %s!', __filename ) ); 17 | }); 18 | --------------------------------------------------------------------------------