diff --git a/docs/DEV.md b/docs/DEV.md index 433d301..7220cdf 100644 --- a/docs/DEV.md +++ b/docs/DEV.md @@ -58,6 +58,8 @@ Tout fichier épinglé à une frontière Next.js porte le suffixe dans son nom : Ces suffixes ne sont pas cosmétiques : **le build les utilise comme source de vérité**. Le build compile l'intégralité de `src/` avec `bundle: false` — chaque fichier reste un module séparé, ce qui permet à Next.js de respecter les frontières RSC et `'use client'` sans que le bundler ne fusionne les modules. +**Tout fichier qui `import 'pg'`, `'fs'`, `'net'`, `'node:*'` doit porter `.server.js`.** Sans ce suffixe, le fichier est réputé neutre — un barrel client peut le ré-exporter par mégarde, et Turbopack/Webpack tracent la chaîne d'imports statiques jusqu'à `pg` dans le bundle browser. L'erreur `Module not found: Can't resolve 'dns'` côté client est typiquement causée par un fichier serveur sans suffixe atteint via un barrel mixte. + --- ## Build et configuration tsup diff --git a/docs/MODULES.md b/docs/MODULES.md index b2f2ebc..d2b59d3 100644 --- a/docs/MODULES.md +++ b/docs/MODULES.md @@ -284,6 +284,24 @@ import BlogAdminPage from './admin/BlogAdminPage.client.js'; registerPage({ slug: 'blog', Component: BlogAdminPage, title: 'Blog' }); ``` +### Sous-entrées `@zen/core/*` safe-pour-client + +Le `client.js` d'un module ne doit jamais importer un barrel mixte (un barrel qui ré-exporte du code serveur à côté de constantes client-safe). Si on le fait, Turbopack/Webpack tracent toute la chaîne d'imports statiques et ramènent `pg`/`fs`/`next/headers` dans le bundle browser — qui crashe avec `Module not found: Can't resolve 'dns'` ou équivalent. + +Sous-entrées explicitement safe pour un import depuis un fichier `'use client'` : + +| Sous-entrée | Contenu | +|-------------|---------| +| `@zen/core/users/constants` | `PERMISSIONS`, `PERMISSION_DEFINITIONS`, `getPermissionGroups` — aucun import serveur. | +| `@zen/core/features/admin` | `registerPage`, `registerWidget`, `registerNavItem`, `registerNavSection`, `buildNavigationSections`. Neutre côté boundary. | +| `@zen/core/features/admin/components` | Composants client. | +| `@zen/core/themes` | Tokens/utilitaires de thème. | +| `@zen/core/toast` | API toast côté client. | +| `@zen/core/shared/icons` | Composants d'icônes. | +| `@zen/core/shared/components` | Composants partagés. | + +Tout ce qui n'est pas dans cette liste — en particulier `@zen/core/users` (barrel complet), `@zen/core/database`, `@zen/core/api`, `@zen/core/storage` — est du code serveur. Ne JAMAIS l'importer depuis un fichier `'use client'` ou un fichier transitivement importé par un `'use client'`. + --- ## Variables d'environnement diff --git a/package.json b/package.json index 8806de9..abbd139 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zen/core", - "version": "1.4.131", + "version": "1.4.132", "description": "Une plateforme multi-usage construite sur l'essentiel, rien de plus, rien de moins.", "repository": { "type": "git", diff --git a/src/core/database/crud.js b/src/core/database/crud.js index 9d99adb..89f78ce 100644 --- a/src/core/database/crud.js +++ b/src/core/database/crud.js @@ -3,7 +3,7 @@ * Provides convenient methods for Create, Read, Update, Delete operations */ -import { query, queryOne, queryAll } from './db.js'; +import { query, queryOne, queryAll } from './db.server.js'; /** * Filter a data object to only the columns present in allowedColumns. diff --git a/src/core/database/index.js b/src/core/database/index.js index 7a0e0fe..ac67260 100644 --- a/src/core/database/index.js +++ b/src/core/database/index.js @@ -14,7 +14,7 @@ export { closePool, testConnection, tableExists -} from './db.js'; +} from './db.server.js'; // CRUD helper functions export { diff --git a/src/core/modules/discover.server.js b/src/core/modules/discover.server.js index e102f14..5ed89a9 100644 --- a/src/core/modules/discover.server.js +++ b/src/core/modules/discover.server.js @@ -1,7 +1,7 @@ import { readFile } from 'node:fs/promises'; import { readFileSync } from 'node:fs'; import { resolve, join, dirname } from 'node:path'; -import { createRequire } from 'node:module'; +import { pathToFileURL } from 'node:url'; import { info, warn } from '@zen/core/shared/logger'; import { registerModule } from './registry.js'; @@ -132,6 +132,20 @@ export async function findInstalledModuleNames({ cwd = process.cwd() } = {}) { return out.sort(); } +/** + * Résout l'URL ESM du main entry d'un module à partir de son `package.json`. + * On lit `exports['.'].import` puis `main`, sinon `./index.js` par défaut. + * Pas de `require.resolve` : un module `@zen/module-*` peut déclarer uniquement + * la condition `"import"` (ESM-only) — la résolution CJS échouerait alors avec + * "No exports main defined". Comme on a déjà localisé le `package.json` via + * `resolveModulePackageJson`, on construit l'URL nous-mêmes. + */ +function resolveModuleEntryUrl(found) { + const pkgDir = dirname(found.path); + const main = found.pkg?.exports?.['.']?.import ?? found.pkg?.main ?? './index.js'; + return pathToFileURL(resolve(pkgDir, main)).href; +} + /** * Variante "Node-only" du chargement de modules — utilisée par le CLI * `zen-db init` qui ne passe jamais par un bundler. Charge dynamiquement @@ -145,20 +159,17 @@ export async function findInstalledModuleNames({ cwd = process.cwd() } = {}) { */ 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}`); + const found = resolveModulePackageJson(name, cwd); + if (!found) { + warn(`zen-modules: cannot find package "${name}" in node_modules`); continue; } let mod; try { - mod = await import(entryPath); + mod = await import(resolveModuleEntryUrl(found)); } catch (err) { warn(`zen-modules: failed to import "${name}" — ${err.message}`); continue; diff --git a/src/features/admin/navigation.js b/src/features/admin/navigation.js index 784b20e..2c75e11 100644 --- a/src/features/admin/navigation.js +++ b/src/features/admin/navigation.js @@ -5,7 +5,7 @@ import { getNavItems, } from './registry.js'; import { isDevkitEnabled } from '../../shared/lib/appConfig.js'; -import { PERMISSIONS } from '@zen/core/users'; +import { PERMISSIONS } from '@zen/core/users/constants'; // Sections et items core — enregistrés à l'import de ce module. registerNavSection({ id: 'dashboard', title: 'Tableau de bord', icon: 'DashboardSquare03Icon', order: 10 }); diff --git a/src/features/admin/registry.js b/src/features/admin/registry.js index 25e306e..d9024bb 100644 --- a/src/features/admin/registry.js +++ b/src/features/admin/registry.js @@ -7,17 +7,26 @@ * - navItem : une entrée de la sidebar admin (section optionnelle pour grouper). * - page : un composant rendu sous /admin/. * - * Les instances de module sont séparées entre le bundle serveur et le bundle - * client de Next.js ; c'est attendu : les fetchers vivent côté serveur, les - * Composants côté client. Les navItems et les pages sont enregistrés côté - * neutre et visibles des deux côtés. + * Les Maps sont stockées sur `globalThis` via `Symbol.for` pour survivre : + * 1. au hot-reload de Next.js dev (sinon les enregistrements disparaissent). + * 2. à la double-instanciation du fichier — l'instrumentation hook tourne en + * Node natif (require ESM), tandis que les Server Components passent par + * le bundle Turbopack/Webpack. Sans `globalThis`, les nav items poussés + * par `register-server.js` au boot ne seraient pas visibles côté Server + * Component qui rend la sidebar — la sidebar resterait vide. */ -const widgetFetchers = new Map(); // id -> async () => data -const widgetComponents = new Map(); // id -> { Component, order } -const navItems = new Map(); // id -> { id, label, icon, href, order, sectionId } -const navSections = new Map(); // id -> { id, title, icon, order } -const pages = new Map(); // slug -> { slug, Component, title? } +const REGISTRY_KEY = Symbol.for('__ZEN_ADMIN_REGISTRY__'); +if (!globalThis[REGISTRY_KEY]) { + globalThis[REGISTRY_KEY] = { + widgetFetchers: new Map(), // id -> async () => data + widgetComponents: new Map(), // id -> { Component, order, permission } + navItems: new Map(), // id -> { id, label, icon, href, order, sectionId, position, permission } + navSections: new Map(), // id -> { id, title, icon, order } + pages: new Map(), // slug -> { slug, Component, title?, breadcrumbLabel? } + }; +} +const { widgetFetchers, widgetComponents, navItems, navSections, pages } = globalThis[REGISTRY_KEY]; // ---- Widgets ---------------------------------------------------------------