npm.io
0.5.0 • Published 1 week ago

@spacefast/zero-compat-emdash

Licence
MIT
Version
0.5.0
Deps
2
Size
75 kB
Vulns
0
Weekly
0

@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).