为 EmDash 贡献代码
本指南涵盖如何设置本地开发环境、理解代码库架构以及为 EmDash 贡献代码。
EmDash 是一个 pnpm monorepo,包含多个包:
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-
克隆仓库
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
对于包开发,请与演示一起使用监视模式:
# Terminal 1: Watch packages/core for changespnpm --filter emdash dev
# Terminal 2: Run the demo (demos/simple/)pnpm --filter emdash-demo devpnpm testpnpm --filter emdash testpnpm --filter emdash test --watchpnpm test:e2e类型检查和代码检查
Section titled “类型检查和代码检查”# Type check TypeScript packagespnpm typecheck
# Type check Astro demospnpm typecheck:demos
# Fast lint (< 1s) — run after every editpnpm lint:quick
# Full lint with type-aware rules (~10s) — run before commitspnpm lint:jsonpnpm formatEmDash 使用 oxfmt(Oxc 格式化器)。配置位于 .oxfmtrc.json 中。使用制表符,而非空格。
D1 是唯一可信源。 模式存在于两个系统表中:
_emdash_collections— 集合元数据_emdash_fields— 字段定义
当你创建一个集合时,EmDash 会运行 ALTER TABLE 来创建一个具有类型化列的真实 ec_* 表。这里没有采用 EAV(实体-属性-值)方法。
中间件链(每个请求按顺序执行):
- 运行时初始化 — 创建数据库连接,初始化
EmDashRuntime - 设置检查 — 如果未配置,则重定向到设置向导
- 认证 — 验证会话,填充
locals.user - 请求上下文 — 为预览/编辑模式设置 AsyncLocalStorage
处理程序层: 业务逻辑位于 api/handlers/*.ts 中。路由文件是薄包装器,负责解析输入、调用处理程序并格式化响应。处理程序返回 ApiResponse<T> = { success: boolean; data?: T; error?: { code, message } }。
| 文件 | 用途 |
|---|---|
src/astro/integration/index.ts | Astro 集成入口点;生成虚拟模块 |
src/emdash-runtime.ts | 核心运行时;协调数据库、插件、存储 |
src/schema/registry.ts | 管理 ec_* 表的创建/修改 |
src/database/migrations/runner.ts | StaticMigrationProvider;在此注册新迁移 |
src/plugins/manager.ts | 加载并协调受信任的插件 |
EmDash 使用 Kysely 处理所有查询。关键规则:
// CORRECT: parameterized valuesconst post = await db .selectFrom("ec_posts") .selectAll() .where("slug", "=", slug) // parameterized .executeTakeFirst();
// CORRECT: validated identifier in raw SQLvalidateIdentifier(tableName);const result = await sql.raw(`SELECT * FROM ${tableName}`).execute(db);
// WRONG: never interpolate unvalidated values into SQLconst result = await sql.raw(`SELECT * FROM ${userInput}`).execute(db);切勿将 sql.raw() 与字符串插值一起用于值。对标识符使用 sql.ref(),其他所有操作都使用 Kysely 流畅 API。
-
创建
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";// Add to getMigrations() return value:"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 returns a 403 Response if denied, or null if authorized const denied = requirePerm(user, "content:edit_any"); if (denied) return denied;
const body = await parseBody(request, createInput); if (body instanceof Response) return body;
try { // business logic here return Response.json({ success: true }); } catch (error) { return handleError(error, "Failed to create resource", "CREATE_ERROR"); }};然后在 packages/core/src/astro/integration/routes.ts 中注册该路由。
插件使用 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()],});测试位于 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";在 API 路由中使用共享的错误工具:
// Error responsesreturn apiError("NOT_FOUND", "Content not found", 404);
// Catch blockscatch (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
# Less goodfix: add header to media endpoint- 阅读
AGENTS.md了解架构决策和代码模式 - 查看文档网站获取指南和 API 参考