Beiträge zu EmDash
Diese Anleitung behandelt die Einrichtung einer lokalen Entwicklungsumgebung, das Verständnis der Codebasis-Architektur und das Beitragen zu EmDash.
Repository-Struktur
Abschnitt betitelt „Repository-Struktur“EmDash ist ein pnpm-Monorepo mit mehreren Paketen:
emdash/├── packages/│ ├── core/ # emdash — Astro-Integration, APIs und Admin-Oberfläche (Hauptpaket)│ ├── auth/ # @emdash-cms/auth — Authentifizierung (Passkeys, OAuth, Magic Links)│ ├── cloudflare/ # @emdash-cms/cloudflare — Cloudflare-Adapter und Sandbox-Runner│ ├── admin/ # @emdash-cms/admin — React-SPA für die Administration│ ├── create-emdash/ # create-emdash — Projekt-Scaffolder│ ├── gutenberg-to-portable-text/ # Konverter von WordPress-Blöcken zu Portable Text│ └── plugins/ # Plugins der ersten Partei; jedes Unterverzeichnis ist ein eigenes Paket├── demos/│ ├── simple/ # emdash-demo — wichtigste Demo für Entwicklung und Tests (Node.js)│ ├── cloudflare/ # Demo für Cloudflare Workers│ └── ... # plugins-demo, showcase, wordpress-import└── docs/ # Dokumentationsseite (Starlight)Das Hauptpaket ist packages/core. Es enthält:
packages/core/src/├── astro/│ ├── integration/ # Einstiegspunkt der Astro-Integration und Generierung virtueller Module│ ├── middleware/ # Auth, Setup-Prüfung und Anfragekontext (ALS)│ └── routes/│ ├── api/ # Handler für REST-API-Routen│ └── admin-shell.astro # Shell der Admin-SPA├── database/│ ├── migrations/ # nummerierte Migrationsdateien (001_initial.ts, ...)│ │ └── runner.ts # StaticMigrationProvider — hier werden Migrationen registriert│ ├── repositories/ # Datenzugriffsschicht (content, media, settings, ...)│ └── types.ts # Kysely-Datenbanktyp├── plugins/│ ├── types.ts # Typen der Plugin-API│ ├── define-plugin.ts # definePlugin()│ ├── context.ts # Factory für PluginContext│ ├── hooks.ts # HookPipeline│ ├── manager.ts # PluginManager (vertrauenswürdige Plugins)│ └── sandbox/ # Sandbox-Schnittstelle und No-op-Runner├── schema/│ └── registry.ts # SchemaRegistry — verwaltet ec_*-Tabellen├── media/ # Medienanbieter (local, types)├── auth/ # Challenge-Store und OAuth-State-Store├── query.ts # getEmDashCollection, getEmDashEntry├── loader.ts # Implementierung von Astro LiveLoader└── emdash-runtime.ts # EmDashRuntime — zentraler OrchestratorVoraussetzungen
Abschnitt betitelt „Voraussetzungen“- Node.js 22 oder höher
- pnpm 10 oder höher
- Git
# Installieren Sie pnpm, falls es noch nicht vorhanden istnpm install -g pnpmLokale Einrichtung
Abschnitt betitelt „Lokale Einrichtung“-
Repository klonen
Terminal-Fenster git clone <repository-url>cd emdash -
Abhängigkeiten installieren
Terminal-Fenster pnpm install -
Pakete bauen (erforderlich vor dem Starten der Demo)
Terminal-Fenster pnpm build -
Demo-Datenbank befüllen (
demos/simple/)Terminal-Fenster pnpm --filter emdash-demo seed -
Entwicklungsserver starten
Terminal-Fenster pnpm --filter emdash-demo dev -
Admin öffnen
Besuchen Sie http://localhost:4321/_emdash/admin
Im Entwicklungsmodus können Sie den Dev-Bypass-Endpunkt nutzen, um die Passkey-Authentifizierung zu umgehen:
http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin
Entwicklungs-Workflow
Abschnitt betitelt „Entwicklungs-Workflow“Watch-Modus
Abschnitt betitelt „Watch-Modus“Für die Paketentwicklung verwenden Sie den Watch-Modus parallel zur Demo:
# Terminal 1: packages/core auf Änderungen beobachtenpnpm --filter emdash dev
# Terminal 2: Demo ausführen (demos/simple/)pnpm --filter emdash-demo devTests ausführen
Abschnitt betitelt „Tests ausführen“pnpm testpnpm --filter emdash testpnpm --filter emdash test --watchpnpm test:e2eTypüberprüfung und Linting
Abschnitt betitelt „Typüberprüfung und Linting“# TypeScript-Pakete typprüfenpnpm typecheck
# Astro-Demos typüberprüfenpnpm typecheck:demos
# Schnelles Linting (< 1s) — nach jeder Bearbeitung ausführenpnpm lint:quick
# Vollständiges Linting mit typbewussten Regeln (~10s) — vor Commits ausführenpnpm lint:jsonFormatierung
Abschnitt betitelt „Formatierung“pnpm formatEmDash verwendet oxfmt (Oxc-Formatter). Die Konfiguration befindet sich in .oxfmtrc.json. Tabulatoren, keine Leerzeichen.
Architektur-Überblick
Abschnitt betitelt „Architektur-Überblick“Kernkonzepte
Abschnitt betitelt „Kernkonzepte“D1 ist die Quelle der Wahrheit. Das Schema lebt in zwei Systemtabellen:
_emdash_collections— Metadaten der Sammlungen_emdash_fields— Felddefinitionen
Wenn Sie eine Sammlung erstellen, führt EmDash ALTER TABLE aus, um eine echte ec_*-Tabelle mit typisierten Spalten zu erstellen. Es wird kein EAV-Ansatz (Entity-Attribute-Value) verwendet.
Middleware-Kette (in dieser Reihenfolge für jede Anfrage):
- Runtime-Initialisierung — erstellt Datenbankverbindung, initialisiert
EmDashRuntime - Setup-Prüfung — leitet zum Setup-Assistenten weiter, falls nicht konfiguriert
- Authentifizierung — validiert die Sitzung, füllt
locals.user - Anfragekontext — richtet AsyncLocalStorage für Vorschau-/Bearbeitungsmodus ein
Handler-Schicht: Die Geschäftslogik befindet sich in api/handlers/*.ts. Routendateien sind dünne Wrapper, die Eingaben parsen, Handler aufrufen und Antworten formatieren. Handler geben ApiResponse<T> = { success: boolean; data?: T; error?: { code, message } } zurück.
Wichtige Dateien
Abschnitt betitelt „Wichtige Dateien“| Datei | Zweck |
|---|---|
src/astro/integration/index.ts | Astro-Integration-Einstiegspunkt; generiert virtuelle Module |
src/emdash-runtime.ts | Zentrale Runtime; orchestriert DB, Plugins, Storage |
src/schema/registry.ts | Verwaltet die Erstellung/Modifikation von ec_*-Tabellen |
src/database/migrations/runner.ts | StaticMigrationProvider; neue Migrationen hier registrieren |
src/plugins/manager.ts | Lädt und orchestriert vertrauenswürdige Plugins |
Datenbank-Muster
Abschnitt betitelt „Datenbank-Muster“EmDash verwendet Kysely für alle Abfragen. Wichtige Regeln:
// KORREKT: parametrisierte Werteconst post = await db .selectFrom("ec_posts") .selectAll() .where("slug", "=", slug) // parametrisiert .executeTakeFirst();
// KORREKT: validierter Bezeichner in Raw-SQLvalidateIdentifier(tableName);const result = await sql.raw(`SELECT * FROM ${tableName}`).execute(db);
// FALSCH: niemals unvalidierte Werte in SQL interpolierenconst result = await sql.raw(`SELECT * FROM ${userInput}`).execute(db);Verwenden Sie niemals sql.raw() mit String-Interpolation für Werte. Verwenden Sie sql.ref() für Bezeichner und die Kysely-Fluent-API für alles andere.
Eine Migration hinzufügen
Abschnitt betitelt „Eine Migration hinzufügen“-
Erstellen Sie
packages/core/src/database/migrations/NNN_beschreibung.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();} -
Registrieren Sie sie in
packages/core/src/database/migrations/runner.ts:import * as m018 from "./018_my_migration.js";// Zum Rückgabewert von getMigrations() hinzufügen:"018_meine_migration": m018,
Eine API-Route hinzufügen
Abschnitt betitelt „Eine API-Route hinzufügen“Routendateien befinden sich in packages/core/src/astro/routes/api/. Befolgen Sie diese Konventionen:
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 ist nicht initialisiert", 500);
// requirePerm gibt eine 403 Response zurück, wenn verweigert, oder null, wenn autorisiert const denied = requirePerm(user, "content:edit_any"); if (denied) return denied;
const body = await parseBody(request, createInput); if (body instanceof Response) return body;
try { // Geschäftslogik hier return Response.json({ success: true }); } catch (error) { return handleError(error, "Ressource konnte nicht erstellt werden", "CREATE_ERROR"); }};Registrieren Sie die Route dann in packages/core/src/astro/integration/routes.ts.
Plugin-Entwicklung
Abschnitt betitelt „Plugin-Entwicklung“Plugins werden mit definePlugin() definiert und in der Astro-Konfiguration registriert. Siehe die Plugin-System-Dokumentation für die vollständige API.
Für die lokale Plugin-Entwicklung:
# Lokales Plugin in packages/ anlegenpnpm --filter emdash dev # Watch-ModusVerknüpfen Sie Ihr Plugin in der astro.config.mjs der Demo:
import myPlugin from "../../../packages/my-plugin/src/index.ts";
emdash({ plugins: [myPlugin()],});Test-Muster
Abschnitt betitelt „Test-Muster“Tests befinden sich in packages/core/tests/. Die Struktur spiegelt die Quelle wider:
tests/├── unit/ # Tests für reine Funktionen├── integration/ # Tests mit echter Datenbank (SQLite im Speicher)└── e2e/ # Browser-Tests mit PlaywrightDatenbanktests verwenden echte SQLite, keine 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("erstellt einen Content-Eintrag", async () => { // mit echter Datenbank testen });});E2E-Tests verwenden Playwright mit dem Dev-Bypass für die Authentifizierung:
await page.goto( "http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin");Code-Konventionen
Abschnitt betitelt „Code-Konventionen“Imports
Abschnitt betitelt „Imports“Verwenden Sie immer .js-Erweiterungen für interne Imports (ESM-Anforderung):
// Korrektimport { ContentRepository } from "../../../database/repositories/content.js";
// Falschimport { ContentRepository } from "../../../database/repositories/content";Verwenden Sie import type für rein typbasierte Imports:
import type { Kysely } from "kysely";import type { User } from "@emdash-cms/auth";Fehlerbehandlung
Abschnitt betitelt „Fehlerbehandlung“Verwenden Sie die gemeinsamen Fehler-Hilfsfunktionen in API-Routen:
// Fehlerantwortenreturn apiError("NOT_FOUND", "Inhalt nicht gefunden", 404);
// Catch-Blöckecatch (error) { return handleError(error, "Inhalt konnte nicht aktualisiert werden", "CONTENT_UPDATE_ERROR");}Autorisierung
Abschnitt betitelt „Autorisierung“Jede zustandsändernde Route muss die Autorisierung prüfen. Verwenden Sie requirePerm() aus #api/authorize.js — es gibt eine Response (403) zurück, wenn verweigert, oder null, wenn autorisiert:
import { requirePerm } from "#api/authorize.js";
const denied = requirePerm(user, "content:edit_any");if (denied) return denied;Für eigentümerbezogene Aktionen verwenden Sie requireOwnerPerm():
import { requireOwnerPerm } from "#api/authorize.js";
const denied = requireOwnerPerm(user, item.authorId, "content:edit_own", "content:edit_any");if (denied) return denied;Commit- und PR-Prozess
Abschnitt betitelt „Commit- und PR-Prozess“- Erstellen Sie einen Feature-Branch von
main - Nehmen Sie Änderungen vor und stellen Sie sicher, dass
pnpm typecheckundpnpm lint:jsonerfolgreich sind - Führen Sie relevante Tests aus
- Committen Sie mit einer beschreibenden Nachricht
- Öffnen Sie einen PR, der auf
mainabzielt
Commit-Nachrichten sollten das Warum beschreiben, nicht nur das Was:
# Gutfix: prevent media MIME sniffing with X-Content-Type-Options header
# Weniger gutfix: add header to media endpointHilfe erhalten
Abschnitt betitelt „Hilfe erhalten“- Lesen Sie
AGENTS.mdfür Architekturentscheidungen und Codemuster - Besuchen Sie die Dokumentationsseite für Anleitungen und API-Referenz