Le gradient

Le gradient n'est pas une fonctionnalité que le framework ajoute. C'est ce qui découle du fait qu'une Frond ne nomme aucune adresse : puisque rien en elle ne dit dans quel process elle tourne, la déplacer dans un autre ne change ni son code métier ni les pages qui l'appellent.

Cinq barreaux, et le prix de chacun

Fougere est progressive en un sens précis, et c'est celui qui se vérifie : monter d'un barreau ne réécrit jamais ce que vous avez écrit. Ce qu'un barreau coûte, en revanche, est énoncé ici plutôt que laissé à découvrir.

Le pasCe qu'il apporteCe qu'il coûte
0une Frond, un processune app qui tourne — ni réseau, ni HTTPrien
1un deuxième domaine, dans fronds/billing/une frontière de propriété, avant tout réseaune plus aller dans les fichiers d'une autre Frond (fougere check le signale en cross-frond-import), et un collector doit vivre dans la Frond qui le consomme — sinon le corps de la requête est lié au paramètre qui voulait un utilisateur
2remotes: { blog: '…' }la Frond tourne ailleurs, même codele saut et son JSON ; au-delà du loopback le receveur doit pouvoir établir son appelant, ce qui fait deux commandes au déploiement ; et deux capacités disparaissent — une surface nommée ne répond rien pour une Frond distante, et une adresse par Frond veut dire que la même Frond ne peut pas être déployée deux fois
3la Frond part dans son propre dépôtson équipe, son cycle de release, son déploiementle scan ne la lit plus : le contrat doit être recopié (fougere sync), le porteur sous la diffusion des faits est à votre charge, et un fait est jugé strictement — donc ses lecteurs se déploient avant son émetteur
4la Frond n'est pas en TypeScriptun host Rust ou Python qui répond aux mêmes appelsc'est à vous d'honorer la carte et le fil

Le barreau 0 ne coûte rien, et c'est là que se tiennent tous les demos de ce dépôt. Le barreau 1 est réversible — la Frond racine ne bouge pas quand le deuxième domaine arrive — mais il n'est pas gratuit : c'est là qu'un collector mal placé devient une question de privilège plutôt que de rangement. Le barreau 2 achète le saut et le paie de deux capacités. Le barreau 3 est la vraie frontière, parce qu'il retire la colocalisation que le scan lisait : voir où le code vit.

L'énoncé de topologie

// fougere.config.ts
export default defineFougere({
  remotes: { blog: 'http://127.0.0.1:4100' },
});

Une Frond déclarée dans remotes reste scannée : ses entités fournissent toujours les métadonnées utilisées par les formulaires, la validation et la DI. Ses opérations sont en revanche exécutées à l'adresse distante. Retirer cette ligne rétablit l'exécution locale. La démo multi-Frond couvre les deux configurations, y compris en build de production :

pnpm dev:blog      # la Frond blog seule, dans son process (:4100)
pnpm dev           # l'app — la consomme via la ligne remotes

Le contrat d'appel

Un appel est une valeur : (entity, operation, invocation) avec invocation = { params, query, body, state }.

  • createLocalRunner exécute strictement en local ;
  • createAppRunner suit la topologie — façades locales, doublures distantes ;
  • les transports sérialisent cette valeur sans changer sa structure.
Un appel, les deux topologies. Le saut est inséré ; la façade qui juge, lie, présente et projette est la même des deux côtés.
Un appel, les deux topologies. Le saut est inséré ; la façade qui juge, lie, présente et projette est la même des deux côtés.

Le format du fil

Process-à-process, c'est JSON-RPC 2.0 sur POST /_fougere/call :

// → requête
{ "jsonrpc": "2.0", "id": 1, "method": "post.publish",
  "params": { "params": { "id": "" }, "query": {}, "body": null, "state": { "user": { } } } }

// ← succès
{ "jsonrpc": "2.0", "id": 1, "result": { "id": "", "status": "published", } }

// ← échec métier — ravivé en FougereError côté appelant
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32000,
    "data": { "code": "CONFLICT", "message": "Déjà publié",
              "entity": "post", "operation": "publish" } } }

method est entity.op et params contient l'invocation. Le navigateur envoie la même trame à Nitro, mais son state est ignoré et reconstruit côté serveur.

L'autre moitié du contrat est ce qu'un hôte répond à rpc.discover : voir La carte d'identité, qui spécifie le document et ce qu'il faut honorer pour écrire une Frond dans un autre langage.

Loopback ou signé

Le récepteur écoute 127.0.0.1 et plafonne les corps à 1 Mio. L'élargir est une ligne, et cela vient avec une condition :

