├── .babelrc ├── .gitignore ├── .huskyrc.json ├── .npmignore ├── .npmrc ├── .prettierrc.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── task.js ├── jest.config.js ├── package.json ├── src ├── index.spec.ts └── index.ts ├── test ├── __snapshots__ │ └── e2e.spec.ts.snap ├── e2e.spec.ts └── sandbox │ ├── package.json │ ├── scripts │ ├── color.js │ ├── error.js │ └── hello.js │ └── tasksfile.js ├── tsconfig.json ├── tslint.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | node_modules 5 | npm-debug.log 6 | /coverage 7 | /lib 8 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged" 4 | } 5 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.spec.ts 2 | /src 3 | /test 4 | /.huskyrc.json 5 | /.travis.yml 6 | /.prettierrc.json 7 | /.vscode 8 | /.babelrc 9 | /test 10 | /jest.config.js 11 | /tslint.json 12 | /tsconfig.json 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.15.0" 4 | - "10.15.0" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 5.1.0 2 | 3 | *API:* 4 | - `sh` function now accepts different set of options. Most importantly it does not accept 5 | `stdio` option anymore. By default it will be always in `stdio=pipe` mode for `stdout` 6 | and `stderr` process streams. It will also print out results to the terminal unless `silent=true` 7 | option given. 8 | 9 | - `sh` function accepts also `transform` option which allows to transform output of 10 | shell process. Usefull if we want to add prefixes to the output. 11 | 12 | - introducing `prefixTransform` function. It is dedicated for usage along with `sh` 13 | function as value for `transform` option. 14 | 15 | ## 5.0.0 16 | 17 | *API:* 18 | 19 | - `help` function as second argument now accepts only `string`, for third argument 20 | accepts detailed help information about `options` and `params` 21 | - removing `option` and `options` helper, now `options` will be passed always as a first 22 | argument to the task 23 | - introducing `rawArgs` function, which returns raw, unparsed args which were provided 24 | to the task 25 | - renaming `run` function to `sh` 26 | - introducing `cli` function which exposes tasks functions to the cli 27 | 28 | 29 | *Mechanics:* 30 | 31 | - `options` are always passed as a first argument to the task function 32 | - calling tasks occurs through `npx task` not `npx run` script or by calling 33 | `tasksfile.js` directly (`node tasksfile.js`). Tasks file now behaves like a 34 | CLI script 35 | - to be able to call tasks from `tasksfile.js` they must be exposed by `cli` 36 | function. Exporting tasks functions won't do the job anymore. 37 | - to be able to call a task through `npx task`, entry to `npm scripts` must be added: 38 | `"task": "node ./tasksfile.js"`. `npx task` always try to execute `task` npm script, 39 | it's just an alias. 40 | - namespaces now can have `default` task 41 | - removing autocomplete feature, as it required too much configuration and it 42 | seems not used by the users 43 | - full support for `TypeScript`. Types files are included within the project. Using 44 | `TypeScript` for `tasksfile` is possible. It just require different entry in `npm scripts`: 45 | `"task": "ts-node ./tasksfile.ts"` 46 | 47 | 48 | *Other:* 49 | 50 | - renaming project from `runjs` to `tasksfile` 51 | 52 | 53 | ## 4.4.0 54 | 55 | - source code migrated from `Flow/Babel` to `TypeScript` 56 | 57 | ## 4.3.0 58 | 59 | - adding experimental bash autocompletion feature 60 | 61 | ## 4.2.0 62 | 63 | *Changes:* 64 | 65 | - introducing `help` utility function 66 | - improving task docs generation by annotations 67 | 68 | *Dev env:* 69 | 70 | - improving e2e tests 71 | - using external module `microcli` as for cli args parsing and `--help` handling 72 | 73 | ## 4.1.0 74 | 75 | - remove log option from `run` api command 76 | - upgrade dependent packages 77 | - introducing `options` helper, deprecating `option` 78 | - better printing of methods list when calling `run` 79 | 80 | *For development env:* 81 | 82 | - add tests coverage check 83 | - introducing `flow` types 84 | 85 | ## 4.0.0 86 | 87 | **Changes:** 88 | 89 | - removing `ask` and `generate` helpers from api, to keep runjs codebase more focused about its main purpose 90 | - dropping support for `node` < 6.11.1 91 | - support for other than `Babel` transpilers, like `TypeScript` 92 | - log option to run function, when `false` it does not log the command 93 | - documentation updates 94 | - support for `async` / `await` 95 | - `option` helper 96 | 97 | **Migration from 3.x to 4.x procedure:** 98 | 99 | - make sure you have node version >=6.11.1 100 | - if you use Babel you need to add `"runjs": {requires: ["./node_modules/babel-register"]}` config to your package.json, otherwise Babel transpiler won't be picked up 101 | - find alternatives for ask and generate, those are not supported by runjs anymore 102 | 103 | ## 3.4.1 104 | 105 | - changing documentation format for calling `run` without arguments (task documentation) 106 | - changing name of the prop for documentation from doc to help 107 | - when typing `--help` option with task run it will provide documentation only for that task (`run sometask --help`) 108 | 109 | ## 3.3.0 110 | 111 | - migrating to `yarn` 112 | - removing task execution logging (decoration function) as it not working well with exporting pure functions 113 | - passing task options through `this.options` inside a task function 114 | 115 | ## 3.2.1 116 | 117 | - fixes within handling dashed arguments when calling tasks, dashed arguments can be "spaced" by "-" or "." now, for example: `--some-argument` or `--some.argument` (#49) 118 | 119 | ## 3.2.0 120 | 121 | - documenting tasks args when calling `run` without arguments 122 | - presenting list of available tasks from `runfile.js` in more readable way 123 | - passing `stdio` directly to `spawn` / `execSync` 124 | - changing run api where now it resolves/returns null by default and resolves/returns with value for option stdio: 'pipe'. This - allows to return colours to the terminal if provided by commands outcome. 125 | 126 | ## 3.1.1 127 | 128 | - Bug fix: pass `process.env` by default to `spawn` and `execSync` 129 | 130 | ## 3.1.0 131 | 132 | - fixes #43 `stderr` maxBuffer exceeded error (use `spawn` for `async` calls not `exec`) 133 | - `runfile.js` example update in README 134 | - drops support for `node` < 4.8.0 135 | 136 | ## 3.0.0 137 | 138 | - task name spacing/nesting, better for scaling tasks into many files 139 | - task descriptions 140 | - `ask` function 141 | - handling dash arguments in tasks (for example `--test`, `-t`) 142 | - logging tasks arguments to console when executing tasks 143 | - fixing exit codes when task not found 144 | - `run` function returns an output of a command now 145 | - improving documentation 146 | - deep code refactor, more unit tests 147 | 148 | ## 2.6.1 149 | 150 | - bugfix: streaming `stderr` also for `async` process 151 | 152 | ## 2.6.0 153 | 154 | - streaming output of an `async` command by default (`run` api function) 155 | 156 | ## 2.5.1 157 | 158 | - presenting straightforward message when no `runfile.js` found 159 | 160 | ## 2.5.0 161 | 162 | - bringing backwards compatibility with `node` >= 4.0.0 (previously `node` >= 6.0.0 required) 163 | - `run` command in `async` mode now returns a `Promise` 164 | 165 | ## 2.4.3 166 | 167 | - handling config from `package.json` to define a custom path to `babel-register` 168 | - executing async commands through `child_process.span` (better `stdio` handling) 169 | 170 | ## 2.4.0 171 | 172 | - removing watch method from api 173 | - RunJS will fallback to pure node now if user `babel-register` not found (falling back to it's own `babel-register` before) 174 | adding information to README: Why RunJS ? and other README update 175 | 176 | ## 2.3.0 177 | 178 | - dropping Babel 5 support 179 | - handling new `exports.default` after babel update 180 | 181 | ## 2.2.0 182 | 183 | - more explicit exceptions 184 | - handling existing `babel-register` or `babel/register` require hooks from the user package 185 | 186 | ## 2.1.0 187 | 188 | - new functions available as part of runjs api: `watch` and `generate` 189 | - broader README with extensive `runfile.js` example 190 | 191 | ## 2.0.0 192 | 193 | - dropping `es5` and `coffeescript` support in favor of `es6` (handled by babel) 194 | - dropping support for `node` < 4.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Paweł Gałązka 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tasksfile ![node version](https://img.shields.io/node/v/tasksfile.svg) [![Build Status](https://travis-ci.org/pawelgalazka/tasksfile.svg?branch=master)](https://travis-ci.org/pawelgalazka/tasksfile) [![npm version](https://badge.fury.io/js/tasksfile.svg)](https://badge.fury.io/js/tasksfile) 2 | 3 | Minimalistic building tool 4 | 5 | > From version >= 5 RunJS was renamed to Tasksfile. 6 | > Link to RunJS version: https://github.com/pawelgalazka/runjs/tree/runjs 7 | 8 | - [Get started](#get-started) 9 | - [Why tasksfile ?](#why-tasksfile-) 10 | - [Features](#features) 11 | - [Executing shell commands](#executing-shell-commands) 12 | - [Handling arguments](#handling-arguments) 13 | - [Documenting tasks](#documenting-tasks) 14 | - [Namespacing](#namespacing) 15 | - [Sharing tasks](#sharing-tasks) 16 | - [TypeScript support](#typescript-support) 17 | - [API](#api) 18 | - [sh](#shcmd-options) 19 | - [prefixTransform](#prefixTransformprefix) 20 | - [help](#helpfunc-description-annotation) 21 | - [rawArgs](#rawArgs) 22 | 23 | 24 | ## Get started 25 | 26 | Install tasksfile in your project 27 | 28 | npm install tasksfile --save-dev 29 | 30 | Create `tasksfile.js` in your root project directory: 31 | 32 | ```js 33 | const { sh, cli } = require('tasksfile') 34 | 35 | function hello(options, name = 'Mysterious') { 36 | console.log(`Hello ${name}!`) 37 | } 38 | 39 | function makedir() { 40 | sh('mkdir somedir') 41 | } 42 | 43 | cli({ 44 | hello, 45 | makedir 46 | }) 47 | ``` 48 | 49 | Create `task` entry in your `scripts` section in `package.json`: 50 | 51 | ```json 52 | { 53 | "scripts": { 54 | "task": "node ./tasksfile.js" 55 | } 56 | } 57 | ``` 58 | 59 | Call in your terminal through npm scripts: 60 | 61 | ```bash 62 | $ npm run task -- hello Tommy 63 | $ npm run task -- makedir 64 | $ yarn task hello Tommy 65 | $ yarn task makedir 66 | ``` 67 | 68 | or through shorter `npx task` alias: 69 | 70 | ```bash 71 | $ npx task hello Tommy 72 | Hello Tommy! 73 | $ npx task makedir 74 | mkdir somedir 75 | ``` 76 | 77 | 78 | ## Why tasksfile ? 79 | 80 | We have Grunt, Gulp, npm scripts, Makefile. Why another building tool ? 81 | 82 | Gulp or Grunt files seem overly complex for what they do and the plugin 83 | ecosystem adds a layer of complexity towards the simple command 84 | line tools underneath. The documentation is not always up to date 85 | and the plugin does not always use the latest version of the tool. 86 | After a while customizing the process even with simple things, 87 | reconfiguring it becomes time consuming. 88 | 89 | Npm scripts are simple but they get out of hand pretty quickly if 90 | we need more complex process which make them quite hard to read 91 | and manage. 92 | 93 | Makefiles are simple, better for more complex processes 94 | but they depend on bash scripting. Within `tasksfile` you can use 95 | command line calls as well as JavaScript code and npm 96 | libraries which makes that approach much more flexible. 97 | 98 | [More](https://hackernoon.com/simple-build-tools-npm-scripts-vs-makefile-vs-runjs-31e578278162) 99 | 100 | 101 | ## Features 102 | 103 | ### Executing shell commands 104 | 105 | Tasksfile gives an easy way to execute shell commands in your tasks by `sh` function 106 | in synchronous and asynchronous way: 107 | 108 | ```js 109 | const { sh, cli } = require('tasksfile') 110 | 111 | function command () { 112 | sh('jest') 113 | sh(`webpack-dev-server --config webpack.config.js`, { 114 | async: true 115 | }) 116 | } 117 | 118 | cli({ 119 | command 120 | }) 121 | ``` 122 | 123 | ```bash 124 | $ npx task command 125 | ``` 126 | 127 | Because `./node_modules/.bin` is included in `PATH` when calling shell commands 128 | by `sh` function, you can call "bins" from your local project in the same way as 129 | in npm scripts. 130 | 131 | ### Handling arguments 132 | 133 | Provided arguments in the command line are passed to the function: 134 | 135 | 136 | ```javascript 137 | function sayHello (options, who) { 138 | console.log(`Hello ${who}!`) 139 | } 140 | 141 | cli({ 142 | sayHello 143 | }) 144 | ``` 145 | 146 | ```bash 147 | $ npx task sayHello world 148 | Hello world! 149 | ``` 150 | 151 | You can also provide dash arguments like `-a` or `--test`. Order of them doesn't 152 | matter after task name. They will be always available by `options` helper 153 | from inside a function. 154 | 155 | ```javascript 156 | function sayHello (options, who) { 157 | console.log(`Hello ${who}!`) 158 | console.log('Given options:', options) 159 | } 160 | 161 | cli({ 162 | sayHello 163 | }) 164 | ``` 165 | 166 | ```bash 167 | $ npx task sayHello -a --test=something world 168 | Hello world! 169 | Given options: { a: true, test: 'something' } 170 | ``` 171 | 172 | 173 | ### Documenting tasks 174 | 175 | To display all available tasks for your `tasksfile.js` type `task` in your command line 176 | without any arguments: 177 | 178 | $ npx task --help 179 | 180 | Commands: 181 | 182 | echo - echo task description 183 | buildjs - Compile JS files 184 | 185 | Use `help` utility function for your task to get additional description: 186 | 187 | ```javascript 188 | const { cli, help } = require('tasksfile') 189 | 190 | function buildjs () { 191 | 192 | } 193 | 194 | help(buildjs, 'Compile JS files') 195 | 196 | cli({ 197 | buildjs 198 | }) 199 | ``` 200 | 201 | $ npx task buildjs --help 202 | Usage: buildjs 203 | 204 | Compile JS files 205 | 206 | You can provide detailed annotation to give even more info about the task: 207 | 208 | ```javascript 209 | const dedent = require('dedent') 210 | const { sh, help } = require('tasksfile') 211 | 212 | function test (options, file) { 213 | 214 | } 215 | 216 | help(test, 'Run unit tests', { 217 | params: ['file'], 218 | options: { 219 | watch: 'run tests in a watch mode' 220 | }, 221 | examples: dedent` 222 | task test dummyComponent.js 223 | task test dummyComponent.js --watch 224 | ` 225 | }) 226 | 227 | cli({ 228 | test 229 | }) 230 | ``` 231 | 232 | $ npx task test --help 233 | Usage: test [options] [file] 234 | 235 | Run unit tests 236 | 237 | Options: 238 | 239 | --watch run tests in a watch mode 240 | 241 | Examples: 242 | 243 | task test dummyComponent.js 244 | task test dummyComponent.js --watch 245 | 246 | 247 | ### Namespacing 248 | 249 | To better organise tasks, it is possible to call them from namespaces: 250 | ```js 251 | const test = { 252 | unit () { 253 | console.log('Doing unit testing!') 254 | } 255 | } 256 | 257 | cli({ 258 | test 259 | }) 260 | ``` 261 | 262 | ```bash 263 | $ npx task test:unit 264 | Doing unit testing! 265 | ``` 266 | 267 | This is especially useful if `tasksfile.js` gets too large. We can move some tasks 268 | to external modules and import them back to a namespace: 269 | 270 | `./tasks/test.js`: 271 | 272 | ```javascript 273 | function unit () { 274 | console.log('Doing unit testing!') 275 | } 276 | 277 | function integration () { 278 | console.log('Doing unit testing!') 279 | } 280 | 281 | function default() { 282 | unit() 283 | integration() 284 | } 285 | 286 | module.exports = { 287 | unit, 288 | integration, 289 | default 290 | } 291 | ``` 292 | 293 | `tasksfile.js` 294 | ```js 295 | const test = require('./tasks/test') 296 | 297 | cli({ 298 | test 299 | }) 300 | ``` 301 | 302 | ```bash 303 | $ npx task test:unit 304 | Doing unit testing! 305 | 306 | $ npx task test 307 | Doing unit testing! 308 | Doing integration testing! 309 | ``` 310 | 311 | If we don't want to put imported tasks into a namespace, we can always use spread 312 | operator: 313 | 314 | ```js 315 | cli({ 316 | ...test 317 | }) 318 | ``` 319 | 320 | ```bash 321 | $ npx task unit 322 | Doing unit testing! 323 | ``` 324 | 325 | With ES6 modules import/export syntax this becomes even simpler: 326 | 327 | ```js 328 | // export with no namespace 329 | export * from './tasks/test' // no namespace 330 | 331 | // export with namespace 332 | import * as test from './tasks/test' 333 | export { test } // add namespace 334 | ``` 335 | 336 | ```bash 337 | $ npx task unit 338 | $ npx task test:unit 339 | ``` 340 | 341 | ### Sharing tasks 342 | 343 | Because `tasksfile.js` is just a node.js module and `tasksfile` just calls exported 344 | functions from that module based on cli arguments, nothing stops you to move 345 | some repetitive tasks across your projects to external npm package and 346 | just reuse it. 347 | 348 | `shared-tasksfile` module: 349 | ```js 350 | function shared1 () { 351 | console.log('This task is shared!') 352 | } 353 | 354 | function shared2 () { 355 | console.log('This task is shared!') 356 | } 357 | 358 | module.exports = { 359 | shared1, 360 | shared2 361 | } 362 | ``` 363 | 364 | Local `tasksfile.js` 365 | ```js 366 | const shared = require('shared-tasksfile') 367 | 368 | function local () { 369 | console.log('This task is local!') 370 | } 371 | 372 | cli({ 373 | ...shared, 374 | local 375 | }) 376 | ``` 377 | 378 | ```bash 379 | $ npx task shared1 380 | $ npx task shared2 381 | $ npx task local 382 | ``` 383 | 384 | ### TypeScript support 385 | 386 | It's very easy to run your tasks in `TypeScript` if you have `TypeScript` already 387 | in your project. Just: 388 | 389 | - change your `tasksfile.js` to `tasksfile.ts` and adjust the code 390 | - install `ts-node`: `npm install --save-dev ts-node` 391 | - change command in your `package.json`: 392 | 393 | ```json 394 | { 395 | "scripts": { 396 | "task": "ts-node ./tasksfile.ts" 397 | } 398 | } 399 | ``` 400 | 401 | `Tasksfile` project already has `TypeScript` declaration files in source files. 402 | 403 | ## API 404 | 405 | For inside `tasksfile.js` usage. 406 | 407 | #### sh(cmd, options) 408 | 409 | Run given command as a child process and log the call in the output. 410 | `./node_modules/.bin/` is included into `PATH` so you can call installed scripts directly. 411 | 412 | Function will return output of executed command. 413 | 414 | ```js 415 | const { sh } = require('tasksfile') 416 | ``` 417 | 418 | *Options:* 419 | 420 | ```ts 421 | interface IShellOptions { 422 | // current working directory 423 | cwd?: string 424 | 425 | // environment key-value pairs 426 | env?: NodeJS.ProcessEnv 427 | 428 | // timeout after which execution will be cancelled 429 | timeout?: number 430 | 431 | // default: false, if true it runs command asynchronously and returns a Promise 432 | async?: boolean 433 | 434 | // if true, it will send output directly to parent process (stdio="inherit"), it won't return the output though 435 | // usefull if default piping strips too much colours when printing to the terminal 436 | // if enabled, transform option won't work 437 | nopipe?: boolean 438 | 439 | // if true, it won't print anything to the terminal but it will still return the output as a string 440 | silent?: boolean 441 | 442 | // function which allows to transform the output, line by line 443 | // usefull for adding prefixes to async commands output 444 | transform?: (output: string) => string 445 | } 446 | ``` 447 | 448 | #### prefixTransform(prefix) 449 | 450 | Transform function which can be used as `transform` option of `sh` function. 451 | It allows to add prefixes to shell output. 452 | 453 | *Example:* 454 | 455 | ```js 456 | const { cli, sh, prefixTransform } = require('tasksfile') 457 | 458 | function test() { 459 | sh('echo "test"', { 460 | transform: prefixTransform('[prefix]') 461 | }) 462 | } 463 | 464 | cli({ 465 | test 466 | }) 467 | ``` 468 | 469 | ```sh 470 | $ npx task test 471 | echo "test" 472 | [prefix] test 473 | ``` 474 | 475 | 476 | #### help(func, description, annotation) 477 | 478 | Define help annotation for task function, so it will be printed out when calling task with `--help` 479 | option and when calling `run` without any arguments. 480 | 481 | ```js 482 | const { help } = require('tasksfile') 483 | ``` 484 | 485 | 486 | ```javascript 487 | help(build, 'Generate JS bundle') 488 | 489 | help(test, 'Run unit tests', { 490 | params: ['file'], 491 | options: { 492 | watch: 'run tests in a watch mode' 493 | }, 494 | examples: ` 495 | task test dummyComponent.js 496 | task test dummyComponent.js --watch 497 | ` 498 | }) 499 | ``` 500 | 501 | $ npx task build --help 502 | $ npx task test --help 503 | 504 | 505 | #### rawArgs() 506 | 507 | Returns arguments / options passed to task in a raw, unparsed format. 508 | 509 | ```javascript 510 | const { cli, rawArgs } = require('tasksfile') 511 | 512 | function hello(options) { 513 | console.log('RAW ARGS', rawArgs()) 514 | } 515 | 516 | cli({ 517 | hello 518 | }) 519 | ``` 520 | 521 | ```sh 522 | $ npx task hello 1 2 3 --test 523 | RAW ARGS ['1', '2', '3', '--test'] 524 | ``` -------------------------------------------------------------------------------- /bin/task.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { execSync } = require('child_process') 3 | const path = require('path') 4 | const packageJson = require(path.resolve('./package.json')) 5 | 6 | const taskScript = packageJson.scripts.task 7 | 8 | try { 9 | execSync(`${taskScript} ${process.argv.slice(2).join(' ')}`, {shell: true, stdio: 'inherit'}) 10 | } catch (error) { 11 | process.exit(1) 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // A list of paths to directories that Jest should use to search for files in. 6 | roots: ["/src/", "/test/"], 7 | 8 | // The test environment that will be used for testing 9 | testEnvironment: "node", 10 | 11 | // A list of reporter names that Jest uses when writing coverage reports 12 | coverageReporters: [ 13 | "text", 14 | "text-summary" 15 | ], 16 | 17 | // An object that configures minimum threshold enforcement for coverage results 18 | coverageThreshold: { 19 | "global": { 20 | "lines": 30 21 | } 22 | } 23 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tasksfile", 3 | "version": "5.1.1", 4 | "description": "Minimalistic task runner for node.js", 5 | "keywords": [ 6 | "build", 7 | "system", 8 | "make", 9 | "tool" 10 | ], 11 | "main": "lib/index.js", 12 | "types": "lib/index.d.ts", 13 | "bin": { 14 | "task": "bin/task.js" 15 | }, 16 | "scripts": { 17 | "lint": "tslint -c tslint.json 'src/*.ts' 'test/**/*.ts'", 18 | "build": "tsc", 19 | "test": "yarn lint && yarn build && yarn clean:sandbox && jest --coverage", 20 | "test:unit": "jest ./src/", 21 | "test:e2e": "jest ./test/", 22 | "clean": "rm -rf node_modules && yarn clean:build && yarn clean:sandbox", 23 | "clean:build": "rm -rf ./lib", 24 | "clean:sandbox": "rm -rf ./test/e2e/sandbox/node_modules && mkdir -p ./test/sandbox/node_modules/.bin", 25 | "sandbox:task": "cd ./test/sandbox && ../../bin/task.js" 26 | }, 27 | "lint-staged": { 28 | "src/*.{ts,tsx}": [ 29 | "tslint --fix", 30 | "git add" 31 | ] 32 | }, 33 | "engines": { 34 | "node": ">=6.11.1" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/pawelgalazka/tasksfile.git" 39 | }, 40 | "author": "Pawel Galazka", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/pawelgalazka/tasksfile/issues" 44 | }, 45 | "homepage": "https://github.com/pawelgalazka/tasksfile#readme", 46 | "dependencies": { 47 | "@pawelgalazka/cli": "2.0.3", 48 | "@pawelgalazka/shell": "2.0.0", 49 | "chalk": "2.3.0" 50 | }, 51 | "devDependencies": { 52 | "@babel/core": "7.2.2", 53 | "@babel/preset-env": "7.3.1", 54 | "@babel/preset-typescript": "7.1.0", 55 | "@types/jest": "24.0.0", 56 | "@types/lodash.padend": "4.6.4", 57 | "@types/node": "10.12.18", 58 | "husky": "1.3.1", 59 | "jest": "24.1.0", 60 | "lint-staged": "8.1.0", 61 | "prettier": "1.15.3", 62 | "tslint": "5.12.1", 63 | "tslint-config-prettier": "1.17.0", 64 | "tslint-plugin-prettier": "2.0.1", 65 | "typescript": "3.2.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { shell } from '@pawelgalazka/shell' 2 | import chalk from 'chalk' 3 | 4 | import { sh } from './index' 5 | 6 | const shellMock = shell as jest.Mock 7 | 8 | jest.mock('@pawelgalazka/shell') 9 | 10 | process.env = { DEFAULT_ENV: 'default env' } 11 | 12 | describe('sh()', () => { 13 | let logger: any 14 | 15 | beforeEach(() => { 16 | jest.resetAllMocks() 17 | logger = { 18 | error: jest.fn(), 19 | log: jest.fn(), 20 | title: jest.fn(), 21 | warning: jest.fn() 22 | } 23 | }) 24 | 25 | it('calls original @pawelgalazka/shell function with the same command', () => { 26 | sh('test command', undefined, logger) 27 | expect(shellMock).toHaveBeenCalledTimes(1) 28 | expect(shellMock).toHaveBeenCalledWith('test command', expect.anything()) 29 | }) 30 | 31 | it('logs executed command', () => { 32 | sh('test command', undefined, logger) 33 | expect(logger.log).toHaveBeenCalledTimes(1) 34 | expect(logger.log).toHaveBeenCalledWith(chalk.bold('test command')) 35 | }) 36 | 37 | it('calls original @pawelgalazka/shell with default options values', () => { 38 | sh('test command', undefined, logger) 39 | expect(shellMock).toHaveBeenCalledTimes(1) 40 | expect(shellMock).toHaveBeenCalledWith(expect.anything(), { 41 | env: { 42 | DEFAULT_ENV: 'default env', 43 | PATH: expect.anything() 44 | } 45 | }) 46 | }) 47 | 48 | it('calls original @pawelgalazka/shell with given options values', () => { 49 | sh( 50 | 'test command', 51 | { 52 | async: true, 53 | cwd: 'cwd-dir', 54 | env: { CUSTOM_ENV: 'custom env' }, 55 | stdio: 'pipe', 56 | timeout: 1000 57 | }, 58 | logger 59 | ) 60 | expect(shellMock).toHaveBeenCalledTimes(1) 61 | expect(shellMock).toHaveBeenCalledWith(expect.anything(), { 62 | async: true, 63 | cwd: 'cwd-dir', 64 | env: { 65 | CUSTOM_ENV: 'custom env', 66 | PATH: expect.anything() 67 | }, 68 | stdio: 'pipe', 69 | timeout: 1000 70 | }) 71 | }) 72 | 73 | it('adds ./node_modules/.bin to $PATH', () => { 74 | sh('test command', undefined, logger) 75 | expect(shellMock).toHaveBeenCalledWith( 76 | expect.anything(), 77 | expect.objectContaining({ 78 | env: expect.objectContaining({ 79 | PATH: expect.stringContaining('node_modules/.bin:') 80 | }) 81 | }) 82 | ) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cli as cliEngine, 3 | CLIError, 4 | CommandsModule, 5 | Middleware, 6 | useMiddlewares 7 | } from '@pawelgalazka/cli' 8 | import { Logger } from '@pawelgalazka/cli/lib/utils/logger' 9 | import { 10 | IAsyncShellOptions, 11 | IShellOptions, 12 | ISyncShellOptions, 13 | shell, 14 | ShellError 15 | } from '@pawelgalazka/shell' 16 | import chalk from 'chalk' 17 | import path from 'path' 18 | 19 | export { help, rawArgs } from '@pawelgalazka/cli' 20 | export { prefixTransform } from '@pawelgalazka/shell' 21 | 22 | const commandNotFoundHandler: Middleware = next => args => { 23 | const { command } = args 24 | 25 | if (!command) { 26 | throw new CLIError( 27 | 'Command not found. Type "npx task --help" for more information.' 28 | ) 29 | } 30 | 31 | next(args) 32 | } 33 | const shellErrorHandler: ( 34 | logger: Logger 35 | ) => Middleware = logger => next => args => { 36 | const { reject } = args 37 | const nextReject = (error: Error) => { 38 | if (error instanceof ShellError) { 39 | logger.error(error.message) 40 | process.exit(1) 41 | } else { 42 | reject(error) 43 | } 44 | } 45 | try { 46 | next({ 47 | ...args, 48 | reject: nextReject 49 | }) 50 | } catch (error) { 51 | nextReject(error) 52 | } 53 | } 54 | 55 | export function sh( 56 | command: string, 57 | options: IAsyncShellOptions, 58 | logger?: Logger 59 | ): Promise 60 | 61 | export function sh( 62 | command: string, 63 | options?: ISyncShellOptions, 64 | logger?: Logger 65 | ): string | null 66 | 67 | export function sh( 68 | command: string, 69 | options: IShellOptions = {}, 70 | logger: Logger = new Logger() 71 | ) { 72 | const binPath = path.resolve('./node_modules/.bin') 73 | // Include in PATH node_modules bin path 74 | const nextPath = [ 75 | binPath, 76 | (options.env && options.env.PATH) || process.env.PATH 77 | ].join(path.delimiter) 78 | 79 | const nextOptions = { 80 | ...options, 81 | env: { 82 | ...(options.env || process.env), 83 | PATH: nextPath 84 | } 85 | } 86 | 87 | logger.log(chalk.bold(command)) 88 | 89 | return shell(command, nextOptions) 90 | } 91 | 92 | export function cli(definition: CommandsModule) { 93 | return cliEngine( 94 | definition, 95 | useMiddlewares([commandNotFoundHandler, shellErrorHandler(new Logger())]) 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /test/__snapshots__/e2e.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`tasksfile displays commands list if only --help option provided and no task name 1`] = ` 4 | " 5 | Commands: 6 | 7 | asyncAwait 8 | color 9 | echo - Simple echo task 10 | error 11 | errorAsyncAwait 12 | nested 13 | nested:echo [p1 p2] - Description of nested task 14 | npmBin 15 | shell 16 | " 17 | `; 18 | -------------------------------------------------------------------------------- /test/e2e.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { execSync as orgExecSync } from 'child_process' 3 | 4 | describe('tasksfile', () => { 5 | const scriptPath = '../../bin/task.js' 6 | function execSync(cmd: string) { 7 | return orgExecSync(cmd, { 8 | cwd: './test/sandbox', 9 | env: { 10 | ...process.env, 11 | FORCE_COLOR: '0' 12 | }, 13 | stdio: 'pipe' 14 | }).toString() 15 | } 16 | 17 | it('executes simple task', () => { 18 | expect(execSync(`${scriptPath} echo 1 2 3 --foo --bar`)).toEqual( 19 | "echo [ { foo: true, bar: true }, '1', '2', '3' ]\n" 20 | ) 21 | }) 22 | 23 | it('executes shell commands in a task', () => { 24 | const output = execSync(`${scriptPath} shell`) 25 | expect(output).toContain( 26 | 'echo "sync terminal"\nsync terminal\necho "sync silent"\noutput sync silent' 27 | ) 28 | expect(output).toContain('\nasync terminal\n') 29 | expect(output).toContain('\noutput async silent\n') 30 | }) 31 | 32 | it('executes task from a namespace', () => { 33 | expect(execSync(`${scriptPath} nested:echo 1 2 3 --foo --bar`)).toEqual( 34 | "nested echo [ { foo: true, bar: true }, '1', '2', '3' ]\n" 35 | ) 36 | }) 37 | 38 | it('executes default task from a namespace', () => { 39 | expect(execSync(`${scriptPath} nested 1 2 3 --foo --bar`)).toEqual( 40 | "nested default [ { foo: true, bar: true }, '1', '2', '3' ]\n" 41 | ) 42 | }) 43 | 44 | it('includes ./node_modules/.bin to PATH when executing bin scripts', () => { 45 | execSync('cp -p ./scripts/hello.js ./node_modules/.bin/hello') 46 | expect(execSync(`${scriptPath} npmBin`)).toEqual('hello\nHello!\n') 47 | }) 48 | 49 | it('executes async/await task', () => { 50 | expect(execSync(`${scriptPath} asyncAwait`)).toContain( 51 | 'echo "async and await"\noutput async and await\n\nafter await\n' 52 | ) 53 | }) 54 | 55 | it('displays help for a task', () => { 56 | expect(execSync(`${scriptPath} echo --help`)).toEqual( 57 | 'Usage: echo \n\nSimple echo task\n\n' 58 | ) 59 | }) 60 | 61 | it('displays detailed help', () => { 62 | expect(execSync(`${scriptPath} nested:echo --help`)).toEqual( 63 | 'Usage: nested:echo [options] [p1 p2]\n\nDescription of nested task\n\n' + 64 | 'Options:\n\n --foo foo option description\n' + 65 | ' --bar bar option description\n' 66 | ) 67 | }) 68 | 69 | it('displays error from executed task', () => { 70 | expect(() => execSync(`${scriptPath} error`)).toThrow( 71 | `Command failed: ${scriptPath} error` 72 | ) 73 | }) 74 | 75 | it('displays error from executed async task', () => { 76 | expect(() => execSync(`${scriptPath} errorAsyncAwait`)).toThrow( 77 | `Command failed: ${scriptPath} errorAsyncAwait` 78 | ) 79 | }) 80 | 81 | it('displays commands list if only --help option provided and no task name', () => { 82 | expect(execSync(`${scriptPath} --help`)).toMatchSnapshot() 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /test/sandbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sandbox", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "task": "node ./tasksfile.js" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/sandbox/scripts/color.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | 3 | console.log(chalk.yellow('This should be in yellow color')) 4 | -------------------------------------------------------------------------------- /test/sandbox/scripts/error.js: -------------------------------------------------------------------------------- 1 | throw new Error('error message') 2 | -------------------------------------------------------------------------------- /test/sandbox/scripts/hello.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log('Hello!') 4 | -------------------------------------------------------------------------------- /test/sandbox/tasksfile.js: -------------------------------------------------------------------------------- 1 | const { sh, help, cli } = require('../../lib') 2 | 3 | help(echo, 'Simple echo task') 4 | 5 | function echo(...args) { 6 | console.log('echo', args) 7 | } 8 | 9 | const nested = { 10 | echo(...args) { 11 | console.log('nested echo', args) 12 | }, 13 | 14 | default (...args) { 15 | console.log('nested default', args) 16 | } 17 | } 18 | 19 | help(nested.echo, 'Description of nested task', { 20 | options: { 21 | foo: 'foo option description', 22 | bar: 'bar option description' 23 | }, 24 | params: ['p1', 'p2'] 25 | }) 26 | 27 | function shell() { 28 | sh('echo "sync terminal"') 29 | console.log('output', sh('echo "sync silent"', {silent: true})) 30 | sh('echo "async terminal"', { async: true }) 31 | sh('echo "async silent"', { async: true, silent: true}).then(output => 32 | console.log('output', output) 33 | ) 34 | } 35 | 36 | function npmBin() { 37 | sh('hello') 38 | } 39 | 40 | async function asyncAwait() { 41 | const output = await sh('echo "async and await"', { 42 | async: true, 43 | silent: true 44 | }) 45 | console.log('output', output) 46 | console.log('after await') 47 | } 48 | 49 | function error() { 50 | sh('node ./scripts/error.js', { async: true }) 51 | sh('node ./scripts/error.js') 52 | } 53 | 54 | async function errorAsyncAwait() { 55 | await Promise.reject(new Error('async await error')) 56 | } 57 | 58 | function color() { 59 | sh('node ./scripts/color.js') 60 | sh('node ./scripts/color.js', { async: true }) 61 | } 62 | 63 | 64 | cli({ 65 | echo, 66 | shell, 67 | nested, 68 | npmBin, 69 | asyncAwait, 70 | errorAsyncAwait, 71 | error, 72 | color 73 | }) 74 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["es2017"], /* Specify library files to be included in the compilation. */ 7 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 8 | "sourceMap": true, /* Generates corresponding '.map' file. */ 9 | "outDir": "./lib", /* Redirect output structure to the directory. */ 10 | 11 | /* Strict Type-Checking Options */ 12 | "strict": true, /* Enable all strict type-checking options. */ 13 | 14 | /* Additional Checks */ 15 | "noUnusedLocals": true, /* Report errors on unused locals. */ 16 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 17 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 18 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 19 | 20 | /* Module Resolution Options */ 21 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 22 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 23 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 24 | }, 25 | "include": [ 26 | "src/**/*" 27 | ], 28 | "exclude": [ 29 | "**/*.spec.ts" 30 | ] 31 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest", "tslint-config-prettier" 4 | ], 5 | "rules": { 6 | "prettier": true, 7 | "max-classes-per-file": false, 8 | "no-console": false, 9 | "no-empty": false, 10 | "no-shadowed-variable": false, 11 | "no-submodule-imports": false 12 | }, 13 | "rulesDirectory": ["tslint-plugin-prettier"] 14 | } --------------------------------------------------------------------------------