Cycle de vie d'un module

Découverte, manifestes générés, boot serveur et client, migrations, ce qui se passe entre npm install et le premier rendu.

Un module @lrvo/module-* n'est jamais déclaré à la main. Cette page décrit ce qui se passe entre npm install et le premier rendu, et où regarder quand un module ne s'active pas.

Pour écrire un module, voir Créer un module.

Vue d'ensemble

ÉtapeCôté coreCôté module
Installation—npm install @lrvo/module-X
Configuration—Ajout des envVars au .env
Synchronisationlrvo-modules sync régénère les manifestes—
Migrationlrvo-db init charge le module, enregistre les permissions, applique le seedcreateTables() exécuté
Boot serveurinstrumentation.js appelle initializeLrvo(), qui scanne et chargeregister() exécuté
Premier rendu clientLe bundle traverse les imports du manifeste clientregisterWidget(), registerPage() exécutés
ExécutionLe routeur dispatche, l'admin résout pages et widgets via le registrerien de plus

Détection

lrvo-modules sync scanne dependencies et devDependencies du package.json du projet et inclut tout package qui matche :

FormeExemple
Préfixe officiel@lrvo/module-posts
Préfixe non scopélrvo-module-blog
Mot-clétout package dont le package.json contient "keywords": ["lrvo-module"]

Les trois manifestes de modules

L'activation passe par trois manifestes statiques générés dans app/.lrvo/. Chacun a son point d'accroche dans le projet, et ils ne sont pas interchangeables : c'est le point d'accroche qui décide de la compilation Next.js qui verra le code.

ManifesteImporté parRôle
modules.generated.jsinstrumentation.jsDéclenche les register() au boot Node.
modules.client.jsapp/layout.js, renduExécute les enregistrements côté navigateur.
public-pages.server.jsapp/lrvo/[...path]/page.jsFait entrer les pages publiques dans le graphe RSC de leur route.

Manifeste serveur

// app/.lrvo/modules.generated.js — AUTO-GÉNÉRÉ
import * as m0_lrvo_module_posts from '@lrvo/module-posts';

export const modules = [
  { name: '@lrvo/module-posts', exports: m0_lrvo_module_posts },
];

await Promise.all(modules.map(m => m.exports.register?.()));

Importé par instrumentation.js, il peuple le registre côté Node par le top-level await.

Le Promise.all lance les register() en parallèle : deux modules n'ont aucun ordre d'exécution garanti entre eux. Un module qui a besoin de savoir si un autre est activé doit passer par onModulesReady(), jamais par une lecture au niveau racine de son register-server.js. Voir Enregistrement inter-modules.

Manifeste client

// app/.lrvo/modules.client.js — AUTO-GÉNÉRÉ
'use client';
import '@lrvo/module-posts/client';

export default function LrvoModulesClient() {
  return null;
}
// app/layout.js
import LrvoModulesClient from './.lrvo/modules.client.js';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <LrvoModulesClient />
        {children}
      </body>
    </html>
  );
}

Le composant doit être rendu, pas seulement importé. Sous Next.js 15 et plus avec Turbopack, un import purement side-effect d'un fichier 'use client' orphelin inclut bien le fichier dans le bundle navigateur mais n'exécute pas son code top-level. Les registerPage() et registerWidget() ne tourneraient jamais. Rendre le composant force l'exécution.

Pourquoi la sous-entrée ./client

Le manifeste client importe @lrvo/module-X/client, pas l'entrée principale. C'est essentiel : l'entrée principale tire createTables, registerApiRoutes et toute la chaîne register-server.js, qui dépend de pg, fs et next/headers. Ce code est incompatible avec le bundle navigateur.

Les modules qui n'exposent pas ./client sont simplement absents du manifeste client. Un module purement back-end n'entre donc jamais dans le bundle navigateur.

Manifeste des pages publiques

// app/.lrvo/public-pages.server.js — AUTO-GÉNÉRÉ
import * as m0_lrvo_module_billing from '@lrvo/module-billing/public-page';

export const publicPages = [
  { name: '@lrvo/module-billing', exports: m0_lrvo_module_billing },
];
// app/lrvo/[...path]/page.js
import '../../.lrvo/public-pages.server.js';

export { default, generateMetadata } from '@lrvo/core/public-pages/server';

