docs(modules): update module discovery architecture to static manifest approach

- replace dynamic import strategy with static manifest generated by `zen-modules sync` cli
- add `zen-modules` binary entry point in `package.json`
- add `cli.js` implementing the `zen-modules sync` command
- update `discover.server.js` to consume static manifest instead of scanning at runtime
- update `index.js` to reflect new module registration flow
- update `init.js` to accept pre-resolved modules from manifest
- revise docs to document manifest format, sync triggers, and build requirements
This commit is contained in:
2026-04-25 14:24:56 -04:00
parent c9a3634fc9
commit 94ab6c36cb
7 changed files with 269 additions and 96 deletions
+42 -7
View File
@@ -15,13 +15,50 @@ Aucun fichier de configuration manuelle. La plateforme découvre les modules par
## Découverte
Au boot et au lancement de `zen-db init`, le core scanne `dependencies` + `devDependencies` du `package.json` du projet consommateur et charge tout package matchant :
Les modules sont activés via un **manifeste statique** généré par la CLI `zen-modules sync` dans le projet consommateur, à `app/.zen/modules.generated.js`. Ce fichier contient des `import * as ...` pour chaque package détecté et appelle `register()` au top level :
```js
// app/.zen/modules.generated.js — AUTO-GÉNÉRÉ
import * as m0_zen_module_posts from '@zen/module-posts';
export const modules = [
{ name: '@zen/module-posts', exports: m0_zen_module_posts },
];
await Promise.all(modules.map(m => m.exports.register?.()));
```
Le manifeste est importé deux fois :
- **Côté serveur** par `instrumentation.js` qui le passe à `initializeZen({ modules })`.
- **Côté client** par `app/layout.js` (side-effect import).
Cette importation statique permet à Turbopack/Webpack d'analyser le graphe complet des modules — JSX, `next/headers`, `next/navigation`, frontières `'use client'` — exactement comme pour le code de l'application elle-même.
### Critères de détection
`zen-modules sync` scanne `dependencies` + `devDependencies` du `package.json` du projet et inclut tout package matchant :
- **Préfixe officiel** : `@zen/module-*`
- **Préfixe non-scopé** : `zen-module-*`
- **Tiers** : tout package dont le `package.json` contient `"keywords": ["zen-module"]`
Pour chaque module trouvé, le core vérifie qu'il exporte les bons symboles, puis l'enregistre.
### Quand resync ?
Le template `@zen/start` câble la CLI dans :
```json
{
"scripts": {
"postinstall": "zen-modules sync",
"dev": "zen-modules sync && next dev",
"build": "zen-modules sync && next build"
}
}
```
`postinstall` couvre `npm install @zen/module-X`. Les hooks `dev`/`build` couvrent les retraits / changements de version qui ne déclenchent pas de re-install. La commande est idempotente — pas d'écriture si le contenu est identique.
`app/.zen/modules.generated.js` est gitignoré : régénéré localement, jamais commit.
---
@@ -253,13 +290,11 @@ Le champ `files` dans `package.json` publie **uniquement** `dist/`, `README.md`
---
## Build obligatoire avant publish
## Build avant publish
**Tout module `@zen/module-*` doit être pré-compilé avant publication.** Les fichiers du module ne doivent jamais contenir de JSX brut au runtime — le JSX doit être transformé en `React.createElement` (ou équivalent) par le build du module lui-même.
**Tout module `@zen/module-*` est pré-compilé avant publication**, comme `@zen/core` lui-même. Le build `tsup` avec `bundle: false` transforme le JSX, préserve les directives `'use client'` en haut des fichiers compilés, et garde un fichier d'entrée par fichier de sortie pour respecter les frontières RSC quand le projet consommateur bundle le module.
**Pourquoi.** Le core découvre les modules via `import(/* turbopackIgnore */ name)` dans [`discover.server.js`](../src/core/modules/discover.server.js) ; le commentaire `turbopackIgnore` est nécessaire pour empêcher Turbopack/Webpack de tenter de bundler un nom de package dynamique. Conséquence : tout l'arbre d'imports transitifs du module est résolu et exécuté par Node natif, **sans** transformation JSX. Un fichier `.client.js` qui contient `<MyComponent />` au runtime fait planter `register()` avec `Unexpected token '<'`.
Le module doit donc utiliser le même setup que `@zen/core` : un build `tsup` avec `bundle: false`, qui transforme JSX → JS standard tout en préservant la structure de fichiers (un fichier d'entrée → un fichier de sortie). Le `'use client'` est conservé en haut des fichiers `.client.js` compilés, ce qui permet à Next.js de respecter les frontières RSC quand le projet consommateur bundle le module.
C'est ce qui permet au manifeste statique généré par `zen-modules sync` de simplement faire `import * as ... from '@zen/module-X'` et de laisser Turbopack/Webpack composer le reste : aucune transformation runtime n'est requise côté consommateur.
### Exemple de `tsup.config.js` minimal