├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── doc ├── npm-launch-package.json.gif └── npm-launch.gif ├── launch.scripts.json5 ├── lib ├── cli.js ├── index.js ├── loader.js ├── loaders │ ├── _babel.js │ ├── js.js │ ├── json.js │ └── test │ │ ├── js.test.js │ │ └── json.test.js ├── task │ ├── classes.js │ ├── index.js │ └── registry.js └── util.js ├── package.json └── test ├── fixtures ├── launch.scripts.js └── launch.scripts.json5 └── functional.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 5 5 | - 4 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # npm-launch - Changelog 2 | 3 | This is the brief changelog. For more details see https://github.com/andywer/npm-launch/releases. 4 | 5 | ## v0.3.0 6 | 7 | - Show last (non-empty) line of command output: npm-launch/issues#5 8 | - Minor fix: Prevent unhelpful stack trace: npm-launch/issues#6 9 | - Minor fix: Set process title to `npm-launch` (defaulted to `node` until now) 10 | - Minor fix: Add `engines` prop to `package.json` 11 | 12 | ## v0.2.0 13 | 14 | - Support for running tasks concurrently using `run-p`/`run-parallel` 15 | 16 | ## v0.1.4 17 | 18 | - CLI option: `--list` 19 | 20 | ## v0.1.3 21 | 22 | - Bug fix: Babel transformation fix (concerns JS launch files only, not JSON) 23 | 24 | ## v0.1.2 25 | 26 | - Add unit and functional tests 27 | - Bug fix: Exit with status code 1 if there is an error 28 | 29 | ## v0.1.1 30 | 31 | - Show the currently run shell command for JSON tasks 32 | 33 | ## v0.1.0 34 | 35 | - Initial release. Yeah! 36 | - Supports JSON5 and ES6 modules 37 | - Uses Listr 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andy Wermke 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 npm-launch 2 | 3 | [![Build Status](https://travis-ci.org/andywer/npm-launch.svg?branch=master)](https://travis-ci.org/andywer/npm-launch) 4 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 5 | [![NPM Version](https://img.shields.io/npm/v/npm-launch.svg)](https://www.npmjs.com/package/npm-launch) 6 | 7 | Minimalistic task runner on steroids. Write scripts using JSON5 or ES6 JavaScript modules. 8 | 9 |

10 | Screencast 11 |

12 | 13 | - Small, fast, supports JSON5 and JS code 14 | - Clean and tidy: Show command's output only in case of error 15 | - Fully compatible with package.json `scripts`: You can just copy & paste them to the launch file 16 | - Runs locally installed commands from `node_modules/.bin` like package.json `scripts` 17 | - Uses [Listr](https://github.com/SamVerschueren/listr) to provide beautiful console output 18 | - Runs on node.js 4+ 19 | 20 | 21 | ## Installation 22 | 23 | ```sh 24 | npm install --save-dev npm-launch 25 | ``` 26 | 27 | ## Why? 28 | 29 | - Because `scripts` in package.json are a mess 30 | - When they fail, `npm` prints ~25 lines of non-helpful disclaimers 31 | - If a script called by another script fails, you already got 50 lines to scroll 32 | - `gulp` & `grunt` instead need 100 lines of code for things that take less than five lines on the command line 33 | - Your terminal is constantly flooded with output everytime you run a task 34 | 35 | But no more! Let's take the sample launch file from the screencast before and use 36 | it in our `package.json`: 37 | 38 |

39 | Screencast 40 |

