npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bnomei/emdash-actions

v0.4.0

Published

Native action surface for EmDash provider plugins.

Downloads

1,034

Readme

@bnomei/emdash-actions

npm version npm downloads license types source

Action buttons for EmDash fields and dashboards.

@bnomei/emdash-actions adds configurable buttons to the EmDash admin. A button can copy a field value in the browser, call an EmDash plugin route, write a returned value back into a field, or trigger a dashboard-level provider action.

The core split is simple:

  • emdash-actions owns the admin UI trigger surfaces.
  • Provider plugins own the backend work behind those triggers.

That means a provider can be native, standard trusted, or sandboxed, as long as it exposes normal EmDash plugin API routes.

Install

npm install @bnomei/emdash-actions

Quick Start

This quick start wires a single dashboard button that runs backend TypeScript in an EmDash provider route. The example action clears a cache placeholder, writes plugin-local state with ctx.kv, logs through ctx.log, and returns feedback to the clicked button.

1. Create A Provider

Create src/emdash/cache-actions.ts:

import { definePlugin, type RouteContext } from "emdash";
import { defineActionsManifest, type ActionsManifest } from "@bnomei/emdash-actions";

export function cacheActions() {
  return definePlugin({
    id: "cache-actions",
    version: "0.1.0",
    routes: {
      actions: {
        handler: cacheActionsManifestRoute,
      },
      "cache/clear": {
        handler: clearCacheRoute,
      },
    },
  });
}

async function cacheActionsManifestRoute(): Promise<ActionsManifest> {
  return defineActionsManifest({
    actions: [
      {
        id: "cache.clear",
        label: "Clear cache",
        route: "cache/clear",
        method: "POST",
        placement: "dashboard",
        icon: "bolt",
        tone: "warning",
        confirm: "Clear the site cache?",
      },
    ],
  });
}

async function clearCacheRoute(ctx: RouteContext) {
  const serverTime = new Date().toISOString();
  const clearCount = ((await ctx.kv.get<number>("state:clearCount")) ?? 0) + 1;

  await clearSiteCache(ctx);
  await ctx.kv.set("state:clearCount", clearCount);

  ctx.log.info("Cache clear action completed", {
    clearCount,
    site: ctx.site.name,
    serverTime,
  });

  return {
    ok: true,
    status: 200,
    severity: "success",
    message: `Cache cleared on the server at ${serverTime}.`,
    toast: {
      type: "success",
      title: "Cache cleared",
      message: `${ctx.site.name} cache has been cleared ${clearCount} time(s).`,
    },
  };
}

async function clearSiteCache(ctx: RouteContext) {
  // Replace this with your cache provider, CDN, build, or deployment logic.
  await ctx.kv.set("state:lastClearedAt", new Date().toISOString());
}

2. Register The UI And Provider

Add actionsPlugin() and the provider plugin to the EmDash plugin list in astro.config.mjs:

import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { actionsPlugin } from "@bnomei/emdash-actions";
import { cacheActions } from "./src/emdash/cache-actions";

const cacheActionsProvider = {
  pluginId: "cache-actions",
  label: "Cache actions",
  manifestRoute: "actions",
};

export default defineConfig({
  integrations: [
    emdash({
      plugins: [
        actionsPlugin({
          providers: [cacheActionsProvider],
        }),
        cacheActions(),
      ],
    }),
  ],
});

3. Click The Button

Open the EmDash dashboard. The Actions widget loads:

GET /_emdash/api/plugins/cache-actions/actions

It renders Clear cache. Clicking the button calls:

POST /_emdash/api/plugins/cache-actions/cache/clear

The provider route runs on the server, uses EmDash ctx, and returns the inline button feedback plus toast.

[!NOTE] This quick start uses a dashboard action because it shows the whole provider loop without needing collection schema changes. Field buttons use the same provider route model.

What To Use

| Goal | Start here | | --------------------------------------------------- | -------------------------------------------------------------------- | | Copy a value without backend code | Clipboard Field Button | | Call one backend route from a field | Direct Route Field Action | | Use provider-owned action UI and routes | Manifest Field Action | | Use one safe provider-owned runner route | Runner Field Action | | Add a runner-backed dashboard action | Runner Dashboard Action | | Collect a few scalar inputs before submit | Inline Form Action | | Target a host-provided nested row | Row Target Action | | Add a dashboard action | Dashboard Action | | Return clipboard, open, download, or reload effects | Response effect examples | | Show a Kumo toast | Toast Notification | | Patch a clicked button after success | Action Patch | | Poll long-running work | Async Job | | Run the provider in a sandbox | Sandboxed Provider |

