This content originally appeared on Twilio Blog and was authored by Luís Leão
As interfaces de linha de comando (CLIs) integradas ao Node.js permitem automatizar tarefas repetitivas e, ao mesmo tempo, aproveitar o vasto ecossistema Node.js. Graças aos gerenciadores de pacotes como npm
e yarn
, podem ser facilmente distribuídos e consumidos em várias plataformas. Nesta publicação, veremos por que você pode querer uma CLI, como usar o Node.js para ela, alguns pacotes úteis e como você pode distribuir sua nova CLI.
Por que criar CLIs com Node.js
Uma das razões pelas quais o Node.js ficou tão popular é o rico ecossistema de pacotes com mais de 900 mil pacotes no npm
registry. Ao criar CLIs no Node.js, é possível acessar esse ecossistema, incluindo sua grande quantidade de pacotes focados em CLI. Entre outros:
inquirer
,enquirer
ou prompts para solicitações de entrada complexasemail-prompt
para solicitações de entrada de e-mail convenienteschalk
ou kleur para saída coloridaora
para ter lindos controles giratóriosboxen
para caixas de desenho ao redor da saídastmux
para umtmux
como UIlistr
para listas de progressoink
para criar CLIs com Reactmeow
ouarg
para análise de argumento básicocommander
eyargs
para analisar argumentos complexos e oferecer suporte a subcomandosoclif
, uma estrutura para a construção de CLIs extensíveis por Heroku (gluegun como alternativa)
Além disso, há muitas maneiras convenientes de consumir CLIs publicadas para npm
de yarn
e de npm
. Use como exemplo create-flex-plugin
, uma CLI que você pode usar para iniciar um plugin para o Twilio Flex. É possível instalá-lo como um comando global:
# Using npm:
npm install -g create-flex-plugin
# Using yarn:
yarn global add create-flex-plugin
# Afterwards you will be able to consume it:
create-flex-plugin
Ou como dependências específicas do projeto:
# Using npm:
npm install create-flex-plugin --save-dev
# Using yarn:
yarn add create-flex-plugin --dev
# Afterwards the command will be in
./node_modules/.bin/create-flex-plugin
# Or via npx using npm:
npx create-flex-plugin
# And via yarn:
yarn create-flex-plugin
Na verdade npx
oferece suporte à execução de CLIs mesmo quando ainda não estão instalados. Basta executar npx create-flex-plugin
e ele o baixará em um cache se não conseguir encontrar uma versão instalada local ou globalmente.
Por último, desde a versão 6.1 do npm
, o npm init
e yarn
oferecem suporte a uma forma de iniciar projetos usando CLIs que são chamadas de create-*
. Como exemplo, para nosso create-flex-plugin
, só precisamos definir:
# Using Node.js
npm init flex-plugin
# Using Yarn:
yarn create flex-plugin
Como configurar sua primeira CLI
Se preferir seguir um tutorial em vídeo, confira este tutorial no nosso canal no YouTube.
Agora que já vimos por que criar uma CLI usando Node.js, vamos colocar a mão na massa. Usaremos o npm
neste tutorial, mas há comandos equivalentes para a maioria das coisas no yarn
. Veja se o Node.js e npm
estão instalados no sistema.
Neste tutorial, criaremos uma CLI que inicializa novos projetos de acordo com suas preferências executando npm init @your-username/project
.
Inicie um novo projeto Node.js ao executar:
mkdir create-project && cd create-project
npm init --yes
Depois, crie um diretório chamado src/
na raiz do projeto e coloque um arquivo chamado cli.js
com o seguinte código:
export function cli(args) {
console.log(args);
}
Mais tarde, nessa mesma parte, analisaremos a lógica e, em seguida, acionaremos a lógica de negócios real. Em seguida, precisamos criar o ponto de entrada da CLI. Crie um novo diretório bin/
na raiz do projeto e crie um novo arquivo dentro chamado create-project
. Insira as seguintes linhas de código:
#!/usr/bin/env node
require = require('esm')(module /*, options*/);
require('../src/cli').cli(process.argv);
Há algumas questões neste pequeno trecho. Primeiro, precisamos de um módulo chamado esm
que permita usar import
nos outros arquivos. Não está diretamente relacionado à criação de CLIs, mas usaremos os módulos ES neste tutorial, e o pacote esm
permite isso sem a necessidade de transpor versões Node.js sem o suporte. Depois, precisaremos do arquivo cli.js
e chamaremos a função cli
exposta com process.argv
, que é um array de todos os argumentos passados para esse script a partir da linha de comando.
Antes de testar o script, precisaremos instalar a dependência esm
ao executar:
npm install esm
Também precisamos informar ao gerenciador de pacotes que estamos expondo um script de CLI. É preciso adicionar a entrada apropriada no package.json
. Além disso, atualize as propriedades description
, name
, keyword
e main
:
{
"name": "@your_npm_username/create-project",
"version": "1.0.0",
"description": "A CLI to bootstrap my new projects",
"main": "src/index.js",
"bin": {
"@your_npm_username/create-project": "bin/create-project",
"create-project": "bin/create-project"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"cli",
"create-project"
],
"author": "YOUR_AUTHOR",
"license": "MIT",
"dependencies": {
"esm": "^3.2.18"
}
}
Se observar a chave bin
, estamos passando um objeto com dois pares de chave/valor. Eles definem os comandos da CLI instalados pelo gerenciador de pacotes. Neste caso, registraremos o mesmo script para dois comandos. Com o uso do nosso próprio escopo do npm
, usando nosso nome de usuário e uma vez como o comando genérico create-project
por questões práticas.
Depois disso, podemos testar o script. A maneira mais fácil é usar o comando npm link
. Execute no terminal dentro do projeto:
npm link
Dessa forma, um link simbólico é instalado para o projeto atual, de modo que não há necessidade de nova execução quando atualizarmos nosso código. Depois de executar o npm link
, os comandos CLI devem estar disponíveis. Tente executar:
create-project
A saída deve ser parecida com esta:
[ '/usr/local/Cellar/node/11.6.0/bin/node',
'/Users/dkundel/dev/create-project/bin/create-project' ]
Observe que ambos os caminhos serão diferentes para você, dependendo de onde está o seu projeto e onde o Node.js está instalado. Esta matriz será mais longa com cada argumento adicionado. Tente executar:
create-project --yes
A saída deve mostrar o novo argumento:
[ '/usr/local/Cellar/node/11.6.0/bin/node',
'/Users/dkundel/dev/create-project/bin/create-project',
'--yes' ]
Análise de argumentos e processamento de entrada
Agora estamos prontos para analisar os argumentos que estão sendo transferidos para o script e podemos começar a entender eles. A CLI oferece suporte a um argumento e algumas opções:
[template]
: oferecemos suporte a diferentes modelos prontos para uso. Se não for aprovado, solicitarão ao usuário que selecione um template (modelo)--git
: executagit init
para instanciar um novo projeto git--install
: instala automaticamente todas as dependências do projeto--yes
: preenche todos os prompts e utiliza opções padrão
Para este projeto, usamos inquirer
para solicitar valores faltantes e a biblioteca arg
para analisar nossos argumentos de CLI. Instale as dependências faltantes ao executar:
npm install inquirer arg
Primeiro, vamos escrever a lógica que analisa os argumentos em um objeto options
com a qual podemos trabalhar. Adicione o seguinte código a cli.js
:
import arg from 'arg';
function parseArgumentsIntoOptions(rawArgs) {
const args = arg(
{
'--git': Boolean,
'--yes': Boolean,
'--install': Boolean,
'-g': '--git',
'-y': '--yes',
'-i': '--install',
},
{
argv: rawArgs.slice(2),
}
);
return {
skipPrompts: args['--yes'] || false,
git: args['--git'] || false,
template: args._[0],
runInstall: args['--install'] || false,
};
}
export function cli(args) {
let options = parseArgumentsIntoOptions(args);
console.log(options);
}
Tente executar create-project --yes
e possível visualizar skipPrompt
se tornar true
ou tente transferir outro argumento, como create-project cli
e a propriedade template
deve ser definida.
Agora que podemos analisar os argumentos da CLI, precisamos adicionar a funcionalidade para solicitar as informações faltantes, bem como ignorar o prompt e recorrer aos argumentos padrão se o sinalizador --yes
for transferido. Adicione o seguinte código ao arquivo cli.js
:
import arg from 'arg';
import inquirer from 'inquirer';
function parseArgumentsIntoOptions(rawArgs) {
// ...
}
async function promptForMissingOptions(options) {
const defaultTemplate = 'JavaScript';
if (options.skipPrompts) {
return {
...options,
template: options.template || defaultTemplate,
};
}
const questions = [];
if (!options.template) {
questions.push({
type: 'list',
name: 'template',
message: 'Please choose which project template to use',
choices: ['JavaScript', 'TypeScript'],
default: defaultTemplate,
});
}
if (!options.git) {
questions.push({
type: 'confirm',
name: 'git',
message: 'Initialize a git repository?',
default: false,
});
}
const answers = await inquirer.prompt(questions);
return {
...options,
template: options.template || answers.template,
git: options.git || answers.git,
};
}
export async function cli(args) {
let options = parseArgumentsIntoOptions(args);
options = await promptForMissingOptions(options);
console.log(options);
}
Salve o arquivo e execute o create-project
, e você deve receber uma solicitação de seleção de template (modelo):
E depois será perguntado se deseja inicializar o git
. Depois de selecionar ambos, verá uma saída como esta impressa:
{ skipPrompts: false,
git: false,
template: 'JavaScript',
runInstall: false }
Tente executar o mesmo comando com -y
e ignore os prompts. Em vez disso, você verá imediatamente a saída de opções determinadas.
Como gravar a lógica
Agora que podemos determinar as respectivas opções por meio de prompts e argumentos de linha de comando, vamos gravar a lógica real que cria os projetos. Nossa CLI grava em um diretório existente semelhante ao npm init
e copia todos os arquivos de um diretório templates
no projeto. Vamos permitir que o diretório de destino também seja modificado por meio das opções, caso queira reutilizar a mesma lógica dentro de outro projeto.
Antes de gravar a lógica real, crie um diretório templates
na raiz do projeto e coloque dois diretórios com os nomes typescript
e javascript
. Essas são as versões em letras minúsculas dos dois valores escolhidos pelo usuário. Esta publicação usa esses nomes, mas fique à vontade para usar outros nomes de sua escolha. Dentro desse diretório, insira qualquer package.json
que você gostaria de usar como base do seu projeto e qualquer tipo de arquivo que queira copiar para o projeto. Mais tarde, nosso código simplesmente copia esses arquivos no novo projeto. Se precisar de inspiração, você pode conferir meus arquivos em https://github.com/dkundel/create-project.
Para fazer a cópia recursiva dos arquivos, usamos uma biblioteca chamada ncp
. Essa biblioteca oferece suporte para a cópia recursiva entre plataformas e até mesmo tem um sinalizador para forçar a substituição de arquivos existentes. Além disso, instalamos chalk
para saída colorida. Para instalar as dependências, execute:
npm install ncp chalk
Colocaremos toda a lógica principal em um arquivo main-js
dentro do diretório src/
do projeto. Crie o novo arquivo e adicione o seguinte código:
import chalk from 'chalk';
import fs from 'fs';
import ncp from 'ncp';
import path from 'path';
import { promisify } from 'util';
const access = promisify(fs.access);
const copy = promisify(ncp);
async function copyTemplateFiles(options) {
return copy(options.templateDirectory, options.targetDirectory, {
clobber: false,
});
}
export async function createProject(options) {
options = {
...options,
targetDirectory: options.targetDirectory || process.cwd(),
};
const currentFileUrl = import.meta.url;
const templateDir = path.resolve(
new URL(currentFileUrl).pathname,
'../../templates',
options.template.toLowerCase()
);
options.templateDirectory = templateDir;
try {
await access(templateDir, fs.constants.R_OK);
} catch (err) {
console.error('%s Invalid template name', chalk.red.bold('ERROR'));
process.exit(1);
}
console.log('Copy project files');
await copyTemplateFiles(options);
console.log('%s Project ready', chalk.green.bold('DONE'));
return true;
}
Esse código exporta uma nova função chamada createProject
, que primeiro verifica se o modelo especificado é realmente um modelo disponível, verificando o acesso read
(fs.constants.R_OK
) usando fs.access
e, em seguida, copia os arquivos para o diretório de destino usando ncp
. Além disso, registramos alguma saída colorida informando DONE Project ready
quando copiarmos os arquivos com êxito.
Depois, atualize cli.js
para chamar a nova função createProject
:
import arg from 'arg';
import inquirer from 'inquirer';
import { createProject } from './main';
function parseArgumentsIntoOptions(rawArgs) {
// ...
}
async function promptForMissingOptions(options) {
// ...
}
export async function cli(args) {
let options = parseArgumentsIntoOptions(args);
options = await promptForMissingOptions(options);
await createProject(options);
}
Para testar nosso progresso, crie um novo diretório em algum lugar como ~/test-dir
no sistema e execute dentro dele o comando usando um de seus modelos. Por exemplo:
create-project typescript --git
Aparece uma confirmação de que o projeto foi criado e os arquivos devem ser copiados para o diretório.
Agora, há mais duas etapas que queremos fazer na nossa CLI. Opcionalmente, queremos inicializar o git
e instalar nossas dependências. Para isso, usaremos mais três dependências:
execa
que permite executar facilmente comandos externos comogit
pkg-install
para acionaryarn install
ounpm install
, dependendo do que o usuário usalistr
que permite especificar uma lista de tarefas e fornecer ao usuário uma visão geral clara do progresso
Instale as dependências ao executar:
npm install execa pkg-install listr
Depois, atualize main.js
para conter o seguinte código:
import chalk from 'chalk';
import fs from 'fs';
import ncp from 'ncp';
import path from 'path';
import { promisify } from 'util';
import execa from 'execa';
import Listr from 'listr';
import { projectInstall } from 'pkg-install';
const access = promisify(fs.access);
const copy = promisify(ncp);
async function copyTemplateFiles(options) {
return copy(options.templateDirectory, options.targetDirectory, {
clobber: false,
});
}
async function initGit(options) {
const result = await execa('git', ['init'], {
cwd: options.targetDirectory,
});
if (result.failed) {
return Promise.reject(new Error('Failed to initialize git'));
}
return;
}
export async function createProject(options) {
options = {
...options,
targetDirectory: options.targetDirectory || process.cwd()
};
const templateDir = path.resolve(
new URL(import.meta.url).pathname,
'../../templates',
options.template
);
options.templateDirectory = templateDir;
try {
await access(templateDir, fs.constants.R_OK);
} catch (err) {
console.error('%s Invalid template name', chalk.red.bold('ERROR'));
process.exit(1);
}
const tasks = new Listr([
{
title: 'Copy project files',
task: () => copyTemplateFiles(options),
},
{
title: 'Initialize git',
task: () => initGit(options),
enabled: () => options.git,
},
{
title: 'Install dependencies',
task: () =>
projectInstall({
cwd: options.targetDirectory,
}),
skip: () =>
!options.runInstall
? 'Pass --install to automatically install dependencies'
: undefined,
},
]);
await tasks.run();
console.log('%s Project ready', chalk.green.bold('DONE'));
return true;
}
Assim, executa git init
sempre que --git
for transferido ou o usuário escolher git
no prompt e executa npm install
ou yarn
sempre que o usuário transferir --install
, caso contrário, ele ignora a tarefa com uma mensagem informando ao usuário para transferir --install
se quiser a instalação automática.
Tente excluir primeiro sua pasta de teste existente e criar uma nova. Em seguida, execute:
create-project typescript --git --install
Agora você visualiza uma pasta .git
na pasta que indica que o git
foi inicializado e uma pasta node_modules
com suas dependências especificadas no package.json
instalado.
Parabéns, você já está pronto para a sua primeira CLI!
Se quiser tornar seu código consumível como um módulo real para que outros possam reutilizar sua lógica no código, teremos que adicionar um arquivo index.js
ao diretório /src
que expõe o conteúdo de main.js
:
require = require('esm')(module);
require('../src/cli').cli(process.argv);
O que vem a seguir?
Agora que você já tem seu código CLI pronto, você tem algumas opções a partir daqui. Se quiser apenas para uso próprio e não quiser compartilhar com o mundo, é possível continuar com a opção de usar npm link
. Na verdade, tente executar npm init project
e ele deve acionar seu código.
Se quiser compartilhar seus templates (modelos) com o mundo, envie seu código para o GitHub e use-o a partir daí ou, ainda melhor, envie-o como um pacote com escopo para o registro npm
com o npm publish
. Antes disso, verifique se adiciona uma chave files
no seu package.json
para especificar quais arquivos devem ser publicados.
},
"files": [
"bin/",
"src/",
"templates/"
]
}
Se quiser verificar quais arquivos são publicados, execute npm pack --dry-run
e verifique a saída. Depois, use npm publish
para publicar sua CLI. Você pode encontrar meu projeto em @dkundel/create-project
ou tentar executar npm init @dkundel/project
.
Há também muitas funcionalidades que você pode adicionar. No meu caso, adicionei algumas dependências que criam um arquivo LICENSE
, CODE_OF_CONDUCT.md
e .gitignore
para mim. Você pode encontrar o código-fonte no GitHub ou verificar algumas das bibliotecas mencionadas acima para obter outras funcionalidades. Se tiver uma biblioteca não listada e acha que não deveria ficar de fora da lista, ou se quiser me mostrar sua própria CLI, fique à vontade para enviar uma mensagem pelo e-mail lleao@twilio.com!
Este artigo foi traduzido do original "How to build a CLI with Node.js". Enquanto melhoramos nossos processos de tradução, adoraríamos receber seus comentários em help@twilio.com - contribuições valiosas podem render brindes da Twilio.
This content originally appeared on Twilio Blog and was authored by Luís Leão
Luís Leão | Sciencx (2021-07-12T17:15:43+00:00) Como criar uma CLI com Node.js. Retrieved from https://www.scien.cx/2021/07/12/como-criar-uma-cli-com-node-js/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.