@womp/message-format
v0.1.1
Published
Isomorphic message rich-text layer for Womp: DraftJS <-> TipTap/ProseMirror converters, self-describing format detection, and the one shared editor schema. Consumed by swan-frontend, swan-backend, and swan-console.
Keywords
Readme
@womp/message-format
Isomorphic rich-text format layer for Womp messages. Shared by swan-frontend, swan-backend, and swan-console so the DraftJS → TipTap migration is a pure content-string concern (no schema/API changes).
IMessage.content is a single opaque string. Legacy messages store a bare
JSON.stringify(convertToRaw(...)) DraftJS RawDraftContentState. This package
lets every consumer read either format and convert between them.
Phase 0 (this package)
Phase 0 builds the isomorphic core only — no consumer is wired up yet, no editor exists, no write path flips. Exit criterion: a green Jest suite proving the converters, detection, and schema validity are correct.
Two entry points
The package root is TipTap-free — safe to import anywhere, including the
backend and console where @tiptap/* is not installed:
import { detectFormat, draftRawToProseMirror, getMentionIds } from "@womp/message-format";The TipTap schema lives behind a separate subpath and is the only thing that needs the editor runtime (declared as an optional peer dependency):
import { createMessageExtensions } from "@womp/message-format/editor";API
| Export (root) | Purpose |
| --- | --- |
| detectFormat(content) | Classify a content string: envelope | draft | tiptap | empty | plaintext. Never throws. The envelope result is a discriminated union (format↔doc correlated). A declared envelope format is trusted only if the doc's shape matches. |
| draftRawToProseMirror(raw) | DraftJS RawDraftContentState → ProseMirror JSON. Output is always schema-valid (verified in tests against the real schema). |
| proseMirrorToDraftRaw(doc) | ProseMirror JSON → DraftJS raw. Writes mentions in the nested { mention: {...} } shape the existing readers expect. |
| getPlainText / getMentionIds / isEmpty | Format-agnostic helpers, robust to malformed nodes. getMentionIds is the shared replacement for the backend's getUserIdsInMention. |
| wrapEnvelope / isEnvelope / serializeEnvelope | The self-describing { v, format, doc } envelope. |
| Export (/editor) | Purpose |
| --- | --- |
| createMessageExtensions(opts?) | THE shared TipTap extension set / PM schema (StarterKit + Image + user mention + custom channelMention). |
Conversion coverage (from the whole-table scan, 2026-07-07)
Blocks: unstyled→paragraph, code-block→codeBlock, unordered-list-item→bulletList, ordered-list-item→orderedList (with depth-aware nesting), atomic→image.
Marks: BOLD/ITALIC/UNDERLINE (StarterKit provides all three + link).
Entities: mention→user mention node, #mention→channelMention node, LINK→link mark, IMAGE→image (block-level; inline IMAGEs are lifted to sibling blocks so the doc stays schema-valid), emoji→unicode text, UploadPlaceholder→dropped. Soft newlines (\n) ↔ hardBreak.
Mention data shape: entity data is NESTED — data.mention.{id,name,avatar?/title?} (written by @draft-js-plugins/mention), matching getUserIdsInMention (backend) and NewMessageSpeech (FE). avatar/title are preserved as node attrs for lossless round-trips.
Intentionally lossy (excluded from round-trip guarantees): emoji (collapsed
to its unicode char, no reverse entity) and UploadPlaceholder (dropped).
Everything else round-trips PM → draft → PM losslessly. code-block inline
styles and block-level data (e.g. text-align) are dropped — the current PM
schema has no representation for them.
Scripts
npm install
npm test # Jest — the Phase 0 exit criterion (incl. real-schema validation)
npm run typecheck # tsc --noEmit (strict + noUncheckedIndexedAccess)
npm run build # emit dist/ (CJS + d.ts) for both entry pointsThe pure-core test files import no @tiptap runtime; schema-validation.test.ts
does (via /editor) to prove every converter output is schema-valid.
Publishing
Published to npm as @womp/message-format (public) via GitHub Actions
trusted publishing (OIDC) — same mechanism as @womp/kookie-ui, no
NPM_TOKEN. A one-time trusted publisher is configured on npm for repo
wompxyz/message-format + workflow publish.yml.
Cut a release — bump, tag, and push in one step (each runs the
typecheck + test gate first via the preversion hook):
npm run release:patch # 0.1.0 -> 0.1.1
npm run release:minor # 0.1.0 -> 0.2.0
npm run release:major # 0.1.0 -> 1.0.0Each command bumps package.json, commits, creates a vX.Y.Z git tag, and
runs git push --follow-tags. The pushed v* tag triggers
.github/workflows/publish.yml, which derives
the version from the tag, re-runs typecheck + test + build, and publishes —
skipping if that version already exists on npm (so re-running a tag is safe).
Prefer to tag by hand? git tag v0.2.0 && git push origin v0.2.0 works too, as
does the workflow's manual Run workflow button (workflow_dispatch) with a
version input. The committed version in package.json is only a placeholder —
the tag is the source of truth for what gets published.
Not in Phase 0
FE read-site adoption (Phase 1), backend/console tolerance + gc flag (Phase 2),
the TipTap editor (Phase 3), the write flip (Phase 4), backfill + dropping
draft-js (Phase 5).
Packaging follow-ups for Phase 1 (before wiring consumers)
- ESM build. Currently ships CJS only (
main/exports.default). Add an ESM build +import/requireconditions inexportsfor cleanest vite (FE/console) consumption. - swan-console
convertFromRawis unguarded (Messages.tsx) — it will throw on a TipTap payload. Must adoptdetectFormatbefore any write flip (Phase 2). - Whether channel
#mentions should notify remains a product call (getMentionIdsgates it behindincludeChannels; they don't notify today).
