UNPKG

gluegun

Version:

A delightful toolkit for building Node-powered CLIs.

177 lines (129 loc) 5.11 kB
# Guide: Architecting Your Gluegun CLI There are many ways to architect Gluegun-powered CLIs. This guide is intended to be a living document, collecting the lessons learned along the way of building CLIs with Gluegun. It is not necessarily the only correct way to build a CLI. If you have ideas, suggestions, or questions, feel free to [open an issue](https://github.com/infinitered/gluegun/issues/new)! ## Commands Commands should be focused on user interaction and not necessarily on implementation details. Don't write your whole app inside a command; instead, parse out user-provided info, then delegate to other functions (which can be provided via extensions, which are described below) to do work. _Do this_ ```js module.exports = { name: 'world', alias: ['w', 'earth'], run: async toolbox => { // in this case, `hello` is provided by an extension const { hello, prompt } = toolbox // user interaction is great in a command const isEarthling = await prompt.confirm('Are you an earthling?') // delegate the actual work to an extension if (isEarthling) { hello.greetEarthling() } else { hello.greetAlien() } }, } ``` ### Commands file structure Nest commands in a folder structure that mirrors their command line equivalent. Unlike other libraries, we don't use `index.js` for the default command in a folder. Instead, use the same name as the folder. This helps avoid the situation where you might have 12 `index.js` files open in your editor, which is confusing. For example: _Don't do this_ ``` commands hello index.js ``` _Do this_ ``` commands hello hello.js ``` _Or this_ ``` commands hello.js ``` If you have nested commands, keep them all in the folder, like this: ``` commands hello hello.js world.js ``` You don't have to have a default command for a folder. Gluegun will pick up on it (as of 2.0.0-beta.7). ``` commands hello world.js ``` As of Gluegun 4.1.0, you can also nest commands in a `build` folder, if for example you're using TypeScript and want to compile to `./build`. ## Extensions Think of extensions as "drawers" full of tools in your Gluegun toolbox. In the above example, the `hello` extension adds two functions, `greetEarthling` and `greetAlien`. ```js module.exports = toolbox => { const { print } = toolbox toolbox.hello = { greetEarthling: () => print.info('Hello, earthling!'), greetAlien: () => print.info('Greetings, alien!'), } } ``` _Hint: In most cases, you probably don't want to use `prompt` in your extensions. They should be more general purpose tools and not specific user flows._ ### Additional Functionality The above code snippet is a good simple example of an extension. However, as your extensions grow, you'll probably find that they start getting quite large. In that case, you'll probably want to break your functions out into separate folders. Just like Gluegun itself, we recommend a separate folder for these. Gluegun uses `src/toolbox`, but you can name it whatever makes sense for you. Here's an example: ``` commands hello world.js extensions hello-extension.js toolbox greetings earthling.js martian.js venusian.js ``` You can access Gluegun tools by using `require` (or `import` if you're using TypeScript). ```js const { print } = require('gluegun/print') // toolbox/greetings/earth.js module.exports = () => print.info('Hello, earthling!'), ``` Then attach the functions or objects to your toolbox: ```js // extensions/hello-extension.js const earthling = require('../toolbox/greetings/earthling') const martian = require('../toolbox/greetings/martian') const venusian = require('../toolbox/greetings/venusian') module.exports = toolbox => { toolbox.hello = { earthling, martian, venusian } } ``` ### Performance If you use many NPM packages, it's a good idea for performance reasons to "hide" `require` statements inside your command `run` functions so only the command that is invoked loads its dependencies. ([Here](https://github.com/aws-amplify/amplify-cli/pull/511) is an example that improved Amazon AWS Amplify CLI performance by nearly 2.5x) _Don't do this_ ```js const R = require('ramda') module.exports = { name: 'mycommand', run: async toolbox => { // use Ramda }, } ``` _Do this_ ```js module.exports = { name: 'mycommand', run: async toolbox => { const R = require('ramda') // use Ramda }, } ``` `require` will only load on-demand when the function is run. It will also only load `ramda` once (in the examples given) even if you `require` multiple times. So you can safely `require('ramda')` in as many functions or extensions as you want. ## Additional Topics The topics above will get you very far. Some other things to consider as you dig deeper into your CLI are: 1. Where do I access and store configuration values? 2. How do consumers of my CLI install and configure [plugins](/plugins)? 3. How will templates be organized? (Hint: look at the [Gluegun CLI source](https://github.com/infinitered/gluegun/tree/master/src/cli))