Criando e publicando um pacote NPM global para executar com CLI
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:
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:
#!/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:
{
"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.
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:
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:
npm login
- Caso tudo tenha dado certo, o comando abaixo te deverá te retornar seu username:
npm whoami
- Uma vez que tudo esteja alinhado, publique o pacote com:
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:
npm install -g [NOME_DO_PACOTE]
create-node-basic [NOME_DO_PROJETO]
Ou ainda podemos usar o npx para isso, o que ficaria:
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!