Technical handoff · for reviewers · Email Editor v1 (Linear)

Notification Email V2 — visual block editor

Package @avada/email-editor (block-tree → MJML → HTML, một renderTree dùng cho cả preview & send) + tích hợp vào Joy admin, gate theo flag để khách V1 byte-identical.

feature/email-editor-impl ~100 commits · 249 files · +35.9k typecheck 0 · suite 1079 green

1Tóm tắt

Block-tree JSON (emailDocV2) là source-of-truth ta sở hữu (anti-lock-in). Một renderTree(doc, ctx, registry) dựng block → MJML JSON-AST → mjml2html → email HTML, chạy cho cả preview lẫn send (chỉ khác merge-tag mode). Tích hợp host gate theo shop.useEmailEditorV2; nhánh send cắm vào đúng chokepoint getEmailDatatái dùng bộ resolver Liquid sẵn có của Joy (prepareBodyHtml, liquid@4) → khách V1 không đổi một ký tự.

2Architecture & data flow

Send path (preview tương tự, chỉ khác resolver):

emailDocV2 (block tree)
Field trên notification doc (Firestore .update merge, non-destructive).
getEmailData · gate
useV2 = shop.useEmailEditorV2 && emailDocV2?.root?.length
renderTree → MJML → HTML
tag giữ raw (identity resolver); React-free server entry.
prepareBodyHtml (Liquid)
liquid@4 resolve {{tag}}+{% if %} bằng CÙNG variable map V1.
Mailgun
try/catch → lỗi render thì fallback V1.

Quyết định thiết kế

Quyết địnhChọn gìVì sao (vs phương án khác)
Source-of-truthBlock-tree JSON ta tự định nghĩaAnti-lock-in: không phụ thuộc 1 render engine; swap render không vỡ model.
Render preview vs sendMột renderTreePreview = send → không drift; chỉ khác merge-tag mode (sample vs real/raw).
React trong send bundleTách toMjml ra *.tomjml.ts React-free + createServerRegistryFunctions Node bundle KHÔNG kéo React/Tiptap (esbuild metafile = 0). Lazy-import vô dụng (esbuild vẫn bundle).
Điểm gate send1 nhánh if tại getEmailData, reuse variable map sẵn có1 chokepoint phủ cả send thật + test; nhánh V1 byte-identical; KHÔNG phải sửa ~20 publisher.
Resolve merge-tag/LiquidQua prepareBodyHtml (liquid@4) của Joy, tag giữ raw tới đóDesign note: Joy resolve Liquid server-side (KHÔNG phải Mailgun) — để raw sẽ lọt {{tag}} ra email. Verified end-to-end (mjml giữ nguyên cú pháp Liquid incl >).
Liquid trong preview (canvas)liquidjs (browser, sync) với sample varsliquid@4 không có sync API (canvas resolveMergeTags là sync). Subset Liquid khớp spec Shopify → drift thấp; preview giờ khớp send.
mjml bundle size (functions)esbuild alias cosmiconfig→noopmjml kéo cosmiconfig→typescript 8.7MB; noop (config-loader không chạy trên inline-options path) → bundle 14.5→4.7MB, react=0 giữ nguyên.

3Files changed (high-level, theo layer)

Khu vựcVai trò
packages/email-editor/src/{model,state,persistence}Block-tree TreeOps (immutable) + EditorStore (command/undo) + PersistenceAdapter + migrate V1→V2 (tolerant loader).
src/blocks (17× block + *.tomjml)Mỗi block: fields + renderEditor (canvas) + toMjml (output). Split React-free toMjml + createServerRegistry cho send.
src/dnd, src/uiDrag-drop (dnd-kit), selection/overlay/resize, canvas + khung inbox-simulation.
src/inspector, src/textInspector tự sinh từ FieldSchema; rich-text (Tiptap) cho block Text, per-locale.
src/render-mjml, src/merge-tagsrenderTree + server.ts (React-free entry); resolver (vocab 60 tag, preview=liquidjs, send=identity).
src/labelsCơ chế i18n UI-label host-overridable (English-source).
packages/assets/src/pages/EmailEditorV2Host: mount flag-gated, persist emailDocV2, save UX, Settings panel (Performance/Trigger/Subject/Sender), Send-test.
functions/.../subscribeSentEmailNotification.jsSend gate trong getEmailData (V2 render + Liquid + fail-safe; nhánh V1 giữ nguyên).
functions notificationController.js · esbuild.config.js · package.jsonsaveEmailDocV2 (persist), esbuild cosmiconfig-noop alias, mjml dep.

4Edge cases & verify

CaseExpectedVerify
Flag off / không có emailDocV2Đi đúng đường V1, byte-identical reviewer xác nhận diff: nhánh V1 char-identical, gate eval sau khi variable build.
V2 render throwFallback V1, không mất mail try/catch quanh toàn bộ V2 render → useV2=false.
Send bundle React-free0 react/tiptap/prosemirror + mjml chạy esbuild metafile trên server.ts + smoke render từ bundle.
Migrate V1 → V2Không mất content; unknown → passthrough fuzz/property tests (tolerant loader).
Preview Liquid lỗi cú phápFallback regex, canvas không crash unit test malformed → no-throw.
Escape boundary (XSS)Resolver trả raw, escape 1 lần ở block layer unit pin {{evil}}→raw + integration escape-once.
Gửi V2 thậtEmail tới inbox, block + tag + Liquid đúng Send-test → inbox thật (dev). Full live-send QA ở staging/UAT.

5Follow-ups & known limitations