Pitch: 7 lições de um CLI que edita app.json, Podfile e Gradle sem estragar o projeto de ninguém
Se você já escreveu um script que "só adiciona uma linha" num arquivo de configuração de outra pessoa, sabe que essa linha nunca é só uma linha. O arquivo tem comentários, um fim de linha diferente, um formato que o seu código não esperava, e quem rodou o script descobre o estrago dias depois.
A gente escreveu um CLI de setup de push para apps React Native. Ele instala pacotes e mexe em app.json, Podfile, nos dois build.gradle e no AppDelegate de projetos que não são nossos. Abaixo estão as lições que valem para qualquer ferramenta que edita arquivos de projeto: codemod, gerador, instalador de plugin. Cada uma vem com o caso real que nos ensinou e uma dica para quem for fazer o mesmo.
Lição 1: deixe o projeto responder, não a pessoa
O caminho fácil seria perguntar "seu app usa Expo?". Preferimos ler o package.json: com expo, um fluxo; com react-native e sem expo, outro; sem nenhum dos dois, o CLI para com exit code 1.
flowchart TD
A[Lê o package.json] --> B{Tem expo?}
B -- sim --> C[Fluxo Expo]
B -- não --> D{Tem react-native?}
D -- sim --> E[Fluxo React Native CLI]
D -- não --> F[Para com exit code 1]
Para o seu CLI: toda pergunta que o próprio projeto responde é uma chance a menos de a pessoa errar. Pergunte só o que não está escrito em lugar nenhum.
Lição 2: quando não dá para saber, não chute
Para casar a versão dos pacotes com o SDK do Expo, o CLI usa npx expo install em vez de npm install, e lê a versão do expo instalado. Antes da instalação, ele só confia no range do package.json quando o range aponta uma versão (~57.0.26, ^53.0.0). Com >=52 <58 ou latest, ele não adivinha: segue com um aviso dizendo que não sabe.
Para o seu CLI: um aviso honesto ("não consegui ler a versão") custa uma linha. Uma suposição errada custa uma tarde de quem está do outro lado.
Lição 3: JSON.parse + JSON.stringify não é uma edição, é uma reescrita
O jeito óbvio de adicionar um item ao app.json é ler, mexer no objeto e salvar. Veja o que isso faz com um arquivo inocente:
JSON.stringify(
JSON.parse('{"version": 1.0, "id": 12345678901234567890, "name": "Caf\\u00e9"}'),
null,
2
)
// {
// "version": 1,
// "id": 12345678901234567000,
// "name": "Café"
// }
O 1.0 virou 1, o inteiro grande perdeu precisão e o escape virou outro texto. Também somem as chaves repetidas, e mudam a indentação, o fim de linha (CRLF ou LF) e o BOM.
A solução foi inserir um único pedaço de texto no lugar certo da lista plugins, copiando o layout dos itens ao redor. Todo o resto do arquivo fica byte a byte como estava.
Para o seu CLI: se o arquivo é de outra pessoa, trate-o como texto e mude o mínimo. Uma checagem útil para quem for fazer isso: depois de inserir, faça o parse do resultado e compare com o original mais o item novo; se der diferente, não escreva.
Lição 4: a ferramenta que você chama também edita arquivos
Uma surpresa no caminho: o próprio npx expo install adiciona plugins ao app.json e reescreve o arquivo com o writer dele. Isso perde comentários e muda fins de linha e números.
A saída foi guardar o arquivo antes de chamar o Expo: os bytes, se ele era um link simbólico e as permissões. Se algo mudou, o CLI devolve tudo e avisa ("Expo rewrote app.json on its own").
Depois veio um segundo susto, do npm. O npx expo install às vezes termina a instalação e para quando react ou react-native precisam mudar de versão, sem chegar a adicionar os plugins. Hoje o CLI roda o mesmo comando uma segunda vez nesse caso e avisa que fez isso.
Para o seu CLI: antes de chamar outra ferramenta, fotografe os arquivos que ela pode tocar e compare depois. Não dá para confiar no que você não controla.
Lição 5: na dúvida, o passo é da pessoa
app.config.js e app.config.ts são código, então o CLI nunca os edita. Ele imprime a linha exata para a pessoa adicionar e termina com exit code 2, que quer dizer "ficou um passo manual". O mesmo vale para:
- JSON com comentário ou vírgula sobrando;
- arquivo somente leitura;
- qualquer JSON que ele não consiga estender sem mudar outra coisa.
A regra que mais nos ajudou: o CLI nunca diz "pronto" quando não tem certeza. Se o arquivo de config faz ...config, o texto mostra que o plugin está lá, mas não garante que ele chega ao final. Nesse caso o CLI pede para conferir.
Para o seu CLI: separe três saídas, porque CI e humanos tratam cada uma de um jeito: deu certo (0), recusei (1) e fiz o que pude, mas falta um passo seu (2).
Lição 6: em arquivo nativo, marque, guarde e escreva de uma vez
No fluxo React Native CLI, cada mudança entra entre marcadores. No android/app/build.gradle fica assim:
// espresso-notify:begin google-services
apply plugin: 'com.google.gms.google-services'
// espresso-notify:end
Além disso, o CLI:
- só mexe num arquivo que tem a forma do template do React Native; um arquivo diferente vira passo manual;
- antes da primeira escrita, faz uma cópia
.bakcom data e hora ao lado do arquivo; - escreve num arquivo temporário e depois renomeia, para nunca deixar um arquivo pela metade;
- mantém o fim de linha que o arquivo já tinha e nunca apaga uma linha da pessoa.
Os marcadores são o que permite rodar de novo com segurança: o CLI sabe exatamente o que é dele.
O AppDelegate.swift ensinou mais uma. O FirebaseApp.configure() derruba o app na abertura quando o GoogleService-Info.plist não está no projeto. Esse arquivo vem do Firebase de quem usa, e o CLI não tem como criá-lo. Por isso a chamada entra protegida:
if Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist") != nil {
FirebaseApp.configure()
}
O app abre antes de o arquivo existir e passa a usar o Firebase quando ele chega.
Para o seu CLI: nunca deixe o projeto num estado em que ele não abre só porque falta um passo que a pessoa ainda vai fazer.
Lição 7: rodar duas vezes é o teste mais barato que existe
Todo passo é idempotente: a segunda execução não toca em arquivo nenhum e diz isso. O --dry-run mostra cada mudança sem escrever nada.
Em 23/10 rodamos o CLI em apps novos do template React Native CLI: React Native 0.87.1 no simulador iOS 27 e num emulador Android 16, e React Native 0.79.7 no emulador Android. Em cada um:
- o dry run não mudou nada;
- a execução real adicionou só as linhas entre os marcadores;
- a segunda execução não mudou nada;
- o app abriu e pediu a permissão de notificação.
O código é Node puro, sem nenhuma dependência (os prompts usam node:readline). Tudo que toca o mundo (pasta, spawn, streams, fs, path) entra por injeção. Assim os testes simulam o Windows com path.win32 sem precisar de uma máquina Windows.
Para o seu CLI: "rodar de novo não muda nada" é um teste curto que, na nossa experiência, pega muito bug de edição de arquivo.
Dois detalhes que custaram caro
- Windows: o
npxsobe pelocmd.exedo sistema, nunca por um achado no PATH, e com argumentos fixos. A pasta do projeto entra só como diretório de trabalho, nunca dentro do comando, então pasta com espaço e acento funciona. - Segredos: o CLI recusa uma chave secreta (
sk_...) e nunca grava a chave pública em disco; só imprime o trecho de código com ela.
Sobre o projeto
O CLI é o assistente de setup do SDK React Native do Espresso Notify, um serviço de push para iOS, Android e web com dashboard e API. É grátis até 10 mil aparelhos. Para quem quiser ver o resultado das lições acima:
npx @espresso-notify/react-native@latest setup --dry-run
O --dry-run mostra o que ele faria sem mudar nada. A documentação, com cada passo e os códigos de saída, está aqui:
- Expo: https://docs.espressonotify.com/docs/react-native/expo
- React Native CLI: https://docs.espressonotify.com/docs/react-native/bare
E uma pergunta para quem já automatizou edição de arquivo de projeto: vocês preferem inserir texto, como fizemos, ou usar um parser que preserva formato? Que Podfile ou Gradle "fora do template" vocês já encontraram por aí? Respondo qualquer dúvida nos comentários.