fix(email-renderer): preserve fallbacks when AMP validation is unavailable
Quality / quality (ubuntu-latest) (push) Failing after 1s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 13:24:26 +05:30
1 parent 2f735b267b
commit 42b91f765d
3 files changed
+30 -3

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/email-renderer",
"version": "0.1.18",
"version": "0.1.19",
"type": "module",
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
"exports": {
+18 -2
View File
@@ -110,6 +110,9 @@ export async function renderEmail(
variables?: Record<string, unknown>;
requireVariables?: boolean;
countdownImageUrl?: string;
/** Trusted host injection; never sourced from the editable document. */
ampValidator?: (content: string) => Promise<ValidationReport>;
ampValidationTimeoutMs?: number;
} = {},
): Promise<Result<EmailRenderBundle>> {
const personalized = resolveDocumentVariables(document, emailKind.variables, options.variables, {
@@ -164,10 +167,23 @@ export async function renderEmail(
const amp = await renderTree(source, emailKind, registry, { ...base, target: "ampEmail", data: options.variables });
if (amp.ok) {
const content = ampHead(source, amp.value.content, base.direction);
const validation = await validateAmpEmail(content);
let timer: ReturnType<typeof setTimeout> | undefined;
let validation: ValidationReport;
const requestedTimeout = options.ampValidationTimeoutMs ?? 2000;
const timeout = Number.isFinite(requestedTimeout) ? Math.min(10000, Math.max(10, requestedTimeout)) : 2000;
try {
validation = await Promise.race([
Promise.resolve().then(() => (options.ampValidator ?? validateAmpEmail)(content)),
new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error("AMP validation timed out.")), timeout); }),
]);
} catch {
validation = { valid: false, diagnostics: [{code:"amp.validatorUnavailable",message:"AMP validation was unavailable or timed out. HTML and plain text remain available.",severity:"warning"}] };
} finally {
if (timer !== undefined) clearTimeout(timer);
}
if (validation.valid)
bundle.parts.amp = { contentType: "text/x-amp-html; charset=utf-8", content, validation };
else warnings.push(...validation.diagnostics);
else warnings.push(...validation.diagnostics.map(diagnostic => ({...diagnostic,severity:"warning" as const})));
} else
warnings.push(...amp.errors.map((error) => ({ ...error, severity: "warning" as const })));
}
@@ -57,6 +57,17 @@ const document: BlockDocument = {
};
describe("email renderer", () => {
test("optional AMP validation failures preserve HTML/text fallbacks", async () => {
for (const ampValidator of [async () => {throw new Error("network unavailable")}, () => new Promise<never>(() => {})]) {
const result=await renderEmail(document,{amp:true,ampValidator,ampValidationTimeoutMs:10});
expect(result.ok).toBe(true);
if(!result.ok)continue;
expect(result.value.parts.amp).toBeUndefined();
expect(result.value.parts.html.content).toContain("Hello &lt;world&gt;");
expect(result.value.parts.text.content).toContain("Hello <world>");
expect(result.value.warnings.some(item=>item.code==="amp.validatorUnavailable"&&item.severity==="warning")).toBe(true);
}
});
test("builder column rows export without nesting mj-column inside mj-column", async () => {
for (const count of [1,2,3,4]) {
const draft=structuredClone(document);