@spacefast/zero-compat-payloadcms
v0.4.1
Published
Translates a Payload CMS config into a Spacefast Zero content model.
Readme
@spacefast/zero-compat-payloadcms
Move a Payload CMS project onto Spacefast Zero.
import { compilePayloadConfig, formatTranslationRefusals } from "@spacefast/zero-compat-payloadcms";
const result = await compilePayloadConfig(JSON.parse(await readFile("payload.json", "utf8")));
if (result.status === "refused") {
console.error(formatTranslationRefusals(result.report));
process.exit(1);
}
// result.artifacts is the content model a publish declares.Compatibility with another CMS is a translation you run once, not an adapter the
platform runs forever. This package reads a Payload config, emits the content
model Spacefast compiles into WordPress — native posts, pages and media, a
zero_collection per Payload collection, and the Secure Custom Fields that back
them — and hands back a report of what the translation cost. After that the
Payload project is gone. The platform only ever runs Zero.
What it refuses, and why that is the point
A translation that quietly drops half its input is worse than one that stops, so every construct in the config lands in exactly one report entry:
| outcome | meaning |
| --------- | ---------------------------------------------------------------------------------- |
| mapped | translated with its meaning and its name intact |
| renamed | translated, but Zero spells or enforces it differently; reason says what changed |
| ignored | read and deliberately not carried, because it holds no content |
| refused | no Zero construct holds it; reason says why and action says what to change |
One refusal withholds the whole content model. The line between renamed and
refused is content, not comfort: losing a field loses documents, so an
untranslatable field stops the run, while losing a rule about a field that
does translate — an email format, a numeric bound, a uniqueness constraint —
costs no content and is reported as a rename with the rule named.
Coverage
| Payload | Zero |
| ----------------------------------------------------------------- | --------------------------------------------------------------- |
| text, textarea | text, multiline from the type, maxLength carried |
| email, code | text — renamed; format and language hints stop being enforced |
| number, checkbox, date | number, boolean, datetime |
| richText | blocks, any component |
| select, radio | enum, multiple from hasMany |
| relationship (one target) | reference |
| upload | media |
| row, collapsible, unnamed tab | children inlined |
| group, named tab | children inlined under a name- prefix |
| ui | ignored — an admin control storing no value |
| collection posts / pages / an upload collection | adopts the native WordPress store |
| other collections | a zero_collection resource with SCF fields |
| global | a collection holding one entry |
| collection versions | WordPress post status; maxPerDoc has no equivalent |
| access.read | publicRead, and only when it serializes to a constant |
| json, point, array, blocks, join | refused |
| polymorphic relationship | refused |
| localized fields, config localization | refused |
| field or collection hooks / access / validate / endpoints | refused |
| auth collections | refused — identity is Spacefast Auth's |
| plugins | refused — they rewrite the config before Payload sees it |
Shape of the input
Payload's config is TypeScript, and payload.config.ts is executed, not parsed.
This package reads the exported JSON form, because a translator that
evaluated a customer's config would be running their server code to find out
what their content is. Executing a TS config needs a sandbox and a capability
model; it is a separate lane, not a flag on this one.
Unrecognized keys are refused rather than skipped: a key whose effect on content shape this translator does not know is exactly the silent loss the report exists to prevent.
Shape of the output
compilePayloadConfig returns the artifacts a published version declares under
sourceMetadata.statticZero.contentModel — a ContentModelReleaseV1 and
the four derived artifacts the control plane re-verifies before it stages
content-model.php. contentModelSourceMetadata wraps them for a publish.
That target is the compiled content model, not the repo authoring format. The
authoring format exists — sf zero import payloadcms calls translatePayloadConfig
and lets @spacefast/zero-compile print the capsule content module the author edits
next — and compilePayloadConfig is the other half, for callers that want the build
output the publish path would have produced. Carrying both is cheap because the
package is split: translate.ts knows Payload and produces the model in
content-model.ts, emit.ts knows the platform and nothing else.
Not yet translated
- Documents. A Payload config carries no content, so nothing is emitted
under
content/and no Markdown sync bindings are declared. Translating a data export needs a Lexical-to-Markdown pass of its own. - Abilities. A translated content model declares content and nothing that runs. Emitting a query would mean emitting the executable behind it, which is the content model compiler's job.
sfCLI command. Reach the package directly untilsf import payloadcmsexists.
