Publica tu primer paquete en JSR con Deno.

Aprende a utilizar el registro de paquetes JSR y publica tu primer paquete con esta guía disponible en La Esquina Gris

Publica tu primer paquete en JSR con Deno.

Sé lo que estás pensando al ver este artículo en mi blog:

Te estás dejando llevar por el hype.

Y… bueno, no puedo negarlo. Estás en lo correcto, pero pasa y resulta que caí en una situación que me vino como anillo al dedo para sacar un nuevo artículo para mi blog. Y nada mejor que documentarlo aquí para que, al menos, la pequeña comunidad que sigue a este espacio en internet tenga a la mano una guía de cómo publicar su primer paquete en JSR.

¿Por qué un paquete de TypeScript? ¿Por qué ahora?

En resumen, porque lo necesitaba hacer. Tengo un par de servicios privados para un número muy pequeño de usuarios y quería hacer una pequeña REST API para automatizar mis interacciones con los usuarios y reducir la cantidad de tiempo que ellos debían esperar entre peticiones de su parte y respuestas de la mía. Todo iba bien, decidí no usar Express aunque Deno tiene compatibilidad con paquetes de NPM porque quería ver que podía ofrecerme el Dinosaurio chistoso.

Di con un framework/middleware llamado Oak que es el equivalente (más o menos) de ExpressJS en Deno, bastante simple, sencillo y práctico de usar. Creo yo, es perfecto para hacer una REST simple ahí y explorarlo a fondo con eso. Pero en medio del desarrollo me topé con… bueno, hubo pedos 🙃.

Uno de los paquetes que es algo popular para usar en conjunto con Express se llama helmet. Si bien, es posible usar ExpressJS (y sus plugins) con Deno, lo cierto es que sería un despropósito fuera de una migración o de un motivo más fuerte como compatibilidad con otras codebases o simplemente porque tu equipo de desarrollo no sabe/puede darse el lujo de aprender una herramienta nueva.

Helmet no tiene un equivalente en Deno. Existía snelm, sin embargo, lleva más o menos 4 años (a partir de la fecha de escritura de este artículo) sin recibir actualizaciones. Por lo que me di a la tarea de crear mi propia solución para colocar headers de seguridad en Oak. Lo llamé SLSy por la canción de Riverside "Second Life Syndrome". El concepto es simple, modernizar con mi propia mano el código de snelm de hace 4 años, usar Deno moderno y publicarlo en JSR para poder utilizarlo en mis proyectos que involucran REST API con Deno.

En este artículo te voy a enseñar el proceso que yo seguí para publicar mi paquete en JSR. Así, tendrás una guía de referencia para hacer tus bibliotecas para Deno.

🦕
Debo aclarar que NO soy un desarrollador experto en TypeScript, de hecho creo que tampoco me consideraría de nivel medio. Por lo que, si eres un experto y ves blasfemias aquí, agradecería tu feedback o que perdones mis pecados desde la comodidad de tu monitor.

Hace poco los mismos sujetos de Deno liberaron JSR, un registro de paquetes superior a npm con muchos beneficios.

Para no inflar el blog te dejo aquí el video de JSR:

🎬
El vídeo está en idioma Inglés

Vídeo original del canal oficial de Deno - Todos los derechos reservados a sus respectivos autores.

Si no viste el vídeo, aquí te doy un pequeño resumen de lo que expone el narrador del mismo:

  • JSR es un "superset" de NPM.
  • Publicar paquetes de TypeScript en JSR no requiere de todos los pasos que sí requiere en NPM
  • Los paquetes de JSR pueden ser enlazados a un repositorio con Pipelines para asegurar su integridad
  • JSR es compatible con la mayoría de gestores de paquetes disponibles en el ecosistema de JavaScript.

Requisitos 🧠🦕

Para seguir este tutorial deberás cumplir con los siguientes requisitos:

Conocimiento básico de JavaScript/TypeScript 🧠

Algo de conocimiento de TypeScript/JavaScript es necesario para comprender y absorber algunos conceptos vistos aquí.

Conocimiento básico de GitHub Actions 🐱

Saber trabajar con GitHub Actions de forma básica te dará un entendimiento de como trabajar con los pipelines aquí expuestos.

Conocimiento básico de Deno 🦕

Estar familiarizado con los comandos que nos ofrece Deno es un plus si quieres entender un poco mejor este artículo.

Busca una idea para alguna biblioteca que tengas en mente. Y vamos a programar un ratito para sacar los venenos que nos deja programar en esta cochinada de lenguaje.

Estructura de archivos y directorios 📂

Vamos a preparar el entorno primero. Asumiré que estás usando un sistema operativo Gnu/Linux para esto. El proyecto se compone de 5 directorios en la raíz del proyecto, mismos que podemos crear usando el explorador de archivos o la línea de comandos, te muestro el proceso que seguí yo:

# Crear el directorio para el proyecto en algún lugar donde no se me olvide que existe.

~$ mkdir SLSY && cd SLSY

# Iniciar el proyecto de Deno (Lo explicaré más adelante)
~$ deno init