The full recipe index is in examples.

Core Concepts

Surfaces

  • actions:button: field widget for content forms. It supports clipboard mode for browser-native copying and run mode for provider-backed actions.
  • Actions: dashboard widget for provider/global actions. It reads configured provider manifests and renders matching action buttons.

Action Labels And Icons

Every action must have a human-readable label. The action label is the user-facing command text for the rendered button. icon is optional decoration only; it must not be the only visible affordance for the action.

Field labels and action labels describe different things. The collection field label names the field or slot, while the manifest action label or field options.label names the command the button runs. They may match, but the button still renders the resolved action label as visible text.

Idle buttons show the resolved action label. The button title, tooltip text, and aria-label use that same resolved label. Feedback, progress, success, and error text may temporarily replace or supplement the label after interaction, but the idle state should always remain clear.

Providers And Manifests

A provider manifest describes buttons:

{
  actions: [
    {
      id: "cache.clear",
      label: "Clear cache",
      route: "cache/clear",
      method: "POST",
      placement: "dashboard",
    },
  ],
}

Use clear labels for every action surface:

{
  actions: [
    {
      id: "cache.clear",
      label: "Clear cache",
      route: "cache/clear",
      method: "POST",
      placement: "dashboard",
      target: { surfaces: ["dashboard"] },
      icon: "bolt",
    },
    {
      id: "entry.rebuild",
      label: "Rebuild entry",
      runner: { route: "actions/run-entry" },
      placement: "entry",
      target: { surfaces: ["entry"], idFrom: "entryId" },
      icon: "refresh",
    },
    {
      id: "field.summarize",
      label: "Summarize field",
      runner: true,
      placement: "field",
      target: { surfaces: ["field"], idFrom: "entryId" },
      icon: "bolt",
    },
    {
      id: "row.translate",
      label: "Translate row",
      runner: true,
      placement: "field",
      target: { surfaces: ["row"], idFrom: "rowId" },
      icon: "repeat",
    },
  ],
}

emdash-actions loads the manifest from a provider route such as:

GET /_emdash/api/plugins/cache-actions/actions

Clicking the action calls the manifest route target:

POST /_emdash/api/plugins/cache-actions/cache/clear

Field buttons can also skip the manifest and call a direct route from field options. Dashboard discovery uses actionsPlugin({ providers }).

Manifest actions can also use runner mode:

{
  actions: [
    {
      id: "field.summarize",
      runner: true,
      label: "Summarize",
      placement: "field",
      target: { surfaces: ["field"], idFrom: "entryId" },
      payload: { format: "short" },
      form: {
        mode: "inline",
        fields: [{ name: "format", type: "select", options: ["short", "long"] }],
      },
    },
  ],
}

Runner actions always call the provider-owned runner route, defaulting to:

POST /_emdash/api/plugins/<provider>/.well-known/actions/run

with a normalized invocation:

{
  "invocationId": "inv_...",
  "actionId": "field.summarize",
  "payload": {
    "format": "long"
  },
  "target": {
    "type": "field",
    "surface": "field",
    "collection": "posts",
    "entryId": "post-1",
    "fieldName": "summary",
    "value": "Current field value"
  }
}

Use direct route mode when the browser should call one explicit provider route such as field/slugify. Use runner mode when the provider should keep a fixed server-side action registry and avoid exposing one callable route per button. Runner providers must treat actionId as an identifier only, look it up in that registry, authorize it server-side, and re-read target documents before mutating content or protected state.

Runner actions may use the provider default runner route or override it per action:

{ id: "entry.rebuild", runner: { route: "actions/run-entry" }, label: "Rebuild" }

payload is only static action input defaults. Inline form values and field option payload values are merged into the request payload at submit time, and user-provided form values win over defaults. context and target stay top-level in runner invocations.

target.idFrom and target.idKeys can produce client-side missing-target warnings before a request is sent. They are ergonomics only: provider runners must re-read and validate the authoritative target server-side.

[!IMPORTANT] Field JSON belongs in the target collection schema's fields array. It does not go inside actionsPlugin() or astro.config.mjs.

[!WARNING] Provider routes must be relative plugin routes such as cache/clear or .well-known/actions. Absolute URLs, query strings, hashes, encoded paths, traversal segments, spaces, and backslashes are rejected.

Action Responses

