From f14731e554b403dc982109e953df20f0643a04a4 Mon Sep 17 00:00:00 2001 From: Hyko Date: Sat, 25 Apr 2026 15:15:27 -0400 Subject: [PATCH] fix(cli): export ZenModulesClient component from client manifest to ensure side-effects execute in browser - update `renderClientManifest` to export a `ZenModulesClient` React component instead of `export {}` - update docs to explain why rendering the component is required under Next.js 15+/Turbopack and add usage example in `app/layout.js` --- docs/MODULES.md | 24 +++++++++++++++++++++--- src/core/modules/cli.js | 28 +++++++++++++++++----------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/docs/MODULES.md b/docs/MODULES.md index d2b59d3..ea23369 100644 --- a/docs/MODULES.md +++ b/docs/MODULES.md @@ -18,9 +18,9 @@ Aucun fichier de configuration manuelle. La plateforme découvre les modules par Les modules sont activés via deux **manifestes statiques** générés par la CLI `zen-modules sync` dans le projet consommateur : - `app/.zen/modules.generated.js` — manifeste serveur (importé par `instrumentation.js`). -- `app/.zen/modules.client.js` — manifeste client (`'use client'`, importé par `app/layout.js`). +- `app/.zen/modules.client.js` — manifeste client (`'use client'`, exporte un Component à rendre dans `app/layout.js`). -Les deux fichiers contiennent les mêmes `import * as ... from '@zen/module-X'` mais déclenchent `register()` dans leur bundle respectif. La séparation est nécessaire parce que `app/layout.js` est un Server Component : son graphe d'imports tourne dans le bundle serveur. Pour que les `registerPage()` / `registerWidget()` peuplent aussi le registre côté browser au moment de l'hydratation, il faut un fichier `'use client'` dédié. +Le manifeste serveur peuple le registre côté Node via le top-level await à l'import. Le manifeste client exporte `ZenModulesClient` — un Component React — que `app/layout.js` doit **rendre** dans son tree (pas seulement importer). Sous Next.js 15+/Turbopack, un `import` purement side-effect d'un fichier `'use client'` orphelin (sans Component utilisé) inclut bien le fichier dans le bundle browser mais n'en exécute pas le code top-level — `registerPage()` / `registerWidget()` ne tourneraient jamais côté client. Rendre le Component force l'exécution. ```js // app/.zen/modules.generated.js — AUTO-GÉNÉRÉ (serveur) @@ -35,7 +35,25 @@ await Promise.all(modules.map(m => m.exports.register?.())); // app/.zen/modules.client.js — AUTO-GÉNÉRÉ (client) 'use client'; import '@zen/module-posts/client'; -export {}; +export default function ZenModulesClient() { + return null; +} +``` + +```js +// app/layout.js — projet consommateur +import ZenModulesClient from './.zen/modules.client.js'; + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + ); +} ``` 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. diff --git a/src/core/modules/cli.js b/src/core/modules/cli.js index 932fd56..611dea4 100644 --- a/src/core/modules/cli.js +++ b/src/core/modules/cli.js @@ -60,10 +60,14 @@ function renderServerManifest(names) { } function renderClientManifest(clientNames) { - // Doit être un Client Component pour que les side-effects (registerPage, etc.) - // s\'exécutent dans le bundle browser au moment de l\'hydratation. app/layout.js - // (Server Component) importe ce fichier ; Next.js bascule sur le bundle client - // et exécute les imports `@zen/module-X/client` lors du chargement de la page. + // Exporte un Component React `ZenModulesClient` que le layout consommateur doit + // RENDRE dans son tree (``). C'est le seul moyen fiable + // sous Next.js 15+/Turbopack pour garantir que les side-effects top-level + // (registerPage, registerWidget) s'exécutent côté browser. Un simple + // `import './.zen/modules.client.js'` dans un Server Component met bien le + // fichier dans le bundle client mais n'en exécute jamais le code top-level + // — la transformation 'use client' s'applique aux Components, pas aux + // side-effect imports orphelins. // // Importe `@zen/module-X/client` (sous-entrée 'use client') et NON le main // entry du module — le main entry tire createTables/registerApiRoutes/etc. @@ -74,15 +78,17 @@ function renderClientManifest(clientNames) { '', ].join('\n'); - if (clientNames.length === 0) { - return header + '\nexport {};\n'; - } + const body = [ + 'export default function ZenModulesClient() {', + ' return null;', + '}', + '', + ].join('\n'); - const imports = clientNames - .map(name => `import '${name}/client';`) - .join('\n'); + if (clientNames.length === 0) return header + '\n' + body; - return [header, imports, '', 'export {};', ''].join('\n'); + const imports = clientNames.map(name => `import '${name}/client';`).join('\n'); + return [header, imports, '', body].join('\n'); } async function readIfExists(path) {