# Crear los directorios necesarios
~$ mkdir .github examples lib tests types

# Crear los subdirectorios
~$ mkdir -p lib/shields/{crossdomain,dns-prefetch-control,dont-sniff-mimetype ...}
~$ mkdir -p tests/{crossdomain,dns-prefetch-control ...}

# Puedes seguir un enfoque similar para crear los archivos mod.ts dentro de los directorios, por ejemplo:
~$ touch tests/crossdomain/{test-crossdomain-all.ts,test-crossdomain-default.ts ...}

Al final yo terminé con una estructura de directorios así:

├── deno.json
├── examples
│   └── example-oak-server.ts
├── lib
│   ├── mod.ts
│   └── shields
│       ├── crossdomain
│       │   └── mod.ts
│       ├── dns-prefetch-control
│       │   └── mod.ts
│       ├── dont-sniff-mimetype
│       │   └── mod.ts
│       ├── expect-ct
│       │   └── mod.ts
│       ├── expect-header
│       │   └── mod.ts
│       ├── feature-policy
│       │   └── mod.ts
│       ├── frameguard
│       │   └── mod.ts
│       ├── hide-powered-by
│       │   └── mod.ts
│       ├── hsts
│       │   └── mod.ts
│       ├── ienoopen
│       │   └── mod.ts
│       └── referrer-policy
│           └── mod.ts
├── tests
│   ├── crossdomain
│   │   ├── test-crossdomain-all.ts
│   │   ├── test-crossdomain-by-content-type.ts
│   │   ├── test-crossdomain-default.ts
│   │   ├── test-crossdomain-error.ts
│   │   ├── test-crossdomain-master-only.ts
│   │   └── test-crossdomain-none.ts
│   ├── dns-prefetch-control
│   │   ├── test-dns-prefetch-control-off.ts
│   │   └── test-dns-prefetch-control-on.ts
│   ├── dont-sniff-mimetype
│   │   └── test-dont-sniff-mimetype-true.ts
│   ├── expect-ct
│   │   └── test-expect-ct-headers.ts
│   ├── expect-header
│   │   └── test-expect-header.ts
│   └── ienoopen
│       └── test-ienoopen.ts
└── types
    └── mod.ts

Si vas a hacer tu proyecto de otra forma, puedes buscar una estructura más sencilla. En mi caso preferí adoptar una estructura similar a lo que se haría en Rust. La decisión está en ti.

Sí, sí, sí, muy chido, pero, ¿Qué fue ese deno init?

Al ejecutar la orden deno init se iniciará un nuevo proyecto con un archivo deno.json, este archivo JSON es similar al package.json presente en proyectos de Node.js.

Dentro de este archivo JSON debemos colocar las especificaciones de nuestro paquete, así como las opciones de TypeScript, formateo, linternas, etc. Te mostraré como se ve el archivo en su totalidad e iremos explorando poco a poco la estructura del mismo:

{
    "name": "@ventgrey/slsy",
    "version": "0.2.5",
    "exports": "./lib/mod.ts",
    "publish": {
        "include": [
            "deno.json",
            "lib",
            "types",
            "README.md"
        ]
    },
    "compilerOptions": {
        "allowJs": false,
        "strict": true
    },
    "fmt": {
        "useTabs": false,
        "lineWidth": 80,
        "indentWidth": 4,
        "semiColons": true,
        "singleQuote": false,
        "proseWrap": "always"
    },
    "lint": {
        "include": [
            "lib/**/*.ts",
            "tests/**/*.ts",
            "types/**/*.ts"
        ],
        "rules": {
            "tags": ["recommended"],
            "include": [
                "ban-untagged-todo",
                "eqeqeq",
                "explicit-function-return-type",
                "no-console",
                "prefer-ascii"
            ]
        }
    },
    "imports": {
        // Explicado más abajo
    },
    "test": {
        // Explicado más abajo
    }
}

Vamos a descomponer este archivo en partes para que sus secciones sean más sencillas de entender.

Metadatos de publicación

La primera sección que podemos ver corresponde a los metadatos de nuestra biblioteca, dichos metadatos serán usados por JSR para darle detalles a los usuarios sobre la versión, nombre y símbolos exportados en el proyecto:

  • name: Es el nombre del paquete, en JSR todos los paquetes deben tener un scope definido. Por lo que tu paquete no puede llevar un nombre como slsyjs o slsy-js a secas, debe tener un scope que tú mismo puedes crear en el sitio oficial. Vamos a ver como hacer eso más adelante.
  • version: Corresponde a la versión de tu biblioteca. Por defecto, JSR le otorgará a tus usuarios la versión más actual de tu biblioteca, además de que seguirá la notación de versionado semántico.
  • exports: Este campo es crucial, pues define el punto de entrada de nuestra biblioteca. Aquí, debemos especificar el archivo que será exportado como módulo cuando otros desarrolladores importen nuestro paquete de Deno. En mi paquete estoy exportando ./lib/mod.ts que es el módulo principal de SLSy. Dicho archivo tiene todas las exportaciones relevantes.
  • publish: Es la sección donde definimos qué archivos y directorios vamos a incluir en la biblioteca. Es importante notar que, solo debemos incluir los archivos que sean 100% necesarios, de esta forma los usuarios de nuestra biblioteca no recibirán archivos innecesarios. En mi caso, se incluye el archivo de configuración deno.json requerido por JSR, el directorio /lib con el código fuente de la biblioteca, el directorio /types con las definiciones de tipos y el archivo README.md como documentación inicial del paquete.

