├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .npmrc ├── .travis.yml ├── .verb.md ├── LICENSE ├── README.md ├── appveyor.yml ├── changelog.md ├── docs ├── events.md └── execution.md ├── examples ├── dynamic-tasks-events.js ├── dynamic-tasks.js ├── generator.js ├── mixed.js ├── parallel-skip.js ├── parallel.js ├── series.js └── task-noop.js ├── index.js ├── lib ├── generator.js ├── parse.js ├── task.js ├── tasks.js ├── timer.js └── utils.js ├── package.json └── test ├── app.default-generator.js ├── app.findGenerator.js ├── app.generate.js ├── app.generator.js ├── app.getGenerator.js ├── app.register.js ├── app.task.js ├── app.tasks.js ├── app.toAlias.js ├── events.js ├── parallel.js ├── parse-tasks.js ├── series.js ├── task.js ├── tasks.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [{**/{actual,fixtures,expected,templates}/**,*.md}] 13 | trim_trailing_whitespace = false 14 | insert_final_newline = false 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | 6 | "env": { 7 | "browser": false, 8 | "es6": true, 9 | "node": true, 10 | "mocha": true 11 | }, 12 | 13 | "parserOptions":{ 14 | "ecmaVersion": 9, 15 | "sourceType": "module", 16 | "ecmaFeatures": { 17 | "modules": true, 18 | "experimentalObjectRestSpread": true 19 | } 20 | }, 21 | 22 | "globals": { 23 | "document": false, 24 | "navigator": false, 25 | "window": false 26 | }, 27 | 28 | "rules": { 29 | "accessor-pairs": 2, 30 | "arrow-spacing": [2, { "before": true, "after": true }], 31 | "block-spacing": [2, "always"], 32 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 33 | "comma-dangle": [2, "never"], 34 | "comma-spacing": [2, { "before": false, "after": true }], 35 | "comma-style": [2, "last"], 36 | "constructor-super": 2, 37 | "curly": [2, "multi-line"], 38 | "dot-location": [2, "property"], 39 | "eol-last": 2, 40 | "eqeqeq": [2, "allow-null"], 41 | "generator-star-spacing": [2, { "before": true, "after": true }], 42 | "handle-callback-err": [2, "^(err|error)$" ], 43 | "indent": [2, 2, { "SwitchCase": 1 }], 44 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 45 | "keyword-spacing": [2, { "before": true, "after": true }], 46 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 47 | "new-parens": 2, 48 | "no-array-constructor": 2, 49 | "no-caller": 2, 50 | "no-class-assign": 2, 51 | "no-cond-assign": 2, 52 | "no-const-assign": 2, 53 | "no-control-regex": 2, 54 | "no-debugger": 2, 55 | "no-delete-var": 2, 56 | "no-dupe-args": 2, 57 | "no-dupe-class-members": 2, 58 | "no-dupe-keys": 2, 59 | "no-duplicate-case": 2, 60 | "no-empty-character-class": 2, 61 | "no-eval": 2, 62 | "no-ex-assign": 2, 63 | "no-extend-native": 2, 64 | "no-extra-bind": 2, 65 | "no-extra-boolean-cast": 2, 66 | "no-extra-parens": [2, "functions"], 67 | "no-fallthrough": 2, 68 | "no-floating-decimal": 2, 69 | "no-func-assign": 2, 70 | "no-implied-eval": 2, 71 | "no-inner-declarations": [2, "functions"], 72 | "no-invalid-regexp": 2, 73 | "no-irregular-whitespace": 2, 74 | "no-iterator": 2, 75 | "no-label-var": 2, 76 | "no-labels": 2, 77 | "no-lone-blocks": 2, 78 | "no-mixed-spaces-and-tabs": 2, 79 | "no-multi-spaces": 2, 80 | "no-multi-str": 2, 81 | "no-multiple-empty-lines": [2, { "max": 1 }], 82 | "no-native-reassign": 0, 83 | "no-negated-in-lhs": 2, 84 | "no-new": 2, 85 | "no-new-func": 2, 86 | "no-new-object": 2, 87 | "no-new-require": 2, 88 | "no-new-wrappers": 2, 89 | "no-obj-calls": 2, 90 | "no-octal": 2, 91 | "no-octal-escape": 2, 92 | "no-proto": 0, 93 | "no-redeclare": 2, 94 | "no-regex-spaces": 2, 95 | "no-return-assign": 2, 96 | "no-self-compare": 2, 97 | "no-sequences": 2, 98 | "no-shadow-restricted-names": 2, 99 | "no-spaced-func": 2, 100 | "no-sparse-arrays": 2, 101 | "no-this-before-super": 2, 102 | "no-throw-literal": 2, 103 | "no-trailing-spaces": 0, 104 | "no-undef": 2, 105 | "no-undef-init": 2, 106 | "no-unexpected-multiline": 2, 107 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 108 | "no-unreachable": 2, 109 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 110 | "no-useless-call": 0, 111 | "no-with": 2, 112 | "one-var": [0, { "initialized": "never" }], 113 | "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], 114 | "padded-blocks": [0, "never"], 115 | "quotes": [2, "single", "avoid-escape"], 116 | "radix": 2, 117 | "semi": [2, "always"], 118 | "semi-spacing": [2, { "before": false, "after": true }], 119 | "space-before-blocks": [2, "always"], 120 | "space-before-function-paren": [2, "never"], 121 | "space-in-parens": [2, "never"], 122 | "space-infix-ops": 2, 123 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 124 | "spaced-comment": [0, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 125 | "use-isnan": 2, 126 | "valid-typeof": 2, 127 | "wrap-iife": [2, "any"], 128 | "yoda": [2, "never"] 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text eol=lf 3 | 4 | # binaries 5 | *.ai binary 6 | *.psd binary 7 | *.jpg binary 8 | *.gif binary 9 | *.png binary 10 | *.jpeg binary 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # always ignore files 2 | *.DS_Store 3 | .idea 4 | .vscode 5 | *.sublime-* 6 | 7 | # test related, or directories generated by tests 8 | test/actual 9 | actual 10 | coverage 11 | .nyc* 12 | 13 | # npm 14 | node_modules 15 | npm-debug.log 16 | 17 | # yarn 18 | yarn.lock 19 | yarn-error.log 20 | 21 | # misc 22 | _gh_pages 23 | _draft 24 | _drafts 25 | bower_components 26 | vendor 27 | temp 28 | tmp 29 | TODO.md 30 | package-lock.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | os: 3 | - linux 4 | - osx 5 | language: node_js 6 | node_js: 7 | - node 8 | - '10' 9 | - '9' 10 | - '8' 11 | -------------------------------------------------------------------------------- /.verb.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | 4 | ```js 5 | // Create an instance of `Composer` 6 | const Composer = require('{%= name %}'); 7 | const composer = new Composer(); 8 | 9 | // Define tasks with the .task() method 10 | composer.task('foo', callback => { 11 | callback(); // do stuff 12 | }); 13 | composer.task('bar', callback => { 14 | callback(); // do stuff 15 | }); 16 | 17 | composer.task('baz', ['foo'. 'bar']); 18 | 19 | // Run tasks with the .build() method 20 | composer.build('baz') 21 | .then(() => console.log('done!')) 22 | .catch(console.error); 23 | ``` 24 | 25 | ## API 26 | 27 | ### Tasks 28 | {%= apidocs("lib/tasks.js") %} 29 | 30 | ### Generators 31 | {%= apidocs("lib/generator.js") %} 32 | 33 | 34 | ## Events 35 | 36 | ### task 37 | 38 | ```js 39 | app.on('task', function(task) { 40 | switch (task.status) { 41 | case 'starting': 42 | // Task is running 43 | break; 44 | case 'finished': 45 | // Task is finished running 46 | break; 47 | } 48 | }); 49 | ``` 50 | 51 | ### task-pending 52 | 53 | Emitted after a task is registered. 54 | 55 | 56 | ### task-preparing 57 | 58 | Emitted when a task is preparing to run, right before it's called. You can use this event to dynamically skip tasks by updating `task.skip` to `true` or a function. 59 | 60 | 61 | ## Release history 62 | 63 | See the [changelog](./CHANGELOG.md). 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present, Brian Woodward. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # composer [![NPM version](https://img.shields.io/npm/v/composer.svg?style=flat)](https://www.npmjs.com/package/composer) [![NPM monthly downloads](https://img.shields.io/npm/dm/composer.svg?style=flat)](https://npmjs.org/package/composer) [![NPM total downloads](https://img.shields.io/npm/dt/composer.svg?style=flat)](https://npmjs.org/package/composer) [![Linux Build Status](https://img.shields.io/travis/doowb/composer.svg?style=flat&label=Travis)](https://travis-ci.org/doowb/composer) [![Windows Build Status](https://img.shields.io/appveyor/ci/doowb/composer.svg?style=flat&label=AppVeyor)](https://ci.appveyor.com/project/doowb/composer) 2 | 3 | > Run and compose async tasks. Easily define groups of tasks to run in series or parallel. 4 | 5 | Please consider following this project's author, [Brian Woodward](https://github.com/doowb), and consider starring the project to show your :heart: and support. 6 | 7 | - [Install](#install) 8 | - [Usage](#usage) 9 | - [API](#api) 10 | * [Tasks](#tasks) 11 | * [Generators](#generators) 12 | - [Events](#events) 13 | * [task](#task) 14 | * [task-pending](#task-pending) 15 | * [task-preparing](#task-preparing) 16 | - [Release history](#release-history) 17 | - [About](#about) 18 | 19 | _(TOC generated by [verb](https://github.com/verbose/verb) using [markdown-toc](https://github.com/jonschlinkert/markdown-toc))_ 20 | 21 | ## Install 22 | 23 | Install with [npm](https://www.npmjs.com/): 24 | 25 | ```sh 26 | $ npm install --save composer 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```js 32 | // Create an instance of `Composer` 33 | const Composer = require('composer'); 34 | const composer = new Composer(); 35 | 36 | // Define tasks with the .task() method 37 | composer.task('foo', callback => { 38 | callback(); // do stuff 39 | }); 40 | composer.task('bar', callback => { 41 | callback(); // do stuff 42 | }); 43 | 44 | composer.task('baz', ['foo'. 'bar']); 45 | 46 | // Run tasks with the .build() method 47 | composer.build('baz') 48 | .then(() => console.log('done!')) 49 | .catch(console.error); 50 | ``` 51 | 52 | ## API 53 | 54 | ### [.factory](lib/tasks.js#L27) 55 | 56 | Factory for creating a custom `Tasks` class that extends the given `Emitter`. Or, simply call the factory function to use the built-in emitter. 57 | 58 | **Params** 59 | 60 | * `Emitter` **{function}**: Event emitter. 61 | * `returns` **{Class}**: Returns a custom `Tasks` class. 62 | 63 | **Example** 64 | 65 | ```js 66 | // custom emitter 67 | const Emitter = require('events'); 68 | const Tasks = require('composer/lib/tasks')(Emitter); 69 | // built-in emitter 70 | const Tasks = require('composer/lib/tasks')(); 71 | const composer = new Tasks(); 72 | ``` 73 | 74 | ### [Tasks](lib/tasks.js#L42) 75 | 76 | Create an instance of `Tasks` with the given `options`. 77 | 78 | **Params** 79 | 80 | * `options` **{object}** 81 | 82 | **Example** 83 | 84 | ```js 85 | const Tasks = require('composer').Tasks; 86 | const composer = new Tasks(); 87 | ``` 88 | 89 | ### [.task](lib/tasks.js#L86) 90 | 91 | Define a task. Tasks run asynchronously, either in series (by default) or parallel (when `options.parallel` is true). In order for the build to determine when a task is complete, _one of the following_ things must happen: 1) the callback must be called, 2) a promise must be returned, or 3) a stream must be returned. Inside tasks, the "this" object is a composer Task instance created for each task with useful properties like the task name, options and timing information, which can be useful for logging, etc. 92 | 93 | **Params** 94 | 95 | * `name` **{String}**: The task name. 96 | * `deps` **{Object|Array|String|Function}**: Any of the following: task dependencies, callback(s), or options object, defined in any order. 97 | * `callback` **{Function}**: (optional) If the last argument is a function, it will be called after all of the task's dependencies have been run. 98 | * `returns` **{undefined}** 99 | 100 | **Example** 101 | 102 | ```js 103 | // 1. callback 104 | app.task('default', cb => { 105 | // do stuff 106 | cb(); 107 | }); 108 | // 2. promise 109 | app.task('default', () => { 110 | return Promise.resolve(null); 111 | }); 112 | // 3. stream (using vinyl-fs or your stream of choice) 113 | app.task('default', function() { 114 | return vfs.src('foo/*.js'); 115 | }); 116 | ``` 117 | 118 | ### [.build](lib/tasks.js#L209) 119 | 120 | Run one or more tasks. 121 | 122 | **Params** 123 | 124 | * `tasks` **{object|array|string|function}**: One or more tasks to run, options, or callback function. If no tasks are defined, the default task is automatically run. 125 | * `callback` **{function}**: (optional) 126 | * `returns` **{undefined}** 127 | 128 | **Example** 129 | 130 | ```js 131 | const build = app.series(['foo', 'bar', 'baz']); 132 | // promise 133 | build().then(console.log).catch(console.error); 134 | // or callback 135 | build(function() { 136 | if (err) return console.error(err); 137 | }); 138 | ``` 139 | 140 | ### [.series](lib/tasks.js#L251) 141 | 142 | Compose a function to run the given tasks in series. 143 | 144 | **Params** 145 | 146 | * `tasks` **{object|array|string|function}**: Tasks to run, options, or callback function. If no tasks are defined, the `default` task is automatically run, if one exists. 147 | * `callback` **{function}**: (optional) 148 | * `returns` **{promise|undefined}**: Returns a promise if no callback is passed. 149 | 150 | **Example** 151 | 152 | ```js 153 | const build = app.series(['foo', 'bar', 'baz']); 154 | // promise 155 | build().then(console.log).catch(console.error); 156 | // or callback 157 | build(function() { 158 | if (err) return console.error(err); 159 | }); 160 | ``` 161 | 162 | ### [.parallel](lib/tasks.js#L304) 163 | 164 | Compose a function to run the given tasks in parallel. 165 | 166 | **Params** 167 | 168 | * `tasks` **{object|array|string|function}**: Tasks to run, options, or callback function. If no tasks are defined, the `default` task is automatically run, if one exists. 169 | * `callback` **{function}**: (optional) 170 | * `returns` **{promise|undefined}**: Returns a promise if no callback is passed. 171 | 172 | **Example** 173 | 174 | ```js 175 | // call the returned function to start the build 176 | const build = app.parallel(['foo', 'bar', 'baz']); 177 | // promise 178 | build().then(console.log).catch(console.error); 179 | // callback 180 | build(function() { 181 | if (err) return console.error(err); 182 | }); 183 | // example task usage 184 | app.task('default', build); 185 | ``` 186 | 187 | ### [.create](lib/tasks.js#L388) 188 | 189 | Static method for creating a custom Tasks class with the given `Emitter. 190 | 191 | **Params** 192 | 193 | * `Emitter` **{Function}** 194 | * `returns` **{Class}**: Returns the custom class. 195 | 196 | ### [.create](lib/generator.js#L30) 197 | 198 | Static factory method for creating a custom `Composer` class that extends the given `Emitter`. 199 | 200 | **Params** 201 | 202 | * `Emitter` **{Function}**: Event emitter. 203 | * `returns` **{Class}**: Returns a custom `Composer` class. 204 | 205 | **Example** 206 | 207 | ```js 208 | // Composer extends a basic event emitter by default 209 | const Composer = require('composer'); 210 | const composer = new Composer(); 211 | 212 | // Create a custom Composer class with your even emitter of choice 213 | const Emitter = require('some-emitter'); 214 | const CustomComposer = Composer.create(Emitter); 215 | const composer = new CustomComposer(); 216 | ``` 217 | 218 | **Params** 219 | 220 | * `name` **{String}** 221 | * `options` **{Object}** 222 | * `returns` **{Object}**: Returns an instance of Composer. 223 | 224 | **Example** 225 | 226 | ```js 227 | const composer = new Composer(); 228 | ``` 229 | 230 | Create a wrapped generator function with the given `name`, `config`, and `fn`. 231 | 232 | **Params** 233 | 234 | * `name` **{String}** 235 | * `config` **{Object}**: (optional) 236 | * `fn` **{Function}** 237 | * `returns` **{Function}** 238 | 239 | Returns true if the given value is a Composer generator object. 240 | 241 | **Params** 242 | 243 | * `val` **{Object}** 244 | * `returns` **{Boolean}** 245 | 246 | ### [.register](lib/generator.js#L167) 247 | 248 | Alias to `.setGenerator`. 249 | 250 | **Params** 251 | 252 | * `name` **{String}**: The generator's name 253 | * `options` **{Object|Function|String}**: or generator 254 | * `generator` **{Object|Function|String}**: Generator function, instance or filepath. 255 | * `returns` **{Object}**: Returns the generator instance. 256 | 257 | **Example** 258 | 259 | ```js 260 | app.register('foo', function(app, base) { 261 | // "app" is a private instance created for the generator 262 | // "base" is a shared instance 263 | }); 264 | ``` 265 | 266 | ### [.generator](lib/generator.js#L190) 267 | 268 | Get and invoke generator `name`, or register generator `name` with the given `val` and `options`, then invoke and return the generator instance. This method differs from `.register`, which lazily invokes generator functions when `.generate` is called. 269 | 270 | **Params** 271 | 272 | * `name` **{String}** 273 | * `fn` **{Function|Object}**: Generator function, instance or filepath. 274 | * `returns` **{Object}**: Returns the generator instance or undefined if not resolved. 275 | 276 | **Example** 277 | 278 | ```js 279 | app.generator('foo', function(app, options) { 280 | // "app" - private instance created for generator "foo" 281 | // "options" - options passed to the generator 282 | }); 283 | ``` 284 | 285 | ### [.setGenerator](lib/generator.js#L222) 286 | 287 | Store a generator by file path or instance with the given `name` and `options`. 288 | 289 | **Params** 290 | 291 | * `name` **{String}**: The generator's name 292 | * `options` **{Object|Function|String}**: or generator 293 | * `generator` **{Object|Function|String}**: Generator function, instance or filepath. 294 | * `returns` **{Object}**: Returns the generator instance. 295 | 296 | **Example** 297 | 298 | ```js 299 | app.setGenerator('foo', function(app, options) { 300 | // "app" - new instance of Generator created for generator "foo" 301 | // "options" - options passed to the generator 302 | }); 303 | ``` 304 | 305 | ### [.getGenerator](lib/generator.js#L247) 306 | 307 | Get generator `name` from `app.generators`, same as [findGenerator], but also invokes the returned generator with the current instance. Dot-notation may be used for getting sub-generators. 308 | 309 | **Params** 310 | 311 | * `name` **{String}**: Generator name. 312 | * `returns` **{Object|undefined}**: Returns the generator instance or undefined. 313 | 314 | **Example** 315 | 316 | ```js 317 | const foo = app.getGenerator('foo'); 318 | 319 | // get a sub-generator 320 | const baz = app.getGenerator('foo.bar.baz'); 321 | ``` 322 | 323 | ### [.findGenerator](lib/generator.js#L280) 324 | 325 | Find generator `name`, by first searching the cache, then searching the cache of the `base` generator. Use this to get a generator without invoking it. 326 | 327 | **Params** 328 | 329 | * `name` **{String}** 330 | * `options` **{Function}**: Optionally supply a rename function on `options.toAlias` 331 | * `returns` **{Object|undefined}**: Returns the generator instance if found, or undefined. 332 | 333 | **Example** 334 | 335 | ```js 336 | // search by "alias" 337 | const foo = app.findGenerator('foo'); 338 | 339 | // search by "full name" 340 | const foo = app.findGenerator('generate-foo'); 341 | ``` 342 | 343 | **Params** 344 | 345 | * `name` **{String}** 346 | * `returns` **{Boolean}** 347 | 348 | **Example** 349 | 350 | ```js 351 | console.log(app.hasGenerator('foo')); 352 | console.log(app.hasGenerator('foo.bar')); 353 | ``` 354 | 355 | ### [.generate](lib/generator.js#L362) 356 | 357 | Run one or more tasks or sub-generators and returns a promise. 358 | 359 | **Params** 360 | 361 | * `name` **{String}** 362 | * `tasks` **{String|Array}** 363 | * `returns` **{Promise}** 364 | 365 | **Events** 366 | 367 | * `emits`: `generate` with the generator `name` and the array of `tasks` that are queued to run. 368 | 369 | **Example** 370 | 371 | ```js 372 | // run tasks `bar` and `baz` on generator `foo` 373 | app.generate('foo', ['bar', 'baz']); 374 | 375 | // or use shorthand 376 | app.generate('foo:bar,baz'); 377 | 378 | // run the `default` task on generator `foo` 379 | app.generate('foo'); 380 | 381 | // run the `default` task on the `default` generator, if defined 382 | app.generate(); 383 | ``` 384 | 385 | ### [.toAlias](lib/generator.js#L413) 386 | 387 | Create a generator alias from the given `name`. By default, `generate-` is stripped from beginning of the generator name. 388 | 389 | **Params** 390 | 391 | * `name` **{String}** 392 | * `options` **{Object}** 393 | * `returns` **{String}**: Returns the alias. 394 | 395 | **Example** 396 | 397 | ```js 398 | // customize the alias 399 | const app = new Generate({ toAlias: require('camel-case') }); 400 | ``` 401 | 402 | ### [.isGenerators](lib/generator.js#L434) 403 | 404 | Returns true if every name in the given array is a registered generator. 405 | 406 | **Params** 407 | 408 | * `names` **{Array}** 409 | * `returns` **{Boolean}** 410 | 411 | ### [.formatError](lib/generator.js#L446) 412 | 413 | Format task and generator errors. 414 | 415 | **Params** 416 | 417 | * `name` **{String}** 418 | * `returns` **{Error}** 419 | 420 | ### [.disableInspect](lib/generator.js#L466) 421 | 422 | Disable inspect. Returns a function to re-enable inspect. Useful for debugging. 423 | 424 | ### [.base](lib/generator.js#L504) 425 | 426 | Get the first ancestor instance of Composer. Only works if `generator.parent` is 427 | defined on child instances. 428 | 429 | ### [.name](lib/generator.js#L517) 430 | 431 | Get or set the generator name. 432 | 433 | **Params** 434 | 435 | * **{String}** 436 | 437 | * `returns` **{String}** 438 | 439 | ### [.alias](lib/generator.js#L534) 440 | 441 | Get or set the generator `alias`. By default, the generator alias is created 442 | by passing the generator name to the [.toAlias](#toAlias) method. 443 | 444 | **Params** 445 | 446 | * **{String}** 447 | 448 | * `returns` **{String}** 449 | 450 | ### [.namespace](lib/generator.js#L551) 451 | 452 | Get the generator namespace. The namespace is created by joining the generator's `alias` 453 | to the alias of each ancestor generator. 454 | 455 | **Params** 456 | 457 | * **{String}** 458 | 459 | * `returns` **{String}** 460 | 461 | ### [.depth](lib/generator.js#L564) 462 | 463 | Get the depth of a generator - useful for debugging. The root generator 464 | has a depth of `0`, sub-generators add `1` for each level of nesting. 465 | 466 | * `returns` **{Number}** 467 | 468 | ### [Composer#parse](lib/generator.js#L577) 469 | 470 | Static method that returns a function for parsing task arguments. 471 | 472 | **Params** 473 | 474 | * `register` **{Function}**: Function that receives a name of a task or generator that cannot be found by the parse function. This allows the `register` function to dynamically register tasks or generators. 475 | * `returns` **{Function}**: Returns a function for parsing task args. 476 | 477 | ### [Composer#isGenerator](lib/generator.js#L590) 478 | 479 | Static method that returns true if the given `val` is an instance of Generate. 480 | 481 | **Params** 482 | 483 | * `val` **{Object}** 484 | * `returns` **{Boolean}** 485 | 486 | ### [Composer#create](lib/generator.js#L603) 487 | 488 | Static method for creating a custom Composer class with the given `Emitter. 489 | 490 | **Params** 491 | 492 | * `Emitter` **{Function}** 493 | * `returns` **{Class}**: Returns the custom class. 494 | 495 | ### [Composer#Tasks](lib/generator.js#L617) 496 | 497 | Static getter for getting the Tasks class with the same `Emitter` class as Composer. 498 | 499 | **Params** 500 | 501 | * `Emitter` **{Function}** 502 | * `returns` **{Class}**: Returns the Tasks class. 503 | 504 | ### [Composer#Task](lib/generator.js#L633) 505 | 506 | Static getter for getting the `Task` class. 507 | 508 | **Example** 509 | 510 | ```js 511 | const { Task } = require('composer'); 512 | ``` 513 | 514 | ## Events 515 | 516 | ### task 517 | 518 | ```js 519 | app.on('task', function(task) { 520 | switch (task.status) { 521 | case 'starting': 522 | // Task is running 523 | break; 524 | case 'finished': 525 | // Task is finished running 526 | break; 527 | } 528 | }); 529 | ``` 530 | 531 | ### task-pending 532 | 533 | Emitted after a task is registered. 534 | 535 | ### task-preparing 536 | 537 | Emitted when a task is preparing to run, right before it's called. You can use this event to dynamically skip tasks by updating `task.skip` to `true` or a function. 538 | 539 | ## Release history 540 | 541 | See the [changelog](./CHANGELOG.md). 542 | 543 | ## About 544 | 545 |
546 | Contributing 547 | 548 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). 549 | 550 |
551 | 552 |
553 | Running Tests 554 | 555 | 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: 556 | 557 | ```sh 558 | $ npm install && npm test 559 | ``` 560 | 561 |
562 | 563 |
564 | Building docs 565 | 566 | _(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.)_ 567 | 568 | To generate the readme, run the following command: 569 | 570 | ```sh 571 | $ npm install -g verbose/verb#dev verb-generate-readme && verb 572 | ``` 573 | 574 |
575 | 576 | ### Related projects 577 | 578 | You might also be interested in these projects: 579 | 580 | * [assemble](https://www.npmjs.com/package/assemble): Get the rocks out of your socks! Assemble makes you fast at creating web projects… [more](https://github.com/assemble/assemble) | [homepage](https://github.com/assemble/assemble "Get the rocks out of your socks! Assemble makes you fast at creating web projects. Assemble is used by thousands of projects for rapid prototyping, creating themes, scaffolds, boilerplates, e-books, UI components, API documentation, blogs, building websit") 581 | * [enquirer](https://www.npmjs.com/package/enquirer): Stylish, intuitive and user-friendly prompt system. Fast and lightweight enough for small projects, powerful and… [more](https://github.com/enquirer/enquirer) | [homepage](https://github.com/enquirer/enquirer "Stylish, intuitive and user-friendly prompt system. Fast and lightweight enough for small projects, powerful and extensible enough for the most advanced use cases.") 582 | * [generate](https://www.npmjs.com/package/generate): Command line tool and developer framework for scaffolding out new GitHub projects. Generate offers the… [more](https://github.com/generate/generate) | [homepage](https://github.com/generate/generate "Command line tool and developer framework for scaffolding out new GitHub projects. Generate offers the robustness and configurability of Yeoman, the expressiveness and simplicity of Slush, and more powerful flow control and composability than either.") 583 | * [update](https://www.npmjs.com/package/update): Be scalable! Update is a new, open source developer framework and CLI for automating updates… [more](https://github.com/update/update) | [homepage](https://github.com/update/update "Be scalable! Update is a new, open source developer framework and CLI for automating updates of any kind in code projects.") 584 | * [verb](https://www.npmjs.com/package/verb): Documentation generator for GitHub projects. Verb is extremely powerful, easy to use, and is used… [more](https://github.com/verbose/verb) | [homepage](https://github.com/verbose/verb "Documentation generator for GitHub projects. Verb is extremely powerful, easy to use, and is used on hundreds of projects of all sizes to generate everything from API docs to readmes.") 585 | 586 | ### Contributors 587 | 588 | | **Commits** | **Contributor** | 589 | | --- | --- | 590 | | 227 | [doowb](https://github.com/doowb) | 591 | | 72 | [jonschlinkert](https://github.com/jonschlinkert) | 592 | 593 | ### Author 594 | 595 | **Brian Woodward** 596 | 597 | * [GitHub Profile](https://github.com/doowb) 598 | * [Twitter Profile](https://twitter.com/doowb) 599 | * [LinkedIn Profile](https://linkedin.com/in/woodwardbrian) 600 | 601 | ### License 602 | 603 | Copyright © 2018, [Brian Woodward](https://github.com/doowb). 604 | Released under the [MIT License](LICENSE). 605 | 606 | *** 607 | 608 | _This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.8.0, on November 11, 2018._ -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against this version of Node.js 2 | environment: 3 | matrix: 4 | # node.js 5 | - nodejs_version: "9.0" 6 | - nodejs_version: "8.0" 7 | 8 | # Install scripts. (runs after repo cloning) 9 | install: 10 | # Get the latest stable version of Node.js or io.js 11 | - ps: Install-Product node $env:nodejs_version 12 | # install modules 13 | - npm install 14 | 15 | # Post-install test scripts. 16 | test_script: 17 | # Output useful info for debugging. 18 | - node --version 19 | - npm --version 20 | # run tests 21 | - npm test 22 | 23 | # Don't actually build. 24 | build: off 25 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ### v3.0.0 2 | 3 | **Breaking changes** 4 | 5 | - Support for custom inspect methods was removed. Please follow Node's [recommended practices](https://nodejs.org/api/util.html#util_util_inspect_custom) for creating a custom inspect method. 6 | 7 | ### v3.0.0 8 | 9 | - Completely refactored from the ground up. 10 | - For the most part, `.task`, `.build`, `.series` and `.parallel` work the same way. However, event handling has changed. Please see the readme for more information and documentation. 11 | 12 | ### v2.0.0 13 | 14 | - Now requires Node.js v4.0 or higher 15 | 16 | ### v1.0.0 17 | 18 | - Updates the events that are emitted and adds statuses to the objects emitted on the events. see issues [#20](../../issues/20) and [#21](../../issues/21) 19 | - Updates the event objects to expose human readable durations. [see issue #23](../../issues/23) 20 | - Removes unused properties. [see issue #24](../../issues/24) 21 | - Updates `.build` to return a promise when the callback is not passed in. [see issue #28](../../issues/28) 22 | 23 | ### v0.14.0 24 | 25 | - Updates [bach][] to `1.0.0`. 26 | - Errors emitted from inside a task now have the `'in task "foo":'` prefixed to the error message. [see issue #22](../../issues/22) 27 | - Expose `.runInfo` on the task object for use in event listeners and task functions. 28 | - Add `.duration` to the `.run/.runInfo` object that shows the duration in a human friendly format. This will also show the current duration from the time the task started to the time it's called if used inside a task function. [see issue #23](../../issues/23) 29 | 30 | ```js 31 | app.task('foo', function(cb) { 32 | console.log(this.runInfo.duration); 33 | }); 34 | ``` 35 | 36 | ### v0.13.0 37 | 38 | - Skip tasks by setting the `options.skip` option to the name of the task or an array of task names. 39 | - Making additional `err` properties non-enumerable to cut down on error output. 40 | 41 | ### v0.12.0 42 | 43 | - You can no longer get a task from the `.task()` method by passing only the name. Instead do `var task = app.tasks[name];` 44 | - Passing only a name and no dependencies to `.task()` will result in a `noop` task being created. 45 | - `options` may be passed to `.build()`, `.series()` and `.parallel()` 46 | - `options` passed to `.build()` will be merged onto task options before running the task. 47 | - Skip tasks by setting their `options.run` option to `false`. 48 | 49 | ### v0.11.3 50 | 51 | - Allow passing es2015 javascript generator functions to `.task()`. 52 | 53 | ### v0.11.2 54 | 55 | - Allow using glob patterns for task dependencies. 56 | 57 | ### v0.11.0 58 | 59 | - **BREAKING CHANGE**: Removed `.watch()`. Watch functionality can be added to [base][] applications using [base-watch][]. 60 | 61 | ### v0.10.0 62 | 63 | - Removes `session`. 64 | 65 | ### v0.9.0 66 | 67 | - Use `default` when no tasks are passed to `.build()`. 68 | 69 | ### v0.8.4 70 | 71 | - Ensure task dependencies are unique. 72 | 73 | ### v0.8.2 74 | 75 | - Emitting `task` when adding a task through `.task()` 76 | - Returning task when calling `.task(name)` with only a name. 77 | 78 | ### v0.8.0 79 | 80 | - Emitting `task:*` events instead of generic `*` events. See [event docs](#events) for more information. 81 | 82 | ### v0.7.0 83 | 84 | - No longer returning the current task when `.task()` is called without a name. 85 | - Throwing an error when `.task()` is called without a name. 86 | 87 | ### v0.6.0 88 | 89 | - Adding properties to `err` instances and emitting instead of emitting multiple parameters. 90 | - Adding series and parallel flows/methods. 91 | 92 | ### v0.5.0 93 | 94 | - **BREAKING CHANGE** Renamed `.run()` to `.build()` 95 | 96 | ### v0.4.2 97 | 98 | - `.watch` returns an instance of `FSWatcher` 99 | 100 | ### v0.4.1 101 | 102 | - Currently running task returned when calling `.task()` without a name. 103 | 104 | ### v0.4.0 105 | 106 | - Add session-cache to enable per-task data contexts. 107 | 108 | ### v0.3.0 109 | 110 | - Event bubbling/emitting changed. 111 | 112 | ### v0.1.0 113 | 114 | - Initial release. 115 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | [composer][] is an event emitter that may emit the following events: 2 | 3 | ### build 4 | 5 | This event is emitted when the build is starting and when it's finished. The event emits an object containing the build runtime information. 6 | 7 | ```js 8 | app.on('build', build => {}); 9 | ``` 10 | 11 | #### `build` properties 12 | 13 | * `.app` (object) - instance of Composer 14 | * `.status` (string) - current build status[^1], either `register`, `starting` or `finished`. 15 | * `.date` (object) - with a `.start` property indicating start time as a `Date` object. 16 | * `.hr` (object) - with a `.start` property indicating the start time as an `hrtime` array. 17 | * `.duration` (string) - that will provide the duration in a human readable format. 18 | * `.diff` (string) - diff between the start and end times. 19 | * `.offset` (string) offset between the start date and the start `hr` time 20 | 21 | [^1]: When `build.status` is `finished`, the `.hr` object also has `.duration` and `.diff` properties containing timing information calculated using `process.hrtime`. 22 | 23 | ### task 24 | 25 | This event is emitted when the task is registered, starting, and when it's finished. The event emits 2 arguments, the current instance of the task object and an object containing the task runtime information. 26 | 27 | ```js 28 | app.on('task', (task, run) => {}); 29 | ``` 30 | 31 | #### `task` properties 32 | 33 | * `.status` (string) - current status[^2] of the task. May be `register`, `starting`, or `finished`. 34 | 35 | #### `run` properties 36 | 37 | * `.date` (object) - has a `.start` property indicating the start time as a `Date` object. 38 | * `.hr` (object) - has a `.start` property indicating the start time as an `hrtime` array. 39 | * `.duration` (string) that will provide the duration in a human readable format. 40 | * `.diff` (string) that will provide the diff between the start and end times. 41 | * `.offset` (string) offset between the start date and the start hr time 42 | 43 | [^2]: When `task.status` is `finished`, the `.hr` object also has `.duration` and `.diff` properties containing timing information calculated using `process.hrtime`. 44 | 45 | 46 | ### error 47 | 48 | This event is emitted when an error occurrs during a `build`. The event emits an `Error` object with extra properties for debugging the _build and task_ that were running when the error occurred. 49 | 50 | ```js 51 | app.on('error', err => {}); 52 | ``` 53 | 54 | #### err properties 55 | 56 | * `app`: current composer instance running the build 57 | * `build`: current build runtime information 58 | * `task`: current task instance running when the error occurred 59 | * `run`: current task runtime information 60 | -------------------------------------------------------------------------------- /docs/execution.md: -------------------------------------------------------------------------------- 1 | When an individual task is run, a new [Run](lib/run.js) instance is created with start, end, and duration information. This `run` object is emitted with [some events](#taskstarting) and also exposed on the `task` instance as the `.runInfo` property. 2 | 3 | ### properties 4 | 5 | The `run` instance has the the following properties 6 | 7 | **.date** 8 | 9 | The `.date` property is an object containing the `.start` and `.end` date timestamps created with `new Date()`. 10 | 11 | **.hr** 12 | 13 | The `.hr` property is an object containing the `.start`, `.end` and `.duration` properties that are created by using `process.hrtime()`. These properties are the actual arrays returned from `process.hrtime()`. 14 | There is also `.diff` and `.offset` computed properties that use the other properties to calculate the difference between `.start` and `.end` times (`.diff`) and the offset (error for time calculations) between the `.duration` and the `.diff` (this is usually very small). 15 | 16 | **.duration** 17 | 18 | The `.duration` property is a computed property that uses [pretty-time][] to format the `.hr.duration` value into a human readable format. 19 | -------------------------------------------------------------------------------- /examples/dynamic-tasks-events.js: -------------------------------------------------------------------------------- 1 | const Composer = require('..'); 2 | const app = new Composer({ skip: [] }); 3 | const task = cb => cb(); 4 | 5 | app.on('build', function(build) { 6 | console.log('build', build.status, build.time.duration); 7 | }); 8 | 9 | app.on('task', function(task) { 10 | if (task.status === 'finished' || task.status === 'starting') { 11 | console.log(task.status, 'task', task.name, task.time.duration); 12 | 13 | if (task.status === 'finished') { 14 | app.options.skip.push(task.name); 15 | } 16 | } 17 | }); 18 | 19 | app.task('foo', ['qux', 'fez'], task); 20 | app.task('bar', ['qux', 'fez'], task); 21 | app.task('baz', ['qux', 'fez'], task); 22 | app.task('qux', task); 23 | app.task('fez', task); 24 | 25 | app.task('default', ['foo', 'bar', 'baz']); 26 | app.build('default') 27 | .then(() => console.log('done')) 28 | .catch(console.error); 29 | -------------------------------------------------------------------------------- /examples/dynamic-tasks.js: -------------------------------------------------------------------------------- 1 | const names = []; 2 | const Composer = require('..'); 3 | const app = new Composer({skip: names}); 4 | 5 | const task = function(cb) { 6 | console.log('running', this.name); 7 | names.push(this.name); 8 | cb(); 9 | }; 10 | 11 | app.task('foo', ['qux', 'fez'], task); 12 | app.task('bar', ['qux', 'fez'], task); 13 | app.task('baz', ['qux', 'fez'], task); 14 | app.task('qux', task); 15 | app.task('fez', task); 16 | 17 | app.task('default', ['foo', 'bar', 'baz']); 18 | app.build('default') 19 | .then(() => console.log('done')) 20 | .catch(console.error); 21 | -------------------------------------------------------------------------------- /examples/generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.time('total'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const util = require('util'); 7 | const Composer = require('..'); 8 | const composer = new Composer(); 9 | const writeFile = util.promisify(fs.writeFile); 10 | const argv = require('minimist')(process.argv.slice(2)); 11 | 12 | composer.register('git', git => { 13 | git.task('ignore', () => { 14 | return writeFile('.gitignore', '*.sublime-*'); 15 | }); 16 | 17 | git.task('attributes', () => { 18 | return writeFile('.gitattributes', '* text eol=lf'); 19 | }); 20 | 21 | git.task('default', ['ignore', 'attributes']); 22 | }); 23 | 24 | composer.register('npm', npm => { 25 | npm.task('default', () => { 26 | return writeFile('.npmrc', 'package-lock=false'); 27 | }); 28 | }); 29 | 30 | composer.register('foo', foo => { 31 | foo.register('dotfiles', dotfiles => { 32 | dotfiles.task('git', () => composer.generate('git')); 33 | dotfiles.task('npm', () => composer.generate('npm')); 34 | dotfiles.task('default', ['git', 'npm']); 35 | }); 36 | }); 37 | 38 | composer.generate(argv._.length ? argv._ : 'git') 39 | .then(() => console.timeEnd('total')) 40 | .catch(console.log) 41 | 42 | // class App { 43 | // constructor(alias) { 44 | // this.alias = alias; 45 | // this.cache = {}; 46 | // } 47 | 48 | // register(alias, fn) { 49 | // this.cache[alias] = fn; 50 | // return this; 51 | // } 52 | 53 | // generate(alias) { 54 | // const fn = this.cache[alias]; 55 | // const app = new App(alias); 56 | // fn(app); 57 | // return app; 58 | // } 59 | // } 60 | 61 | // const app = new App('base'); 62 | 63 | // app.register('foo', function(foo) { 64 | // console.log('GENERATOR ALIAS:', foo); 65 | // }); 66 | 67 | // app.register('bar', function(bar) { 68 | // console.log('GENERATOR ALIAS:', bar); 69 | // }); 70 | 71 | // app.register('baz', function(baz) { 72 | // console.log('GENERATOR ALIAS:', baz); 73 | // }); 74 | 75 | // app.generate('foo'); 76 | // app.generate('bar'); 77 | // app.generate('baz'); 78 | 79 | // console.log(app) 80 | -------------------------------------------------------------------------------- /examples/mixed.js: -------------------------------------------------------------------------------- 1 | const Composer = require('..'); 2 | const app = new Composer(); 3 | let names = []; 4 | 5 | app.task('foo', function(cb) { 6 | setTimeout(function() { 7 | names.push('foo'); 8 | cb(); 9 | }, 20); 10 | }); 11 | 12 | app.task('bar', function(cb) { 13 | names.push('bar'); 14 | cb(); 15 | }); 16 | 17 | app.task('baz', function(cb) { 18 | setTimeout(function() { 19 | names.push('baz'); 20 | cb(); 21 | }, 10); 22 | }); 23 | 24 | app.task('names', function(cb) { 25 | setTimeout(function() { 26 | console.log(names); 27 | names = []; 28 | cb(); 29 | }, 30); 30 | }); 31 | 32 | app.task('one', app.series(['foo', 'bar', 'baz', 'names'])); 33 | app.task('two', app.parallel(['foo', 'bar', 'baz', 'names'])); 34 | app.task('three', app.series(['foo', 'bar', 'baz', 'names'])); 35 | app.task('default', ['one', 'two', 'three']); 36 | app.build({ parallel: true }, err => err && console.error(err)); 37 | -------------------------------------------------------------------------------- /examples/parallel-skip.js: -------------------------------------------------------------------------------- 1 | const Composer = require('..'); 2 | const app = new Composer({ skip: ['bar', 'baz'] }); 3 | let names = []; 4 | 5 | app.task('foo', function(cb) { 6 | setTimeout(function() { 7 | names.push('foo'); 8 | cb(); 9 | }, 20); 10 | }); 11 | 12 | app.task('bar', function(cb) { 13 | names.push('bar'); 14 | cb(); 15 | }); 16 | 17 | app.task('baz', function(cb) { 18 | setTimeout(function() { 19 | names.push('baz'); 20 | cb(); 21 | }, 10); 22 | }); 23 | 24 | app.task('names', function(cb) { 25 | setTimeout(function() { 26 | console.log(names); 27 | names = []; 28 | cb(); 29 | }, 30); 30 | }); 31 | 32 | app.task('one', app.parallel(['foo', 'bar', 'baz', 'names'])); 33 | app.task('two', app.parallel(['foo', 'bar', 'baz', 'names'])); 34 | app.task('default', ['one', 'two']); 35 | app.build(err => err && console.error(err)); 36 | -------------------------------------------------------------------------------- /examples/parallel.js: -------------------------------------------------------------------------------- 1 | const Composer = require('..'); 2 | const app = new Composer(); 3 | let names = []; 4 | 5 | app.task('foo', function(cb) { 6 | setTimeout(function() { 7 | names.push('foo'); 8 | cb(); 9 | }, 20); 10 | }); 11 | 12 | app.task('bar', function(cb) { 13 | names.push('bar'); 14 | cb(); 15 | }); 16 | 17 | app.task('baz', function(cb) { 18 | setTimeout(function() { 19 | names.push('baz'); 20 | cb(); 21 | }, 10); 22 | }); 23 | 24 | app.task('names', function(cb) { 25 | setTimeout(function() { 26 | console.log(names); 27 | names = []; 28 | cb(); 29 | }, 30); 30 | }); 31 | 32 | app.task('one', app.parallel(['foo', 'bar', 'baz', 'names'])); 33 | app.task('two', app.parallel(['foo', 'bar', 'baz', 'names'])); 34 | app.task('default', ['one', 'two']); 35 | app.build(err => err && console.error(err)); 36 | -------------------------------------------------------------------------------- /examples/series.js: -------------------------------------------------------------------------------- 1 | const Composer = require('..'); 2 | const app = new Composer(); 3 | let names = []; 4 | 5 | app.task('foo', function(cb) { 6 | setTimeout(function() { 7 | names.push('foo'); 8 | cb(); 9 | }, 20); 10 | }); 11 | 12 | app.task('bar', function(cb) { 13 | names.push('bar'); 14 | cb(); 15 | }); 16 | 17 | app.task('baz', function(cb) { 18 | setTimeout(function() { 19 | names.push('baz'); 20 | cb(); 21 | }, 10); 22 | }); 23 | 24 | app.task('names', function(cb) { 25 | setTimeout(function() { 26 | console.log(names); 27 | names = []; 28 | cb(); 29 | }, 30); 30 | }); 31 | 32 | app.task('one', app.series(['foo', 'bar', 'baz', 'names'])); 33 | app.task('two', app.series(['foo', 'bar', 'baz', 'names'])); 34 | app.task('default', ['one', 'two']); 35 | app.build('default') 36 | .then(() => console.log('done')) 37 | .catch(console.error); 38 | -------------------------------------------------------------------------------- /examples/task-noop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Composer = require('..'); 4 | const composer = new Composer(); 5 | 6 | /** 7 | * Listen for tasks 8 | */ 9 | 10 | composer.on('task', task => console.log(task.status.padStart(9), task.name)); 11 | 12 | /** 13 | * Example of a noop task with a callback. 14 | */ 15 | 16 | composer.task('noop-callback', cb => cb()); 17 | 18 | /** 19 | * Example of a noop task that returns a promise 20 | */ 21 | 22 | composer.task('noop-promise', () => Promise.resolve(null)); 23 | 24 | /** 25 | * Example of how to run them 26 | */ 27 | 28 | composer.build(['noop-promise', 'noop-callback']) 29 | .then(() => console.log('Done!')) 30 | .catch(console.error); 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/generator'); 2 | -------------------------------------------------------------------------------- /lib/generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const Task = require('./task'); 5 | const Tasks = require('./tasks'); 6 | const parse = require('./parse'); 7 | const Events = require('events'); 8 | const { define } = require('./utils'); 9 | 10 | /** 11 | * Static factory method for creating a custom `Composer` class that 12 | * extends the given `Emitter`. 13 | * 14 | * ```js 15 | * // Composer extends a basic event emitter by default 16 | * const Composer = require('composer'); 17 | * const composer = new Composer(); 18 | * 19 | * // Create a custom Composer class with your even emitter of choice 20 | * const Emitter = require('some-emitter'); 21 | * const CustomComposer = Composer.create(Emitter); 22 | * const composer = new CustomComposer(); 23 | * ``` 24 | * @name .create 25 | * @param {Function} `Emitter` Event emitter. 26 | * @return {Class} Returns a custom `Composer` class. 27 | * @api public 28 | */ 29 | 30 | const factory = (Emitter = Events) => { 31 | 32 | /** 33 | * Create a new instance of Composer. 34 | * 35 | * ```js 36 | * const composer = new Composer(); 37 | * ``` 38 | * @extends EventEmitter 39 | * @param {String} `name` 40 | * @param {Object} `options` 41 | * @return {Object} Returns an instance of Composer. 42 | * @api public 43 | */ 44 | 45 | class Generator extends Tasks.create(Emitter) { 46 | constructor(name, options = {}) { 47 | if (name && typeof name !== 'string') { 48 | options = name; 49 | name = void 0; 50 | } 51 | // ensure that options aren't passed to generic 52 | // emitter to prevent unintended side-effects 53 | super(!/(Event)?Emitter/i.test(Emitter.name) ? options : null); 54 | if (this.setMaxListeners) this.setMaxListeners(0); 55 | this.name = name; 56 | this.options = options; 57 | this.namespaces = new Map(); 58 | this.generators = new Map(); 59 | this.isGenerate = true; 60 | if (!this.use) { 61 | require('use')(this); 62 | } 63 | } 64 | 65 | /** 66 | * Create a wrapped generator function with the given `name`, `config`, and `fn`. 67 | * 68 | * @param {String} `name` 69 | * @param {Object} `config` (optional) 70 | * @param {Function} `fn` 71 | * @return {Function} 72 | * @api public 73 | */ 74 | 75 | toGenerator(name, config, fn) { 76 | if (typeof config === 'function' || this.isGenerator(config)) { 77 | fn = config; 78 | config = fn || {}; 79 | } 80 | 81 | let alias = this.toAlias(name); 82 | let generator = options => { 83 | if (generator.instance && generator.once !== false) { 84 | return generator.instance; 85 | } 86 | 87 | let opts = Object.assign({}, config, options); 88 | let app = this.isGenerator(fn) ? fn : new this.constructor(opts); 89 | this.run(app); 90 | 91 | generator.instance = app; 92 | generator.called++; 93 | fn.called = generator.called; 94 | 95 | app.createGenerator = generator; 96 | app.alias = alias; 97 | app.name = name; 98 | app.fn = fn; 99 | 100 | define(app, 'parent', this); 101 | this.emit('generator', app); 102 | 103 | // emit errors that happen on initialization 104 | let listeners = {}; 105 | let bubble = events => { 106 | for (let name of events) { 107 | let listener = listeners[name] || (listeners[name] = this.emit.bind(this, name)); 108 | app.off(name, listener); 109 | app.on(name, listener); 110 | } 111 | }; 112 | 113 | bubble(['error', 'task', 'build', 'plugin']); 114 | 115 | if (typeof fn === 'function') { 116 | fn.call(app, app, opts); 117 | // re-register emitters that we just registered a few lines ago, 118 | // to ensure that errors are bubbled up in the correct order 119 | bubble(['error', 'task', 'build', 'plugin']); 120 | } 121 | 122 | if (opts && opts.once === false) { 123 | generator.once = false; 124 | } 125 | return app; 126 | }; 127 | 128 | define(generator, 'name', alias); 129 | define(generator, 'parent', this); 130 | define(generator, 'instance', null); 131 | generator.called = 0; 132 | generator.isGenerator = true; 133 | generator.alias = alias; 134 | generator.fn = fn; 135 | return generator; 136 | } 137 | 138 | /** 139 | * Returns true if the given value is a Composer generator object. 140 | * 141 | * @param {Object} `val` 142 | * @return {Boolean} 143 | * @api public 144 | */ 145 | 146 | isGenerator(val) { 147 | return this.constructor.isGenerator(val); 148 | } 149 | 150 | /** 151 | * Alias to `.setGenerator`. 152 | * 153 | * ```js 154 | * app.register('foo', function(app, base) { 155 | * // "app" is a private instance created for the generator 156 | * // "base" is a shared instance 157 | * }); 158 | * ``` 159 | * @name .register 160 | * @param {String} `name` The generator's name 161 | * @param {Object|Function|String} `options` or generator 162 | * @param {Object|Function|String} `generator` Generator function, instance or filepath. 163 | * @return {Object} Returns the generator instance. 164 | * @api public 165 | */ 166 | 167 | register(...args) { 168 | return this.setGenerator(...args); 169 | } 170 | 171 | /** 172 | * Get and invoke generator `name`, or register generator `name` with 173 | * the given `val` and `options`, then invoke and return the generator 174 | * instance. This method differs from `.register`, which lazily invokes 175 | * generator functions when `.generate` is called. 176 | * 177 | * ```js 178 | * app.generator('foo', function(app, options) { 179 | * // "app" - private instance created for generator "foo" 180 | * // "options" - options passed to the generator 181 | * }); 182 | * ``` 183 | * @name .generator 184 | * @param {String} `name` 185 | * @param {Function|Object} `fn` Generator function, instance or filepath. 186 | * @return {Object} Returns the generator instance or undefined if not resolved. 187 | * @api public 188 | */ 189 | 190 | generator(name, options, fn) { 191 | if (typeof options === 'function') { 192 | fn = options; 193 | options = {}; 194 | } 195 | 196 | if (typeof fn !== 'function') { 197 | return this.getGenerator(name, options); 198 | } 199 | 200 | this.setGenerator(name, options, fn); 201 | return this.getGenerator(name); 202 | } 203 | 204 | /** 205 | * Store a generator by file path or instance with the given 206 | * `name` and `options`. 207 | * 208 | * ```js 209 | * app.setGenerator('foo', function(app, options) { 210 | * // "app" - new instance of Generator created for generator "foo" 211 | * // "options" - options passed to the generator 212 | * }); 213 | * ``` 214 | * @name .setGenerator 215 | * @param {String} `name` The generator's name 216 | * @param {Object|Function|String} `options` or generator 217 | * @param {Object|Function|String} `generator` Generator function, instance or filepath. 218 | * @return {Object} Returns the generator instance. 219 | * @api public 220 | */ 221 | 222 | setGenerator(name, options, fn) { 223 | const generator = this.toGenerator(name, options, fn); 224 | const alias = generator.alias; 225 | this.base.namespaces.set(`${this.namespace}.${alias}`, generator); 226 | this.generators.set(alias, generator); 227 | return this; 228 | } 229 | 230 | /** 231 | * Get generator `name` from `app.generators`, same as [findGenerator], but also invokes 232 | * the returned generator with the current instance. Dot-notation may be used for getting 233 | * sub-generators. 234 | * 235 | * ```js 236 | * const foo = app.getGenerator('foo'); 237 | * 238 | * // get a sub-generator 239 | * const baz = app.getGenerator('foo.bar.baz'); 240 | * ``` 241 | * @name .getGenerator 242 | * @param {String} `name` Generator name. 243 | * @return {Object|undefined} Returns the generator instance or undefined. 244 | * @api public 245 | */ 246 | 247 | getGenerator(name, options) { 248 | const fn = this.findGenerator(name); 249 | 250 | if (!this.isGenerator(fn)) { 251 | throw this.formatError(name); 252 | } 253 | 254 | if (typeof fn === 'function') { 255 | // return the generator instance if one has already created, 256 | // otherwise call the generator function with the parent instance 257 | return fn.instance || fn.call(fn.parent, options); 258 | } 259 | return fn; 260 | } 261 | 262 | /** 263 | * Find generator `name`, by first searching the cache, then searching the 264 | * cache of the `base` generator. Use this to get a generator without invoking it. 265 | * 266 | * ```js 267 | * // search by "alias" 268 | * const foo = app.findGenerator('foo'); 269 | * 270 | * // search by "full name" 271 | * const foo = app.findGenerator('generate-foo'); 272 | * ``` 273 | * @name .findGenerator 274 | * @param {String} `name` 275 | * @param {Function} `options` Optionally supply a rename function on `options.toAlias` 276 | * @return {Object|undefined} Returns the generator instance if found, or undefined. 277 | * @api public 278 | */ 279 | 280 | findGenerator(name) { 281 | if (!name) return null; 282 | let cached = this.base.namespaces.get(name); 283 | let names = []; 284 | let app = this; 285 | 286 | if (this.isGenerator(cached)) { 287 | return cached; 288 | } 289 | 290 | names = typeof name === 'string' 291 | ? name.split('.').map(n => this.toAlias(n)) 292 | : name; 293 | 294 | let key = names.join('.'); 295 | 296 | if (names.length === 1) { 297 | app = this.generators.get(key); 298 | 299 | } else { 300 | do { 301 | let alias = names.shift(); 302 | let gen = app.generators.get(alias); 303 | 304 | if (!this.isGenerator(gen)) { 305 | return null; 306 | } 307 | 308 | // only invoke the generator if it's not the last one 309 | if (names.length) { 310 | app = gen.instance || app.getGenerator(alias); 311 | } else { 312 | app = gen; 313 | } 314 | 315 | } while (app && names.length); 316 | } 317 | 318 | return this.isGenerator(app) ? app : null; 319 | } 320 | 321 | /** 322 | * Returns true if the given name is a registered generator. Dot-notation may be 323 | * used to check for sub-generators. 324 | * 325 | * ```js 326 | * console.log(app.hasGenerator('foo')); 327 | * console.log(app.hasGenerator('foo.bar')); 328 | * ``` 329 | * @param {String} `name` 330 | * @return {Boolean} 331 | * @api public 332 | */ 333 | 334 | hasGenerator(name) { 335 | return this.findGenerator(name) != null; 336 | } 337 | 338 | /** 339 | * Run one or more tasks or sub-generators and returns a promise. 340 | * 341 | * ```js 342 | * // run tasks `bar` and `baz` on generator `foo` 343 | * app.generate('foo', ['bar', 'baz']); 344 | * 345 | * // or use shorthand 346 | * app.generate('foo:bar,baz'); 347 | * 348 | * // run the `default` task on generator `foo` 349 | * app.generate('foo'); 350 | * 351 | * // run the `default` task on the `default` generator, if defined 352 | * app.generate(); 353 | * ``` 354 | * @name .generate 355 | * @emits `generate` with the generator `name` and the array of `tasks` that are queued to run. 356 | * @param {String} `name` 357 | * @param {String|Array} `tasks` 358 | * @return {Promise} 359 | * @api public 360 | */ 361 | 362 | async generate(...args) { 363 | let parsed = this.parseTasks(...args); 364 | let { tasks, missing, options, callback } = parsed; 365 | 366 | let promise = new Promise(async(resolve, reject) => { 367 | if (missing.length > 0) { 368 | reject(new Error('Invalid task(s) or generator(s): ' + missing.join(', '))); 369 | return; 370 | } 371 | 372 | let generator = name => { 373 | let app = this.hasGenerator(name) ? this.getGenerator(name, options) : this; 374 | this.emit('generate', app); 375 | return app; 376 | }; 377 | 378 | if (options.parallel === true) { 379 | let pending = []; 380 | for (let ele of tasks) pending.push(generator(ele.name).build(ele.tasks)); 381 | Promise.all(pending).then(resolve).catch(reject); 382 | } else { 383 | for (let ele of tasks) { 384 | await generator(ele.name).build(ele.tasks).catch(reject); 385 | } 386 | resolve(); 387 | } 388 | }); 389 | 390 | if (typeof callback === 'function') { 391 | promise.then(() => callback()).catch(callback); 392 | return; 393 | } 394 | 395 | return promise; 396 | } 397 | 398 | /** 399 | * Create a generator alias from the given `name`. By default, `generate-` 400 | * is stripped from beginning of the generator name. 401 | * 402 | * ```js 403 | * // customize the alias 404 | * const app = new Generate({ toAlias: require('camel-case') }); 405 | * ``` 406 | * @name .toAlias 407 | * @param {String} `name` 408 | * @param {Object} `options` 409 | * @return {String} Returns the alias. 410 | * @api public 411 | */ 412 | 413 | toAlias(name, options) { 414 | if (typeof options === 'function') { 415 | return options(name); 416 | } 417 | if (options && typeof options.toAlias === 'function') { 418 | return options.toAlias(name); 419 | } 420 | if (typeof this.options.toAlias === 'function') { 421 | return this.options.toAlias(name); 422 | } 423 | return name ? name.replace(/^generate-/, '') : ''; 424 | } 425 | 426 | /** 427 | * Returns true if every name in the given array is a registered generator. 428 | * @name .isGenerators 429 | * @param {Array} `names` 430 | * @return {Boolean} 431 | * @api public 432 | */ 433 | 434 | isGenerators(names) { 435 | return names.every(name => this.hasGenerator(name)); 436 | } 437 | 438 | /** 439 | * Format task and generator errors. 440 | * @name .formatError 441 | * @param {String} `name` 442 | * @return {Error} 443 | * @api public 444 | */ 445 | 446 | formatError(name, type = 'generator', appname = 'generator') { 447 | let key = this.namespace || 'default'; 448 | let suffix = '.'; 449 | 450 | // if not the base instance, remove the first name segment 451 | if (this !== this.base) { 452 | key = key.split('.').slice(1).join('.'); 453 | suffix = ` on ${appname} "${key}"`; 454 | } 455 | 456 | let message = `${type} "${name}" is not registered`; 457 | return new Error(message + suffix); 458 | } 459 | 460 | /** 461 | * Disable inspect. Returns a function to re-enable inspect. Useful for debugging. 462 | * @name .disableInspect 463 | * @api public 464 | */ 465 | 466 | disableInspect() { 467 | let inspect = this[util.inspect.custom]; 468 | this[util.inspect.custom] = void 0; 469 | 470 | return () => { 471 | define(this, util.inspect.custom, inspect); 472 | }; 473 | } 474 | 475 | /** 476 | * Parse task arguments into an array of task configuration objects. 477 | */ 478 | 479 | parseTasks(...args) { 480 | return parse(this.options.register)(this, ...args); 481 | } 482 | 483 | /** 484 | * Custom inspect function 485 | */ 486 | 487 | [util.inspect.custom]() { 488 | if (typeof this.options.inspectFn === 'function') { 489 | return this.options.inspectFn(this); 490 | } 491 | const names = this.generators ? [...this.generators.keys()].join(', ') : ''; 492 | const tasks = this.tasks ? [...this.tasks.keys()].join(', ') : ''; 493 | return ``; 494 | } 495 | 496 | /** 497 | * Get the first ancestor instance of Composer. Only works if `generator.parent` is 498 | * defined on child instances. 499 | * @name .base 500 | * @getter 501 | * @api public 502 | */ 503 | 504 | get base() { 505 | return this.parent ? this.parent.base : this; 506 | } 507 | 508 | /** 509 | * Get or set the generator name. 510 | * @name .name 511 | * @getter 512 | * @param {String} [name="root"] 513 | * @return {String} 514 | * @api public 515 | */ 516 | 517 | set name(val) { 518 | define(this, '_name', val); 519 | } 520 | get name() { 521 | return this._name || 'generate'; 522 | } 523 | 524 | /** 525 | * Get or set the generator `alias`. By default, the generator alias is created 526 | * by passing the generator name to the [.toAlias](#toAlias) method. 527 | * @name .alias 528 | * @getter 529 | * @param {String} [alias="generate"] 530 | * @return {String} 531 | * @api public 532 | */ 533 | 534 | set alias(val) { 535 | define(this, '_alias', val); 536 | } 537 | get alias() { 538 | return this._alias || this.toAlias(this.name, this.options); 539 | } 540 | 541 | /** 542 | * Get the generator namespace. The namespace is created by joining the generator's `alias` 543 | * to the alias of each ancestor generator. 544 | * @name .namespace 545 | * @getter 546 | * @param {String} [namespace="root"] 547 | * @return {String} 548 | * @api public 549 | */ 550 | 551 | get namespace() { 552 | return this.parent ? this.parent.namespace + '.' + this.alias : this.alias; 553 | } 554 | 555 | /** 556 | * Get the depth of a generator - useful for debugging. The root generator 557 | * has a depth of `0`, sub-generators add `1` for each level of nesting. 558 | * @name .depth 559 | * @getter 560 | * @return {Number} 561 | * @api public 562 | */ 563 | 564 | get depth() { 565 | return this.parent ? this.parent.depth + 1 : 0; 566 | } 567 | 568 | /** 569 | * Static method that returns a function for parsing task arguments. 570 | * @name Composer#parse 571 | * @param {Function} `register` Function that receives a name of a task or generator that cannot be found by the parse function. This allows the `register` function to dynamically register tasks or generators. 572 | * @return {Function} Returns a function for parsing task args. 573 | * @api public 574 | * @static 575 | */ 576 | 577 | static parseTasks(register) { 578 | return parse(register); 579 | } 580 | 581 | /** 582 | * Static method that returns true if the given `val` is an instance of Generate. 583 | * @name Composer#isGenerator 584 | * @param {Object} `val` 585 | * @return {Boolean} 586 | * @api public 587 | * @static 588 | */ 589 | 590 | static isGenerator(val) { 591 | return val instanceof this || (typeof val === 'function' && val.isGenerator === true); 592 | } 593 | 594 | /** 595 | * Static method for creating a custom Composer class with the given `Emitter. 596 | * @name Composer#create 597 | * @param {Function} `Emitter` 598 | * @return {Class} Returns the custom class. 599 | * @static 600 | * @api public 601 | */ 602 | 603 | static create(Emitter) { 604 | return factory(Emitter); 605 | } 606 | 607 | /** 608 | * Static getter for getting the Tasks class with the same `Emitter` class as Composer. 609 | * @name Composer#Tasks 610 | * @param {Function} `Emitter` 611 | * @return {Class} Returns the Tasks class. 612 | * @getter 613 | * @static 614 | * @api public 615 | */ 616 | 617 | static get Tasks() { 618 | return Tasks.create(Emitter); 619 | } 620 | 621 | /** 622 | * Static getter for getting the `Task` class. 623 | * 624 | * ```js 625 | * const { Task } = require('composer'); 626 | * ``` 627 | * @name Composer#Task 628 | * @getter 629 | * @static 630 | * @api public 631 | */ 632 | 633 | static get Task() { 634 | return Task; 635 | } 636 | } 637 | 638 | return Generator; 639 | }; 640 | 641 | /** 642 | * Expose `factory` function 643 | */ 644 | 645 | module.exports = factory(); 646 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { isObject } = require('./utils'); 4 | const noop = () => {}; 5 | 6 | /** 7 | * Parse task expressions from the argv._ (splat) array 8 | */ 9 | 10 | module.exports = (register = noop) => { 11 | return function parse(app, ...rest) { 12 | if (rest.length === 1 && Array.isArray(rest[0])) rest = rest[0]; 13 | let options = rest.find(val => isObject(val) && val.isTask !== true); 14 | rest = rest.filter(val => val !== options); 15 | 16 | let callback = rest.find(val => typeof val === 'function'); 17 | let args = rest.filter(val => val !== options && val !== callback); 18 | let opts = { ...app.options, ...options }; 19 | 20 | if (typeof args[0] === 'string' && Array.isArray(args[1])) { 21 | args = [args[0] + ':' + args[1].join(',')]; 22 | } 23 | 24 | args = args.join(' ').split(' '); 25 | let missing = []; 26 | let result = []; 27 | 28 | for (const arg of args) { 29 | let segs = arg.split(':'); 30 | if (segs.length > 2) { 31 | throw new SyntaxError('spaces must be used to separate multiple generator names'); 32 | } 33 | 34 | let tasks = segs[1] ? segs[1].split(',') : segs[0].split(','); 35 | let name = segs[1] ? segs[0] : null; 36 | let task = { name: null, tasks: [] }; 37 | 38 | tasks.forEach(val => { 39 | if (!app.tasks.has(val) && app.hasGenerator(name + '.' + val)) { 40 | result.push({ name: [name, val].join('.'), tasks: ['default'] }); 41 | tasks = tasks.filter(v => v !== val); 42 | } 43 | }); 44 | 45 | if (segs.length === 2 && tasks.length) { 46 | task.name = name; 47 | task.tasks = tasks; 48 | result.push(task); 49 | continue; 50 | } 51 | 52 | for (let key of tasks) { 53 | if ((!app.tasks.has(key) || app.taskStack.has(key)) && app.hasGenerator(key)) { 54 | if (app.name === 'default' && !key.startsWith('default.')) { 55 | key = `default.${key}`; 56 | } 57 | result.push({ name: key, tasks: ['default' ]}); 58 | } else if (key && app.tasks.has(key)) { 59 | task.name = 'default'; 60 | task.tasks.push(key); 61 | } else if (key) { 62 | missing.push(key); 63 | } 64 | } 65 | 66 | if (task.name) { 67 | result.push(task); 68 | } 69 | } 70 | 71 | if (rest.length && !result.length && app.name !== 'default') { 72 | if (app.hasGenerator('default')) { 73 | return parse(app.getGenerator('default'), ...rest); 74 | } 75 | } 76 | 77 | if (!rest.length && !result.length && !missing.length) { 78 | result = [{ name: 'default', tasks: ['default'] }]; 79 | } 80 | 81 | if (missing.length && register(missing) === true) { 82 | return parse(app, ...rest); 83 | } 84 | 85 | register(result.map(task => task.name)); 86 | 87 | return { options: opts, callback, tasks: result, missing }; 88 | }; 89 | }; 90 | -------------------------------------------------------------------------------- /lib/task.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const Events = require('events'); 5 | const Timer = require('./timer'); 6 | const { define, noop } = require('./utils'); 7 | 8 | class Task extends Events { 9 | constructor(task = {}) { 10 | if (typeof task.name !== 'string') { 11 | throw new TypeError('expected task name to be a string'); 12 | } 13 | super(); 14 | define(this, 'isTask', true); 15 | define(this, 'app', task.app); 16 | this.name = task.name; 17 | this.status = 'pending'; 18 | this.options = Object.assign({ deps: [] }, task.options); 19 | this.callback = task.callback || noop; 20 | this.deps = [...task.deps || [], ...this.options.deps]; 21 | this.time = new Timer(); 22 | if (this.setMaxListeners) { 23 | this.setMaxListeners(0); 24 | } 25 | } 26 | 27 | [util.inspect.custom]() { 28 | return ``; 29 | } 30 | 31 | run(options) { 32 | let finished = false; 33 | let orig = Object.assign({}, this.options); 34 | this.options = Object.assign({}, this.options, options); 35 | this.status = 'preparing'; 36 | this.emit('preparing', this); 37 | 38 | if (this.skip(options)) { 39 | return () => Promise.resolve(null); 40 | } 41 | 42 | this.time = new Timer(); 43 | this.time.start(); 44 | this.status = 'starting'; 45 | this.emit('starting', this); 46 | 47 | return () => new Promise(async(resolve, reject) => { 48 | const finish = (err, value) => { 49 | if (finished) return; 50 | finished = true; 51 | try { 52 | this.options = orig; 53 | this.time.end(); 54 | this.status = 'finished'; 55 | this.emit('finished', this); 56 | if (err) { 57 | define(err, 'task', this); 58 | reject(err); 59 | this.emit('error', err); 60 | } else { 61 | resolve(value); 62 | } 63 | } catch (err) { 64 | reject(err); 65 | } 66 | }; 67 | 68 | try { 69 | if (typeof this.callback !== 'function') { 70 | finish(); 71 | return; 72 | } 73 | 74 | let res = this.callback.call(this, finish); 75 | if (res instanceof Promise) { 76 | let val = await res; 77 | if (val) res = val; 78 | } 79 | 80 | if (isEmitter(res)) { 81 | res.on('error', finish); 82 | res.on('finish', finish); 83 | res.on('end', finish); 84 | return; 85 | } 86 | 87 | if (this.callback.length === 0) { 88 | if (res && res.then) { 89 | res.then(() => finish()); 90 | } else { 91 | finish(null, res); 92 | } 93 | } 94 | 95 | } catch (err) { 96 | finish(err); 97 | } 98 | }); 99 | } 100 | 101 | skip(options) { 102 | let app = this.app || {}; 103 | let opts = Object.assign({}, app.options, this.options, options); 104 | 105 | if (opts.run === false) { 106 | return true; 107 | } 108 | 109 | if (Array.isArray(opts.skip)) { 110 | return opts.skip.includes(this.name); 111 | } 112 | 113 | switch (typeof opts.skip) { 114 | case 'boolean': 115 | return opts.skip === true; 116 | case 'function': 117 | return opts.skip(this) === true; 118 | case 'string': 119 | return opts.skip === this.name; 120 | default: { 121 | return false; 122 | } 123 | } 124 | } 125 | } 126 | 127 | function isEmitter(val) { 128 | return val && (typeof val.on === 'function' || typeof val.pipe === 'function'); 129 | } 130 | 131 | module.exports = Task; 132 | -------------------------------------------------------------------------------- /lib/tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Task = require('./task'); 4 | const Timer = require('./timer'); 5 | const Events = require('events'); 6 | const { createOptions, flatten, noop } = require('./utils'); 7 | 8 | /** 9 | * Factory for creating a custom `Tasks` class that extends the 10 | * given `Emitter`. Or, simply call the factory function to use 11 | * the built-in emitter. 12 | * 13 | * ```js 14 | * // custom emitter 15 | * const Emitter = require('events'); 16 | * const Tasks = require('composer/lib/tasks')(Emitter); 17 | * // built-in emitter 18 | * const Tasks = require('composer/lib/tasks')(); 19 | * const composer = new Tasks(); 20 | * ``` 21 | * @name .factory 22 | * @param {function} `Emitter` Event emitter. 23 | * @return {Class} Returns a custom `Tasks` class. 24 | * @api public 25 | */ 26 | 27 | const factory = (Emitter = Events) => { 28 | 29 | /** 30 | * Create an instance of `Tasks` with the given `options`. 31 | * 32 | * ```js 33 | * const Tasks = require('composer').Tasks; 34 | * const composer = new Tasks(); 35 | * ``` 36 | * @class 37 | * @name Tasks 38 | * @param {object} `options` 39 | * @api public 40 | */ 41 | 42 | class Tasks extends Emitter { 43 | constructor(options = {}) { 44 | super(!Emitter.name.includes('Emitter') ? options : null); 45 | this.options = options; 46 | this.taskStack = new Map(); 47 | this.tasks = new Map(); 48 | this.taskId = 0; 49 | 50 | if (this.off === void 0 && typeof this.removeListener === 'function') { 51 | this.off = this.removeListener.bind(this); 52 | } 53 | } 54 | 55 | /** 56 | * Define a task. Tasks run asynchronously, either in series (by default) or parallel 57 | * (when `options.parallel` is true). In order for the build to determine when a task is 58 | * complete, _one of the following_ things must happen: 1) the callback must be called, 2) a 59 | * promise must be returned, or 3) a stream must be returned. Inside tasks, the "this" 60 | * object is a composer Task instance created for each task with useful properties like 61 | * the task name, options and timing information, which can be useful for logging, etc. 62 | * 63 | * ```js 64 | * // 1. callback 65 | * app.task('default', cb => { 66 | * // do stuff 67 | * cb(); 68 | * }); 69 | * // 2. promise 70 | * app.task('default', () => { 71 | * return Promise.resolve(null); 72 | * }); 73 | * // 3. stream (using vinyl-fs or your stream of choice) 74 | * app.task('default', function() { 75 | * return vfs.src('foo/*.js'); 76 | * }); 77 | * ``` 78 | * @name .task 79 | * @param {String} `name` The task name. 80 | * @param {Object|Array|String|Function} `deps` Any of the following: task dependencies, callback(s), or options object, defined in any order. 81 | * @param {Function} `callback` (optional) If the last argument is a function, it will be called after all of the task's dependencies have been run. 82 | * @return {undefined} 83 | * @api public 84 | */ 85 | 86 | task(name, ...rest) { 87 | if (typeof name !== 'string') { 88 | throw new TypeError('expected task "name" to be a string'); 89 | } 90 | const { options, tasks } = createOptions(this, false, ...rest); 91 | const callback = typeof tasks[tasks.length - 1] === 'function' ? tasks.pop() : noop; 92 | return this.setTask(name, options, tasks, callback); 93 | } 94 | 95 | /** 96 | * Set a task on `app.tasks` 97 | * @name .setTask 98 | * @param {string} name Task name 99 | * @param {object} name Task options 100 | * @param {object|array|string|function} `deps` Task dependencies 101 | * @param {Function} `callback` (optional) Final callback function to call after all task dependencies have been run. 102 | * @return {object} Returns the instance. 103 | */ 104 | 105 | setTask(name, options = {}, deps = [], callback) { 106 | const task = new Task({ name, options, deps, callback, app: this }); 107 | const emit = (key = 'task') => this.emit(key, task); 108 | task.on('error', this.emit.bind(this, 'error')); 109 | task.on('preparing', () => emit('task-preparing')); 110 | task.on('starting', task => { 111 | this.taskStack.set(task.name, task); 112 | emit(); 113 | }); 114 | task.on('finished', task => { 115 | this.taskStack.delete(task.name); 116 | emit(); 117 | }); 118 | this.tasks.set(name, task); 119 | task.status = 'registered'; 120 | emit('task-registered'); 121 | return this; 122 | } 123 | 124 | /** 125 | * Get a task from `app.tasks`. 126 | * @name .getTask 127 | * @param {string} name 128 | * @return {object} Returns the task object. 129 | */ 130 | 131 | getTask(name) { 132 | if (!this.tasks.has(name)) { 133 | throw this.formatError(name, 'task'); 134 | } 135 | return this.tasks.get(name); 136 | } 137 | 138 | /** 139 | * Returns true if all values in the array are registered tasks. 140 | * @name .isTasks 141 | * @param {array} tasks 142 | * @return {boolean} 143 | */ 144 | 145 | isTasks(arr) { 146 | return Array.isArray(arr) && arr.every(name => this.tasks.has(name)); 147 | } 148 | 149 | /** 150 | * Create an array of tasks to run by resolving registered tasks from the values 151 | * in the given array. 152 | * @name .expandTasks 153 | * @param {...[string|function|glob]} tasks 154 | * @return {array} 155 | */ 156 | 157 | expandTasks(...args) { 158 | let vals = flatten(args).filter(Boolean); 159 | let keys = [...this.tasks.keys()]; 160 | let tasks = []; 161 | 162 | for (let task of vals) { 163 | if (typeof task === 'function') { 164 | let name = `task-${this.taskId++}`; 165 | this.task(name, task); 166 | tasks.push(name); 167 | continue; 168 | } 169 | 170 | if (typeof task === 'string') { 171 | if (/\*/.test(task)) { 172 | let matches = match(keys, task); 173 | if (matches.length === 0) { 174 | throw new Error(`glob "${task}" does not match any registered tasks`); 175 | } 176 | tasks.push.apply(tasks, matches); 177 | continue; 178 | } 179 | 180 | tasks.push(task); 181 | continue; 182 | } 183 | 184 | let msg = 'expected task dependency to be a string or function, but got: '; 185 | throw new TypeError(msg + typeof task); 186 | } 187 | return tasks; 188 | } 189 | 190 | /** 191 | * Run one or more tasks. 192 | * 193 | * ```js 194 | * const build = app.series(['foo', 'bar', 'baz']); 195 | * // promise 196 | * build().then(console.log).catch(console.error); 197 | * // or callback 198 | * build(function() { 199 | * if (err) return console.error(err); 200 | * }); 201 | * ``` 202 | * @name .build 203 | * @param {object|array|string|function} `tasks` One or more tasks to run, options, or callback function. If no tasks are defined, the default task is automatically run. 204 | * @param {function} `callback` (optional) 205 | * @return {undefined} 206 | * @api public 207 | */ 208 | 209 | async build(...args) { 210 | let state = { status: 'starting', time: new Timer(), app: this }; 211 | state.time.start(); 212 | this.emit('build', state); 213 | 214 | args = flatten(args); 215 | let cb = typeof args[args.length - 1] === 'function' ? args.pop() : null; 216 | 217 | let { options, tasks } = createOptions(this, true, ...args); 218 | if (!tasks.length) tasks = ['default']; 219 | 220 | let each = options.parallel ? this.parallel : this.series; 221 | let build = each.call(this, options, ...tasks); 222 | let promise = build() 223 | .then(() => { 224 | state.time.end(); 225 | state.status = 'finished'; 226 | this.emit('build', state); 227 | }); 228 | 229 | return resolveBuild(promise, cb); 230 | } 231 | 232 | /** 233 | * Compose a function to run the given tasks in series. 234 | * 235 | * ```js 236 | * const build = app.series(['foo', 'bar', 'baz']); 237 | * // promise 238 | * build().then(console.log).catch(console.error); 239 | * // or callback 240 | * build(function() { 241 | * if (err) return console.error(err); 242 | * }); 243 | * ``` 244 | * @name .series 245 | * @param {object|array|string|function} `tasks` Tasks to run, options, or callback function. If no tasks are defined, the `default` task is automatically run, if one exists. 246 | * @param {function} `callback` (optional) 247 | * @return {promise|undefined} Returns a promise if no callback is passed. 248 | * @api public 249 | */ 250 | 251 | series(...args) { 252 | let stack = new Set(); 253 | let compose = this.iterator('series', async(tasks, options, resolve) => { 254 | for (let ele of tasks) { 255 | let task = this.getTask(ele); 256 | task.series = true; 257 | 258 | if (task.skip(options) || stack.has(task)) { 259 | continue; 260 | } 261 | 262 | task.once('finished', () => stack.delete(task)); 263 | task.once('starting', () => stack.add(task)); 264 | let run = task.run(options); 265 | 266 | if (task.deps.length) { 267 | let opts = Object.assign({}, options, task.options); 268 | let each = opts.parallel ? this.parallel : this.series; 269 | let build = each.call(this, ...task.deps); 270 | await build(); 271 | } 272 | 273 | await run(); 274 | } 275 | 276 | resolve(); 277 | }); 278 | 279 | return compose(...args); 280 | } 281 | 282 | /** 283 | * Compose a function to run the given tasks in parallel. 284 | * 285 | * ```js 286 | * // call the returned function to start the build 287 | * const build = app.parallel(['foo', 'bar', 'baz']); 288 | * // promise 289 | * build().then(console.log).catch(console.error); 290 | * // callback 291 | * build(function() { 292 | * if (err) return console.error(err); 293 | * }); 294 | * // example task usage 295 | * app.task('default', build); 296 | * ``` 297 | * @name .parallel 298 | * @param {object|array|string|function} `tasks` Tasks to run, options, or callback function. If no tasks are defined, the `default` task is automatically run, if one exists. 299 | * @param {function} `callback` (optional) 300 | * @return {promise|undefined} Returns a promise if no callback is passed. 301 | * @api public 302 | */ 303 | 304 | parallel(...args) { 305 | let stack = new Set(); 306 | let compose = this.iterator('parallel', (tasks, options, resolve) => { 307 | let pending = []; 308 | 309 | for (let ele of tasks) { 310 | let task = this.getTask(ele); 311 | task.parallel = true; 312 | 313 | if (task.skip(options) || stack.has(task)) { 314 | continue; 315 | } 316 | 317 | task.once('finished', () => stack.delete(task)); 318 | task.once('starting', () => stack.add(task)); 319 | let run = task.run(options); 320 | 321 | if (task.deps.length) { 322 | let opts = Object.assign({}, options, task.options); 323 | let each = opts.parallel ? this.parallel : this.series; 324 | let build = each.call(this, ...task.deps); 325 | pending.push(build().then(() => run())); 326 | } else { 327 | pending.push(run()); 328 | } 329 | } 330 | 331 | resolve(Promise.all(pending)); 332 | }); 333 | 334 | return compose(...args); 335 | } 336 | 337 | /** 338 | * Create an async iterator function that ensures that either a promise is 339 | * returned or the user-provided callback is called. 340 | * @param {function} `fn` Function to invoke inside the promise. 341 | * @return {function} 342 | */ 343 | 344 | iterator(type, fn) { 345 | return (...args) => { 346 | let { options, tasks } = createOptions(this, true, ...args); 347 | 348 | return cb => { 349 | let promise = new Promise(async(resolve, reject) => { 350 | if (tasks.length === 0) { 351 | resolve(); 352 | return; 353 | } 354 | 355 | try { 356 | let p = fn(tasks, options, resolve); 357 | if (type === 'series') await p; 358 | } catch (err) { 359 | reject(err); 360 | } 361 | }); 362 | 363 | return resolveBuild(promise, cb); 364 | }; 365 | }; 366 | } 367 | 368 | /** 369 | * Format task and generator errors. 370 | * @name .formatError 371 | * @param {String} `name` 372 | * @return {Error} 373 | */ 374 | 375 | formatError(name) { 376 | return new Error(`task "${name}" is not registered`); 377 | } 378 | 379 | /** 380 | * Static method for creating a custom Tasks class with the given `Emitter. 381 | * @name .create 382 | * @param {Function} `Emitter` 383 | * @return {Class} Returns the custom class. 384 | * @api public 385 | * @static 386 | */ 387 | 388 | static create(Emitter) { 389 | return factory(Emitter); 390 | } 391 | } 392 | return Tasks; 393 | }; 394 | 395 | function resolveBuild(promise, cb) { 396 | if (typeof cb === 'function') { 397 | promise.then(val => cb(null, val)).catch(cb); 398 | } else { 399 | return promise; 400 | } 401 | } 402 | 403 | function match(keys, pattern) { 404 | let chars = [...pattern].map(ch => ({ '*': '.*?', '.': '\\.' }[ch] || ch)); 405 | let regex = new RegExp(chars.join('')); 406 | return keys.filter(key => regex.test(key)); 407 | } 408 | 409 | module.exports = factory(); 410 | -------------------------------------------------------------------------------- /lib/timer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { nano } = require('./utils'); 4 | 5 | class Timer { 6 | constructor() { 7 | this.date = {}; 8 | this.hr = {}; 9 | } 10 | 11 | start() { 12 | this.date.start = new Date(); 13 | this.hr.start = process.hrtime(); 14 | return this; 15 | } 16 | 17 | end() { 18 | this.date.end = new Date(); 19 | this.hr.end = process.hrtime(); 20 | this.hr.duration = process.hrtime(this.hr.start); 21 | return this; 22 | } 23 | 24 | get diff() { 25 | return nano(this.hr.end) - nano(this.hr.start); 26 | } 27 | 28 | get duration() { 29 | return this.hr.duration ? require('pretty-time')(this.hr.duration) : ''; 30 | } 31 | } 32 | 33 | /** 34 | * Expose `Timer` 35 | */ 36 | 37 | module.exports = Timer; 38 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Get hr time in nanoseconds 5 | */ 6 | 7 | const nano = time => +time[0] * 1e9 + +time[1]; 8 | 9 | /** 10 | * Flatten an array 11 | */ 12 | 13 | const flatten = arr => [].concat.apply([], arr); 14 | 15 | /** 16 | * Return true if `val` is an object 17 | */ 18 | 19 | const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); 20 | 21 | /** 22 | * Create an options object from the given arguments. 23 | * @param {object} `app` 24 | * @param {...[function|string|object]} `rest` 25 | * @return {object} 26 | */ 27 | 28 | const createOptions = (app, expand, ...rest) => { 29 | const args = flatten(rest); 30 | const config = args.find(val => isObject(val) && !val.isTask) || {}; 31 | const options = Object.assign({}, app.options, config); 32 | const tasks = expand === true 33 | ? app.expandTasks(args.filter(val => val && val !== config)) 34 | : args.filter(val => val && val !== config); 35 | return { tasks, options }; 36 | }; 37 | 38 | /** 39 | * Noop for tasks 40 | */ 41 | 42 | const noop = cb => cb(); 43 | 44 | /** 45 | * Create a non-enumerable property on `obj` 46 | */ 47 | 48 | const define = (obj, key, value) => { 49 | Reflect.defineProperty(obj, key, { 50 | configurable: true, 51 | enumerable: false, 52 | writable: true, 53 | value 54 | }); 55 | }; 56 | 57 | /** 58 | * Expose "utils" 59 | */ 60 | 61 | module.exports = { 62 | createOptions, 63 | define, 64 | flatten, 65 | isObject, 66 | nano, 67 | noop 68 | }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "composer", 3 | "description": "Run and compose async tasks. Easily define groups of tasks to run in series or parallel.", 4 | "version": "4.1.0", 5 | "homepage": "https://github.com/doowb/composer", 6 | "author": "Brian Woodward (https://github.com/doowb)", 7 | "contributors": [ 8 | "Brian Woodward (https://twitter.com/doowb)", 9 | "Jon Schlinkert (http://twitter.com/jonschlinkert)" 10 | ], 11 | "repository": "doowb/composer", 12 | "bugs": { 13 | "url": "https://github.com/doowb/composer/issues" 14 | }, 15 | "license": "MIT", 16 | "files": [ 17 | "index.js", 18 | "lib" 19 | ], 20 | "main": "index.js", 21 | "engines": { 22 | "node": ">=8" 23 | }, 24 | "scripts": { 25 | "test": "mocha" 26 | }, 27 | "dependencies": { 28 | "pretty-time": "^1.1.0", 29 | "use": "^3.1.1" 30 | }, 31 | "devDependencies": { 32 | "gulp-format-md": "^1.0.0", 33 | "minimist": "^1.2.0", 34 | "mocha": "^5.2.0", 35 | "through2": "^2.0.3" 36 | }, 37 | "keywords": [ 38 | "async", 39 | "await", 40 | "build", 41 | "build-system", 42 | "compose", 43 | "composer", 44 | "composition", 45 | "control", 46 | "flow", 47 | "run", 48 | "system", 49 | "task", 50 | "workflow" 51 | ], 52 | "verb": { 53 | "toc": true, 54 | "layout": "default", 55 | "data": { 56 | "author": { 57 | "linkedin": "woodwardbrian", 58 | "twitter": "doowb" 59 | } 60 | }, 61 | "tasks": [ 62 | "readme" 63 | ], 64 | "plugins": [ 65 | "gulp-format-md" 66 | ], 67 | "related": { 68 | "list": [ 69 | "enquirer", 70 | "assemble", 71 | "generate", 72 | "update", 73 | "verb" 74 | ] 75 | }, 76 | "lint": { 77 | "reflinks": true 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/app.default-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Generator = require('..'); 6 | let app; 7 | 8 | describe('default generator', () => { 9 | beforeEach(() => { 10 | app = new Generator(); 11 | }); 12 | 13 | it('should throw an error when a default generator is not found', () => { 14 | return app.generate('default') 15 | .catch(err => { 16 | assert(err); 17 | assert(/invalid/i.test(err.message)); 18 | }); 19 | }); 20 | 21 | it('should throw an error when a default task on default generator is not found', cb => { 22 | app.register('default', () => {}); 23 | app.generate('default', 'default') 24 | .catch(err => { 25 | assert(err); 26 | assert(/task/i.test(err.message)); 27 | assert(/is not registered/i.test(err.message)); 28 | cb(); 29 | }); 30 | }); 31 | 32 | it('should throw an error when a default task on default generator is not found', cb => { 33 | app.register('default', () => {}); 34 | app.generate('default', 'default') 35 | .catch(err => { 36 | assert(err); 37 | assert(/task/i.test(err.message)); 38 | assert(/is not registered/i.test(err.message)); 39 | cb(); 40 | }); 41 | }); 42 | 43 | it('should prefer tasks on the default generator over anything on the app instance', () => { 44 | let count = { app: 0, default: 0 }; 45 | 46 | app.task('foo', next => { 47 | count.app++; 48 | next(); 49 | }); 50 | 51 | app.register('foo', foo => { 52 | foo.task('default', next => { 53 | count.app++; 54 | next(); 55 | }); 56 | }); 57 | 58 | app.register('default', gen => { 59 | // I should win 60 | gen.task('foo', next => { 61 | count.default++; 62 | next(); 63 | }); 64 | }); 65 | 66 | return app.generate('foo') 67 | .then(() => { 68 | assert.equal(count.app, 0); 69 | assert.equal(count.default, 1); 70 | }); 71 | }); 72 | 73 | it('should prefer generators on the default generator over anything on the app instance', () => { 74 | let count = { app: 0, default: 0 }; 75 | 76 | app.task('foo', next => { 77 | count.app++; 78 | next(); 79 | }); 80 | 81 | app.register('foo', foo => { 82 | foo.task('default', next => { 83 | count.app++; 84 | next(); 85 | }); 86 | }); 87 | 88 | app.register('default', gen => { 89 | gen.task('foo', next => { 90 | // I should win 91 | count.default++; 92 | next(); 93 | }); 94 | 95 | gen.register('foo', foo => { 96 | foo.task('default', next => { 97 | next(); 98 | }); 99 | }); 100 | }); 101 | 102 | return app.generate('foo') 103 | .then(() => { 104 | assert.equal(count.app, 0); 105 | assert.equal(count.default, 1); 106 | }); 107 | }); 108 | 109 | it('should run default tasks on an array of nested sub-generators', () => { 110 | let count = 0; 111 | const task = async() => count++; 112 | const def = gen => gen.task('default', task); 113 | 114 | app.register('default', function() { 115 | this.register('foo', app => { 116 | app.register('one', def); 117 | app.register('two', def); 118 | }); 119 | 120 | this.register('bar', app => { 121 | app.register('one', def); 122 | app.register('two', def); 123 | app.register('three', function() { 124 | this.task('zzz', task); 125 | this.task('default', ['zzz']); 126 | }); 127 | }); 128 | }); 129 | 130 | return app.generate(['foo.one', 'foo.two', 'bar.one', 'bar.two', 'bar.three']) 131 | .then(() => { 132 | assert.equal(count, 5); 133 | }); 134 | }); 135 | 136 | it('should throw an error when default task is called and missing on default generator', () => { 137 | let count = { app: 0, default: 0 }; 138 | 139 | app.register('default', gen => { 140 | gen.task('default', next => { 141 | count.default++; 142 | next(); 143 | }); 144 | }); 145 | 146 | return app.generate('default') 147 | .then(() => { 148 | assert.equal(count.default, 1); 149 | }); 150 | }); 151 | 152 | it('should throw an error when default task is called and missing on default generator', () => { 153 | app.register('default', gen => {}); 154 | 155 | return app.generate('default') 156 | .catch(err => { 157 | assert(err); 158 | assert(/default/.test(err.message)); 159 | assert(/not registered/.test(err.message)); 160 | }); 161 | }); 162 | 163 | it('should handle errors from default generator tasks', () => { 164 | let errors = []; 165 | 166 | app.register('default', gen => { 167 | gen.task('foo', next => { 168 | next(new Error('huge error!')); 169 | }); 170 | }); 171 | 172 | app.on('error', err => { 173 | errors.push(err); 174 | }); 175 | 176 | return app.generate('foo') 177 | .catch(err => { 178 | assert(err); 179 | assert(err === errors[0]); 180 | assert(/huge error/.test(err.message)); 181 | }); 182 | }); 183 | 184 | it('should handle error events from default generator tasks', () => { 185 | app.register('default', gen => { 186 | gen.task('foo', next => { 187 | next(new Error('huge error!')); 188 | }); 189 | }); 190 | 191 | return app.generate('foo') 192 | .then(() => { 193 | throw new Error('expected an error'); 194 | }) 195 | .catch(err => { 196 | assert(err); 197 | assert(/huge error/.test(err.message)); 198 | }); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /test/app.findGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Generator = require('..'); 6 | let base; 7 | 8 | describe('.findGenerator', () => { 9 | beforeEach(() => { 10 | base = new Generator(); 11 | }); 12 | 13 | it('should not throw an error when a generator is not found', () => { 14 | assert.doesNotThrow(() => base.findGenerator('whatever'), /cannot find/); 15 | }); 16 | 17 | it('should not throw an error when a generator is not found on a nested instance', () => { 18 | base.register('foo', () => {}); 19 | assert.doesNotThrow(() => base.findGenerator('foo.whatever'), /cannot find/); 20 | }); 21 | 22 | it('should get a generator function from the base instance', () => { 23 | base.register('abc', () => {}); 24 | const generator = base.findGenerator('abc'); 25 | assert.equal(typeof generator, 'function'); 26 | }); 27 | 28 | it('should not invoke nested generators', () => { 29 | let count = 0; 30 | 31 | base.register('abc', () => {}); 32 | base.register('xyz', app => { 33 | app.register('one', () => { 34 | const generator = this.base.findGenerator('abc'); 35 | assert(generator); 36 | assert.equal(typeof generator, 'function'); 37 | assert.equal(generator.name, 'abc'); 38 | count++; 39 | }); 40 | 41 | app.register('two', () => { 42 | const generator = base.findGenerator('abc'); 43 | assert(generator); 44 | assert.equal(typeof generator, 'object'); 45 | assert.equal(generator.name, 'abc'); 46 | count++; 47 | }); 48 | 49 | app.register('three', function(three) { 50 | const generator = three.base.findGenerator('abc'); 51 | assert(generator); 52 | assert.equal(typeof generator, 'object'); 53 | assert.equal(generator.name, 'abc'); 54 | count++; 55 | }); 56 | }); 57 | 58 | assert.equal(typeof base.findGenerator('xyz.one'), 'function'); 59 | assert.equal(typeof base.findGenerator('xyz.two'), 'function'); 60 | assert.equal(typeof base.findGenerator('xyz.three'), 'function'); 61 | assert.equal(count, 0); 62 | }); 63 | 64 | it('should get a generator from the base instance using `this`', () => { 65 | base.register('abc', () => {}); 66 | base.register('xyz', app => { 67 | app.register('sub', function(sub) { 68 | const generator = this.findGenerator('abc'); 69 | assert(generator); 70 | assert.equal(typeof generator, 'object'); 71 | assert.equal(generator.name, 'abc'); 72 | }); 73 | }); 74 | base.findGenerator('xyz'); 75 | }); 76 | 77 | it('should get a base generator from "app" from a nested generator', () => { 78 | base.register('abc', () => {}); 79 | base.register('xyz', app => { 80 | app.register('sub', function(sub) { 81 | const generator = app.findGenerator('abc'); 82 | assert(generator); 83 | assert.equal(typeof generator, 'object'); 84 | assert.equal(generator.name, 'abc'); 85 | }); 86 | }); 87 | base.findGenerator('xyz'); 88 | }); 89 | 90 | it('should get a nested generator', () => { 91 | base.register('abc', function(abc) { 92 | abc.register('def', function(def) {}); 93 | }); 94 | 95 | const generator = base.findGenerator('abc.def'); 96 | assert(generator); 97 | assert.equal(typeof generator, 'function'); 98 | assert.equal(generator.name, 'def'); 99 | }); 100 | 101 | it('should get a deeply nested generator', () => { 102 | base.register('abc', function(abc) { 103 | abc.register('def', function(def) { 104 | def.register('ghi', function(ghi) { 105 | ghi.register('jkl', function(jkl) { 106 | jkl.register('mno', () => {}); 107 | }); 108 | }); 109 | }); 110 | }); 111 | 112 | const generator = base.findGenerator('abc.def.ghi.jkl.mno'); 113 | assert(generator); 114 | assert.equal(typeof generator, 'function'); 115 | assert.equal(generator.name, 'mno'); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/app.generate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Generator = require('..'); 6 | let base; 7 | 8 | const ensureError = () => { 9 | throw new Error('expected an error'); 10 | }; 11 | 12 | describe('.generate', () => { 13 | beforeEach(() => { 14 | base = new Generator(); 15 | }); 16 | 17 | describe('generators', () => { 18 | it('should throw an error when a generator is not found', () => { 19 | return base.generate('fdsslsllsfjssl') 20 | .then(ensureError) 21 | .catch(err => { 22 | assert(err); 23 | assert(/invalid/i.test(err.message)); 24 | }); 25 | }); 26 | 27 | it('should throw an error when the default task is not found', () => { 28 | base.register('foo', () => {}); 29 | return base.generate('foo', ['default']) 30 | .then(ensureError) 31 | .catch(err => { 32 | assert(err); 33 | assert(/is not registered/i.test(err.message)); 34 | }); 35 | }); 36 | 37 | it('should throw an error when a task is not found (task array)', () => { 38 | base.register('fdsslsllsfjssl', () => {}); 39 | return base.generate('fdsslsllsfjssl', ['foo']) 40 | .then(ensureError) 41 | .catch(err => { 42 | assert(err); 43 | assert(/task/i.test(err.message)); 44 | assert(/is not registered/i.test(err.message)); 45 | }); 46 | }); 47 | 48 | it('should throw an error when a task is not found (task string)', () => { 49 | base.register('fdsslsllsfjssl', () => {}); 50 | return base.generate('fdsslsllsfjssl', 'foo') 51 | .then(ensureError) 52 | .catch(err => { 53 | assert(err); 54 | assert(/task/i.test(err.message)); 55 | assert(/invalid/i.test(err.message)); 56 | }); 57 | }); 58 | 59 | it('should handle task errors', () => { 60 | base.task('default', next => { 61 | next(new Error('whatever')); 62 | }); 63 | 64 | return base.generate('default') 65 | .then(ensureError) 66 | .catch(err => { 67 | assert(err); 68 | assert.equal(err.message, 'whatever'); 69 | }); 70 | }); 71 | 72 | it('should handle task error event', () => { 73 | let errors = []; 74 | base.task('default', next => { 75 | next(new Error('whatever')); 76 | }); 77 | 78 | base.on('error', err => { 79 | errors.push(err); 80 | }); 81 | 82 | return base.generate('default') 83 | .then(ensureError) 84 | .catch(err => { 85 | assert(err); 86 | assert(err === errors[0]); 87 | assert.equal(err.message, 'whatever'); 88 | }); 89 | }); 90 | 91 | it('should run a task on the instance', () => { 92 | base.task('abc123', next => next()); 93 | return base.generate('abc123'); 94 | }); 95 | 96 | it('should run a task on the instance', () => { 97 | base.task('abc123', next => next()); 98 | return base.generate('abc123'); 99 | }); 100 | 101 | it('should run same-named task instead of a generator', () => { 102 | base.register('123xyz', app => { 103 | throw new Error('expected the task to run first'); 104 | }); 105 | 106 | base.task('123xyz', cb => cb()); 107 | return base.generate('123xyz'); 108 | }); 109 | 110 | it('should run a task instead of a generator with a default task', () => { 111 | base.register('123xyz', app => { 112 | app.task('default', () => { 113 | return Promise.resolve(new Error('expected the task to run first')); 114 | }); 115 | }); 116 | base.task('123xyz', cb => cb()); 117 | return base.generate('123xyz'); 118 | }); 119 | 120 | it('should run a task on a same-named generator when the task is specified', () => { 121 | let count = 0; 122 | base.register('foo', app => { 123 | app.task('default', next => { 124 | count++; 125 | next(); 126 | }); 127 | }); 128 | 129 | base.task('foo', () => { 130 | throw new Error('expected the generator to run'); 131 | }); 132 | 133 | return base.generate('foo', ['default']) 134 | .then(() => { 135 | assert.equal(count, 1); 136 | }); 137 | }); 138 | 139 | it('should run a generator from inside a task with the same name', () => { 140 | base.register('foo', app => { 141 | app.task('default', next => { 142 | next(); 143 | }); 144 | }); 145 | 146 | base.task('foo', () => base.generate('foo')); 147 | return base.build('foo'); 148 | }); 149 | 150 | it('should run the default task on a generator as a promise', () => { 151 | base.register('foo', app => app.task('default', next => next())); 152 | 153 | return base.generate('foo'); 154 | }); 155 | 156 | it('should run the default task on a generator with a callback', cb => { 157 | base.register('foo', function(app) { 158 | app.task('default', function(next) { 159 | next(); 160 | }); 161 | }); 162 | 163 | base.generate('foo', cb); 164 | }); 165 | 166 | it('should run a list of tasks on the instance', () => { 167 | let count = 0; 168 | base.task('a', next => { 169 | count++; 170 | next(); 171 | }); 172 | base.task('b', next => { 173 | count++; 174 | next(); 175 | }); 176 | base.task('c', next => { 177 | count++; 178 | next(); 179 | }); 180 | 181 | return base.generate('a', 'b', 'c') 182 | .then(() => { 183 | assert.equal(count, 3); 184 | }); 185 | }); 186 | 187 | it('should run tasks in parallel', cb => { 188 | const app = new Generator({ parallel: true }); 189 | const actual = []; 190 | 191 | app.task('foo', function(next) { 192 | setTimeout(() => { 193 | actual.push('foo'); 194 | next(); 195 | }, 5); 196 | }); 197 | 198 | app.task('bar', function(next) { 199 | setTimeout(() => { 200 | actual.push('bar'); 201 | next(); 202 | }, 1); 203 | }); 204 | 205 | app.task('baz', function(next) { 206 | actual.push('baz'); 207 | next(); 208 | }); 209 | 210 | app.generate(['foo', 'bar', 'baz'], err => { 211 | if (err) return cb(err); 212 | assert.deepEqual(actual, ['baz', 'bar', 'foo']); 213 | cb(); 214 | }); 215 | }); 216 | 217 | it('should run an array of tasks on the instance', () => { 218 | let count = 0; 219 | base.task('a', next => { 220 | count++; 221 | next(); 222 | }); 223 | base.task('b', next => { 224 | count++; 225 | next(); 226 | }); 227 | base.task('c', next => { 228 | count++; 229 | next(); 230 | }); 231 | 232 | return base.generate(['a', 'b', 'c']) 233 | .then(() => { 234 | assert.equal(count, 3); 235 | }); 236 | }); 237 | 238 | it('should run the default task on a registered generator', () => { 239 | let count = 0; 240 | 241 | base.register('foo', app => { 242 | app.task('default', next => { 243 | count++; 244 | next(); 245 | }); 246 | }); 247 | 248 | return base.generate('foo') 249 | .then(() => { 250 | assert.equal(count, 1); 251 | }); 252 | }); 253 | 254 | it('should run an array of generators', () => { 255 | let count = 0; 256 | base.register('foo', app => { 257 | app.task('default', next => { 258 | count++; 259 | next(); 260 | }); 261 | }); 262 | 263 | base.register('bar', app => { 264 | app.task('default', next => { 265 | count++; 266 | next(); 267 | }); 268 | }); 269 | 270 | return base.generate(['foo', 'bar']) 271 | .then(() => { 272 | assert.equal(count, 2); 273 | }); 274 | }); 275 | 276 | it('should throw an error when an invalid name is passed on an array', () => { 277 | let count = 0; 278 | base.register('foo', app => { 279 | app.task('default', next => { 280 | count++; 281 | next(); 282 | }); 283 | }); 284 | 285 | base.register('bar', app => { 286 | app.task('default', next => { 287 | count++; 288 | next(); 289 | }); 290 | }); 291 | 292 | return base.generate(['foo', 'bar', 'lfdsflsjfsjflksjfsfjs']) 293 | .then(() => { 294 | throw new Error('expected an error'); 295 | }) 296 | .catch(err => { 297 | assert(/invalid/i.test(err)); 298 | }) 299 | }); 300 | 301 | it('should run the default tasks on an array of generators', () => { 302 | let count = 0; 303 | base.register('a', function(app) { 304 | this.task('default', cb => { 305 | count++; 306 | cb(); 307 | }); 308 | }); 309 | 310 | base.register('b', function(app) { 311 | this.task('default', cb => { 312 | count++; 313 | cb(); 314 | }); 315 | }); 316 | 317 | base.register('c', function(app) { 318 | this.task('default', cb => { 319 | count++; 320 | cb(); 321 | }); 322 | }); 323 | 324 | return base.generate(['a', 'b', 'c']) 325 | .then(() => { 326 | assert.equal(count, 3); 327 | }); 328 | }); 329 | }); 330 | 331 | describe('options', cb => { 332 | it('should pass options to generator.options', () => { 333 | let count = 0; 334 | base.register('default', (app, options) => { 335 | app.task('default', next => { 336 | count++; 337 | assert.equal(options.foo, 'bar'); 338 | next(); 339 | }); 340 | }); 341 | 342 | return base.generate({ foo: 'bar' }) 343 | .then(() => { 344 | assert.equal(count, 1); 345 | }); 346 | }); 347 | 348 | it('should expose options on generator options', () => { 349 | let count = 0; 350 | base.register('default', (app, options) => { 351 | app.task('default', next => { 352 | count++; 353 | assert.equal(options.foo, 'bar'); 354 | next(); 355 | }); 356 | }); 357 | 358 | return base.generate({foo: 'bar'}) 359 | .then(() => { 360 | assert.equal(count, 1); 361 | }); 362 | }); 363 | 364 | it('should not mutate options on parent instance', () => { 365 | let count = 0; 366 | base.register('default', function(app, options) { 367 | app.task('default', next => { 368 | count++; 369 | assert.equal(options.foo, 'bar'); 370 | assert(!base.options.foo); 371 | next(); 372 | }); 373 | }); 374 | 375 | return base.generate({foo: 'bar'}) 376 | .then(() => { 377 | assert.equal(count, 1); 378 | }); 379 | }); 380 | }); 381 | 382 | describe('default tasks', cb => { 383 | it('should run the default task on the base instance', () => { 384 | let count = 0; 385 | base.task('default', next => { 386 | count++; 387 | next(); 388 | }); 389 | 390 | return base.generate() 391 | .then(() => { 392 | assert.equal(count, 1); 393 | }); 394 | }); 395 | 396 | it('should run the default task on the _default_ generator', () => { 397 | let count = 0; 398 | base.register('default', function(app) { 399 | app.task('default', next => { 400 | count++; 401 | next(); 402 | }); 403 | }); 404 | 405 | return base.generate() 406 | .then(() => { 407 | assert.equal(count, 1); 408 | }); 409 | }); 410 | 411 | it('should run the default task on the _specified_ generator', () => { 412 | let count = 0; 413 | 414 | base.register('foo', app => { 415 | app.task('default', next => { 416 | count++; 417 | next(); 418 | }); 419 | }); 420 | 421 | return base.generate('foo') 422 | .then(() => { 423 | assert.equal(count, 1); 424 | }); 425 | }); 426 | }); 427 | 428 | describe('specified tasks', cb => { 429 | it('should run the specified task on a registered generator', () => { 430 | let count = 0; 431 | base.register('foo', app => { 432 | app.task('default', next => { 433 | count++; 434 | next(); 435 | }); 436 | 437 | app.task('abc', next => { 438 | count++; 439 | next(); 440 | }); 441 | }); 442 | 443 | return base.generate('foo', ['abc']) 444 | .then(() => { 445 | assert.equal(count, 1); 446 | }); 447 | }); 448 | }); 449 | 450 | describe('sub-generators', cb => { 451 | it('should run the default task on a registered sub-generator', () => { 452 | let count = 0; 453 | base.register('foo', app => { 454 | app.register('sub', function(sub) { 455 | sub.task('default', next => { 456 | count++; 457 | next(); 458 | }); 459 | 460 | sub.task('abc', next => { 461 | count++; 462 | next(); 463 | }); 464 | }); 465 | }); 466 | 467 | return base.generate('foo.sub') 468 | .then(() => { 469 | assert.equal(count, 1); 470 | }); 471 | }); 472 | 473 | it('should run the specified task array on a registered sub-generator', () => { 474 | let count = 0; 475 | base.register('foo', app => { 476 | app.register('sub', function(sub) { 477 | sub.task('default', next => { 478 | count++; 479 | next(); 480 | }); 481 | 482 | sub.task('abc', next => { 483 | count++; 484 | next(); 485 | }); 486 | }); 487 | }); 488 | 489 | return base.generate('foo.sub', ['abc']) 490 | .then(() => { 491 | assert.equal(count, 1); 492 | }); 493 | }); 494 | 495 | it('should run default tasks on an array of nested sub-generators', () => { 496 | let count = 0; 497 | base.register('foo', app => { 498 | app.register('one', function(one) { 499 | one.task('default', next => { 500 | count++; 501 | next(); 502 | }); 503 | }); 504 | 505 | app.register('two', function(two) { 506 | two.task('default', next => { 507 | count++; 508 | next(); 509 | }); 510 | }); 511 | }); 512 | 513 | return base.generate(['foo.one', 'foo.two']) 514 | .then(() => { 515 | assert.equal(count, 2); 516 | }); 517 | }); 518 | 519 | it('should run an array of nested sub-generators', () => { 520 | let count = 0; 521 | base.register('foo', app => { 522 | app.register('one', function(one) { 523 | one.task('default', next => { 524 | count++; 525 | next(); 526 | }); 527 | }); 528 | 529 | app.register('two', function(two) { 530 | two.task('default', next => { 531 | count++; 532 | next(); 533 | }); 534 | }); 535 | }); 536 | 537 | return base.generate('foo', ['one', 'two']) 538 | .then(() => { 539 | assert.equal(count, 2); 540 | }); 541 | }); 542 | 543 | it('should run an array of tasks on a registered sub-generator', () => { 544 | let count = 0; 545 | base.register('foo', app => { 546 | app.register('bar', function(bar) { 547 | bar.task('default', next => { 548 | count++; 549 | next(); 550 | }); 551 | 552 | bar.task('a', next => { 553 | count++; 554 | next(); 555 | }); 556 | 557 | bar.task('b', next => { 558 | count++; 559 | next(); 560 | }); 561 | 562 | bar.task('c', next => { 563 | count++; 564 | next(); 565 | }); 566 | }); 567 | }); 568 | 569 | return base.generate('foo.bar', ['a', 'b', 'c']) 570 | .then(() => { 571 | assert.equal(count, 3); 572 | }); 573 | }); 574 | }); 575 | 576 | describe('cross-generator', cb => { 577 | it('should run a generator from another generator', () => { 578 | var res = ''; 579 | 580 | base.register('foo', function(app, two) { 581 | app.register('sub', function(sub) { 582 | sub.task('default', next => { 583 | res += 'foo > sub > default '; 584 | base.generate('bar.sub') 585 | .then(() => next()) 586 | .catch(next); 587 | }); 588 | }); 589 | }); 590 | 591 | base.register('bar', app => { 592 | app.register('sub', function(sub) { 593 | sub.task('default', next => { 594 | res += 'bar > sub > default '; 595 | next(); 596 | }); 597 | }); 598 | }); 599 | 600 | return base.generate('foo.sub') 601 | .then(() => { 602 | assert.equal(res, 'foo > sub > default bar > sub > default '); 603 | }); 604 | }); 605 | 606 | it('should run the specified task on a registered sub-generator', () => { 607 | let count = 0; 608 | base.register('foo', app => { 609 | app.register('sub', function(sub) { 610 | sub.task('default', next => { 611 | count++; 612 | next(); 613 | }); 614 | 615 | sub.task('abc', next => { 616 | count++; 617 | next(); 618 | }); 619 | }); 620 | }); 621 | 622 | return base.generate('foo.sub', ['abc']) 623 | .then(() => { 624 | assert.equal(count, 1); 625 | }); 626 | }); 627 | }); 628 | }); 629 | -------------------------------------------------------------------------------- /test/app.generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Generator = require('..'); 6 | let base; 7 | 8 | describe('.generator', () => { 9 | beforeEach(() => { 10 | base = new Generator(); 11 | }); 12 | 13 | describe('generator naming', () => { 14 | it('should cache a generator by alias when alias is given', () => { 15 | base.generator('foo', () => {}); 16 | assert(base.generators.has('foo')); 17 | }); 18 | 19 | it('should cache a generator by alias when by full name is given', () => { 20 | base.generator('generate-abc', () => {}); 21 | assert(base.generators.has('abc')); 22 | }); 23 | 24 | it('should get a generator by alias, when registered with full name', () => { 25 | base.register('generate-foo', () => {}); 26 | const app = base.getGenerator('foo'); 27 | assert.equal(app.name, 'generate-foo'); 28 | assert.equal(app.alias, 'foo'); 29 | }); 30 | 31 | it('should get a generator by alias, when registered with alias', () => { 32 | base.register('foo', () => {}); 33 | const app = base.getGenerator('foo'); 34 | assert.equal(app.name, 'foo'); 35 | }); 36 | 37 | it('should get a generator by full name, when registered with full name', () => { 38 | base.register('generate-foo', () => {}); 39 | const app = base.getGenerator('generate-foo'); 40 | assert.equal(app.name, 'generate-foo'); 41 | assert.equal(app.alias, 'foo'); 42 | }); 43 | 44 | it('should get a generator by full name, when registered with alias', () => { 45 | base.register('foo', () => {}); 46 | const app = base.getGenerator('generate-foo'); 47 | assert.equal(app.name, 'foo'); 48 | }); 49 | }); 50 | 51 | describe('generators', () => { 52 | it('should invoke a registered generator when `getGenerator` is called', cb => { 53 | base.register('foo', app => { 54 | app.task('default', () => {}); 55 | cb(); 56 | }); 57 | 58 | base.getGenerator('foo'); 59 | }); 60 | 61 | it('should expose the generator instance on `app`', cb => { 62 | base.register('foo', app => { 63 | app.task('default', next => { 64 | assert.equal(app.a, 'b'); 65 | next(); 66 | }); 67 | }); 68 | 69 | const foo = base.getGenerator('foo'); 70 | foo.a = 'b'; 71 | foo.build('default', err => { 72 | if (err) return cb(err); 73 | cb(); 74 | }); 75 | }); 76 | 77 | it('should expose the "base" instance on `app.base`', cb => { 78 | base.x = 'z'; 79 | base.register('foo', app => { 80 | app.task('default', next => { 81 | assert.equal(app.base.x, 'z'); 82 | next(); 83 | }); 84 | }); 85 | 86 | const foo = base.getGenerator('foo'); 87 | foo.build('default', err => { 88 | if (err) return cb(err); 89 | cb(); 90 | }); 91 | }); 92 | 93 | it('should expose an app\'s generators on app.generators', cb => { 94 | base.register('foo', app => { 95 | app.register('a', () => {}); 96 | app.register('b', () => {}); 97 | 98 | app.generators.has('a'); 99 | app.generators.has('b'); 100 | cb(); 101 | }); 102 | 103 | base.getGenerator('foo'); 104 | }); 105 | 106 | it('should expose all root generators on base.generators', cb => { 107 | base.register('foo', app => { 108 | app.base.generators.has('foo'); 109 | app.base.generators.has('bar'); 110 | app.base.generators.has('baz'); 111 | cb(); 112 | }); 113 | 114 | base.register('bar', function(app, base) {}); 115 | base.register('baz', function(app, base) {}); 116 | base.getGenerator('foo'); 117 | }); 118 | }); 119 | 120 | describe('cross-generators', () => { 121 | it('should get a generator from another generator', cb => { 122 | base.register('foo', app => { 123 | const bar = app.base.getGenerator('bar'); 124 | assert(bar); 125 | cb(); 126 | }); 127 | 128 | base.register('bar', function(app, base) {}); 129 | base.register('baz', function(app, base) {}); 130 | base.getGenerator('foo'); 131 | }); 132 | 133 | it('should set options on another generator instance', cb => { 134 | base.generator('foo', app => { 135 | app.task('default', next => { 136 | assert.equal(app.options.abc, 'xyz'); 137 | next(); 138 | }); 139 | }); 140 | 141 | base.generator('bar', app => { 142 | const foo = app.base.getGenerator('foo'); 143 | foo.options.abc = 'xyz'; 144 | foo.build(err => { 145 | if (err) return cb(err); 146 | cb(); 147 | }); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('tasks', () => { 153 | it('should expose a generator\'s tasks on app.tasks', cb => { 154 | base.register('foo', app => { 155 | app.task('a', () => {}); 156 | app.task('b', () => {}); 157 | assert(app.tasks.has('a')); 158 | assert(app.tasks.has('b')); 159 | cb(); 160 | }); 161 | 162 | base.getGenerator('foo'); 163 | }); 164 | 165 | it('should get tasks from another generator', cb => { 166 | base.register('foo', app => { 167 | const baz = app.base.getGenerator('baz'); 168 | const task = baz.tasks.get('aaa'); 169 | assert(task); 170 | cb(); 171 | }); 172 | 173 | base.register('bar', function(app, base) {}); 174 | base.register('baz', function(app, base) { 175 | app.task('aaa', () => {}); 176 | }); 177 | 178 | base.getGenerator('foo'); 179 | }); 180 | }); 181 | 182 | describe('namespace', () => { 183 | it('should expose `app.namespace`', cb => { 184 | base.generator('foo', app => { 185 | assert(typeof app.namespace, 'string'); 186 | cb(); 187 | }); 188 | }); 189 | 190 | it('should create namespace from generator alias', cb => { 191 | base.generator('generate-foo', app => { 192 | assert.equal(app.namespace, 'generate.foo'); 193 | cb(); 194 | }); 195 | }); 196 | 197 | it('should create sub-generator namespace from parent namespace and alias', cb => { 198 | base.generator('generate-foo', app => { 199 | assert(app !== base); 200 | assert(app.base === base); 201 | assert.equal(app.namespace, `${base.name}.foo`); 202 | 203 | app.generator('generate-bar', bar => { 204 | assert(bar !== app); 205 | assert(bar !== base); 206 | assert(bar.base === base); 207 | assert.equal(bar.namespace, `${base.name}.foo.bar`); 208 | 209 | bar.generator('generate-baz', baz => { 210 | assert(baz !== bar); 211 | assert(baz !== app); 212 | assert(baz !== base); 213 | assert(baz.base === base); 214 | assert.equal(baz.namespace, `${base.name}.foo.bar.baz`); 215 | 216 | baz.generator('generate-qux', qux => { 217 | assert(qux !== baz); 218 | assert(qux !== bar); 219 | assert(qux !== app); 220 | assert(qux !== base); 221 | assert(qux.base === base); 222 | assert.equal(qux.namespace, `${base.name}.foo.bar.baz.qux`); 223 | cb(); 224 | }); 225 | }); 226 | }); 227 | }); 228 | }); 229 | 230 | it('should expose namespace on `this`', cb => { 231 | base.generator('generate-foo', function(app) { 232 | assert.equal(this.namespace, 'generate.foo'); 233 | 234 | this.generator('generate-bar', function() { 235 | assert.equal(this.namespace, 'generate.foo.bar'); 236 | 237 | this.generator('generate-baz', function() { 238 | assert.equal(this.namespace, 'generate.foo.bar.baz'); 239 | 240 | this.generator('generate-qux', function() { 241 | assert.equal(this.namespace, 'generate.foo.bar.baz.qux'); 242 | assert.equal(app.namespace, 'generate.foo'); 243 | assert.equal(this.base.namespace, 'generate'); 244 | assert.equal(app.base.namespace, 'generate'); 245 | cb(); 246 | }); 247 | }); 248 | }); 249 | }); 250 | }); 251 | }); 252 | }); 253 | -------------------------------------------------------------------------------- /test/app.getGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Generator = require('..'); 6 | let base; 7 | 8 | describe('.getGenerator', () => { 9 | beforeEach(() => { 10 | base = new Generator(); 11 | }); 12 | 13 | it('should get a generator from the base instance', () => { 14 | base.register('abc', () => {}); 15 | const generator = base.getGenerator('abc'); 16 | assert(Generator.isGenerator(generator)); 17 | assert.equal(generator.name, 'abc'); 18 | }); 19 | 20 | it('should throw an error when a generator is not found', () => { 21 | base.register('foo', () => {}); 22 | assert.throws(() => base.getGenerator('whatever'), /is not registered/); 23 | assert.throws(() => base.getGenerator('foo.whatever'), /is not registered/); 24 | }); 25 | 26 | it('should get a generator from the base instance from a nested generator', () => { 27 | let count = 0; 28 | 29 | base.register('abc', () => {}); 30 | base.register('xyz', app => { 31 | app.register('one', function() { 32 | const generator = this.base.getGenerator('abc'); 33 | assert(generator); 34 | assert.equal(typeof generator, 'object'); 35 | assert.equal(generator.name, 'abc'); 36 | count++; 37 | }); 38 | 39 | app.register('two', () => { 40 | const generator = base.getGenerator('abc'); 41 | assert(generator); 42 | assert.equal(typeof generator, 'object'); 43 | assert.equal(generator.name, 'abc'); 44 | count++; 45 | }); 46 | 47 | app.register('three', function(three) { 48 | const generator = three.base.getGenerator('abc'); 49 | assert(generator); 50 | assert.equal(typeof generator, 'object'); 51 | assert.equal(generator.name, 'abc'); 52 | count++; 53 | }); 54 | }); 55 | 56 | base.getGenerator('xyz.one'); 57 | base.getGenerator('xyz.two'); 58 | base.getGenerator('xyz.three'); 59 | assert.equal(count, 3); 60 | }); 61 | 62 | it('should get a generator from the base instance using `this`', () => { 63 | base.register('abc', () => {}); 64 | base.register('xyz', app => { 65 | app.register('sub', function(sub) { 66 | const generator = this.getGenerator('abc'); 67 | assert(generator); 68 | assert.equal(typeof generator, 'object'); 69 | assert.equal(generator.name, 'abc'); 70 | }); 71 | }); 72 | base.getGenerator('xyz'); 73 | }); 74 | 75 | it('should get a base generator from "app" from a nested generator', () => { 76 | base.register('abc', () => {}); 77 | base.register('xyz', app => { 78 | app.register('sub', function(sub) { 79 | const generator = app.getGenerator('abc'); 80 | assert(generator); 81 | assert.equal(typeof generator, 'object'); 82 | assert.equal(generator.name, 'abc'); 83 | }); 84 | }); 85 | base.getGenerator('xyz'); 86 | }); 87 | 88 | it('should get a nested generator', () => { 89 | base.register('abc', function(abc) { 90 | abc.register('def', function(def) {}); 91 | }); 92 | 93 | const generator = base.getGenerator('abc.def'); 94 | assert(generator); 95 | assert.equal(typeof generator, 'object'); 96 | assert.equal(generator.name, 'def'); 97 | }); 98 | 99 | it('should get a deeply nested generator', () => { 100 | base.register('abc', function(abc) { 101 | abc.register('def', function(def) { 102 | def.register('ghi', function(ghi) { 103 | ghi.register('jkl', function(jkl) { 104 | jkl.register('mno', () => {}); 105 | }); 106 | }); 107 | }); 108 | }); 109 | 110 | const generator = base.getGenerator('abc.def.ghi.jkl.mno'); 111 | assert(generator); 112 | assert.equal(typeof generator, 'object'); 113 | assert.equal(generator.name, 'mno'); 114 | }); 115 | 116 | it('should set deeply nested generators on the base instance cache', () => { 117 | base.disableInspect(); 118 | 119 | base.register('abc', function(abc) { 120 | this.register('def', function(def) { 121 | this.register('ghi', function(ghi) { 122 | this.register('jkl', function(jkl) { 123 | this.register('mno', () => {}); 124 | }); 125 | }); 126 | }); 127 | }); 128 | 129 | base.getGenerator('abc.def.ghi.jkl.mno'); 130 | 131 | assert(base.namespaces.has('generate.abc')); 132 | assert(base.namespaces.has('generate.abc.def')); 133 | assert(base.namespaces.has('generate.abc.def.ghi')); 134 | assert(base.namespaces.has('generate.abc.def.ghi.jkl')); 135 | assert(base.namespaces.has('generate.abc.def.ghi.jkl.mno')); 136 | }); 137 | 138 | it('should allow a generator to be called on multiple instances', () => { 139 | const parents = []; 140 | 141 | function generator() { 142 | parents.push(this.parent.name); 143 | } 144 | 145 | base.register('one', function() { 146 | this.register('sub', generator); 147 | }); 148 | 149 | base.register('two', function() { 150 | this.register('sub', generator); 151 | }); 152 | 153 | assert.equal(typeof base.getGenerator('one.sub'), 'object'); 154 | assert.equal(typeof base.getGenerator('two.sub'), 'object'); 155 | assert.equal(base.findGenerator('one.sub').called, 1); 156 | assert.equal(base.findGenerator('two.sub').called, 1); 157 | }); 158 | 159 | it('should only invoke generators once by default', () => { 160 | let called = 0; 161 | 162 | base.register('one', function() { 163 | this.register('child', function() { 164 | called++; 165 | }); 166 | }); 167 | 168 | // _shouldn't_ be called by ".findGenerator" 169 | const wrappedFn = base.findGenerator('one.child'); 170 | 171 | assert.equal(called, 0); 172 | assert.equal(base.findGenerator('one.child').called, 0); 173 | 174 | // _should_ be called by ".getGenerator" 175 | base.getGenerator('one.child'); 176 | assert.equal(called, 1); 177 | assert.equal(base.findGenerator('one.child').called, called); 178 | 179 | wrappedFn.call(base, base); 180 | assert.equal(called, 1); 181 | assert.equal(base.findGenerator('one.child').called, called); 182 | 183 | wrappedFn.call(base, base); 184 | assert.equal(called, 1); 185 | assert.equal(base.findGenerator('one.child').called, called); 186 | }); 187 | 188 | it('should invoke generator functions multiple times when options.once is false', () => { 189 | let called = 0; 190 | 191 | base.register('one', function() { 192 | this.register('child', { once: false }, function() { 193 | called++; 194 | }); 195 | }); 196 | 197 | // _shouldn't_ be called by ".findGenerator" 198 | const wrappedFn = base.findGenerator('one.child'); 199 | 200 | assert.equal(called, 0); 201 | assert.equal(base.findGenerator('one.child').called, 0); 202 | 203 | // _should_ be called by ".getGenerator" 204 | base.getGenerator('one.child'); 205 | assert.equal(called, 1); 206 | assert.equal(base.findGenerator('one.child').called, called); 207 | 208 | wrappedFn.call(base, base); 209 | assert.equal(called, 2); 210 | assert.equal(base.findGenerator('one.child').called, called); 211 | 212 | wrappedFn.call(base, base); 213 | assert.equal(called, 3); 214 | assert.equal(base.findGenerator('one.child').called, called); 215 | }); 216 | 217 | it('should keep the original (unwrapped) function on generator.fn', () => { 218 | let called = 0; 219 | 220 | base.register('one', function() { 221 | this.register('child', function() { 222 | called++; 223 | }); 224 | }); 225 | 226 | // the wrapped generator function should not be called, but the 227 | // original function will be invoked each time 228 | const originalFn = base.findGenerator('one.child').fn; 229 | assert.equal(called, 0); 230 | assert.equal(base.findGenerator('one.child').called, 0); 231 | 232 | originalFn.call(base, base); 233 | assert.equal(called, 1); 234 | assert.equal(base.findGenerator('one.child').called, 0); 235 | 236 | originalFn.call(base, base); 237 | assert.equal(called, 2); 238 | assert.equal(base.findGenerator('one.child').called, 0); 239 | 240 | originalFn.call(base, base); 241 | assert.equal(called, 3); 242 | assert.equal(base.findGenerator('one.child').called, 0); 243 | }); 244 | }); 245 | -------------------------------------------------------------------------------- /test/app.register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Generator = require('..'); 6 | let base; 7 | 8 | describe('.register', () => { 9 | beforeEach(() => { 10 | base = new Generator('base'); 11 | }); 12 | 13 | describe('function', () => { 14 | it('should register a generator a function', () => { 15 | base.register('foo', () => {}); 16 | const foo = base.getGenerator('foo'); 17 | assert(foo); 18 | assert.equal(foo.name, 'foo'); 19 | }); 20 | 21 | it('should get a task from a generator registered as a function', () => { 22 | base.register('foo', function(foo) { 23 | foo.task('default', () => {}); 24 | }); 25 | const generator = base.getGenerator('foo'); 26 | assert(generator); 27 | assert(generator.tasks); 28 | assert(generator.tasks.has('default')); 29 | }); 30 | 31 | it('should get a generator from a generator registered as a function', () => { 32 | base.register('foo', function(foo) { 33 | foo.register('bar', () => {}); 34 | }); 35 | 36 | const generator = base.getGenerator('foo.bar'); 37 | assert(generator); 38 | assert.equal(generator.name, 'bar'); 39 | }); 40 | 41 | it('should get a sub-generator from a generator registered as a function', () => { 42 | base.register('foo', function(foo) { 43 | foo.register('bar', function(bar) { 44 | bar.task('something', () => {}); 45 | }); 46 | }); 47 | const bar = base.getGenerator('foo.bar'); 48 | assert(bar); 49 | assert(bar.tasks); 50 | assert(bar.tasks.has('something')); 51 | }); 52 | 53 | it('should get a deeply-nested sub-generator registered as a function', () => { 54 | base.register('foo', function(foo) { 55 | foo.register('bar', function(bar) { 56 | bar.register('baz', function(baz) { 57 | baz.register('qux', function(qux) { 58 | qux.task('qux-one', () => {}); 59 | }); 60 | }); 61 | }); 62 | }); 63 | 64 | const qux = base.getGenerator('foo.bar.baz.qux'); 65 | assert(qux); 66 | assert(qux.tasks); 67 | assert(qux.tasks.has('qux-one')); 68 | }); 69 | 70 | it('should expose the instance from each generator', () => { 71 | base.register('foo', function(foo) { 72 | foo.register('bar', function(bar) { 73 | bar.register('baz', function(baz) { 74 | baz.register('qux', function(qux) { 75 | qux.task('qux-one', () => {}); 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | const qux = base 82 | .getGenerator('foo') 83 | .getGenerator('bar') 84 | .getGenerator('baz') 85 | .getGenerator('qux'); 86 | 87 | assert(qux); 88 | assert(qux.tasks); 89 | assert(qux.tasks.has('qux-one')); 90 | }); 91 | 92 | it('should throw an error when getting a generator that does not exist', () => { 93 | base.register('foo', function(foo) { 94 | foo.register('bar', function(bar) { 95 | bar.register('baz', function(baz) { 96 | baz.register('qux', function(qux) { 97 | }); 98 | }); 99 | }); 100 | }); 101 | 102 | assert.throws(() => base.getGenerator('foo.bar.fez'), /is not registered/); 103 | }); 104 | 105 | it('should expose the `base` instance as the second param', cb => { 106 | base.register('foo', function(foo) { 107 | assert(foo.base.generators.has('foo')); 108 | cb(); 109 | }); 110 | base.getGenerator('foo'); 111 | }); 112 | 113 | it('should expose sibling generators on the `base` instance', cb => { 114 | base.register('foo', function(foo) { 115 | foo.task('abc', () => {}); 116 | }); 117 | base.register('bar', function(bar) { 118 | assert(this.base.generators.has('foo')); 119 | assert(this.base.generators.has('bar')); 120 | cb(); 121 | }); 122 | 123 | base.getGenerator('foo'); 124 | base.getGenerator('bar'); 125 | }); 126 | }); 127 | 128 | describe('name', () => { 129 | it('should use a custom function to create the name', () => { 130 | base.options.toAlias = name => name.slice(name.lastIndexOf('-') + 1); 131 | 132 | base.register('base-abc-xyz', () => {}); 133 | const xyz = base.getGenerator('xyz'); 134 | assert(xyz); 135 | assert.equal(xyz.name, 'base-abc-xyz'); 136 | assert.equal(xyz.alias, 'xyz'); 137 | }); 138 | }); 139 | 140 | describe('instance', () => { 141 | it('should register an instance', () => { 142 | base.register('base-inst', new Generator()); 143 | assert(base.generators.has('base-inst')); 144 | }); 145 | 146 | it('should get a generator that was registered as an instance', () => { 147 | const foo = new Generator('foo'); 148 | foo.task('default', () => {}); 149 | base.register('foo', foo); 150 | assert(base.getGenerator('foo')); 151 | }); 152 | 153 | it('should register multiple instances', () => { 154 | const foo = new Generator('foo'); 155 | const bar = new Generator('bar'); 156 | const baz = new Generator('baz'); 157 | base.register('foo', foo); 158 | base.register('bar', bar); 159 | base.register('baz', baz); 160 | assert(base.getGenerator('foo')); 161 | assert(base.getGenerator('bar')); 162 | assert(base.getGenerator('baz')); 163 | }); 164 | 165 | it('should get tasks from a generator that was registered as an instance', () => { 166 | const foo = new Generator(); 167 | foo.task('default', () => {}); 168 | base.register('foo', foo); 169 | const generator = base.getGenerator('foo'); 170 | assert(generator); 171 | assert(generator.tasks.has('default')); 172 | }); 173 | 174 | it('should get sub-generators from a generator registered as an instance', () => { 175 | const foo = new Generator(); 176 | foo.register('bar', () => {}); 177 | base.register('foo', foo); 178 | const generator = base.getGenerator('foo.bar'); 179 | assert(generator); 180 | }); 181 | 182 | it('should get tasks from sub-generators registered as an instance', () => { 183 | const foo = new Generator(); 184 | 185 | foo.options.isFoo = true; 186 | foo.register('bar', function(bar) { 187 | bar.task('whatever', () => {}); 188 | }); 189 | 190 | base.register('foo', foo); 191 | const generator = base.getGenerator('foo.bar'); 192 | assert(generator.tasks); 193 | assert(generator.tasks.has('whatever')); 194 | }); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /test/app.task.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Generator = require('..'); 6 | let base; 7 | 8 | describe('.task', () => { 9 | beforeEach(() => { 10 | base = new Generator(); 11 | }); 12 | 13 | it('should register a task', () => { 14 | const fn = cb => cb(); 15 | base.task('default', fn); 16 | assert.equal(typeof base.tasks.get('default'), 'object'); 17 | assert.equal(base.tasks.get('default').callback, fn); 18 | }); 19 | 20 | it('should register a task with an array of dependencies', cb => { 21 | let count = 0; 22 | base.task('foo', next => { 23 | count++; 24 | next(); 25 | }); 26 | base.task('bar', next => { 27 | count++; 28 | next(); 29 | }); 30 | base.task('default', ['foo', 'bar'], next => { 31 | count++; 32 | next(); 33 | }); 34 | assert.equal(typeof base.tasks.get('default'), 'object'); 35 | assert.deepEqual(base.tasks.get('default').deps, ['foo', 'bar']); 36 | base.build('default', err => { 37 | if (err) return cb(err); 38 | assert.equal(count, 3); 39 | cb(); 40 | }); 41 | }); 42 | 43 | it('should run a glob of tasks', cb => { 44 | let count = 0; 45 | base.task('foo', next => { 46 | count++; 47 | next(); 48 | }); 49 | base.task('bar', next => { 50 | count++; 51 | next(); 52 | }); 53 | base.task('baz', next => { 54 | count++; 55 | next(); 56 | }); 57 | base.task('qux', next => { 58 | count++; 59 | next(); 60 | }); 61 | base.task('default', ['b*']); 62 | assert.equal(typeof base.tasks.get('default'), 'object'); 63 | base.build('default', err => { 64 | if (err) return cb(err); 65 | assert.equal(count, 2); 66 | cb(); 67 | }); 68 | }); 69 | 70 | it('should register a task with a list of strings as dependencies', () => { 71 | base.task('default', 'foo', 'bar', cb => { 72 | cb(); 73 | }); 74 | assert.equal(typeof base.tasks.get('default'), 'object'); 75 | assert.deepEqual(base.tasks.get('default').deps, ['foo', 'bar']); 76 | }); 77 | 78 | it('should run a task', cb => { 79 | let count = 0; 80 | base.task('default', cb => { 81 | count++; 82 | cb(); 83 | }); 84 | 85 | base.build('default', err => { 86 | if (err) return cb(err); 87 | assert.equal(count, 1); 88 | cb(); 89 | }); 90 | }); 91 | 92 | it('should throw an error when a task with unregistered dependencies is run', cb => { 93 | base.task('default', ['foo', 'bar']); 94 | base.build('default', err => { 95 | assert(err); 96 | cb(); 97 | }); 98 | }); 99 | 100 | it('should throw an error when a task does not exist', () => { 101 | return base.build('default') 102 | .then(() => { 103 | throw new Error('expected an error'); 104 | }) 105 | .catch(err => { 106 | assert(/registered/.test(err.message)); 107 | }); 108 | }); 109 | 110 | it('should emit task events', () => { 111 | const expected = []; 112 | 113 | base.on('task-registered', function(task) { 114 | expected.push(task.status + '.' + task.name); 115 | }); 116 | 117 | base.on('task-preparing', function(task) { 118 | expected.push(task.status + '.' + task.name); 119 | }); 120 | 121 | base.on('task', function(task) { 122 | expected.push(task.status + '.' + task.name); 123 | }); 124 | 125 | base.task('foo', cb => cb()); 126 | base.task('bar', ['foo'], cb => cb()); 127 | base.task('default', ['bar']); 128 | 129 | return base.build('default') 130 | .then(() => { 131 | assert.deepEqual(expected, [ 132 | 'registered.foo', 133 | 'registered.bar', 134 | 'registered.default', 135 | 'preparing.default', 136 | 'starting.default', 137 | 'preparing.bar', 138 | 'starting.bar', 139 | 'preparing.foo', 140 | 'starting.foo', 141 | 'finished.foo', 142 | 'finished.bar', 143 | 'finished.default' 144 | ]); 145 | }); 146 | }); 147 | 148 | it('should emit an error event when an error is returned in a task callback', cb => { 149 | base.on('error', err => { 150 | assert(err); 151 | assert.equal(err.message, 'This is an error'); 152 | }); 153 | base.task('default', cb => { 154 | return cb(new Error('This is an error')); 155 | }); 156 | base.build('default', err => { 157 | if (err) return cb(); 158 | cb(new Error('Expected an error')); 159 | }); 160 | }); 161 | 162 | it('should emit an error event when an error is thrown in a task', cb => { 163 | base.on('error', err => { 164 | assert(err); 165 | assert.equal(err.message, 'This is an error'); 166 | }); 167 | base.task('default', cb => { 168 | cb(new Error('This is an error')); 169 | }); 170 | base.build('default', err => { 171 | assert(err); 172 | cb(); 173 | }); 174 | }); 175 | 176 | it('should run dependencies before running the dependent task', cb => { 177 | const expected = []; 178 | 179 | base.task('foo', cb => { 180 | expected.push('foo'); 181 | cb(); 182 | }); 183 | base.task('bar', cb => { 184 | expected.push('bar'); 185 | cb(); 186 | }); 187 | base.task('default', ['foo', 'bar'], cb => { 188 | expected.push('default'); 189 | cb(); 190 | }); 191 | 192 | base.build('default', err => { 193 | if (err) return cb(err); 194 | assert.deepEqual(expected, ['foo', 'bar', 'default']); 195 | cb(); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /test/app.tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const util = require('util'); 6 | const Generator = require('..'); 7 | let app; 8 | 9 | describe('.tasks', () => { 10 | beforeEach(() => { 11 | app = new Generator(); 12 | }); 13 | 14 | it('should throw an error when a name is not given for a task', () => { 15 | assert.throws(() => app.task(), /expected/); 16 | }); 17 | 18 | it('should register a task', () => { 19 | app.task('default', () => {}); 20 | assert(app.tasks.get('default')); 21 | assert.equal(typeof app.tasks.get('default'), 'object'); 22 | assert.equal(typeof app.tasks.get('default').callback, 'function'); 23 | }); 24 | 25 | it('should register a noop task when only name is given', () => { 26 | app.task('default'); 27 | assert(app.tasks.get('default')); 28 | assert.equal(typeof app.tasks.get('default'), 'object'); 29 | assert.equal(typeof app.tasks.get('default').callback, 'function'); 30 | }); 31 | 32 | it('should register a noop task when a name and an empty dependencies array is given', () => { 33 | app.task('default', []); 34 | assert(app.tasks.get('default')); 35 | assert.equal(typeof app.tasks.get('default'), 'object'); 36 | assert.equal(typeof app.tasks.get('default').callback, 'function'); 37 | }); 38 | 39 | it('should register a task with an array of named dependencies', () => { 40 | app.task('default', ['foo', 'bar'], cb => cb()); 41 | assert(app.tasks.get('default')); 42 | assert.equal(typeof app.tasks.get('default'), 'object'); 43 | assert.deepEqual(app.tasks.get('default').deps, ['foo', 'bar']); 44 | }); 45 | 46 | it('should register a task with an array of function dependencies', () => { 47 | const bar = cb => cb(); 48 | const Baz = cb => cb(); 49 | 50 | const deps = ['foo', bar, Baz, cb => cb()]; 51 | app.task('default', deps, cb => cb()); 52 | 53 | assert.equal(typeof app.tasks.get('default'), 'object'); 54 | assert.deepEqual(app.tasks.get('default').deps.length, 4); 55 | }); 56 | 57 | it('should register a task with a list of strings as dependencies', () => { 58 | app.task('default', 'foo', 'bar', cb => cb()); 59 | assert.equal(typeof app.tasks.get('default'), 'object'); 60 | assert.deepEqual(app.tasks.get('default').deps.length, 2); 61 | assert.deepEqual(app.tasks.get('default').deps, ['foo', 'bar']); 62 | }); 63 | 64 | it('should register a task as a noop function when only dependencies are given', () => { 65 | app.task('default', ['foo', 'bar']); 66 | assert.equal(typeof app.tasks.get('default'), 'object'); 67 | assert.equal(typeof app.tasks.get('default').callback, 'function'); 68 | }); 69 | 70 | it('should register a task with options as the second argument', () => { 71 | app.task('default', { one: 'two' }, ['foo', 'bar']); 72 | assert.equal(typeof app.tasks.get('default'), 'object'); 73 | assert.equal(typeof app.tasks.get('default').callback, 'function'); 74 | assert.equal(app.tasks.get('default').options.one, 'two'); 75 | }); 76 | 77 | it('should run a task', cb => { 78 | let count = 0; 79 | app.task('default', cb => { 80 | count++; 81 | cb(); 82 | }); 83 | 84 | app.build('default', err => { 85 | if (err) return cb(err); 86 | assert.equal(count, 1); 87 | cb(); 88 | }); 89 | }); 90 | 91 | it('should run a task and return a promise', () => { 92 | let count = 0; 93 | app.task('default', cb => { 94 | count++; 95 | cb(); 96 | }); 97 | 98 | return app.build('default') 99 | .then(() => { 100 | assert.equal(count, 1); 101 | }); 102 | }); 103 | 104 | it('should run a task with options', cb => { 105 | let count = 0; 106 | app.task('default', { silent: false }, function(next) { 107 | assert.equal(this.options.silent, false); 108 | count++; 109 | next(); 110 | }); 111 | 112 | app.build('default', err => { 113 | if (err) return cb(err); 114 | assert.equal(count, 1); 115 | cb(); 116 | }); 117 | }); 118 | 119 | it('should run a task with options defined on .build', cb => { 120 | let count = 0; 121 | app.task('default', { silent: false }, function(next) { 122 | assert.equal(this.options.silent, true); 123 | assert.equal(this.options.foo, 'bar'); 124 | count++; 125 | next(); 126 | }); 127 | 128 | app.build('default', { silent: true, foo: 'bar' }, err => { 129 | if (err) return cb(err); 130 | assert.equal(count, 1); 131 | cb(); 132 | }); 133 | }); 134 | 135 | it('should run the `default` task when no task is given', cb => { 136 | let count = 0; 137 | 138 | app.task('default', next => { 139 | count++; 140 | next(); 141 | }); 142 | 143 | app.build(err => { 144 | if (err) return cb(err); 145 | assert.equal(count, 1); 146 | cb(); 147 | }); 148 | }); 149 | 150 | it('should skip tasks when `run === false`', cb => { 151 | const expected = []; 152 | function callback() { 153 | return function(next) { 154 | expected.push(this.name); 155 | next(); 156 | }; 157 | } 158 | 159 | app.task('foo', callback()); 160 | app.task('bar', {run: false}, callback()); 161 | app.task('baz', callback()); 162 | app.task('bang', {run: false}, callback()); 163 | app.task('beep', callback()); 164 | app.task('boop', callback()); 165 | 166 | app.task('default', ['foo', 'bar', 'baz', 'bang', 'beep', 'boop']); 167 | app.build(err => { 168 | if (err) return cb(err); 169 | assert.deepEqual(expected, ['foo', 'baz', 'beep', 'boop']); 170 | cb(); 171 | }); 172 | }); 173 | 174 | it('should skip tasks when `run === false` (with deps skipped)', cb => { 175 | const expected = []; 176 | function callback() { 177 | return function(next) { 178 | expected.push(this.name); 179 | next(); 180 | }; 181 | } 182 | 183 | app.task('foo', callback()); 184 | app.task('bar', { run: false }, ['foo'], callback()); 185 | app.task('baz', ['bar'], callback()); 186 | app.task('bang', { run: false }, ['baz'], callback()); 187 | app.task('beep', ['bang'], callback()); 188 | app.task('boop', ['beep'], callback()); 189 | 190 | app.task('default', ['boop']); 191 | app.build(err => { 192 | if (err) return cb(err); 193 | assert.deepEqual(expected, ['beep', 'boop']); 194 | cb(); 195 | }); 196 | }); 197 | 198 | it('should skip tasks when `run === false` (complex flow)', cb => { 199 | const expected = []; 200 | 201 | app.task('foo', function(next) { 202 | expected.push(this.name); 203 | // disable running the "bar" task 204 | app.tasks.get('bar').options.run = false; 205 | next(); 206 | }); 207 | 208 | app.task('bar', function(next) { 209 | expected.push(this.name); 210 | next(); 211 | }); 212 | 213 | app.task('baz', function(next) { 214 | expected.push(this.name); 215 | // enable running the "bang" task 216 | app.tasks.get('bang').options.run = true; 217 | next(); 218 | }); 219 | 220 | app.task('bang', {run: false}, function(next) { 221 | expected.push(this.name); 222 | next(); 223 | }); 224 | 225 | app.task('beep', function(next) { 226 | expected.push(this.name); 227 | next(); 228 | }); 229 | 230 | app.task('boop', function(next) { 231 | expected.push(this.name); 232 | next(); 233 | }); 234 | 235 | app.task('default', ['foo', 'bar', 'baz', 'bang', 'beep', 'boop']); 236 | app.build(err => { 237 | if (err) return cb(err); 238 | assert.deepEqual(expected, ['foo', 'baz', 'bang', 'beep', 'boop']); 239 | cb(); 240 | }); 241 | }); 242 | 243 | it('should throw an error when a task with unregistered dependencies is built', cb => { 244 | let count = 0; 245 | app.task('default', ['foo', 'bar'], cb => { 246 | count++; 247 | cb(); 248 | }); 249 | 250 | app.build('default', err => { 251 | if (!err) return cb(new Error('Expected an error to be thrown.')); 252 | assert.equal(count, 0); 253 | cb(); 254 | }); 255 | }); 256 | 257 | it('should throw an error when a task with globbed dependencies cannot be found', cb => { 258 | let count = 0; 259 | app.task('default', ['a-*'], cb => { 260 | count++; 261 | cb(); 262 | }); 263 | 264 | app.build('default', err => { 265 | if (!err) return cb(new Error('Expected an error to be thrown.')); 266 | assert.equal(count, 0); 267 | cb(); 268 | }); 269 | }); 270 | 271 | it('should emit task events', cb => { 272 | const events = []; 273 | const push = task => events.push(task.status + '.' + task.name); 274 | 275 | app.on('task', push); 276 | app.on('task-registered', push); 277 | app.on('task-preparing', push); 278 | 279 | app.on('error', err => { 280 | if (!err.build) { 281 | events.push('error.' + err.task.name); 282 | } 283 | }); 284 | 285 | app.task('foo', cb => cb()); 286 | app.task('bar', ['foo'], cb => cb()); 287 | 288 | app.task('default', ['bar']); 289 | app.build('default', err => { 290 | if (err) return cb(err); 291 | assert.deepEqual(events, [ 292 | 'registered.foo', 293 | 'registered.bar', 294 | 'registered.default', 295 | 'preparing.default', 296 | 'starting.default', 297 | 'preparing.bar', 298 | 'starting.bar', 299 | 'preparing.foo', 300 | 'starting.foo', 301 | 'finished.foo', 302 | 'finished.bar', 303 | 'finished.default' 304 | ]); 305 | cb(); 306 | }); 307 | }); 308 | 309 | it('should emit an error event when an error is returned in a callback', cb => { 310 | let count = 0; 311 | app.on('error', err => { 312 | assert(err); 313 | count++; 314 | }); 315 | 316 | app.task('default', cb => { 317 | cb(new Error('This is an error')); 318 | }); 319 | 320 | app.build('default', err => { 321 | assert(err); 322 | assert.equal(count, 1); 323 | cb(); 324 | }); 325 | }); 326 | 327 | it('should emit an error event when an error is thrown in a task', cb => { 328 | let count = 0; 329 | app.on('error', err => { 330 | assert(err); 331 | count++; 332 | }); 333 | 334 | app.task('default', () => { 335 | throw new Error('This is an error'); 336 | }); 337 | 338 | app.build('default', err => { 339 | assert(err); 340 | assert(/This is an error/.test(err.message)); 341 | assert.equal(count, 1); 342 | cb(); 343 | }); 344 | }); 345 | 346 | it('should emit build events', () => { 347 | const events = []; 348 | const errors = []; 349 | 350 | app.on('build', function(build) { 351 | events.push(build.status); 352 | }); 353 | 354 | app.on('error', err => { 355 | errors.push(err); 356 | }); 357 | 358 | app.task('foo', cb => cb()); 359 | app.task('bar', ['foo'], cb => cb()); 360 | 361 | app.task('default', ['bar']); 362 | return app.build('default') 363 | .then(() => { 364 | assert.equal(errors.length, 0); 365 | assert.deepEqual(events, ['starting', 'finished']); 366 | }); 367 | }); 368 | 369 | it('should emit a build error event when an error is passed back in a task', cb => { 370 | let count = 0; 371 | 372 | app.on('error', err => { 373 | assert(err); 374 | count++; 375 | }); 376 | 377 | app.task('default', cb => { 378 | cb(new Error('This is an error')); 379 | }); 380 | 381 | app.build('default', err => { 382 | assert(err); 383 | assert.equal(count, 1); 384 | cb(); 385 | }); 386 | }); 387 | 388 | it('should emit a build error event when an error is passed back in a task (with promise)', () => { 389 | let count = 0; 390 | 391 | app.on('error', err => { 392 | assert(err); 393 | count++; 394 | }); 395 | 396 | app.task('default', next => { 397 | next(new Error('This is an error')); 398 | }); 399 | 400 | return app.build('default') 401 | .catch(err => { 402 | assert(err); 403 | assert.equal(count, 1); 404 | }); 405 | }); 406 | 407 | it('should emit a build error event when an error is thrown in a task', cb => { 408 | let count = 0; 409 | 410 | app.on('error', err => { 411 | assert(err); 412 | assert(/This is an error/.test(err.message)); 413 | count++; 414 | }); 415 | 416 | app.task('default', () => { 417 | throw new Error('This is an error'); 418 | }); 419 | 420 | app.build('default', err => { 421 | assert(err); 422 | assert.equal(count, 1); 423 | cb(); 424 | }); 425 | }); 426 | 427 | it('should run dependencies before running the dependent task.', cb => { 428 | const seq = []; 429 | app.task('foo', cb => { 430 | seq.push('foo'); 431 | cb(); 432 | }); 433 | 434 | app.task('bar', cb => { 435 | seq.push('bar'); 436 | cb(); 437 | }); 438 | 439 | app.task('default', ['foo', 'bar'], cb => { 440 | seq.push('default'); 441 | cb(); 442 | }); 443 | 444 | app.build('default', err => { 445 | if (err) return cb(err); 446 | assert.deepEqual(seq, ['foo', 'bar', 'default']); 447 | cb(); 448 | }); 449 | }); 450 | 451 | it('should add inspect function to tasks.', () => { 452 | app.task('foo', cb => { 453 | cb(); 454 | }); 455 | 456 | app.task('bar', cb => { 457 | cb(); 458 | }); 459 | 460 | app.task('default', ['foo', 'bar'], cb => { 461 | cb(); 462 | }); 463 | assert.equal(app.tasks.get('foo')[util.inspect.custom](), ''); 464 | assert.equal(app.tasks.get('bar')[util.inspect.custom](), ''); 465 | assert.equal(app.tasks.get('default')[util.inspect.custom](), ''); 466 | }); 467 | 468 | it('should add custom inspect function to tasks.', () => { 469 | app.on('task-registered', task => { 470 | task[util.inspect.custom] = function(task) { 471 | return ''; 474 | }; 475 | }); 476 | 477 | app.task('foo', cb => { 478 | cb(); 479 | }); 480 | 481 | app.task('bar', cb => { 482 | cb(); 483 | }); 484 | 485 | app.task('default', ['foo', 'bar'], cb => { 486 | cb(); 487 | }); 488 | 489 | assert.equal(app.tasks.get('foo')[util.inspect.custom](), ''); 490 | assert.equal(app.tasks.get('bar')[util.inspect.custom](), ''); 491 | assert.equal(app.tasks.get('default')[util.inspect.custom](), ''); 492 | }); 493 | 494 | it('should run globbed dependencies before running the dependent task.', cb => { 495 | const actual = []; 496 | app.task('a-foo', cb => { 497 | actual.push('a-foo'); 498 | cb(); 499 | }); 500 | 501 | app.task('a-bar', cb => { 502 | actual.push('a-bar'); 503 | cb(); 504 | }); 505 | 506 | app.task('b-foo', cb => { 507 | actual.push('b-foo'); 508 | cb(); 509 | }); 510 | 511 | app.task('b-bar', cb => { 512 | actual.push('b-bar'); 513 | cb(); 514 | }); 515 | 516 | app.task('default', ['a-*'], cb => { 517 | actual.push('default'); 518 | cb(); 519 | }); 520 | 521 | app.build(err => { 522 | if (err) return cb(err); 523 | assert.deepEqual(actual, ['a-foo', 'a-bar', 'default']); 524 | cb(); 525 | }); 526 | }); 527 | 528 | it('should get the current task name from `this`', cb => { 529 | const actual = []; 530 | const tasks = []; 531 | 532 | const callback = function(next) { 533 | actual.push(this.name); 534 | next(); 535 | }; 536 | 537 | for (let i = 0; i < 10; i++) { 538 | tasks.push(String(i)); 539 | app.task(String(i), callback); 540 | } 541 | 542 | app.build(tasks, err => { 543 | if (err) return cb(err); 544 | assert.equal(actual.length, 10); 545 | assert.deepEqual(actual, ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); 546 | cb(); 547 | }); 548 | }); 549 | }); 550 | -------------------------------------------------------------------------------- /test/app.toAlias.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Generator = require('..'); 6 | let base; 7 | 8 | describe('.toAlias', () => { 9 | beforeEach(() => { 10 | base = new Generator('base'); 11 | }); 12 | 13 | it('should not create an alias when no prefix is given', () => { 14 | assert.equal(base.toAlias('foo-bar'), 'foo-bar'); 15 | }); 16 | 17 | it('should create an alias using the `options.toAlias` function', () => { 18 | const alias = base.toAlias('one-two-three', { 19 | toAlias: function(name) { 20 | return name.slice(name.indexOf('-') + 1); 21 | } 22 | }); 23 | assert.equal(alias, 'two-three'); 24 | }); 25 | 26 | it('should create an alias using the given function', () => { 27 | const alias = base.toAlias('one-two-three', function(name) { 28 | return name.slice(name.lastIndexOf('-') + 1); 29 | }); 30 | assert.equal(alias, 'three'); 31 | }); 32 | 33 | it('should create an alias using base.options.toAlias function', () => { 34 | base.options.toAlias = function(name) { 35 | return name.slice(name.lastIndexOf('-') + 1); 36 | }; 37 | 38 | const alias = base.toAlias('one-two-three'); 39 | assert.equal(alias, 'three'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Generator = require('..'); 6 | let base; 7 | 8 | describe('generator.events', () => { 9 | beforeEach(() => { 10 | base = new Generator(); 11 | }); 12 | 13 | it('should emit generator when a generator is registered', cb => { 14 | base.on('generator', generator => { 15 | assert.equal(generator.name, 'foo'); 16 | cb(); 17 | }); 18 | 19 | base.generator('foo', () => {}); 20 | }); 21 | 22 | it('should emit generator when base.generators.get is called', cb => { 23 | base.on('generator', generator => { 24 | assert.equal(generator.name, 'foo'); 25 | cb(); 26 | }); 27 | 28 | base.register('foo', () => {}); 29 | base.getGenerator('foo'); 30 | }); 31 | 32 | it('should emit generator.get when base.generators.get is called', cb => { 33 | base.on('generator', generator => { 34 | assert.equal(generator.name, 'foo'); 35 | cb(); 36 | }); 37 | 38 | base.register('foo', () => {}); 39 | base.getGenerator('foo'); 40 | }); 41 | 42 | it('should emit error on base when a base generator emits an error', cb => { 43 | let called = 0; 44 | 45 | base.on('error', err => { 46 | assert.equal(err.message, 'whatever'); 47 | called++; 48 | }); 49 | 50 | base.register('foo', app => { 51 | app.emit('error', new Error('whatever')); 52 | }); 53 | 54 | base.getGenerator('foo'); 55 | assert.equal(called, 1); 56 | cb(); 57 | }); 58 | 59 | it('should emit error on base when a base generator throws an error', () => { 60 | let called = 0; 61 | 62 | base.on('error', err => { 63 | assert.equal(err.message, 'whatever'); 64 | called++; 65 | }); 66 | 67 | base.register('foo', app => { 68 | app.task('default', cb => { 69 | cb(new Error('whatever')); 70 | }); 71 | }); 72 | 73 | return base.getGenerator('foo') 74 | .build(err => { 75 | assert(err); 76 | assert.equal(called, 1); 77 | }); 78 | }); 79 | 80 | it('should emit errors on base from deeply nested generators', cb => { 81 | let called = 0; 82 | 83 | base.on('error', err => { 84 | assert.equal(err.message, 'whatever'); 85 | called++; 86 | }); 87 | 88 | base.register('a', function() { 89 | this.register('b', function() { 90 | this.register('c', function() { 91 | this.register('d', function() { 92 | this.task('default', function(next) { 93 | next(new Error('whatever')); 94 | }); 95 | }); 96 | }); 97 | }); 98 | }); 99 | 100 | base.getGenerator('a.b.c.d') 101 | .build(err => { 102 | assert(err); 103 | assert.equal(called, 1); 104 | cb(); 105 | }); 106 | }); 107 | 108 | it('should bubble up errors to all parent generators', cb => { 109 | const names = []; 110 | let called = 0; 111 | 112 | function count() { 113 | names.push(this.namespace); 114 | called++; 115 | } 116 | 117 | base.name = 'root'; 118 | base.on('error', err => { 119 | assert.equal(err.message, 'whatever'); 120 | called++; 121 | }); 122 | 123 | base.register('a', function() { 124 | this.on('error', count); 125 | 126 | this.register('b', function() { 127 | this.on('error', count); 128 | 129 | this.register('c', function() { 130 | this.on('error', count); 131 | 132 | this.register('d', function() { 133 | this.on('error', count); 134 | 135 | this.task('default', function(next) { 136 | next(new Error('whatever')); 137 | }); 138 | }); 139 | }); 140 | }); 141 | }); 142 | 143 | base.getGenerator('a.b.c.d') 144 | .build(err => { 145 | assert(err); 146 | assert.deepEqual(names, [ 'root.a.b.c.d', 'root.a.b.c', 'root.a.b', 'root.a' ]); 147 | assert.equal(called, 5); 148 | assert.equal(err.message, 'whatever'); 149 | cb(); 150 | }) 151 | .catch(cb); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /test/parallel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Composer = require('..'); 6 | let app; 7 | 8 | describe('parallel', () => { 9 | beforeEach(() => { 10 | app = new Composer(); 11 | }); 12 | 13 | describe('callback', () => { 14 | it('should compose tasks into a function that runs in parallel', cb => { 15 | const actual = []; 16 | 17 | app.task('foo', function(next) { 18 | setTimeout(() => { 19 | actual.push('foo'); 20 | next(); 21 | }, 6); 22 | }); 23 | 24 | app.task('bar', function(next) { 25 | setTimeout(() => { 26 | actual.push('bar'); 27 | next(); 28 | }, 1); 29 | }); 30 | 31 | app.task('baz', function(next) { 32 | actual.push('baz'); 33 | next(); 34 | }); 35 | 36 | const build = app.parallel(['foo', 'bar', 'baz']); 37 | 38 | build(err => { 39 | if (err) return cb(err); 40 | assert.deepEqual(actual, ['baz', 'bar', 'foo']); 41 | cb(); 42 | }); 43 | }); 44 | 45 | it('should return an error when no functions are passed to parallel', cb => { 46 | const build = app.parallel(); 47 | 48 | build(err => { 49 | assert(/actual/, err.message); 50 | assert(/tasks/, err.message); 51 | cb(); 52 | }); 53 | }); 54 | 55 | it('should compose tasks with options into a function that runs in parallel', () => { 56 | const actual = []; 57 | const task = t => { 58 | return next => { 59 | setTimeout(() => { 60 | actual.push(t); 61 | next(); 62 | }, t); 63 | }; 64 | }; 65 | 66 | const build = app.parallel([task(20), task(15), task(10), task(5), task(0)]); 67 | 68 | return build() 69 | .then(() => { 70 | assert.deepEqual(actual, [0, 5, 10, 15, 20]); 71 | }); 72 | }); 73 | 74 | it('should compose tasks with additional options into a function that runs in parallel', () => { 75 | const actual = []; 76 | 77 | app.task('foo', { silent: false }, function(next) { 78 | assert.equal(this.options.silent, true); 79 | assert.equal(this.options.foo, 'bar'); 80 | 81 | setTimeout(() => { 82 | actual.push('foo'); 83 | next(); 84 | }, 2); 85 | }); 86 | 87 | const options = { silent: true, foo: 'bar' }; 88 | 89 | const build = app.parallel('foo', options, next => { 90 | actual.push('bar'); 91 | next(); 92 | }); 93 | 94 | return build() 95 | .then(() => { 96 | assert.deepEqual(actual, ['bar', 'foo']); 97 | }); 98 | }); 99 | 100 | it('should run task dependencies in parallel', () => { 101 | const actual = []; 102 | 103 | app.task('foo', ['baz'], next => { 104 | setTimeout(() => { 105 | actual.push('foo'); 106 | next(); 107 | }, 15); 108 | }); 109 | 110 | app.task('bar', ['qux'], next => { 111 | setTimeout(() => { 112 | actual.push('bar'); 113 | next(); 114 | }, 10); 115 | }); 116 | 117 | app.task('baz', next => { 118 | setTimeout(() => { 119 | actual.push('baz'); 120 | next(); 121 | }, 5); 122 | }); 123 | 124 | app.task('qux', next => { 125 | setTimeout(() => { 126 | actual.push('qux'); 127 | next(); 128 | }, 0); 129 | }); 130 | 131 | const build = app.parallel(['foo', 'bar']); 132 | 133 | return build() 134 | .then(() => { 135 | assert.deepEqual(actual, ['qux', 'baz', 'bar', 'foo']); 136 | }); 137 | }); 138 | }); 139 | 140 | describe('promise', () => { 141 | it('should run registered tasks in parallel', () => { 142 | const actual = []; 143 | 144 | app.task('foo', next => { 145 | setTimeout(() => { 146 | actual.push('foo'); 147 | next(); 148 | }, 8); 149 | }); 150 | 151 | app.task('bar', next => { 152 | setTimeout(() => { 153 | actual.push('bar'); 154 | next(); 155 | }, 1); 156 | }); 157 | 158 | app.task('baz', next => { 159 | actual.push('baz'); 160 | next(); 161 | }); 162 | 163 | const build = app.parallel(['foo', 'bar', 'baz']); 164 | 165 | return build() 166 | .then(() => { 167 | assert.deepEqual(actual, ['baz', 'bar', 'foo']); 168 | }); 169 | }); 170 | 171 | it('should return an error when no functions are passed to parallel', cb => { 172 | const build = app.parallel(); 173 | 174 | build(err => { 175 | assert(/actual/, err.message); 176 | assert(/tasks/, err.message); 177 | cb(); 178 | }); 179 | }); 180 | 181 | it('should compose tasks with options into a function that runs in parallel', () => { 182 | const res = []; 183 | const task = function(t) { 184 | return function(next) { 185 | setTimeout(() => { 186 | res.push(t); 187 | next(); 188 | }, t); 189 | }; 190 | }; 191 | 192 | const build = app.parallel([task(20), task(15), task(10), task(5), task(0)]); 193 | 194 | return build() 195 | .then(() => { 196 | assert.deepEqual(res, [0, 5, 10, 15, 20]); 197 | }); 198 | }); 199 | 200 | it('should compose tasks with additional options into a function that runs in parallel', () => { 201 | const actual = []; 202 | 203 | app.task('foo', { silent: false }, function(next) { 204 | assert.equal(this.options.silent, true); 205 | assert.equal(this.options.foo, 'bar'); 206 | 207 | setTimeout(() => { 208 | actual.push('foo'); 209 | next(); 210 | }, 2); 211 | }); 212 | 213 | const options = { silent: true, foo: 'bar' }; 214 | 215 | const build = app.parallel('foo', options, function(next) { 216 | actual.push('bar'); 217 | next(); 218 | }); 219 | 220 | return build() 221 | .then(() => { 222 | assert.deepEqual(actual, ['bar', 'foo']); 223 | }); 224 | }); 225 | 226 | it('should return a promise when called without a callback function', () => { 227 | const actual = []; 228 | let count = 0; 229 | 230 | app.on('error', err => { 231 | actual.push('error'); 232 | assert.equal(err.message, 'bar error'); 233 | count++; 234 | }); 235 | 236 | app.task('foo', next => { 237 | setTimeout(() => { 238 | actual.push('foo'); 239 | count++; 240 | next(); 241 | }, 2); 242 | }); 243 | 244 | const build = app.parallel('foo', next => { 245 | next(new Error('bar error')); 246 | }); 247 | 248 | return build() 249 | .then(() => { 250 | throw new Error('expected an error'); 251 | }) 252 | .catch(err => { 253 | assert(err); 254 | assert.equal(count, 1); 255 | assert.deepEqual(actual, ['error']); 256 | }); 257 | }); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /test/parse-tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const parseTasks = require('../lib/parse'); 6 | const Generate = require('..'); 7 | let parse; 8 | let app; 9 | 10 | describe('parse-tasks', () => { 11 | beforeEach(() => { 12 | app = new Generate(); 13 | app.task('foo', () => {}); 14 | app.task('bar', () => {}); 15 | 16 | app.register('foo', foo => { 17 | foo.task('default', () => {}); 18 | foo.register('one', one => { 19 | one.task('default', () => {}); 20 | }); 21 | }); 22 | 23 | app.register('bar', bar => { 24 | bar.task('default', () => {}); 25 | bar.register('two', two => { 26 | two.task('default', () => {}); 27 | }); 28 | }); 29 | 30 | app.register('baz', () => {}); 31 | app.register('qux', () => {}); 32 | parse = (...tasks) => parseTasks()(app, ...tasks); 33 | }); 34 | 35 | it('should parse task arguments', () => { 36 | assert.deepEqual(parse('foo').tasks, [{ name: 'default', tasks: ['foo'] }]); 37 | 38 | assert.deepEqual(parse('foo:default').tasks, [{ name: 'foo', tasks: ['default'] }]); 39 | 40 | assert.deepEqual(parse('foo:bar').tasks, [{ name: 'foo', tasks: ['bar'] }]); 41 | assert.deepEqual(parse('foo', ['bar']).tasks, [{ name: 'foo', tasks: ['bar'] }]); 42 | 43 | assert.deepEqual(parse('foo,bar').tasks, [{ name: 'default', tasks: ['foo', 'bar'] }]); 44 | 45 | assert.deepEqual(parse('baz,qux').tasks, [ 46 | { name: 'baz', tasks: ['default'] }, 47 | { name: 'qux', tasks: ['default'] } 48 | ]); 49 | 50 | 51 | assert.deepEqual(parse('foo,bar baz').tasks, [ 52 | { name: 'default', tasks: ['foo', 'bar'] }, 53 | { name: 'baz', tasks: ['default'] } 54 | ]); 55 | 56 | assert.deepEqual(parse('foo bar baz').tasks, [ 57 | { name: 'default', tasks: ['foo'] }, 58 | { name: 'default', tasks: ['bar'] }, 59 | { name: 'baz', tasks: ['default'] } 60 | ]); 61 | 62 | assert.deepEqual(parse('foo:default bar:default baz').tasks, [ 63 | { name: 'foo', tasks: ['default'] }, 64 | { name: 'bar', tasks: ['default'] }, 65 | { name: 'baz', tasks: ['default'] } 66 | ]); 67 | 68 | assert.deepEqual(parse('foo:default,bar baz').tasks, [ 69 | { name: 'foo', tasks: ['default', 'bar'] }, 70 | { name: 'baz', tasks: ['default'] } 71 | ]); 72 | 73 | assert.deepEqual(parse('foo:default,bar,baz baz').tasks, [ 74 | { name: 'foo', tasks: ['default', 'bar', 'baz'] }, 75 | { name: 'baz', tasks: ['default'] } 76 | ]); 77 | 78 | assert.deepEqual(parse('foo:default bar:default baz').tasks, [ 79 | { name: 'foo', tasks: ['default'] }, 80 | { name: 'bar', tasks: ['default'] }, 81 | { name: 'baz', tasks: ['default'] } 82 | ]); 83 | 84 | assert.deepEqual(parse('foo:default bar:default baz,qux').tasks, [ 85 | { name: 'foo', tasks: ['default'] }, 86 | { name: 'bar', tasks: ['default'] }, 87 | { name: 'baz', tasks: ['default'] }, 88 | { name: 'qux', tasks: ['default'] } 89 | ]); 90 | 91 | assert.deepEqual(parse('foo,bar baz,qux').tasks, [ 92 | { name: 'default', tasks: ['foo', 'bar'] }, 93 | { name: 'baz', tasks: ['default'] }, 94 | { name: 'qux', tasks: ['default'] } 95 | ]); 96 | 97 | assert.deepEqual(parse(['foo,bar', 'baz,qux']).tasks, [ 98 | { name: 'default', tasks: ['foo', 'bar'] }, 99 | { name: 'baz', tasks: ['default'] }, 100 | { name: 'qux', tasks: ['default'] } 101 | ]); 102 | 103 | assert.deepEqual(parse('foo.one bar.two').tasks, [ 104 | { name: 'foo.one', tasks: ['default'] }, 105 | { name: 'bar.two', tasks: ['default'] } 106 | ]); 107 | 108 | assert.deepEqual(parse('foo.one:default bar.two:default').tasks, [ 109 | { name: 'foo.one', tasks: ['default'] }, 110 | { name: 'bar.two', tasks: ['default'] } 111 | ]); 112 | 113 | assert.deepEqual(parse('foo.one:abc bar.two:xyz').tasks, [ 114 | { name: 'foo.one', tasks: ['abc'] }, 115 | { name: 'bar.two', tasks: ['xyz'] } 116 | ]); 117 | 118 | assert.deepEqual(parse('foo.one:abc bar.two:xyz'), { 119 | callback: void 0, 120 | options: {}, 121 | tasks: [{ name: 'foo.one', tasks: ['abc'] }, { name: 'bar.two', tasks: ['xyz'] }], 122 | missing: [] 123 | }); 124 | 125 | assert.deepEqual(parse(['foo.one:abc', { foo: 'bar' }, 'bar.two:xyz']), { 126 | callback: void 0, 127 | options: { foo: 'bar' }, 128 | tasks: [{ name: 'foo.one', tasks: ['abc'] }, { name: 'bar.two', tasks: ['xyz'] }], 129 | missing: [] 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /test/series.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const Composer = require('..'); 6 | let app; 7 | 8 | describe('series', () => { 9 | beforeEach(() => { 10 | app = new Composer(); 11 | }); 12 | 13 | describe('callback', () => { 14 | it('should compose tasks into a function that runs in series', cb => { 15 | const actual = []; 16 | 17 | app.task('foo', cb => { 18 | setTimeout(() => { 19 | actual.push('foo'); 20 | cb(); 21 | }, 20); 22 | }); 23 | 24 | app.task('bar', cb => { 25 | setTimeout(() => { 26 | actual.push('bar'); 27 | cb(); 28 | }, 10); 29 | }); 30 | 31 | app.task('baz', cb => { 32 | actual.push('baz'); 33 | cb(); 34 | }); 35 | 36 | const build = app.series(['foo', 'bar', 'baz']); 37 | 38 | build(err => { 39 | if (err) return cb(err); 40 | assert.deepEqual(actual, ['foo', 'bar', 'baz']); 41 | cb(); 42 | }); 43 | }); 44 | 45 | it('should compose tasks with options into a function that runs in series', cb => { 46 | const actual = []; 47 | 48 | app.task('foo', { silent: false }, function(next) { 49 | assert.equal(this.options.silent, false); 50 | actual.push('foo'); 51 | next(); 52 | }); 53 | 54 | const build = app.series('foo', function bar(next) { 55 | actual.push('bar'); 56 | next(); 57 | }); 58 | 59 | build(err => { 60 | if (err) return cb(err); 61 | assert.deepEqual(actual, ['foo', 'bar']); 62 | cb(); 63 | }); 64 | }); 65 | 66 | it('should return an error when no functions are passed to series', cb => { 67 | const build = app.series(); 68 | 69 | build(err => { 70 | assert(/actual/, err.message); 71 | assert(/tasks/, err.message); 72 | cb(); 73 | }); 74 | }); 75 | 76 | it('should compose tasks with additional options into a function that runs in series', cb => { 77 | const actual = []; 78 | 79 | app.task('foo', { silent: false }, function(next) { 80 | assert.equal(this.options.silent, true); 81 | assert.equal(this.options.foo, 'bar'); 82 | actual.push('foo'); 83 | next(); 84 | }); 85 | 86 | const options = { silent: true, foo: 'bar' }; 87 | const build = app.series('foo', function(next) { 88 | actual.push('bar'); 89 | next(); 90 | }, options); 91 | 92 | build(err => { 93 | if (err) { 94 | cb(err); 95 | return; 96 | } 97 | assert.deepEqual(actual, ['foo', 'bar']); 98 | cb(); 99 | }); 100 | }); 101 | 102 | it('should not throw an error when `build` is called without a callback function.', cb => { 103 | const actual = []; 104 | 105 | app.task('foo', next => { 106 | actual.push('foo'); 107 | next(); 108 | }); 109 | 110 | const build = app.series('foo', next => { 111 | actual.push('bar'); 112 | next(); 113 | }); 114 | 115 | build(); 116 | 117 | setTimeout(() => { 118 | assert.deepEqual(actual, ['foo', 'bar']); 119 | cb(); 120 | }, 10); 121 | }); 122 | 123 | it('should handle errors', () => { 124 | const actual = []; 125 | let count = 0; 126 | 127 | app.on('error', err => { 128 | actual.push(err.message); 129 | count++; 130 | }); 131 | 132 | app.task('foo', cb => { 133 | actual.push('foo'); 134 | count++; 135 | cb(); 136 | }); 137 | 138 | const build = app.series('foo', cb => { 139 | count++; 140 | cb(new Error('an error')); 141 | }); 142 | 143 | return build() 144 | .then(() => { 145 | throw new Error('actual an error'); 146 | }) 147 | .catch(() => { 148 | assert.equal(count, 3); 149 | assert.deepEqual(actual, ['foo', 'an error']); 150 | }); 151 | }); 152 | }); 153 | 154 | describe('promise', () => { 155 | it('should compose tasks into a function that runs in series', () => { 156 | const actual = []; 157 | 158 | app.task('foo', cb => { 159 | actual.push('foo'); 160 | cb(); 161 | }); 162 | 163 | const build = app.series('foo', function bar(cb) { 164 | actual.push('bar'); 165 | cb(); 166 | }); 167 | 168 | return build() 169 | .then(() => { 170 | assert.deepEqual(actual, ['foo', 'bar']); 171 | }); 172 | }); 173 | 174 | it('should compose tasks with options into a function that runs in series', () => { 175 | const actual = []; 176 | 177 | app.task('foo', { silent: false }, function(next) { 178 | assert.equal(this.options.silent, false); 179 | actual.push('foo'); 180 | next(); 181 | }); 182 | 183 | const build = app.series('foo', function(next) { 184 | actual.push('bar'); 185 | next(); 186 | }); 187 | 188 | return build() 189 | .then(() => { 190 | assert.deepEqual(actual, ['foo', 'bar']); 191 | }); 192 | }); 193 | 194 | it('should return an error when no functions are passed to series', cb => { 195 | const build = app.series(); 196 | 197 | return build(err => { 198 | assert(err); 199 | cb(); 200 | }); 201 | }); 202 | 203 | it('should compose tasks with additional options into a function that runs in series', () => { 204 | const actual = []; 205 | 206 | app.task('foo', { silent: false }, function(next) { 207 | assert.equal(this.options.silent, true); 208 | assert.equal(this.options.foo, 'bar'); 209 | actual.push('foo'); 210 | next(); 211 | }); 212 | 213 | const options = { silent: true, foo: 'bar' }; 214 | const build = app.series('foo', function(next) { 215 | actual.push('bar'); 216 | next(); 217 | }, options); 218 | 219 | return build() 220 | .then(() => { 221 | assert.deepEqual(actual, ['foo', 'bar']); 222 | }); 223 | }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /test/task.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const through = require('through2'); 6 | const { Task } = require('..'); 7 | 8 | describe('task', () => { 9 | it('should throw an error when Task is not instantiated', () => { 10 | assert.throws(() => Task(), /cannot be invoked without 'new'/); 11 | }); 12 | 13 | it('should throw an error when nothing is passed to new Task', () => { 14 | assert.throws(() => new Task(), /expected/i); 15 | }); 16 | 17 | it('should throw an error when `name` is not passed on `task`.', () => { 18 | assert.throws(() => new Task({}), /expected/i); 19 | }); 20 | 21 | it('should create a new task with a given `name`', () => { 22 | const task = new Task({ name: 'default' }); 23 | assert.equal(task.name, 'default'); 24 | }); 25 | 26 | it('should create a new task with a given task function', () => { 27 | const callback = cb => cb(); 28 | const task = new Task({ name: 'default', callback }); 29 | assert.equal(task.callback, callback); 30 | }); 31 | 32 | it('should create a new task with the given dependencies', () => { 33 | const task = new Task({ name: 'default', deps: ['foo', 'bar'] }); 34 | assert.deepEqual(task.deps, ['foo', 'bar']); 35 | }); 36 | 37 | it('should create a new task with deps from the `options` property', () => { 38 | const task = new Task({ name: 'default', options: { deps: ['foo', 'bar'] } }); 39 | assert.deepEqual(task.deps, ['foo', 'bar']); 40 | }); 41 | 42 | it('should run a task function when `.run` is called', () => { 43 | let count = 0; 44 | const callback = cb => { 45 | count++; 46 | cb(); 47 | }; 48 | 49 | const task = new Task({ name: 'default', callback }); 50 | const run = task.run(); 51 | return run() 52 | .then(() => { 53 | assert.equal(count, 1); 54 | }); 55 | }); 56 | 57 | it('should run a task function that returns a promise when `.run` is called', () => { 58 | let count = 0; 59 | const callback = function() { 60 | return new Promise(function(resolve) { 61 | setImmediate(() => { 62 | count++; 63 | resolve(); 64 | }); 65 | }); 66 | }; 67 | 68 | const task = new Task({ name: 'default', callback }); 69 | const run = task.run(); 70 | return run() 71 | .then(() => { 72 | assert.equal(count, 1); 73 | }); 74 | }); 75 | 76 | it('should skip a task function when `.options.run === false`', () => { 77 | let count = 0; 78 | const callback = cb => { 79 | count++; 80 | cb(); 81 | }; 82 | 83 | const task = new Task({ name: 'default', callback, options: { run: false } }); 84 | const run = task.run(); 85 | 86 | return run() 87 | .then(() => { 88 | assert.equal(count, 0); 89 | }); 90 | }); 91 | it('should skip a task function when `.options.skip` is the task name', () => { 92 | let count = 0; 93 | const callback = cb => { 94 | count++; 95 | cb(); 96 | }; 97 | 98 | const task = new Task({ 99 | name: 'foo', 100 | callback, 101 | options: { skip: 'foo' } 102 | }); 103 | 104 | const run = task.run(); 105 | return run().then(() => { 106 | assert.equal(count, 0); 107 | }); 108 | }); 109 | 110 | it('should skip a task function when `.options.skip` is an array with the task name', () => { 111 | let count = 0; 112 | const callback = cb => { 113 | count++; 114 | cb(); 115 | }; 116 | 117 | const task = new Task({ 118 | name: 'foo', 119 | callback, 120 | options: { skip: ['bar', 'baz', 'foo'] } 121 | }); 122 | 123 | const run = task.run(); 124 | return run().then(() => { 125 | assert.equal(count, 0); 126 | }); 127 | }); 128 | 129 | it('should not skip a task function when `.options.skip` is an array without the task name', () => { 130 | let count = 0; 131 | const callback = cb => { 132 | count++; 133 | cb(); 134 | }; 135 | 136 | const task = new Task({name: 'foo', callback, options: { skip: ['bar', 'baz'] } }); 137 | const run = task.run(); 138 | 139 | return run() 140 | .then(() => { 141 | assert.equal(count, 1); 142 | }); 143 | }); 144 | 145 | it('should run a task function that returns a stream when `.run` is called', () => { 146 | let count = 0; 147 | 148 | const callback = function() { 149 | const stream = through.obj(function(data, enc, next) { 150 | count++; 151 | next(null); 152 | }); 153 | setImmediate(() => { 154 | stream.write(count); 155 | stream.end(); 156 | }); 157 | return stream; 158 | }; 159 | 160 | const task = new Task({ name: 'default', callback }); 161 | const run = task.run(); 162 | 163 | return run() 164 | .then(() => { 165 | assert.equal(count, 1); 166 | }); 167 | }); 168 | 169 | it('should run a task that returns a non-stream when `.run` is called', () => { 170 | let count = 0; 171 | const callback = cb => { 172 | setImmediate(() => { 173 | count++; 174 | cb(); 175 | }); 176 | return count; 177 | }; 178 | 179 | const task = new Task({ name: 'default', callback }); 180 | const run = task.run(); 181 | 182 | return run() 183 | .then(() => { 184 | assert.equal(count, 1); 185 | }); 186 | }); 187 | 188 | it('should emit a `starting` event when the task starts running', () => { 189 | let count = 0; 190 | const callback = cb => { 191 | count++; 192 | cb(); 193 | }; 194 | 195 | const task = new Task({ name: 'default', callback }); 196 | task.on('starting', () => { 197 | count++; 198 | }); 199 | 200 | const run = task.run(); 201 | return run() 202 | .then(() => { 203 | assert.equal(count, 2); 204 | }); 205 | }); 206 | 207 | it('should emit a `finished` event when the task finishes running', () => { 208 | let count = 0; 209 | const callback = cb => { 210 | count++; 211 | cb(); 212 | }; 213 | const task = new Task({ name: 'default', callback }); 214 | task.on('finished', () => { 215 | count++; 216 | }); 217 | const run = task.run(); 218 | return run() 219 | .then(() => { 220 | assert.equal(count, 2); 221 | }); 222 | }); 223 | 224 | it('should emit an `error` event when there is an error during task execution', () => { 225 | let count = 0; 226 | 227 | const callback = cb => cb(new Error('expected an error')); 228 | const task = new Task({ name: 'default', callback }); 229 | 230 | task.on('error', () => { 231 | count++; 232 | }); 233 | 234 | const run = task.run(); 235 | return run() 236 | .catch(err => { 237 | assert.equal(count, 1); 238 | assert.equal(err.message, 'expected an error'); 239 | }); 240 | }); 241 | 242 | it('should have the current task set as `this` inside the function', () => { 243 | const results = []; 244 | const tasks = []; 245 | 246 | const callback = function(next) { 247 | results.push(this.name); 248 | next(); 249 | }; 250 | 251 | for (let i = 0; i < 10; i++) { 252 | tasks.push(new Task({ name: 'task-' + i, callback })); 253 | } 254 | 255 | const series = async() => { 256 | for (const task of tasks) { 257 | await task.run()(); 258 | } 259 | }; 260 | 261 | return series().then(() => { 262 | assert.equal(results.length, 10); 263 | assert.deepEqual(results, [ 264 | 'task-0', 265 | 'task-1', 266 | 'task-2', 267 | 'task-3', 268 | 'task-4', 269 | 'task-5', 270 | 'task-6', 271 | 'task-7', 272 | 'task-8', 273 | 'task-9' 274 | ]); 275 | }); 276 | }); 277 | }); 278 | -------------------------------------------------------------------------------- /test/tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const through = require('through2'); 6 | const { Tasks } = require('..'); 7 | let app; 8 | 9 | describe('tasks', () => { 10 | beforeEach(() => { 11 | app = new Tasks(); 12 | }); 13 | 14 | it('should run a task using a noop when `.run` is called', () => { 15 | const events = []; 16 | const push = task => events.push(task.name + ':' + task.status); 17 | 18 | app.on('task', push); 19 | app.on('task-registered', push); 20 | app.on('task-preparing', push); 21 | 22 | app.task('foo'); 23 | app.task('default', ['foo']); 24 | 25 | return app.build('default').then(() => { 26 | assert.deepEqual(events, [ 27 | 'foo:registered', 28 | 'default:registered', 29 | 'default:preparing', 30 | 'default:starting', 31 | 'foo:preparing', 32 | 'foo:starting', 33 | 'foo:finished', 34 | 'default:finished' 35 | ]); 36 | }); 37 | }); 38 | 39 | it('should cause an error if invalid deps are resolved `.run` is called', () => { 40 | const tasks = []; 41 | app = new Tasks(); 42 | app.on('task', task => tasks.push(`${task.name}:${task.status}`)); 43 | 44 | app.task('foo'); 45 | app.task('default', { deps: ['foo', { foo: 'bar' }, { bang: 'baz' }] }); 46 | 47 | return app.build('default') 48 | .then(() => { 49 | throw new Error('exected an error'); 50 | }) 51 | .catch(err => { 52 | assert(/expected/.test(err.message)); 53 | }); 54 | }); 55 | 56 | it('should signal that a task is complete when a stream is returned', () => { 57 | app = new Tasks(); 58 | const events = []; 59 | let count = 0; 60 | 61 | app.on('task-registered', task => events.push(task.status)); 62 | app.on('task-preparing', task => events.push(task.status)); 63 | app.on('task', task => events.push(task.status)); 64 | app.task('default', () => { 65 | const stream = through.obj(function(data, enc, next) { 66 | count++; 67 | next(); 68 | }); 69 | stream.write(count); 70 | stream.end(); 71 | return stream; 72 | }); 73 | 74 | return app.build('default') 75 | .then(() => { 76 | assert.deepEqual(events, [ 'registered', 'preparing', 'starting', 'finished' ]); 77 | assert.equal(count, 1); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert'); 5 | const util = require('util'); 6 | const Composer = require('..'); 7 | let app; 8 | 9 | describe('composer', () => { 10 | beforeEach(() => { 11 | app = new Composer(); 12 | }); 13 | 14 | it('should throw an error when a name is not given for a task', () => { 15 | assert.throws(() => app.task(), /expected/); 16 | }); 17 | 18 | it('should expose static methods', () => { 19 | assert(Composer.create); 20 | assert(Composer.isGenerator); 21 | }); 22 | 23 | it('should register a task', () => { 24 | app.task('default', () => {}); 25 | assert(app.tasks.get('default')); 26 | assert.equal(typeof app.tasks.get('default'), 'object'); 27 | assert.equal(app.tasks.get('default').callback.name, ''); 28 | }); 29 | 30 | it('should register a noop task when only name is given', () => { 31 | app.task('default'); 32 | assert(app.tasks.get('default')); 33 | assert.equal(typeof app.tasks.get('default'), 'object'); 34 | assert.equal(typeof app.tasks.get('default').callback, 'function'); 35 | }); 36 | 37 | it('should register a noop task when a name and an empty dependencies array is given', () => { 38 | app.task('default', []); 39 | assert(app.tasks.get('default')); 40 | assert.equal(typeof app.tasks.get('default'), 'object'); 41 | assert.equal(typeof app.tasks.get('default').callback, 'function'); 42 | }); 43 | 44 | it('should register a task with an array of named dependencies', () => { 45 | app.task('default', ['foo', 'bar'], cb => cb()); 46 | assert(app.tasks.get('default')); 47 | assert.equal(typeof app.tasks.get('default'), 'object'); 48 | assert.deepEqual(app.tasks.get('default').deps, ['foo', 'bar']); 49 | }); 50 | 51 | it('should register a task with a list of strings as dependencies', () => { 52 | app.task('default', 'foo', 'bar', cb => cb()); 53 | assert.equal(typeof app.tasks.get('default'), 'object'); 54 | assert.deepEqual(app.tasks.get('default').deps, ['foo', 'bar']); 55 | }); 56 | 57 | it('should register a task as a noop function when only dependencies are given', () => { 58 | app.task('default', ['foo', 'bar']); 59 | assert.equal(typeof app.tasks.get('default'), 'object'); 60 | assert.equal(typeof app.tasks.get('default').callback, 'function'); 61 | }); 62 | 63 | it('should register a task with options as the second argument', () => { 64 | app.task('default', { one: 'two' }, ['foo', 'bar']); 65 | assert.equal(typeof app.tasks.get('default'), 'object'); 66 | assert.equal(typeof app.tasks.get('default').callback, 'function'); 67 | assert.equal(app.tasks.get('default').options.one, 'two'); 68 | }); 69 | 70 | it('should register a task as a prompt task', () => { 71 | app.task('default', 'Run task?', 'foo'); 72 | assert.equal(typeof app.tasks.get('default'), 'object'); 73 | }); 74 | 75 | it('should run a task', cb => { 76 | let count = 0; 77 | app.task('default', cb => { 78 | count++; 79 | cb(); 80 | }); 81 | 82 | app.build('default', err => { 83 | if (err) return cb(err); 84 | assert.equal(count, 1); 85 | cb(); 86 | }); 87 | }); 88 | 89 | it('should run a task and return a promise', () => { 90 | let count = 0; 91 | app.task('default', cb => { 92 | count++; 93 | cb(); 94 | }); 95 | 96 | return app.build('default').then(() => { 97 | assert.equal(count, 1); 98 | }); 99 | }); 100 | 101 | it('should run a task with options', cb => { 102 | let count = 0; 103 | app.task('default', { silent: false }, function(cb) { 104 | assert.equal(this.options.silent, false); 105 | count++; 106 | cb(); 107 | }); 108 | 109 | app.build('default', err => { 110 | if (err) return cb(err); 111 | assert.equal(count, 1); 112 | cb(); 113 | }); 114 | }); 115 | 116 | it('should run a task with additional options', cb => { 117 | let count = 0; 118 | app.task('default', { silent: false }, function(cb) { 119 | assert.equal(this.options.silent, true); 120 | assert.equal(this.options.foo, 'bar'); 121 | count++; 122 | cb(); 123 | }); 124 | 125 | app.build('default', { silent: true, foo: 'bar' }, err => { 126 | if (err) return cb(err); 127 | assert.equal(count, 1); 128 | cb(); 129 | }); 130 | }); 131 | 132 | it('should run the `default` task when no task is given', cb => { 133 | let count = 0; 134 | app.task('default', cb => { 135 | count++; 136 | cb(); 137 | }); 138 | 139 | app.build(err => { 140 | if (err) return cb(err); 141 | assert.equal(count, 1); 142 | cb(); 143 | }); 144 | }); 145 | 146 | it('should skip tasks when `run === false`', cb => { 147 | const expected = []; 148 | function callback() { 149 | return function(cb) { 150 | expected.push(this.name); 151 | cb(); 152 | }; 153 | } 154 | 155 | app.task('foo', callback()); 156 | app.task('bar', { run: false }, callback()); 157 | app.task('baz', callback()); 158 | app.task('bang', { run: false }, callback()); 159 | app.task('beep', callback()); 160 | app.task('boop', callback()); 161 | 162 | app.task('default', ['foo', 'bar', 'baz', 'bang', 'beep', 'boop']); 163 | app.build(err => { 164 | if (err) return cb(err); 165 | assert.deepEqual(expected, ['foo', 'baz', 'beep', 'boop']); 166 | cb(); 167 | }); 168 | }); 169 | 170 | it('should skip tasks when `run === false` (with deps skipped)', cb => { 171 | const expected = []; 172 | function callback() { 173 | return function(cb) { 174 | expected.push(this.name); 175 | cb(); 176 | }; 177 | } 178 | 179 | app.task('foo', callback()); 180 | app.task('bar', { run: false }, ['foo'], callback()); 181 | app.task('baz', ['bar'], callback()); 182 | app.task('bang', { run: false }, ['baz'], callback()); 183 | app.task('beep', ['bang'], callback()); 184 | app.task('boop', ['beep'], callback()); 185 | 186 | app.task('default', ['boop']); 187 | app.build(err => { 188 | if (err) return cb(err); 189 | assert.deepEqual(expected, ['beep', 'boop']); 190 | cb(); 191 | }); 192 | }); 193 | 194 | it('should skip tasks when `run === false` (complex flow)', cb => { 195 | const expected = []; 196 | 197 | app.task('foo', function(next) { 198 | expected.push(this.name); 199 | // disable running the "bar" task 200 | app.tasks.get('bar').options.run = false; 201 | next(); 202 | }); 203 | 204 | app.task('bar', function(next) { 205 | expected.push(this.name); 206 | next(); 207 | }); 208 | 209 | app.task('baz', function(next) { 210 | expected.push(this.name); 211 | // enable running the "bang" task 212 | app.tasks.get('bang').options.run = true; 213 | next(); 214 | }); 215 | 216 | app.task('bang', { run: false }, function(next) { 217 | expected.push(this.name); 218 | next(); 219 | }); 220 | 221 | app.task('beep', function(next) { 222 | expected.push(this.name); 223 | next(); 224 | }); 225 | 226 | app.task('boop', function(next) { 227 | expected.push(this.name); 228 | next(); 229 | }); 230 | 231 | app.task('default', ['foo', 'bar', 'baz', 'bang', 'beep', 'boop']); 232 | app.build(err => { 233 | if (err) return cb(err); 234 | assert.deepEqual(expected, ['foo', 'baz', 'bang', 'beep', 'boop']); 235 | cb(); 236 | }); 237 | }); 238 | 239 | it('should throw an error when a task with unregistered dependencies is run', cb => { 240 | let count = 0; 241 | app.task('default', ['foo', 'bar'], cb => { 242 | count++; 243 | cb(); 244 | }); 245 | 246 | app.build('default', err => { 247 | if (!err) return cb(new Error('Expected an error to be thrown.')); 248 | assert.equal(count, 0); 249 | cb(); 250 | }); 251 | }); 252 | 253 | it('should throw an error when a task with globbed dependencies cannot be found', cb => { 254 | let count = 0; 255 | app.task('default', ['a-*'], cb => { 256 | count++; 257 | cb(); 258 | }); 259 | 260 | app.build('default', err => { 261 | if (!err) return cb(new Error('Expected an error to be thrown.')); 262 | assert.equal(count, 0); 263 | cb(); 264 | }); 265 | }); 266 | 267 | it('should emit task events', cb => { 268 | const events = []; 269 | app.on('task', function(task) { 270 | events.push(task.status + '.' + task.name); 271 | }); 272 | 273 | app.on('error', err => { 274 | if (err.build) { 275 | return; 276 | } 277 | events.push('error.' + err.task.name); 278 | }); 279 | 280 | app.task('foo', cb => cb()); 281 | 282 | app.task('bar', ['foo'], cb => cb()); 283 | 284 | app.task('default', ['bar']); 285 | app.build('default', err => { 286 | if (err) return cb(err); 287 | assert.deepEqual(events, [ 288 | 'starting.default', 289 | 'starting.bar', 290 | 'starting.foo', 291 | 'finished.foo', 292 | 'finished.bar', 293 | 'finished.default' 294 | ]); 295 | cb(); 296 | }); 297 | }); 298 | 299 | it('-should emit an error event when an error is passed back in a task', cb => { 300 | const errors = []; 301 | app.on('error', err => { 302 | errors.push(err); 303 | }); 304 | 305 | app.task('default', next => { 306 | next(new Error('This is an error')); 307 | }); 308 | 309 | app.build('default', err => { 310 | assert(err); 311 | assert.equal(errors.length, 1); 312 | cb(); 313 | }); 314 | }); 315 | 316 | it('should emit build events', () => { 317 | const events = []; 318 | 319 | app.on('build', function(build) { 320 | events.push(build.status); 321 | }); 322 | 323 | app.on('error', () => { 324 | events.push('error'); 325 | }); 326 | 327 | app.task('foo', cb => cb()); 328 | app.task('bar', ['foo'], cb => cb()); 329 | app.task('default', ['bar']); 330 | 331 | return app.build('default').then(() => { 332 | assert.deepEqual(events, ['starting', 'finished']); 333 | }); 334 | }); 335 | 336 | it('should emit a build error event when an error is passed back in a task', () => { 337 | let count = 0; 338 | 339 | app.on('error', () => { 340 | count++; 341 | }); 342 | 343 | app.task('default', cb => { 344 | cb(new Error('This is an error')); 345 | }); 346 | 347 | return app 348 | .build('default') 349 | .then(() => { 350 | throw new Error('exected an error'); 351 | }) 352 | .catch(() => { 353 | assert.equal(count, 1); 354 | }); 355 | }); 356 | 357 | it('should stop build and return errors when thrown in a task', () => { 358 | let count = 0; 359 | 360 | app.task('foo', () => { 361 | throw new Error('This is an error'); 362 | }); 363 | 364 | app.task('bar', () => { 365 | count++; 366 | }); 367 | 368 | return app.build(['foo', 'bar']) 369 | .catch(err => { 370 | assert(err); 371 | assert.equal(count, 0); 372 | }); 373 | }); 374 | 375 | it('should emit an error event when an error is thrown in a task', () => { 376 | let count = 0; 377 | 378 | app.on('error', () => { 379 | count++; 380 | }); 381 | 382 | app.task('default', () => { 383 | throw new Error('This is an error'); 384 | }); 385 | 386 | return app 387 | .build('default') 388 | .then(() => { 389 | throw new Error('exected an error'); 390 | }) 391 | .catch(() => { 392 | assert.equal(count, 1); 393 | }); 394 | }); 395 | 396 | it('should run dependencies before running the dependent task.', () => { 397 | const events = []; 398 | 399 | app.task('foo', cb => { 400 | events.push('foo'); 401 | cb(); 402 | }); 403 | 404 | app.task('bar', cb => { 405 | events.push('bar'); 406 | cb(); 407 | }); 408 | 409 | app.task('default', ['foo', 'bar'], cb => { 410 | events.push('default'); 411 | cb(); 412 | }); 413 | 414 | return app.build('default').then(() => { 415 | assert.deepEqual(events, ['foo', 'bar', 'default']); 416 | }); 417 | }); 418 | 419 | it('should add inspect function to tasks.', () => { 420 | app.task('foo', cb => cb()); 421 | app.task('bar', cb => cb()); 422 | app.task('default', ['foo', 'bar'], cb => cb()); 423 | 424 | assert.equal(app.tasks.get('foo')[util.inspect.custom](), ''); 425 | assert.equal(app.tasks.get('bar')[util.inspect.custom](), ''); 426 | assert.equal(app.tasks.get('default')[util.inspect.custom](), ''); 427 | }); 428 | 429 | it('should disable inspect function on tasks.', () => { 430 | app.options = { inspectFn: false }; 431 | 432 | app.task('foo', cb => cb()); 433 | app.task('bar', cb => cb()); 434 | app.task('default', ['foo', 'bar'], cb => cb()); 435 | 436 | assert.equal(typeof app.tasks.get('foo').inspect, 'undefined'); 437 | assert.equal(typeof app.tasks.get('bar').inspect, 'undefined'); 438 | assert.equal(typeof app.tasks.get('default').inspect, 'undefined'); 439 | }); 440 | 441 | it('should run globbed dependencies before running the dependent task.', () => { 442 | const events = []; 443 | const task = function(cb) { 444 | events.push(this.name); 445 | cb(); 446 | }; 447 | 448 | app.task('foo', task); 449 | app.task('bar', task); 450 | app.task('baz', task); 451 | app.task('qux', task); 452 | app.task('default', ['b*'], task); 453 | 454 | return app.build('default').then(() => { 455 | assert.deepEqual(events, ['bar', 'baz', 'default']); 456 | }); 457 | }); 458 | 459 | it('should get the current task name from `this`', () => { 460 | const names = []; 461 | const tasks = []; 462 | 463 | const callback = function(cb) { 464 | names.push(this.name); 465 | cb(); 466 | }; 467 | 468 | for (let i = 0; i < 10; i++) { 469 | tasks.push(String(i)); 470 | app.task(String(i), callback); 471 | } 472 | 473 | return app.build(tasks).then(() => { 474 | assert.deepEqual(names, ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); 475 | }); 476 | }); 477 | }); 478 | --------------------------------------------------------------------------------