f14731e554
- 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`
175 lines
5.4 KiB
JavaScript
175 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Zen Modules CLI
|
|
*
|
|
* Génère `app/.zen/modules.generated.js` à partir des dépendances `@zen/module-*`
|
|
* détectées dans le `package.json` du projet consommateur. Le fichier généré est
|
|
* ensuite importé par `instrumentation.js` (côté serveur) et par `app/layout.js`
|
|
* (côté client) pour rendre les modules visibles aux deux bundles Next.js.
|
|
*
|
|
* Usage : `npx zen-modules sync`
|
|
*
|
|
* Conçu pour être appelé depuis postinstall + dev/build du projet consommateur.
|
|
* Idempotent : si le contenu généré est identique au fichier existant, aucune
|
|
* écriture n'est effectuée.
|
|
*/
|
|
|
|
import { writeFile, mkdir, readFile } from 'node:fs/promises';
|
|
import { resolve, dirname } from 'node:path';
|
|
import { step, done, warn, fail } from '@zen/core/shared/logger';
|
|
import { findInstalledModuleNames, moduleHasClientEntry } from './discover.server.js';
|
|
|
|
const OUTPUT_SERVER = 'app/.zen/modules.generated.js';
|
|
const OUTPUT_CLIENT = 'app/.zen/modules.client.js';
|
|
|
|
function safeIdentifier(name, idx) {
|
|
// `@zen/module-posts` → `m0_zen_module_posts`
|
|
const cleaned = name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
return `m${idx}_${cleaned}`;
|
|
}
|
|
|
|
function renderServerManifest(names) {
|
|
const header = '// AUTO-GÉNÉRÉ par `npx zen-modules sync` — ne pas modifier à la main.\n';
|
|
|
|
if (names.length === 0) {
|
|
return header + '\nexport const modules = [];\n';
|
|
}
|
|
|
|
const imports = names
|
|
.map((name, i) => `import * as ${safeIdentifier(name, i)} from '${name}';`)
|
|
.join('\n');
|
|
|
|
const entries = names
|
|
.map((name, i) => ` { name: '${name}', exports: ${safeIdentifier(name, i)} },`)
|
|
.join('\n');
|
|
|
|
return [
|
|
header,
|
|
imports,
|
|
'',
|
|
'export const modules = [',
|
|
entries,
|
|
'];',
|
|
'',
|
|
'// Top-level await : déclenche register() de chaque module au moment de l\'import',
|
|
'// côté serveur. instrumentation.js importe ce fichier au boot.',
|
|
'await Promise.all(modules.map(m => m.exports.register?.()));',
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
function renderClientManifest(clientNames) {
|
|
// Exporte un Component React `ZenModulesClient` que le layout consommateur doit
|
|
// RENDRE dans son tree (`<ZenModulesClient />`). 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.
|
|
// qui dépendent de pg/fs/net et ne peuvent pas être bundlés pour le browser.
|
|
const header = [
|
|
'// AUTO-GÉNÉRÉ par `npx zen-modules sync` — ne pas modifier à la main.',
|
|
"'use client';",
|
|
'',
|
|
].join('\n');
|
|
|
|
const body = [
|
|
'export default function ZenModulesClient() {',
|
|
' return null;',
|
|
'}',
|
|
'',
|
|
].join('\n');
|
|
|
|
if (clientNames.length === 0) return header + '\n' + body;
|
|
|
|
const imports = clientNames.map(name => `import '${name}/client';`).join('\n');
|
|
return [header, imports, '', body].join('\n');
|
|
}
|
|
|
|
async function readIfExists(path) {
|
|
try {
|
|
return await readFile(path, 'utf-8');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function writeIfChanged(path, contents) {
|
|
const prev = await readIfExists(path);
|
|
if (prev === contents) return false;
|
|
await mkdir(dirname(path), { recursive: true });
|
|
await writeFile(path, contents, 'utf-8');
|
|
return true;
|
|
}
|
|
|
|
async function syncCommand({ cwd = process.cwd() } = {}) {
|
|
const names = await findInstalledModuleNames({ cwd });
|
|
const clientNames = [];
|
|
for (const name of names) {
|
|
if (await moduleHasClientEntry(name, cwd)) clientNames.push(name);
|
|
}
|
|
|
|
const serverCount = `(${names.length} module${names.length === 1 ? '' : 's'})`;
|
|
const clientCount = `(${clientNames.length} client entr${clientNames.length === 1 ? 'y' : 'ies'})`;
|
|
|
|
const serverPath = resolve(cwd, OUTPUT_SERVER);
|
|
const clientPath = resolve(cwd, OUTPUT_CLIENT);
|
|
|
|
const wroteServer = await writeIfChanged(serverPath, renderServerManifest(names));
|
|
const wroteClient = await writeIfChanged(clientPath, renderClientManifest(clientNames));
|
|
|
|
if (!wroteServer && !wroteClient) {
|
|
step(`zen-modules: manifests already up to date ${serverCount} ${clientCount}`);
|
|
return;
|
|
}
|
|
|
|
if (wroteServer) done(`zen-modules: wrote ${OUTPUT_SERVER} ${serverCount}`);
|
|
if (wroteClient) done(`zen-modules: wrote ${OUTPUT_CLIENT} ${clientCount}`);
|
|
for (const name of names) {
|
|
const hasClient = clientNames.includes(name);
|
|
step(` → ${name}${hasClient ? ' (+ /client)' : ''}`);
|
|
}
|
|
}
|
|
|
|
function printHelp() {
|
|
console.log(`
|
|
Zen Modules CLI
|
|
|
|
Usage:
|
|
npx zen-modules <command>
|
|
|
|
Commands:
|
|
sync Régénère app/.zen/modules.generated.js à partir des @zen/module-*
|
|
déclarés dans le package.json du projet courant.
|
|
help Affiche cette aide.
|
|
`);
|
|
}
|
|
|
|
const [, , command] = process.argv;
|
|
|
|
try {
|
|
switch (command) {
|
|
case 'sync':
|
|
await syncCommand();
|
|
break;
|
|
case 'help':
|
|
case '--help':
|
|
case '-h':
|
|
case undefined:
|
|
printHelp();
|
|
break;
|
|
default:
|
|
warn(`Unknown command: ${command}`);
|
|
printHelp();
|
|
process.exit(1);
|
|
}
|
|
} catch (err) {
|
|
fail(`zen-modules: ${err.message}`);
|
|
process.exit(1);
|
|
}
|