Skip to content

Bundling and publishing

Once your sandboxed plugin works, you can publish it to the EmDash marketplace so other sites can install it from the admin dashboard with one click. The publishing flow is sandboxed-only — native plugins distribute via npm and aren’t bundled into marketplace tarballs.

Before publishing, make sure your plugin:

  • Has a package.json with both a "." export (the descriptor) and a "./sandbox" export (the runtime entry).
  • Uses format: "standard" on the descriptor.
  • Has a unique id and a valid semver version.
  • Declares its capabilities and allowedHosts accurately on the descriptor.

Published plugins are distributed as .tar.gz tarballs containing:

FileRequiredDescription
manifest.jsonYesPlugin metadata extracted from the descriptor and sandbox entry
backend.jsYesBundled sandbox code (self-contained ES module)
admin.jsNoBundled admin UI code (only if Block Kit interactions ship JS)
README.mdNoPlugin documentation
icon.pngNoPlugin icon (256×256 PNG)
screenshots/NoUp to 5 screenshots (PNG/JPEG, max 1920×1080)

manifest.json is generated automatically. It contains the plugin id, version, capabilities, allowed hosts, hook names, route names, and admin configuration — but no executable code.

Terminal window
cd packages/plugins/my-plugin
emdash plugin bundle

This will:

  1. Read your package.json to find entrypoints.
  2. Build the descriptor entry to extract id, version, capabilities, and admin config.
  3. Bundle backend.js from the "./sandbox" export — minified, tree-shaken, fully self-contained.
  4. Bundle admin.js if a "./admin" export exists.
  5. Collect assets (README, icon, screenshots).
  6. Validate the bundle (size limits, no Node.js built-ins in backend.js, capability checks).
  7. Write {id}-{version}.tar.gz to dist/.

The bundle command finds your code through package.json exports:

package.json
{
"exports": {
".": { "import": "./dist/index.mjs" },
"./sandbox": { "import": "./dist/sandbox-entry.mjs" },
"./admin": { "import": "./dist/admin.mjs" }
}
}
ExportPurposeBuilt as
"."Descriptor — used to extract the manifestExternals: emdash, @emdash-cms/*
"./sandbox"Runtime code (hooks, routes) executed in the sandboxFully self-contained (no externals)
"./admin"Admin UI components (only if you ship them)Fully self-contained

If "./sandbox" is missing, the command looks for src/sandbox-entry.ts as a fallback. The bundler maps dist paths back to source automatically — if your "." export points to ./dist/index.mjs, it will find and build src/index.ts.

Terminal window
emdash plugin bundle [--dir <path>] [--outDir <path>]
FlagDefaultDescription
--dirCurrent directoryPlugin source directory
--outDir, -odistOutput directory for the tarball

The bundle command checks:

  • Size limit — total bundle must be under 5 MB.
  • No Node.js built-ins in backend.js — sandbox code can’t import fs, path, child_process, etc. Replace them with Web APIs or move the logic to a native plugin.
  • Capability allowlist — declared capabilities must be in the known set (typos fail).
  • Deprecated capability names trigger warnings here and a hard fail at publish time.
  • network:request without allowedHosts triggers a warning (consider network:request:unrestricted if hosts are operator-configured at runtime, or list the hosts explicitly).
  • Icon dimensionsicon.png should be 256×256 (warns if wrong; still includes it).
  • Screenshot limits — max 5 screenshots, max 1920×1080.
Terminal window
emdash plugin publish

This finds the most recent .tar.gz in dist/ and uploads it. To be explicit about the tarball or to build before publishing:

Terminal window
# Explicit tarball path
emdash plugin publish --tarball dist/my-plugin-1.0.0.tar.gz
# Build first, then publish
emdash plugin publish --build

The first time you publish, the CLI authenticates you via GitHub:

  1. The CLI opens GitHub’s device authorization page in your browser.
  2. You enter the code displayed in your terminal.
  3. GitHub issues an access token.
  4. The CLI exchanges it for a marketplace JWT (stored in ~/.config/emdash/auth.json).

The token lasts 30 days. After it expires you’ll be prompted to re-authenticate on the next publish.

You can manage authentication separately:

Terminal window
emdash plugin login # log in without publishing
emdash plugin logout # clear stored token

If your plugin id isn’t yet known to the marketplace, emdash plugin publish registers it automatically before uploading the first version.

Each published version must have a higher semver than the last. You can’t overwrite or republish an existing version — bump the version in both package.json and the descriptor before publishing again.

Every published version goes through an automated security audit. The audit scans backend.js and admin.js for:

  • Data exfiltration patterns
  • Credential harvesting via settings
  • Obfuscated code
  • Resource abuse (crypto mining, etc.)
  • Suspicious network activity

The audit produces a verdict of pass, warn, or fail, displayed on the plugin’s marketplace listing. Depending on the marketplace’s enforcement level, a fail verdict may block publication entirely.

Terminal window
emdash plugin publish [--tarball <path>] [--build] [--dir <path>] [--registry <url>]
FlagDefaultDescription
--tarballLatest .tar.gz in dist/Explicit tarball path
--buildfalseRun emdash plugin bundle before publishing
--dirCurrent directoryPlugin directory (used with --build)
--registryhttps://marketplace.emdashcms.comMarketplace URL

Typical publish cycle:

Terminal window
# 1. Make your changes
# 2. Bump the version in src/index.ts and package.json
# 3. Bundle and publish
emdash plugin publish --build

If you want to inspect the bundle first:

Terminal window
emdash plugin bundle
tar tzf dist/my-plugin-1.1.0.tar.gz
emdash plugin publish