listr
Advanced tools
Comparing version 0.4.3 to 0.5.0
23
index.js
@@ -7,3 +7,8 @@ 'use strict'; | ||
constructor(tasks) { | ||
constructor(tasks, opts) { | ||
if (tasks && !Array.isArray(tasks) && typeof tasks === 'object') { | ||
opts = tasks; | ||
tasks = []; | ||
} | ||
if (tasks && !Array.isArray(tasks)) { | ||
@@ -15,2 +20,7 @@ throw new TypeError('Expected an array of tasks'); | ||
this._tasks = []; | ||
this._options = Object.assign({ | ||
showSubtasks: true, | ||
concurrent: false | ||
}, opts); | ||
this.level = 0; | ||
@@ -29,3 +39,3 @@ | ||
for (const task of tasks) { | ||
this._tasks.push(new Task(this, task)); | ||
this._tasks.push(new Task(this, task, this._options)); | ||
} | ||
@@ -47,3 +57,10 @@ | ||
return this._tasks.reduce((promise, task) => promise.then(() => task.run()), Promise.resolve()) | ||
let tasks; | ||
if (this._options.concurrent === true) { | ||
tasks = Promise.all(this._tasks.map(task => task.run())); | ||
} else { | ||
tasks = this._tasks.reduce((promise, task) => promise.then(() => task.run()), Promise.resolve()); | ||
} | ||
return tasks | ||
.then(() => { | ||
@@ -50,0 +67,0 @@ this._renderer.end(); |
'use strict'; | ||
exports.LocalRenderer = require('./local-renderer'); | ||
exports.StringRenderer = require('./string-renderer'); | ||
exports.CLIRenderer = require('./cli-renderer'); |
@@ -5,3 +5,4 @@ 'use strict'; | ||
COMPLETED: 1, | ||
FAILED: 2 | ||
FAILED: 2, | ||
SKIPPED: 3 | ||
}; |
117
lib/task.js
@@ -8,5 +8,7 @@ 'use strict'; | ||
const isStream = require('is-stream'); | ||
const isPromise = require('is-promise'); | ||
const streamToObservable = require('stream-to-observable'); | ||
const stripAnsi = require('strip-ansi'); | ||
const LocalRenderer = require('./renderer').LocalRenderer; | ||
const cliTruncate = require('cli-truncate'); | ||
const StringRenderer = require('./renderer').StringRenderer; | ||
const state = require('./state'); | ||
@@ -16,4 +18,7 @@ | ||
const pointer = chalk[color](figures.pointer); | ||
const skipped = chalk[color](figures.arrowDown); | ||
const isListr = obj => obj.setRenderer && obj.add && obj.run; | ||
const isListr = obj => obj && obj.setRenderer && obj.add && obj.run; | ||
// https://github.com/sindresorhus/is-observable/issues/1 | ||
const isObservable = obj => obj && obj.subscribe && obj.forEach; | ||
@@ -23,3 +28,3 @@ const getSymbol = task => { | ||
case state.PENDING: | ||
return task._result ? `${pointer} ` : task._spinner.frame(); | ||
return task.showSubtasks() ? `${pointer} ` : task._spinner.frame(); | ||
case state.COMPLETED: | ||
@@ -29,2 +34,4 @@ return `${logSymbols.success} `; | ||
return task._result ? `${pointer} ` : `${logSymbols.error} `; | ||
case state.SKIPPED: | ||
return `${skipped} `; | ||
default: | ||
@@ -35,5 +42,7 @@ return ' '; | ||
const defaultSkipFn = () => false; | ||
class Task { | ||
constructor(listr, task) { | ||
constructor(listr, task, options) { | ||
if (!task) { | ||
@@ -51,13 +60,24 @@ throw new TypeError('Expected a task'); | ||
if (task.skip && typeof task.skip !== 'function') { | ||
throw new TypeError(`Expected property \`skip\` of type \`function\` but got \`${typeof task.skip}\``); | ||
} | ||
this._listr = listr; | ||
this._options = options || {}; | ||
this._spinner = new Ora({color}); | ||
this.title = task.title; | ||
this.skip = task.skip || defaultSkipFn; | ||
this.task = task.task; | ||
} | ||
showSubtasks() { | ||
return this._result && (this._options.showSubtasks !== false || this.state === state.FAILED); | ||
} | ||
render() { | ||
let out = indentString(` ${getSymbol(this)}${this.title}`, ' ', this._listr.level); | ||
const skipped = this.state === state.SKIPPED ? ` ${chalk.dim('[skipped]')}` : ''; | ||
let out = indentString(` ${getSymbol(this)}${this.title}${skipped}`, ' ', this._listr.level); | ||
if (this._result) { | ||
if (this.showSubtasks()) { | ||
const output = this._result.render(); | ||
@@ -69,4 +89,8 @@ if (output !== null) { | ||
if (this.state === state.PENDING && this._lastLine) { | ||
out += `\n ${indentString(chalk.gray(`${figures.arrowRight} ${this._lastLine}`), ' ', this._listr.level)}`; | ||
if ((this.state === state.PENDING || this.state === state.SKIPPED) && this._output) { | ||
const lastLine = stripAnsi(this._output.trim().split('\n').pop().trim()); | ||
if (lastLine) { | ||
const output = indentString(`${figures.arrowRight} ${lastLine}`, ' ', this._listr.level); | ||
out += `\n ${chalk.gray(cliTruncate(output, process.stdout.columns - 3))}`; | ||
} | ||
} | ||
@@ -78,38 +102,57 @@ | ||
run() { | ||
const handleResult = result => { | ||
// Detect the subtask | ||
if (isListr(result)) { | ||
result.level = this._listr.level + 1; | ||
result.setRenderer(StringRenderer); | ||
this._result = result; | ||
return result.run(); | ||
} | ||
// Detect stream | ||
if (isStream(result)) { | ||
result = streamToObservable(result); | ||
} | ||
// Detect Observable | ||
if (isObservable(result)) { | ||
result = new Promise((resolve, reject) => { | ||
result.subscribe({ | ||
next: data => { | ||
this._output = data; | ||
}, | ||
error: reject, | ||
complete: resolve | ||
}); | ||
}); | ||
} | ||
// Detect promise | ||
if (isPromise(result)) { | ||
return result.then(handleResult); | ||
} | ||
return result; | ||
}; | ||
return Promise.resolve() | ||
.then(() => { | ||
this.state = state.PENDING; | ||
let result = this.task(); | ||
// Detect the subtask | ||
if (isListr(result)) { | ||
result.level = this._listr.level + 1; | ||
result.setRenderer(LocalRenderer); | ||
this._result = result; | ||
return result.run(); | ||
return this.skip(); | ||
}) | ||
.then(skipped => { | ||
if (skipped) { | ||
this.state = state.SKIPPED; | ||
if (typeof skipped === 'string') { | ||
this._output = skipped; | ||
} | ||
return; | ||
} | ||
// Detect stream | ||
if (isStream(result)) { | ||
result = streamToObservable(result); | ||
} | ||
// https://github.com/sindresorhus/is-observable/issues/1 | ||
// Detect Observable | ||
if (result.subscribe && result.forEach) { | ||
return new Promise((resolve, reject) => { | ||
result.subscribe( | ||
data => { | ||
this._lastLine = stripAnsi(data.trim().split('\n').pop().trim()); | ||
}, | ||
reject, | ||
resolve | ||
); | ||
}); | ||
} | ||
return result; | ||
return handleResult(this.task()); | ||
}) | ||
.then(() => { | ||
this.state = state.COMPLETED; | ||
if (this.state === state.PENDING) { | ||
this.state = state.COMPLETED; | ||
} | ||
}) | ||
@@ -116,0 +159,0 @@ .catch(err => { |
{ | ||
"name": "listr", | ||
"version": "0.4.3", | ||
"version": "0.5.0", | ||
"description": "Terminal task list", | ||
@@ -41,4 +41,6 @@ "license": "MIT", | ||
"chalk": "^1.1.3", | ||
"cli-truncate": "^0.2.1", | ||
"figures": "^1.7.0", | ||
"indent-string": "^2.1.0", | ||
"is-promise": "^2.1.0", | ||
"is-stream": "^1.1.0", | ||
@@ -54,2 +56,3 @@ "log-symbols": "^1.0.2", | ||
"clinton": "*", | ||
"delay": "^1.3.1", | ||
"rxjs": "^5.0.0-beta.9", | ||
@@ -56,0 +59,0 @@ "xo": "*" |
@@ -41,3 +41,3 @@ # listr [![Build Status](https://travis-ci.org/SamVerschueren/listr.svg?branch=master)](https://travis-ci.org/SamVerschueren/listr) | ||
} | ||
]); | ||
], {concurrent: true}); | ||
} | ||
@@ -67,3 +67,3 @@ }, | ||
A `task` can return different values. If a `task` returns, it means the task was completed succesfully. If a task throws an error, the task failed. | ||
A `task` can return different values. If a `task` returns, it means the task was completed successfully. If a task throws an error, the task failed. | ||
@@ -88,3 +88,3 @@ ```js | ||
A `task` can also be async by returning a `Promise`. If the promise resolves, the task completed sucessfully, it it rejects, the task failed. | ||
A `task` can also be async by returning a `Promise`. If the promise resolves, the task completed successfully, it it rejects, the task failed. | ||
@@ -106,3 +106,3 @@ ```js | ||
<img src="media/observable.gif" width="255" align="right"> | ||
<img src="media/observable.gif" width="250" align="right"> | ||
@@ -142,5 +142,40 @@ A `task` can also return an `Observable`. The thing about observables is that it can emit multiple values and can be used to show the output of the | ||
#### Skipping tasks | ||
<img src="media/skipped.png" width="250" align="right"> | ||
Optionally specify a `skip` function to determine whether a task can be skipped. | ||
- If the `skip` function returns a truthy value or a `Promise` that resolves to a truthy value then the task will be skipped. | ||
- If the returned value is a string it will be displayed as the reason for skipping the task. | ||
- If the `skip` function returns a falsey value or a `Promise` that resolves to a falsey value then the task will be executed as normal. | ||
- If the `skip` function throws or returns a `Promise` that rejects, the task (and the whole build) will fail. | ||
```js | ||
const tasks = new Listr([ | ||
{ | ||
title: 'Task 1', | ||
task: () => Promise.resolve('Foo') | ||
}, | ||
{ | ||
title: 'Can be skipped', | ||
skip: () => { | ||
if (Math.random() > 0.5) { | ||
return 'Reason for skipping'; | ||
} | ||
}, | ||
task: () => 'Bar' | ||
}, | ||
{ | ||
title: 'Task 3', | ||
task: () => Promise.resolve('Bar') | ||
} | ||
]); | ||
``` | ||
## API | ||
### Listr([tasks]) | ||
### Listr([tasks], [options]) | ||
@@ -165,2 +200,25 @@ #### tasks | ||
##### skip | ||
Type: `Function` | ||
Skip function. Read more about [skipping tasks](#skipping-tasks). | ||
#### options | ||
##### showSubtasks | ||
Type: `boolean`<br> | ||
Default: `true` | ||
Set to `false` if you want to disable the rendering of the subtasks. Subtasks will be rendered if | ||
an error occurred in one of them. | ||
##### concurrent | ||
Type: `boolean`<br> | ||
Default: `false` | ||
Set to `true` if you want tasks to run concurrently. | ||
### Instance | ||
@@ -167,0 +225,0 @@ |
13817
257
244
11
5
+ Addedcli-truncate@^0.2.1
+ Addedis-promise@^2.1.0
+ Addedcli-truncate@0.2.1(transitive)
+ Addedcode-point-at@1.1.0(transitive)
+ Addedis-fullwidth-code-point@1.0.0(transitive)
+ Addedis-promise@2.2.2(transitive)
+ Addednumber-is-nan@1.0.1(transitive)
+ Addedslice-ansi@0.0.4(transitive)
+ Addedstring-width@1.0.2(transitive)