EmDashへの貢献
このガイドでは、ローカル開発環境のセットアップ方法、コードベースのアーキテクチャの理解、およびEmDashへの貢献方法について説明します。
リポジトリ構造
Section titled “リポジトリ構造”EmDashは、複数のパッケージを含む pnpmモノレポ です:
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)メインパッケージは packages/core です。これには以下が含まれます:
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 orchestrator- Node.js 22以上
- pnpm 10以上
- Git
# Install pnpm if you don't have itnpm install -g pnpmローカルセットアップ
Section titled “ローカルセットアップ”-
リポジトリをクローンする
Terminal window git clone <repository-url>cd emdash -
依存関係をインストールする
Terminal window pnpm install -
パッケージをビルドする (デモを実行する前に必須)
Terminal window pnpm build -
デモデータベースをシードする (
demos/simple/)Terminal window pnpm --filter emdash-demo seed -
開発サーバーを起動する
Terminal window pnpm --filter emdash-demo dev -
管理パネルを開く
http://localhost:4321/_emdash/admin にアクセスしてください。
開発モードでは、パスキー認証をスキップするための開発用バイパスエンドポイントを使用します:
http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin
開発ワークフロー
Section titled “開発ワークフロー”ウォッチモード
Section titled “ウォッチモード”パッケージ開発では、デモと並行してウォッチモードを使用します:
# Terminal 1: Watch packages/core for changespnpm --filter emdash dev
# ターミナル2: デモを実行する (demos/simple/)pnpm --filter emdash-demo devテストの実行
Section titled “テストの実行”pnpm testpnpm --filter emdash testpnpm --filter emdash test --watchpnpm test:e2e型チェックとリンター
Section titled “型チェックとリンター”# Type check TypeScript packagespnpm typecheck
# Astroデモの型チェックpnpm typecheck:demos
# 高速リンター (< 1秒) — 編集後に毎回実行pnpm lint:quick
# 型を考慮した完全なリンター (~10秒) — コミット前に実行pnpm lint:jsonフォーマッティング
Section titled “フォーマッティング”pnpm formatEmDashは oxfmt (Oxcフォーマッター) を使用します。設定は .oxfmtrc.json にあります。スペースではなくタブを使用します。
アーキテクチャ概要
Section titled “アーキテクチャ概要”コアコンセプト
Section titled “コアコンセプト”D1が信頼できる情報源です。 スキーマは2つのシステムテーブルに存在します:
_emdash_collections— コレクションのメタデータ_emdash_fields— フィールド定義
コレクションを作成すると、EmDashは ALTER TABLE を実行して、型付けされたカラムを持つ実際の ec_* テーブルを作成します。EAV (Entity-Attribute-Value) アプローチは使用しません。
ミドルウェアチェーン (すべてのリクエストに対して順番に実行):
- ランタイム初期化 — データベース接続を作成し、
EmDashRuntimeを初期化 - セットアップチェック — 設定されていない場合はセットアップウィザードにリダイレクト
- 認証 — セッションを検証し、
locals.userを設定 - リクエストコンテキスト — プレビュー/編集モード用にAsyncLocalStorageをセットアップ
ハンドラーレイヤー: ビジネスロジックは api/handlers/*.ts に存在します。ルートファイルは、入力を解析し、ハンドラーを呼び出し、応答をフォーマットする薄いラッパーです。ハンドラーは ApiResponse<T> = { success: boolean; data?: T; error?: { code, message } } を返します。
主要ファイル
Section titled “主要ファイル”| ファイル | 目的 |
|---|---|
src/astro/integration/index.ts | Astro統合のエントリーポイント; 仮想モジュールを生成 |
src/emdash-runtime.ts | 中心的なランタイム; DB、プラグイン、ストレージを調整 |
src/schema/registry.ts | ec_* テーブルの作成/変更を管理 |
src/database/migrations/runner.ts | StaticMigrationProvider; 新しいマイグレーションをここに登録 |
src/plugins/manager.ts | 信頼されたプラグインを読み込み、調整 |
データベースパターン
Section titled “データベースパターン”EmDashはすべてのクエリに Kysely を使用します。重要なルール:
// CORRECT: parameterized valuesconst post = await db .selectFrom("ec_posts") .selectAll() .where("slug", "=", slug) // parameterized .executeTakeFirst();
// 正しい例: 生SQL内の検証済み識別子validateIdentifier(tableName);const result = await sql.raw(`SELECT * FROM ${tableName}`).execute(db);
// 間違った例: 未検証の値をSQLに直接埋め込まないconst result = await sql.raw(`SELECT * FROM ${userInput}`).execute(db);値に対して文字列補間で sql.raw() を使用しないでください。識別子には sql.ref() を、それ以外にはKyselyのフルーエントAPIを使用してください。
マイグレーションの追加
Section titled “マイグレーションの追加”-
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();} -
packages/core/src/database/migrations/runner.tsに登録:import * as m018 from "./018_my_migration.js";// getMigrations() の戻り値に追加:"018_my_migration": m018,
APIルートの追加
Section titled “APIルートの追加”ルートファイルは packages/core/src/astro/routes/api/ に存在します。以下の規約に従ってください:
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 is not initialized", 500);
// requirePermは、拒否された場合は403 Responseを、許可された場合はnullを返します const denied = requirePerm(user, "content:edit_any"); if (denied) return denied;
const body = await parseBody(request, createInput); if (body instanceof Response) return body;
try { // ビジネスロジックをここに記述 return Response.json({ success: true }); } catch (error) { return handleError(error, "Failed to create resource", "CREATE_ERROR"); }};その後、ルートを packages/core/src/astro/integration/routes.ts に登録します。
プラグイン開発
Section titled “プラグイン開発”プラグインは definePlugin() で定義され、Astro設定に登録されます。完全なAPIについては、プラグインシステムのドキュメントを参照してください。
ローカルプラグイン開発の場合:
# Create a local plugin in packages/pnpm --filter emdash dev # Watch modeデモの astro.config.mjs でプラグインをリンク:
import myPlugin from "../../../packages/my-plugin/src/index.ts";
emdash({ plugins: [myPlugin()],});テストパターン
Section titled “テストパターン”テストは packages/core/tests/ に存在します。構造はソースを反映しています:
tests/├── unit/ # Pure function tests├── integration/ # Real DB tests (in-memory SQLite)└── e2e/ # Playwright browser testsデータベーステストはモックではなく実際のSQLiteを使用します:
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("creates a content entry", async () => { // test with real DB });});E2Eテスト は、認証に開発用バイパスを使用したPlaywrightを利用します:
await page.goto( "http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin");内部インポートでは常に .js 拡張子を使用します (ESM要件):
// Correctimport { ContentRepository } from "../../../database/repositories/content.js";
// Wrongimport { ContentRepository } from "../../../database/repositories/content";型のみのインポートには import type を使用:
import type { Kysely } from "kysely";import type { User } from "@emdash-cms/auth";エラーハンドリング
Section titled “エラーハンドリング”APIルートでは共有のエーティリティを使用:
// Error responsesreturn apiError("NOT_FOUND", "Content not found", 404);
// catchブロックcatch (error) { return handleError(error, "Failed to update content", "CONTENT_UPDATE_ERROR");}状態を変更するすべてのルートは認可をチェックする必要があります。#api/authorize.js の requirePerm() を使用してください — 拒否された場合は Response (403) を、許可された場合は null を返します:
import { requirePerm } from "#api/authorize.js";
const denied = requirePerm(user, "content:edit_any");if (denied) return denied;所有権に基づくアクションには、requireOwnerPerm() を使用:
import { requireOwnerPerm } from "#api/authorize.js";
const denied = requireOwnerPerm(user, item.authorId, "content:edit_own", "content:edit_any");if (denied) return denied;コミットとPRプロセス
Section titled “コミットとPRプロセス”mainから機能ブランチを作成する- 変更を加え、
pnpm typecheckとpnpm lint:jsonが通ることを確認する - 関連するテストを実行する
- 説明的なメッセージでコミットする
mainをターゲットにPRを開く
コミットメッセージは、何をではなく、なぜを説明するべきです:
# Goodfix: prevent media MIME sniffing with X-Content-Type-Options header
# あまり良くない例fix: add header to media endpointヘルプを得る
Section titled “ヘルプを得る”- アーキテクチャの決定事項とコードパターンについては
AGENTS.mdを読む - ガイドとAPIリファレンスについてはドキュメントサイトを確認する