Node CLI

Automatizando tarefas pela linha de comando

Introdução

Como no dia 07/08/2019 irei apresentar sobre NodeJS e CLIs, resolvi escrever esse pequeno post para deixar como referência após a apresentação e também esclarecer as ideias antes de apresentar.

Motivação

Durante um bom tempo eu utilizei bastante Bash e Python para fazer minhas ferramentas CLI, era um bom arsenal para realizar as tarefas, mas a manutenção começava a ficar ruim depois de um tempo, problemas de edentação quando havia migração de editores...

Um dia eu decidi começar a pesquisar sobre o ferramental de NodeJS e tudo mudou. Como eu sempre tendi mais ao lado de frontend, Javascript era uma linguagem que estava no sangue, e então comecei a fazer todos os meus scripts de Python em NodeJS e com isso obtive o mesmo resultado, porém com um código mais consiso. Ainda mais quando decidi colocar Typescript no meio disso tudo.

npm init

Como o mundo de JS é bastante vasto, existem centenas de boilerplates pra você seguir, como configurar, best practices e blábláblá...mas eu prefiro o basicão pra seguir o KISS.

Vou me ater ao simples de uma CLI pra ordenar versões de tags do git. Usando bash, poderiamos usar o comando sort, fazendo o seguinte comando git tag | sort -V. Mas como o foco é um CLI em NodeJS, vamos lá:

$ mkdir my-cli
$ cd my-cli
$ npm init -y

Usei npm init -y pra ser mais rápido. Mas isso já da pra começar a fazer os ajustes necessários pra começar a escrever código. Temos uns passos a serem executados:

  1. Inserir a chave main e bin com o path de entrypoint da aplicação no package.json
  2. Criar o diretório para código TS
  3. Configurar o tsconfig.json e um tslint.json para fortalecer o desenvolvimento
  4. Instalar as dependências
  5. Codar!!! Codar!!! Codar!!!

Main no package.json

{
  "name": "alfred",
  "version": "3.0.28",
  "description": "",
  "main": "./cli/index.js",
  "bin": {
    "alfred": "./cli/index.js"
  },
  "scripts": {
    "build": "tsc -p .",
    "prettier": "prettier --write \"{.,src/**}/*.{js,jsx,ts,tsx}\""
  },
  "keywords": [],
  "author": "g4rcez",
  "license": "MIT"
}

Como vamos usar Typescript, tanto main quanto bin são os diretórios de transpilação

Apesar do mundo inteiro dizer compilar JS, esse termo é errado, pois TS transpila JS e não compila. Afinal de contas, o bundle não é um arquivo binário.

<small style={{marginTop:"-2em"}}>Essa é a sua cara nesse exato momento</small> <img alt="Meme" src="https://media.giphy.com/media/1L5YuA6wpKkNO/source.gif" style={{width:"100%"}}/>

Mas como irei programar com TS se NodeJS não roda TS?

Escrevendo TS

O primeiro passo antes de tudo é criar um tsconfig.json. Caso você não tenha typescript no PC, vamos resolver isso agora para fazer nossa CLI

$ npm i -g typescript
$ tsc --init

E pronto, já temos o nosso tsconfig.json e vamos deixar ele com essa cara:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": ["esnext", "es7"],
    "allowJs": false,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./cli",
    "rootDir": "./src",
    "removeComments": true,
    "importHelpers": true,
    "downlevelIteration": true,
    "strict": true,
    "esModuleInterop": true
  }
}

Se você estiver no VsCode, aconselho usar CTRL+Space nos campos para ver o que há disponível e se aventurar com suas próprias configurações. Caso queira alguma referência, esse é o meu tsconfig.json que costumo usar no trabalho/projetos.

Como vamos usar TS, é bom em conjunto usar o TSLint pra evitar quaisquer :shit:.

npm i -g tslint
tslint --init

E caso queira referência, esse é o meu tslint.json.

Dependências

Como todo bom programador em NodeJS, você deve ser dependente de diversos...pacotes. E pra fazer uma CLI não é diferente.

Eu curto utilizar o yarn sempre que possível nos meus projetos, pra caso um dia eu pense em usar o workspaces do yarn

$ yarn add typescript semver commander signale chalk
$ yarn add --dev @types/node @types/semver @types/signale

