Criando e publicando um pacote NPM global para executar com CLI
Tags: javascript, npm
Published: 2022-09-06 02:43:50

> Abordagem, necessidades e implementação do nosso pacote para receber luz à vida!

Ei dev,

Já se deparou com cenários onde um conjunto de funcionalidades precisam se comunicar em diversos contextos como web, mobile e desktop, ou ainda que aplicações A, B ou C deveriam realizar alguns dos mesmos trabalhos?
Ou aquela famosa configuração de um ambiente que você precisa repetir a todo novo projeto, que talvez você até tenha um repositório que contém essa configuração mas sente que poderia ser ainda mais prática?
Pois bem, hoje vamos comentar e criar justamente o segundo cenário, sendo que vale o adendo que essencialmente o processo é parecido quando necessitamos atender o primeiro.

Iremos abordar:
- Por que?
- O que vamos fazer e como deve funcionar
- Estrutura
- Implementação e configuração
- Testando localmente
- Publicando
- Consumindo nosso pacote
- Observações

## Por que?

Vale notar que antes de partir pra implementação, devemos entender em qual contexto deve ser aplicado. No nosso caso, dado que temos como funcionalidade executar algo no CLI pode ter diversos motivos, entre eles: 

- Executar ações dentro de um ambiente;
- Configurar projetos/estruturas de pastas e recursos (bem comum e nosso caso)

Ou seja, geralmente executar alguma ação simples e objetiva pra um comportamento.

## O que vamos fazer e como deve funcionar

Pretendemos fazer um pacote simples. Queremos executar um comando A e com ele trazer um projeto Nodejs pré-configurado usando Express e TypeScript, nada demais. Nosso objetivo aqui é explorar possibilidades com essa estrutura.

Note que nosso foco será em como estruturar nosso pacote, então não focaremos na parte que consta a pré-configuração do nodejs.

## Estrutura

Vamos dividir nosso projeto em três áreas, consequentemente diretórios:
- **project**: pasta que cont~em nosso projeto nodejs do qual usaremos para levar a quem executa nosso comando
- **lib**: aqui haverá os arquivos dos quais realizam os processos que nosso pacote tem, ou seja, é aqui que estará o cara que terá a responsabilidade de mandar projeto nodejs ao requisitante.
- **bin**: onde deverã conter os arquivos os quais devem ser chamados pra execução no CLI/terminal. Este, por sua vez, chama os scripts listados em _lib_ e os utiliza passando parâmetros se necessário.

## Implementação e configuração

### Implementação

Primeiro, vamos criar um arquivo chamado _copyFile.js_ na nossa pasta lib, ele deverá conter:

```js
const path = require('path');
const fs = require('fs');
const colors = require('colors');
const exec = require('child_process').exec;

exports.copy = (projectName) => {
  console.log(colors.blue('Initializing project...'));

  const projectDir = path.resolve(__dirname, '..', 'project');
  const pathToExecutable = process.cwd();

  const newProjectFolder = path.resolve(pathToExecutable, projectName);
  const existsDirOfProject = fs.existsSync(newProjectFolder); // check if the project already exists

  if (existsDirOfProject) {
    console.log(colors.red('The project already exists'));
    process.exit(1);
  } else {
    fs.mkdirSync(newProjectFolder); // create the project folder
  }
  
  console.log(colors.blue('Creating project structure...'));
  fs.cpSync(projectDir, newProjectFolder, { recursive: true }); // copy the project folder to the new project folder

  const command = `cd ${newProjectFolder}&&npm install`;
  console.log('--> Executing command: ', command);
  
  exec(command, (err, stdout, stderr) => {
    if (err) {
      console.log(colors.red(err));
      process.exit(1);
    }
    console.log(colors.green('Project created successfully'));
    console.log(colors.green('Thanks for using the CLI'));
  });
}
```

Esse cara não faz nada demais, praticamente exporta uma função que recebe o nome do diretório que deve ser criado, verifica se ele já existe, e caso não exista copia todo projeto nodejs e instala suas dependências. Além de dar alguns console.log pontuais pra atualizar o usuário do processo.

Nossa atenção deve se voltar para o arquivo seguinte, na nossa pasta _bin_, onde teremos um _index.js_:

```js
#!/usr/bin/env node

const colors = require('colors');
const copy = require("../lib/copyFile");

const arguments = process.argv.splice(2);

if (arguments.length === 0) {
  console.log(colors.red("Please provide a project name"));
  process.exit(1);
} else {
  copy.copy(arguments[0]);
}
```

Note a primeira linha do código. Aqui fazemos uso de uma _shebang_ que nada mais é que declarar qual compilador deve interpretar nosso código. Em Windows, é suposto que não haja suporte pra essa funcionalidade e exatamente por isso o _NPM_ se encarregará de criar um arquivo _.cmd_ na pasta global do seu _NPM_ para que o seu sistema operacional use o node como interpretador pro nosso script.

