docs(modules): add client manifest generation and update discovery docs
- introduce `OUTPUT_CLIENT` constant and `renderClientManifest` for `'use client'` bundle - rename `renderManifest` to `renderServerManifest` for clarity - update `sync` command to write both server and client manifests - update `findInstalledModuleNames` to support custom package path resolution - rewrite MODULES.md to explain dual-manifest architecture and client hydration rationale
This commit is contained in:
+59
-11
@@ -20,7 +20,8 @@ import { resolve, dirname } from 'node:path';
|
||||
import { step, done, warn, fail } from '@zen/core/shared/logger';
|
||||
import { findInstalledModuleNames } from './discover.server.js';
|
||||
|
||||
const OUTPUT_PATH = 'app/.zen/modules.generated.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`
|
||||
@@ -28,7 +29,7 @@ function safeIdentifier(name, idx) {
|
||||
return `m${idx}_${cleaned}`;
|
||||
}
|
||||
|
||||
function renderManifest(names) {
|
||||
function renderServerManifest(names) {
|
||||
const header = '// AUTO-GÉNÉRÉ par `npx zen-modules sync` — ne pas modifier à la main.\n';
|
||||
|
||||
if (names.length === 0) {
|
||||
@@ -52,12 +53,48 @@ function renderManifest(names) {
|
||||
'];',
|
||||
'',
|
||||
'// Top-level await : déclenche register() de chaque module au moment de l\'import',
|
||||
'// de ce manifeste, des deux côtés (server bundle + client bundle).',
|
||||
'// côté serveur. instrumentation.js importe ce fichier au boot.',
|
||||
'await Promise.all(modules.map(m => m.exports.register?.()));',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function renderClientManifest(names) {
|
||||
// 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 register() lors du chargement de la page.
|
||||
const header = [
|
||||
'// AUTO-GÉNÉRÉ par `npx zen-modules sync` — ne pas modifier à la main.',
|
||||
"'use client';",
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
if (names.length === 0) {
|
||||
return header + '\nexport {};\n';
|
||||
}
|
||||
|
||||
const imports = names
|
||||
.map((name, i) => `import * as ${safeIdentifier(name, i)} from '${name}';`)
|
||||
.join('\n');
|
||||
|
||||
const calls = names
|
||||
.map((_, i) => `${safeIdentifier(names[i], i)}.register?.();`)
|
||||
.join('\n');
|
||||
|
||||
return [
|
||||
header,
|
||||
imports,
|
||||
'',
|
||||
'// Fire and forget — registerPage/registerWidget sont synchrones, le reste',
|
||||
'// (init asynchrone éventuel) ne bloque pas le rendu.',
|
||||
calls,
|
||||
'',
|
||||
'export {};',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function readIfExists(path) {
|
||||
try {
|
||||
return await readFile(path, 'utf-8');
|
||||
@@ -66,20 +103,31 @@ async function readIfExists(path) {
|
||||
}
|
||||
}
|
||||
|
||||
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 outputPath = resolve(cwd, OUTPUT_PATH);
|
||||
const next = renderManifest(names);
|
||||
const prev = await readIfExists(outputPath);
|
||||
const count = `(${names.length} module${names.length === 1 ? '' : 's'})`;
|
||||
|
||||
if (prev === next) {
|
||||
step(`zen-modules: ${OUTPUT_PATH} already up to date (${names.length} module${names.length === 1 ? '' : 's'})`);
|
||||
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(names));
|
||||
|
||||
if (!wroteServer && !wroteClient) {
|
||||
step(`zen-modules: manifests already up to date ${count}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, next, 'utf-8');
|
||||
done(`zen-modules: wrote ${OUTPUT_PATH} (${names.length} module${names.length === 1 ? '' : 's'})`);
|
||||
if (wroteServer) done(`zen-modules: wrote ${OUTPUT_SERVER} ${count}`);
|
||||
if (wroteClient) done(`zen-modules: wrote ${OUTPUT_CLIENT} ${count}`);
|
||||
for (const name of names) step(` → ${name}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { createRequire } from 'node:module';
|
||||
import { warn } from '@zen/core/shared/logger';
|
||||
import { info, warn } from '@zen/core/shared/logger';
|
||||
import { registerModule } from './registry.js';
|
||||
|
||||
/**
|
||||
@@ -43,6 +43,7 @@ export function registerModules(modules) {
|
||||
createTables: ex.createTables,
|
||||
dropTables: ex.dropTables,
|
||||
});
|
||||
info(`zen-modules: registered ${ex.manifest.name}@${ex.manifest.version ?? '?'}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user