Contribuir a EmDash
Esta guía cubre cómo configurar un entorno de desarrollo local, comprender la arquitectura del código base y contribuir a EmDash.
Estructura del Repositorio
Sección titulada «Estructura del Repositorio»EmDash es un monorepo de pnpm con múltiples paquetes:
emdash/├── packages/│ ├── core/ # emdash — integracion de Astro, APIs y panel de administracion (paquete principal)│ ├── auth/ # @emdash-cms/auth — autenticacion (passkeys, OAuth, magic links)│ ├── cloudflare/ # @emdash-cms/cloudflare — adaptador de Cloudflare y sandbox runner│ ├── admin/ # @emdash-cms/admin — SPA de administracion en React│ ├── create-emdash/ # create-emdash — generador de proyectos│ ├── gutenberg-to-portable-text/ # convertidor de bloques de WordPress a Portable Text│ └── plugins/ # plugins oficiales; cada subdirectorio es su propio paquete├── demos/│ ├── simple/ # emdash-demo — demo principal para desarrollo y pruebas (Node.js)│ ├── cloudflare/ # demo para Cloudflare Workers│ └── ... # plugins-demo, showcase, wordpress-import└── docs/ # sitio de documentacion (Starlight)El paquete principal es packages/core. Contiene:
packages/core/src/├── astro/│ ├── integration/ # punto de entrada de la integracion de Astro y generacion de modulos virtuales│ ├── middleware/ # autenticacion, comprobacion de setup y contexto de solicitud (ALS)│ └── routes/│ ├── api/ # manejadores de rutas REST API│ └── admin-shell.astro # shell de la SPA de administracion├── database/│ ├── migrations/ # archivos de migracion numerados (001_initial.ts, ...)│ │ └── runner.ts # StaticMigrationProvider — registra aqui las migraciones│ ├── repositories/ # capa de acceso a datos (content, media, settings, ...)│ └── types.ts # tipo Database de Kysely├── plugins/│ ├── types.ts # tipos de la API de plugins│ ├── define-plugin.ts # definePlugin()│ ├── context.ts # fabrica de PluginContext│ ├── hooks.ts # HookPipeline│ ├── manager.ts # PluginManager (plugins confiables)│ └── sandbox/ # interfaz de sandbox y runner no-op├── schema/│ └── registry.ts # SchemaRegistry — gestiona las tablas ec_*├── media/ # proveedores de medios (local, types)├── auth/ # almacenes de challenge y estado OAuth├── query.ts # getEmDashCollection, getEmDashEntry├── loader.ts # implementacion de Astro LiveLoader└── emdash-runtime.ts # EmDashRuntime — orquestador centralPrerrequisitos
Sección titulada «Prerrequisitos»- Node.js 22 o superior
- pnpm 10 o superior
- Git
# Instala pnpm si aun no lo tienesnpm install -g pnpmConfiguración Local
Sección titulada «Configuración Local»-
Clonar el repositorio
Ventana de terminal git clone <repository-url>cd emdash -
Instalar dependencias
Ventana de terminal pnpm install -
Construir paquetes (requerido antes de ejecutar la demo)
Ventana de terminal pnpm build -
Poblar la base de datos de demostración (
demos/simple/)Ventana de terminal pnpm --filter emdash-demo seed -
Iniciar el servidor de desarrollo
Ventana de terminal pnpm --filter emdash-demo dev -
Abrir el administrador
Visitar http://localhost:4321/_emdash/admin
En modo de desarrollo, usar el endpoint de omisión de desarrollo para saltar la autenticación con passkey:
http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin
Flujo de Trabajo de Desarrollo
Sección titulada «Flujo de Trabajo de Desarrollo»Modo de Observación
Sección titulada «Modo de Observación»Para el desarrollo de paquetes, usar el modo de observación junto con la demo:
# Terminal 1: observar cambios en packages/corepnpm --filter emdash dev
# Terminal 2: Ejecutar la demo (demos/simple/)pnpm --filter emdash-demo devEjecutar Pruebas
Sección titulada «Ejecutar Pruebas»pnpm testpnpm --filter emdash testpnpm --filter emdash test --watchpnpm test:e2eVerificación de Tipos y Linting
Sección titulada «Verificación de Tipos y Linting»# Comprobar tipos en los paquetes TypeScriptpnpm typecheck
# Verificar tipos en demos de Astropnpm typecheck:demos
# Lint rápido (< 1s) — ejecutar después de cada ediciónpnpm lint:quick
# Lint completo con reglas conscientes de tipos (~10s) — ejecutar antes de commitspnpm lint:jsonFormateo
Sección titulada «Formateo»pnpm formatEmDash usa oxfmt (formateador Oxc). La configuración está en .oxfmtrc.json. Tabulaciones, no espacios.
Descripción General de la Arquitectura
Sección titulada «Descripción General de la Arquitectura»Conceptos Clave
Sección titulada «Conceptos Clave»D1 es la fuente de verdad. El esquema reside en dos tablas del sistema:
_emdash_collections— metadatos de colecciones_emdash_fields— definiciones de campos
Cuando creas una colección, EmDash ejecuta ALTER TABLE para crear una tabla real ec_* con columnas tipadas. No se utiliza un enfoque EAV (Entidad-Atributo-Valor).
Cadena de middleware (en orden para cada solicitud):
- Inicialización del entorno de ejecución — crea la conexión a la base de datos, inicializa
EmDashRuntime - Verificación de configuración — redirige al asistente de configuración si no está configurado
- Autenticación — valida la sesión, llena
locals.user - Contexto de solicitud — configura AsyncLocalStorage para el modo de vista previa/edición
Capa de manejadores: la lógica de negocio reside en api/handlers/*.ts. Los archivos de ruta son envoltorios delgados que analizan la entrada, llaman a los manejadores y formatean las respuestas. Los manejadores devuelven ApiResponse<T> = { success: boolean; data?: T; error?: { code, message } }.
Archivos Clave
Sección titulada «Archivos Clave»| Archivo | Propósito |
|---|---|
src/astro/integration/index.ts | Punto de entrada de la integración de Astro; genera módulos virtuales |
src/emdash-runtime.ts | Entorno de ejecución central; orquesta la base de datos, plugins, almacenamiento |
src/schema/registry.ts | Gestiona la creación/modificación de tablas ec_* |
src/database/migrations/runner.ts | StaticMigrationProvider; registrar nuevas migraciones aquí |
src/plugins/manager.ts | Carga y orquesta plugins confiables |
Patrones de Base de Datos
Sección titulada «Patrones de Base de Datos»EmDash usa Kysely para todas las consultas. Reglas clave:
// CORRECTO: valores parametrizadosconst post = await db .selectFrom("ec_posts") .selectAll() .where("slug", "=", slug) // parametrizado .executeTakeFirst();
// CORRECTO: identificador validado en SQL crudovalidateIdentifier(tableName);const result = await sql.raw(`SELECT * FROM ${tableName}`).execute(db);
// INCORRECTO: nunca interpolar valores no validados en SQLconst result = await sql.raw(`SELECT * FROM ${userInput}`).execute(db);Nunca usar sql.raw() con interpolación de cadenas para valores. Usar sql.ref() para identificadores, y la API fluida de Kysely para todo lo demás.
Agregar una Migración
Sección titulada «Agregar una Migración»-
Crear
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();} -
Registrarla en
packages/core/src/database/migrations/runner.ts:import * as m018 from "./018_my_migration.js";// Agregar al valor de retorno de getMigrations():"018_my_migration": m018,
Agregar una Ruta de API
Sección titulada «Agregar una Ruta de API»Los archivos de ruta viven en packages/core/src/astro/routes/api/. Seguir estas convenciones:
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 no está inicializado", 500);
// requirePerm devuelve un Response (403) si se deniega, o null si está autorizado const denied = requirePerm(user, "content:edit_any"); if (denied) return denied;
const body = await parseBody(request, createInput); if (body instanceof Response) return body;
try { // lógica de negocio aquí return Response.json({ success: true }); } catch (error) { return handleError(error, "No se pudo crear el recurso", "CREATE_ERROR"); }};Luego registrar la ruta en packages/core/src/astro/integration/routes.ts.
Desarrollo de Plugins
Sección titulada «Desarrollo de Plugins»Los plugins se definen con definePlugin() y se registran en la configuración de Astro. Ver la documentación del Sistema de Plugins para la API completa.
Para desarrollo local de plugins:
# Crea un plugin local dentro de packages/pnpm --filter emdash dev # modo observacionEnlazar tu plugin en el astro.config.mjs de la demo:
import myPlugin from "../../../packages/my-plugin/src/index.ts";
emdash({ plugins: [myPlugin()],});Patrones de Pruebas
Sección titulada «Patrones de Pruebas»Las pruebas viven en packages/core/tests/. La estructura refleja la fuente:
tests/├── unit/ # pruebas de funciones puras├── integration/ # pruebas con base de datos real (SQLite en memoria)└── e2e/ # pruebas de navegador con PlaywrightLas pruebas de base de datos usan SQLite real, no 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("creates a content entry", async () => { // probar con una base de datos real });});Pruebas E2E usan Playwright con la omisión de desarrollo para autenticación:
await page.goto( "http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin");Convenciones de Código
Sección titulada «Convenciones de Código»Importaciones
Sección titulada «Importaciones»Siempre usar extensiones .js para importaciones internas (requisito de ESM):
// Correctoimport { ContentRepository } from "../../../database/repositories/content.js";
// Incorrectoimport { ContentRepository } from "../../../database/repositories/content";Usar import type para importaciones solo de tipos:
import type { Kysely } from "kysely";import type { User } from "@emdash-cms/auth";Manejo de Errores
Sección titulada «Manejo de Errores»Usar las utilidades de error compartidas en las rutas de API:
// Respuestas de errorreturn apiError("NOT_FOUND", "Contenido no encontrado", 404);
// Bloques catchcatch (error) { return handleError(error, "No se pudo actualizar el contenido", "CONTENT_UPDATE_ERROR");}Autorización
Sección titulada «Autorización»Cada ruta que cambia el estado debe verificar la autorización. Usar requirePerm() desde #api/authorize.js — devuelve un Response (403) si se deniega, o null si está autorizado:
import { requirePerm } from "#api/authorize.js";
const denied = requirePerm(user, "content:edit_any");if (denied) return denied;Para acciones con alcance de propiedad, usar requireOwnerPerm():
import { requireOwnerPerm } from "#api/authorize.js";
const denied = requireOwnerPerm(user, item.authorId, "content:edit_own", "content:edit_any");if (denied) return denied;Proceso de Commit y PR
Sección titulada «Proceso de Commit y PR»- Crea una rama de funcionalidad desde
main - Realiza cambios, asegúrate de que
pnpm typecheckypnpm lint:jsonpasen - Ejecuta las pruebas relevantes
- Haz un commit con un mensaje descriptivo
- Abre una PR apuntando a
main
Los mensajes de commit deben describir por qué, no solo qué:
# Buenofix: prevent media MIME sniffing with X-Content-Type-Options header
# Menos buenofix: add header to media endpointObtener Ayuda
Sección titulada «Obtener Ayuda»- Lee
AGENTS.mdpara decisiones de arquitectura y patrones de código - Consulta el sitio de documentación para guías y referencia de API