Patrones de Scripts de Utilidad
Patrones comunes para escribir scripts de utilidad en Node.js - E/S de archivos, globbing, salida coloreada, indicaciones interactivas y spinners.
Busca en todas las páginas de la documentación
Patrones comunes para escribir scripts de utilidad en Node.js - E/S de archivos, globbing, salida coloreada, indicaciones interactivas y spinners.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
# Instala el kit de herramientas de scripts de utilidad estándar
npm install --save-dev \
fast-glob \
chalk \
@inquirer/prompts \
ora
# Integrados de Node - no se requiere instalación
# node:fs/promises - E/S de archivos asincrónica
# node:path - rutas multiplataforma
# node:url - fileURLToPath para __dirname en 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";Cuándo usarlo: Cualquier utilidad CLI, codemod, paso de construcción o script de mantenimiento donde necesites retroalimentación de progreso y operaciones de archivo 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. Busca archivos
const files = await fg("src/**/*.ts", {
ignore: ["**/*.d.ts", "**/node_modules/**"],
absolute: true,
});
console.log(chalk.cyan(`Se encontraron ${files.length} archivos TypeScript.`));
if (files.length === 0) {
console.log(chalk.yellow("Nada que hacer."));
return;
}
// 2. Confirma con el usuario
const proceed = await confirm({
message: `¿Procesar ${files.length} archivos?`,
default: false,
});
if (!proceed) {
console.log(chalk.gray("Abortado."));
return;
}
// 3. Procesa con un spinner
const spinner = ora("Procesando archivos...").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 = `Procesando ${path.basename(file)}`;
}
spinner.succeed(chalk.green(`Normalizados ${changed} archivo(s).`));
} catch (err) {
spinner.fail(chalk.red("Error al procesar."));
throw err;
}
}
main().catch((err: unknown) => {
console.error(chalk.red("Error de script:"), err);
process.exitCode = 1;
});Qué demuestra esto:
node:fs/promises (nunca readFileSync en un script asincrónico)fast-glob para coincidencia de archivos rápida y flexible con patrones ignore@inquirer/prompts (reemplaza la API de objeto antigua de inquirer.prompt())ora con actualizaciones de text dinámicas y estados succeed / failprocess.exitCode en lugar de llamar a process.exit()node:fs/promises expone todas las funciones fs como variantes que devuelven promesas. Ya no se necesita util.promisify.fast-glob es la librería glob más rápida y ampliamente utilizada para Node. Devuelve un string[] simple por defecto (o Entry[] con objectMode: true).chalk v5 es solo ESM. Si estás en CommonJS, fija chalk v4 o convierte tu proyecto a ESM.@inquirer/prompts es el reemplazo moderno del paquete monolítico inquirer. Cada prompt (input, confirm, select, checkbox) se importa como una función nombrada que devuelve una promesa.ora escribe en stderr por defecto y detecta TTY para desactivar animaciones en CI. Puedes forzar el estado vía { isEnabled: process.stdout.isTTY }.Lectura y escritura de archivos 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");Procesamiento basado en streams para archivos 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) {
// procesa una línea a la vez - la memoria se mantiene plana
}Recorrido recursivo de directorios sin una librería glob:
import { readdir } from "node:fs/promises";
// Node 20+ admite recursive: true
const entries = await readdir("src", { recursive: true, withFileTypes: true });
const files = entries.filter((e) => e.isFile()).map((e) => e.name);Barras de progreso con 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 la decoración en CI:
const isInteractive = process.stdout.isTTY && !process.env.CI;
const spinner = ora({ text: "Working...", isEnabled: isInteractive });Cadenas de plantilla con chalk:
console.log(`${chalk.bold.blue("info")} Found ${chalk.yellow(files.length)} files`);fs.readFile(path, "utf8") devuelve Promise<string>. Sin la codificación, devuelve Promise<Buffer>. Siempre pasa "utf8" cuando quieras una cadena.fast-glob devuelve Promise<string[]> cuando se llama sin opciones. Con { objectMode: true } devuelve Promise<Entry[]>.@inquirer/prompts infiere tipos de devolución desde el prompt: confirm() devuelve Promise<boolean>, input() devuelve Promise<string>, y select<T>() acepta un genérico para el valor de la opción.(err: unknown) y reduce antes de registrar.Cosas que te atraparán. Cada trampa incluye qué sale mal, por qué sucede y la solución.
Usar fs.readFileSync en un script asincrónico - Bloquea el event loop, anula la concurrencia y hace que los spinners se entrecorten. Solución: Usa import { readFile } from "node:fs/promises" y await. Las APIs sincrónicas solo son apropiadas en código de inicio antes de cualquier trabajo asincrónico.
Olvidar await en una llamada de fs.promises - writeFile(path, data) sin await devuelve una promesa sin controlar. El script se cierra antes de que se complete la escritura, perdiendo datos silenciosamente. Solución: Siempre await, y activa @typescript-eslint/no-floating-promises para detectar esto en tiempo de lint.
Los colores de chalk no aparecen en CI - Chalk detecta automáticamente TTY y desactiva los colores en entornos no interactivos. Los registros en GitHub Actions o CircleCI aparecen sin colorear por diseño. Solución: Establece FORCE_COLOR=1 como variable de entorno en CI, o usa new Chalk(\{ level: 3 \}) para forzar el nivel de color.
Rutas relativas vs absolutas en glob - fast-glob devuelve rutas relativas a cwd por defecto. Pasar esas a fs.readFile funciona solo si el script se ejecuta desde el mismo directorio. Solución: Usa { absolute: true } o resuelve manualmente con path.resolve(process.cwd(), file).
Emoji rompiendo terminales de Windows - cmd.exe heredada y PowerShell más antiguos renderizan emoji como bytes garrapateados. Solución: Detecta Windows vía process.platform === "win32" y recurre a ASCII, o requiere Windows Terminal (UTF-8 por defecto).
Llamar process.exit() antes de que las escrituras se vacíen - process.exit(1) termina inmediatamente, truncando escrituras de stdout y llamadas de fs.writeFile pendientes. Solución: Establece process.exitCode = 1 y deja que el event loop se drene naturalmente.
Otras formas de resolver el mismo problema - y cuándo cada una es la mejor opción.
| Alternativa | Úsalo cuando | No lo uses cuando |
|---|---|---|
| execa | Necesitas generar comandos externos con buen DX | Solo necesitas E/S de archivos en proceso |
| zx | Quieres ergonomía de shell-script con plantillas JS | Necesitas tipado estricto u APIs explícitas |
Bun.file / Bun shell | Estás ejecutando en Bun, no en Node | Apuntas a Node.js en producción |
node:readline | Necesitas entrada interactiva de una sola línea sin deps | Quieres prompts enriquecidos (select, checkbox, etc.) |
node:fs/promises es la API moderna canónica - no se necesita envolver.fs ya tiene una variante de promesa exportada.util.promisify solo es útil para APIs de callback antiguas de terceros.fs.readdir(\{ recursive: true \}) (Node 20+) funciona para casos simples pero no tiene soporte de patrones glob.fast-glob admite **, *, expansión de llaves, negación y patrones ignore.fast-glob es más simple y rápido.inquirer antiguo expone una API monolítica inquirer.prompt([\{ type, name, message \}]).@inquirer/prompts expone cada prompt como una función nombrada: confirm(), input(), select()."type": "module" en package.json).require().await en una llamada de fs.writeFile, o llamaste process.exit() demasiado pronto.await cada llamada asincrónica y usa process.exitCode = 1 en lugar de process.exit(1).isTTY es falso.FORCE_COLOR=1 en tu entorno de CI, o configura un nivel de color más alto explícitamente."utf8": Promise<string>.Promise<Buffer>.select() acepta un parámetro de tipo genérico para el valor elegido.const choice = await select<"a" | "b">(\{ message, choices \}).spinner.text dentro de tu bucle - ora vuelve a renderizar en cada tick.spinner.succeed(msg) o spinner.fail(msg) para detener con un estado final.cli-progress en su lugar.createReadStream + readline.createInterface para iteración línea por línea.JSON.stringify(obj, null, 2) + "\n" - la línea nueva final coincide con convenciones POSIX.await writeFile y usa codificación "utf8".process.exit(code) termina inmediatamente, truncando escrituras de stdout pendientes y operaciones asincrónicas.process.exitCode = code establece el código de salida pero deja que el event loop se drene naturalmente.Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