La CLI
@fougere/cli crée un workspace, héberge une Frond dans un processus séparé et appelle
une opération depuis un shell. La CLI utilise elle-même des entités et des handlers ; ses
options sont donc validées à partir de leurs schémas.
Alpha. Sur npm sous le tag
alpha—npx fougere@alphala lance sans installer,npm create fougerel'entre directement surnew. Les commandes et flags ci-dessous sont ce qui existe.
fougere new — composer un workspace
fougere new shop --frond blog --app nuxt
La commande crée d'abord les Fronds, puis les apps qui les consomment. Elle produit un workspace pnpm :
shop/
fougere.config.ts ← db, auth, topologie
pnpm-workspace.yaml ← packages: [fronds/*, apps/*]
fronds/blog/ ← le domaine
entities/Post.ts
handlers/PostHandler.ts
apps/nuxt/ ← le consommateur
nuxt.config.ts
app/pages/index.vue
| Flag | Ce qu'il fait |
|---|---|
--frond blog,api:catalog | les Fronds à ajouter — template:nom renomme ; le nom du template est le nom par défaut |
--app nuxt:web | pareil, pour les apps |
--bare | la coquille vide, rien de composé |
--flat | un seul domaine : pas de fronds/, pas de workspace — la racine de l'app est la Frond |
--local | lie @fougere/* à un checkout local de Fougere (développement) |
--force | écrase un dossier existant |
Arguments ou mode interactif. Avec --frond et --app, la commande n'affiche aucune
question et peut être utilisée dans un script ou en CI. Sans ces options, elle demande les
templates dans un TTY. Le mode interactif n'est pas disponible sans TTY.
Structure à plat. fougere new shop --flat --frond blog
écrit une app Nuxt dont la racine porte le domaine — pas de segment fronds/, pas de
pnpm-workspace.yaml, un seul pnpm install (la forme à
plat). --app est refusé
avec lui : l'app est la racine. Un deuxième domaine ira plus tard dans fronds/billing/
et la Frond racine ne bouge pas. Le projet généré porte aussi un tsconfig.frond.json
dont le include nomme les dossiers de la convention. pnpm typecheck peut ainsi vérifier
le domaine sans charger Nuxt.
La Frond générée contient une entité, deux vues dérivées (NewPost pour l'entrée et
PostCard pour la liste) et une opération métier en plus du CRUD :
export class NewPost extends Post.pick('title', 'body') {}
export default class PostHandler extends Crud(Post) {
async create(input: NewPost): Promise<Post> { … }
/** La transition draft→published — une opération, pas une écriture de champ. */
async publish(id: string): Promise<Post> { … }
}
Les vues définissent les contrats d'entrée et de sortie de ces opérations.
fougere serve — une Frond, son propre process
fougere serve blog --port 4100
Démarre cette Frond seule et l'expose en JSON-RPC sur POST /_fougere/call. L'app qui la
consomme indique son adresse dans remotes: pour router les appels vers cet hôte. Voir
le gradient.
Elle ne suit pas remotes: elle-même : un hôte est la Frond, il ne route pas vers
l'extérieur.
serve()écoute127.0.0.1sauf sihostsdit autre chose : un récepteur lit l'identité sur le fil, donc le défaut le garde sur la machine et l'élargir est une décision écrite.
fougere call — appeler une opération
fougere call post.list
fougere call post.create --title "Bonjour" --body "…"
La commande utilise la même enveloppe, la même validation et les mêmes erreurs typées que
les autres clients. Une erreur VALIDATION_FAILED conserve donc le format reçu par une
page.
fougere sync — consommer une Frond distante
fougere sync blog --from http://blog-service:4100
Demande rpc.discover à l'hôte et écrit localement les entités reconstruites depuis les
cartes qu'il renvoie :
// .fougere/remotes/blog/entities/Post.ts — généré
import { reconstruct } from '@fougere/schema';
export class Post extends reconstruct<{
id: string;
title: string;
createdAt: Date;
}>({ /* la carte, telle quelle */ }) {}
Une classe, comme celle qu'on aurait écrite à la main : Post est la valeur — un juge qui
valide localement, sans requête supplémentaire vers l'hôte — et le type d'une ligne qu'il
renvoie, si bien que post.titel ne compile pas. Les deux se lisent sur la même carte.
À côté, handlers/PostHandler.ts énonce ce que la frond sert, pour qu'un consommateur
écrive Facade<PostHandler> sans détenir le code du handler :
export interface PostHandler {
list(invocation?: Invocation): Promise<Post[] & { total?: number; hasMore?: boolean }>;
findById(invocation?: Invocation): Promise<Post | undefined>;
}
La commande écrit aussi .fougere/remotes.json, que le module Nuxt lit pour aliaser
@frond/blog. Les pages utilisent ainsi le même chemin d'import pour une entité locale
ou distante.
La relancer supprime ce que l'hôte ne sert plus. Le barrel perd son export tout seul,
mais le fichier restait — et exports liste './entities/*' en joker, si bien que
@frond/blog/entities/Ticket.js résolvait encore vers une classe qui valide parfaitement
et dont plus rien ne répond derrière. Seuls les fichiers portant l'en-tête généré sont
supprimés ; ce que vous déposez vous-même dans ce dossier est laissé intact.
L'hôte renvoie les entités associées à une façade, et les faits que ses fronds annoncent. Une entité qui n'est ni l'un ni l'autre n'est pas dans le résultat de découverte — une forme que personne n'expose et que personne n'a déclarée sortante reste chez elle.
Un fait n'a pas d'opération : il obtient une classe et aucune interface Handler à côté.
// .fougere/remotes/blog/entities/PostPublished.ts — généré
export class PostPublished extends reconstruct<{ id: string; title: string; at: Date }>(…) {}
Pour qu'un fait qui arrive soit jugé, réexportez cette classe dans une de vos propres
fronds — le boot valide contre une entité d'une frond scannée, et .fougere/remotes/
est un paquet, pas une frond :
// fronds/search/entities/PostPublished.ts
export { default } from '../../../.fougere/remotes/blog/entities/PostPublished.js';
Les règles
| code | ce que ça veut dire |
|---|---|
directory-unreadable, handler-parse-failed, heritage-unresolved | ce que le scan n'a pas pu faire — une règle sur une absence n'est fondée que si l'analyse atteste avoir regardé |
operation-unbound | une opération déclare des paramètres et n'a aucun plan de liaison : elle est servie, et elle n'en reçoit aucun |
cross-frond-import | un import relatif qui résout dans une autre frond |
La dernière est un avertissement, pas un refus — ça résout aujourd'hui et l'app tourne. Ce
qu'elle énonce, c'est qu'une contrainte de colocalisation tient l'application et que rien
ne la déclare : '../../user/entities/User.js' dit que ces deux dossiers sont voisins,
dans une chaîne que le scan, la carte d'identité et remotes: ignorent tous. Ça marche
jusqu'au jour où ce dossier n'est plus là, et ça casse alors comme un chemin de fichier,
pas comme un modèle.
'@frond/user/entities/User.js' énonce la même dépendance en termes que le modèle lit, et
c'est la forme qu'écrit fougere sync — elle
survit donc au déménagement de la frond. Le nom résout pareil, que la frond soit locale ou
synchronisée.
fougere graph — lire le modèle
fougere graph
Affiche les entités, leurs références et le nombre de références entrantes. Au-delà de six entités, la commande propose aussi des regroupements calculés depuis ce graphe :
Post → Author, Category (2 incoming)
Author
La page chemin le plus court décrit les usages actuels et envisagés de ce graphe.
fougere build-frond — publier les entités d'une Frond
fougere build-frond blog
Compile fronds/blog/entities/** vers dist/ avec les déclarations de types et configure
le package.json de la Frond. Un consommateur situé dans un autre dépôt peut ensuite
installer ce paquet. Voir où le code vit.
Suite : La philosophie.