Padrões de Scripts Utilitários
Padrões comuns para escrever scripts utilitários Node.js - I/O de arquivos, globbing, saída colorida, prompts interativos e spinners.
Busque em todas as páginas da documentação
Padrões comuns para escrever scripts utilitários Node.js - I/O de arquivos, globbing, saída colorida, prompts interativos e spinners.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de referência rápida - pronto para copiar e colar.
# Instale o kit de ferramentas padrão para scripts utilitários
npm install --save-dev \
fast-glob \
chalk \
@inquirer/prompts \
ora
# Módulos nativos do Node - sem necessidade de instalação
# node:fs/promises - I/O de arquivos assíncrono
# node:path - caminhos multiplataforma
# node:url - fileURLToPath para __dirname em ESM// Esqueleto mínimo
import { readFile, writeFile } from "node:fs/promises";
import fg from "fast-glob";
import chalk from "chalk";
import { confirm } from "@inquirer/prompts";
import ora from "ora";Quando usar isso: Qualquer utilitário de linha de comando, codemod, etapa de build ou script de manutenção onde você precise de feedback de progresso e operações de arquivo seguras.
// scripts/process-ts-files.ts
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import fg from "fast-glob";
import chalk from "chalk";
import { confirm } from "@inquirer/prompts";
import ora from "ora";
async function main(): Promise<void> {
// 1. Encontrar arquivos
const files = await fg("src/**/*.ts", {
ignore: ["**/*.d.ts", "**/node_modules/**"],
absolute: true,
});
console.log(chalk.cyan(`Encontrados ${files.length} arquivos TypeScript.`));
if (files.length === 0) {
console.log(chalk.yellow("Nada a fazer."));
return;
}
// 2. Confirmar com o usuário
const proceed = await confirm({
message: `Processar ${files.length} arquivos?`,
default: false,
});
if (!proceed) {
console.log(chalk.gray("Abortado."));
return;
}
// 3. Processar com um spinner
const spinner = ora("Processando arquivos...").start();
let changed = 0;
try {
for (const file of files) {
const contents = await readFile(file, "utf8");
const next = contents.replace(/\r\n/g, "\n");
if (next !== contents) {
await writeFile(file, next, "utf8");
changed += 1;
}
spinner.text = `Processando ${path.basename(file)}`;
}
spinner.succeed(chalk.green(`Normalizados ${changed} arquivo(s).`));
} catch (err) {
spinner.fail(chalk.red("Falha no processamento."));
throw err;
}
}
main().catch((err: unknown) => {
console.error(chalk.red("Erro no script:"), err);
process.exitCode = 1;
});O que isso demonstra:
node:fs/promises (nunca readFileSync em um script assíncrono)fast-glob para correspondência de arquivos rápida e flexível com padrões de ignorar@inquirer/prompts (substitui a API de objeto legada inquirer.prompt())ora com atualizações de text dinâmicas e estados succeed / failprocess.exitCode em vez de chamar process.exit()node:fs/promises expõe todas as funções fs como variantes que retornam promessas. Não há mais necessidade de util.promisify.fast-glob é a biblioteca glob mais rápida e amplamente utilizada para Node. Ela retorna um string[] simples por padrão (ou Entry[] com objectMode: true).chalk v5 é apenas ESM. Se você estiver usando CommonJS, fixe o chalk na v4 ou converta seu projeto para ESM.@inquirer/prompts é o substituto moderno para o pacote monolítico inquirer. Cada prompt (input, confirm, select, checkbox) é importado como uma função nomeada que retorna uma promessa.ora escreve no stderr por padrão e detecta TTY para desabilitar animações em CI. Você pode forçar o estado via { isEnabled: process.stdout.isTTY }.Lendo e escrevendo arquivos JSON:
import { readFile, writeFile } from "node:fs/promises";
interface Config {
name: string;
version: string;
}
const raw = await readFile("config.json", "utf8");
const config = JSON.parse(raw) as Config;
config.version = "2.0.0";
await writeFile("config.json", JSON.stringify(config, null, 2) + "\n");Processamento baseado em stream para arquivos grandes:
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
const rl = createInterface({
input: createReadStream("huge.log"),
crlfDelay: Infinity,
});
for await (const line of rl) {
// processa uma linha por vez - a memória permanece plana
}Caminhamento recursivo de diretório sem uma biblioteca glob:
import { readdir } from "node:fs/promises";
// Node 20+ suporta recursive: true
const entries = await readdir("src", { recursive: true, withFileTypes: true });
const files = entries.filter((e) => e.isFile()).map((e) => e.name);Barras de progresso com cli-progress:
import cliProgress from "cli-progress";
const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
bar.start(files.length, 0);
for (const file of files) {
await process(file);
bar.increment();
}
bar.stop();Detectando TTY para silenciar decoração em CI:
const isInteractive = process.stdout.isTTY && !process.env.CI;
const spinner = ora({ text: "Working...", isEnabled: isInteractive });Strings de template com chalk:
console.log(`${chalk.bold.blue("info")} Encontrados ${chalk.yellow(files.length)} arquivos`);fs.readFile(path, "utf8") retorna Promise<string>. Sem a codificação, retorna Promise<Buffer>. Sempre passe "utf8" quando quiser uma string.fast-glob retorna Promise<string[]> quando chamado sem opções. Com { objectMode: true } ele retorna Promise<Entry[]>.@inquirer/prompts infere os tipos de retorno do prompt: confirm() retorna Promise<boolean>, input() retorna Promise<string>, e select<T>() aceita um genérico para o valor da escolha.(err: unknown) e refine antes de registrar.Coisas que vão te morder. Cada armadilha inclui o que dá errado, por que acontece e a correção.
Usando fs.readFileSync em um script assíncrono - Bloqueia o event loop, anula a concorrência e faz os spinners gaguejarem. Correção: Use import { readFile } from "node:fs/promises" e await nele. APIs síncronas são apropriadas apenas no código de inicialização antes de qualquer trabalho assíncrono.
Esquecer de usar await em uma chamada fs.promises - writeFile(path, data) sem await retorna uma promessa não tratada. O script sai antes que a escrita seja concluída, perdendo dados silenciosamente. Correção: Sempre use await e habilite @typescript-eslint/no-floating-promises para capturar isso no nível do lint.
Cores do Chalk não aparecem em CI - Chalk auto-detecta TTY e desabilita cores em ambientes não interativos. Logs no GitHub Actions ou CircleCI aparecem sem cor por design. Correção: Defina FORCE_COLOR=1 como uma variável de ambiente em CI, ou use new Chalk(\{ level: 3 \}) para forçar o nível de cor.
Caminhos relativos vs. absolutos em glob - fast-glob retorna caminhos relativos ao cwd por padrão. Passar esses para fs.readFile funciona apenas se o script for executado do mesmo diretório. Correção: Use { absolute: true } ou resolva manualmente com path.resolve(process.cwd(), file).
Emoji quebrando terminais Windows - cmd.exe legado e o PowerShell mais antigo renderizam emojis como bytes embaralhados. Correção: Detecte o Windows via process.platform === "win32" e volte para ASCII, ou exija o Windows Terminal (UTF-8 por padrão).
Chamando process.exit() antes que as escritas sejam concluídas - process.exit(1) termina imediatamente, truncando stdout e chamadas fs.writeFile pendentes. Correção: Defina process.exitCode = 1 e deixe o event loop drenar naturalmente.
Outras maneiras de resolver o mesmo problema - e quando cada uma é a melhor escolha.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| execa | Você precisa executar comandos externos com boa DX | Você só precisa de I/O de arquivo no processo |
| zx | Você quer a ergonomia de shell-script com templates JS | Você precisa de tipagem estrita ou prefere APIs explícitas |
Bun.file / Bun shell | Você está executando no Bun, não no Node | Você tem como alvo o Node.js em produção |
node:readline | Você precisa de entrada interativa de linha única sem dependências | Você quer prompts ricos (select, checkbox, etc.) |
node:fs/promises é a API moderna canônica - sem necessidade de encapsulamento.fs tem uma variante de promessa já exportada.util.promisify é útil apenas para APIs de callback de terceiros mais antigas.fs.readdir(\{ recursive: true \}) (Node 20+) funciona para casos simples, mas não tem suporte a padrões glob.fast-glob suporta **, *, expansão de chaves, negação e padrões de ignorar.fast-glob é mais simples e rápido.inquirer legado expõe uma API monolítica inquirer.prompt([\{ type, name, message \}]).@inquirer/prompts expõe cada prompt como uma função nomeada: confirm(), input(), select()."type": "module" em package.json).require() do CommonJS.await em uma chamada fs.writeFile, ou chamou process.exit() muito cedo.await.await em todas as chamadas assíncronas e use process.exitCode = 1 em vez de process.exit(1).isTTY é falso.FORCE_COLOR=1 no seu ambiente de CI, ou configure explicitamente um nível de cor mais alto."utf8": Promise<string>.Promise<Buffer>.select() aceita um parâmetro genérico para o valor escolhido.const choice = await select<"a" | "b">(\{ message, choices \}).spinner.text dentro do seu loop - ora re-renderiza a cada tick.spinner.succeed(msg) ou spinner.fail(msg) para parar com um estado final.cli-progress em vez disso.createReadStream + readline.createInterface para iteração linha por linha.JSON.stringify(obj, null, 2) + "\n" - a nova linha final corresponde às convenções POSIX.await em writeFile e use codificação "utf8".process.exit(code) termina imediatamente, truncando escritas pendentes de stdout e operações assíncronas.process.exitCode = code define o código de saída, mas permite que o event loop drene naturalmente.Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