Extendiendo deno cli usando una función
Estos días he estado usando deno
con más frecuencia y debo decirles que aún hay un par de cosas que me molestan. Sí logré resolver algunos de esos problemas "agregando" algunos sub-comandos. No, no es magia negra, sólo un pequeño truco que aprendí hace tiempo y hoy voy a decirles cómo funciona.
¿Qué necesitamos?
En teoría cualquier intérprete que nos permita crear funciones debería ser suficiente. En este artículo les estaré mostrando los ejemplos usando una sintaxis compatibles con intérpretes que obedecen el estándar POSIX (piensen en "shells" como zsh
, bash
, ash
, etc...).
¿Qué queremos lograr?
Personalmente a mí me encantaría que deno
tuviera algún equivalente nativo a los npm scripts
, pero parece que no tendremos nada de eso aún. Quisiera poder hacer algo así.
deno start hello world
También me gustaría poder inicializar esos scripts, porque deben existir en algún lado.
deno init
Como ya deben saber los sub-comandos start
e init
no existen y vamos a arreglar eso (casi).
Empecemos
El primer paso sería ubicar el archivo que su intérprete favorito ejecuta cada vez que lo invocan. En zsh
por ejemplo, pueden usar ~/.zshrc
. En bash
pueden usar ~/.bashrc
. Esos dos son los únicos que conozco, si usan otro intérprete busquen en su documentación algún equivalente.
La Definición
¿Están listos? Ahora deben crear una función en ese archivo.
deno()
{
echo "Hola"
}
Ahora si reinician el intérprete (o "recargan" su configuración) e invocan el comando deno
deberían obtener Hola
como resultado. Genial, hemos creado un problema, no podemos usar el comando deno
. No teman, vamos por buen camino.
Vuelve a mi, deno
Si quisieramos invocar el comando deno
lo único que tenemos que hacer es usar el comando command
así:
command deno --version
Eso debería mostrarle la información de deno
.
deno x.x.x (release, x86_64-unknown-linux-gnu)
v8 x
typescript x.x
Con este nuevo conocimiento podemos resolver nuestro problema. Vamos a hacer una prueba.
deno()
{
echo "Estás usando"
command deno --version
}
Y esto es lo que debería ocurrir al invocarlo.
$ deno
Estás usando
deno x.x.x (release, x86_64-unknown-linux-gnu)
v8 x
typescript x.x
Volviendo a la normalidad
Ahora que sabemos cómo llamar a deno
lo que deberíamos hacer ahora es imitar el comportamiento de deno
en nuestra función. Debemos hacer esto de una manera que nos deje espacio para agregar nuestros propios comandos, para esto vamos a usar la declaración case
.
deno()
{
local cmd=$1; shift;
case "$cmd" in
*)
command deno $cmd $@
;;
esac
}
La primera línea lo que hace es asignar el primer parámetro ($1
) a la variable cmd
y luego lo elimina de la lista de argumentos ($@
). Después lo que hacemos es comparar cmd
con un patrón. Por ahora el único patrón que tenemos es *
, que actúa como un comodín que coincide con todo, básicamente es nuestro default. Probemos.
deno --version
Bien, pero ahora si intentamos usar deno
sin argumentos nos dará un error. Para arreglar eso vamos verificar si el primer parámetro está vacío, si ese es el caso sólo llamamos deno
.
deno()
{
if [ -z "$1" ];then
command deno
return
fi
local cmd=$1; shift;
case "$cmd" in
*)
command deno $cmd $@
;;
esac
}
Invocando sub-comandos
Ya estamos en el lugar que queremos, finalmente podremos agregar nuestros propios sub-comandos. Veamos cómo funcionaría.
deno()
{
if [ -z "$1" ];then
command deno
return
fi
local cmd=$1; shift;
case "$cmd" in
+ hola)
+ command deno eval "console.log('deno dice hola')"
+ ;;
*)
command deno $cmd $@
;;
esac
}
Si invocamos el comando hola
:
$ deno hola
deno dice hola
Ahora estamos seguros que funciona.
deno scripts
Ya podemos comenzar con lo más importante, nuestro reemplazo para los npm scripts
. Lo que quiero hacer primero es crear el comando start
, el cual es el más común que usaría.
deno()
{
if [ -z "$1" ];then
command deno
return
fi
local cmd=$1; shift;
case "$cmd" in
+ start)
+ command deno run --allow-run ./Taskfile.js start $@
+ ;;
*)
command deno $cmd $@
;;
esac
}
Esto lo que hará será ejecutar un archivo llamado Taskfile.js
usando deno
. --allow-run
nos permitirá usar Deno.run
en nuestro código para poder llamar comandos externos dentro de Taskfile.js
. Ahora vamos a usarlo.
Creamos Taskfile.js
.
const cmd = ['echo', 'Taskfile: ', ...Deno.args];
Deno.run({ cmd });
Y llamamos start
de esta manera.
$ deno start hola
Taskfile start hola
Genial. El siguiente paso sería crear un Taskfile.js
"más inteligente," que sea capaz de ejecutar varias tareas. Ya he hecho algo así en el pasado (pueden encontrar los detalles aquí), así que les mostraré el código que yo usaría.
const entrypoint = "./src/main.js";
run(Deno.args, {
start(...args) {
exec(["deno", "run", entrypoint, ...args]);
},
list() {
console.log('Available tasks: ');
Object.keys(this).forEach((k) => console.log(`* ${k}`));
},
});
function run([name, ...args], tasks) {
if(tasks[name]) {
tasks[name](...args);
} else {
console.log(`Task "${name}" not found\n`);
tasks.list();
}
}
async function exec(args) {
const proc = await Deno.run({ cmd: args }).status();
if (proc.success == false) {
Deno.exit(proc.code);
}
return proc;
}
Con esto podríamos ejecutar el archivo ./src/main.js
al invocar deno start
. Ahora tenemos otro problema, no queremos escribir todo eso cada vez que queremos iniciar un proyecto. Lo que haremos será crear un comando init
que copie esta plantilla en la carpeta de nuestro proyecto.
deno()
{
if [ -z "$1" ];then
command deno
return
fi
local cmd=$1; shift;
case "$cmd" in
start)
command deno run --allow-run ./Taskfile.js start $@
;;
+ init)
+ cp /path/to/template/Taskfile.js ./
+ echo "Taskfile.js created"
+ ;;
*)
command deno $cmd $@
;;
esac
}
Ya tiene buena forma. La última cosa que nos quedaría por hacer es manejar otras tareas además de start
. Con npm
podemos invocar npm run
para elegir qué script queremos ejecutar, nuestro equivalente se llamará x
.
deno()
{
if [ -z "$1" ];then
command deno
return
fi
local cmd=$1; shift;
case "$cmd" in
+ x)
+ command deno run --allow-run ./Taskfile.js $@
+ ;;
start)
command deno run --allow-run ./Taskfile.js start $@
;;
init)
cp /path/to/template/Taskfile.js ./
echo "Taskfile.js created"
;;
*)
command deno $cmd $@
;;
esac
}
Ahora podríamos tener una "tarea" llamada test:api
y llamarla de esta manera.
deno x test:api
Un poco más de conveniencia
Lo último que quiero hacer es crear una forma de ejecutar un script el cual pueda llamar las librerías que uso con más frecuencia sin usar su URL. Podemos hacer esto con el apoyo de un import-map, un archivo .json
en el cual podemos vincular un "alias" a una URL.
Yo uso uno como este.
{
"imports": {
"@std/": "https://deno.land/std@0.93.0/",
"@npm/": "https://jspm.dev/",
"ansi-colors": "https://jspm.dev/ansi-colors@4.1.1",
"arg": "https://jspm.dev/arg@5.0.0",
"cheerio": "https://jspm.dev/cheerio@1.0.0-rc.5",
"exec": "https://deno.land/x/exec@0.0.5/mod.ts",
"ramda": "https://jspm.dev/ramda@0.27.1",
"@utils/": "/path/to/deno/utils/"
}
}
deno
puede leerlo si le pasamos el argumento --import-map
. Ahora vamos a crear un comando para usarlo.
deno()
{
if [ -z "$1" ];then
command deno
return
fi
local cmd=$1; shift;
case "$cmd" in
x)
command deno run --allow-run ./Taskfile.js $@
;;
start)
command deno run --allow-run ./Taskfile.js start $@
;;
init)
cp /path/to/template/Taskfile.js ./
echo "Taskfile.js created"
;;
+ s|script)
+ command deno run --import-map="/path/to/deno/import-map.json" $@
+ ;;
*)
command deno $cmd $@
;;
esac
}
Como ya es costumbre, tenemos que probar esto. Vamos a crear un script de prueba con este contenido.
import dayjs from '@npm/dayjs';
import c from 'ansi-colors';
c.enabled = !Deno.noColor;
const date = dayjs().format('{YYYY} MM-DDTHH:mm:ss SSS [Z] A');
console.log(c.green(date));
Tal vez quieran especificar la versión de la librería que están usando con
@npm
, en ese caso harían esto@npm/dayjs@1.10.4
Lo ejecutamos.
$ deno script ./test.js
Download https://jspm.dev/dayjs@1.10.4
Download https://jspm.dev/npm:dayjs@1.10.4!cjs
{2021} 04-18T11:28:05 929 Z AM
¡Jaja! Ahora tengo todo lo que quiero.
El resultado final
Después de todo este proceso la función deno
debería quedar así.
deno()
{
if [ -z "$1" ];then
command deno
return
fi
local cmd=$1; shift
case "$cmd" in
x)
command deno run --allow-run ./Taskfile.js $@
;;
start)
command deno run --allow-run ./Taskfile.js start $@
;;
init)
cp /path/to/template/Taskfile.js ./
echo "Taskfile.js created"
;;
s|script)
command deno run --import-map="/path/to/deno/import-map.json" $@
;;
*)
command deno $cmd $@
;;
esac
}
Conclusión
Entonces, ¿qué hicimos hoy? "Escondimos" el comando deno
detrás de una función para poder agregar "sub-comandos" creados por nosotros. Logramos crear una especie de equivalente de npm run
para deno
y finalmente usamos import maps para simplificar la declaración de dependencias en un script. No está mal para un día de trabajo.
Gracias por su tiempo. Si este artículo les pareció útil y quieren apoyar mis esfuerzos para crear más contenido, pueden dejar una propina en buymeacoffee ☕.