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",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"type": "module",
|
||||
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
|
||||
"exports": {
|
||||
|
||||
@@ -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 <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 () => {
|
||||
for (const count of [1,2,3,4]) {
|
||||
const draft=structuredClone(document);
|
||||
|
||||
Reference in new issue
Block a user