Opciones de TypeScript

Deno al tener soporte de primera clase, nos permite crear una sección compilerOptions en nuestro archivo deno.config donde podemos usar una serie de opciones para configurar el compilador de TypeScript. Aquí podemos configurar si queremos permitir el uso de JS con allowJS o si queremos aplicar las reglas estrictas de TypeScript con strict.

Deno F(or)M(a)T

Generado con: https://imgflip.com/

Deno incluye un formateador de código integrado, por lo que ya no es necesario rogarle al mugroso Prettier que funcione. En la sección fmt del archivo deno.json podemos personalizar como queremos que se comporte el formateador de código. Por ejemplo, definir si quieres usar tabulaciones en lugar de espacios, el ancho máximo de línea, el ancho de la sangría, si prefieres comillas simples o dobles y cómo manejar los saltos de línea en prosa.

En mi caso, preferí un enfoque más tradicional a la hora del formateo. Con la diferencia de que preferí quedarme con sangrías de 4 espacios. Sin sacrificar el límite de la columna 80.

¿Mencioné que el formateador también tiene una bandera para revisar que el formato sea correcto y que es útil para CI? ¿No? Bueno, ahora lo sabes 😉

Deno Lint

Otra cosa que incluye Deno es un linter. El linter de Deno nos permitirá mantener la calidad en nuestro código. En las opciones del linter, la propiedad include se usa para definir los patrones de archivos que deseamos que se evalúen con el linter. En mi caso, necesito que el linter revise el código fuente de la biblioteca, las definiciones de tipos y las pruebas unitarias de la misma

En la propiedad rules nos permite un control más granular sobre las reglas del linter. Podemos optar por usar las reglas recomendadas por Deno usando la etiqueta recommended que incluye un conjunto de reglas consideradas como "buenas prácticas" para la mayoría de proyectos.

Yo opté por usar algunas reglas adicionales que no están presentes en la etiqueta recommended para hacer un poco más estricto el desarrollo de mi biblioteca:

  • ban-untagged-todo: Prohíbe los comentarios TODO que no tengan etiquetas de un usuario o de un problema del proyecto.
  • eqeqeq: Exige el uso de === y !== en lugar del == y del != para ser más estrictos en la comparación de tipos.
  • explicit-function-return-type: Requiere que todas las funciones declaren explícitamente su tipo de retorno.
  • no-console: Prohíbe el uso de console.log y otros métodos de impresión relacionados para evitar salidas de depuración accidentales a producción.
  • prefer-ascii: Obliga al código a estar escrito en caracteres ASCII. Esto puede ser utilizado como una medida de seguridad para evitar filtraciones de caracteres no deseados que pudieran ocasionar rupturas en la biblioteca.

Import maps 🗄️

Omití la sección de imports en el ejemplo de mi archivo deno.json por una razón en específico y es que usa algo llamado import maps. Vamos a ver el archivo para entender de que estoy hablando:

    "imports": {
        "$shields/crossdomain": "./lib/shields/crossdomain/mod.ts",
        "$shields/dnsprefetch": "./lib/shields/dns-prefetch-control/mod.ts",
        "$shields/dontsniff": "./lib/shields/dont-sniff-mimetype/mod.ts",
        "$shields/expectct": "./lib/shields/expect-ct/mod.ts",
        "$shields/expectheader": "./lib/shields/expect-header/mod.ts",
        "$shields/featurepolicy": "./lib/shields/feature-policy/mod.ts",
        "$shields/frameguard": "./lib/shields/frameguard/mod.ts",
        "$shields/hide_powered_by": "./lib/shields/hide-powered-by/mod.ts",
        "$shields/hsts": "./lib/shields/hsts/mod.ts",
        "$shields/ienoopen": "./lib/shields/ienoopen/mod.ts",
        "$shields/referrer": "./lib/shields/referrer-policy/mod.ts",
        "$types": "./types/mod.ts",
        "@oak/oak": "jsr:@oak/oak@^16.0.0",
        "@std/assert": "jsr:@std/assert@^0.225.1"
    },

Los import maps son una característica de Deno que nos permite controlar cómo se resuelven las importaciones de módulos en nuestra biblioteca/aplicación de Deno. Esta característica nos ahorra rutas de importación largas y complejas. Además de que, nos ahorra hacer un archivo deps.ts porque podemos usar la sección para añadir las dependencias del proyecto ahí mismo.

