docs(modules): update server/client boundary docs and client manifest generation

- update MODULES.md to document dual-entry pattern (main vs ./client) and explain why client entry must not import server-only code
- filter client manifest to only include modules exposing a `./client` subpath export
- add `moduleHasClientEntry` helper in discover.server.js to check package.json exports
- update cli.js to use `moduleHasClientEntry` when rendering the client manifest
- update init.js and modules/index.js to align with new client entry convention
This commit is contained in:
2026-04-25 14:43:00 -04:00
parent 92f3e4c561
commit b460ed0619
5 changed files with 159 additions and 51 deletions
+42 -11
View File
@@ -34,11 +34,12 @@ await Promise.all(modules.map(m => m.exports.register?.()));
```js
// app/.zen/modules.client.js — AUTO-GÉNÉRÉ (client)
'use client';
import * as m0_zen_module_posts from '@zen/module-posts';
m0_zen_module_posts.register?.();
import '@zen/module-posts/client';
export {};
```
Le manifeste client importe la **sous-entrée `./client`** de chaque module (et non son main entry). C'est essentiel : le main entry tire `createTables` / `registerApiRoutes` / la chaîne `register-server.js` qui dépend de `pg`, `fs`, `next/headers`, etc. — code serveur incompatible avec le bundle browser. Seuls les modules qui exposent `./client` dans leur `package.json#exports` sont inclus dans le manifeste client ; les modules purement back-end (API/DB) sont absents du bundle browser.
L'importation statique (le `import * as ...`) permet à Turbopack/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.
### Critères de détection
@@ -240,18 +241,48 @@ Convention : préfixer toutes les tables par `zen_<module>_` pour éviter les co
## Frontières serveur/client
Comme pour le core (voir [DEV.md](DEV.md)), les fichiers du module portent les suffixes `.server.js` / `.client.js`. Le hook `register()` côté serveur est appelé par le core ; les enregistrements client (widgets, par ex.) doivent être triggés par un import dans le bundle client — typiquement via le composant client lui-même qui appelle `registerWidget()` à l'import.
Un module avec partie admin expose **deux entrées** dans son `package.json#exports` :
```js
// Pattern recommandé pour un module avec partie client :
// src/register-server.js (importé par register())
import './admin/BlogAdminPage.client.js'; // chaîne d'imports vers client
import './widgets/BlogWidget.server.js';
// ... registerNavItem, registerPage, registerApiRoutes, etc.
```json
{
"exports": {
".": { "import": "./dist/index.js" },
"./client": { "import": "./dist/client.js" }
}
}
```
Le bundle Next.js du projet consommateur traverse le graphe d'import et inclut les composants client dans le bundle client. Côté serveur, seules les fonctions et fetchers serveur sont chargés.
| Entrée | Bundle | Contenu |
|--------|--------|---------|
| `.` (main) | serveur | `manifest`, `register()`, `createTables`, `dropTables`. Le `register()` tire la chaîne `register-server.js` (API routes, navigation, fetchers, storage prefixes, hooks DB). |
| `./client` | client | `'use client'` ; uniquement `registerPage({ Component })` et `registerWidget({ Component })`. Aucun import vers `pg`, `fs`, `.server.js` ou `register-server.js`. |
Le manifeste serveur importe `@zen/module-X` (main) ; le manifeste client importe `@zen/module-X/client`. La séparation est obligatoire : tout ce qui est statiquement importé depuis l'entrée client finit dans le bundle browser, et `pg` / `fs` / `next/headers` y crashent.
```js
// src/index.js (entrée serveur)
export const manifest = { /* ... */ };
export async function register() { await import('./register-server.js'); }
export { createTables, dropTables } from './db.server.js';
```
```js
// src/register-server.js (server-only — appelée par register())
import { registerNavItem, registerNavSection } from '@zen/core/features/admin';
import { registerApiRoutes } from '@zen/core/api';
import { routes } from './api.server.js';
// PAS d'import de Component .client.js ici — ils vivent dans client.js.
registerNavSection({ /* ... */ });
registerApiRoutes(routes);
```
```js
// src/client.js (entrée client — chargée par le manifeste client uniquement)
'use client';
import { registerPage } from '@zen/core/features/admin';
import BlogAdminPage from './admin/BlogAdminPage.client.js';
registerPage({ slug: 'blog', Component: BlogAdminPage, title: 'Blog' });
```
---