fix(email): preserve personalization tokens for template rendering
Quality / quality (ubuntu-latest) (push) Failing after 10m0s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 15:37:36 +05:30
1 parent 8bc8fe85c3
commit e1ea6e2977
5 files changed
+41 -3

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/email-renderer",
"version": "0.1.22",
"version": "0.1.23",
"type": "module",
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
"exports": {
+3
View File
@@ -110,6 +110,8 @@ export async function renderEmail(
direction?: "ltr" | "rtl";
variables?: Record<string, unknown>;
requireVariables?: boolean;
/** Preserve allowlisted tokens and fallbacks for template export, never for final delivery. */
preserveVariables?: boolean;
countdownImageUrl?: string;
/** Trusted host injection; never sourced from the editable document. */
ampValidator?: (content: string) => Promise<ValidationReport>;
@@ -118,6 +120,7 @@ export async function renderEmail(
): Promise<Result<EmailRenderBundle>> {
const personalized = resolveDocumentVariables(document, emailKind.variables, options.variables, {
requireValues: options.requireVariables,
preserveTokens: options.preserveVariables,
});
if (!personalized.ok) return personalized;
if (options.countdownImageUrl) {
@@ -0,0 +1,32 @@
import { expect, test } from "bun:test";
import { resolveDocumentVariables, type BlockDocument } from "@wrnexus/blocks";
import { renderEmail } from "../src/index.ts";
const document: BlockDocument = {
schemaVersion: 1, kind: "email", meta: { subject: "Hi {{recipientName|friend}}", lang: "en" },
theme: { tokens: {} },
root: { id: "root", type: "layout.email-document", version: 1, props: {}, styles: {}, slots: {
content: [{ id: "text", type: "content.text", version: 1, props: { text: "Hello {{recipientName|friend}}" }, styles: {}, slots: {} }],
} },
};
test("template rendering preserves fallback tokens rather than baking in sample or fallback values", async () => {
const before = structuredClone(document);
const rendered = await renderEmail(document, { preserveVariables: true, mjml: true, variables: { recipientName: "Preview only" } });
expect(rendered.ok).toBe(true);
if (!rendered.ok) return;
expect(rendered.value.subject).toBe("Hi {{recipientName|friend}}");
expect(rendered.value.parts.html.content).toContain("Hello {{recipientName|friend}}");
expect(rendered.value.parts.text.content).toContain("Hello {{recipientName|friend}}");
expect(rendered.value.exports?.mjml).toContain("Hello {{recipientName|friend}}");
expect(document).toEqual(before);
const resolved = await renderEmail(document, { variables: { recipientName: "Ada" }, requireVariables: true });
expect(resolved.ok && resolved.value.subject).toBe("Hi Ada");
});
test("preservation still validates token allowlists and cannot masquerade as resolved delivery", () => {
const unknown = structuredClone(document);
unknown.meta.subject = "{{unregistered}}";
expect(resolveDocumentVariables(unknown, { recipientName: "string" }, {}, { preserveTokens: true }).ok).toBe(false);
expect(resolveDocumentVariables(document, { recipientName: "string" }, {}, { preserveTokens: true, requireValues: true }).ok).toBe(false);
});