1 puntos por abcdkh1209 4 시간 전 | Aún no hay comentarios. | Compartir por WhatsApp

Hola. Creé prewire, una librería de DI en tiempo de compilación para frontend en TypeScript.

La razón por la que terminé creando esta librería fue la estructura del monorepo de frontend que administro.

Los frontends de varios negocios comparten un mismo código core/kit, y actualmente las apps usan Next.js. En adelante quería probar TanStack Router en algunos negocios, pero tampoco quería que el core compartido terminara dependiendo directamente de next/navigation o @tanstack/react-router.

Necesitaba una estructura en la que cada negocio pudiera elegir una implementación distinta sin modificar el core compartido.

En prewire, el código compartido primero declara el port y el token.

export interface RouterPort {  
  href(to: string): string  
  push(to: string): void  
}  
  
export const ROUTER = new InjectionToken<RouterPort>('router')  

Cada app implementa este port con el framework que usa.

export const tanstackRouter = injectable(  
  { logger: LOGGER },  
  ({ logger }): RouterPort => ({  
    href: (to) => to,  
    push: (to) => {  
      logger.info(`navigate → ${to}`)  
      throw redirect({ to })  
    },  
  }),  
  { provides: ROUTER },  
)  

Del lado del consumidor, incluyendo el código compartido, solo se importa la misma ruta sin necesidad de saber de dónde viene la implementación.

import { router } from '#prewire'  

prewire codegen analiza estáticamente las declaraciones injectable() de la app y de los paquetes compartidos, y genera un composition root normal de TypeScript según el orden de dependencias.

export const logger = consoleLoggerBinding.factory({})  
export const router = tanstackRouterBinding.factory({ logger })  

No usa contenedores en runtime, ni reflect-metadata, ni decorators. Problemas como bindings faltantes, dependencias circulares o bindings duplicados se manejan como errores de compilación en la etapa de generación de código, no durante la ejecución. El código generado se puede leer directamente y, si hace falta, también se puede separar y usar como un composition root manual.

Además, los overrides están cerrados por defecto para que la app no pueda sobrescribir arbitrariamente implementaciones del paquete compartido. Solo los bindings del código compartido que indiquen default: true pueden ser reemplazados por la app. La intención es parecida a open de Kotlin.

También dejé separado el eje de environment. Se pueden crear roots distintos con criterios que el usuario quiera, como live/test, server/client o incluso el nombre de una app de negocio. Por ejemplo, un binding exclusivo del servidor no es que simplemente no se ejecute en el root del cliente: el import ni siquiera queda incluido en el código generado.

En los ejemplos del repositorio actual se incluye una configuración donde un mismo kit compartido se conecta de forma distinta en estas tres apps:

  • Next.js
  • TanStack Start
  • React Router

La integración de build ofrece unplugin para Vite, webpack y rspack, además de withPrewire() para Next.js.

Eso sí, todavía no lo he introducido en código de producto real. Primero validé y publiqué el diseño con una librería separada y ejemplos. Ahora mismo está publicado en npm como 0.1.1, y como sigue en una versión inicial 0.x, la API puede cambiar.

Además, si se trata de una sola app, un solo entorno y apenas unos cuantos bindings, no hay mucha razón para usar prewire. En ese tipo de proyecto es más simple escribir directamente un solo archivo de composition root. Lo pienso principalmente para casos donde el mismo código compartido necesita conectarse de manera distinta entre varias apps, entornos y configuraciones de prueba.

Yo he trabajado sobre todo en backend, y al administrar un monorepo de frontend resolví este problema con DI y generación de código en tiempo de compilación. Por eso también siento que la solución en sí es bastante "backendera".

Me da curiosidad cómo suelen resolver esto quienes trabajan profesionalmente en frontend: cuando varias apps usan un core compartido pero solo cambian implementaciones específicas del framework, como el router, ¿cómo lo manejan normalmente? Quisiera saber si algo como la DI en tiempo de compilación al estilo de prewire les parece adecuado, o si hay enfoques más simples o más familiares dentro del ecosistema frontend.

GitHub: https://github.com/clroot/prewire
npm: https://www.npmjs.com/package/@prewire/core

Tiene licencia MIT. Como está en una etapa temprana, también agradezco feedback crítico sobre el diseño y la API.

Aún no hay comentarios.

Aún no hay comentarios.