Provider routes return JSON that controls the clicked button and optional browser effects:

  • message, severity, color, backgroundColor, and borderColor drive temporary inline feedback.
  • action patches the stable clicked button descriptor, useful for toggles.
  • effects or top-level aliases can run clipboard, open, download, and reload.
  • effects.reload accepts true or { scope, delayMs }, where scope is field, entry, dashboard, or page. Scoped reloads dispatch the cancelable emdash-actions:reload browser event so hosts can refresh a narrower surface; if unhandled, they fall back to a page reload.
  • toast shows a Kumo toast.
  • status: 202, jobStatus, and statusRoute start async polling.
  • An action result body with status: 409, severity: "warning", and a reload effect is the canonical stale-target conflict result. Return this as the normal JSON action response body so the widget can run result effects.
  • resultValueKey on a field button can write a returned value back into the field.

See the focused response recipes for complete payloads: Clipboard Effect, Open Effect, Download Effect, Toast Notification, Feedback And Colors, Action Patch, and Async Job.

Entry Context

Field buttons can use inferred entry context. For direct-route actions, contextKey and contextValueKey merge that context into the flat request payload for compatibility. Runner actions keep the same context top-level in the ActionInvocation body instead of hiding it inside payload.

Without a host-provided field-widget context, the plugin infers what it can from the admin route and saved EmDash APIs: collection, entry id, new/edit state, field name, current field value, saved entry data, and current user when available.

[!WARNING] Entry context is best-effort. It cannot infer live unsaved values from sibling fields. Pass the current field value with valueKey, use static payload, or have the provider read the latest saved entry server-side.

Provider Formats

emdash-actions is a native EmDash UI plugin and belongs in plugins: []. Provider plugins can use different formats:

  • Native providers use definePlugin() from emdash and receive one merged RouteContext argument.
  • Standard trusted providers can expose the same manifest and routes in plugins: [].
  • Standard sandboxed providers export a SandboxedPlugin route map, run in sandboxed: [], and receive (routeCtx, ctx).

See Sandboxed Provider for the sandboxed shape and caveats.

Configuration At A Glance

actionsPlugin() Options

  • providers: provider plugins the dashboard widget should discover.
  • placement: dashboard manifest placement to render. Defaults to dashboard. Set to null to show all placements.
  • title: dashboard widget title. Defaults to Actions.
  • size: dashboard widget size, full, half, or third.
  • entrypoint and adminEntry: advanced descriptor overrides.

Provider Options

  • pluginId: provider plugin id.
  • label: human-readable provider label. This names the provider, not an action button.
  • manifestRoute: provider route that returns the manifest. Defaults to .well-known/actions.
  • runnerRoute: provider route for runner actions. Defaults to .well-known/actions/run.
  • allowedTargetPluginIds: cross-plugin route targets this provider may call.

Field Button Options

The most common field options are:

  • mode: clipboard or run.
  • provider or pluginId: provider plugin id.
  • route: direct provider route.
  • action: provider manifest action id.
  • label: field-local button command text. With action, this overrides the manifest action label for this field. Without it, manifest-backed buttons use the manifest action label.
  • description, icon, tone, confirm: button UI. icon is optional decoration and the text label still renders.
  • payload: static JSON body values.
  • valueKey: include the current field value in the request body.
  • contextKey and contextValueKey: include inferred context in direct-route payloads. Runner actions receive context top-level.
  • resultValueKey: write a returned result value back into the field.
  • clipboardText, clipboardValueKey, clipboardContextValueKey, clipboardSuccess: clipboard mode.
  • feedback, buttonStyle, cooldownMs: temporary feedback and styling.
  • resultEffect, pollIntervalMs, pollTimeoutMs: response shortcuts and async polling.

The exported TypeScript contracts live in src/types.ts.

Special Cases

[!NOTE] Clipboard mode uses navigator.clipboard.writeText() in the browser. It does not call the backend, and browsers require HTTPS or localhost plus clipboard permission.

[!CAUTION] Use confirm for destructive dashboard actions such as cache clearing, maintenance toggles, deploys, exports, or purge operations.

  • Direct field routes can specify options.provider and options.route in the field JSON. They do not need to be listed in actionsPlugin({ providers }) unless the dashboard should discover them too.
  • Cross-plugin target routes require allowedTargetPluginIds; otherwise a provider manifest can only target its own plugin id.
  • DELETE actions do not receive a JSON body, so payload, value, and context keys are useful with POST, PUT, and PATCH.
  • Protected downloads can use effects.download.route to fetch through the action target plugin with EmDash auth headers. Sandboxed providers should prefer public or signed URLs for binary downloads, or delegate streaming to a trusted native route.
  • Async jobs need a provider-owned statusRoute. Without one, the widget can show that work was accepted but cannot infer queued, running, failed, or completed state.

Development

npm ci
npm run check
npm run typecheck
npm run build
npm run pack:check