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:
@@ -1,5 +1,6 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve, join, dirname } from 'node:path';
|
||||
import { createRequire } from 'node:module';
|
||||
import { info, warn } from '@zen/core/shared/logger';
|
||||
import { registerModule } from './registry.js';
|
||||
@@ -65,16 +66,46 @@ export async function readJson(path) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function isThirdPartyModule(name, projectCwd) {
|
||||
const require = createRequire(join(projectCwd, 'package.json'));
|
||||
let pkgJsonPath;
|
||||
try {
|
||||
pkgJsonPath = require.resolve(`${name}/package.json`);
|
||||
} catch {
|
||||
return false;
|
||||
/**
|
||||
* Résout le chemin du `package.json` d'un module installé. Ne dépend PAS de
|
||||
* `require.resolve(name)` (qui peut échouer si la sous-entrée `./package.json`
|
||||
* n'est pas exposée par `exports`) ni de `import.meta.resolve` (variable selon
|
||||
* la version Node). On remonte simplement depuis `projectCwd` en cherchant
|
||||
* `node_modules/<name>/package.json` à chaque niveau — c'est le mécanisme de
|
||||
* résolution npm standard.
|
||||
*/
|
||||
function resolveModulePackageJson(name, projectCwd) {
|
||||
let dir = projectCwd;
|
||||
while (true) {
|
||||
const candidate = join(dir, 'node_modules', name, 'package.json');
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(candidate, 'utf-8'));
|
||||
return { path: candidate, pkg };
|
||||
} catch {
|
||||
// pas trouvé à ce niveau, remonter
|
||||
}
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) return null;
|
||||
dir = parent;
|
||||
}
|
||||
const pkg = await readJson(pkgJsonPath);
|
||||
return Array.isArray(pkg?.keywords) && pkg.keywords.includes('zen-module');
|
||||
}
|
||||
|
||||
export async function isThirdPartyModule(name, projectCwd) {
|
||||
const found = resolveModulePackageJson(name, projectCwd);
|
||||
return Array.isArray(found?.pkg?.keywords) && found.pkg.keywords.includes('zen-module');
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un module installé expose une sous-entrée `./client` dans son
|
||||
* `package.json#exports`. Utilisé par le CLI `zen-modules sync` pour décider
|
||||
* si le manifeste client doit l'importer. Les modules sans partie cliente
|
||||
* (modules purement back-end / API / DB) ne sont pas inclus dans le manifeste
|
||||
* client — ça évite d'embarquer leur graphe d'imports serveur dans le bundle
|
||||
* browser.
|
||||
*/
|
||||
export async function moduleHasClientEntry(name, projectCwd) {
|
||||
const found = resolveModulePackageJson(name, projectCwd);
|
||||
return Boolean(found?.pkg?.exports?.['./client']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,6 +132,53 @@ export async function findInstalledModuleNames({ cwd = process.cwd() } = {}) {
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Variante "Node-only" du chargement de modules — utilisée par le CLI
|
||||
* `zen-db init` qui ne passe jamais par un bundler. Charge dynamiquement
|
||||
* chaque module depuis `node_modules/` du projet, pour que le CLI ait accès
|
||||
* à `manifest`, `createTables`, `dropTables`. Ne déclenche PAS `register()`
|
||||
* (la chaîne register-server tirerait des imports Next.js incompatibles avec
|
||||
* le contexte CLI).
|
||||
*
|
||||
* À ne PAS utiliser depuis le runtime Next.js — utiliser le manifeste statique
|
||||
* via `initializeZen({ modules })`.
|
||||
*/
|
||||
export async function loadModulesForCli({ cwd = process.cwd() } = {}) {
|
||||
const names = await findInstalledModuleNames({ cwd });
|
||||
const require = createRequire(join(cwd, 'package.json'));
|
||||
|
||||
for (const name of names) {
|
||||
let entryPath;
|
||||
try {
|
||||
entryPath = require.resolve(name);
|
||||
} catch (err) {
|
||||
warn(`zen-modules: cannot resolve "${name}" — ${err.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mod;
|
||||
try {
|
||||
mod = await import(entryPath);
|
||||
} catch (err) {
|
||||
warn(`zen-modules: failed to import "${name}" — ${err.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mod.manifest || typeof mod.register !== 'function') {
|
||||
warn(`zen-modules: "${name}" missing manifest/register — skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
registerModule({
|
||||
manifest: mod.manifest,
|
||||
register: mod.register,
|
||||
createTables: mod.createTables,
|
||||
dropTables: mod.dropTables,
|
||||
});
|
||||
info(`zen-modules: loaded ${mod.manifest.name}@${mod.manifest.version ?? '?'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide les variables d'environnement requises par chaque module.
|
||||
* Ne lance pas — log un warning pour chaque variable absente.
|
||||
|
||||
Reference in New Issue
Block a user