Technical handoff · for reviewers · Email Editor v1 (Linear)
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
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 getEmailData và tá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ự.
Send path (preview tương tự, chỉ khác resolver):
.update merge, non-destructive).useV2 = shop.useEmailEditorV2 && emailDocV2?.root?.length{{tag}}+{% if %} bằng CÙNG variable map V1.| Quyết định | Chọn gì | Vì sao (vs phương án khác) |
|---|---|---|
| Source-of-truth | Block-tree JSON ta tự định nghĩa | Anti-lock-in: không phụ thuộc 1 render engine; swap render không vỡ model. |
| Render preview vs send | Một renderTree | Preview = send → không drift; chỉ khác merge-tag mode (sample vs real/raw). |
| React trong send bundle | Tách toMjml ra *.tomjml.ts React-free + createServerRegistry | Functions Node bundle KHÔNG kéo React/Tiptap (esbuild metafile = 0). Lazy-import vô dụng (esbuild vẫn bundle). |
| Điểm gate send | 1 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/Liquid | Qua 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 vars | liquid@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→noop | mjml 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. |
| Khu vực | Vai 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/ui | Drag-drop (dnd-kit), selection/overlay/resize, canvas + khung inbox-simulation. |
src/inspector, src/text | Inspector tự sinh từ FieldSchema; rich-text (Tiptap) cho block Text, per-locale. |
src/render-mjml, src/merge-tags | renderTree + server.ts (React-free entry); resolver (vocab 60 tag, preview=liquidjs, send=identity). |
src/labels | Cơ chế i18n UI-label host-overridable (English-source). |
packages/assets/src/pages/EmailEditorV2 | Host: mount flag-gated, persist emailDocV2, save UX, Settings panel (Performance/Trigger/Subject/Sender), Send-test. |
functions/.../subscribeSentEmailNotification.js | Send gate trong getEmailData (V2 render + Liquid + fail-safe; nhánh V1 giữ nguyên). |
functions notificationController.js · esbuild.config.js · package.json | saveEmailDocV2 (persist), esbuild cosmiconfig-noop alias, mjml dep. |
| Case | Expected | Verify |
|---|---|---|
| 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 throw | Fallback V1, không mất mail | ✓ try/catch quanh toàn bộ V2 render → useV2=false. |
| Send bundle React-free | 0 react/tiptap/prosemirror + mjml chạy | ✓ esbuild metafile trên server.ts + smoke render từ bundle. |
| Migrate V1 → V2 | Không mất content; unknown → passthrough | ✓ fuzz/property tests (tolerant loader). |
| Preview Liquid lỗi cú pháp | Fallback 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ật | Email tới inbox, block + tag + Liquid đúng | ✓ Send-test → inbox thật (dev). Full live-send QA ở staging/UAT. |
customer.locale (hiện dùng shop.primaryLanguage); chờ task locale chung của Joy deferred.