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`
This commit is contained in:
+21
-3
@@ -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 :
|
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.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
|
```js
|
||||||
// app/.zen/modules.generated.js — AUTO-GÉNÉRÉ (serveur)
|
// 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)
|
// app/.zen/modules.client.js — AUTO-GÉNÉRÉ (client)
|
||||||
'use client';
|
'use client';
|
||||||
import '@zen/module-posts/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 (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<ZenModulesClient />
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|||||||
+17
-11
@@ -60,10 +60,14 @@ function renderServerManifest(names) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderClientManifest(clientNames) {
|
function renderClientManifest(clientNames) {
|
||||||
// Doit être un Client Component pour que les side-effects (registerPage, etc.)
|
// Exporte un Component React `ZenModulesClient` que le layout consommateur doit
|
||||||
// s\'exécutent dans le bundle browser au moment de l\'hydratation. app/layout.js
|
// RENDRE dans son tree (`<ZenModulesClient />`). C'est le seul moyen fiable
|
||||||
// (Server Component) importe ce fichier ; Next.js bascule sur le bundle client
|
// sous Next.js 15+/Turbopack pour garantir que les side-effects top-level
|
||||||
// et exécute les imports `@zen/module-X/client` lors du chargement de la page.
|
// (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
|
// Importe `@zen/module-X/client` (sous-entrée 'use client') et NON le main
|
||||||
// entry du module — le main entry tire createTables/registerApiRoutes/etc.
|
// entry du module — le main entry tire createTables/registerApiRoutes/etc.
|
||||||
@@ -74,15 +78,17 @@ function renderClientManifest(clientNames) {
|
|||||||
'',
|
'',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
if (clientNames.length === 0) {
|
const body = [
|
||||||
return header + '\nexport {};\n';
|
'export default function ZenModulesClient() {',
|
||||||
}
|
' return null;',
|
||||||
|
'}',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
const imports = clientNames
|
if (clientNames.length === 0) return header + '\n' + body;
|
||||||
.map(name => `import '${name}/client';`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
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) {
|
async function readIfExists(path) {
|
||||||
|
|||||||
Reference in New Issue
Block a user