Kus Cámara
Front dev
$ mkdir my-cli && cd $_ # "take my-cli" si usas Oh My Zsh
$ npm init -y
1. Crea el paquete NPM
#!/usr/bin/env node
console.log('Hola');
2. Un archivo marcado como ejecutable por Node
{
"name": "my-cli",
"main": "index.js",
"bin": {
"di-hola": "./index.js"
}
}
3. Campo bin en el package.json con el nombre del comando y la ruta al ejecutable:
3. También puede ser un string con la ruta al ejecutable. El nombre del comando es el nombre del paquete.
{
"name": "my-cli",
"main": "index.js",
"bin": "./index.js"
}
1. Globalmente
# desde la carpeta del paquete (my-cli)
$ npm link
$ cd ~/Desktop
$ di-hola
¿Qué ha pasado?
/your-global-node/bin/di-hola ->
/your-global-node/lib/node_modules/my-cli/index.js
La aplicación (di-hola) es un enlace simbólico que apunta al script ejecutable en la instalación global de nuestro paquete.
/your-global-node/lib/node_modules/my-cli ->
/Users/projects/my-cli
La instalación de nuestro paquete dentro del node_modules global es a su vez un enlace simbólico que apunta al directorio original.
2. Dentro de otro paquete (como dependencia)
$ cd ~/another-package
$ npm link my-cli
¿Qué ha pasado?
$ cd ~/another-package
$ npm link my-cli
Lo mismo que ocurre globalmente pero dentro de la carpeta node_modules del paquete donde lo instalamos (enlazamos).
¿Qué ha pasado?
Duda frecuente. Si lo tengo instalado globalmente y como dependencia... ¿?
{
"name": "another-package",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"my-npm-script": "./node_modules/bin/di-hola"
},
"dependencies": {
"my-cli": "^1.0.0"
}
}
{
"name": "another-package",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"my-npm-script": "di-hola"
},
"dependencies": {
"my-cli": "^1.0.0"
}
}
npm run añade node_modules/.bin al PATH usado por los scripts.
$ say hello
$ say bye
Comandos (CLI multi command)
$ say hello --name="Pepe"
$ say bye --uppercase
Comandos con opciones
$ sayhello
$ sayhello --name="Pepe"
Opciones o flags (CLI single command)
process.argv
#!/usr/bin/env node
console.log(process.argv);
➜ di-hola hola mundo
[ '/your-global-node/bin/node',
'/your-global-node/bin/di-hola',
'hola',
'mundo' ]
#!/usr/bin/env node
const [command] = process.argv.slice(2);
switch (command) {
case 'hello':
console.log('Hello');
break;
case 'bye':
console.log('Bye');
break;
default:
console.log('Unknown or missing command :(');
}
$ say hello --name="Pepe"
$ say hello --name Pepe
$ say hello -n="Pepe"
La cosa se complica con los flags
Necesitamos parsearlos. ¿Alguna ayudita?
Yargs
Yargs
const yargs = require('yargs');
// comando --help muestra la descripcion de comandos y opciones
// comando --version muestra la version del package.json
// yargs API: https://github.com/yargs/yargs/blob/HEAD/docs/api.md
yargs
.command({
command: 'saluda',
describe: 'Dice hola',
handler() {
console.log(`Hola ${yargs.argv.nombre}`);
}
})
.option('nombre', {
alias: 'n',
default: 'persona'
})
.argv;
Yargs
$ di-hola saluda -n Pepe
Hola Pepe
$ di-hola saluda --nombre Pepe
Hola Pepe
$ di-hola saluda --nombre="Pepe García"
Hola Pepe García
$ di-hola saluda
Hola persona
Commander
Funcionalidades
➜ npx oclif single localpen
_-----_ ╭──────────────────────────╮
| | │ Time to build a │
|--(o)--| │ single-command CLI with │
`---------´ │ oclif! Version: 1.15.2 │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? npm package name localpen
? command bin name the CLI will export localpen
? description Crea codepens en mi equipo
...
// src/index.js
LocalpenCommand.flags = {
// add --version flag to show CLI version
version: flags.version({char: 'v'}),
// add --help flag to show CLI version
help: flags.help({char: 'h'}),
title: flags.string({
char: 't',
description: 'page title',
}),
sass: flags.boolean({
char: 's',
description: 'use sass for styles',
}),
}
// src/index.js
class LocalpenCommand extends Command {
async run() {
const {flags} = this.parse(LocalpenCommand)
this.log(flags)
}
}
Lo puedes ir probando con npm link
$ localpen --title "My local pen" --sass
Usaremos cosmiconfig
$ npm i cosmiconfig
const {Command, flags} = require('@oclif/command')
const {cosmiconfig} = require('cosmiconfig')
class LocalpenCommand extends Command {
async run() {
const options = await this.getOptions()
console.log('options', options)
}
async getOptions() {
// read config found in files (.*rc, package.json, etc.)
const explorer = cosmiconfig('localpen');
const configInFiles = await explorer.search()
const localConfig = configInFiles ? configInFiles.config : {}
// override local config with flags
const {flags} = this.parse(LocalpenCommand)
return Object.assign(localConfig, flags)
}
}
$ npm i inquirer
const {Command, flags} = require('@oclif/command')
const {cosmiconfig} = require('cosmiconfig')
const {prompt} = require('inquirer')
class LocalpenCommand extends Command {
async run() {
const options = await this.getOptions()
console.log('options', options)
}
async getOptions() {
// read config found in files (.*rc, package.json, etc.)
const explorer = cosmiconfig('localpen');
const configInFiles = await explorer.search()
const localConfig = configInFiles ? configInFiles.config : {}
// override local config with flags
const {flags} = this.parse(LocalpenCommand)
const params = Object.assign(localConfig, flags)
return this.requestMissingParams(params)
}
}
class LocalpenCommand extends Command {
// ...
async requestMissingParams(params) {
let answers = {}
const questions = [
{
type: 'input',
name: 'title',
message: 'Page title',
},
{
type: 'confirm',
name: 'sass',
message: 'Do you want to use sass?',
default: false,
},
]
const notInParams = entry => !Object.keys(params).includes(entry.name)
const missingParams = questions.filter(notInParams)
if (missingParams.length > 0) {
answers = await prompt(missingParams).catch(this.exit)
}
// mix the user answers with the params
return Object.assign(params, answers)
}
}
$ npm i conf
const {Command, flags} = require('@oclif/command')
const {cosmiconfig} = require('cosmiconfig')
const {prompt} = require('inquirer')
const Conf = require('conf')
class LocalpenCommand extends Command {
constructor() {
super(...arguments)
this.name = 'localpen'
this.userPrefs = new Conf({projectName: this.name})
}
async run() {
const options = await this.getOptions()
this.saveUserPreferences(options)
}
saveUserPreferences(options) {
this.userPrefs.set(`${this.name}.sass`, options.sass)
}
}
async requestMissingParams(params) {
const userPrefs = this.userPrefs.get(this.name) || {}
let answers = {}
const questions = [
{
type: 'input',
name: 'title',
message: 'Page title',
},
{
type: 'confirm',
name: 'sass',
message: 'Do you want to use sass?',
default: userPrefs.sass,
},
]
//...
}
En la siguiente ejecución, el prompt nos marcará por defecto la opción elegida previamente para sass.
Usaremos copy-template-dir
$ npm i copy-template-dir
Descarga los templates (ya hechos) en la carpeta template
$ npx degit kcmr/template-localpen template
const copyTemplateDir = require('copy-template-dir')
const {promisify} = require('util')
const path = require('path')
const copy = promisify(copyTemplateDir)
class LocalpenCommand extends Command {
async run() {
const options = await this.getOptions()
this.saveUserPreferences(options)
await this.createLocalpen(options)
}
async createLocalpen(options) {
const srcDir = path.join(__dirname, '..', 'template')
const destDir = process.cwd()
const stylesExtension = options.sass ? 'scss' : 'css'
const templateVars = {
title: options.title,
format: stylesExtension,
}
await copy(srcDir, destDir, templateVars)
}
}
$ npm i execa
const execa = require('execa')
class LocalpenCommand extends Command {
async run() {
const options = await this.getOptions()
this.saveUserPreferences(options)
await this.createLocalpen(options)
this.installDependencies()
}
installDependencies() {
const subprocess = execa('npm', ['i'], {
cwd: process.cwd(),
stdio: 'inherit',
})
subprocess.on('close', () => {
this.log('💥 Run "npm start" to lauch your localpen!')
})
}
}
$ npm i -g yourgithubuser/create-localpen
$ cd some-folder
$ localpen
Instalado globalmente:
# repo yourgithubusername/create-localpen
$ npm init yourgithubusername/localpen
Ejecutado sin instalación:
By Kus Cámara
Ingredientes básicos de un CLI con Node. Librerías, frameworks y utilidades. Uso de Oclif para crear un generador. Distribución a través de Github y npm (npx).