├── .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 [](http://npm.org/cha) [](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 |
--------------------------------------------------------------------------------