Tengo un proyecto personal donde estoy utilizando mi propio paquete, JSR nos hace el favor de añadirlo al archivo deno.json si ejecutamos la orden: deno add @ventgrey/slsy:

    "imports": {
        // Módulos locales recortados
        "@oak/oak": "jsr:@oak/oak@^16.0.0",
        "@std/cli": "jsr:@std/cli@^0.224.1",
        "@std/dotenv": "jsr:@std/dotenv@^0.224.0",
        "@std/encoding": "jsr:@std/encoding@^0.224.0",
        "@std/fs": "jsr:@std/fs@^0.224.0",
        "@ventgrey/slsy": "jsr:@ventgrey/slsy@^0.2.6",
        "pocketbase": "npm:pocketbase@^0.21.2",
        "zod": "https://deno.land/x/zod@v3.23.8/mod.ts"
    },

Gracias a estas definiciones podemos hacer más corta la orden de importación en nuestro código de Deno, pues algo que se vería así:

import { Slsy } from "jsr:@ventgrey/slsy@^0.2.6";

// Resto del código...

A algo como esto:

import { Slsy } from "@ventgrey/slsy";

// Resto del código

Pero esto no solo se limita a módulos remotos, podemos utilizarlo para bibliotecas locales muy anidadas:

// Antes (Esto es herejía, espero que no tengas algo así)
import { convertToMXN } from "./lib/utils/currency_utils/mxn/conversion/mod.ts";

// Después
import { converToMXN } from "currency-utils/";
Asumiendo que en tu archivo deno.json hay un import map "currency-utils/": "./lib/utils/currency_utils/mxn/conversion/mod.ts"

Si has usado Sveltekit esto podría recordarte mucho al uso de $lib y $types.

Gracias a los import maps podemos simplificar las importaciones y hacer que el código sea más legible para nuevos contribuidores y centralizar las fuentes de las dependencias en un solo lugar.

Volvamos al archivo JSON de la biblioteca que estamos usando aquí:

    "imports": {
        "$shields/crossdomain": "./lib/shields/crossdomain/mod.ts",
        "$shields/dnsprefetch": "./lib/shields/dns-prefetch-control/mod.ts",
        "$shields/dontsniff": "./lib/shields/dont-sniff-mimetype/mod.ts",
        "$shields/expectct": "./lib/shields/expect-ct/mod.ts",
        "$shields/expectheader": "./lib/shields/expect-header/mod.ts",
        "$shields/featurepolicy": "./lib/shields/feature-policy/mod.ts",
        "$shields/frameguard": "./lib/shields/frameguard/mod.ts",
        "$shields/hide_powered_by": "./lib/shields/hide-powered-by/mod.ts",
        "$shields/hsts": "./lib/shields/hsts/mod.ts",
        "$shields/ienoopen": "./lib/shields/ienoopen/mod.ts",
        "$shields/referrer": "./lib/shields/referrer-policy/mod.ts",
        "$types": "./types/mod.ts",
        "@oak/oak": "jsr:@oak/oak@^16.0.0",
        "@std/assert": "jsr:@std/assert@^0.225.1"
    },

Alias locales 📥

Los alias que comienzan con $shields me los turbo robé de Sveltekit porque… bueno me acostumbré a trabajar con $lib en el framework. Además de que es un símbolo diferente al @ utilizado para los módulos añadidos por JSR.

Alias Externos 📤

Los alias que comienzan con @ son para paquetes externos. En este caso, hemos definido dos:

  • @oak/oak apunta a una versión específica del paquete Oak. La notación jsr:@oak/oak@^16.0.0 indica que estamos utilizando la versión 16.0.0 o superior, pero menor que la versión 17.0.0, siguiendo el versionado semántico. Esto con el propósito de obtener revisiones menores de la biblioteca y mantenerla actualizada, pero evitando cambios mayores que puedan introducir errores nuevos.
  • @std/assert apunta a un módulo de aserciones estándar para Deno, con una versión específica definida de manera similar a Oak.

Un pequeño tour por SLSy 📦

Antes de que me regañes por hacerle un shameless plug a mi biblioteca de Deno, permíteme defenderme:

  1. JSR no cuenta la popularidad de los paquetes por visita o por descarga (al menos que yo sepa)
  2. Está hecha para un caso muy específico (Deno + Oak), por lo que no gano nada spammeandola a lo loco ya que, de todas formas no te va a funcionar si estás usando Express.
  3. No tiene (ni tendrá) soporte o headers CORS, más por una razón de filosofía que por una razón funcional.

No deseo extender mucho este artículo, por lo que, te compartiré en breve lo que hace esta pequeña biblioteca y que hacen algunos archivos de la misma:

El archivo principal lib/mod.ts:

Este archivo define la clase (y método de clase) que exporta la biblioteca para ser usada en Oak. No puedo pegar el código aquí por una razón y es que podría cambiar en el futuro, sin embargo puedo compartirlo de forma recortada:

export class Slsy {

    private options: SLSyOptions;

    constructor(options: SLSyOptions = {}) {
        this.options = options;
    }

    public slsy(request: Request, response: Response): Response {
        const requestResponse: OakRequestResponse = new OakRequestResponse(
            request,
            response,
        );

        // Ejecutar la función (con las opciones) dependiendo de como se construyó el objeto.

        return requestResponse.response;
    }
}