await serve(runner, { hosts: ['0.0.0.0'] })   // refuse de démarrer tout seul
await serve(runner, { hosts: ['0.0.0.0'], verify })   // établit son appelant

Un récepteur atteignable depuis l'extérieur de cette machine, sans moyen d'établir qui appelle, croirait n'importe quel état qu'on lui tend — alors il ne démarre pas. L'adresse porte déjà la décision : en loopback l'ensemble des appelants vaut « cette machine », et élargir hosts est l'endroit où vous écrivez que ce n'est plus le cas.

Ce qui referme, c'est une enveloppe signée, vérifiée contre une seule clé publique. L'identité entre Fronds est cette page.

Comportement après séparation

ÉlémentComportement
résultatsformes identiques des deux côtés, listes comprises
erreursmême FougereError avec code, message et details, reconstruite à l'arrivée
entrée optionnellel'absence vaut undefined ; JSON omet la propriété ; un null explicite est conservé
statela session de l'app consommatrice atteint les collectors distants, portée par l'enveloppe signée
callerinvocation.caller nomme la Frond qui a signé — absent quand rien n'a été établi
hôte inaccessibleSERVICE_UNAVAILABLE typée → statut 503 ; les appels reprennent après le redémarrage de l'hôte
transporttimeout et retry sur l'enveloppe, sans retry automatique sur une command

Le prix du split

Le split ajoute un saut HTTP et l'encodage/décodage JSON qu'il implique. Un appel reste une valeur (entity, operation, invocation) ; le transport la met sur le fil et l'en retire, puis le côté receveur exécute la même façade que le chemin local. Le coût dépend donc du réseau, de la taille de la charge et du runtime : Fougere ne prétend pas le rendre nul.

Aucun chiffre n'est cité ici : ce dépôt ne livre pas de harnais de bench, et un nombre qu'on ne peut pas rejouer n'est pas une mesure.

Où le code vit

Une Frond peut aussi être déplacée dans un dépôt distinct et rester appelée via remotes. Trois choses s'appellent « ensemble » et une seule est obligatoire : le contrat doit voyager, le code non, les ports doivent se joindre.

Deux commandes couvrent le contrat, selon ce que vous voulez partager :

Ce qu'elle faitQuand
fougere syncdemande rpc.discover à l'hôte et reconstruit ses entités en locall'hôte tourne ; c'est aussi la seule voie pour une Frond écrite dans un autre langage
fougere build-frondcompile entities/** en paquet installablevous publiez le contrat comme une dépendance

Le code source de la Frond ne traverse dans aucun des deux cas. Ce qui reste ouvert, ce sont les ports : voir Déploiement.

Un destinataire, et un seul

remotes nomme une adresse par Frond, et Facade<T> résout exactement une façade — facadeKeyOf produit une clé, le conteneur rend un objet. Tout appel dans Fougere a un destinataire unique, par construction.

Ça couvre plus large qu'il n'y paraît. Un appareil derrière un NAT, qu'on ne peut pas appeler, déclare quand même remotes et envoie vers une passerelle qu'il nomme : un destinataire — et le fait qu'il doive ouvrir la connexion lui-même est une propriété de déploiement, pas un autre genre d'appel.

Ce qui n'est pas couvert, c'est plusieurs destinataires pour un envoi :

qui détermine les destinataires
une flottel'émetteur nomme l'ensemble
un fait — un post a été publiépersonne : ceux qui ont déclaré s'y intéresser

Les deux demandent la même machinerie — le fan-out, un canal vers des membres qui ne portent pas d'adresse, et un verdict quand trois sur cinq réussissent. Fougere n'a rien de tout ça. Donc « un post est publié, la recherche réindexe et la newsletter met en file » s'écrit aujourd'hui en nommant les deux à l'endroit qui émet : l'émetteur porte la liste de tout ce qui découle de son propre geste, et un troisième lecteur le rouvre.

Cette section existe pour nommer la moitié qui est là. remotes se lit comme *l'*énoncé de topologie et il en est un sur deux ; l'autre est conçu et non livré. Le dire garde le manque visible plutôt que surprenant.

Limites

Le passage à une Frond distante ajoute les pannes et la latence propres au réseau. Fougere les expose sous forme d'erreurs typées, mais ne les masque pas. Le mode local reste le mode de référence ; remotes permet de changer la topologie lorsque le besoin apparaît.

Suite : Surfaces — les mêmes opérations derrière REST et GraphQL.

Construit avec Fougere — ce site tourne sur le framework qu'il documente.