Contrairement au manifeste client, celui-ci n'a rien à faire exécuter côté navigateur : c'est un module serveur ordinaire, sans 'use client', et son import side-effect suffit. Ce qu'il apporte, c'est une arête d'import statique entre la route et la page publique du module.

Sans elle, le composant enregistré arriverait par instrumentation.js, une autre compilation, et ses enfants 'use client' seraient absents du React Client Manifest de la route. La page échouerait au rendu sur « Could not find the module … in the React Client Manifest ». Le tableau publicPages n'a aucun consommateur : il rend seulement l'import « utilisé », donc insensible à un "sideEffects": false.

Corollaire : le registre des pages publiques est local à cette route. getPublicModulePages() retourne un tableau vide partout ailleurs.

Pourquoi un import statique

Le import * as statique permet à Turbopack et Webpack d'analyser le graphe complet du module : JSX, next/headers, next/navigation, frontières 'use client'. Exactement comme pour le code de l'application elle-même.

Manifestes de blocs et de templates

La même commande génère trois autres fichiers, alimentés par le contenu de components/lrvo/ du projet hôte :

FichierSource
blocks.server.jscomponents/lrvo/blocks/<id>/render.js
blocks.client.jscomponents/lrvo/blocks/<id>/template.js
templates.server.jscomponents/lrvo/templates/<id>.js

Il n'existe pas de manifeste client pour les templates. Voir Templates.

Quand la synchronisation se lance

Le template @lrvo/start la câble dans trois scripts :

{
  "scripts": {
    "postinstall": "lrvo-modules sync",
    "dev": "lrvo-modules sync && next dev",
    "build": "lrvo-modules sync && next build"
  }
}

postinstall couvre npm install @lrvo/module-X. Les hooks dev et build couvrent les retraits et les changements de version, qui ne déclenchent pas toujours une réinstallation.

La commande est idempotente : pas d'écriture si le contenu est identique. Les fichiers générés sont gitignorés.

Chargement en ESM natif

discover.server.js charge chaque module via import(/* turbopackIgnore */ name), pour empêcher les bundlers de traiter un nom dynamique.

Conséquence : Node résout tout l'arbre d'imports transitifs en ESM natif, sans la condition react-server. C'est ce qui interdit un import { cookies } from 'next/headers' au top-level dans la chaîne register(). Voir Frontières serveur/client.

Activation

La découverte inclut un package installé. L'activation est un second interrupteur, porté par une variable d'environnement propre au module.

LRVO_MODULE_MEDIAS=true

Un module découvert mais non activé n'enregistre rien. C'est ce qui permet d'installer un module et de le laisser dormant, ou de l'activer par environnement.

Diagnostiquer

Quand une page d'admin, une entrée de sidebar ou un widget n'apparaît pas :

  1. Le module est-il dans le manifeste ? Ouvrir app/.lrvo/modules.generated.js. S'il est absent, le nom du package ne matche aucune forme de détection, ou la synchronisation n'a pas tourné.
  2. Est-il activé ? Vérifier sa variable LRVO_MODULE_*.
  3. Le composant client est-il rendu ? Vérifier que <LrvoModulesClient /> est bien dans le JSX de app/layout.js, pas seulement importé.
  4. Le module expose-t-il ./client ? Sans cette sous-entrée, registerPage et registerWidget ne tournent pas.
  5. Les tables existent-elles ? Relancer npx lrvo-db init.
  6. La permission est-elle attribuée ? Une entrée avec permission est masquée pour qui ne la possède pas. Vérifier dans /admin/roles.

Quand c'est la page publique /lrvo/<module>/… qui ne répond pas :

  1. Le module expose-t-il ./public-page ? Sans cette sous-entrée, il n'est pas dans app/.lrvo/public-pages.server.js et rien n'enregistre sa page : la route répond 404.
  2. La route importe-t-elle le manifeste ? app/lrvo/[...path]/page.js doit commencer par import '../../.lrvo/public-pages.server.js';. Un projet resté à l'ancienne version se répare par npx lrvo-doctor --fix.
  3. Le rendu échoue-t-il sur « Could not find the module … in the React Client Manifest » ? L'enregistrement se fait encore depuis register-server.js au lieu de public-page.js.

Les erreurs d'enregistrement sont journalisées au boot. Voir Logger.

Sur cette page