Centralize palette block creation in the email framework
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 18:59:28 +05:30
1 parent 89954181ec
commit 16d804e16f
3 files changed
+36 -2

No files matched your search

+3 -2
View File
@@ -1,11 +1,12 @@
{
"name": "@wrnexus/email-renderer",
"version": "0.1.40",
"version": "0.1.41",
"type": "module",
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
"exports": {
".": "./src/index.ts",
"./blocks": "./src/blocks.ts"
"./blocks": "./src/blocks.ts",
"./create": "./src/create.ts"
},
"files": [
"src",
+15
View File
@@ -0,0 +1,15 @@
import type { BlockNode } from "@wrnexus/blocks";
import { emailCoreBlocks } from "./blocks.ts";
/** Canonical initial content for palette blocks; time and instance IDs are injectable. */
export function createEmailBlock(type: string, context: { id(): string; now?: number }): BlockNode {
const now = context.now ?? Date.now();
const defaults: Record<string, Record<string, unknown>> = {"content.heading":{text:'New heading',level:'2'},"content.text":{text:'New paragraph'},"content.button":{label:'Call to action',href:'https://example.com'},"content.image":{src:'https://placehold.co/600x300',alt:'',width:'600',height:'300',decorative:false},"content.divider":{},"content.spacer":{height:'24'},"content.video":{title:'Watch video',href:'https://example.com',thumbnail:'https://placehold.co/600x338',alt:'Video thumbnail',width:'600',height:'338'},"content.social":{label:'Follow us',facebook:'',instagram:'',linkedin:'',x:''},"content.countdown":{label:'Offer ends',deadline:new Date(now+86400000).toISOString(),fallback:'This offer has ended.'},"content.survey":{question:'What do you think?',buttonLabel:'Answer survey',href:'https://example.com'},"content.feed-item":{width:'600',height:'400',feedUrl:'',title:'Latest article',summary:'Article summary',href:'https://example.com',image:''},"commerce.product":{width:'600',height:'400',provider:'manual',storeUrl:'',productId:'',title:'Featured product',price:'$0.00',image:'https://placehold.co/600x400',href:'https://example.com',buttonLabel:'Shop now'}};
const definition = emailCoreBlocks.find(block => block.manifest.type === type);
if (!definition || !Object.prototype.hasOwnProperty.call(defaults, type))
throw new Error("This block is not available for palette insertion.");
const props = definition.manifest.props.parse(defaults[type], "props");
if (!props.ok) throw new Error(props.errors.map(error => error.message).join("\n"));
const node = definition.create(context);
return { ...node, type: definition.manifest.type, version: definition.manifest.version, props: props.value };
}
@@ -0,0 +1,18 @@
import { expect, test } from "bun:test";
import { createEmailBlock } from "../src/create.ts";
import { emailCoreBlocks } from "../src/blocks.ts";
test("palette factory produces schema-valid independent blocks with injected IDs and time", () => {
let sequence = 0;
for (const definition of emailCoreBlocks.filter(block => /^(content|commerce)\./.test(block.manifest.type))) {
const context = { id: () => `node-${++sequence}`, now: Date.parse("2026-09-05T00:00:00Z") };
const first = createEmailBlock(definition.manifest.type, context), second = createEmailBlock(definition.manifest.type, context);
expect(definition.manifest.props.parse(first.props).ok).toBe(true);
expect(first.id).not.toBe(second.id);
expect(first.props).toEqual(second.props);
expect(first.props).not.toBe(second.props);
expect(first.version).toBe(definition.manifest.version);
if (first.type === "content.countdown") expect(first.props.deadline).toBe("2026-09-06T00:00:00.000Z");
}
expect(() => createEmailBlock("untrusted.block", { id: () => "id" })).toThrow();
});