41 | 42 | Here we go, everything is clean and concise now! 43 | 44 | **A clean and short package.json. Commented tasks. Concurrency support out-of-the-box.** 45 | 46 | 47 | ## Usage (JSON) 48 | 49 | ```js 50 | // File: launch.scripts.json5 51 | { 52 | build: "webpack -c webpack-config.production.js", 53 | test: "run-parallel lint mocha", 54 | 55 | ////////// 56 | // Hooks: 57 | 58 | prepush: "run build && run test", 59 | 60 | ////////////////// 61 | // Testing tasks: 62 | 63 | lint: "standard lib/**/*.js", 64 | mocha: "mocha" 65 | } 66 | ``` 67 | 68 | ```sh 69 | $ launch build test # run tasks "build" and "test" 70 | # or 71 | $ launch -f path/to/launch-file build test 72 | ``` 73 | 74 | Features: 75 | - Comments in JSON 76 | - Run tasks concurrently by using `run-parallel ...` (or short `run-p`) 77 | - Nicer syntax, easy to read and write 78 | - Very short and concise 79 | - Fully compatible with standard JSON 80 | 81 | 82 | ## Usage (ES6 module) 83 | 84 | ```js 85 | // File: launch.scripts.js 86 | 87 | const shell = module.parent.exports.shell 88 | const delay = 1500 89 | 90 | // Define a task "npmPruneList" 91 | export async function npmPruneList () { 92 | // Execute `npm prune` 93 | await shell('npm prune') 94 | // Then execute `npm list` 95 | await shell('npm list') 96 | } 97 | 98 | // Define a task "wasteSomeTime" 99 | export function wasteSomeTime () { 100 | return new Promise((resolve) => { 101 | setTimeout(() => resolve(), delay) 102 | }) 103 | } 104 | 105 | // Define a task "default" that will just run "npmPruneList" & "wasteSomeTime" sequentially 106 | export default [ npmPruneList, wasteSomeTime ] 107 | ``` 108 | 109 | Features: 110 | - Completely transparent API: No need to import anything, just export 111 | - ES6 module syntax is available out-of-the-box 112 | - `async`/`await` are available out-of-the-box 113 | - The additional power you need to solve complex tasks 114 | 115 | 116 | ## CLI 117 | 118 | Run the tasks `build` and `test`: 119 | ```sh 120 | launch build test 121 | ``` 122 | 123 | Run the `default` task: 124 | ```sh 125 | launch 126 | ``` 127 | 128 | List all available tasks: 129 | ```sh 130 | launch --list 131 | ``` 132 | 133 | Print CLI usage help text: 134 | ```sh 135 | launch --help 136 | ``` 137 | 138 | 139 | ## Tips 140 | 141 | - If you do not provide a filename for a launch file it will look for a file named `launch.scripts.js`/`launch.scripts.json`/`launch.scripts.json5` 142 | - Auto-camelcasing: Instead of `$ launch myTask` you can also run `$ launch my-task` 143 | 144 | 145 | ## Minor known limitations 146 | 147 | These limitations only apply if your launch file contains custom JS code rather than JSON tasks. 148 | 149 | #### No checkmark list for tasks being called by code 150 | 151 | Tasks called by code are tracked and displayed, but not as a checkmark list, 152 | but just as a hint which one is currently run. It's simple: In this case we 153 | cannot create a list of subtasks beforehand, since there is no way to know 154 | which sub-tasks the function is going to call. 155 | 156 | #### console.log() in tasks may disturb the output 157 | 158 | If you call `console.log()` (or similar) in your launch file then the checkmark 159 | list will probably be corrupted. 160 | 161 | 162 | ## Changelog 163 | 164 | See [CHANGELOG.md](./CHANGELOG.md) or [Releases Page](https://github.com/andywer/npm-launch/releases) for more details. 165 | 166 | 167 | ## License 168 | 169 | This library is released under the terms of the MIT license. See [LICENSE](./LICENSE) for details. 170 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - CLI option to not use Listr and write all console output to terminal 4 | (--plain-output || !require('tty').isatty(process.stdout)) 5 | (see https://github.com/SamVerschueren/listr/issues/11) 6 | - Come up with some way to let tasks print a tail of n lines to terminal when all is done 7 | (Maybe just parse `| tail -n ` at the end of commands) 8 | - Fix: `babel-register` is currently run with `{ babelrc: false }`, since otherwise a local `.babelrc` would override our babel plugins config 9 | - Implement a way to run stuff in parallel in JSON 10 | - Show full stack traces on errors thrown by npm-launch itself only if a `verbose` flag is set 11 | - Run JS code in sub process to prevent stdout/stderr to be written to unfiltered (breaks Listr output) 12 | - Maybe: Allow methods to return an array of task names to execute 13 | -------------------------------------------------------------------------------- /doc/npm-launch-package.json.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andywer/npm-launch/a192fe3696cba23c14854d43a1550f81f1dff356/doc/npm-launch-package.json.gif -------------------------------------------------------------------------------- /doc/npm-launch.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andywer/npm-launch/a192fe3696cba23c14854d43a1550f81f1dff356/doc/npm-launch.gif -------------------------------------------------------------------------------- /launch.scripts.json5: -------------------------------------------------------------------------------- 1 | { 2 | test: "run-p ava lint", 3 | 4 | ava: "ava {lib,test}/**/*.test.js", 5 | lint: "standard lib/*.js lib/**/*.js" 6 | } 7 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | const colors = require('colors') 5 | const EventEmitter = require('events') 6 | const fs = require('fs') 7 | 8 | const argv = require('yargs') 9 | .usage('Usage: $0 [ ...] []') 10 | .example('$0', 'Runs task "default" from file "launch.scripts.{js,json,json5}".') 11 | .example('$0 build test', 'Runs task "build" and then "test".') 12 | .example('$0 -f filename.js foo', 'Runs task "foo" of task file "filename.js".') 13 | .example('$0 --list', 'Lists all available tasks.') 14 | .describe('f', 'Specify input file') 15 | .nargs('f', 1) 16 | .alias('f', 'file') 17 | .describe('list', 'List all available tasks') 18 | .describe('no-color', 'Prevents output from being colored') // require('colors') will check for --no-color 19 | .help() 20 | .alias('h', 'help') 21 | .version() 22 | .argv 23 | 24 | const loader = require('./loader') 25 | const taskRunner = require('./index') 26 | 27 | const DEFAULT_INPUT_FILENAMES = [ 'launch.scripts.js', 'launch.scripts.json', 'launch.scripts.json5' ] 28 | 29 | process.title = 'npm-launch' 30 | 31 | try { 32 | main(argv) 33 | } catch (error) { 34 | console.error(error.stack) 35 | } 36 | 37 | function main (args) { 38 | const taskFile = args.file ? checkTasksFile(args.file) : locateTasksFile() 39 | const tasksToRun = args._.length > 0 ? args._ : [ 'default' ] 40 | 41 | // This emitter will be used to inform subscribers about calls on methods 42 | // if the taskFile is an ES6 module (JS code) 43 | const taskCallEmitter = new EventEmitter() 44 | 45 | const tasks = loader.loadTasksFromFile(taskFile, taskCallEmitter) 46 | beforeRunningTasks(args, tasks, taskFile) 47 | runTasks(tasks, taskCallEmitter, tasksToRun) 48 | } 49 | 50 | function checkTasksFile (filePath) { 51 | if (!fileExists(filePath)) { 52 | throw new Error(`File cannot be found: ${filePath}`) 53 | } 54 | return filePath 55 | } 56 | 57 | function locateTasksFile (filePath) { 58 | const located = DEFAULT_INPUT_FILENAMES.find((fileName) => fileExists(fileName)) 59 | 60 | if (located) { 61 | return located 62 | } else { 63 | throw new Error('Non of the default task files found in current working directory: ' + DEFAULT_INPUT_FILENAMES.join(', ')) 64 | } 65 | } 66 | 67 | function beforeRunningTasks (args, tasks, taskFile) { 68 | if (args.list) { 69 | printAvailableTasks(tasks, taskFile) 70 | process.exit(0) 71 | } 72 | } 73 | 74 | function printAvailableTasks (tasks, taskFile) { 75 | const taskNames = Object.keys(tasks) 76 | 77 | console.log(`Tasks in ${taskFile}:`) 78 | taskNames.sort().forEach((taskName) => console.log(` ${taskName}`)) 79 | } 80 | 81 | function runTasks (allTasks, taskCallEmitter, taskNamesToRun) { 82 | return taskRunner 83 | .runTasks(allTasks, taskNamesToRun, taskCallEmitter) 84 | .catch((error) => { 85 | if (error instanceof Error && 'stdout' in error && 'stderr' in error) { 86 | console.error(colors.red(error.message)) 87 | } else if (error instanceof Error) { 88 | console.error(colors.red(error.stack)) 89 | } else { 90 | console.error(colors.red(error)) 91 | } 92 | process.exit(1) 93 | }) 94 | } 95 | 96 | function fileExists (filePath) { 97 | let stat 98 | try { 99 | stat = fs.statSync(filePath) 100 | } catch (error) { 101 | return false 102 | } 103 | return stat.isFile() 104 | } 105 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const camelcase = require('camelcase') 2 | const Observable = require('zen-observable') 3 | const createTaskFork = require('./task').createFork 4 | 5 | /** 6 | * @param {object} allTasks { : } 7 | * @param {Array} taskNamesToRun 8 | * @param {EventEmitter} taskCallEmitter 9 | * @return {Promise} 10 | */ 11 | function runTasks (allTasks, taskNamesToRun, taskCallEmitter) { 12 | const tasksToRun = selectTasks(allTasks, taskNamesToRun) 13 | 14 | const taskCalls = new Observable((observer) => { 15 | taskCallEmitter.on('call', (taskName) => observer.next(taskName)) 16 | }) 17 | 18 | const rootTask = createTaskFork('$root', tasksToRun) 19 | const listrRoot = rootTask.createListrTask(taskCalls).task() 20 | 21 | return listrRoot.run() 22 | } 23 | 24 | function selectTasks (tasks, taskNamesToRun) { 25 | const filteredTasks = [] 26 | 27 | taskNamesToRun.forEach((taskName) => { 28 | const camelCasedTaskName = camelcase(taskName) 29 | 30 | if (tasks[ taskName ]) { 31 | filteredTasks.push(tasks[ taskName ]) 32 | } else if (tasks[ camelCasedTaskName ]) { 33 | filteredTasks.push(tasks[ camelCasedTaskName ]) 34 | } else { 35 | throw new Error(`Task is not defined: ${taskName}`) 36 | } 37 | }) 38 | 39 | return filteredTasks 40 | } 41 | 42 | exports.runTasks = runTasks 43 | -------------------------------------------------------------------------------- /lib/loader.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function loadTasksFromFile (filePath, taskCallEmitter) { 4 | // Moved the loaders into here, since especially the JS loader is pretty expensive 5 | 6 | if (filePath.match(/\.json5?$/i)) { 7 | const loadTasksFromJsonFile = require('./loaders/json').loadFile 8 | return loadTasksFromJsonFile(filePath, taskCallEmitter) 9 | } else { 10 | const loadTasksFromJsFile = require('./loaders/js').loadFile 11 | return loadTasksFromJsFile(filePath, taskCallEmitter) 12 | } 13 | } 14 | 15 | exports.loadTasksFromFile = loadTasksFromFile 16 | -------------------------------------------------------------------------------- /lib/loaders/_babel.js: -------------------------------------------------------------------------------- 1 | exports.plugins = [ 2 | 'rewire', 3 | 'transform-async-to-generator', 4 | 'transform-es2015-modules-commonjs' 5 | ] 6 | -------------------------------------------------------------------------------- /lib/loaders/js.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const babelPlugins = require('./_babel').plugins 5 | const taskModule = require('../task') 6 | const util = require('../util') 7 | 8 | const createTaskFromMethod = taskModule.createFromMethod 9 | const createAnonymousCommandTask = taskModule.createAnonymousCommand 10 | const createTaskFork = taskModule.createFork 11 | const createTaskReference = taskModule.createReference 12 | 13 | require('babel-register')({ 14 | babelrc: false, 15 | plugins: babelPlugins 16 | }) 17 | 18 | function loadFile (filePath, taskCallEmitter) { 19 | if (filePath.charAt(0) !== '/' && filePath.charAt(0) !== '.') { 20 | filePath = path.join(process.cwd(), filePath) 21 | } 22 | 23 | const exports = require(filePath) 24 | 25 | observeExports(exports, taskCallEmitter) 26 | return createTasksForExports(exports) 27 | } 28 | 29 | function observeExports (exportedTasks, taskCallEmitter) { 30 | if (!exportedTasks.__Rewire__) { 31 | return exportedTasks 32 | } 33 | 34 | Object.keys(exportedTasks) 35 | .filter((taskName) => typeof exportedTasks[ taskName ] === 'function' && !taskName.match(/^__/)) 36 | .forEach((taskName) => { 37 | const origMethod = exportedTasks[ taskName ] 38 | 39 | exportedTasks.__Rewire__(taskName, function () { 40 | taskCallEmitter.emit('call', taskName) 41 | return origMethod.apply(null, arguments) 42 | }) 43 | }) 44 | } 45 | 46 | function createTasksForExports (exportedTasks) { 47 | const tasks = {} 48 | 49 | Object.keys(exportedTasks) 50 | .filter((taskName) => typeof exportedTasks[ taskName ] === 'function' && !taskName.match(/^__/)) 51 | .forEach((taskName) => { 52 | tasks[ taskName ] = createTaskFromMethod(taskName, exportedTasks[ taskName ]) 53 | }) 54 | 55 | Object.keys(exportedTasks) 56 | .filter((taskName) => Array.isArray(exportedTasks[ taskName ])) 57 | .forEach((taskName) => { 58 | const subTasks = exportedTasks[ taskName ].map((item) => createTaskReferenceForArrayItem(item, taskName)) 59 | tasks[ taskName ] = createTaskFork(taskName, subTasks) 60 | }) 61 | 62 | return tasks 63 | } 64 | 65 | function createTaskReferenceForArrayItem (item, arrayTaskName) { 66 | switch (typeof item) { 67 | case 'function': 68 | return createAnonymousCommandTask(item.name, item) 69 | case 'string': 70 | return createTaskReference(item, arrayTaskName) 71 | default: 72 | throw new Error(`Tasks defined as array must include string and function items only. Check "${arrayTaskName}".`) 73 | } 74 | } 75 | 76 | exports.loadFile = loadFile 77 | 78 | // This export is supposed to be used by ES6 module input files: 79 | exports.shell = util.shell 80 | -------------------------------------------------------------------------------- /lib/loaders/json.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const JSON5 = require('json5') 5 | const taskModule = require('../task') 6 | const util = require('../util') 7 | let shellObservable = util.shellObservable // must be `let`, since rewiring in unit tests would fail otherwise 8 | 9 | const createAnonymousCommandTask = taskModule.createAnonymousCommand 10 | const createTaskFork = taskModule.createFork 11 | const createAnonymousConcurrentTaskFork = taskModule.createAnonymousConcurrentTaskFork 12 | const createTaskReference = taskModule.createReference 13 | 14 | function loadFile (filePath) { 15 | const tasksJson = JSON5.parse(fs.readFileSync(filePath, { encoding: 'utf-8' })) 16 | const tasksResolved = {} 17 | 18 | Object.keys(tasksJson).forEach((taskName) => { 19 | tasksResolved[ taskName ] = resolveJsonTask(tasksJson[ taskName ], taskName, tasksResolved) 20 | }) 21 | 22 | return tasksResolved 23 | } 24 | 25 | function resolveJsonTask (task, taskName, tasksResolved) { 26 | if (Array.isArray(task)) { 27 | const taskReferences = task.map((subTaskName) => createTaskReference(subTaskName, taskName)) 28 | return createTaskFork(taskName, taskReferences) 29 | } 30 | if (typeof task !== 'string') { 31 | throw new Error(`Task must be a string or array. Invalid task given: ${taskName} (type ${typeof task})`) 32 | } 33 | 34 | return createTaskForTaskString(task, taskName) 35 | } 36 | 37 | function createTaskForTaskString (taskContent, taskName) { 38 | const subCommands = taskContent.split('&&').map((subCommand) => subCommand.trim()) 39 | const subCommandTasks = subCommands.map((subCommand) => createTaskForSubCommand(subCommand, taskName)) 40 | 41 | return createTaskFork(taskName, subCommandTasks) 42 | } 43 | 44 | function createTaskForSubCommand (subCommand, taskName) { 45 | const commandRun = /^run /i 46 | const commandRunParallel = /^(run-p|run-parallel) /i 47 | 48 | if (subCommand.match(commandRun)) { 49 | const referencedTaskName = subCommand.replace(commandRun, '').trim() 50 | return createTaskReference(referencedTaskName, taskName) 51 | } else if (subCommand.match(commandRunParallel)) { 52 | const spaceSeparatedSubTasks = subCommand.replace(commandRunParallel, '') 53 | const title = subCommand.length > 30 ? subCommand.substr(0, 30) + '...' : subCommand 54 | return createAnonymousConcurrencyTask(spaceSeparatedSubTasks, taskName, title) 55 | } else { 56 | const title = subCommand.replace(/\s.*$/, '') 57 | return createAnonymousCommandTask(title, () => shellObservable(subCommand)) 58 | } 59 | } 60 | 61 | function createAnonymousConcurrencyTask (spaceSeparatedSubTasks, taskName, title) { 62 | const referencedTaskNames = spaceSeparatedSubTasks 63 | .split(' ') 64 | .map((subTaskName) => subTaskName.trim()) 65 | .filter((subTaskName) => subTaskName.length > 0) 66 | 67 | const taskReferences = referencedTaskNames.map( 68 | (subTaskName) => createTaskReference(subTaskName, taskName) 69 | ) 70 | return createAnonymousConcurrentTaskFork(taskName, title, taskReferences) 71 | } 72 | 73 | exports.loadFile = loadFile 74 | -------------------------------------------------------------------------------- /lib/loaders/test/js.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava').test 2 | const path = require('path') 3 | const rewire = require('rewire') 4 | const sinon = require('sinon') 5 | const EventEmitter = require('events') 6 | 7 | const jsLoader = rewire('../js') 8 | const taskClasses = require('../../task/classes') 9 | 10 | const FIXTURE_FILE_PATH = path.join(__dirname, '../../../test/fixtures/launch.scripts.js') 11 | 12 | function take (thing, callback) { 13 | callback(thing) 14 | } 15 | 16 | test('loading JS file', (t) => { 17 | const taskCallEmitter = new EventEmitter() 18 | const tasks = jsLoader.loadFile(FIXTURE_FILE_PATH, taskCallEmitter) 19 | 20 | t.deepEqual( 21 | Object.keys(tasks).sort(), 22 | [ 'default', 'fancyTasking', 'npmList', 'runStandardLinter', 'wasteSomeTime', 'willFail' ] 23 | ) 24 | 25 | /////////////////// 26 | // Task "default": 27 | take(tasks.default, (task) => { 28 | t.true(task instanceof taskClasses.TaskFork) 29 | t.is(task.name, 'default') 30 | t.is(task.children.length, 3) 31 | }) 32 | 33 | take(tasks.default.children[0], (subTask) => { 34 | t.true(subTask instanceof taskClasses.AnonymousCommandTask) 35 | t.is(subTask.title, 'npmList') 36 | }) 37 | 38 | //////////////////////// 39 | // Task "fancyTasking": 40 | t.true(tasks.fancyTasking instanceof taskClasses.TaskLeaf) 41 | t.is(tasks.fancyTasking.name, 'fancyTasking') 42 | 43 | /////////////////// 44 | // Task "npmList": 45 | t.true(tasks.npmList instanceof taskClasses.TaskLeaf) 46 | t.is(tasks.npmList.name, 'npmList') 47 | 48 | ///////////////////////////// 49 | // Task "runStandardLinter": 50 | t.true(tasks.runStandardLinter instanceof taskClasses.TaskLeaf) 51 | t.is(tasks.runStandardLinter.name, 'runStandardLinter') 52 | 53 | ///////////////////////// 54 | // Task "wasteSomeTime": 55 | t.true(tasks.wasteSomeTime instanceof taskClasses.TaskLeaf) 56 | t.is(tasks.wasteSomeTime.name, 'wasteSomeTime') 57 | 58 | //////////////////// 59 | // Task "willFail": 60 | t.true(tasks.willFail instanceof taskClasses.TaskLeaf) 61 | t.is(tasks.willFail.name, 'willFail') 62 | }) 63 | 64 | test('observeExports() works, calls cause taskCallEmitter events', (t) => { 65 | const fakeTasks = { 66 | __Rewire__, 67 | fakeTask (...args) { 68 | return 'args: ' + args.join(', ') 69 | }, 70 | shouldNotBeTouched: 'some string' 71 | } 72 | 73 | function __Rewire__ (taskName, newMethod) { 74 | fakeTasks[ taskName ] = newMethod 75 | } 76 | 77 | const taskCallEmitter = new EventEmitter() 78 | const calls = [] 79 | taskCallEmitter.on('call', (methodName) => calls.push(methodName)) 80 | 81 | jsLoader.__get__('observeExports')(fakeTasks, taskCallEmitter) 82 | t.is(calls.length, 0) 83 | 84 | const returnValue = fakeTasks.fakeTask('foo', 'bar') 85 | t.deepEqual(calls, [ 'fakeTask' ]) 86 | t.is(returnValue, 'args: foo, bar') 87 | 88 | t.is(fakeTasks.shouldNotBeTouched, 'some string') 89 | }) 90 | 91 | test('task instances are created correctly', (t) => { 92 | const method = sinon.spy(() => 'method called') 93 | 94 | const fakeTasks = { 95 | method, 96 | arrayOfMethods: [ method ], 97 | arrayOfStrings: [ 'method' ] 98 | } 99 | 100 | const createdTasks = jsLoader.__get__('createTasksForExports')(fakeTasks) 101 | 102 | //////////////////// 103 | // fakeTask.method: 104 | take(createdTasks.method, (task) => { 105 | t.true(task instanceof taskClasses.TaskLeaf) 106 | t.is(task.name, 'method') 107 | t.is(task.title, 'method') 108 | t.false(method.called) 109 | t.is(task.method(), 'method called') 110 | t.true(method.called) 111 | }) 112 | 113 | //////////////////////////// 114 | // fakeTask.arrayOfMethods: 115 | take(createdTasks.arrayOfMethods, (task) => { 116 | t.true(task instanceof taskClasses.TaskFork) 117 | t.is(task.name, 'arrayOfMethods') 118 | t.is(task.title, 'arrayOfMethods') 119 | t.is(task.children.length, 1) 120 | }) 121 | 122 | take(createdTasks.arrayOfMethods.children[0], (task) => { 123 | t.true(task instanceof taskClasses.AnonymousCommandTask) 124 | t.is(task.title, 'proxy') // the sinon spy's name is "proxy" 125 | }) 126 | 127 | //////////////////////////// 128 | // fakeTask.arrayOfStrings: 129 | take(createdTasks.arrayOfStrings, (task) => { 130 | t.true(task instanceof taskClasses.TaskFork) 131 | t.is(task.name, 'arrayOfStrings') 132 | t.is(task.title, 'arrayOfStrings') 133 | t.is(task.children.length, 1) 134 | }) 135 | 136 | take(createdTasks.arrayOfStrings.children[0], (task) => { 137 | t.true(task instanceof taskClasses.TaskReference) 138 | t.is(task.reference, 'method') 139 | t.is(task.referencingTaskName, 'arrayOfStrings') 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /lib/loaders/test/json.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava').test 2 | const path = require('path') 3 | const sinon = require('sinon') 4 | const rewire = require('rewire') 5 | const Observable = require('zen-observable') 6 | 7 | const jsonLoader = rewire('../json') 8 | const taskClasses = require('../../task/classes') 9 | 10 | const FIXTURE_FILE_PATH = path.join(__dirname, '../../../test/fixtures/launch.scripts.json5') 11 | 12 | const shellObservable = sinon.spy(() => new Observable((observer) => observer.complete())) 13 | jsonLoader.__set__('shellObservable', shellObservable) 14 | 15 | const tasks = jsonLoader.loadFile(FIXTURE_FILE_PATH) 16 | 17 | function take (thing, callback) { 18 | callback(thing) 19 | } 20 | 21 | function runTask (task) { 22 | task.children.forEach((task) => task.method()) 23 | } 24 | 25 | test('all tasks have been loaded', (t) => { 26 | t.deepEqual( 27 | Object.keys(tasks).sort(), 28 | [ 'default', 'defaultAsArray', 'parallel', 'sleep1', 'sleep2', 'willFail', 'willWork' ] 29 | ) 30 | }) 31 | 32 | test('task "default"', (t) => { 33 | take(tasks.default, (task) => { 34 | t.true(task instanceof taskClasses.TaskFork) 35 | t.is(task.name, 'default') 36 | t.is(task.title, 'default') 37 | t.is(task.children.length, 2) 38 | 39 | take(task.children[0], (subTask) => { 40 | t.true(subTask instanceof taskClasses.TaskReference) 41 | t.is(subTask.reference, 'willWork') 42 | t.is(subTask.referencingTaskName, 'default') 43 | }) 44 | take(task.children[1], (subTask) => { 45 | t.true(subTask instanceof taskClasses.TaskReference) 46 | t.is(subTask.reference, 'willFail') 47 | t.is(subTask.referencingTaskName, 'default') 48 | }) 49 | }) 50 | }) 51 | 52 | test('task "defaultAsArray"', (t) => { 53 | take(tasks.defaultAsArray, (task) => { 54 | t.true(task instanceof taskClasses.TaskFork) 55 | t.is(task.name, 'defaultAsArray') 56 | t.is(task.title, 'defaultAsArray') 57 | t.is(task.children.length, 2) 58 | 59 | take(task.children[0], (subTask) => { 60 | t.true(subTask instanceof taskClasses.TaskReference) 61 | t.is(subTask.reference, 'willWork') 62 | t.is(subTask.referencingTaskName, 'defaultAsArray') 63 | }) 64 | take(task.children[1], (subTask) => { 65 | t.true(subTask instanceof taskClasses.TaskReference) 66 | t.is(subTask.reference, 'willFail') 67 | t.is(subTask.referencingTaskName, 'defaultAsArray') 68 | }) 69 | }) 70 | }) 71 | 72 | test('task "willWork"', (t) => { 73 | take(tasks.willWork, (task) => { 74 | t.true(task instanceof taskClasses.TaskFork) 75 | t.is(task.name, 'willWork') 76 | t.is(task.title, 'willWork') 77 | t.is(task.children.length, 1) 78 | 79 | take(task.children[0], (subTask) => { 80 | t.true(subTask instanceof taskClasses.AnonymousCommandTask) 81 | t.is(subTask.title, 'npm') 82 | }) 83 | }) 84 | }) 85 | 86 | test('task "willFail"', (t) => { 87 | take(tasks.willFail, (task) => { 88 | t.true(task instanceof taskClasses.TaskFork) 89 | t.is(task.name, 'willFail') 90 | t.is(task.title, 'willFail') 91 | t.is(task.children.length, 1) 92 | 93 | take(task.children[0], (subTask) => { 94 | t.true(subTask instanceof taskClasses.AnonymousCommandTask) 95 | t.is(subTask.title, 'git') 96 | }) 97 | }) 98 | }) 99 | 100 | test('task "parallel"', (t) => { 101 | take(tasks.parallel, (task) => { 102 | t.true(task instanceof taskClasses.TaskFork) 103 | t.is(task.name, 'parallel') 104 | t.is(task.title, 'parallel') 105 | t.is(task.children.length, 1) 106 | }) 107 | 108 | take(tasks.parallel.children[0], (task) => { 109 | t.true(task instanceof taskClasses.AnonymousConcurrentTaskFork) 110 | t.is(task.title, 'run-parallel sleep1 sleep2') 111 | t.is(task.children.length, 2) 112 | 113 | take(task.children[0], (taskReference) => { 114 | t.true(taskReference instanceof taskClasses.TaskReference) 115 | t.is(taskReference.reference, 'sleep1') 116 | t.is(taskReference.referencingTaskName, 'parallel') 117 | }) 118 | 119 | take(task.children[1], (taskReference) => { 120 | t.true(taskReference instanceof taskClasses.TaskReference) 121 | t.is(taskReference.reference, 'sleep2') 122 | t.is(taskReference.referencingTaskName, 'parallel') 123 | }) 124 | }) 125 | }) 126 | 127 | test('it actually run shell commands', (t) => { 128 | /////////////////// 129 | // Task "willWork" 130 | 131 | t.false(shellObservable.called) 132 | runTask(tasks.willWork) 133 | t.is(shellObservable.callCount, 1) 134 | t.true(shellObservable.calledWith('npm --version')) 135 | shellObservable.reset() 136 | 137 | /////////////////// 138 | // Task "willFail" 139 | 140 | runTask(tasks.willFail) 141 | t.is(shellObservable.callCount, 1) 142 | t.true(shellObservable.calledWith('git push not-existent')) 143 | shellObservable.reset() 144 | }) 145 | -------------------------------------------------------------------------------- /lib/task/classes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Listr = require('listr') 4 | const Observable = require('zen-observable') 5 | 6 | const getTaskByName = require('./registry').getTaskByName 7 | 8 | /** 9 | * Abstract base class for tasks. 10 | */ 11 | class Task { 12 | constructor (name, title) { 13 | this.name = name 14 | this.title = title || name 15 | } 16 | } 17 | 18 | /** 19 | * Abstract base class for tasks that actually run stuff themselves. 20 | */ 21 | class ExecutableTask extends Task { 22 | createListrTask (taskCallsObservable) { 23 | const task = taskCallsObservable 24 | ? () => createObservableFor(this.method, this.name, taskCallsObservable) 25 | : () => this.method() 26 | 27 | return { title: this.title, task } 28 | } 29 | } 30 | 31 | /** 32 | * A TaskLeaf executes a method. 33 | */ 34 | class TaskLeaf extends ExecutableTask { 35 | constructor (name, method) { 36 | super(name) 37 | this.method = method 38 | } 39 | } 40 | 41 | /** 42 | * An AnonymousCommandTask is not a real task, but only some shell command. 43 | */ 44 | class AnonymousCommandTask extends ExecutableTask { 45 | constructor (title, method) { 46 | super(null, title) 47 | this.method = method 48 | } 49 | } 50 | 51 | /** 52 | * A TaskFork does not directly execute anything, but rather triggers other tasks. 53 | */ 54 | class TaskFork extends Task { 55 | constructor (name, children) { 56 | super(name) 57 | this.children = children || [] 58 | } 59 | 60 | createListrTask (taskCallsObservable) { 61 | const listrSubTasks = this.children.map((task) => task.createListrTask(taskCallsObservable)) 62 | 63 | return { 64 | title: this.title, 65 | task: () => new Listr(listrSubTasks) 66 | } 67 | } 68 | } 69 | 70 | class AnonymousConcurrentTaskFork extends ExecutableTask { 71 | constructor (title, children) { 72 | super(null, title) 73 | this.children = children 74 | } 75 | 76 | createListrTask (taskCallsObservable, listrOptions) { 77 | const listrSubTasks = this.children.map((task) => task.createListrTask(taskCallsObservable)) 78 | 79 | return { 80 | title: this.title, 81 | task: () => new Listr(listrSubTasks, { concurrent: true }) 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * A TaskReference is just for referencing a task (which might have not been created yet) 88 | */ 89 | class TaskReference { 90 | constructor (name, referencingTaskName) { 91 | this.reference = name 92 | this.referencingTaskName = referencingTaskName 93 | } 94 | 95 | resolve () { 96 | const task = getTaskByName(this.reference) 97 | 98 | if (!task) { 99 | throw new Error(`Task not found: "${this.reference}" (referenced by "${this.referencingTaskName}")`) 100 | } 101 | return task 102 | } 103 | 104 | createListrTask () { 105 | return this.resolve().createListrTask() 106 | } 107 | } 108 | 109 | function createObservableFor (method, taskName, taskCallsObservable) { 110 | return new Observable((observer) => { 111 | // inform Listr when another task is run 112 | taskCallsObservable 113 | .filter((calledTaskName) => calledTaskName !== taskName) 114 | .subscribe(observer) 115 | 116 | // run the initial task (`method` a.k.a `taskName`), subscribe to it 117 | observify(() => method()).subscribe(observer) 118 | }) 119 | } 120 | 121 | function observify (method) { 122 | return new Observable((observer) => { 123 | const result = method() 124 | 125 | if (result.subscribe) { 126 | // Observable, so pass through 127 | result.subscribe(observer) 128 | } else if (result.then && result.catch) { 129 | // Promise, so map .then/.catch to observer 130 | result.then((value) => { 131 | observer.next(value) 132 | observer.complete() 133 | }).catch((error) => { 134 | observer.error(error) 135 | }) 136 | } else { 137 | // Just complete synchronously 138 | observer.next(result) 139 | observer.complete() 140 | } 141 | }) 142 | } 143 | 144 | exports.Task = Task 145 | exports.TaskLeaf = TaskLeaf 146 | exports.AnonymousCommandTask = AnonymousCommandTask 147 | exports.TaskFork = TaskFork 148 | exports.AnonymousConcurrentTaskFork = AnonymousConcurrentTaskFork 149 | exports.TaskReference = TaskReference 150 | -------------------------------------------------------------------------------- /lib/task/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const classes = require('./classes') 4 | const Task = classes.Task 5 | const TaskLeaf = classes.TaskLeaf 6 | const AnonymousCommandTask = classes.AnonymousCommandTask 7 | const TaskFork = classes.TaskFork 8 | const AnonymousConcurrentTaskFork = classes.AnonymousConcurrentTaskFork 9 | const TaskReference = classes.TaskReference 10 | 11 | const registerTask = require('./registry').registerTask 12 | 13 | function createTaskFromMethod (name, method) { 14 | return registerTask(new TaskLeaf(name, method)) 15 | } 16 | 17 | function createAnonymousCommandTask (title, method) { 18 | return new AnonymousCommandTask(title, method) 19 | } 20 | 21 | function createTaskFork (name, subTasks) { 22 | subTasks.forEach((subTask) => assertSubTaskOrReference(subTask, name)) 23 | return registerTask(new TaskFork(name, subTasks)) 24 | } 25 | 26 | function createAnonymousConcurrentTaskFork (parentTaskName, title, subTasks) { 27 | subTasks.forEach((subTask) => assertSubTaskOrReference(subTask, parentTaskName)) 28 | return new AnonymousConcurrentTaskFork(title, subTasks) 29 | } 30 | 31 | function createTaskReference (referencedTaskName, referencingTaskName) { 32 | return new TaskReference(referencedTaskName, referencingTaskName) 33 | } 34 | 35 | function createListrTaskItemFor (task, taskCallsObservable) { 36 | if (!(task instanceof Task) && !(task instanceof TaskReference)) { 37 | throw new Error(`Expected Task or TaskReference. Instead got: ${task} (type ${typeof task})`) 38 | } 39 | 40 | return task.createListrTask(taskCallsObservable) 41 | } 42 | 43 | function assertSubTaskOrReference (subTask, superTaskName) { 44 | if (!(subTask instanceof Task) && !(subTask instanceof TaskReference)) { 45 | throw new Error(`Error creating task "${superTaskName}": Found invalid sub-task: ${subTask} (type ${typeof subTask})`) 46 | } 47 | } 48 | 49 | exports.createFromMethod = createTaskFromMethod 50 | exports.createAnonymousCommand = createAnonymousCommandTask 51 | exports.createFork = createTaskFork 52 | exports.createAnonymousConcurrentTaskFork = createAnonymousConcurrentTaskFork 53 | exports.createReference = createTaskReference 54 | exports.createListrTaskItemFor = createListrTaskItemFor 55 | -------------------------------------------------------------------------------- /lib/task/registry.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const allTasksByName = {} 4 | 5 | function registerTask (task) { 6 | if (allTasksByName[ task.name ]) { 7 | throw new Error(`Attempted double definition of task "${task.name}"`) 8 | } 9 | 10 | allTasksByName[ task.name ] = task 11 | return task 12 | } 13 | 14 | function getTaskByName (name) { 15 | return allTasksByName[ name ] 16 | } 17 | 18 | exports.registerTask = registerTask 19 | exports.getTaskByName = getTaskByName 20 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const execa = require('execa') 4 | const Observable = require('zen-observable') 5 | 6 | /** 7 | * @param {string} command 8 | * @return {Promise<{ stdout: string, stderr: string }>} 9 | */ 10 | function shell (command) { 11 | return execa.shell(command) 12 | } 13 | 14 | /** 15 | * @param {string} command 16 | * @return {Observable} 17 | * Notifies its subscribers constantly about the lastest line of stdout/stderr output. 18 | */ 19 | function shellObservable (command) { 20 | const promisifiedProcess = execa.shell(command) 21 | 22 | return new Observable((observer) => { 23 | promisifiedProcess 24 | .then(() => observer.complete()) 25 | .catch((error) => observer.error(error)) 26 | 27 | if (promisifiedProcess.stdout) { 28 | observeStream(promisifiedProcess.stdout, (mostRecentLine) => observer.next(mostRecentLine)) 29 | } 30 | if (promisifiedProcess.stderr) { 31 | observeStream(promisifiedProcess.stderr, (mostRecentLine) => observer.next(mostRecentLine)) 32 | } 33 | }) 34 | } 35 | 36 | /** 37 | * @param {Stream} stream 38 | * @param {function} onNewLine function (mostRecentLine: string) 39 | * @return void 40 | */ 41 | function observeStream (stream, onNewLine) { 42 | stream.on('data', (text) => { 43 | if (typeof text !== 'string') { 44 | return 45 | } 46 | 47 | const mostRecentLine = text.split('\n').filter((line) => line.length > 0).pop() 48 | onNewLine(mostRecentLine) 49 | }) 50 | } 51 | 52 | exports.shell = shell 53 | exports.shellObservable = shellObservable 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-launch", 3 | "version": "0.3.0", 4 | "description": "Minimalistic task runner on steroids.", 5 | "bin": { 6 | "launch": "lib/cli.js" 7 | }, 8 | "main": "lib/index.js", 9 | "scripts": { 10 | "coverage": "nyc ./lib/cli.js ava", 11 | "prepublish": "npm test", 12 | "prepush": "npm test", 13 | "test": "./lib/cli.js test" 14 | }, 15 | "keywords": [ 16 | "launch", 17 | "npm", 18 | "run", 19 | "task", 20 | "package.json", 21 | "json5", 22 | "async", 23 | "await", 24 | "es6", 25 | "script" 26 | ], 27 | "author": "Andy Wermke ", 28 | "license": "MIT", 29 | "engines": { 30 | "node": ">= 4.0" 31 | }, 32 | "devDependencies": { 33 | "ava": "^0.15.2", 34 | "husky": "^0.11.4", 35 | "nyc": "^8.1.0", 36 | "rewire": "^2.5.2", 37 | "sinon": "^1.17.4", 38 | "standard": "^7.1.2" 39 | }, 40 | "dependencies": { 41 | "babel-plugin-rewire": "^1.0.0-rc-4", 42 | "babel-plugin-transform-async-to-generator": "^6.8.0", 43 | "babel-plugin-transform-es2015-modules-commonjs": "^6.10.3", 44 | "babel-register": "^6.9.0", 45 | "camelcase": "^3.0.0", 46 | "colors": "^1.1.2", 47 | "execa": "^0.4.0", 48 | "json5": "^0.5.0", 49 | "listr": "^0.5.0", 50 | "yargs": "^4.7.1", 51 | "zen-observable": "^0.3.0" 52 | }, 53 | "repository": "andywer/npm-launch", 54 | "bugs": "https://github.com/andywer/npm-launch/issues" 55 | } 56 | -------------------------------------------------------------------------------- /test/fixtures/launch.scripts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run from command line: `launch []` 3 | */ 4 | 5 | const shell = module.parent.exports.shell 6 | 7 | export function npmList () { 8 | // Run a shell command 9 | return shell('npm list') 10 | } 11 | 12 | export function runStandardLinter () { 13 | // Run a binary from node_modules/.bin 14 | return shell('standard --version') 15 | } 16 | 17 | export function wasteSomeTime () { 18 | // You can return any Promise 19 | return new Promise((resolve) => { 20 | setTimeout(() => resolve(), 1500) 21 | }) 22 | } 23 | 24 | // Can even use async/await out-of-the-box 25 | export async function fancyTasking () { 26 | await npmList() 27 | await runStandardLinter() 28 | await wasteSomeTime() 29 | } 30 | 31 | // Try `launch willFail` to see what an error looks like 32 | export function willFail () { 33 | return shell('git push not-existent') 34 | } 35 | 36 | // This will set up a task "default" triggering the given tasks one after another 37 | export default [ npmList, runStandardLinter, wasteSomeTime ] 38 | -------------------------------------------------------------------------------- /test/fixtures/launch.scripts.json5: -------------------------------------------------------------------------------- 1 | /** 2 | * Run from command line: `launch -f launch.scripts.json5 []` 3 | */ 4 | { 5 | // Will just run "npm --version" 6 | willWork: "npm --version", 7 | 8 | // Will cause the task to fail 9 | willFail: "git push not-existent", 10 | 11 | // Will run "willWork", then "willFail" 12 | default: "run willWork && run willFail", 13 | 14 | // You can also use array syntax! 15 | defaultAsArray: [ "willWork", "willFail" ], 16 | 17 | parallel: "run-parallel sleep1 sleep2", 18 | 19 | sleep1: "sleep 2.5", 20 | sleep2: "sleep 2 && sleep 2" 21 | } 22 | -------------------------------------------------------------------------------- /test/functional.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava').test 2 | const execa = require('execa') 3 | const path = require('path') 4 | 5 | process.chdir('./fixtures') 6 | 7 | const NPM_LAUNCH = '../../lib/cli.js' 8 | 9 | test('running npm-launch --help', () => { 10 | // will reject the returned Promise if something goes wrong 11 | // (test with: execa.shell(`${NPM_LAUNCH} -f`)) 12 | return execa.shell(`${NPM_LAUNCH} --help`) 13 | }) 14 | 15 | test('running npm-launch --list', async (t) => { 16 | const { stdout } = await execa.shell(`${NPM_LAUNCH} --list`) 17 | const expectedTasks = [ 'default', 'fancyTasking', 'npmList', 'runStandardLinter', 'wasteSomeTime', 'willFail' ] 18 | const expectedOutput = `Tasks in launch.scripts.js:\n${expectedTasks.map((taskName) => ` ${taskName}`).join('\n')}` 19 | t.is(stdout, expectedOutput) 20 | }) 21 | 22 | test('running npm-launch', () => { 23 | return execa.shell(`${NPM_LAUNCH}`) 24 | }) 25 | 26 | test('running npm-launch default', () => { 27 | return execa.shell(`${NPM_LAUNCH} default`) 28 | }) 29 | 30 | test('running npm-launch fancyTasking', () => { 31 | return execa.shell(`${NPM_LAUNCH} fancyTasking`) 32 | }) 33 | 34 | test('running npm-launch fancy-tasking', () => { 35 | return execa.shell(`${NPM_LAUNCH} fancy-tasking`) 36 | }) 37 | 38 | test('running npm-launch willFail', (t) => { 39 | return t.throws(execa.shell(`${NPM_LAUNCH} willFail`)) 40 | }) 41 | 42 | test('running npm-launch -f launch.scripts.json5 willWork', () => { 43 | return execa.shell(`${NPM_LAUNCH} -f launch.scripts.json5 willWork`) 44 | }) 45 | 46 | test('running npm-launch -f launch.scripts.json5 willFail', (t) => { 47 | return t.throws(execa.shell(`${NPM_LAUNCH} -f launch.scripts.json5 willFail`)) 48 | }) 49 | --------------------------------------------------------------------------------