docs(core): update server boundary rules and fix db import paths
- document `.server.js` suffix requirement for node-only imports in DEV.md - add client-safe subentries table and server-only barrel warnings in MODULES.md - fix `crud.js` and `database/index.js` to import from `db.server.js` - replace `createRequire` with `pathToFileURL` in `discover.server.js` for ESM-only modules - update admin navigation and registry to use safe client-compatible imports - bump version to 1.4.132
This commit is contained in:
@@ -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.
|
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
|
## Build et configuration tsup
|
||||||
|
|||||||
@@ -284,6 +284,24 @@ import BlogAdminPage from './admin/BlogAdminPage.client.js';
|
|||||||
registerPage({ slug: 'blog', Component: BlogAdminPage, title: 'Blog' });
|
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
|
## Variables d'environnement
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@zen/core",
|
"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.",
|
"description": "Une plateforme multi-usage construite sur l'essentiel, rien de plus, rien de moins.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Provides convenient methods for Create, Read, Update, Delete operations
|
* 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.
|
* Filter a data object to only the columns present in allowedColumns.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export {
|
|||||||
closePool,
|
closePool,
|
||||||
testConnection,
|
testConnection,
|
||||||
tableExists
|
tableExists
|
||||||
} from './db.js';
|
} from './db.server.js';
|
||||||
|
|
||||||
// CRUD helper functions
|
// CRUD helper functions
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { resolve, join, dirname } from 'node:path';
|
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 { info, warn } from '@zen/core/shared/logger';
|
||||||
import { registerModule } from './registry.js';
|
import { registerModule } from './registry.js';
|
||||||
|
|
||||||
@@ -132,6 +132,20 @@ export async function findInstalledModuleNames({ cwd = process.cwd() } = {}) {
|
|||||||
return out.sort();
|
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
|
* Variante "Node-only" du chargement de modules — utilisée par le CLI
|
||||||
* `zen-db init` qui ne passe jamais par un bundler. Charge dynamiquement
|
* `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() } = {}) {
|
export async function loadModulesForCli({ cwd = process.cwd() } = {}) {
|
||||||
const names = await findInstalledModuleNames({ cwd });
|
const names = await findInstalledModuleNames({ cwd });
|
||||||
const require = createRequire(join(cwd, 'package.json'));
|
|
||||||
|
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
let entryPath;
|
const found = resolveModulePackageJson(name, cwd);
|
||||||
try {
|
if (!found) {
|
||||||
entryPath = require.resolve(name);
|
warn(`zen-modules: cannot find package "${name}" in node_modules`);
|
||||||
} catch (err) {
|
|
||||||
warn(`zen-modules: cannot resolve "${name}" — ${err.message}`);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mod;
|
let mod;
|
||||||
try {
|
try {
|
||||||
mod = await import(entryPath);
|
mod = await import(resolveModuleEntryUrl(found));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
warn(`zen-modules: failed to import "${name}" — ${err.message}`);
|
warn(`zen-modules: failed to import "${name}" — ${err.message}`);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
getNavItems,
|
getNavItems,
|
||||||
} from './registry.js';
|
} from './registry.js';
|
||||||
import { isDevkitEnabled } from '../../shared/lib/appConfig.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.
|
// Sections et items core — enregistrés à l'import de ce module.
|
||||||
registerNavSection({ id: 'dashboard', title: 'Tableau de bord', icon: 'DashboardSquare03Icon', order: 10 });
|
registerNavSection({ id: 'dashboard', title: 'Tableau de bord', icon: 'DashboardSquare03Icon', order: 10 });
|
||||||
|
|||||||
@@ -7,17 +7,26 @@
|
|||||||
* - navItem : une entrée de la sidebar admin (section optionnelle pour grouper).
|
* - navItem : une entrée de la sidebar admin (section optionnelle pour grouper).
|
||||||
* - page : un composant rendu sous /admin/<slug>.
|
* - page : un composant rendu sous /admin/<slug>.
|
||||||
*
|
*
|
||||||
* Les instances de module sont séparées entre le bundle serveur et le bundle
|
* Les Maps sont stockées sur `globalThis` via `Symbol.for` pour survivre :
|
||||||
* client de Next.js ; c'est attendu : les fetchers vivent côté serveur, les
|
* 1. au hot-reload de Next.js dev (sinon les enregistrements disparaissent).
|
||||||
* Composants côté client. Les navItems et les pages sont enregistrés côté
|
* 2. à la double-instanciation du fichier — l'instrumentation hook tourne en
|
||||||
* neutre et visibles des deux côtés.
|
* 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 REGISTRY_KEY = Symbol.for('__ZEN_ADMIN_REGISTRY__');
|
||||||
const widgetComponents = new Map(); // id -> { Component, order }
|
if (!globalThis[REGISTRY_KEY]) {
|
||||||
const navItems = new Map(); // id -> { id, label, icon, href, order, sectionId }
|
globalThis[REGISTRY_KEY] = {
|
||||||
const navSections = new Map(); // id -> { id, title, icon, order }
|
widgetFetchers: new Map(), // id -> async () => data
|
||||||
const pages = new Map(); // slug -> { slug, Component, title? }
|
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 ---------------------------------------------------------------
|
// ---- Widgets ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user