Tirando typescript, os demais são novos, então vou explicar

Os types são devDependencies para nos auxiliar com o typing do TS

Desenvolvimento selvagem

Pronto, tudo certo (eu espero que sim...) para o desenvolvimento selvagem. Primeiro o código e depois a explicação

import cli from "commander";
import { exec } from "child_process";
import semver from "semver";
import signale from "signale";
const program = new cli.Command();

/*
    Vamos usar $ por ser o símbolo que identifica a shell usuários
    no Unix, não por causa do Jquery
*/
const $ = (command: string): Promise<string> =>
  new Promise((res, rej) =>
    exec(command, (err, stdout, stderr) => {
      if (err) {
        return rej(stderr);
      }
      return res(stdout);
    })
  );

//  Como não exigimos parâmetros aqui, então não esperamos receber nada
const Tags = () => {
  try {
    const gitOutput = await $("git tag"); // Output de todas as tags
    const tags = gitOutput.split("\n");
    // Uma ordenação simples de acordo com as versões apresentadas
    // Versões não válidas ficaram no topo da pilha
    tags.sort((v1: string, v2: string) => {
      if (semver.valid(v1) && semver.valid(v2)) {
        if (semver.eq(v1, v2)) {
          return 0;
        }
        return semver.gte(v1, v2) ? 1 : -1;
      }
      return -1;
    });
    signale.success(tags.join("\n"));
  } catch (e) {}
};

program
  .version("0.0.1")
  .allowUnknownOption(false)
  .description("Ordenador de tags")
  .usage("tag")
  .command("tag")
  .name("my-cli")
  .alias("t")
  .description("Ordena as tags do repositório git corrente")
  .action(Tags);

if (process.argv.length === 2) {
  program.help();
  process.exit();
}
program.parse(process.argv);

E pronto, temos nosso primeiro CLI em TS. Para você rodar ele como Node, basta realizar esses passos

$ tsc # Isso irá transpilar do diretório src para cli
$ node cli tag # Ou então
$ node cli t # alias definido no programa

Não vou deixar de explicar como podemos fazer para receber os parâmetros no caso de ser necessário, é bem simples usando a interface do commander, só fazer da seguinte maneira

/*
    Os mesmos imports anteriormente e quase o mesmo código.
    Agora iremos receber params para ter acesso ao que foi recebido.
    Vou colocar o tipo any pra não ficar
    muito verboso (e nesse caso específico),
    eu acabo não tipando pois as vezes é no GoHorse

    Acaba que na própria definição no seu schema do commander vc anota
    os tipos que irá receber com os seus argumentos do programa
*/
const Tags = (params: any) => {
  try {
    // Até o próximo comentário, ta tudo igual
    const gitOutput = await $("git tag"); // Output de todas as tags
    const tags = gitOutput.split("\n");
    tags.sort((v1: string, v2: string) => {
      if (semver.valid(v1) && semver.valid(v2)) {
        if (semver.eq(v1, v2)) {
          return 0;
        }
        return semver.gte(v1, v2) ? 1 : -1;
      }
      return -1;
    });
    // Até aqui, nada mudou, mas vamos colocar um info e estilizar
    // com o chalk pra dizer que mostrei ele
    signale.info(chalk.bold.visible.underline.blue(params.msg));
    signale.success(tags.join("\n"));
  } catch (e) {}
};

program
  .version("0.0.1")
  .allowUnknownOption(false)
  .description("Ordenador de tags")
  .usage("tag")
  .command("tag")
  .name("my-cli")
  .alias("t")
  // Aqui está o novo trecho no commander
  // Ele irá entregar uma property "msg" para você usar como valor
  // caso não exista, ela será o valor padrão que definiu
  // E se não definir, será undefined
  .option(
    "-m, --msg <mensagem>",
    "Mensagem a ser exibida antes de exibir as tags",
    "Ordenação de tags"
  ) // "Ordenação de tags" é a mensagem padrão caso não haja
  .description("Ordena as tags do repositório git corrente")
  .action(Tags);

if (process.argv.length === 2) {
  program.help();
  process.exit();
}
program.parse(process.argv);

Bom, creio que isso seja o necessário para que todos possam começar a fazer suas CLI com NodeJS e considerar manter como sua linguagem para scripts, afinal de conta NodeJS é JavaSCRIPT.