Código fuente de SLSy

En esencia, eso es todo lo que hace la biblioteca. Al extraer los ejemplos de los JSDocs que incluí en la misma, podemos ver como se usa:

import { Application, Context, Next } from "@oak/oak";
import { Slsy } from "@ventgrey/slsy";

// Crear una nueva aplicación de Oak
const app: Application = new Application();

// Instanciar un nuevo middleware de SLSy
const slsy: Slsy = new Slsy({
    hidePoweredBy: {
        setTo: "Deno with Typoscript",
    },
    ienoopen: true,
});

// Utilizar el Middleware de SLSy en las rutas de la aplicación de Oak
app.use((ctx: Context, next: Next) => {
    ctx.response = slsy.slsy(ctx.request, ctx.response);
    // Print headers
    console.log(ctx.response.headers);
    next();
});

// Escribe aquí tus rutas extra
app.use((ctx: Context) => {
    ctx.response.body = { message: "Hello from Dino-Saurio" };
});

// Iniciar la escucha del servidor
await app.listen({ port: 9555 });

Hay una parte de la biblioteca que creo que vale la pena mencionar y es en extremo cancerígena y es la inicialización de las opciones de slsy:

        if (this.options.crossdomain !== null) {
            crossdomain(requestResponse, this.options.crossdomain);
        }

        if (this.options.dontSniffMimetype !== null) {
            dontSniffMimetype(requestResponse);
        }

        if (this.options.dnsPrefetchControl !== null) {
            dnsPrefetchControl(
                requestResponse,
                this.options.dnsPrefetchControl,
            );
        }

  // más de estas blasfemias abajo

La razón de esto es que, también buscaba que la biblioteca estuviera 100% tipeada. Y aunque aún necesito de un solo módulo para lograr eso. Usar un vergo de ifs para evitarme una optimización que use any fue la salida más confiable que pude encontrar.

Como puedes ver, la biblioteca no es nada más que un middleware sencillo de Oak que coloca headers de seguridad en las respuestas del servidor.

Pruebas unitarias 🧪

Para darle un poco más de confianza a los usuarios que deseen usar nuestra biblioteca (y para comprobar que funciona como debería de) es importante crear pruebas unitarias en nuestro proyecto.

Volviendo al archivo deno.json que ya mencioné mil veces:

    "test": {
        "include": [
            "tests/**/*.ts"
        ]
    }

De forma similar a la sección lint del archivo de configuración, en la sección test debemos indicarle a Deno donde vamos a colocar nuestras pruebas unitarias. Esto es hermoso, pues nos da un control granular de los directorios de pruebas y tampoco necesitamos de un framework de pruebas externo como Jest. Deno más que un runtime es una infraestructura de desarrollo de TypeScript muy completa.

Veamos una de las pruebas unitarias que tengo en el proyecto. En esta prueba estoy comprobando si el módulo $shields/crossdomain funciona correctamente y le pone a la respuesta del servidor el header que debería:

import { testing } from "@oak/oak";
import type { Context, Middleware, Next } from "@oak/oak";
import { assertEquals } from "@std/assert";

import { Slsy } from "../../lib/mod.ts";

const slsy: Slsy = new Slsy({
    crossdomain: {
        permittedPolicies: "all",
    },
});

const mw: Middleware = async (ctx: Context, next: Next) => {
    ctx.response = slsy.slsy(ctx.request, ctx.response);
    await next();
};

Deno.test({
    name: "Test Cross Domain header -  Value: all",
    ignore: Deno.build.os === "windows",
    fn: async () => {
        const ctx = testing.createMockContext();
        const next = testing.createMockNext();

        await mw(ctx, next);

        // Test for "all" as "Cross-Origin-Resource-Policy" header value
        assertEquals(
            ctx.response.headers.get("cross-origin-resource-policy"),
            "all",
        );
    },
});

Ignorando los imports vamos a ver las partes del archivo de pruebas unitarias dentro de tests/:

  1. Inicializar una instancia de Slsyconst slsy: Slsy = new Slsy({ crossdomain: { permittedPolicies: "all", }, });: Aquí se crea una nueva instancia de la clase Slsy, pasando un objeto de configuración que establece la política de dominio cruzado crossdomain en con valor: "all". Esto significa que todas las políticas de dominio cruzado están permitidas.
  2. Middlewareconst mw: Middleware = async (ctx: Context, next: Next) => { ctx.response = slsy.slsy(ctx.request, ctx.response); await next(); };: Aquí se define un middleware que toma un objeto Context y una función Next como argumentos. El middleware llama al método slsy de la instancia Slsy con la solicitud y la respuesta del contexto, y luego pasa el control al siguiente middleware.
  3. Prueba de DenoDeno.test({ name: "Test Cross Domain header - Value: all", ignore: Deno.build.os === "windows", fn: async () => { const ctx = testing.createMockContext(); const next = testing.createMockNext(); await mw(ctx, next); assertEquals( ctx.response.headers.get("cross-origin-resource-policy"), "all", ); }, });: Aquí se define una prueba de Deno que crea un contexto y una función Next simulados, aplica el middleware a ellos, y luego verifica si el valor del encabezado Cross-Origin-Resource-Policy de la respuesta es “all”. Si no es así, la prueba fallará. La prueba se ignora en sistemas operativos Windows.

Se que te estás preguntando por ese createMockContext y createMockNext. La razón es que Oak nos da un módulo de pruebas para el mismo framework o las bibliotecas que se cuelguen de el. En este caso createMockContext nos permite crear un objeto de contexto simulado.

En los tests, a menudo necesitamos un objeto de contexto de Oak (Context) para pasar a las funciones de middleware. Sin embargo, en un entorno de prueba, no tenemos una solicitud HTTP real para crear un objeto de contexto normal. Aquí es donde createMockContext resulta útil. Nos permite crear un objeto de contexto con propiedades personalizadas que podemos usar para probar nuestras funciones de middleware.

Un caso similar con createMockNext. En el middleware de Oak, la función next() se utiliza para pasar el control al siguiente middleware en la pila. En un entorno de prueba, podemos usar createMockNext para crear una función next que podemos monitorear para ver si se llama correctamente.

Podemos seguir esta estructura en otras pruebas. Por ejemplo, vamos a intentarlo con un header más sencillo (que ya está deprecated):

import { testing } from "@oak/oak";
import type { Context, Middleware, Next } from "@oak/oak";
import { assertEquals } from "@std/assert";

import { Slsy } from "../../lib/mod.ts";

const slsy: Slsy = new Slsy({
    ienoopen: true,
});

const mw: Middleware = async (ctx: Context, next: Next) => {
    ctx.response = slsy.slsy(ctx.request, ctx.response);
    await next();
};

Deno.test({
    name: "Test IE No Open (X-Download-Options) header -  Value: noopen",
    ignore: Deno.build.os === "windows",
    fn: async () => {
        const ctx = testing.createMockContext();
        const next = testing.createMockNext();

        await mw(ctx, next);

        // Header should be "noopen" ("X-Download-Options")
        assertEquals(
            ctx.response.headers.get("x-download-options"),
            "noopen",
        );
    },
});

Una vez tengamos todas nuestras pruebas unitarias hechas, es momento de ejecutarlas para comprobar que nuestro código es funcional. Podemos hacer esto con el comando deno test. Vamos a probarlo en SLSy:

~$ deno test

running 1 test from ./tests/crossdomain/test-crossdomain-all.ts
Test Cross Domain header -  Value: all ... ok (2ms)
running 1 test from ./tests/crossdomain/test-crossdomain-by-content-type.ts
Test Cross Domain header -  Value: by-content-type ... ok (3ms)
running 1 test from ./tests/crossdomain/test-crossdomain-default.ts
Test Cross Domain header -  Value: Default (Should be None) ... ok (2ms)
running 1 test from ./tests/crossdomain/test-crossdomain-error.ts
Test Cross Domain header -  Expected Error (Invalid Value) ... ok (1ms)
running 1 test from ./tests/crossdomain/test-crossdomain-master-only.ts
Test Cross Domain header -  Value: master-only ... ok (3ms)
running 1 test from ./tests/crossdomain/test-crossdomain-none.ts
Test Cross Domain header -  Value: None ... ok (2ms)
running 1 test from ./tests/dns-prefetch-control/test-dns-prefetch-control-off.ts
Test DNS Prefetch Control header -  Value: off ... ok (2ms)
running 1 test from ./tests/dns-prefetch-control/test-dns-prefetch-control-on.ts
Test DNS Prefetch Control header -  Value: on ... ok (2ms)
running 1 test from ./tests/dont-sniff-mimetype/test-dont-sniff-mimetype-true.ts
Test Dont Sniff Mimetype header -  Value: nosniff ... ok (2ms)
running 3 tests from ./tests/expect-ct/test-expect-ct-headers.ts
Test expect-ct header -  Value: enforce + maxAge + reportUri ... ok (2ms)
Test expect-ct header - Value: enforce + maxAge (without reportUri) ... ok (0ms)
Test expect-ct header - Value: enforce + reportUri (without maxAge) ... ok (0ms)
running 1 test from ./tests/expect-header/test-expect-header.ts
Test Expected Header -  Value: ( Header: X-Reality-Dream, Value: III ) ... ok (2ms)
running 1 test from ./tests/ienoopen/test-ienoopen.ts
Test IE No Open (X-Download-Options) header -  Value: noopen ... ok (2ms)

ok | 14 passed | 0 failed (872ms)

Genial, de las 14 pruebas que hemos definido hasta ahora, las 14 pasaron los resultados que esperábamos.

La interfaz de JSR 🦕📦

Bien. Cuando estemos contentos con nuestra biblioteca llegó el momento de ingresar a JSR.

Página principal de JSR (2024) - Todos los derechos reservados a los creadores del sitio.

En la esquina superior derecha, podremos ver un icono que nos invita a iniciar sesión con GitHub. Si ya tienes una cuenta del mismo, no necesitas hacer nada más que conectar tu cuenta con JSR:

Esquina superior derecha del sitio de JSR mostrando tres botones "Browse Packages", "Docs" y "Sign in" con GItHub.
Login de GitHub

Una vez inicies sesión con GitHub, el indicador cambiará a tu avatar de GitHub, si haces click en el, podrás comenzar a crear un paquete:

Pulsa en el botón amarillo que dice "Publish a package" para comenzar.

Ahora debemos rellenar el formulario que nos pide JSR. Es un formulario de dos campos. Si te pide crear un Scope no te alarmes, hablamos de eso al inicio del blog, no es nada más que el @ que viste al incio. Puedes usar el nombre de tu organización, tu nombre de usuario de GitHub o cualquier otro scope que no esté ya en uso por alguien más:

Si tus datos son correctos, aparecerá la siguiente pantalla:

Cuando tu paquete sea creado en JSR podrás explorarlo en tu perfil:

Si el paquete es nuevo, JSR te proporcionará instrucciones para hacer tu primer upload. En este blog no haremos las cosas a mano si podemos hacer que la computadora nos haga el trabajo sucio.

Publicar un paquete con GitHub Actions 🐱 📦⬆️

💡
Antes de proceder, revisa que tu paquete está en el estado que lo deseas.

Algunas recomendaciones que te doy para aumentar tu puntuación en JSR son:

  • Añade JSDoc en tu paquete de JSR. Documenta extensivamente tu biblioteca o módulo. No importa que eso haga enojar a los soydevs de Tech Twitter. Mejor un módulo bien documentado a uno que sea módulo y juego de adivinanzas.
  • Agrega ejemplos de uso a tu README.md o a los JSDoc de tu código.
  • No uses tipos lentos
    • TL;DR - No dejes la inferencia de tipos como si fuera JavaScript, trata de dejar tu paquete lo más "strongly typed" posible.
  • Marca más de un runtime como compatible en la pestaña de Settings. (Prueba primero si funciona en ese runtime)

Por supuesto que publicar un paquete a mano da una flojera tremenda, más si es trabajo que tenemos que hacer cada que deseemos publicar cambios menores o actualizar las instrucciones de uso del mismo. Por eso, Deno y JSR nos dan archivos para comprobar que nuestro paquete funciona correctamente sin sacrificar la calidad de nuestro paquete.

No voy a dar el tutorial completo de GitHub actions aquí. Pero en forma resumida necesitarás crear un directorio .github/workflows en la raíz de tu proyecto y ahí colocar los .yml de las pipelines que desees ejecutar en GitHub. Te dejo el mío por si quieres basarte en algo antes de empezar:

name: Deno CI/CD JSR

on:
  push:
    branches: ["master"]
  pull_request:
    branches: ["master"]

permissions:
  contents: read
  id-token: write # The OIDC ID token is used for authentication with JSR.    

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Setup repo
        uses: actions/checkout@v4

      - name: Setup Deno Stable
        uses: denoland/setup-deno@v1
        with:
          deno-version: v1.x

      - name: Verify formatting
        run: deno fmt --check

      - name: Run linter
        run: deno lint

      - name: Run tests
        run: deno test -A
  publish:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx jsr publish

También puedes descargarlo aquí:

Este Worflow se encargará de:

  • Ejecutar las pruebas y el código en la última versión de Ubuntu y la última versión estable de Deno
  • Verificar que se ejecutó deno fmt antes de realizar un commit para subir a producción solamente el código bien formateado
  • Ejecutar deno lint para asegurar un poco de calidad del código.
  • Ejecutar las pruebas unitarias del proyecto con todos los permisos habilitados en Deno.
  • Publicar las actualizaciones de nuestra biblioteca SOLO si los pasos anteriores se ejecutaron satisfactoriamente

Esto quiere decir que si subiste código sin formatear, código erróneo de acuerdo a deno lint o tus pruebas unitarias dejan de funcionar, la actualización de tu biblioteca NO se va a subir a JSR. Así, proteges a tus usuarios de commits accidentales o de Pull Requests experimentales.

En la interfaz de GitHub Actions esto se ve un poco mejor:

El paso publish solo se ejecutará si el paso test se completa de forma exitosa.

Podrás comprobar que tu biblioteca se subió correctamente a JSR entrando a su página:

Como bonus, JSR aumentará la calificación de tu paquete si lo publicas a través de GitHub Actions, ya que, con esto, creas un build verificado con el fin de evitar copias maliciosas de tu biblioteca:

Vamos a explorar ese "Transparency log":

Me gustaría decirte como funciona este sitio a detalle. En resumidas cuentas presenta una comprobación extendida con certificado. Verificando que, en efecto, la biblioteca proviene de ti como autor y que fue firmada por los workflows de GitHub en tu cuenta.

¿Te interesa si probamos que nuestro paquete funciona?

Probar en otros entornos 🧅💠

