Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .changeset/busy-rivers-drive.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
"@emdash-cms/registry-cli": minor
"@emdash-cms/plugin-cli": minor
---

Adds `emdash-plugin.jsonc` manifest support. Plugin authors can now declare profile fields (license, author, security contact, name, description, keywords, repo) once in a hand-edited JSONC file instead of passing them as flags on every publish. The CLI loads `./emdash-plugin.jsonc` automatically; explicit flags still win for CI use.

New `emdash-registry validate` command checks a manifest against the schema offline with `tsc`-style file:line:column diagnostics.
New `emdash-plugin validate` command checks a manifest against the schema offline with `tsc`-style file:line:column diagnostics.

The manifest's optional `publisher` field pins the publishing identity. On first successful publish, the CLI writes the active session's DID back to the manifest. Subsequent publishes verify the active session matches the pinned publisher and refuse on mismatch to prevent accidental cross-account publishes.

JSON Schema for IDE completion ships in the package at `schemas/emdash-plugin.schema.json`; reference it via `"$schema": "./node_modules/@emdash-cms/registry-cli/schemas/emdash-plugin.schema.json"`.
JSON Schema for IDE completion ships in the package at `schemas/emdash-plugin.schema.json`; reference it via `"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json"`.
59 changes: 59 additions & 0 deletions .changeset/emdash-sandboxed-plugin-authoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
"emdash": minor
---

**BREAKING (plugin authors):** Reworks how sandboxed plugins are defined. The `definePlugin()` helper is removed for sandboxed-format plugins; the new shape is a bare default export with a `satisfies SandboxedPlugin` annotation. A new type-only subpath `emdash/plugin` provides the types.

This affects anyone _writing_ a sandboxed plugin. Sites that _use_ plugins are unaffected (see the per-plugin changesets for the import-shape change in published plugins).

```diff
- import { definePlugin, type ContentHookEvent, type PluginContext } from "emdash";
+ import type { SandboxedPlugin } from "emdash/plugin";

- export default definePlugin({
+ export default {
hooks: {
"content:beforeSave": {
- handler: async (event: ContentHookEvent, ctx: PluginContext) => {
+ handler: async (event, ctx) => {
// ...
return event.content;
},
},
},
- });
+ } satisfies SandboxedPlugin;
```

Three changes:

1. **Drop `import { definePlugin } from "emdash"`** and the `definePlugin(...)` wrapping call. Sandboxed plugins now default-export the bare object.
2. **`import type { SandboxedPlugin } from "emdash/plugin"`** and add `satisfies SandboxedPlugin` to the default export. The `emdash/plugin` subpath is type-only — the bundler erases the import, so no runtime resolution of `emdash` is needed (and the heavy `emdash` runtime no longer enters the plugin bundle).
3. **Drop handler parameter annotations** like `event: ContentSaveEvent, ctx: PluginContext`. The strict mapped type on `SandboxedPlugin` infers them per hook name, with the full canonical event type. If you need to reference an event type by name (e.g. in a helper function), `emdash/plugin` re-exports them: `import type { ContentHookEvent, PluginContext } from "emdash/plugin"`.

**Why:** the old `definePlugin` was an identity function whose only job was to alias `emdash` to a Proxy shim at build time so the import would resolve. With the new shape, sandboxed plugins have _no_ runtime `emdash` import — only type-only imports from `emdash/plugin`. The bundler doesn't need to alias anything; the build pipeline is simpler; and authors get strict per-hook event/return type inference for free.

The trade-off: previously you could narrow an event type locally (e.g. `interface ContentSaveEvent { content: ... & { id: string } }`). Under the strict mapped type, the canonical event type wins (TypeScript's contravariance on function parameters means narrowing isn't assignable). Authors validate fields at runtime with `typeof` / `isRecord` checks instead — which is the right pattern for input that comes from outside the type system anyway.

**Routes** follow the same simplification. The two-arg `(routeCtx, ctx)` shape is unchanged; only the annotations disappear:

```ts
export default {
routes: {
health: async (routeCtx, ctx) => {
// routeCtx: SandboxedRouteContext, ctx: PluginContext — both inferred.
return new Response("ok");
},
},
} satisfies SandboxedPlugin;
```

`SandboxedRouteContext` exposes `{ input, request, requestMeta? }`. `request` is typed as `SandboxedRequest` — a `{ url, method, headers }` record that's portable across in-process and isolate execution (Worker Loader can't pass real `Request` objects across the boundary).

**Native plugins are unaffected.** This change applies only to sandboxed-format plugins. Native plugins continue to use `definePlugin()` from `emdash` and the existing `PluginDefinition` shape.

