Nodefony 10.0.0-beta.5
Framework Node.js fullstack — HTTP & WebSocket, même contexte

Deux paquets nodefony dans un process : reconnaître et corriger

stablemis à jour 2026-09-14

📍 Documentation › Guides › Deux paquets nodefony

Une panne qui n’a l’air de rien : l’application ne démarre pas, ou démarre amputée, et tout ce qu’on inspecte est sain. La configuration est juste, les versions concordent, le dist/ est frais. Cette page dit comment la reconnaître en une commande, et pourquoi elle est si difficile à voir.

Le symptôme#

Il en existe plusieurs, et aucun ne nomme la cause :

La cause#

Deux copies du paquet nodefony ont été chargées dans le même process.

En JavaScript, l’identité d’un module est son chemin. Deux fichiers différents sur le disque donnent deux modules différents, donc deux jeux de classes, deux registres de symboles, deux singletons — même si les deux fichiers sont identiques au bit près.

🔴 Les deux copies peuvent être la MÊME version. C’est ce qui rend la panne si déroutante : il n’y a aucune incohérence à repérer, rien de « périmé », rien à mettre à jour. Chercher un conflit de versions est une impasse.

Ce qui casse alors, concrètement :

Ce qui est propre à chaque copie Conséquence
La classe Container (src/nodefony/src/Container.ts#Container) container instanceof Container échoue à la frontière — le service refusait le container reçu et s’en fabriquait un vide, donc plus de kernel, plus de journal, plus d’injection. C’est la CAUSE RACINE ; depuis, ce cas lève une erreur qui la nomme
Le contexte de requête (src/nodefony/src/runtime/RequestContext.ts#RequestContext) un AsyncLocalStorage par copie : l’identité posée par l’une est invisible aux modules de l’autre
Le singleton Nodefony (src/nodefony/src/Nodefony.ts#Nodefony) un champ statique privé par copie — Nodefony.getKernel() rend null du mauvais côté
Les registres d’injection et de décorateurs un service déclaré d’un côté est introuvable de l’autre

Un module construit à cette frontière n’attache aucun hook de cycle de vie. Il ne lève pas d’erreur : il est là, et inerte.

Le diagnostic — deux commandes#

npm ls nodefony          # ou : pnpm why nodefony

deduped partout ⇒ il n’y a qu’une copie dans l’arbre npm :

mon-app@1.0.0
├─┬ @nodefony/http@10.0.0
│ └── nodefony@10.0.0 deduped          ← une seule copie

Une copie imbriquée sous un module ⇒ c’est la cause :

├─┬ un-module-tiers@2.0.0
│ └── nodefony@10.0.2                  ← sa copie à lui
NF_CLI_DEBUG=1 nodefony --version

Elle dit quel CLI s’exécute vraiment. ⚠️ Elle ne répond qu’à une des trois causes ci-dessous — celle du binaire lié ailleurs. Pour les deux autres, c’est npm ls qui tranche.

Les trois causes, et leur remède#

1. Un binaire lié vers un autre dossier#

Une installation globale, un npm link, ou un lien manuel dans ~/.local/bin. Taper nodefony prend ce binaire-là ; npm run dev et npx nodefony prennent celui de l’application, parce que npm place node_modules/.bin en tête du PATH. C’est exactement ce qui fait qu’une commande marche et l’autre non.

Ce n’est pas une mauvaise pratique en soi — le binaire global sert à nodefony create app, quand il n’y a pas encore d’application — et le lanceur passe normalement la main au CLI local. Le remède est de tenir le binaire lié à jour : c’est le noyau réellement exécuté qui porte les gardes, un noyau ancien ne peut ni se corriger ni se détecter.

2. Un module tiers qui déclare nodefony en dependencies#

Il dit alors « j’apporte ma propre copie » au lieu de « je me branche sur la tienne », et npm l’installe sous lui.

Un module Nodefony déclare nodefony en peerDependencies, jamais en dependencies. Un peer n’installe rien : il exige que l’application fournisse. Tous les paquets officiels le font — c’est ce qui garantit qu’ils ne peuvent pas dupliquer le cœur.

Remède : demander au module de corriger sa déclaration ; en attendant, aligner les versions pour que npm dédoublonne.

3. Un monorepo, ou pnpm en mode strict#

Deux paquets locaux qui exigent des versions différentes, ou un store strict qui donne à chacun ses propres dépendances. Remède : une seule version déclarée à la racine, ou une surcharge de résolution.

Ce que le framework fait de lui-même#

Chaque copie s’inscrit d’elle-même à son évaluation (src/nodefony/src/runtime/packageInstances.ts#registerPackageInstance), dans une entrée de globalThis adressée par un symbole du registre global — la seule case mémoire que deux copies partagent. Le boot les COMPTE, à deux instants : après l’import de l’application (src/nodefony/src/kernel/Kernel.ts#Kernel.loadApp), puis après le chargement des modules du manifeste, qui peut en apporter une seconde (src/nodefony/src/kernel/Kernel.ts#Kernel.preRegister). Le verdict (src/nodefony/src/kernel/Kernel.ts#Kernel.packageDualityVerdict) tranche alors :

Le refus n’est pas de la rigidité : sous dualité, ce qui disparaît en silence est exactement ce dont dépend la sécurité — un @nodefony/security écarté, et l’application sert sans pare-feu, sans que rien ne le dise.

⚠️ Si le symptôme est là et que le boot ne dit rien, c’est que le noyau exécuté est antérieur à cette garde : le code qui détecte vit dans le noyau qui tourne, aucune version ultérieure ne peut le rattraper de l’extérieur.

Ce que le framework ne fait PAS — et pourquoi#

Il ne recolle jamais les deux copies en partageant le kernel. Ce serait la correction la plus tentante, et la pire : le kernel n’est qu’un des états dupliqués. Le contexte de requête, instanceof, les registres d’injection resteraient scindés — l’application démarrerait, servirait du trafic, et l’identité de l’utilisateur serait invisible à la moitié des modules. Une dégradation plus difficile à voir que celle qu’on aurait corrigée.

On détecte, et on le dit.

📖 Lexique#

Terme Ce que c’est
Dualité de paquet Deux copies du paquet nodefony chargées dans le même process. Elles peuvent porter la MÊME version : c’est le chemin du fichier qui fait l’identité d’un module, pas son contenu.
peerDependencies Une dépendance qu’un paquet EXIGE sans l’installer : c’est l’application qui la fournit. C’est ce qui empêche un module de dupliquer le cœur.
deduped Le mot qu’affiche npm ls quand plusieurs paquets partagent une seule copie d’une dépendance. Sa présence partout est la preuve qu’il n’y en a qu’une.
Fail-soft Un module dont le chargement échoue est ÉCARTÉ, et le démarrage continue. Utile pour un module optionnel, dangereux quand c’est le pare-feu qui tombe.
EX_CONFIG Code de sortie 78, convention BSD : « la configuration est fautive », par opposition à une erreur interne. C’est celui que rend le refus de démarrage.

⚠️ Pièges#

🧪 Tests & couverture#

Ce qui garde ce comportement, et ce que chaque test prouve :

Type Où Ce qui est prouvé
Unitaires (registre) nodefony packageInstances.test.ts une copie s’inscrit d’elle-même ; deux copies sont détectées et NOMMÉES ; une même URL réenregistrée ne compte pas pour deux (anti-faux-positif)
Unitaires (verdict) nodefony packageInstances.test.ts avertissement en développement, BootConfigurationError ailleurs, et le mode runtime CONSTATÉ dans le message
Unitaires (câblage) nodefony packageInstances.test.ts la garde est consultée par preRegister() — donc une copie apportée par un MODULE est vue
Unitaires (cause racine) nodefony packageInstances.test.ts un container venu d’une autre copie est REFUSÉ au lieu d’être jeté en silence
Bout en bout nodefony packageDualityBoot.test.ts le vrai binaire, sur une application réelle : refus AVANT le chargement des modules, et démarrage avec avertissement en développement

🔗 Pour aller plus loin#