Contribuer à EmDash
Ce guide explique comment configurer un environnement de développement local, comprendre l’architecture du codebase et contribuer à EmDash.
Structure du dépôt
Section intitulée « Structure du dépôt »EmDash est un monorepo pnpm avec plusieurs packages :
emdash/├── packages/│ ├── core/ # emdash — Astro integration, APIs, admin (main package)│ ├── auth/ # @emdash-cms/auth — Authentication (passkeys, OAuth, magic links)│ ├── cloudflare/ # @emdash-cms/cloudflare — Cloudflare adapter + sandbox runner│ ├── admin/ # @emdash-cms/admin — Admin React SPA│ ├── create-emdash/ # create-emdash — project scaffolder│ ├── gutenberg-to-portable-text/ # WordPress block → Portable Text converter│ └── plugins/ # First-party plugins (each subdirectory is its own package)├── demos/│ ├── simple/ # emdash-demo — primary dev/test demo (Node.js)│ ├── cloudflare/ # Cloudflare Workers demo│ └── ... # plugins-demo, showcase, wordpress-import└── docs/ # Documentation site (Starlight)Le package principal est packages/core. Il contient :
packages/core/src/├── astro/│ ├── integration/ # Astro integration entry point + virtual module generation│ ├── middleware/ # Auth, setup check, request context (ALS)│ └── routes/│ ├── api/ # REST API route handlers│ └── admin-shell.astro # Admin SPA shell├── database/│ ├── migrations/ # Numbered migration files (001_initial.ts, ...)│ │ └── runner.ts # StaticMigrationProvider — register migrations here│ ├── repositories/ # Data access layer (content, media, settings, ...)│ └── types.ts # Kysely Database type├── plugins/│ ├── types.ts # Plugin API types│ ├── define-plugin.ts # definePlugin()│ ├── context.ts # PluginContext factory│ ├── hooks.ts # HookPipeline│ ├── manager.ts # PluginManager (trusted plugins)│ └── sandbox/ # Sandbox interface + no-op runner├── schema/│ └── registry.ts # SchemaRegistry — manages ec_* tables├── media/ # Media providers (local, types)├── auth/ # Challenge store, OAuth state store├── query.ts # getEmDashCollection, getEmDashEntry├── loader.ts # Astro LiveLoader implementation└── emdash-runtime.ts # EmDashRuntime — central orchestratorPrérequis
Section intitulée « Prérequis »- Node.js 22 ou supérieur
- pnpm 10 ou supérieur
- Git
# Install pnpm if you don't have itnpm install -g pnpmConfiguration locale
Section intitulée « Configuration locale »-
Cloner le dépôt
Fenêtre de terminal git clone <repository-url>cd emdash -
Installer les dépendances
Fenêtre de terminal pnpm install -
Compiler les packages (requis avant d’exécuter la démo)
Fenêtre de terminal pnpm build -
Peupler la base de données de démonstration (
demos/simple/)Fenêtre de terminal pnpm --filter emdash-demo seed -
Démarrer le serveur de développement
Fenêtre de terminal pnpm --filter emdash-demo dev -
Ouvrir l’administration
Visitez http://localhost:4321/_emdash/admin
En mode développement, utilisez le point de terminaison de contournement pour ignorer l’authentification par clé d’accès :
http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin
Flux de travail de développement
Section intitulée « Flux de travail de développement »Mode Watch
Section intitulée « Mode Watch »Pour le développement de package, utilisez le mode watch parallèlement à la démo :
# Terminal 1: Watch packages/core for changespnpm --filter emdash dev
# Terminal 2 : Exécuter la démo (demos/simple/)pnpm --filter emdash-demo devExécution des tests
Section intitulée « Exécution des tests »pnpm testpnpm --filter emdash testpnpm --filter emdash test --watchpnpm test:e2eVérification de type et linting
Section intitulée « Vérification de type et linting »# Type check TypeScript packagespnpm typecheck
# Vérification de type pour les démos Astropnpm typecheck:demos
# Lint rapide (< 1s) — à exécuter après chaque modificationpnpm lint:quick
# Lint complet avec règles de type (~10s) — à exécuter avant les commitspnpm lint:jsonFormatage
Section intitulée « Formatage »pnpm formatEmDash utilise oxfmt (formateur Oxc). La configuration est dans .oxfmtrc.json. Des tabulations, pas des espaces.
Vue d’ensemble de l’architecture
Section intitulée « Vue d’ensemble de l’architecture »Concepts clés
Section intitulée « Concepts clés »D1 est la source de vérité. Le schéma réside dans deux tables système :
_emdash_collections— métadonnées des collections_emdash_fields— définitions des champs
Lorsque vous créez une collection, EmDash exécute ALTER TABLE pour créer une vraie table ec_* avec des colonnes typées. Il n’y a pas d’approche EAV (Entity-Attribute-Value).
Chaîne de middleware (dans l’ordre pour chaque requête) :
- Initialisation du runtime — crée la connexion à la base de données, initialise
EmDashRuntime - Vérification de la configuration — redirige vers l’assistant de configuration si non configuré
- Authentification — valide la session, peuple
locals.user - Contexte de la requête — configure AsyncLocalStorage pour le mode prévisualisation/édition
Couche Handler : la logique métier se trouve dans api/handlers/*.ts. Les fichiers de route sont des enveloppes minces qui analysent l’entrée, appellent les handlers et formatent les réponses. Les handlers retournent ApiResponse<T> = { success: boolean; data?: T; error?: { code, message } }.
Fichiers clés
Section intitulée « Fichiers clés »| Fichier | Objectif |
|---|---|
src/astro/integration/index.ts | Point d’entrée de l’intégration Astro ; génère des modules virtuels |
src/emdash-runtime.ts | Runtime central ; orchestre la base de données, les plugins, le stockage |
src/schema/registry.ts | Gère la création/modification des tables ec_* |
src/database/migrations/runner.ts | StaticMigrationProvider ; enregistrez les nouvelles migrations ici |
src/plugins/manager.ts | Charge et orchestre les plugins de confiance |
Modèles de base de données
Section intitulée « Modèles de base de données »EmDash utilise Kysely pour toutes les requêtes. Règles clés :
// CORRECT: parameterized valuesconst post = await db .selectFrom("ec_posts") .selectAll() .where("slug", "=", slug) // parameterized .executeTakeFirst();
// CORRECT : identifiant validé en SQL brutvalidateIdentifier(tableName);const result = await sql.raw(`SELECT * FROM ${tableName}`).execute(db);
// FAUX : n'interpolez jamais de valeurs non validées dans le SQLconst result = await sql.raw(`SELECT * FROM ${userInput}`).execute(db);N’utilisez jamais sql.raw() avec une interpolation de chaîne pour les valeurs. Utilisez sql.ref() pour les identifiants, et l’API fluide de Kysely pour tout le reste.
Ajout d’une migration
Section intitulée « Ajout d’une migration »-
Créez
packages/core/src/database/migrations/NNN_description.ts:import type { Kysely } from "kysely";export async function up(db: Kysely<unknown>): Promise<void> {await db.schema.createTable("my_table").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull()).execute();}export async function down(db: Kysely<unknown>): Promise<void> {await db.schema.dropTable("my_table").execute();} -
Enregistrez-la dans
packages/core/src/database/migrations/runner.ts:import * as m018 from "./018_my_migration.js";// Ajoutez à la valeur de retour de getMigrations() :"018_my_migration": m018,
Ajout d’une route API
Section intitulée « Ajout d’une route API »Les fichiers de route se trouvent dans packages/core/src/astro/routes/api/. Suivez ces conventions :
import type { APIRoute } from "astro";import type { User } from "@emdash-cms/auth";import { apiError, handleError } from "#api/error.js";import { requirePerm } from "#api/authorize.js";import { parseBody } from "#api/parse.js";import { z } from "zod";
export const prerender = false;
const createInput = z.object({ name: z.string().min(1),});
export const POST: APIRoute = async ({ request, locals }) => { const { emdash } = locals; const user = (locals as { user?: User }).user;
if (!emdash) return apiError("NOT_CONFIGURED", "EmDash n'est pas initialisé", 500);
// requirePerm retourne une Response (403) si refusé, ou null si autorisé const denied = requirePerm(user, "content:edit_any"); if (denied) return denied;
const body = await parseBody(request, createInput); if (body instanceof Response) return body;
try { // logique métier ici return Response.json({ success: true }); } catch (error) { return handleError(error, "Failed to create resource", "CREATE_ERROR"); }};Ensuite, enregistrez la route dans packages/core/src/astro/integration/routes.ts.
Développement de plugin
Section intitulée « Développement de plugin »Les plugins sont définis avec definePlugin() et enregistrés dans la configuration Astro. Consultez la documentation du système de plugins pour l’API complète.
Pour le développement local de plugin :
# Create a local plugin in packages/pnpm --filter emdash dev # Watch modeLie votre plugin dans le astro.config.mjs de la démo :
import myPlugin from "../../../packages/my-plugin/src/index.ts";
emdash({ plugins: [myPlugin()],});Modèles de test
Section intitulée « Modèles de test »Les tests se trouvent dans packages/core/tests/. La structure reflète la source :
tests/├── unit/ # Pure function tests├── integration/ # Real DB tests (in-memory SQLite)└── e2e/ # Playwright browser testsLes tests de base de données utilisent un vrai SQLite, pas des mocks :
import { describe, it, beforeEach, afterEach } from "vitest";import { setupTestDatabase } from "../../utils/test-db.js";import type { Kysely } from "kysely";import type { Database } from "../../../src/database/types.js";
describe("ContentRepository", () => { let db: Kysely<Database>;
beforeEach(async () => { db = await setupTestDatabase(); });
afterEach(async () => { await db.destroy(); });
it("crée une entrée de contenu", async () => { // test with real DB });});Les tests E2E utilisent Playwright avec le contournement de développement pour l’authentification :
await page.goto( "http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin");Conventions de code
Section intitulée « Conventions de code »Utilisez toujours les extensions .js pour les imports internes (exigence ESM) :
// Correctimport { ContentRepository } from "../../../database/repositories/content.js";
// Wrongimport { ContentRepository } from "../../../database/repositories/content";Utilisez import type pour les imports de type uniquement :
import type { Kysely } from "kysely";import type { User } from "@emdash-cms/auth";Gestion des erreurs
Section intitulée « Gestion des erreurs »Utilisez les utilitaires d’erreur partagés dans les routes API :
// Error responsesreturn apiError("NOT_FOUND", "Content not found", 404);
// Blocs catchcatch (error) { return handleError(error, "Failed to update content", "CONTENT_UPDATE_ERROR");}Autorisation
Section intitulée « Autorisation »Chaque route modifiant l’état doit vérifier l’autorisation. Utilisez requirePerm() de #api/authorize.js — elle retourne une Response (403) si refusé, ou null si autorisé :
import { requirePerm } from "#api/authorize.js";
const denied = requirePerm(user, "content:edit_any");if (denied) return denied;Pour les actions limitées à la propriété, utilisez requireOwnerPerm() :
import { requireOwnerPerm } from "#api/authorize.js";
const denied = requireOwnerPerm(user, item.authorId, "content:edit_own", "content:edit_any");if (denied) return denied;Processus de commit et de PR
Section intitulée « Processus de commit et de PR »- Créez une branche de fonctionnalité à partir de
main - Effectuez les modifications, assurez-vous que
pnpm typechecketpnpm lint:jsonpassent - Exécutez les tests pertinents
- Commitez avec un message descriptif
- Ouvrez une PR ciblant
main
Les messages de commit doivent décrire le pourquoi, pas seulement le quoi :
# Goodfix: prevent media MIME sniffing with X-Content-Type-Options header
# Moins bonfix: add header to media endpointObtenir de l’aide
Section intitulée « Obtenir de l’aide »- Lisez
AGENTS.mdpour les décisions d’architecture et les modèles de code - Consultez le site de documentation pour les guides et la référence API