├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── lib ├── cha.js └── promise.js ├── package.json └── test ├── build.js ├── expr.js └── fixtures ├── coffee ├── bar.coffee └── foo.coffee └── js ├── bar.js └── foo.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [package.json] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | # Numerous always-ignore extensions 5 | *.diff 6 | *.err 7 | *.orig 8 | *.log 9 | *.rej 10 | *.swo 11 | *.swp 12 | *.vi 13 | *~ 14 | *.sass-cache 15 | 16 | # OS or Editor folders 17 | .DS_Store 18 | ._* 19 | Thumbs.db 20 | .cache 21 | .project 22 | .settings 23 | .tmproj 24 | nbproject 25 | *.sublime-project 26 | *.sublime-workspace 27 | 28 | # Dreamweaver added files 29 | _notes 30 | dwsync.xml 31 | 32 | # Komodo 33 | *.komodoproject 34 | .komodotools 35 | 36 | # Espresso 37 | *.esproj 38 | *.espressostorage 39 | 40 | # Rubinius 41 | *.rbc 42 | 43 | # Folders to ignore 44 | .hg 45 | .svn 46 | .CVS 47 | intermediate 48 | publish 49 | .idea 50 | node_modules 51 | dist 52 | out 53 | test/out 54 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | doc/ 2 | test/ 3 | example/ 4 | node_modules/ 5 | .* 6 | *~ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | - 0.11 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | cha [![NPM version](https://badge.fury.io/js/cha.png)](http://npm.org/cha) [![Build Status](https://travis-ci.org/chajs/cha.png?branch=master)](http://travis-ci.org/chajs/cha) 6 | === 7 | > Make task chaining. 8 | 9 | Cha allows tasks to be connected together into a chain that makes better readability and easier to maintain. 10 | 11 | ## Getting Started 12 | 13 | Installing cha via NPM, this will install the latest version of cha in your project folder 14 | and adding it to your `devDependencies` in `package.json`: 15 | ```sh 16 | npm install cha --save-dev 17 | ``` 18 | 19 | Touch a tasks file and naming whatever you love like `build.js`: 20 | ```js 21 | // Load cha library. 22 | var cha = require('cha') 23 | 24 | // Register tasks that should chaining. 25 | cha.in('glob', require('task-glob')) 26 | .in('combine', require('task-combine')) 27 | .in('replace', require('task-replace')) 28 | .in('writer', require('task-writer')) 29 | .in('uglifyjs',require('task-uglifyjs')) 30 | .in('copy', require('task-copy')) 31 | .in('request', require('task-request')) 32 | 33 | // Define task via chaining calls. 34 | cha() 35 | .glob({ 36 | patterns: './fixtures/js/*.js', 37 | cwd: __dirname 38 | }) 39 | .replace({ 40 | search: 'TIMESTAMP', 41 | replace: +new Date 42 | }) 43 | .replace({ 44 | search: /DEBUG/g, 45 | replace: true 46 | }) 47 | .request('http://underscorejs.org/underscore-min.js') 48 | .combine() 49 | .uglifyjs() 50 | .writer('./out/foobar.js') 51 | .copy('./out/foobar2.js') 52 | ``` 53 | 54 | Add a arbitrary command to the `scripts` object: 55 | ```json 56 | { 57 | "name": "cha-example", 58 | "scripts": { 59 | "build": "node ./build.js" 60 | }, 61 | "devDependencies": { 62 | "cha": "~0.1.0" 63 | } 64 | } 65 | ``` 66 | 67 | To run the command we prepend our script name with run: 68 | ```sh 69 | $ npm run build 70 | 71 | > cha@0.0.1 build 72 | > node ./test/build 73 | 74 | request http://underscorejs.org/underscore-min.js 75 | concat ./test/fixtures/bar.js,./test/fixtures/foo.js,http://underscorejs.org/underscore-min.js 76 | write ./out/foobar.js 77 | copy out/foobar.js > ./out/foobar2.js 78 | ``` 79 | 80 | ## Cha Extensions 81 | 82 | * [cha-load](https://github.com/chajs/cha-load) - Automatically load cha and register tasks. 83 | * [cha-watch](https://github.com/chajs/cha-watch) - File watcher. 84 | * [cha-target](https://github.com/chajs/cha-target) - Target runner. 85 | * [cha-gulp](https://github.com/chajs/cha-gulp) - Gulp plugin adapter. 86 | 87 | ## Cha Expressions 88 | 89 | ```js 90 | // Load cha library. 91 | var cha = require('cha') 92 | 93 | // Register tasks that should chaining. 94 | cha.in('glob', require('task-glob')) 95 | .in('combine', require('task-combine')) 96 | .in('replace', require('task-replace')) 97 | .in('writer', require('task-writer')) 98 | .in('uglifyjs',require('task-uglifyjs')) 99 | .in('request', require('task-request')) 100 | 101 | // Start with cha expressions. 102 | cha(['glob:./fixtures/js/*.js', 'request:http://underscorejs.org/underscore-min.js']) 103 | .replace({ 104 | search: 'TIMESTAMP', 105 | replace: +new Date 106 | }) 107 | .replace({ 108 | search: /DEBUG/g, 109 | replace: true 110 | }) 111 | .combine() 112 | .uglifyjs() 113 | .writer('./test/out/foobar.js') 114 | ``` 115 | 116 | To run the command we prepend our script name with run: 117 | ```sh 118 | $ npm run expr 119 | 120 | > cha@0.0.1 expr 121 | > node ./test/expr 122 | 123 | request http://underscorejs.org/underscore-min.js 124 | concat http://underscorejs.org/underscore-min.js 125 | write ./out/foobar.js 126 | ``` 127 | 128 | ## Task Settings 129 | 130 | ```js 131 | // Load cha library. 132 | var cha = require('cha') 133 | 134 | // Register tasks that should chaining. 135 | cha.in('glob', require('task-glob')) 136 | .in('writer', require('task-writer')) 137 | .in('uglifyjs',require('task-uglifyjs')) 138 | .in('request', require('task-request')) 139 | 140 | // Start with cha expressions. 141 | cha() 142 | .glob({ 143 | patterns: './fixtures/js/*.js' 144 | }) 145 | .request({ 146 | url: 'http://underscorejs.org/underscore.js' 147 | }, { 148 | ignore: true, // Ignore task inputs. 149 | timeout: 2000 // 2000ms timeout. 150 | }) 151 | .uglifyjs() 152 | .writer('./underscore-min.js') 153 | ``` 154 | 155 | ## JavaScript Tasks 156 | 157 | All register task should based on the [JavaScript Task](https://github.com/taskjs/spec) specification. 158 | You could get available tasks from [JavaScript Task Packages] (http://taskjs.github.io/packages/) website. 159 | 160 | ## Release History 161 | 162 | * 2014-05-19 0.2.1 Task accept `settings` param with general options. 163 | * 2014-05-18 0.2.0 Remove Internal methods. 164 | * 2014-03-17 0.1.2 Extensions for cha. 165 | * 2014-03-10 0.1.1 Custom tasks could override internal methods. 166 | * 2014-03-05 0.1.0 Initial release. 167 | -------------------------------------------------------------------------------- /lib/cha.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var path = require('path'); 3 | var Promise = require('./promise'); 4 | 5 | module.exports = cha; 6 | 7 | function cha(exprs) { 8 | var promises = []; 9 | 10 | if (exprs != null) { 11 | exprs = [].concat(exprs); 12 | exprs = _.map(exprs, function (expr) { 13 | var rule = new RegExp("\\s*(\\w+)\\s*:\\s*(.*)"); 14 | var match = rule.exec(expr); 15 | 16 | if (match) { 17 | return { 18 | task: match[1], 19 | options: match[2] 20 | } 21 | } else { 22 | throw new SyntaxError("Unrecognized expression: " + expr); 23 | } 24 | }); 25 | 26 | promises = _.map(exprs, function (expr) { 27 | var task = cha.task[expr.task]; 28 | if(!task) throw new Error("Unregistered task: " + expr.task); 29 | return cha.run(task, null, expr.options); 30 | }) 31 | } 32 | 33 | // Chaining tasks. 34 | chaining(Promise, cha.task); 35 | // Flatten results. 36 | return Promise.all(promises).then(function (results) { 37 | return _.flatten(results); 38 | }) 39 | } 40 | 41 | function chaining(constructor, fns, logger) { 42 | // Make task chaining. 43 | var fn = constructor.prototype; 44 | // Custom tasks. 45 | _.each(fns, function (task, name) { 46 | fn[name] = function (options, settings) { 47 | return this.then(function (records) { 48 | return new constructor(function (resolve) { 49 | var thenable = cha.run(task, records, options, logger, settings); 50 | resolve(thenable); 51 | }); 52 | }); 53 | } 54 | }); 55 | } 56 | 57 | /** 58 | * Plugins register. 59 | */ 60 | cha.in = function (name, task) { 61 | if (_.isObject(task) || _.isFunction(task)) { 62 | cha.task[name] = task; 63 | return this; 64 | } else { 65 | throw new Error("Unrecognized task: " + task); 66 | } 67 | }; 68 | 69 | /** 70 | * Task collection. 71 | */ 72 | cha.task = {}; 73 | 74 | /** 75 | * Logging object. 76 | */ 77 | cha.logger = console; 78 | 79 | /** 80 | * Run task 81 | */ 82 | cha.run = function (task, records, options, logger, settings) { 83 | var run; 84 | logger = logger || cha.logger; 85 | 86 | if (_.isFunction(task) && task.prototype.run) { 87 | var t = new task; 88 | run = t.run.bind(t); 89 | } else if (_.isObject(task) && task.run) { 90 | run = task.run.bind(task); 91 | } else if (_.isFunction(task)) { 92 | run = task; 93 | } else { 94 | throw new TypeError("Unrecognized task"); 95 | } 96 | 97 | var thenable = run(records, options, logger, settings); 98 | 99 | if (!_.isFunction(thenable.then)) { 100 | throw new TypeError("Must return a thenable"); 101 | } 102 | 103 | return thenable; 104 | }; 105 | -------------------------------------------------------------------------------- /lib/promise.js: -------------------------------------------------------------------------------- 1 | // Based on Promises/A+ implementation 2 | 3 | module.exports = Promise 4 | 5 | function Handler(onFulfilled, onRejected, resolve, reject){ 6 | this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null 7 | this.onRejected = typeof onRejected === 'function' ? onRejected : null 8 | this.resolve = resolve 9 | this.reject = reject 10 | } 11 | 12 | function Promise(resolver) { 13 | if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new') 14 | if (typeof resolver !== 'function') throw new TypeError('not a function') 15 | var state = null 16 | var value = null 17 | var deferreds = [] 18 | var self = this 19 | 20 | function handle(deferred) { 21 | if (state === null) { 22 | deferreds.push(deferred) 23 | return 24 | } 25 | 26 | var cb = state ? deferred.onFulfilled : deferred.onRejected 27 | if (cb === null) { 28 | (state ? deferred.resolve : deferred.reject)(value) 29 | return 30 | } 31 | var ret 32 | try { 33 | ret = cb(value) 34 | } 35 | catch (e) { 36 | deferred.reject(e) 37 | return 38 | } 39 | deferred.resolve(ret) 40 | 41 | } 42 | 43 | function resolve(newValue) { 44 | try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure 45 | if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.') 46 | if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { 47 | var then = newValue.then 48 | if (typeof then === 'function') { 49 | invokeResolver(then.bind(newValue), resolve, reject) 50 | return 51 | } 52 | } 53 | state = true 54 | value = newValue 55 | finale() 56 | } catch (e) { reject(e) } 57 | } 58 | 59 | function reject(newValue) { 60 | state = false 61 | value = newValue 62 | finale() 63 | } 64 | 65 | function finale() { 66 | for (var i = 0, len = deferreds.length; i < len; i++) 67 | handle(deferreds[i]) 68 | 69 | deferreds = null 70 | 71 | } 72 | 73 | this.then = function(onFulfilled, onRejected) { 74 | return new Promise(function(resolve, reject) { 75 | handle(new Handler(onFulfilled, onRejected, resolve, reject)) 76 | }) 77 | } 78 | 79 | invokeResolver(resolver, resolve, reject) 80 | 81 | /** 82 | * Take a potentially misbehaving resolver function and make sure 83 | * onFulfilled and onRejected are only called once. 84 | * Makes no guarantees about asynchrony. 85 | */ 86 | function invokeResolver(resolver, onFulfilled, onRejected) { 87 | var done = false; 88 | try { 89 | resolver(function (value) { 90 | if (done) return 91 | done = true 92 | onFulfilled(value) 93 | }, function (reason) { 94 | if (done) return 95 | done = true 96 | onRejected(reason) 97 | }) 98 | } catch (ex) { 99 | if (done) return 100 | done = true 101 | onRejected(ex) 102 | } 103 | } 104 | } 105 | 106 | Promise.prototype.catch = function (onRejected) { 107 | return this.then(null, onRejected); 108 | } 109 | 110 | function ValuePromise(value) { 111 | this.then = function (onFulfilled) { 112 | if (typeof onFulfilled !== 'function') return this 113 | return new Promise(function (resolve, reject) { 114 | asap(function () { 115 | try { 116 | resolve(onFulfilled(value)) 117 | } catch (ex) { 118 | reject(ex); 119 | } 120 | }) 121 | }) 122 | } 123 | } 124 | 125 | ValuePromise.prototype = Object.create(Promise.prototype) 126 | 127 | var TRUE = new ValuePromise(true) 128 | var FALSE = new ValuePromise(false) 129 | var NULL = new ValuePromise(null) 130 | var UNDEFINED = new ValuePromise(undefined) 131 | var ZERO = new ValuePromise(0) 132 | var EMPTYSTRING = new ValuePromise('') 133 | 134 | Promise.cast = function (value) { 135 | if (value instanceof Promise) return value 136 | 137 | if (value === null) return NULL 138 | if (value === undefined) return UNDEFINED 139 | if (value === true) return TRUE 140 | if (value === false) return FALSE 141 | if (value === 0) return ZERO 142 | if (value === '') return EMPTYSTRING 143 | 144 | if (typeof value === 'object' || typeof value === 'function') { 145 | try { 146 | var then = value.then 147 | if (typeof then === 'function') { 148 | return new Promise(then.bind(value)) 149 | } 150 | } catch (ex) { 151 | return new Promise(function (resolve, reject) { 152 | reject(ex) 153 | }) 154 | } 155 | } 156 | 157 | return new ValuePromise(value) 158 | } 159 | 160 | Promise.all = function () { 161 | var args = Array.prototype.slice.call(arguments.length === 1 && Array.isArray(arguments[0]) ? arguments[0] : arguments) 162 | 163 | return new Promise(function (resolve, reject) { 164 | if (args.length === 0) return resolve([]) 165 | var remaining = args.length 166 | function res(i, val) { 167 | try { 168 | if (val && (typeof val === 'object' || typeof val === 'function')) { 169 | var then = val.then 170 | if (typeof then === 'function') { 171 | then.call(val, function (val) { res(i, val) }, reject) 172 | return 173 | } 174 | } 175 | args[i] = val 176 | if (--remaining === 0) { 177 | resolve(args); 178 | } 179 | } catch (ex) { 180 | reject(ex) 181 | } 182 | } 183 | for (var i = 0; i < args.length; i++) { 184 | res(i, args[i]) 185 | } 186 | }) 187 | } 188 | 189 | Promise.resolve = function (value) { 190 | return new Promise(function (resolve) { 191 | resolve(value); 192 | }); 193 | } 194 | 195 | Promise.reject = function (value) { 196 | return new Promise(function (resolve, reject) { 197 | reject(value); 198 | }); 199 | } 200 | 201 | Promise.race = function (values) { 202 | return new Promise(function (resolve, reject) { 203 | values.map(function(value){ 204 | Promise.cast(value).then(resolve, reject); 205 | }) 206 | }); 207 | } 208 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cha", 3 | "version": "0.2.1", 4 | "description": "Make task chaining.", 5 | "main": "./lib/cha.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/chajs/cha.git" 9 | }, 10 | "scripts": { 11 | "build": "node ./test/build", 12 | "expr": "node ./test/expr", 13 | "test": "node ./test/build && node ./test/expr", 14 | "debug": "node --debug-brk ./test/build" 15 | }, 16 | "dependencies": { 17 | "lodash": "~2.4.1" 18 | }, 19 | "devDependencies": { 20 | "task-combine": "^0.1.0", 21 | "task-copy": "^0.1.1", 22 | "task-glob": "^0.1.0", 23 | "task-replace": "^0.1.0", 24 | "task-request": "^0.1.0", 25 | "task-uglifyjs": "^0.1.0", 26 | "task-writer": "^0.1.0" 27 | }, 28 | "author": "Yuanyan Cao", 29 | "license": "ISC" 30 | } 31 | -------------------------------------------------------------------------------- /test/build.js: -------------------------------------------------------------------------------- 1 | // Load cha library. 2 | var cha = require('../') 3 | 4 | // Register tasks that should chaining. 5 | cha.in('glob', require('task-glob')) 6 | .in('combine', require('task-combine')) 7 | .in('replace', require('task-replace')) 8 | .in('writer', require('task-writer')) 9 | .in('uglifyjs',require('task-uglifyjs')) 10 | .in('copy', require('task-copy')) 11 | .in('request', require('task-request')) 12 | 13 | // Define task via chaining calls. 14 | cha() 15 | .glob({ 16 | patterns: './fixtures/js/*.js', 17 | cwd: __dirname 18 | }) 19 | .replace({ 20 | search: 'TIMESTAMP', 21 | replace: +new Date 22 | }) 23 | .replace({ 24 | search: /DEBUG/g, 25 | replace: true 26 | }) 27 | .request('http://underscorejs.org/underscore-min.js') 28 | .combine() 29 | .uglifyjs() 30 | .writer('./test/out/foobar.js') 31 | .copy('./test/out/foobar2.js') 32 | .catch(function (err) { 33 | console.log(err.stack || err) 34 | }) 35 | -------------------------------------------------------------------------------- /test/expr.js: -------------------------------------------------------------------------------- 1 | // Load cha library. 2 | var cha = require('../') 3 | 4 | // Register tasks that should chaining. 5 | cha.in('glob', require('task-glob')) 6 | .in('combine', require('task-combine')) 7 | .in('replace', require('task-replace')) 8 | .in('writer', require('task-writer')) 9 | .in('uglifyjs',require('task-uglifyjs')) 10 | .in('request', require('task-request')) 11 | 12 | // Start with cha expressions. 13 | cha(['glob:./fixtures/js/*.js', 'request:http://underscorejs.org/underscore-min.js']) 14 | .replace({ 15 | search: 'TIMESTAMP', 16 | replace: +new Date 17 | }) 18 | .replace({ 19 | search: /DEBUG/g, 20 | replace: true 21 | }) 22 | .combine() 23 | .uglifyjs() 24 | .writer('./test/out/foobar.js') 25 | -------------------------------------------------------------------------------- /test/fixtures/coffee/bar.coffee: -------------------------------------------------------------------------------- 1 | # Objects: 2 | math = 3 | root: Math.sqrt 4 | square: square 5 | cube: (x) -> x * square x 6 | 7 | # Splats: 8 | race = (winner, runners...) -> 9 | print winner, runners 10 | 11 | # Existence: 12 | alert "I knew it!" if elvis? 13 | 14 | # Array comprehensions: 15 | cubes = (math.cube num for num in list) -------------------------------------------------------------------------------- /test/fixtures/coffee/foo.coffee: -------------------------------------------------------------------------------- 1 | # Assignment: 2 | number = 42 3 | opposite = true 4 | 5 | # Conditions: 6 | number = -42 if opposite 7 | 8 | # Functions: 9 | square = (x) -> x * x 10 | 11 | # Arrays: 12 | list = [1, 2, 3, 4, 5] -------------------------------------------------------------------------------- /test/fixtures/js/bar.js: -------------------------------------------------------------------------------- 1 | var debug = DEBUG; 2 | var ver = "v4"; 3 | -------------------------------------------------------------------------------- /test/fixtures/js/foo.js: -------------------------------------------------------------------------------- 1 | var ts = TIMESTAMP; 2 | --------------------------------------------------------------------------------