fix(email-renderer): preserve fallbacks when AMP validation is unavailable
This commit is contained in:
1 parent
2f735b267b
commit
42b91f765d
3 files changed
+30
-3
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/email-renderer",
|
"name": "@wrnexus/email-renderer",
|
||||||
"version": "0.1.18",
|
"version": "0.1.19",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
|
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ export async function renderEmail(
|
|||||||
variables?: Record<string, unknown>;
|
variables?: Record<string, unknown>;
|
||||||
requireVariables?: boolean;
|
requireVariables?: boolean;
|
||||||
countdownImageUrl?: string;
|
countdownImageUrl?: string;
|
||||||
|
/** Trusted host injection; never sourced from the editable document. */
|
||||||
|
ampValidator?: (content: string) => Promise<ValidationReport>;
|
||||||
|
ampValidationTimeoutMs?: number;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<Result<EmailRenderBundle>> {
|
): Promise<Result<EmailRenderBundle>> {
|
||||||
const personalized = resolveDocumentVariables(document, emailKind.variables, options.variables, {
|
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 });
|
const amp = await renderTree(source, emailKind, registry, { ...base, target: "ampEmail", data: options.variables });
|
||||||
if (amp.ok) {
|
if (amp.ok) {
|
||||||
const content = ampHead(source, amp.value.content, base.direction);
|
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)
|
if (validation.valid)
|
||||||
bundle.parts.amp = { contentType: "text/x-amp-html; charset=utf-8", content, validation };
|
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
|
} else
|
||||||
warnings.push(...amp.errors.map((error) => ({ ...error, severity: "warning" as const })));
|
warnings.push(...amp.errors.map((error) => ({ ...error, severity: "warning" as const })));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,17 @@ const document: BlockDocument = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("email renderer", () => {
|
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 <world>");
|
||||||
|
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 () => {
|
test("builder column rows export without nesting mj-column inside mj-column", async () => {
|
||||||
for (const count of [1,2,3,4]) {
|
for (const count of [1,2,3,4]) {
|
||||||
const draft=structuredClone(document);
|
const draft=structuredClone(document);
|
||||||
|
|||||||
Reference in new issue
Block a user