Saltar al contenido

Middleware, endpoints y plugins de Vite en una integración

Cómo una integración añade comportamiento de servidor y de bundler: addMiddleware para inyectar un onRequest antes o después del middleware del usuario, injectRoute para servir endpoints y páginas que viven dentro del paquete, y updateConfig con vite para colar plugins de Vite. El patrón de los módulos virtuales —el prefijo virtual, el byte nulo y el par resolveId y load— para exponer datos generados como si fueran un import normal.

addMiddleware: correr lógica en cada petición

Ya sabes que src/middleware.ts intercepta toda petición del sitio. Una integración puede aportar su propio middleware sin que el usuario cree ese fichero, usando addMiddleware dentro de astro:config:setup. Le das un entrypoint —un módulo que exporta un onRequest— y un order que decide si tu middleware corre antes o después del que el usuario haya escrito.

TypeScript
'astro:config:setup': ({ addMiddleware }) => {  addMiddleware({    order: 'pre',    entrypoint: 'mi-integracion/middleware.js',  });},

El order tiene dos valores y la elección importa. Con 'pre', tu middleware corre antes que el del usuario, útil para preparar locals o cabeceras que sus rutas darán por sentadas. Con 'post', corre después, útil para rematar la respuesta ya construida. Varias integraciones pueden inyectar middleware a la vez; todos se encadenan con el del usuario en una sola tubería, exactamente como los sequence que estudiaste en el nivel de middleware.

Endpoints y páginas dentro del paquete

injectRoute no sirve solo para páginas: su entrypoint puede ser un endpoint —un módulo que exporta GET, POST y demás verbos— y así tu integración expone una API sin que el usuario escriba un fichero en src/pages. Es el mecanismo con el que una integración regala un webhook, un endpoint de estado o un feed.

TypeScript
// mi-integracion/endpoints/salud.ts  (dentro del paquete)import type { APIRoute } from 'astro';export const GET: APIRoute = () =>  Response.json({ estado: 'ok', hora: Date.now() });
TypeScript
'astro:config:setup': ({ injectRoute }) => {  injectRoute({    pattern: '/api/salud',    entrypoint: 'mi-integracion/endpoints/salud.ts',    prerender: false,  });},

La ruta inyectada es indistinguible de una escrita a mano: participa del enrutado, del middleware y del render como cualquier otra. La diferencia es de procedencia —vive en tu paquete, viaja con él y aparece en cada proyecto que instale la integración— sin pedirle nada al usuario más que enrolarla.

Plugins de Vite desde la integración

Cuando necesitas transformar módulos, resolver imports especiales o participar del empaquetado, la herramienta es un plugin de Vite, y lo enchufas fundiéndolo en la configuración con updateConfig. Como Astro corre sobre Vite, todo el poder del bundler queda a tu alcance desde la integración.

TypeScript
'astro:config:setup': ({ updateConfig }) => {  updateConfig({    vite: {      plugins: [        {          name: 'mi-plugin-vite',          transform(code, id) {            // transforma modulos que coincidan con un criterio            return null;          },        },      ],    },  });},

Como updateConfig funde en profundidad, añadir tu plugin no borra los que el usuario u otras integraciones ya pusieron: se suma al array. Esta es la puerta trasera controlada por la que una integración accede al pipeline de compilación —la misma capa que en su día tocaste a mano en la clave vite de la configuración, ahora accionada por código—.

Módulos virtuales: importar lo que no existe

El patrón que corona esta lección resuelve un problema recurrente: quieres que el código del usuario importe datos que tu integración genera —opciones resueltas, una lista construida en el build—, pero esos datos no viven en ningún fichero. La solución es un módulo virtual: un plugin de Vite intercepta un especificador imaginario y devuelve su contenido al vuelo.

TypeScript
function moduloVirtual(saludo: string) {  const id = 'virtual:mi-integracion';  const resuelto = '\0' + id;  return {    name: 'mi-integracion:virtual',    resolveId(spec: string) {      if (spec === id) return resuelto;    },    load(spec: string) {      if (spec === resuelto) return `export const saludo = ${JSON.stringify(saludo)};`;    },  };}

El convenio tiene dos mitades. El especificador público lleva el prefijo virtual:, la señal idiomática de que no es un fichero real. Y el identificador interno que devuelve resolveId lleva por delante un byte nulo —el prefijo que le dice a Vite y a Rollup que no busquen eso en disco porque es suyo—. La función load reconoce ese identificador interno y devuelve el código como una cadena. A partir de ahí, el usuario escribe import { saludo } from 'virtual:mi-integracion' y recibe tus datos como si viniesen de un fichero.