Skip to content

Latest commit

 

History

History
260 lines (181 loc) · 11.3 KB

File metadata and controls

260 lines (181 loc) · 11.3 KB

Contributing to EmDash

EmDash is published to npm and in active use. During development you work inside the monorepo -- packages use workspace:* links, so everything works without publishing.

This guide covers setup, policy, and the rules around opening a PR. For code patterns (SQL, API routes, authorization, performance, Lingui, RTL, etc.), see AGENTS.md.

Prerequisites

  • Node.js 22+
  • pnpm 10+ (corepack enable if you don't have it)
  • Git

Setup

git clone https://github.com/emdash-cms/emdash.git && cd emdash
pnpm install
pnpm build   # required before first run

Run the demo

demos/simple/ is the primary development target. Node.js + SQLite, no Cloudflare account needed.

cd demos/simple
pnpm dev    # http://localhost:4321

Open the admin at http://localhost:4321/_emdash/admin. The setup wizard runs on first launch.

In dev, skip passkey auth with the dev bypass:

http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin

Demo sites apply their seed/seed.json automatically on the first request when the database is empty -- there's no separate seed command.

demos/cloudflare/ runs on the real workerd runtime with D1. See its README.

Templates

Templates in templates/ are workspace members and runnable directly:

cd templates/portfolio
pnpm dev

Available templates: blank, starter, blog, portfolio, marketing, plus a -cloudflare variant of each runnable template. Seed content is applied automatically on first request. To start fresh, delete the local database (data.db or the D1 binding) and restart the dev server.

Watch mode

When iterating on packages/core alongside a demo, run two terminals:

# Terminal 1
cd packages/core && pnpm dev

# Terminal 2
cd demos/simple && pnpm dev

Core changes propagate to the demo automatically.

Repository Layout

Directory What it is
packages/core/ Main emdash package -- Astro integration, REST API, database, schema, plugins
packages/admin/ React admin UI SPA (@emdash-cms/admin)
packages/auth/ Auth -- passkeys, OAuth, magic links (@emdash-cms/auth)
packages/cloudflare/ Cloudflare Workers adapter + plugin sandbox
packages/blocks/ Portable Text block definitions
packages/create-emdash/ create-emdash CLI scaffolder
packages/plugins/ First-party plugins
demos/ Dev/test apps (simple, cloudflare, postgres, ...)
templates/ Starter templates
docs/ Documentation site (Starlight)
e2e/ Playwright test infrastructure
i18n/ Translation status dashboard (Lunaria)

Checks

Run before pushing:

pnpm typecheck   # TypeScript (packages)
pnpm lint        # full type-aware lint
pnpm format      # auto-format with oxfmt (tabs)
pnpm test        # all packages
pnpm test:e2e    # Playwright

Tests use real in-memory SQLite -- no mocking. Each test gets a fresh database. Typecheck and lint must pass.

Building your own site in the monorepo

Copy a template into demos/, give it a unique name in package.json, install, and run:

cp -r templates/blog demos/my-site
# edit demos/my-site/package.json to set a unique name
pnpm install
cd demos/my-site && pnpm dev

Your site uses workspace:* links, so core changes are reflected immediately.

Contribution Policy

What we accept

Type Process
Bug fixes Open a PR directly. Include a failing test that reproduces the bug.
Docs / typos Open a PR directly.
Translations Open a PR directly. See Translating EmDash.
Features Open a Discussion and wait for maintainer approval.
Refactors Open a Discussion first.
Performance Open a Discussion first with benchmarks.

Feature PRs without prior maintainer approval will be closed. Not gatekeeping -- it's about not wasting your time on work that might not align with the project's direction.

What we don't accept

  • Drive-by feature additions. No Discussion, no PR.
  • Speculative refactors that don't solve a concrete problem.
  • Dependency upgrades outside Renovate/Dependabot.
  • Drive-by "improvements" in code unrelated to your change.
  • Bulk/spray PRs ("fix all lint warnings", "add types everywhere"). Open a Discussion first.

AI-generated PRs

AI-assisted contributions are welcome and held to the same quality bar as any other PR:

  • The submitter is responsible for correctness, not the tool.
  • AI-generated PRs must pass CI, follow project patterns, and include tests.
  • Check the PR template's AI disclosure box and name the model/tool (e.g. Claude Opus 4.7, GPT-5.5, Cursor + Sonnet 4.6). This isn't punitive -- it helps reviewers focus on edge cases that AI tools tend to miss and run the review pass with a different model family.

PR rules

  • Branch from main.
  • Fill out the PR template completely. PRs with an empty or missing template will be closed automatically. The template is loaded by the GitHub UI; if you create a PR via API/CLI, copy .github/PULL_REQUEST_TEMPLATE.md into the body.
  • pnpm typecheck and pnpm lint must pass before pushing.
  • Run relevant tests.
  • Commit messages describe why, not just what.

Changesets

Every PR that changes a published package's behavior needs a changeset -- a small Markdown file that describes the change for the CHANGELOG and determines the version bump. Without one, the change won't trigger a release.

When you need one

  • Bug fixes, features, refactors, or anything that affects a published package's behavior or API.
  • Multi-package changes need one changeset listing all affected packages.
  • A PR making multiple distinct changes can include a changeset per change -- each becomes its own CHANGELOG entry.

When you don't

  • Docs-only, test-only, CI/tooling changes, or changes to demos and templates (these are in the ignore list -- see .changeset/config.json).

How

pnpm changeset

The CLI walks you through affected packages, bump type, and description. Edit the resulting .md file in .changeset/ if needed.

Writing the description

A changeset is the release note a user reads while upgrading -- it lands verbatim in the CHANGELOG. It is not a commit message, a PR description, or a summary of your diff. Don't paste your PR text into it: those explain the change to a reviewer reading the code, the changeset explains the effect to someone who will run the new version.

Write for that reader:

  • Start with a present-tense verb -- Fixes (bug), Adds (feature), Updates (enhancement), Removes (removed functionality), Refactors (no behavior change).
  • Describe the observable effect -- what's different for someone using the package.
  • Leave out internal mechanics -- file names, function names, which catalog entry you bumped, how you implemented it. If a sentence only makes sense to someone who has read the diff, it doesn't belong here.
  • For a breaking change, include the migration step.

One sentence is often enough.

- # too low-level -- reads like a commit message
- Align the catalog so identity-resolver's lexicons peer resolves; migrates parseCanonicalResourceUri off the result-object API in backfill.ts.
+ # right altitude -- the effect on the user
+ Fixes peer dependency warnings on install caused by mismatched `@atcute` package versions.

Patch (bug fix or small improvement):

---
"emdash": patch
---

Fixes CLI `--json` flag so JSON output is clean. Log messages now go to stderr when `--json` is set.

Minor (new non-breaking feature):

---
"emdash": minor
---

Adds `scheduled_at` field to content entries, enabling scheduled publishing via the admin UI.

Major (breaking change) -- include migration guidance:

---
"emdash": major
---

Removes the `legacyAuth` option from the integration config. All sites must use passkey authentication.

To migrate, remove `legacyAuth: true` from your `emdash()` config in `astro.config.mjs`.

Internationalization

The admin UI is translatable using Lingui. All user-visible strings in packages/admin/src/ should be wrapped.

import { Trans, useLingui } from "@lingui/react/macro";

function MyComponent() {
	const { t } = useLingui();
	return (
		<div>
			<h1>{t`Settings`}</h1>
			<p>{t`Authentication error: ${error}`}</p>
			<p>
				<Trans>
					Don't have an account? <a href="/signup">Sign up</a>
				</Trans>
			</p>
		</div>
	);
}

Wrap button labels, headings, descriptions, error messages, placeholders, and aria-label on interactive controls. Don't wrap log messages, developer-facing errors, brand names, or URLs. For decorative elements, prefer aria-hidden="true" over a translated aria-label.

Don't include messages.po changes in feature or bugfix PRs. A workflow runs pnpm locale:extract on merge to main and commits catalog updates automatically. Including extracted PO changes in non-translation PRs creates churn and merge conflicts because line-number references shift on every edit. If you ran extraction locally and ended up with .po changes, revert them before opening the PR.

Translation PRs are the exception -- see Translating EmDash for the full contributor guide.

For RTL rules and the full Lingui pattern reference, see AGENTS.md § Admin UI: Localization.

Getting Help