Após importarmos as bibliotecas que faremos uso, note que pegamos nossos argumentos com _process.argv.splice(2)_. Acontece que ao nosso usuário executar _npx nossoPacote argumento_, a variável _process.argv_ deverá retornar um array de strings dos quais a primeira posição terá o caminho do interpretador nodejs e na segunda posição o caminho onde o arquivo está sendo executado. 

No caso, apenas nos interessa argumentos passados ao comando que executa nosso script, este que vem logo após os dados citados, portanto nos atentamos apenas a eles.

Após isso, apenas passamos o nome do projeto para a função que criamos anteriormente ou informamos ao usuário que é necessário ter o nome do projeto.

Simples, não?

### Configuração

Aqui, precisamos configurar nosso _package.json_. Segue o fio:

```json
{
  "name": "basic-node-ts-config",
  "version": "1.0.0",
  "description": "Configuração básica para um ambiente de desenvolvimento com NodeJs + TypeScript",
  "main": "./lib/copyFile.js",
  "preferGlobal": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "bin": {
    "create-node-basic": "./bin/index.js"
  },
  "keywords": [
    "node",
    "typescript",
    "config"
  ],
  "author": "Mateus Cardoso dos Santos",
  "license": "ISC",
  "dependencies": {
    "colors": "^1.4.0"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/mugarate12/basic-node-ts-config.git"
  },
  "bugs": {
    "url": "https://github.com/mugarate12/basic-node-ts-config/issues"
  },
  "homepage": "https://github.com/mugarate12/basic-node-ts-config"
}
```

Iremos destacar três propriedades:

- _main_: aqui é interessante termos nosso arquivo do qual exporta todas as funções do nosso projeto, e isso tem um fator importante: O _NPM_ não impede que nosso pacote seja instalado localmente, portanto, no _main_ devemos informar qual arquivo tem as funcionalidades, mesmo que não seja nosso objetivo que seja usado desta forma.
- _preferGlobal_: Como citado o _NPM_ não impede que nosso pacote seja instalando localmente, todavia podemos indicar que a nossa preferência é que ele seja um pacote global. Isso _ainda_ não impedirá uma instalação local, mas emitirá um _warning_ pra avisar que nós preferimos que o uso seja dado de outra forma.
- _bin_: Aqui é onde indicamos os comandos e quais scripts devem ser executados por eles. Como bem deve ter notado, esses devem ser os scripts que serão executados no terminal, então nos atentemos a referenciar corretamente o que queremos aqui.

## Testando localmente

Certamente será útil testar nosso pacote localmente antes de publicá-lo e para isso o _NPM_ tem um recurso muito interessante. Basta executa uma instalação normal, usando a flag _-g_ apontando para um diretório, ou melhor, para nosso projeto em questão. Ele instalará nosso pacote globalmente e o mesmo estará disponível pra testarmos nosso comando.

```shell
npm install -g path/to/project
```

Vale ainda dizer que uma vez feito isso, não há necessidade de instalar esse nosso pacote novamente caso mudemos alguma coisa no projeto. Nosso gerenciador de pacotes terá feito um link entre nosso diretório local e a instalação do pacote, bastando apenas se preocupar com o desenvolvimento.

Assumindo nosso projeto em questão, poderíamos testar com:

```shell
create-node-basic [NOME_DO_PROJETO]
```

## Publicando

Finalmente, estamos chegando nas ultimas tratativas!
Vou apenas citar o processo pois além de bastante simples, temos um ponto ou dois pra por em pauta. Os passos são:

- Crie uma conta no _NPM_ e verifique a mesma pelo email.
- Faça login na sua conta, executando um:

```shell
npm login
```
- Caso tudo tenha dado certo, o comando abaixo te deverá te retornar seu username:

```shell
npm whoami
```
- Uma vez que tudo esteja alinhado, publique o pacote com:

```shell
npm publish
```

E tudo pronto! Nosso projeto está devidamente publicado!

## Consumindo nosso pacote

Temos duas formas bastante simples de usar nosso pacote. O processo mais comum (e antigo) que seria instalar globalmente e fazer uso do mesmo:

```shell
npm install -g [NOME_DO_PACOTE]
```

```shell
create-node-basic [NOME_DO_PROJETO]
```

Ou ainda podemos usar o npx para isso, o que ficaria:

```shell
npx create-node-basic [NOME_DO_PROJETO]
```

## Observações

- Há um detalhe **muito importante** que deve ser comentado: caso estejamos publicando um pacote para fins de estudo e testes, precisamos retirá-lo do _NPM_ em até 72h. Isso porque após esse período não será possível removê-lo.
- Nosso principal objetivo foi explorar como essa funcionalidade pode nos ajudar em n's cenários, e portanto, adicione suas observações, críticas e comentários se julgar necessário.
- Crie um arquivo _.gitignore_ **e** um arquivo _.npmignore_, os dois cumprem a mesma função, cada um para a sua devida plataforma.
- Repositório: https://github.com/mugarate12/basic-node-ts-config

Agradeço sua atenção, abraços!