¿Recuerdas que al inicio te dije que JSR era un superset de NPM? Bueno. Al publicar nuestro paquete, JSR se encargará de convertir el TypeScript a módulos válidos de JS para usarse donde sea.

¿No me crees? Míralo por ti mismo:

Usar paquete para Deno
Usar paquete para npm
Usar paquete para yarn
Usar paquete para pnpm
Usar paquete para Bun

Pero no nos limitemos a simples screenshots. Vamos a probar hacer un pequeño servidor de Oak + SLSy en NodeJS y en Bun para comprobar que nuestro paquete de JSR realmente funciona.

Para estas pruebas estaré usando contenedores de Podman. No explicaré como hacerlos porque, luego de más de 3 tutoriales que lo involucran en este sitio, me parecería una blasfemia contra mis dedos hacerlo 😆.

En NodeJS 💠

Usé el contenedor de NodeJS en su versión 20.x con Alpine. Para configurar el proyecto seguí los siguientes pasos:

Consola de Podman Desktop

Pero... ¿Funciona en código? Vamos a ver:

No te olvides de instalar Oak con npx jsr add @oak/oak

El código usado en el contenedor se ve así:

import { Slsy } from "@ventgrey/slsy";
import { Application, Context } from "@oak/oak";

const app = new Application();
const slsy = new Slsy({
    hidePoweredBy: {
        setTo: "Deno with Typoscript",
    },
    ienoopen: true,
});

app.use((ctx, next) => {
    ctx.response = slsy.slsy(ctx.request, ctx.response);
    // Print headers
    console.log(ctx.response.headers);
    next();
});

app.use((ctx) => {
    ctx.response.body = { message: "Hello from Dino-Saurio" };
});

console.log("started server at port 9555");
await app.listen({ port: 9555 });

La salida del contenedor es la siguiente:

Y si probamos enviando una petición con Curl:

¡Whohoo! ¿Ves esos headers? x-powered-by tiene como valor Deno with TypoScript. Y los otros headers de seguridad también están presentes 😄.

Para comprobar que es el mismo contenedor:

Si no funciona Curl en tu contenedor de alpine es probable que no esté instalado, eso puedes solucionarlo haciendo apk add curl

El archivo package.json al final terminó así:

{
  "type": "module",
  "dependencies": {
    "@oak/oak": "npm:@jsr/oak__oak@^16.0.0",
    "@ventgrey/slsy": "npm:@jsr/ventgrey__slsy@^0.2.7"
  }
}

Archivo package.json de pruebas.

En Bun 🧅

Vamos a replicar lo mismo pero en Bun. Aquí no usaré JS. Bun también tiene soporte para TypeScript, así que, vamos a probar la biblioteca en el lenguaje en el que se escribió originalmente. Aquí también lo probaré en Bun dentro de un contenedor de Podman:

Vamos a usar el mismo código que usamos antes, pero con anotaciones de tipos:

import { Application, Context, Next } from "@oak/oak";
import { Slsy } from "@ventgrey/slsy";

const app: Application = new Application();
const slsy: Slsy = new Slsy({
    hidePoweredBy: {
        setTo: "Deno-saurius",
    },
    ienoopen: true,
});

app.use((ctx: Context, next: Next) => {
    ctx.response = slsy.slsy(ctx.request, ctx.response);

    next();
});

app.use((ctx: Context) => {
    ctx.response.body = { message: "Hello JSR fellas!" };
});

console.log("Escuchando en el puerto 9555");
await app.listen({ port: 9555 });

Vamos a ver si se ejecuta como debería de:

Probemos de nuevo con Curl 😋:

¡Genial! Podría decir que también funciona con Bun 😄

Claro que, esto solo son pruebas sencillas. La idea es hacer una aplicación un poco más completa y probar que funciona igual en todos los runtimes. Eso abre la puerta a que, si es verdaderamente portable, podrías hacer pruebas en entornos aislados para ver su rendimiento en un escenario del mundo real.

No lo hagas en producción, por el amor de Dios.

Conclusión ✍️

No acostumbro a hacerle speedrun a un artículo de este blog. Y, aunque es cierto que me dejé llevar por el hype de Deno, lo cierto es que la experiencia ha sido bastante útil para adquirir y practicar el conocimiento en el campo "real" de las cosas. Me alegro de haberme equivocado al juzgar mal a Deno en el pasado, el desarrollo sin problemas y la velocidad para ponerse a trabajar es increíble. Me calló la boca con una gracia casi artística. Además, quería traer un blog un poco más enfocado al desarrollo de software, ya hace falta dejar descansar a Podman un rato.

¿Y a ti qué te pareció? ¿Te animarías a hacer tu propia biblioteca? Me gustaría leer tus comentarios en las redes sociales de "La Esquina Gris" 😸 Si haces tu propia biblioteca, ten la libertad de publicarla en los comentarios o de enviarla a cualquier correo de La Esquina Gris para hacerte una mención :)

No hay contenido extra en este artículo, porque... bueno, creo que no hay nada extra que agregar 🙃

Canción triste del día 🍂

I erase you now, with all of my past…