UNPKG

20.5 kBMarkdownView Raw
1# taskr [![npm](https://img.shields.io/npm/v/taskr.svg)](https://npmjs.org/package/taskr)
2
3Taskr is a highly performant task automation tool, much like Gulp or Grunt, but written with concurrency in mind. With Taskr, everything is a [coroutine](https://medium.com/@tjholowaychuk/callbacks-vs-coroutines-174f1fe66127#.vpryf5tyb), which allows for cascading and composable tasks; but unlike Gulp, it's not limited to the stream metaphor.
4
5Taskr is extremely extensible, so _anything_ can be a task. Our core system will accept whatever you throw at it, resulting in a modular system of reusable plugins and tasks, connected by a declarative `taskfile.js` that's easy to read.
6
7<h2>Table of Contents</h2>
8
9<details>
10<summary>Table of Contents</summary>
11
12- [Features](#features)
13- [Example](#example)
14- [Concepts](#concepts)
15 * [Core](#core)
16 * [Plugins](#plugins)
17 * [Tasks](#tasks)
18 * [Taskfiles](#taskfiles)
19- [CLI](#cli)
20- [API](#api)
21 * [Taskr](#taskr-1)
22 * [Plugin](#plugin)
23 * [Task](#task-1)
24 * [Utilities](#utilities)
25- [Installation](#installation)
26- [Usage](#usage)
27 * [Getting Started](#getting-started)
28 * [Programmatic](#programmatic)
29- [Credits](#credits)
30</details>
31
32## Features
33
34- **lightweight:** with `6` dependencies, [installation](#installation) takes seconds
35- **minimal API:** Taskr only exposes a couple methods, but they're everything you'll ever need
36- **performant:** because of [Bluebird](https://github.com/petkaantonov/bluebird/), creating and running Tasks are quick and inexpensive
37- **cascading:** sequential Task chains can cascade their return values, becoming the next Task's argument
38- **asynchronous:** concurrent Task chains run without side effects & can be `yield`ed consistently
39- **composable:** chain APIs and Tasks directly; say goodbye to `pipe()` x 100!
40- **modular:** easily share or export individual Tasks or Plugins for later use
41- **stable:** requires Node `>= 4.6` to run (LTS is `6.11`)
42
43## Example
44
45Here's a simple [`taskfile`](#taskfiles) (with [shorthand generator methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions#Shorthand_generator_methods)) depicting a [parallel](#taskrparalleltasks-options) chain.
46
47```js
48const sass = 'src/{admin,client}/*.sass';
49const js = 'src/{admin,client}/*.js';
50const dist = 'build';
51
52module.exports = {
53 *lint(task) {
54 yield task.source(js).xo({ esnext:true });
55 },
56 *scripts(task) {
57 yield task.source(js).babel({ presets:['es2015'] }).target(`${dist}/js`);
58 },
59 *styles(task) {
60 yield task.source(sass).sass({ outputStyle:'compressed' }).autoprefixer().target(`${dist}/css`);
61 },
62 *build(task) {
63 yield task.parallel(['lint', 'scripts', 'styles']);
64 }
65}
66```
67
68## Concepts
69
70### Core
71
72Taskr is a task runner. It's designed to get you from `A` to `B` -- that's it.
73
74If it helps, imagine you're dining in a restaurant and Taskr is the food runner. Taskr's role is solely to collect meals from the kitchen (`task.source`) and deliver them to the correct table (`task.target`). As a food runner, Taskr may do this one plate at a time (`task.serial`) or deliver multiple plates at once (`task.parallel`). Either way, Taskr only cares about going from `A` to `B`. It may not be the most glamorous job, but as far as you (the patron) are concerned, it's incredibly important because it brings you food.
75
76### Plugins
77
78Because Taskr is single-minded and cares only about executing [tasks](#tasks), **everything else is a plugin**. This keeps development with Taskr easy, approachable, and lightweight.
79
80You see, installing Taskr gives access to a reliable task runner. You decide what it _can do_, provide it functionality, and dictate when to do it. You're in full control.
81
82Through plugins, you are able to capture useful behavior and share them across tasks or projects for repeated use. Plugins come in three flavors:
83
84* **external** - installed via NPM; called "external" because they live outside your codebase
85* **inline** - generally simple, one-time functions; not sensible for reuse since declared within a task (hence "inline")
86* **local** - private, reusable plugins; appear exactly like external plugins but not public on NPM.
87
88### Tasks
89
90Tasks are used to tell Taskr what to do. They are written as generator functions & converted to coroutines internally. They're also fully self-contained and, like plugins, can be shared across projects if desired.
91
92Upon runtime, tasks are cheap to create, so are also destroyed once completed. This also helps Taskr remain efficient; history won't weigh it down.
93
94Lastly, tasks have the power to [start](#taskstarttask-options) other Tasks, including [serial](#taskserialtasks-options) and [parallel](#taskparalleltasks-options) chains!
95
96### Taskfiles
97
98Much like Gulp, Taskr uses a `taskfile.js` (case sensitive) to read and run your Tasks. However, because it's a regular JavaScript file, you may also `require()` additional modules and incorporate them directly into your Tasks, without the need for a custom Plugin!
99
100```js
101const browserSync = require('browser-sync');
102
103exports.serve = function * (task) {
104 browserSync({
105 port: 3000,
106 server: 'dist',
107 middleware: [
108 require('connect-history-api-fallback')()
109 ]
110 });
111 yield task.$.log('> Listening on localhost:3000');
112}
113```
114
115Taskfiles should generally be placed in the root of your project, alongside your `package.json`. Although this is not required, Taskr (strongly) prefers this location.
116
117> **Note:** You may set an alternate directory path through the CLI's `cwd` option.
118
119Through Node, Taskr only supports ES5 syntax; however, if you prefer ES6 or ES7, just install [`@taskr/esnext`](https://github.com/lukeed/taskr/tree/master/packages/esnext)!
120
121## CLI
122
123Taskr's CLI tool is very simple and straightforward.
124
125```
126taskr [options] <task names>
127taskr --mode=parallel task1 task2 ...
128```
129> Please run `taskr --help` or `taskr -h` for usage information.
130
131Most commonly, the CLI is used for [NPM script](https://docs.npmjs.com/misc/scripts) definitions.
132
133```js
134// package.json
135{
136 "scripts": {
137 "build": "taskr foo bar"
138 }
139}
140```
141
142## API
143
144### Taskr
145
146Taskr itself acts as a "parent" class to its `Task` children. Because of this, Taskr's methods are purely executive; aka, they manage Tasks and tell them how & when to run.
147
148#### Taskr.start(task, [options])
149Yield: `Any`<br>
150Start a Task by its name; may also pass initial values. Can return anything the Task is designed to.
151
152##### task
153Type: `String`<br>
154Default: `'default'`<br>
155The Task's name to run. Task must exist/be defined or an Error is thrown.<br>
156> **Important!** Taskr expects a `default` task if no task is specified. This also applies to CLI usage.
157
158##### options
159Type: `Object`<br>
160Default: `{ src:null, val:null }`<br>
161Initial/Custom values to start with. You may customize the object shape, but only `val` will be cascaded from Task to Task.
162
163#### Taskr.parallel(tasks, [options])
164Yield: `Any`<br>
165Run a group of tasks simultaneously. Cascading is disabled.
166##### tasks
167Type: `Array`<br>
168The names of Tasks to run. Task names must be `string`s and must be defined.
169##### options
170Type: `Object`<br>
171Initial values to start with; passed to each task in the group. Does not cascade.
172
173#### Taskr.serial(tasks, [options])
174Yield: `Any`<br>
175Run a group of tasks sequentially. Cascading is enabled.
176##### tasks
177Type: `Array`<br>
178The names of Tasks to run. Task names must be `string`s and must be defined.
179##### options
180Type: `Object`<br>
181Initial values to start with; passed to each task in the group. Does cascade.
182
183```js
184module.exports = {
185 *default(task) {
186 yield task.serial(['first', 'second'], { val:10 });
187 },
188 *first(task, opts) {
189 yield task.$.log(`first: ${opts.val}`);
190 return opts.val * 4;
191 },
192 *second(task, opts) {
193 yield task.$.log(`second: ${opts.val}`);
194 return opts.val + 2;
195 }
196}
197
198const output = yield task.start();
199//=> first: 10
200//=> second: 40
201console.log(output);
202//=> 42
203```
204
205### Plugin
206
207Plugins can be external, internal, or local. However, all plugins share the same options:
208
209##### options.every
210Type: `Boolean`<br>
211Default: `true`<br>
212If the plugin function should iterate through _every_ `file|glob`.
213
214##### options.files
215Type: `Boolean`<br>
216Default: `true`<br>
217If the plugin should receive the Task's `glob` patterns or its expanded `file` objects. Uses `globs` if `false`.
218
219Every plugin must also pass a **generator function**, which will be wrapped into a coroutine. This function's arguments will be the `file|glob`(s), depending on the `options.every` and `options.files` combination. The function's second argument is the user-provided config object.
220
221The plugin's generator function is **always** bound to the current `Task`, which means `this` refers to the Task instance.
222
223#### Internal Plugins
224
225Internal plugins are for single-use only. If you're defining the same behavior repeatedly, it should be extracted to a local or external plugin instead.
226
227> **Note:** Inline plugins have no need for a second argument in their generator function; you are the "user" here.
228
229See [`task.run`](#taskrunoptions-generator) for a simple example. The same inline example may be written purely as an object:
230
231```js
232exports.foo = function * (task) {
233 yield task.source('src/*.js').run({
234 every: false,
235 *func(files) {
236 Array.isArray(files); //=> true
237 yield Promise.resolve('this will run once.');
238 }
239 }).target('dist');
240}
241```
242
243#### External Plugins
244
245Unlike "inline" plugins, external and local plugins are defined before a Task is performed. Because of this, they must define a `name` for their method to use within a Task.
246
247Similar to inline plugins, there are two ways of defining an exported module -- via functional or object definitions.
248
249When using a _functional definition_, the **definition** receives the [Taskr](#taskr-1) instance and the [utilities](#utilities) object.
250
251```js
252module.exports = function (task, utils) {
253 // promisify before running else repeats per execution
254 const render = utils.promisify(function () {});
255 // verbose API
256 task.plugin('myName', {every: false}, function * (files, opts) {
257 console.log('all my files: ', files); //=> Array
258 console.log(this._.files === files); //=> true
259 console.log(this instanceof Task); //=> true
260 console.log('user options: ', opts);
261 yield render(opts);
262 });
263 // or w/condensed API
264 task.plugin({
265 name: 'myName',
266 every: false,
267 *func(files, opts) {
268 // ...same
269 }
270 });
271}
272```
273
274When using an _object definition_, you are not provided the `task` or `utils` parameters. **This assumes that you do not need any prep work for your plugin!**
275
276```js
277module.exports = {
278 name: 'myName',
279 every: false,
280 *func(files, opts) {
281 // do stuff
282 }
283}
284```
285
286Then, within your Task, you may use it like so:
287
288```js
289exports.default = function * (task) {
290 yield task.source('src/*.js').myName({ foo:'bar' }).target('dist');
291}
292```
293
294#### Local Plugins
295
296Local plugins are defined exactly like external plugins. The only difference is that they're not installable via NPM.
297
298In order to use a local plugin, add a `taskr` key to your `package.json` file. Then define a `requires` array with paths to your plugins.
299
300```js
301{
302 "taskr": {
303 "requires": [
304 "./build/custom-plugin-one.js",
305 "./build/custom-plugin-two.js"
306 ]
307 }
308}
309```
310
311For [programmatic usage](#programmatic), simply pass an array of definitions to the `plugins` key:
312
313```js
314const Taskr = require('taskr')
315const taskr = new Taskr({
316 plugins: [
317 require('./build/custom-plugin-one.js'),
318 require('./build/custom-plugin-two.js'),
319 require('@taskr/clear')
320 {
321 name: 'plugThree',
322 every: false,
323 files: false,
324 *func(globs, opts) {
325 // nifty, eh?
326 }
327 }
328 ]
329});
330```
331
332### Task
333
334A Task receives itself as its first argument. We choose to name the parameter `task` simply as a convention; of course, you may call it whatever you'd like.
335
336Tasks are exported from a `taskfile.js`, which means you can use either syntax:
337
338```js
339exports.foo = function * (task) {
340 yield task.source('src/*.js').target('dist/js');
341}
342exports.bar = function * (task) {
343 yield task.source('src/*.css').target('dist/css');
344}
345// or
346module.exports = {
347 *foo(task) {
348 yield task.source('src/*.js').target('dist/js');
349 },
350 *bar(task) {
351 yield task.source('src/*.css').target('dist/css');
352 }
353}
354```
355
356Each Task also receives an `opts` object, consisting of `src` and `val` keys. Although `src` is primarily used for [`@taskr/watch`](https://github.com/lukeed/taskr/tree/master/packages/watch), the `val` key can be used or set at any time see [`Taskr.serial`](#taskrserialtasks-options).
357
358All methods and values below are exposed within a Task's function.
359
360#### task.root
361Type: `String`<br>
362The directory wherein `taskfile.js` resides, now considered the root. Also accessible within plugins.
363
364#### task.$
365Type: `Object`<br>
366The Task's utility helpers. Also accessible within plugins. See [Utilities](#utilities).
367
368#### task._
369Type: `Object`<br>
370The Task's internal state, populated by `task.source()`. Also accessible within plugins.
371##### task._.files
372Type: `Array`<br>
373The Task's active files. Each object contains a `dir` and `base` key from its [`pathObject`](https://nodejs.org/api/path.html#path_path_format_pathobject) and maintains the file's Buffer contents as a `data` key.
374##### task._.globs
375Type: `Array`<br>
376The Task's glob patterns, from `task.source()`. Used to populate `task._.files`.
377##### task._.prevs
378Type: `Array`<br>
379The Task's last-known (aka, outdated) set of glob patterns. Used **only** for [`@taskr/watch`](https://github.com/lukeed/taskr/tree/master/packages/watch).
380
381#### task.source(globs, [options])
382##### globs
383Type: `Array|String`<br>
384Any valid glob pattern or array of patterns.
385##### options
386Type: `Object`<br>
387Default: `{}`<br>
388Additional options, passed directly to [`node-glob`](https://github.com/isaacs/node-glob#options).
389
390#### task.target(dirs, [options])
391##### dirs
392Type: `Array|String`<br>
393The destination folder(s).
394##### options
395Type: `Object`<br>
396Default: `{}`<br>
397Additional options, passed directly to [`fs.writeFile`](https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback).
398
399Please note that `task.source()` glob ambiguity affects the destination structure.
400
401```js
402yield task.source('src/*.js').target('dist');
403//=> dist/foo.js, dist/bar.js
404yield task.source('src/**/*.js').target('dist');
405//=> dist/foo.js, dist/bar.js, dist/sub/baz.js, dist/sub/deep/bat.js
406```
407
408#### task.run(options, generator)
409Perform an inline plugin.
410
411##### options
412Type: `Object`<br>
413The See [plugin options](#plugin).
414##### generator
415Type: `Function`<br>
416The action to perform; must be a `Generator` function.
417
418```js
419exports.foo = function * (task) {
420 yield task.source('src/*.js').run({ every:false }, function * (files) {
421 Array.isArray(files); //=> true
422 yield Promise.resolve('this will run once.');
423 }).target('dist')
424}
425```
426
427#### task.start(task, [options])
428See [`Taskr.start`](#taskrstarttask-options).
429
430#### task.parallel(tasks, [options])
431See [`Taskr.parallel`](#taskrparalleltasks-options).
432
433#### task.serial(tasks, [options])
434See [`Taskr.serial`](#taskrserialtasks-options).
435
436### Utilities
437
438A collection of utility helpers to make life easy.
439
440#### alert(...msg)
441Print to console with timestamp and alert coloring. See [`utils.log`](#logmsg).
442##### msg
443Type: `String`
444
445#### coroutine(generator)
446See [Bluebird.coroutine](http://bluebirdjs.com/docs/api/promise.coroutine.html).
447
448#### error(...msg)
449Print to console with timestamp and error coloring. See [`utils.log`](#logmsg).
450##### msg
451Type: `String`
452
453#### expand(globs, options)
454Yield: `Array`<br>
455Get all filepaths that match the glob pattern constraints.
456##### globs
457Type: `Array|String`
458##### options
459Type: `Object`<br>
460Default: `{}`<br>
461Additional options, passed directly to [`node-glob`](https://github.com/isaacs/node-glob#options).
462
463#### find(filename, dir)
464Yield: `String|null`<br>
465Find a complete filepath from a given path, or optional directory.
466##### filename
467Type: `String`<br>
468The file to file; may also be a complete filepath.
469##### dir
470Type: `String`<br>
471Default: `'.'`<br>
472The directory to look within. Will be prepended to the `filename` value.
473
474#### log(...msg)
475Print to console with timestamp and normal coloring.
476##### msg
477Type: `String`<br>
478You may pass more than one `msg` string.
479
480```js
481utils.log('Hello');
482//=> [10:51:04] Hello
483utils.log('Hello', 'World');
484//=> [10:51:14] Hello World
485```
486
487#### promisify(function, callback)
488See [Bluebird.promisify](http://bluebirdjs.com/docs/api/promise.promisify.html).
489
490#### read(filepath, options)
491Yield: `Buffer|String|null`<br>
492Get a file's contents. Ignores directory paths.
493##### filepath
494Type: `String`<br>
495The full filepath to read.
496##### options
497Type: `Object`<br>
498Additional options, passed to [`fs.readFile`](https://nodejs.org/api/fs.html#fs_fs_readfile_file_options_callback).
499
500#### trace(stack)
501Parse and prettify an Error's stack.
502
503#### write(filepath, data, options)
504Yield: `null`<br>
505Write given data to a filepath. Will create directories as needed.
506##### filepath
507Type: `String`<br>
508The full filepath to write into.
509##### data
510Type: `String|Buffer`<br>
511The data to be written; see [`fs.writeFile`](https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback).
512##### options
513Type: `Object`<br>
514Additional options, passed to [`fs.writeFile`](https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback).
515
516## Installation
517
518```sh
519$ npm install --save-dev taskr
520```
521
522## Usage
523
524### Getting Started
525
5261. Install Taskr & any desired plugins. (see [installation](#installation) and [ecosystem](https://github.com/lukeed/taskr#packages))
5272. Create a `taskfile.js` next to your `package.json`.
5283. Define `default` and additional tasks within your `taskfile.js`.
529
530 ```js
531 exports.default = function * (task) {
532 yield task.parallel(['styles', 'scripts']);
533 }
534
535 exports.styles = function * (task) {
536 yield task.source('src/**/*.css').autoprefixer().target('dist/css');
537 }
538
539 exports.scripts = function * (task) {
540 yield task.source('src/**/*.js').babel({
541 presets: [
542 ['es2015', { loose:true, modules:false }]
543 ]
544 }).target('dist/js');
545 }
546 ```
5474. Add a `"scripts"` key to your `package.json`:
548
549 ```json
550 {
551 "name": "my-project",
552 "scripts": {
553 "build": "taskr"
554 }
555 }
556 ```
557
558 > **Note:** The `default` task is run if no other tasks are specified.
5595. Run your `build` command:
560
561 ```sh
562 $ npm run build
563 ```
564
565You may be interested in checking out [Web Starter Kit](https://github.com/lukeed/fly-kit-web) for a head start.
566
567### Programmatic
568
569Taskr is extremely flexible should you choose to use Taskr outside of its standard configuration.
570
571The quickest path to a valid `Taskr` instance is to send a `tasks` object:
572
573```js
574const Taskr = require('Taskr');
575const taskr = new Taskr({
576 tasks: {
577 *foo(f) {},
578 *bar(f) {}
579 }
580});
581taskr.start('foo');
582```
583
584By default, your new Taskr instance will not include any plugins. You have the power to pick and choose what your instance needs!
585
586To do this, you may pass an array of [external](#external-plugins) and [local](#local-plugins) `plugins`:
587
588```js
589const taskr = new Taskr({
590 plugins: [
591 require('@taskr/concat'),
592 require('@taskr/clear'),
593 require('./my-plugin')
594 ]
595});
596```
597
598> **Important:** This assumes you have provided a valid `file` _or_ `tasks` object. Without either of these, your Taskr instance will be incomplete and therefore invalid. This will cause the instance to exit early, which means that your `plugins` will not be mounted to the instance.
599
600You may also define your `tasks` by supplying a `taskfile.js` path to `file`. Whenever you do this, you **should** also update the `cwd` key because your [root](#taskroot) has changed!
601
602```js
603const join = require('path').join;
604
605const cwd = join(__dirname, '..', 'build');
606const file = join(cwd, 'taskfile.js');
607
608const taskr = new Taskr({ file, cwd });
609```
610
611
612## Credits
613
614This project used to be called `fly`, but [Jorge Bucaran](https://github.com/jbucaran), its original author, generously handed over the project to [Luke Edwards](https://github.com/lukeed) in order to further development. The project is now named `taskr` to reflect the transition and its exicitng new future!
615
616A **big thanks** to [Constantin Titarenko](https://github.com/titarenko) for donating the name `taskr` on NPM -- very much appreciated!