Empaquetar, publicar y pulir una integración
El último tramo: convertir una integración que funciona en tu proyecto en un paquete que otros instalan. El package.json con la keyword astro-integration y astro como peerDependency, las opciones tipadas y validadas con zod, aportar variables al esquema de astro:env desde la integración, exponer tipos con injectTypes, probarla con proyectos de prueba y build programático, y el decálogo de buenas prácticas que separan una integración amable de una hostil.
Una integración que funciona en tu carpeta y una que un desconocido instala desde npm y usa sin leer tu código son dos cosas distintas separadas por un tramo de oficio: el empaquetado correcto, las opciones validadas, los tipos publicados, las pruebas que la sostienen y un puñado de cortesías que la hacen convivir con las demás. Este cierre del nivel recorre ese tramo. No añade poderes nuevos a la API; enseña a envolver los que ya dominas en un paquete que el ecosistema reconoce, que astro add sabe instalar y que no sorprende con efectos que nadie pidió.
- Preparar el
package.jsoncon la keywordastro-integrationyastrocomo peer. - Tipar y validar las opciones de la fábrica, y aportar variables a
astro:env. - Publicar los tipos de tu API pública y de tus módulos virtuales.
- Probar la integración con proyectos de prueba y conocer las buenas prácticas.
El package.json que Astro reconoce
Empaquetar una integración es sobre todo declarar bien su package.json. Tres detalles la convierten en ciudadana del ecosistema: la keyword astro-integration, que hace que astro add y los buscadores la encuentren; astro declarado como peerDependencies en lugar de dependencia normal, para compartir la instancia del usuario y no duplicarla; y un mapa exports que exponga el punto de entrada y los ficheros que inyectas.
{
"name": "astro-mi-integracion",
"type": "module",
"keywords": ["astro-integration"],
"exports": {
".": "./dist/index.js",
"./middleware.js": "./dist/middleware.js"
},
"peerDependencies": {
"astro": "^7.0.0"
}
}
Fíjate en que los entrypoint que pasaste a injectRoute o addMiddleware deben ser especificadores importables, y por eso tienen que figurar en exports: si un fichero no está exportado, el proyecto que instale tu paquete no podrá resolverlo. El exports no es burocracia —es el contrato de qué partes de tu paquete son públicas—.
Opciones tipadas y astro:env
La fábrica recibe las opciones del usuario, y una integración robusta no confía en ellas a ciegas: las valida. La herramienta idiomática es zod, el mismo validador que usan los esquemas de contenido, que comprueba la forma en runtime y deriva el tipo para el editor de una sola definición.
import { z } from 'astro/zod';
const esquema = z.object({
saludo: z.string().default('hola'),
intensidad: z.number().min(1).max(10).default(3),
});
export default function miIntegracion(opciones: unknown) {
const opts = esquema.parse(opciones);
return { name: 'astro-mi-integracion', hooks: { /* ... */ } };
}
Cuando tu integración necesita variables de entorno tipadas —una clave de API, una URL de servicio—, no las leas de process.env a mano: aporta su definición al esquema de astro:env desde astro:config:setup, fundiéndola con updateConfig. Así el usuario obtiene las mismas garantías de validación y tipado que para sus propias variables, y tú las consumes desde astro:env/server.
import { envField } from 'astro/config';
'astro:config:setup': ({ updateConfig }) => {
updateConfig({
env: {
schema: {
MI_API_KEY: envField.string({ context: 'server', access: 'secret' }),
},
},
});
},
Publicar los tipos de tu API
Una integración en TypeScript debe viajar con sus tipos, y hay dos superficies que tipar. La primera es la API pública: las opciones de la fábrica y su valor de retorno, que se compilan a un .d.ts junto al código y se anuncian en package.json. La segunda son los ambientes que tu integración añade al proyecto del usuario —módulos virtuales, locals que rellena tu middleware— que se declaran con injectTypes en astro:config:done.
'astro:config:done': ({ injectTypes }) => {
injectTypes({
filename: 'tipos.d.ts',
content: `declare namespace App {
interface Locals {
miIntegracion: { saludo: string };
}
}`,
});
},
Con esto, cuando el usuario lea Astro.locals.miIntegracion el editor conocerá su forma sin que él declare nada. La regla es simple: todo lo que tu integración inyecta en el espacio del usuario —un import virtual, una propiedad de locals, un global— merece su declaración, o se sentirá como magia sin tipo.
Probar y convivir: buenas prácticas
Se prueba una integración de dos maneras complementarias. En pequeño, llamando a la fábrica y comprobando el objeto que devuelve, o invocando un hook con utilidades falsas y verificando que llama a updateConfig o injectRoute como esperas. En grande, con un proyecto de prueba —una mini app que enrola la integración— sobre el que corres el build de Astro de forma programática y afirmas sobre lo que aparece en dist.
import { build } from 'astro';
// en un test: construye un proyecto de prueba que usa la integracion
await build({ root: new URL('./fixtures/basico', import.meta.url) });
// luego afirma sobre los ficheros y rutas generados en dist
keyword y peer
La keyword astro-integration te hace visible a astro add; astro como peerDependency evita duplicar el framework.
valida opciones
Comprueba las opciones con zod y da defectos. Falla pronto y claro en vez de romper a mitad del build.
publica tipos
Tipa la API pública y usa injectTypes para lo que inyectas en el espacio del usuario.
proyectos de prueba
Prueba en pequeño la fábrica y en grande con un build programático sobre una app de prueba.
flowchart TD DEV[integracion que funciona en local] --> PKG[package json con keyword y peer] PKG --> VAL[opciones validadas con zod] VAL --> TYP[tipos publicados e injectTypes] TYP --> TEST[pruebas con proyectos de prueba] TEST --> PUB[publicada en npm y lista para astro add] style DEV fill:#89b4fa,color:#11111b style PUB fill:#a6e3a1,color:#11111b
Declarar astro en dependencies instalaría una segunda copia del framework junto a la del usuario, y dos instancias de Astro en el mismo proyecto es una fuente segura de fallos incomprensibles. Va en peerDependencies, que expresa lo correcto: tu integración espera que el proyecto anfitrión aporte Astro, y se acopla a la versión que él ya tiene. Lo mismo vale para cualquier framework de UI cuyo renderer envuelvas.
Cada hook recibe un logger atribuido a tu integración, que antepone su nombre y respeta el nivel de detalle que el usuario configuró. Prefiérelo siempre a console.log: tus mensajes aparecen ordenados junto a los de Astro, identificados como tuyos, y silenciables sin tocar tu código. Una integración que ensucia la terminal con console.log crudos se siente amateur y es imposible de callar.
La cortesía cardinal de una integración es la contención. No fijes claves de configuración que el usuario ya puso, no inyectes scripts que no hacen falta, no corras trabajo pesado en comandos donde no aporta, no dejes efectos en su proyecto que él no solicitó. Lee command para actuar solo cuando toca, respeta isRestart para no duplicar trabajo en cada reinicio del dev, y guíate por una máxima: una integración debe ser la mínima que resuelve su problema, y ni un hook más.
El salto de una integración que funciona en tu carpeta a una que otros instalan es, en el fondo, un cambio de naturaleza más que de grado, y conviene nombrarlo. Mientras la integración vive en tu proyecto, tú eres a la vez su autor y su único usuario: si algo va mal, lo entiendes, porque conoces sus tripas. En el instante en que la publicas, ese vínculo se rompe. La usará gente que jamás abrirá tu código, que la enrolará en una línea y esperará que se comporte, que no sabrá distinguir un fallo suyo de uno tuyo, y que compartirá el proceso de Astro con otras cinco integraciones escritas por otras cinco personas que tampoco se conocen entre sí. Todo el oficio de este cierre —la keyword, el peer, las opciones validadas, los tipos, el logger, la contención— no es decoración: es la materialización de un contrato con esos desconocidos. La keyword promete soy una integración, trátame como tal. El peer dependency promete no traeré un segundo Astro a estropear el tuyo. La validación promete si te equivocas al configurarme, te lo diré pronto y con claridad, no a mitad del build con un error críptico. Los tipos prometen no tendrás que leer mi fuente para saber qué acepto. El logger promete mis mensajes serán tuyos de silenciar. Y la contención promete lo más difícil: no tocaré nada que no me hayas pedido tocar. Un ecosistema de plugins solo prospera si esos contratos se cumplen por defecto, porque cada integración hostil —la que pisa la config, la que ensucia la consola, la que hace de más— envenena la confianza en todas las demás. Escribir la integración fue aprender la API; publicarla bien es aprender algo más hondo, que vale para todo software que otros tocarán sin verte: que el código que compartes ya no habla por ti, y que tu única voz ante quien lo usa son las promesas que supiste inscribir en sus bordes.
- Redacta el
package.json: añade la keywordastro-integration, mueveastroapeerDependenciesy declara enexportscadaentrypointque inyectas. - Envuelve las opciones de tu fábrica en un esquema de
zodcon defectos, y prueba a pasarle una opción inválida para ver el error temprano. - Aporta una variable al esquema de
astro:envconenvFielddesdeastro:config:setupy consúmela desdeastro:env/server. - Escribe un proyecto de prueba mínimo que enrole tu integración, constrúyelo con
buildde forma programática y afirma sobre un fichero que tu integración generó.