docs(modules): update module discovery architecture to static manifest approach
- replace dynamic import strategy with static manifest generated by `zen-modules sync` cli - add `zen-modules` binary entry point in `package.json` - add `cli.js` implementing the `zen-modules sync` command - update `discover.server.js` to consume static manifest instead of scanning at runtime - update `index.js` to reflect new module registration flow - update `init.js` to accept pre-resolved modules from manifest - revise docs to document manifest format, sync triggers, and build requirements
This commit is contained in:
+42
-7
@@ -15,13 +15,50 @@ Aucun fichier de configuration manuelle. La plateforme découvre les modules par
|
||||
|
||||
## Découverte
|
||||
|
||||
Au boot et au lancement de `zen-db init`, le core scanne `dependencies` + `devDependencies` du `package.json` du projet consommateur et charge tout package matchant :
|
||||
Les modules sont activés via un **manifeste statique** généré par la CLI `zen-modules sync` dans le projet consommateur, à `app/.zen/modules.generated.js`. Ce fichier contient des `import * as ...` pour chaque package détecté et appelle `register()` au top level :
|
||||
|
||||
```js
|
||||
// app/.zen/modules.generated.js — AUTO-GÉNÉRÉ
|
||||
import * as m0_zen_module_posts from '@zen/module-posts';
|
||||
|
||||
export const modules = [
|
||||
{ name: '@zen/module-posts', exports: m0_zen_module_posts },
|
||||
];
|
||||
|
||||
await Promise.all(modules.map(m => m.exports.register?.()));
|
||||
```
|
||||
|
||||
Le manifeste est importé deux fois :
|
||||
- **Côté serveur** par `instrumentation.js` qui le passe à `initializeZen({ modules })`.
|
||||
- **Côté client** par `app/layout.js` (side-effect import).
|
||||
|
||||
Cette importation statique permet à Turbopack/Webpack d'analyser le graphe complet des modules — JSX, `next/headers`, `next/navigation`, frontières `'use client'` — exactement comme pour le code de l'application elle-même.
|
||||
|
||||
### Critères de détection
|
||||
|
||||
`zen-modules sync` scanne `dependencies` + `devDependencies` du `package.json` du projet et inclut tout package matchant :
|
||||
|
||||
- **Préfixe officiel** : `@zen/module-*`
|
||||
- **Préfixe non-scopé** : `zen-module-*`
|
||||
- **Tiers** : tout package dont le `package.json` contient `"keywords": ["zen-module"]`
|
||||
|
||||
Pour chaque module trouvé, le core vérifie qu'il exporte les bons symboles, puis l'enregistre.
|
||||
### Quand resync ?
|
||||
|
||||
Le template `@zen/start` câble la CLI dans :
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"postinstall": "zen-modules sync",
|
||||
"dev": "zen-modules sync && next dev",
|
||||
"build": "zen-modules sync && next build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`postinstall` couvre `npm install @zen/module-X`. Les hooks `dev`/`build` couvrent les retraits / changements de version qui ne déclenchent pas de re-install. La commande est idempotente — pas d'écriture si le contenu est identique.
|
||||
|
||||
`app/.zen/modules.generated.js` est gitignoré : régénéré localement, jamais commit.
|
||||
|
||||
---
|
||||
|
||||
@@ -253,13 +290,11 @@ Le champ `files` dans `package.json` publie **uniquement** `dist/`, `README.md`
|
||||
|
||||
---
|
||||
|
||||
## Build obligatoire avant publish
|
||||
## Build avant publish
|
||||
|
||||
**Tout module `@zen/module-*` doit être pré-compilé avant publication.** Les fichiers du module ne doivent jamais contenir de JSX brut au runtime — le JSX doit être transformé en `React.createElement` (ou équivalent) par le build du module lui-même.
|
||||
**Tout module `@zen/module-*` est pré-compilé avant publication**, comme `@zen/core` lui-même. Le build `tsup` avec `bundle: false` transforme le JSX, préserve les directives `'use client'` en haut des fichiers compilés, et garde un fichier d'entrée par fichier de sortie pour respecter les frontières RSC quand le projet consommateur bundle le module.
|
||||
|
||||
**Pourquoi.** Le core découvre les modules via `import(/* turbopackIgnore */ name)` dans [`discover.server.js`](../src/core/modules/discover.server.js) ; le commentaire `turbopackIgnore` est nécessaire pour empêcher Turbopack/Webpack de tenter de bundler un nom de package dynamique. Conséquence : tout l'arbre d'imports transitifs du module est résolu et exécuté par Node natif, **sans** transformation JSX. Un fichier `.client.js` qui contient `<MyComponent />` au runtime fait planter `register()` avec `Unexpected token '<'`.
|
||||
|
||||
Le module doit donc utiliser le même setup que `@zen/core` : un build `tsup` avec `bundle: false`, qui transforme JSX → JS standard tout en préservant la structure de fichiers (un fichier d'entrée → un fichier de sortie). Le `'use client'` est conservé en haut des fichiers `.client.js` compilés, ce qui permet à Next.js de respecter les frontières RSC quand le projet consommateur bundle le module.
|
||||
C'est ce qui permet au manifeste statique généré par `zen-modules sync` de simplement faire `import * as ... from '@zen/module-X'` et de laisser Turbopack/Webpack composer le reste : aucune transformation runtime n'est requise côté consommateur.
|
||||
|
||||
### Exemple de `tsup.config.js` minimal
|
||||
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@
|
||||
"release": "npm version patch --no-git-tag-version && npm i && git add package.json package-lock.json && git commit -m \"chore: bump version to $(node -p \"require('./package.json').version\")\" && git push && npm publish"
|
||||
},
|
||||
"bin": {
|
||||
"zen-db": "./dist/core/database/cli.js"
|
||||
"zen-db": "./dist/core/database/cli.js",
|
||||
"zen-modules": "./dist/core/modules/cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.0.0",
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
# Modules
|
||||
|
||||
Registre des modules `@zen/module-*` chargés dans le projet consommateur. Voir [docs/MODULES.md](../../../docs/MODULES.md) pour le guide complet de création d'un module.
|
||||
Registre runtime des modules `@zen/module-*` activés dans le projet consommateur. Voir [docs/MODULES.md](../../../docs/MODULES.md) pour le guide complet de création d'un module.
|
||||
|
||||
## Architecture
|
||||
|
||||
Les modules sont activés via un **manifeste statique** généré dans le projet consommateur (`app/.zen/modules.generated.js`). Le manifeste fait des `import * as ...` statiques pour chaque package et appelle `register()` au top level. Importé par `instrumentation.js` (serveur) et `app/layout.js` (client), il rend l'arbre d'imports du module visible aux deux bundles Next.js — Turbopack et Webpack le bundlent comme n'importe quel autre fichier source.
|
||||
|
||||
Le manifeste est régénéré par `npx zen-modules sync` (typiquement depuis le `postinstall` + les scripts `dev` / `build` du projet).
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
import { registerModule, getRegisteredModules } from '@zen/core/modules';
|
||||
|
||||
// Le core utilise discoverModules() pour peupler ce registre automatiquement.
|
||||
// La plupart des consommateurs n'appellent jamais registerModule() directement.
|
||||
import {
|
||||
registerModules,
|
||||
registerModule,
|
||||
getRegisteredModules,
|
||||
getRegisteredModule,
|
||||
findInstalledModuleNames,
|
||||
validateModuleEnvVars,
|
||||
} from '@zen/core/modules';
|
||||
```
|
||||
|
||||
| Fonction | Usage |
|
||||
|----------|-------|
|
||||
| `registerModules(modules)` | Appelée par `initializeZen({ modules })`. Peuple le registre interne à partir du manifeste. |
|
||||
| `registerModule(mod)` | Bas niveau — utilisé par `registerModules`. |
|
||||
| `getRegisteredModules()` | Retourne tous les modules connus (utilisé par `zen-db init` et l'env validation). |
|
||||
| `findInstalledModuleNames({ cwd })` | Scan readonly du `package.json` du projet — utilisé par le CLI `zen-modules sync`. |
|
||||
| `validateModuleEnvVars(modules)` | Logge un warning par variable d'env requise absente. |
|
||||
|
||||
## Forme attendue d'un module
|
||||
|
||||
Le point d'entrée d'un package `@zen/module-X` doit exporter :
|
||||
@@ -21,3 +39,9 @@ Le point d'entrée d'un package `@zen/module-X` doit exporter :
|
||||
| `register` | `() => void \| Promise<void>` | oui |
|
||||
| `createTables` | `async () => { created?, skipped? }` | si le module a des tables |
|
||||
| `dropTables` | `async () => void` | si le module a des tables |
|
||||
|
||||
Le code du module doit être pré-compilé avant publication (transformation JSX, voir [docs/MODULES.md](../../../docs/MODULES.md)).
|
||||
|
||||
## CLI
|
||||
|
||||
`npx zen-modules sync` — régénère `app/.zen/modules.generated.js`. Idempotent : pas d'écriture si le contenu est inchangé.
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/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 } from './discover.server.js';
|
||||
|
||||
const OUTPUT_PATH = 'app/.zen/modules.generated.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 renderManifest(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',
|
||||
'// de ce manifeste, des deux côtés (server bundle + client bundle).',
|
||||
'await Promise.all(modules.map(m => m.exports.register?.()));',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function readIfExists(path) {
|
||||
try {
|
||||
return await readFile(path, 'utf-8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (prev === next) {
|
||||
step(`zen-modules: ${OUTPUT_PATH} already up to date (${names.length} module${names.length === 1 ? '' : 's'})`);
|
||||
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'})`);
|
||||
for (const name of names) step(` → ${name}`);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,34 +1,62 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { createRequire } from 'node:module';
|
||||
import { info, warn, fail } from '@zen/core/shared/logger';
|
||||
import { registerModule, getRegisteredModule } from './registry.js';
|
||||
import { warn } from '@zen/core/shared/logger';
|
||||
import { registerModule } from './registry.js';
|
||||
|
||||
/**
|
||||
* Découverte automatique des modules `@zen/module-*` installés dans le projet consommateur.
|
||||
* Sources de modules `@zen/module-*` activés dans le projet consommateur.
|
||||
*
|
||||
* Stratégie :
|
||||
* 1. Lire `package.json` du process.cwd() (le projet consommateur, pas @zen/core).
|
||||
* 2. Pour chaque dépendance dont le nom matche `^@zen/module-` ou `^zen-module-`,
|
||||
* résoudre son point d'entrée et l'importer.
|
||||
* 3. Pour les noms qui ne matchent pas le préfixe, fallback : lire
|
||||
* `keywords` du package.json du package — si "zen-module" est présent, charger.
|
||||
* 4. Valider la forme du module (manifest, register, createTables/dropTables) et
|
||||
* l'enregistrer via registerModule().
|
||||
* Le projet consommateur fournit un manifeste statique (généré par
|
||||
* `npx zen-modules sync`) et le passe à `initializeZen({ modules })`. Le
|
||||
* manifeste a la forme :
|
||||
*
|
||||
* Cette fonction ne lance PAS les hooks register() — elle se contente de découvrir
|
||||
* et d'enregistrer les modules dans le registre. Le boot (initializeZen) et le CLI
|
||||
* (zen-db) consomment ensuite getRegisteredModules() selon leurs besoins.
|
||||
* import * as posts from '@zen/module-posts';
|
||||
* export const modules = [{ name: '@zen/module-posts', exports: posts }];
|
||||
* await Promise.all(modules.map(m => m.exports.register?.()));
|
||||
*
|
||||
* Idempotente : appeler plusieurs fois ne charge chaque module qu'une seule fois.
|
||||
* Le `import *` rend l'arbre d'imports du module visible aux deux bundles
|
||||
* Next.js (server + client) ; Turbopack/Webpack le bundlent comme n'importe
|
||||
* quel autre fichier source. C'est ce qui permet au module de référencer du
|
||||
* JSX, `next/headers`, `next/navigation`, etc. — chaque côté reçoit la bonne
|
||||
* condition.
|
||||
*
|
||||
* Cette fonction ne fait QUE peupler le registre interne du core (pour que
|
||||
* `getRegisteredModules()` retourne les bons objets côté serveur). Le top-level
|
||||
* await dans le manifeste a déjà appelé `register()` au moment de son import.
|
||||
*/
|
||||
export function registerModules(modules) {
|
||||
if (!Array.isArray(modules)) return;
|
||||
|
||||
for (const entry of modules) {
|
||||
const ex = entry?.exports;
|
||||
const name = entry?.name ?? ex?.manifest?.name ?? '<unknown>';
|
||||
|
||||
if (!ex?.manifest || typeof ex.register !== 'function') {
|
||||
warn(`zen-modules: "${name}" missing manifest/register — skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
registerModule({
|
||||
manifest: ex.manifest,
|
||||
register: ex.register,
|
||||
createTables: ex.createTables,
|
||||
dropTables: ex.dropTables,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers de scan (utilisés par le CLI `zen-modules sync` et l'env validation).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const NAME_PREFIX = /^(@zen\/module-|zen-module-)/;
|
||||
|
||||
function isCandidate(name) {
|
||||
export function isCandidateName(name) {
|
||||
return NAME_PREFIX.test(name);
|
||||
}
|
||||
|
||||
async function readJson(path) {
|
||||
export async function readJson(path) {
|
||||
try {
|
||||
return JSON.parse(await readFile(path, 'utf-8'));
|
||||
} catch {
|
||||
@@ -36,8 +64,7 @@ async function readJson(path) {
|
||||
}
|
||||
}
|
||||
|
||||
async function isThirdPartyModule(name, projectCwd) {
|
||||
// Fallback pour les modules tiers : on regarde le keywords du package.
|
||||
export async function isThirdPartyModule(name, projectCwd) {
|
||||
const require = createRequire(join(projectCwd, 'package.json'));
|
||||
let pkgJsonPath;
|
||||
try {
|
||||
@@ -49,68 +76,28 @@ async function isThirdPartyModule(name, projectCwd) {
|
||||
return Array.isArray(pkg?.keywords) && pkg.keywords.includes('zen-module');
|
||||
}
|
||||
|
||||
async function loadModule(name) {
|
||||
if (getRegisteredModule(name)) return; // déjà chargé
|
||||
|
||||
let mod;
|
||||
try {
|
||||
// Node résout via node_modules à partir du module appelant ; en pratique
|
||||
// depuis dist/core/modules/ dans @zen/core (lui-même installé chez le
|
||||
// consommateur), Node remonte jusqu'aux node_modules du consommateur.
|
||||
mod = await import(/* turbopackIgnore: true */ /* webpackIgnore: true */ name);
|
||||
} catch (error) {
|
||||
fail(`zen-modules: failed to import "${name}" — ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mod.manifest || typeof mod.register !== 'function') {
|
||||
warn(`zen-modules: "${name}" missing required exports (manifest, register) — skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
registerModule({
|
||||
manifest: mod.manifest,
|
||||
register: mod.register,
|
||||
createTables: mod.createTables,
|
||||
dropTables: mod.dropTables,
|
||||
});
|
||||
info(`zen-modules: discovered ${mod.manifest.name}@${mod.manifest.version ?? '?'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Découvre et enregistre tous les modules installés dans le projet consommateur.
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.cwd] - Répertoire racine du projet consommateur.
|
||||
* @returns {Promise<{ loaded: string[] }>}
|
||||
* Scanne `package.json` du projet consommateur et retourne la liste des noms
|
||||
* de packages `@zen/module-*` (ou compatibles). N'effectue AUCUN import — le
|
||||
* CLI `zen-modules sync` consomme cette liste pour générer le manifeste
|
||||
* statique.
|
||||
*/
|
||||
export async function discoverModules({ cwd = process.cwd() } = {}) {
|
||||
const pkgPath = resolve(cwd, 'package.json');
|
||||
const pkg = await readJson(pkgPath);
|
||||
if (!pkg) {
|
||||
warn(`zen-modules: no package.json at ${pkgPath} — skipping discovery`);
|
||||
return { loaded: [] };
|
||||
}
|
||||
export async function findInstalledModuleNames({ cwd = process.cwd() } = {}) {
|
||||
const pkg = await readJson(resolve(cwd, 'package.json'));
|
||||
if (!pkg) return [];
|
||||
|
||||
const allDeps = {
|
||||
...(pkg.dependencies ?? {}),
|
||||
...(pkg.devDependencies ?? {}),
|
||||
};
|
||||
|
||||
const candidates = [];
|
||||
const out = [];
|
||||
for (const name of Object.keys(allDeps)) {
|
||||
if (isCandidate(name)) {
|
||||
candidates.push(name);
|
||||
} else if (await isThirdPartyModule(name, cwd)) {
|
||||
candidates.push(name);
|
||||
if (isCandidateName(name) || (await isThirdPartyModule(name, cwd))) {
|
||||
out.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const name of candidates) {
|
||||
await loadModule(name);
|
||||
}
|
||||
|
||||
return { loaded: candidates };
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { registerModule, getRegisteredModules, getRegisteredModule, clearRegisteredModules } from './registry.js';
|
||||
export { registerModules, findInstalledModuleNames, validateModuleEnvVars } from './discover.server.js';
|
||||
|
||||
+17
-13
@@ -21,13 +21,22 @@ import { routes as authRoutes } from '../../features/auth/api.js';
|
||||
import { storageAccessPolicies } from '../../features/auth/storage-policies.js';
|
||||
import { PERMISSION_DEFINITIONS } from '../../core/users/constants.js';
|
||||
import { registerPermissions, clearRegisteredPermissions } from '../../core/users/permissions-registry.js';
|
||||
import { discoverModules, validateModuleEnvVars } from '../../core/modules/discover.server.js';
|
||||
import { registerModules, validateModuleEnvVars } from '../../core/modules/discover.server.js';
|
||||
import { getRegisteredModules, clearRegisteredModules } from '../../core/modules/registry.js';
|
||||
import { done, warn, fail } from './logger.js';
|
||||
import { done, warn } from './logger.js';
|
||||
|
||||
const ZEN_INIT_KEY = Symbol.for('__ZEN_INITIALIZED__');
|
||||
|
||||
export async function initializeZen() {
|
||||
/**
|
||||
* @param {object} [options]
|
||||
* @param {Array<{ name: string, exports: object }>} [options.modules]
|
||||
* Manifeste de modules statique généré par `npx zen-modules sync` dans le
|
||||
* projet consommateur. Le top-level await dans le manifeste a déjà déclenché
|
||||
* `exports.register()` au moment de son import — initializeZen() se contente
|
||||
* d'enregistrer chaque module dans le registre interne pour que `getRegisteredModules`
|
||||
* retourne les bons objets côté serveur.
|
||||
*/
|
||||
export async function initializeZen({ modules = [] } = {}) {
|
||||
if (typeof window !== 'undefined') {
|
||||
return { skipped: true, reason: 'client-side' };
|
||||
}
|
||||
@@ -44,19 +53,14 @@ export async function initializeZen() {
|
||||
registerStoragePolicies(storageAccessPolicies);
|
||||
registerPermissions(PERMISSION_DEFINITIONS);
|
||||
|
||||
// Découverte et activation des modules @zen/module-*
|
||||
await discoverModules();
|
||||
const modules = getRegisteredModules();
|
||||
validateModuleEnvVars(modules);
|
||||
for (const mod of modules) {
|
||||
// Activation des modules @zen/module-* via le manifeste statique fourni.
|
||||
registerModules(modules);
|
||||
const registered = getRegisteredModules();
|
||||
validateModuleEnvVars(registered);
|
||||
for (const mod of registered) {
|
||||
if (Array.isArray(mod.manifest?.permissions)) {
|
||||
registerPermissions(mod.manifest.permissions);
|
||||
}
|
||||
try {
|
||||
await mod.register();
|
||||
} catch (error) {
|
||||
fail(`zen-modules: ${mod.manifest.name} register() threw — ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
done('ZEN: ready');
|
||||
|
||||
Reference in New Issue
Block a user