**Type rename:** `SandboxedPlugin` on the `emdash` package now refers to the new author-facing source-shape type. The runtime-side handle type (returned by `SandboxRunner.load`, held in the runtime's plugin cache) is renamed to `SandboxedPluginInstance`. If you import `SandboxedPlugin` from `emdash` to type a sandbox runner implementation or hold runtime plugin handles, update those imports to `SandboxedPluginInstance`. Public consumers of this type are mostly limited to `@emdash-cms/cloudflare` and other sandbox runner adapters; standard plugin / site code is unaffected.

**Removed types:** `StandardPluginDefinition`, `StandardHookHandler`, `StandardHookEntry`, `StandardRouteHandler`, `StandardRouteEntry` are no longer exported from `emdash`. These were authoring-helper aliases under the old permissive `definePlugin` standard overload. Use `SandboxedPlugin` from `emdash/plugin` for the same purpose under the new shape.

**Removed function:** `isStandardPluginDefinition` is gone. There's no equivalent — sandboxed plugins are identified by structure (`{ hooks?, routes? }`) and you should treat the default export as already typed via `satisfies SandboxedPlugin`.
21 changes: 21 additions & 0 deletions .changeset/plugin-atproto-default-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@emdash-cms/plugin-atproto": minor
---

**BREAKING:** Removes the `atprotoPlugin` named export and the factory call shape. Import the default export and pass it directly into `plugins:` or `sandboxed:`.

```diff
- import { atprotoPlugin } from "@emdash-cms/plugin-atproto";
+ import atproto from "@emdash-cms/plugin-atproto";

export default defineConfig({
integrations: [
emdash({
- sandboxed: [atprotoPlugin()],
+ sandboxed: [atproto],
}),
],
});
```

Two changes: drop the `{ }` around the import, and drop the `()` after the plugin name. Per-install configuration moved to the admin UI's settings (KV-backed) when the sandboxed plugin redesign landed, so there's no longer a need for a factory call.
21 changes: 21 additions & 0 deletions .changeset/plugin-audit-log-default-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@emdash-cms/plugin-audit-log": minor
---

**BREAKING:** Removes the `auditLogPlugin` named export and the factory call shape. Import the default export and pass it directly into `plugins:` or `sandboxed:`.

```diff
- import { auditLogPlugin } from "@emdash-cms/plugin-audit-log";
+ import auditLog from "@emdash-cms/plugin-audit-log";

export default defineConfig({
integrations: [
emdash({
- plugins: [auditLogPlugin()],
+ plugins: [auditLog],
}),
],
});
```

Two changes: drop the `{ }` around the import, and drop the `()` after the plugin name. Per-install configuration moved to the admin UI's settings (KV-backed) when the sandboxed plugin redesign landed, so there's no longer a need for a factory call.
37 changes: 37 additions & 0 deletions .changeset/plugin-cli-build-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
"@emdash-cms/plugin-cli": minor
---

Renames `@emdash-cms/registry-cli` to `@emdash-cms/plugin-cli` and the binary from `emdash-registry` to `emdash-plugin`. The package's job has outgrown the original name — `init`, `build`, `dev`, `bundle`, `publish`, `search`, `info`, `login`, `logout`, `whoami`, and `switch` cover plugin authoring + identity + discovery, not just registry interaction. Adopt the new name on first install; the old package is no longer published.

This release also adds `emdash-plugin build` and `emdash-plugin dev` and consolidates the build pipeline so `bundle` is a thin packaging step on top of `build`.

**`emdash-plugin build`** reads `emdash-plugin.jsonc` and `src/plugin.ts`, then emits:

- `dist/plugin.mjs` (+ `dist/plugin.d.mts`) — runtime bytes (hooks + routes). The same artifact is consumed both in-process (when the plugin is in `plugins: []`) and by the sandbox loader (when in `sandboxed: []`).
- `dist/manifest.json` — wire-shape `PluginManifest` including hooks + routes harvested from probing `src/plugin.ts`. `bundle` packs this verbatim into the registry tarball; on the npm path it's metadata that consumers can read without parsing JSONC.
- `dist/index.mjs` (+ `dist/index.d.mts`) — descriptor module that default-exports a bare `PluginDescriptor` object. Emitted only when a sibling `package.json` exists (registry-only plugins skip this, since nothing would import it).

**`emdash-plugin dev`** watches `src/**`, `emdash-plugin.jsonc`, and `package.json`, debouncing rebuilds at 150ms. On a failed rebuild it leaves the last good `dist/` in place so a downstream site importing the plugin keeps working until the next successful build. Stop with Ctrl-C.

A typical plugin `package.json`:

```json
{
"scripts": {
"build": "emdash-plugin build",
"dev": "emdash-plugin dev"
}
}
```

**`version` in `emdash-plugin.jsonc` is now optional.** The build reconciles the manifest's `version` with `package.json#version`:

- Both set and matching → fine.
- Both set and different → hard error.
- One set → that value wins.
- Neither set → hard error.

The recommended pattern for npm-distributed plugins is to omit `version` from the manifest and let `package.json` be the source of truth. Registry-only plugins (no `package.json`) must set `version` in the manifest.

**`emdash-plugin bundle`** has been reduced to a packaging step: it now calls `build` to produce `dist/`, validates the bundle contents (no Node-builtin imports, no oversized files, capability sanity), collects optional assets (README, icon, screenshots), and tarballs. Inside the tarball, `plugin.mjs` is renamed to `backend.js` to match the registry's wire-side filename. `validateOnly` still skips tarball creation but now produces the `dist/` artifacts (since "validate" implies "build first").
21 changes: 21 additions & 0 deletions .changeset/plugin-webhook-notifier-default-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@emdash-cms/plugin-webhook-notifier": minor
---

**BREAKING:** Removes the `webhookNotifierPlugin` named export and the factory call shape. Import the default export and pass it directly into `plugins:` or `sandboxed:`.

```diff
- import { webhookNotifierPlugin } from "@emdash-cms/plugin-webhook-notifier";
+ import webhookNotifier from "@emdash-cms/plugin-webhook-notifier";

export default defineConfig({
integrations: [
emdash({
- sandboxed: [webhookNotifierPlugin()],
+ sandboxed: [webhookNotifier],
}),
],
});
```

Two changes: drop the `{ }` around the import, and drop the `()` after the plugin name. Per-install configuration moved to the admin UI's settings (KV-backed) when the sandboxed plugin redesign landed, so there's no longer a need for a factory call.
39 changes: 3 additions & 36 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,48 +91,15 @@ jobs:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
# Build emdash + its deps AND the registry packages. The registry
# packages aren't deps of `emdash`, so the `emdash...` filter would
# Build emdash + its deps AND the plugin-cli + registry packages.
# They aren't deps of `emdash`, so the `emdash...` filter would
# leave them unbuilt and their tests would fail to resolve workspace
# links to dist/.
- run: pnpm run --filter emdash... --filter "@emdash-cms/registry-*" --filter "@emdash-cms/plugin-types" build
- run: pnpm run --filter emdash... --filter "@emdash-cms/plugin-cli" --filter "@emdash-cms/registry-*" --filter "@emdash-cms/plugin-types" build
- run: pnpm test:unit
env:
EMDASH_TEST_PG: postgres://postgres:test@localhost:5432/emdash_test

validate-plugins:
name: Validate Plugins
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run --filter emdash... build
- name: Validate marketplace plugins
run: |
CLI="node packages/core/dist/cli/index.mjs"
for dir in packages/plugins/*/; do
[ -f "$dir/package.json" ] || continue
if [ ! -f "$dir/src/sandbox-entry.ts" ] && \
! grep -q '"./sandbox"' "$dir/package.json" 2>/dev/null; then
continue
fi
name=$(basename "$dir")
case "$name" in
marketplace-test|sandboxed-test|api-test) continue ;;
esac
echo "::group::Validating $name"
$CLI plugin bundle --validateOnly --dir "$dir"
echo "::endgroup::"
done

test-smoke:
name: Smoke Tests
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"**/package.json",
"**/emdash-env.d.ts",
"packages/registry-lexicons/src/generated/**",
"packages/registry-cli/schemas/**"
"packages/plugin-cli/schemas/**"
]
}
14 changes: 7 additions & 7 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@
"**/client/transport.ts",
"**/client/portable-text.ts",
"**/cli/**/*.ts",
"packages/registry-cli/src/bundle/api.ts",
"packages/registry-cli/src/bundle/utils.ts",
"packages/registry-cli/src/bundle/command.ts",
"packages/registry-cli/src/bundle/types.ts",
"packages/registry-cli/src/oauth.ts",
"packages/registry-cli/src/publish/api.ts",
"packages/registry-cli/src/commands/publish.ts",
"packages/plugin-cli/src/bundle/api.ts",
"packages/plugin-cli/src/bundle/utils.ts",
"packages/plugin-cli/src/bundle/command.ts",
"packages/plugin-cli/src/bundle/types.ts",
"packages/plugin-cli/src/oauth.ts",
"packages/plugin-cli/src/publish/api.ts",
"packages/plugin-cli/src/commands/publish.ts",
"packages/registry-client/src/publishing/index.ts",
"**/api/handlers/api-tokens.ts",
"**/api/handlers/device-flow.ts",
Expand Down
4 changes: 2 additions & 2 deletions demos/cloudflare/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
cloudflareStream,
} from "@emdash-cms/cloudflare";
import { formsPlugin } from "@emdash-cms/plugin-forms";
import { webhookNotifierPlugin } from "@emdash-cms/plugin-webhook-notifier";
import webhookNotifier from "@emdash-cms/plugin-webhook-notifier";
import { defineConfig, fontProviders } from "astro/config";
import emdash from "emdash/astro";

