1<p align="center"> 2 <a href="http://liftoffjs.com"> 3 <img height="100" width="297" src="https://cdn.rawgit.com/tkellen/js-liftoff/master/artwork/liftoff.svg"/> 4 </a> 5</p> 6 7# liftoff [![Build Status](http://img.shields.io/travis/js-cli/js-liftoff.svg?label=travis-ci)](http://travis-ci.org/js-cli/js-liftoff) [![Build status](https://img.shields.io/appveyor/ci/phated/js-liftoff.svg?label=appveyor)](https://ci.appveyor.com/project/phated/js-liftoff) 8 9 10> Launch your command line tool with ease. 11 12[![NPM](https://nodei.co/npm/liftoff.png)](https://nodei.co/npm/liftoff/) 13 14## What is it? 15[See this blog post](http://weblog.bocoup.com/building-command-line-tools-in-node-with-liftoff/), [check out this proof of concept](https://github.com/js-cli/js-hacker), or read on. 16 17Say you're writing a CLI tool. Let's call it [hacker](https://github.com/js-cli/js-hacker). You want to configure it using a `Hackerfile`. This is node, so you install `hacker` locally for each project you use it in. But, in order to get the `hacker` command in your PATH, you also install it globally. 18 19Now, when you run `hacker`, you want to configure what it does using the `Hackerfile` in your current directory, and you want it to execute using the local installation of your tool. Also, it'd be nice if the `hacker` command was smart enough to traverse up your folders until it finds a `Hackerfile`—for those times when you're not in the root directory of your project. Heck, you might even want to launch `hacker` from a folder outside of your project by manually specifying a working directory. Liftoff manages this for you. 20 21So, everything is working great. Now you can find your local `hacker` and `Hackerfile` with ease. Unfortunately, it turns out you've authored your `Hackerfile` in coffee-script, or some other JS variant. In order to support *that*, you have to load the compiler for it, and then register the extension for it with node. Good news, Liftoff can do that, and a whole lot more, too. 22 23## API 24 25### constructor(opts) 26 27Create an instance of Liftoff to invoke your application. 28 29An example utilizing all options: 30```js 31const Hacker = new Liftoff({ 32 name: 'hacker', 33 processTitle: 'hacker', 34 moduleName: 'hacker', 35 configName: 'hackerfile', 36 extensions: { 37 '.js': null, 38 '.json': null, 39 '.coffee': 'coffee-script/register' 40 }, 41 v8flags: ['--harmony'] // or v8flags: require('v8flags') 42}); 43``` 44 45#### opts.name 46 47Sugar for setting `processTitle`, `moduleName`, `configName` automatically. 48 49Type: `String` 50Default: `null` 51 52These are equivalent: 53```js 54const Hacker = Liftoff({ 55 processTitle: 'hacker', 56 moduleName: 'hacker', 57 configName: 'hackerfile' 58}); 59``` 60```js 61const Hacker = Liftoff({name:'hacker'}); 62``` 63 64#### opts.moduleName 65 66Sets which module your application expects to find locally when being run. 67 68Type: `String` 69Default: `null` 70 71#### opts.configName 72 73Sets the name of the configuration file Liftoff will attempt to find. Case-insensitive. 74 75Type: `String` 76Default: `null` 77 78#### opts.extensions 79 80Set extensions to include when searching for a configuration file. If an external module is needed to load a given extension (e.g. `.coffee`), the module name should be specified as the value for the key. 81 82Type: `Object` 83Default: `{".js":null,".json":null}` 84 85**Examples:** 86 87In this example Liftoff will look for `myappfile{.js,.json,.coffee}`. If a config with the extension `.coffee` is found, Liftoff will try to require `coffee-script/require` from the current working directory. 88```js 89const MyApp = new Liftoff({ 90 name: 'myapp', 91 extensions: { 92 '.js': null, 93 '.json': null, 94 '.coffee': 'coffee-script/register' 95 } 96}); 97``` 98 99In this example, Liftoff will look for `.myapp{rc}`. 100```js 101const MyApp = new Liftoff({ 102 name: 'myapp', 103 configName: '.myapp', 104 extensions: { 105 'rc': null 106 } 107}); 108``` 109 110In this example, Liftoff will automatically attempt to load the correct module for any javascript variant supported by [interpret](https://github.com/js-cli/js-interpret) (as long as it does not require a register method). 111 112```js 113const MyApp = new Liftoff({ 114 name: 'myapp', 115 extensions: require('interpret').jsVariants 116}); 117``` 118#### opts.v8flags 119 120Any flag specified here will be applied to node, not your program. Useful for supporting invocations like `myapp --harmony command`, where `--harmony` should be passed to node, not your program. This functionality is implemented using [flagged-respawn](http://github.com/js-cli/js-flagged-respawn). To support all v8flags, see [v8flags](https://github.com/js-cli/js-v8flags). 121 122Type: `Array|Function` 123Default: `null` 124 125If this method is a function, it should take a node-style callback that yields an array of flags. 126 127#### opts.processTitle 128 129Sets what the [process title](http://nodejs.org/api/process.html#process_process_title) will be. 130 131Type: `String` 132Default: `null` 133 134#### opts.completions(type) 135 136A method to handle bash/zsh/whatever completions. 137 138Type: `Function` 139Default: `null` 140 141#### opts.configFiles 142 143An object of configuration files to find. Each property is keyed by the default basename of the file being found, and the value is an object of [path arguments](#path-arguments) keyed by unique names. 144 145__Note:__ This option is useful if, for example, you want to support an `.apprc` file in addition to an `appfile.js`. If you only need a single configuration file, you probably don't need this. In addition to letting you find multiple files, this option allows more fine-grained control over how configuration files are located. 146 147Type: `Object` 148Default: `null` 149 150#### Path arguments 151 152The [`fined`](https://github.com/js-cli/fined) module accepts a string representing the path to search or an object with the following keys: 153 154* `path` __(required)__ 155 156 The path to search. Using only a string expands to this property. 157 158 Type: `String` 159 Default: `null` 160 161* `name` 162 163 The basename of the file to find. Extensions are appended during lookup. 164 165 Type: `String` 166 Default: Top-level key in `configFiles` 167 168* `extensions` 169 170 The extensions to append to `name` during lookup. See also: [`opts.extensions`](#optsextensions). 171 172 Type: `String|Array|Object` 173 Default: The value of [`opts.extensions`](#optsextensions) 174 175* `cwd` 176 177 The base directory of `path` (if relative). 178 179 Type: `String` 180 Default: The value of [`opts.cwd`](#optscwd) 181 182* `findUp` 183 184 Whether the `path` should be traversed up to find the file. 185 186 Type: `Boolean` 187 Default: `false` 188 189**Examples:** 190 191In this example Liftoff will look for the `.hacker.js` file relative to the `cwd` as declared in `configFiles`. 192```js 193const MyApp = new Liftoff({ 194 name: 'hacker', 195 configFiles: { 196 '.hacker': { 197 cwd: '.' 198 } 199 } 200}); 201``` 202 203In this example, Liftoff will look for `.hackerrc` in the home directory. 204```js 205const MyApp = new Liftoff({ 206 name: 'hacker', 207 configFiles: { 208 '.hacker': { 209 home: { 210 path: '~', 211 extensions: { 212 'rc': null 213 } 214 } 215 } 216 } 217}); 218``` 219 220In this example, Liftoff will look in the `cwd` and then lookup the tree for the `.hacker.js` file. 221```js 222const MyApp = new Liftoff({ 223 name: 'hacker', 224 configFiles: { 225 '.hacker': { 226 up: { 227 path: '.', 228 findUp: true 229 } 230 } 231 } 232}); 233``` 234 235In this example, the `name` is overridden and the key is ignored so Liftoff looks for `.override.js`. 236```js 237const MyApp = new Liftoff({ 238 name: 'hacker', 239 configFiles: { 240 hacker: { 241 override: { 242 path: '.', 243 name: '.override' 244 } 245 } 246 } 247}); 248``` 249 250In this example, Liftoff will use the home directory as the `cwd` and looks for `~/.hacker.js`. 251```js 252const MyApp = new Liftoff({ 253 name: 'hacker', 254 configFiles: { 255 '.hacker': { 256 home: { 257 path: '.', 258 cwd: '~' 259 } 260 } 261 } 262}); 263``` 264 265## launch(opts, callback(env)) 266Launches your application with provided options, builds an environment, and invokes your callback, passing the calculated environment as the first argument. 267 268##### Example Configuration w/ Options Parsing: 269```js 270const Liftoff = require('liftoff'); 271const MyApp = new Liftoff({name:'myapp'}); 272const argv = require('minimist')(process.argv.slice(2)); 273const invoke = function (env) { 274 console.log('my environment is:', env); 275 console.log('my cli options are:', argv); 276 console.log('my liftoff config is:', this); 277}; 278MyApp.launch({ 279 cwd: argv.cwd, 280 configPath: argv.myappfile, 281 require: argv.require, 282 completion: argv.completion 283}, invoke); 284``` 285 286#### opts.cwd 287 288Change the current working directory for this launch. Relative paths are calculated against `process.cwd()`. 289 290Type: `String` 291Default: `process.cwd()` 292 293**Example Configuration:** 294```js 295const argv = require('minimist')(process.argv.slice(2)); 296MyApp.launch({ 297 cwd: argv.cwd 298}, invoke); 299``` 300 301**Matching CLI Invocation:** 302``` 303myapp --cwd ../ 304``` 305 306#### opts.configPath 307 308Don't search for a config, use the one provided. **Note:** Liftoff will assume the current working directory is the directory containing the config file unless an alternate location is explicitly specified using `cwd`. 309 310Type: `String` 311Default: `null` 312 313**Example Configuration:** 314```js 315var argv = require('minimist')(process.argv.slice(2)); 316MyApp.launch({ 317 configPath: argv.myappfile 318}, invoke); 319``` 320 321**Matching CLI Invocation:** 322``` 323myapp --myappfile /var/www/project/Myappfile.js 324``` 325 326**Examples using `cwd` and `configPath` together:** 327 328These are functionally identical: 329``` 330myapp --myappfile /var/www/project/Myappfile.js 331myapp --cwd /var/www/project 332``` 333 334These can run myapp from a shared directory as though it were located in another project: 335``` 336myapp --myappfile /Users/name/Myappfile.js --cwd /var/www/project1 337myapp --myappfile /Users/name/Myappfile.js --cwd /var/www/project2 338``` 339 340#### opts.require 341 342A string or array of modules to attempt requiring from the local working directory before invoking the launch callback. 343 344Type: `String|Array` 345Default: `null` 346 347**Example Configuration:** 348```js 349var argv = require('minimist')(process.argv.slice(2)); 350MyApp.launch({ 351 require: argv.require 352}, invoke); 353``` 354 355**Matching CLI Invocation:** 356```js 357myapp --require coffee-script/register 358``` 359 360#### opts.forcedFlags 361 362Allows you to force node or V8 flags during the launch. This is useful if you need to make sure certain flags will always be enabled or if you need to enable flags that don't show up in `opts.v8flags` (as these flags aren't validated against `opts.v8flags`). 363 364If this is specified as a function, it will receive the built `env` as its only argument and must return a string or array of flags to force. 365 366Type: `String|Array|Function` 367Default: `null` 368 369**Example Configuration:** 370```js 371MyApp.launch({ 372 forcedFlags: ['--trace-deprecation'] 373}, invoke); 374``` 375 376**Matching CLI Invocation:** 377```js 378myapp --trace-deprecation 379``` 380 381#### callback(env) 382 383A function to start your application. When invoked, `this` will be your instance of Liftoff. The `env` param will contain the following keys: 384 385- `cwd`: the current working directory 386- `require`: an array of modules that liftoff tried to pre-load 387- `configNameSearch`: the config files searched for 388- `configPath`: the full path to your configuration file (if found) 389- `configBase`: the base directory of your configuration file (if found) 390- `modulePath`: the full path to the local module your project relies on (if found) 391- `modulePackage`: the contents of the local module's package.json (if found) 392- `configFiles`: an object of filepaths for each found config file (filepath values will be null if not found) 393 394### events 395 396#### require(name, module) 397 398Emitted when a module is pre-loaded. 399 400```js 401var Hacker = new Liftoff({name:'hacker'}); 402Hacker.on('require', function (name, module) { 403 console.log('Requiring external module: '+name+'...'); 404 // automatically register coffee-script extensions 405 if (name === 'coffee-script') { 406 module.register(); 407 } 408}); 409``` 410 411#### requireFail(name, err) 412 413Emitted when a requested module cannot be preloaded. 414 415```js 416var Hacker = new Liftoff({name:'hacker'}); 417Hacker.on('requireFail', function (name, err) { 418 console.log('Unable to load:', name, err); 419}); 420``` 421 422#### respawn(flags, child) 423 424Emitted when Liftoff re-spawns your process (when a [`v8flags`](#optsv8flags) is detected). 425 426```js 427var Hacker = new Liftoff({ 428 name: 'hacker', 429 v8flags: ['--harmony'] 430}); 431Hacker.on('respawn', function (flags, child) { 432 console.log('Detected node flags:', flags); 433 console.log('Respawned to PID:', child.pid); 434}); 435``` 436 437Event will be triggered for this command: 438`hacker --harmony commmand` 439 440## Examples 441 442Check out how [gulp](https://github.com/gulpjs/gulp-cli/blob/master/index.js) uses Liftoff. 443 444For a bare-bones example, try [the hacker project](https://github.com/js-cli/js-hacker/blob/master/bin/hacker.js). 445 446To try the example, do the following: 447 4481. Install the sample project `hacker` with `npm install -g hacker`. 4492. Make a `Hackerfile.js` with some arbitrary javascript it. 4503. Install hacker next to it with `npm install hacker`. 4513. Run `hacker` while in the same parent folder. 452