@nodefony/documentation — la doc de tes modules, servie par ton serveur
stable@nodefony/documentationmis à jour 2026-07-19
Chaque module range sa documentation à côté de son code, dans son propre dossier
docs/. Ce module fait le tour de tous ces dossiers — les tiens, ceux du framework, et même ceux des paquets installés mais pas encore activés — en dresse un catalogue unique, et le sert en JSON sous/nodefony/documentation/api/*. Il ne rend aucune page : il produit de la donnée, que le portail de Studio (ou ton propre générateur de site) transforme en pages. C’est ce qui permet à la doc d’un module d’arriver avec le paquet npm, sans site à déployer ni index à tenir à jour.
📍 Documentation › @nodefony/documentation
🧭 Par où commencer#
Trois parcours selon ce que tu viens faire. L’ordre compte : chaque étape suppose la précédente.
J’écris la documentation de mon module — la doc qui voyagera avec le paquet.
- ADR-0001 — où poser un fichier — dans le module ou à la racine. C’est la première décision, et celle qu’on défait le plus mal : déplacer une page change son identifiant, donc tous les liens qui y menaient.
- Démarrage rapide — déclarer le module, écrire la page, la voir apparaître. L’étape 2 porte le contrat de frontmatter : les six clés réellement lues par le serveur.
- Ce que le module apporte — les quatre propriétés qui expliquent
pourquoi une page atterrit où elle atterrit, et pourquoi un
index.mdouvre toujours sa section. - Architecture interne — le trajet complet du fichier au portail, si tu veux comprendre plutôt que suivre la recette.
Je publie la documentation de mon application — un portail interne, sans déployer de site.
- Démarrage rapide — le module se déclare comme n’importe quel autre, et avant Studio : le portail consomme son data plane.
- Configuration — ce qui est scanné (
docs/racine, modules chargés, paquets installés) et vers quel dépôt pointe le lien « Modifier » de chaque page. - Observabilité — Studio — les deux portes du data plane et le rôle
qu’il faut porter pour les ouvrir. Elles répondent aussi en
curl, sans interface. @nodefony/studio— la surface qui rend ces pages ; elle ne fait que consommer ce que le module publie.
Un lien tombe à côté, une page reste introuvable — le dépannage le plus fréquent.
- Ce que le module apporte, propriété « tes liens relatifs restent valides des deux côtés » — un lien non traduit signifie presque toujours une cible hors de l’index, pas un bug de rendu.
- Architecture interne — la table chemin → identifiant, seule à savoir à quoi
correspond un
../index.md, et pourquoi elle vit côté serveur. - Tests & couverture — un banc rejoue la navigation sur le corpus réel
du dépôt : il attrape le
../en trop qu’aucune relecture ne voit.
🗂️ Les pages à lire#
Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce qu’on y trouve. Ce module est volontairement mince : une seule page de brique, plus deux repères transverses qui décident où ta doc doit vivre.
| Page | Ce qu’elle résout | Tu en as besoin quand… |
|---|---|---|
| Architecture interne | le trajet d’un .md : scan, cache, identifiant, liens |
une page manque, ou tu branches un autre lecteur |
| ADR-0001 — emplacement des docs | module ou racine : la règle de placement, et pourquoi | tu crées la documentation d’un module |
| Le portail général | le catalogue de toute la doc, rangé par type | tu cherches une page dont tu ignores le module |
🧩 Ce que le module apporte#
Quatre propriétés, toutes vérifiables dans le code — c’est ce qui distingue ce module d’un
readFile sur un dossier.
La doc voyage avec le code qu’elle décrit. Le service scanne le docs/ racine du projet et
le docs/ de chaque module chargé (DocumentationService.#scanAll(), DocumentationService.ts:231).
Pour ton module, la seule condition est d’avoir déclaré docs dans le champ files de son
package.json — sans quoi npm ne publie pas le dossier, et la doc disparaît à l’installation.
Le regroupement en sections ne se déclare nulle part : il est calculé depuis le dossier parent
du fichier (group, docScanner.ts:76), et l’index.md d’un dossier est présenté en premier
(DocumentationService.#orderPages(), DocumentationService.ts:486) — un point d’entrée trié
alphabétiquement se retrouverait au milieu de ses propres pages.
La doc d’un module non activé est lisible quand même. Les paquets présents dans
node_modules/@nodefony/* sont scannés même s’ils ne figurent pas dans le manifeste de
l’application (DocumentationService.#installedDocDirs(), DocumentationService.ts:386). C’est
précisément le moment où on lit la doc d’un module : pour décider de l’activer. Les chemins sont
résolus en lien réel, donc un dépôt en espace de travail indexe la source, jamais le lien
symbolique — sinon le même fichier existerait sous deux chemins, et ses liens ne résoudraient plus.
Un identifiant de page est une clé, jamais un chemin. Servir une page consiste à retrouver son
entrée par égalité d’identifiant dans le catalogue scanné, puis à ouvrir le chemin absolu déjà
connu (DocumentationService.getPage(), DocumentationService.ts:151). Le mod~http~index reçu du
client n’est jamais concaténé à un chemin de système de fichiers. Une garde en défense de profondeur
(isSafeSlug(), slug.ts:39) rejette en plus tout identifiant suspect — segment .., séparateur,
octet nul, hors jeu de caractères — avant même la recherche.
Tes liens relatifs restent valides des deux côtés. Une page se lie à ses voisines par chemin
relatif ([Architecture](architecture/)), ce qui la rend lisible sur GitHub et dans l’éditeur ;
le portail, lui, navigue par identifiant. La traduction est faite au service
(rewriteInternalLinks(), linkResolver.ts:90), seul à connaître la table chemin → identifiant.
Une cible absente de l’index est laissée intacte plutôt que réécrite au hasard : mieux vaut un
lien inerte qu’un identifiant inventé.
❗ Important
Le module ne rend aucun HTML. Il produit deux formes de données,
IDocTree(IDocumentation.ts:57) etIDocPage(IDocumentation.ts:67), et s’arrête là. Conséquence pratique : tout ce que montre le portail est aussi lisible encurl, en script, ou par un agent — et le même data plane alimentera un générateur de site statique ou une indexation documentaire sans qu’une ligne du module change. Le rendu appartient au lecteur, jamais au serveur.
Le module se déclare par ailleurs non critique (Documentation.critical, index.ts:33) : un
échec de son démarrage n’emporte jamais le processus. On perd le catalogue, jamais l’application.
🚀 Démarrage rapide#
Vu depuis une application créée par nodefony create app, qui veut publier sa propre documentation
interne.
1. Déclarer le module#
// nodefony.config.ts — l'orchestrateur de l'application
export default defineConfig(() => ({
modules: [
"@nodefony/http",
"@nodefony/framework",
// Le data plane est protégé par rôle : sans pare-feu, personne ne porte
// le rôle qui ouvre /nodefony/documentation/api/*.
"@nodefony/security",
use("@nodefony/documentation", {
// `docs/` à la racine du projet = la doc transverse de TON application.
scan: { rootDir: "docs", includeModules: true, includeInstalled: true },
// Le lien « Modifier » de chaque page pointera vers TON dépôt.
repo: { url: "https://github.com/acme/boutique", editPathPrefix: "blob" },
// 0 = rescan à chaque appel : un nouveau `.md` apparaît sans redémarrer.
// En production, garder le défaut (30 s) — le scan touche le disque.
cache: { ttlMs: 0 },
}),
// Studio APRÈS : son portail consomme le data plane déclaré au-dessus.
"@nodefony/studio",
],
}));
2. Écrire une page#
Un fichier .md dans docs/ (ou <ton-module>/docs/), ouvert par un bloc de métadonnées. Le
parseur est un YAML plat volontairement restreint (parseFrontmatter(), frontmatter.ts:51) :
clé/valeur, liste en ligne [a, b] ou liste en bloc. Ni objets imbriqués, ni valeurs multilignes.
---
title: "Facturation — cycle d'une facture"
audience: [developer]
status: stable
updated: 2026-07-19
version: "1.4.0"
source: "docs/facturation.md"
---
Six clés seulement sont consommées par le serveur ; les autres (tags, topic, module…) sont
conservées telles quelles, sans effet sur le catalogue — elles servent à l’indexation documentaire.
| Clé | Ce qu’elle change | À défaut |
|---|---|---|
title |
le titre affiché dans le catalogue et en tête de page | le nom de fichier, humanisé |
audience |
les personas qui voient la page (filtre de vue) | vide = visible par toutes |
status |
le badge de maturité affiché à côté du titre | aucun badge |
version |
la version montrée pour la page | "doc" |
updated |
la date de fraîcheur affichée | aucune date |
source |
le chemin dépôt qui construit le lien « Modifier » | le chemin réel du fichier, relatif au projet |
Les valeurs de audience et de status sont des énumérations fermées, DocAudience
(IDocumentation.ts:10) et DocStatus (IDocumentation.ts:13) : toute valeur hors liste est
silencieusement écartée, jamais affichée telle quelle.
⚠️ Attention
Deux pièges coûtent une page mal rangée. La date se déclare
updated— unlast-updatedn’est pas lu, et la page paraît sans fraîcheur. Et une clésectiondans le frontmatter ne regroupe rien : le regroupement vient du dossier parent du fichier (group,docScanner.ts:76). Pour ranger une page ailleurs, on la déplace ; on ne la renomme pas.
3. La lire#
# L'index complet : sections, pages, personas. Un compte porteur du rôle suffit.
curl -k --cookie-jar /tmp/j -b /tmp/j \
https://127.0.0.1:5152/nodefony/documentation/api/tree
# Une page précise, markdown résolu + lien « Modifier » assemblé côté serveur.
curl -k -b /tmp/j \
https://127.0.0.1:5152/nodefony/documentation/api/page/root~facturation
Ce qu’on observe : …/api/tree renvoie les sections dans l’ordre — la racine d’abord, puis un
groupe par module — chaque section ouverte par son index.md. …/api/page/{slug} renvoie le
markdown sans son bloc de métadonnées, variables résolues et liens internes traduits. Un
identifiant inconnu ou rejeté répond un 404 volontairement muet ({slug, error}) : le détail reste
dans les journaux du serveur. La même page s’affiche dans Studio sur /nodefony/documentation.
🏛️ Place dans le framework#
Le module s’appuie sur @nodefony/framework pour le routage et sur @nodefony/http pour le
contexte de requête ; il n’impose aucune base de données et n’écrit rien. La flèche ne part jamais
dans l’autre sens : aucun module ne dépend de lui pour fonctionner, et Studio n’en est qu’un
consommateur parmi d’autres.
🧰 Surface publique#
Côté serveur, le module expose DocumentationService — sa méthode getTree()
(DocumentationService.ts:185) construit le catalogue, getPage()
(DocumentationService.ts:244) sert une page, invalidate() (DocumentationService.ts:177) force
un rescan immédiat, et registerVar() (DocumentationService.ts:138) branche une variable
dynamique.
Les variables sont la seule extension du module. Une page écrit {{ nom }} ; le serveur substitue
la valeur au moment de servir (DocumentationService.#resolveVars(),
DocumentationService.ts:541). Trois variables sont fournies d’office — version du noyau, branche
et empreinte git — enregistrées quand tous les modules sont montés
(Documentation.onKernelReady(), index.ts:70). Ton module peut ajouter les siennes :
// Dans le hook onKernelReady de ton module : tous les services existent.
const docs = this.get<IDocumentationService>("documentation");
docs?.registerVar("tarif-socle", () => "29 € / mois");
Une variable sans fournisseur est laissée telle quelle dans la page : l’auteur voit qu’il manque un branchement, au lieu d’un trou silencieux. Un fournisseur qui échoue ne casse jamais le rendu.
Le module publie aussi ses briques pures, utilisables hors serveur — parseFrontmatter(),
scanDocsDir() (docScanner.ts:55), isSafeSlug() et pathToSlug() (slug.ts:60) — de quoi
écrire un générateur de site qui range les fichiers exactement comme le portail. Les signatures
exactes vivent dans le graphe généré (jq '.symbols.DocumentationService' .ai/symbols.json), jamais
recopiées ici : elles divergeraient en silence.
⚙️ Configuration#
Un seul point d’entrée : use("@nodefony/documentation", { … }) dans nodefony.config.ts, validé
au démarrage contre le schéma du module (documentationConfigSchema,
nodefony/config/config.ts:134). Quatre blocs :
| Bloc | Ce qu’il décide | Défaut d’usine |
|---|---|---|
enabled |
active le data plane ; false = module chargé mais inerte |
true |
scan |
les sources indexées : dossier racine, modules chargés, paquets installés, exclusions | docs · tout activé |
repo |
le dépôt visé par le lien « Modifier » d’une page, et la forme du lien | dépôt Nodefony · segment edit |
cache |
la durée de vie du catalogue ; 0 = rescan à chaque appel |
30000 ms |
Deux réglages se surchargent par l’environnement, appliqués après la validation
(defineDocumentationConfig(), defineModuleConfig.ts:32) : DOCS_REPO_URL et DOCS_REPO_BRANCH.
Le second sert en conteneur, où le dépôt git n’est pas embarqué — sans lui, la branche est lue au
runtime dans le dépôt réel, et retombe sur main s’il n’y en a pas.
💡 Astuce
Le cache ne porte que le catalogue, jamais le contenu. Une page est relue à chaque demande (
DocumentationService.#ensureCache(),DocumentationService.ts:298) : corriger une phrase se voit au rafraîchissement. C’est ajouter ou supprimer un fichier qui attend l’expiration — d’oùttlMs: 0en développement, et le défaut en production.
📡 Observabilité — Studio#
Le portail vit sur /nodefony/documentation : l’arbre des sections à gauche, la page rendue au
centre, le sommaire et le lien « Modifier » à droite. La page du module,
/nodefony/modules/documentation, montre par ailleurs sa configuration résolue, ses routes et ses
symboles.
Deux portes composent le data plane, toutes deux réservées aux rôles de développement et de
supervision (DocumentationController.ts:48) — la documentation technique n’est pas une page
publique :
| Route | Ce qu’elle renvoie |
|---|---|
GET /nodefony/documentation/api/tree |
le catalogue : sections, pages, personas (DocumentationController.ts:50) |
GET …/api/page/{slug} |
une page résolue + son lien source (DocumentationController.ts:65) |
Le lien « Modifier » est assemblé côté serveur à partir d’un chemin relatif au dépôt
(DocumentationService.#buildSourceUrl(), DocumentationService.ts:560) : aucun chemin absolu de
système de fichiers ne sort jamais du serveur.
🧪 Tests & couverture#
Les compteurs sont régénérés depuis vitest, jamais figés dans cette prose. Ce qui mérite d’être dit ici, c’est ce que les suites prouvent — et la frontière volontaire de ce qu’elles ne couvrent pas.
| Type | Où | Ce qui est prouvé |
|---|---|---|
| Métadonnées | nodefony/tests/unit/frontmatter.test.ts |
YAML plat : listes, quotes, absence de bloc, clés déclarées vides |
| Identifiants | nodefony/tests/unit/slug.test.ts |
fabrication et garde anti-traversée, jeu de caractères, bornes |
| Découverte | nodefony/tests/unit/docScanner.test.ts |
dossier absent, exclusions par segment, titre déduit du nom de fichier |
| Traduction des liens | nodefony/tests/unit/linkResolver.test.ts |
remontées relatives, ancres, cibles hors index laissées intactes |
| Navigation du corpus | nodefony/tests/unit/corpusLinks.test.ts |
les liens des vraies pages du dépôt résolvent tous |
Le dernier est le plus utile au quotidien : il rejoue la navigation sur le corpus réel plutôt que
sur un index fabriqué, et attrape ce qu’aucun test à double ne voit — un ../ en trop, une page
renommée, une cible supprimée. La frontière est délibérée : le service et le contrôleur dépendent du
noyau et du conteneur, ils relèvent donc de l’intégration sur serveur vivant, pas du run unitaire.
cd src/packages/@nodefony/documentation
npm test # suite unitaire, sans serveur
npm run coverage # + rapport lisible dans l'onglet Couverture de Studio
🔗 Pour aller plus loin#
- ⬆️ Remonter : Toute la documentation
- 📄 La page du module : Architecture interne — du fichier au portail
- 🧭 Modules voisins :
@nodefony/studio(le portail qui rend ces pages) ·@nodefony/framework(routage et décorateurs) ·@nodefony/security(les rôles qui ouvrent le data plane) ·nodefony(le noyau, ses modules et son cycle de vie) - 🏛️ Transverse : ADR-0001 — emplacement des docs · vue d’ensemble du framework · configuration
- 📖 Lexique général du framework.