Expand Down Expand Up @@ -74,7 +74,7 @@ export default defineConfig({
formsPlugin(),
],
// Sandboxed plugins (run in isolated workers)
sandboxed: [webhookNotifierPlugin()],
sandboxed: [webhookNotifier],
// Sandbox runner for Cloudflare
sandboxRunner: sandbox(),
// Plugin marketplace
Expand Down
5 changes: 2 additions & 3 deletions demos/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@emdash-cms/cloudflare": "workspace:*",
"@emdash-cms/plugin-forms": "workspace:*",
"@emdash-cms/plugin-webhook-notifier": "workspace:*",
"@emdash-cms/plugin-cli": "workspace:*",
"@tanstack/react-query": "catalog:",
"@tanstack/react-router": "catalog:",
"astro": "catalog:",
Expand All @@ -33,7 +34,5 @@
},
"emdash": {
"seed": "seed/seed.json"
},
"peerDependencies": {},
"optionalDependencies": {}
}
}
14 changes: 4 additions & 10 deletions demos/plugins-demo/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import node from "@astrojs/node";
import react from "@astrojs/react";
import { apiTestPlugin } from "@emdash-cms/plugin-api-test";
import { auditLogPlugin } from "@emdash-cms/plugin-audit-log";
import auditLog from "@emdash-cms/plugin-audit-log";
import { embedsPlugin } from "@emdash-cms/plugin-embeds";
import { webhookNotifierPlugin } from "@emdash-cms/plugin-webhook-notifier";
import webhookNotifier from "@emdash-cms/plugin-webhook-notifier";
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sqlite } from "emdash/db";
Expand All @@ -22,18 +22,12 @@ export default defineConfig({
// Register plugins - order matters for hook execution!
plugins: [
// 1. Audit log runs last (priority 200) to capture final state
// Settings (retention, data changes, excluded collections) are
// configured at runtime via the admin UI, not constructor options.
auditLogPlugin(),
auditLog,

// 2. Webhook notifier sends events to external URLs
// Demonstrates: network:fetch:any, apiRoutes, settings.secret(),
// hook dependencies, errorPolicy: "continue"
// Webhook URL, collections, and actions are configured via admin settings.
webhookNotifierPlugin(),
webhookNotifier,

// 3. Embeds plugin for YouTube, Vimeo, Twitter, etc.
// Components are auto-registered with PortableText
embedsPlugin(),

// 4. API Test plugin - exercises all v2 APIs
Expand Down
9 changes: 4 additions & 5 deletions demos/plugins-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
"dependencies": {
"@astrojs/node": "catalog:",
"@astrojs/react": "catalog:",
"@emdash-cms/plugin-audit-log": "workspace:*",
"@emdash-cms/plugin-api-test": "workspace:*",
"@emdash-cms/plugin-webhook-notifier": "workspace:*",
"@emdash-cms/plugin-audit-log": "workspace:*",
"@emdash-cms/plugin-embeds": "workspace:*",
"@emdash-cms/plugin-webhook-notifier": "workspace:*",
"@emdash-cms/plugin-cli": "workspace:*",
"@tanstack/react-query": "catalog:",
"@tanstack/react-router": "catalog:",
"astro": "catalog:",
Expand All @@ -27,7 +28,5 @@
},
"devDependencies": {
"@types/node": "catalog:"
},
"peerDependencies": {},
"optionalDependencies": {}
}
}
4 changes: 2 additions & 2 deletions demos/simple/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import node from "@astrojs/node";
import react from "@astrojs/react";
import { auditLogPlugin } from "@emdash-cms/plugin-audit-log";
import auditLog from "@emdash-cms/plugin-audit-log";
import { defineConfig, fontProviders } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
Expand All @@ -22,7 +22,7 @@ export default defineConfig({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
plugins: [auditLogPlugin()],
plugins: [auditLog],
}),
],
fonts: [
Expand Down
5 changes: 2 additions & 3 deletions demos/simple/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@emdash-cms/plugin-atproto": "workspace:*",
"@emdash-cms/plugin-audit-log": "workspace:*",
"@emdash-cms/plugin-color": "workspace:*",
"@emdash-cms/plugin-cli": "workspace:*",
"astro": "catalog:",
"better-sqlite3": "catalog:",
"emdash": "workspace:*",
Expand All @@ -28,7 +29,5 @@
},
"devDependencies": {
"@astrojs/check": "catalog:"
},
"peerDependencies": {},
"optionalDependencies": {}
}
}
Loading
Loading