Ejecutar Scripts de Node.js en JavaScript
Cómo ejecutar archivos JavaScript planos con Node.js — ejecución directa, scripts de npm, líneas shebang, y análisis de argumentos de línea de comandos.
Recipe
Tarjeta de referencia rápida — lista para copiar y pegar.
# Ejecutar un script directamente
node script.js
# Ejecutar con reinicio automático cuando cambian archivos (Node 18.11+)
node --watch script.js
# Ejecutar con variables de entorno
NODE_ENV=production node script.js
# Pasar argumentos al script
node script.js --name Alice --verbose// package.json — scripts de npm
{
"name": "my-cli",
"version": "1.0.0",
"type": "module",
"bin": {
"my-cli": "./bin/cli.js"
},
"scripts": {
"start": "node script.js",
"dev": "node --watch script.js",
"cli": "node bin/cli.js"
}
}// bin/cli.js — con shebang
#!/usr/bin/env node
import { parseArgs } from 'node:util';
const { values } = parseArgs({
options: {
name: { type: 'string', short: 'n' },
verbose: { type: 'boolean', short: 'v' },
},
});
console.log(`Hello, ${values.name ?? 'world'}!`);# Hacer el script ejecutable (solo Unix/macOS)
chmod +x bin/cli.js
# Luego ejecutarlo directamente
./bin/cli.js --name AliceCuándo usarlo: Scripts de construcción, tareas de automatización, utilidades puntuales, herramientas CLI, o cualquier cosa que no quieras enviar a través de un navegador.
Ejemplo Completo
Una CLI de renombre de archivo completa que convierte nombres de archivo a minúsculas en un directorio.
#!/usr/bin/env node
// bin/rename-lower.js
import { readdir, rename } from 'node:fs/promises';
import { join } from 'node:path';
import { parseArgs } from 'node:util';
const { values, positionals } = parseArgs({
options: {
dry: { type: 'boolean', short: 'd' },
help: { type: 'boolean', short: 'h' },
},
allowPositionals: true,
});
if (values.help || positionals.length === 0) {
console.log('Usage: rename-lower <dir> [--dry]');
process.exit(values.help ? 0 : 1);
}
const dir = positionals[0];
try {
const files = await readdir(dir);
for (const file of files) {
const lower = file.toLowerCase();
if (file === lower) continue;
const from = join(dir, file);
const to = join(dir, lower);
if (values.dry) {
console.log(`[dry] ${from} -> ${to}`);
} else {
await rename(from, to);
console.log(`renamed ${from} -> ${to}`);
}
}
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}// package.json
{
"name": "rename-lower",
"version": "1.0.0",
"type": "module",
"bin": {
"rename-lower": "./bin/rename-lower.js"
}
}# Pruébalo localmente
npm link
rename-lower ./photos --dry
rename-lower ./photosLo que esto demuestra:
- Línea shebang para que el archivo se ejecute como un ejecutable independiente
parseArgsdenode:utilpara análisis de argv sin dependencias- Argumentos posicionales y de bandera juntos
- Manejo de errores elegante con códigos de salida distintos de cero
- Campo
binenpackage.jsonpara quenpm linkexponga el comando globalmente
Análisis Profundo
Cómo Funciona
Cuando escribes node script.js, Node.js:
- Lee el archivo del disco.
- Determina el tipo de módulo — ESM si
"type": "module"está enpackage.jsono el archivo termina en.mjs, de lo contrario CommonJS. - Compila y evalúa el módulo de nivel superior.
- Expone los globales
process,console,Buffery compañía. - Sale cuando el event loop está vacío — o inmediatamente si llamas a
process.exit().
Una línea shebang como #!/usr/bin/env node le dice al cargador del sistema operativo qué intérprete usar cuando el archivo se ejecuta directamente. La herramienta env busca node en PATH, que es más portátil que una ruta codificada /usr/local/bin/node.
process.argv es un array donde el índice 0 es la ruta del binario de Node.js, el índice 1 es la ruta del script, y el índice 2+ son los argumentos suministrados por el usuario. Por eso parseArgs omite los primeros dos por defecto.
Variaciones
# Pasar argumentos a través de un script de npm (nota el --)
npm run cli -- --name Alice
# Variables de entorno en línea
API_KEY=abc node script.js
# Modo watch (Node 18.11+)
node --watch script.js
# Modo watch con una entrada específica
node --watch --watch-path=./src script.js
# Modo inspeccionar/depurar
node --inspect-brk script.js
# Analizar args con la utilidad integrada
node -e "console.log(require('node:util').parseArgs({options:{n:{type:'string'}}}))" -- -n hiNotas de TypeScript
Incluso para scripts JavaScript planos, puedes optar por type-checking sin agregar un paso de construcción habilitando // @ts-check en la parte superior del archivo y creando un jsconfig.json:
// jsconfig.json
{
"compilerOptions": {
"checkJs": true,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"lib": ["ES2022"]
},
"include": ["bin/**/*.js", "scripts/**/*.js"]
}Agrega tipos JSDoc para parámetros y el editor — y tsc --noEmit — capturará errores:
// @ts-check
/** @param {string} name @returns {string} */
function greet(name) {
return `Hello, ${name}!`;
}Errores Comunes
- Shebang faltante. Sin
#!/usr/bin/env node, ejecutar el archivo directamente (./script.js) en shells Unix genera un error "exec format error" o ejecuta el archivo como un script de shell. - Olvidar
chmod +x. Un shebang correcto es inútil si el archivo no es ejecutable. El permiso debe agregarse conchmod +x bin/cli.jsy el bit debe ser confirmado en Git (git update-index --chmod=+x). - Confusión de extensión ESM vs CJS. Un archivo
.jsen un paquete con"type": "module"es ESM; el mismo archivo en un paquete CJS es CommonJS. Usa.mjs/.cjspara ser explícito y evitar ambigüedad. __dirnamees indefinido en ESM. En scripts ESM debes reconstruirlo:const __dirname = path.dirname(fileURLToPath(import.meta.url)).- Terminaciones de línea Windows en scripts. Un shebang con CRLF (
\r\n) falla en Linux/macOS con "bad interpreter". Fuerza LF a través de.gitattributes:*.js text eol=lf. - Olvidar
--en scripts de npm.npm run cli --name Alicepasa--name Aliceanpm, no a tu script. Usanpm run cli -- --name Alice. - Indexación de
process.argv. El índice 0 y 1 son el binario de Node y la ruta del script — tus argumentos de usuario comienzan en el índice 2.
Alternativas
| Herramienta | Fortalezas | Debilidades |
|---|---|---|
node | Integrado, sin configuración, ampliamente compatible | Sin TS, sin modo watch antes de 18.11 |
bun | Inicio más rápido, incluido, ejecuta TS/JSX | Ecosistema más nuevo, algunos gaps de API |
deno | Seguro por defecto, TS nativo, librería estándar | Resolución de módulos diferente, ecosistema más pequeño |
tsx | Ejecutor TS/ESM rápido sobre Node | Dependencia adicional |
pnpm exec | Ejecuta binarios locales sin instalaciones globales | Solo un ejecutor, no un runtime |
Preguntas Frecuentes
¿Cuál es la diferencia entre node script.js y ./script.js?
node script.js invoca explícitamente el binario de Node.js. ./script.js depende del sistema operativo para leer la línea shebang y elegir el intérprete — así que solo funciona si el archivo comienza con #!/usr/bin/env node y tiene el bit de ejecución establecido.
¿Cómo accedo a argumentos de línea de comandos dentro de un script?
Usa process.argv, que es un array que comienza con el binario de Node (índice 0) y la ruta del script (índice 1). Los argumentos del usuario comienzan en el índice 2. Para cualquier cosa más allá de banderas triviales, usa parseArgs de node:util.
¿Qué hace el campo "bin" en package.json?
Mapea un nombre de comando a un archivo de script. Cuando el paquete se instala globalmente — o se vincula a través de npm link — npm crea un symlink en su directorio bin para que puedas ejecutar el comando desde cualquier lugar.
¿Cómo ejecuto un script cada vez que cambia un archivo?
Usa la bandera --watch integrada de Node (Node 18.11+): node --watch script.js. Para más control, emparéjala con --watch-path=./src para restringir los directorios supervisados.
¿Cómo paso argumentos a través de un script de npm? (error común)
Debes separar las banderas de npm de las banderas de tu script con --. Por ejemplo: npm run cli -- --name Alice. Sin el doble guión, npm consume los argumentos por sí solo.
¿Por qué mi script de shebang falla con "bad interpreter"? (error común)
Casi siempre terminaciones de línea Windows. El shell lee la línea shebang incluyendo el \r final e intenta ejecutar /usr/bin/env node\r, que no existe. Fuerza terminaciones LF para archivos de shell y script a través de .gitattributes.
¿Puedo type-check JavaScript plano sin TypeScript? (typescript)
Sí. Agrega // @ts-check en la parte superior de un archivo .js, crea un jsconfig.json con "checkJs": true, e anota con JSDoc. Ejecutar tsc --noEmit expone errores de tipo sin impacto en tiempo de ejecución.
¿Cómo describo tipos de función en JSDoc para un script JavaScript? (typescript)
Usa etiquetas JSDoc como /** @param {string} name @returns {Promise<void>} */. Los editores y tsc entienden estas anotaciones de la misma manera que entienden los tipos de TypeScript.
¿Cuál es la diferencia entre process.env y un archivo .env?
process.env son las variables de entorno en memoria que el proceso de Node.js heredó. Un archivo .env es solo un archivo de texto — Node.js no lo analiza automáticamente. Usa --env-file=.env (Node 20.6+) o una librería como dotenv.
¿Cómo debo devolver un código de salida distinto de cero desde un script?
Llama a process.exit(1) después de registrar el error, o lanza un error no capturado y deja que Node salga con el código 1 automáticamente. Reserva códigos distintos de cero para fallas reales para que pipelines de shell y sistemas de CI puedan detectarlos.
¿Qué es el global __dirname y por qué falta en algunos scripts?
__dirname es un global solo de CommonJS que apunta al directorio del script actual. En ESM reconstruyes desde import.meta.url con fileURLToPath y path.dirname.
¿Puedo usar top-level await en un script JavaScript plano?
Solo en ESM — es decir, un archivo .mjs o un archivo en un paquete con "type": "module". En CommonJS debes envolver el código async dentro de un IIFE async.