Skip to content

@unlayer/migrate: MJML and Beefree importers (--from mjml|beefree) - #70

Open
ivoIturrieta wants to merge 6 commits into
feat/react-elements-0.3from
feat/migrate-mjml-beefree
Open

ivoIturrieta wants to merge 6 commits into
feat/react-elements-0.3from
feat/migrate-mjml-beefree

Conversation

@ivoIturrieta

@ivoIturrieta ivoIturrieta commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on the 0.3.0 library PR (#69), since it uses @unlayer/react-elements/codegen: it shows only the importer changes. Merge it after #69, with its base changed to main first (GitHub does that itself when #69's branch is deleted on merge).

The repo's tests only run on pull requests into main, so CI starts here once the base is main (a push after that triggers it). Until then, the validation below was run locally on the branch.

What

npx @unlayer/migrate <dir> --from mjml and --from beefree, with the same flow as --from react-email:

  • convert, check against the original, report;
  • --write / --out / --design / --report / --strict, the same git-safety and collision rules, the same ✓/✗ output and exit codes.

The source files stay in place, and <name>.tsx is written beside each one.

Both converters are pure source → Unlayer design functions (src/from-mjml, src/from-beefree). The library's codegen writes the design as Elements code, and --design writes the design itself. MJML and Beefree templates carry no program logic, so nothing is lost by going through the design. The same functions could later power editor imports.

MJML (mjml pinned at 5.4.1)

  • mj-attributes, mj-class and mj-include are resolved. An include it can't read (outside the folder given) fails the template, naming it: its check can't see what's missing.
  • These become editable blocks: sections, columns, text, buttons, images, dividers, spacers, social (as Social blocks), navbar and simple tables.
  • Hero overlays, carousels and accordions stay as MJML's own HTML. They're counted separately as not editable, so they don't inflate ✓.
  • MJML 3 files fail with a pointer to mjml-migrate.
  • Checked against MJML's own output.

Beefree

  • Classic JSON in its known wrappers and versions. An unknown version fails loudly.
  • Rows, columns and modules map to blocks.
  • Checked against Beefree's HTML export when <name>.html sits beside the JSON. Otherwise the template is reported as unverified, and --strict fails it.

Declared differences (convert-core)

A converter can declare specific changes it knows it makes. The editor's Social block writes alt text as "Facebook icon", and Beefree's phone hamburger menu has no equivalent. The check expects these and lists them under "looks different" instead of flagging a converter bug. Anything undeclared still fails, and --strict still fails declared ones.

Results on real templates

MJML (114 documents) 112 ✓, 0 ✗, 96.2% of blocks editable. The other 2 don't compile in MJML either.
Beefree (107 templates) 107 converted, exact design round trip, 97.6% editable. 33 ✓ / 0 ✗ against Beefree's exports; 74 have no export.

Committed fixtures are permissively licensed (MIT, Apache-2.0), with LICENSE and NOTICE files. The rest of the corpus was used locally only.

Validation

  • migrate 361 tests and convert-core 219 pass; typecheck is clean.
  • New coverage: per-tag and per-module unit tests, fixture tests, CLI e2e for both sources, and declared-difference tests (an undeclared alt change still fails).

Release

@unlayer/migrate publishes as 0.2.0 (committed: new sources are a minor), with the Elements range ^0.3.0 set in #69.

ivoIturrieta and others added 5 commits October 15, 2026 19:09
…urpose

Some content changes are known and accepted, such as the editor's Social block writing an
icon's alt text as "Facebook icon", or Beefree's phone hamburger menu having no equivalent.
Without a way to say so, the content check reported them as a converter bug.

applyDeclared(originalHtml, declarations) applies only the named substitutions (an alt or src
on an exact image, a glyph inside a named class, an element shown or hidden) to the reference
before the unchanged content check runs, and describeDeclared lists each one as a difference.
Anything undeclared still fails; --strict still fails declared ones.

Co-Authored-By: Claude Code (Claude Opus 5.5 xhigh) <noreply@anthropic.com>
Same flow as React Email: convert, check against the original, report, and write with --write
or --out under the same git-safety, collision and exit-code rules. Both converters are pure
source → Unlayer design functions; @unlayer/react-elements/codegen writes the design as code and
--design writes the design itself.

- MJML (mjml pinned at 5.4.1): mj-attributes, mj-class and mj-include resolved; sections,
  columns, text, buttons, images, dividers, spacers, social, navbar and simple tables become
  editable blocks; hero overlays, carousels and accordions stay as MJML's own HTML, counted
  separately as not editable. MJML 3 files fail with a pointer to mjml-migrate. Checked against
  MJML's own output.
- Beefree: classic JSON in its known wrappers and versions (unknown versions fail loudly); rows,
  columns and modules map to blocks. Checked against Beefree's HTML export when a <name>.html
  sits beside the JSON, otherwise reported as unverified (--strict fails it).
- Social alt text and Beefree's hamburger menu are declared differences.

On real templates: MJML 112 of 114 ✓, 0 ✗, 96.2% of blocks editable (the other 2 don't compile in
MJML either); Beefree 107 of 107 converted, 33 ✓ and 0 ✗ against its exports. Committed fixtures
are permissively licensed, with LICENSE and NOTICE files.

Co-Authored-By: Claude Code (Claude Opus 5.5 xhigh) <noreply@anthropic.com>
…porters

New sources are a feature, so a minor: the release job publishes a
committed version ahead of npm instead of its default patch.

Co-Authored-By: Claude Code (Claude Opus 5.5 xhigh) <noreply@anthropic.com>
An mj-include outside the folder given (or one that fails to read) is
left out of the conversion, and the reference is compiled from the same
document, so the check couldn't see the missing content and passed it.
The template now fails, naming what wasn't read, in the run and the
report; it's still written, flagged, as other content failures are.

Co-Authored-By: Claude Code (Claude Opus 5.5 xhigh) <noreply@anthropic.com>
The file name went into a // comment of the code that's written and
loaded to check the template, so a name with a line break ended the
comment and ran what followed. Line breaks become spaces.

Co-Authored-By: Claude Code (Claude Opus 5.5 xhigh) <noreply@anthropic.com>
@ivoIturrieta
ivoIturrieta marked this pull request as ready for review October 16, 2026 05:16
A template that fails for an mj-include it couldn't read now says it
failed the check, not that its content differs, and the summary no
longer asks to report it as a converter bug.

Co-Authored-By: Claude Code (Claude Opus 5.5 xhigh) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant