Bonjour. J’ai créé prewire, une bibliothèque de DI au build time pour les frontends TypeScript.
Ce qui m’a poussé à créer cette bibliothèque, c’est la structure du monorepo frontend que je maintiens.
Les frontends de plusieurs activités partagent un même code core/kit, et les apps actuelles utilisent Next.js. À l’avenir, certaines activités aimeraient essayer TanStack Router, mais je ne voulais pas pour autant que le core partagé dépende directement de next/navigation ou de @tanstack/react-router.
Il fallait une structure permettant à chaque activité de choisir une implémentation différente sans modifier le core partagé.
Avec prewire, le code partagé déclare d’abord des ports et des tokens.
export interface RouterPort {
href(to: string): string
push(to: string): void
}
export const ROUTER = new InjectionToken<RouterPort>('router')
Chaque app implémente ensuite ce port avec le framework qu’elle utilise.
export const tanstackRouter = injectable(
{ logger: LOGGER },
({ logger }): RouterPort => ({
href: (to) => to,
push: (to) => {
logger.info(`navigate → ${to}`)
throw redirect({ to })
},
}),
{ provides: ROUTER },
)
Côté consommateur, y compris dans le code partagé, il suffit d’importer depuis le même chemin, sans avoir à savoir d’où vient l’implémentation.
import { router } from '#prewire'
prewire codegen analyse statiquement les déclarations injectable() de l’app et des packages partagés, puis génère une composition root TypeScript ordinaire, dans l’ordre des dépendances.
export const logger = consoleLoggerBinding.factory({})
export const router = tanstackRouterBinding.factory({ logger })
Il n’utilise pas de conteneur runtime, ni reflect-metadata, ni décorateurs. Les problèmes comme les bindings manquants, les dépendances circulaires ou les bindings en double sont traités comme des erreurs de build à l’étape de génération de code, et non pendant l’exécution. Le code généré est lisible directement et peut, si besoin, être extrait pour servir de composition root manuelle.
Les overrides sont également fermés par défaut afin d’empêcher une app d’écraser arbitrairement les implémentations d’un package partagé. Seuls les bindings pour lesquels le code partagé a indiqué default: true peuvent être remplacés par l’app. L’intention est proche de open en Kotlin.
J’ai aussi prévu un axe environment distinct. On peut créer différentes roots selon les critères souhaités, par exemple live/test, server/client ou le nom d’une app métier. Ainsi, un binding réservé au serveur n’est pas simplement « non exécuté » dans la root client : son import n’apparaît même pas dans le code généré.
Les exemples du dépôt montrent une configuration où un même kit partagé est câblé différemment dans les trois apps suivantes.
- Next.js
- TanStack Start
- React Router
Pour l’intégration au build, des plugins unplugin pour Vite, webpack et rspack sont fournis, ainsi que withPrewire() pour Next.js.
Cela dit, je ne l’ai pas encore intégré dans du code produit réel. J’en suis au stade où j’ai d’abord validé le design avec une bibliothèque séparée et des exemples, puis l’ai publié. Il est actuellement distribué sur npm en version 0.1.1 ; comme il s’agit d’une version initiale 0.x, l’API peut changer.
Par ailleurs, pour une app unique, dans un seul environnement, avec seulement quelques bindings, prewire n’a pas vraiment de raison d’être. Pour ce type de projet, écrire directement un fichier de composition root est plus simple. Je vise principalement les cas où le même code partagé doit être connecté différemment selon plusieurs apps, environnements ou configurations de test.
J’ai surtout travaillé côté backend, et c’est en gérant un monorepo frontend que j’ai abordé ce problème avec de la DI et de la génération de code au build time. La solution me semble donc assez « backend » dans son approche.
Je serais curieux de savoir comment les personnes spécialisées en frontend résolvent habituellement le problème de plusieurs apps qui utilisent un core partagé, mais avec seulement certaines implémentations propres au framework, comme le routeur, qui changent. Est-ce qu’une DI au build time comme prewire vous semble appropriée ? S’il existe une approche plus simple ou plus familière dans l’écosystème frontend, vos avis m’intéressent.
GitHub : https://github.com/clroot/prewire
npm : https://www.npmjs.com/package/@prewire/core
Licence MIT. Le projet en est à ses débuts, donc les retours critiques sur le design et l’API sont aussi les bienvenus.
Aucun commentaire pour le moment.