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

@spacefast/zero-compat-emdash

v0.4.1

Published

Translates an EmDash CMS project into a Spacefast Zero content model, entirely on the CLI side.

Readme

@spacefast/zero-compat-emdash

Translates an EmDash project into Spacefast Zero content.

This is a CLI-side translation, not a server-side adapter. Spacefast runs Zero and only Zero; nothing here ships to the platform, and there is no EmDash compatibility layer at runtime. You point it at an EmDash seed, you get Zero content and a report of exactly what happened to your site.

Two emit targets; the CLI takes the authored one. sf zero import emdash reads translateEmDashSeed's mapped model and lets @spacefast/zero-compile print the capsule content module the author edits next — that authoring lane has landed. This package's own translateEmDashProject still emits the compiled spacefast.zero-wordpress ContentModelRelease plus the Markdown files its sync bindings name, for callers that want the build output directly. The split is what makes carrying both cheap: translate.ts maps an EmDash seed onto a source-agnostic model and knows nothing about either target; only emit-content-model.ts is about the compiled one.

Input

An EmDash site declares its whole schema and optional content in one seed document, resolved from .emdash/seed.json, package.json#emdash.seed, or seed/seed.json. Export a whole project with:

npx emdash export-seed --with-content > seed.json

Use

import { readFile } from "node:fs/promises";
import { formatTranslationReport, translateEmDashProject } from "@spacefast/zero-compat-emdash";

const seed = JSON.parse(await readFile("seed.json", "utf8"));
const { contentModel, files, report } = await translateEmDashProject(seed);

console.log(formatTranslationReport(report));
for (const [path, markdown] of Object.entries(files)) {
  await writeFile(path, markdown);
}

contentModel is already validated against contentModelReleaseV1Schema, revision included. files are the repo Markdown files, keyed by the exact path each sync binding names.

The report

Silent loss is the one thing a compatibility translator must never do, so every part of the source project leaves a record:

| status | meaning | | ------------ | ---------------------------------------------------------------------------------------- | | mapped | carried over with its meaning intact | | renamed | carried over under a different name, because the original is not a legal Zero identifier | | downgraded | carried over, but the target expresses less than the source did | | refused | not carried over; Zero has no counterpart |

A translation is faithful exactly to the extent the refused list is empty. It usually is not, and that is the useful part.

What carries over

| EmDash field type | Zero | note | | ----------------- | ---------------------------- | ------------------------------------------ | | string, url | text (single line) | | | text | text (multiline) | | | number | number | | | integer | number | downgraded — integrality is lost | | boolean | boolean | | | datetime | datetime | | | select | enum | refused when the field declares no options | | multiSelect | enum (multiple) | refused when the field declares no options | | portableText | blocks + a Markdown source | | | image, file | media | | | reference | reference | refused when the target is not declared | | slug | WordPress post slug | native, so not a content model field | | json | — | refused — needs a Lakebed table | | repeater | — | refused |

A WordPress content model always declares exactly one posts, pages, and media resource. The seed's own posts and pages collections are adopted onto the natives; any that the seed does not declare are synthesized. A seed collection named media is refused — the media library is the platform's.

Every translated collection gets list and get queries and a save mutation, so the SDK's invalidation graph is real: save writes the resource its own reads read. Synthesized natives the seed never declared stay read-only.

What does not

Refused with a reason, never dropped quietly: menus, widgetAreas, sections, redirects, bylines, taxonomies, settings, and any localization (defaultLocale, per-entry locale, translationOf). None have a counterpart in a Zero content model.

Content data is the subtle one. A ContentModelRelease describes a content schema; the only content it carries is Markdown reached through a sync binding. So an entry's portableText field becomes content/<collection>/<slug>.md (or content/<collection>/<slug>.<field>.md when the collection declares more than one), and every other value on that entry — title, excerpt, featured_image, taxonomy assignments, bylines, draft status — is refused by name. Importing that data is a separate job from compiling a content model.

Portable Text

Rich text becomes Markdown, because Markdown is what a sync binding reconciles against a WordPress block field. The WordPress php-toolkit owns Markdown-to-blocks and back; nothing here produces blocks directly.

Paragraphs, h1–h6, blockquotes, bullet and numbered lists, strong, em, code, strike-through, links, and image nodes convert. Anything else — a custom node type, an unknown decorator, an unresolvable mark definition — is refused by name. Real EmDash templates ship custom node types (marketing.hero and friends), and a converter that quietly flattened one into its text would corrupt a page while reporting success.

A full Portable Text converter, including custom node types mapped onto registered Zero components, is follow-up work.

Fixture

src/fixtures/emdash-blog-seed.json is the unmodified templates/blog/seed/seed.json from emdash-cms/emdash at commit 1717d31b351164a5f78e95fe004ee582c7c50f40 (MIT).