@momentumcms/server-analog 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs +511 -46
- package/index.js +510 -46
- package/package.json +9 -3
- package/src/lib/server-analog.d.ts +2 -2
package/index.cjs
CHANGED
|
@@ -317,7 +317,7 @@ function detectMimeType(buffer) {
|
|
|
317
317
|
if (match) {
|
|
318
318
|
if (sig.bytes[0] === 82 && sig.bytes[1] === 73) {
|
|
319
319
|
if (buffer.length >= 12) {
|
|
320
|
-
const formatId = buffer.
|
|
320
|
+
const formatId = String.fromCharCode(...buffer.subarray(8, 12));
|
|
321
321
|
if (formatId === "WEBP") {
|
|
322
322
|
return "image/webp";
|
|
323
323
|
}
|
|
@@ -330,7 +330,7 @@ function detectMimeType(buffer) {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
if (sig.mimeType === "video/mp4" && buffer.length >= 8) {
|
|
333
|
-
const boxType = buffer.
|
|
333
|
+
const boxType = String.fromCharCode(...buffer.subarray(4, 8));
|
|
334
334
|
if (boxType === "ftyp") {
|
|
335
335
|
return "video/mp4";
|
|
336
336
|
}
|
|
@@ -339,7 +339,7 @@ function detectMimeType(buffer) {
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
if (isTextContent(buffer)) {
|
|
342
|
-
const text2 = buffer.
|
|
342
|
+
const text2 = new TextDecoder().decode(buffer.subarray(0, Math.min(buffer.length, 1e3)));
|
|
343
343
|
if (text2.trim().startsWith("{") || text2.trim().startsWith("[")) {
|
|
344
344
|
return "application/json";
|
|
345
345
|
}
|
|
@@ -531,6 +531,383 @@ var init_src = __esm({
|
|
|
531
531
|
}
|
|
532
532
|
});
|
|
533
533
|
|
|
534
|
+
// libs/email/src/lib/utils/escape-html.ts
|
|
535
|
+
function escapeHtml2(unsafe) {
|
|
536
|
+
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
537
|
+
}
|
|
538
|
+
var init_escape_html = __esm({
|
|
539
|
+
"libs/email/src/lib/utils/escape-html.ts"() {
|
|
540
|
+
"use strict";
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// libs/email/src/lib/utils/css-inliner.ts
|
|
545
|
+
function inlineCss(html) {
|
|
546
|
+
return (0, import_juice.default)(html, {
|
|
547
|
+
removeStyleTags: true,
|
|
548
|
+
preserveMediaQueries: true,
|
|
549
|
+
preserveFontFaces: true,
|
|
550
|
+
insertPreservedExtraCss: true
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
var import_juice;
|
|
554
|
+
var init_css_inliner = __esm({
|
|
555
|
+
"libs/email/src/lib/utils/css-inliner.ts"() {
|
|
556
|
+
"use strict";
|
|
557
|
+
import_juice = __toESM(require("juice"));
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// libs/email/src/lib/utils/replace-variables.ts
|
|
562
|
+
function replaceVariables(text2, variables) {
|
|
563
|
+
return text2.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? "");
|
|
564
|
+
}
|
|
565
|
+
function replaceBlockVariables(blocks2, variables) {
|
|
566
|
+
return blocks2.map((block) => ({
|
|
567
|
+
...block,
|
|
568
|
+
data: replaceDataVariables(block.data, variables)
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
function replaceDataVariables(data, variables) {
|
|
572
|
+
const result = {};
|
|
573
|
+
for (const [key, value] of Object.entries(data)) {
|
|
574
|
+
if (typeof value === "string") {
|
|
575
|
+
result[key] = replaceVariables(value, variables);
|
|
576
|
+
} else if (Array.isArray(value)) {
|
|
577
|
+
result[key] = value.map((item) => {
|
|
578
|
+
if (typeof item === "object" && item !== null && "blocks" in item) {
|
|
579
|
+
const col = item;
|
|
580
|
+
const nestedBlocks = Array.isArray(col["blocks"]) ? col["blocks"] : [];
|
|
581
|
+
return { ...col, blocks: replaceBlockVariables(nestedBlocks, variables) };
|
|
582
|
+
}
|
|
583
|
+
return item;
|
|
584
|
+
});
|
|
585
|
+
} else {
|
|
586
|
+
result[key] = value;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return result;
|
|
590
|
+
}
|
|
591
|
+
var init_replace_variables = __esm({
|
|
592
|
+
"libs/email/src/lib/utils/replace-variables.ts"() {
|
|
593
|
+
"use strict";
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// libs/email/src/lib/utils/sanitize.ts
|
|
598
|
+
function sanitizeAlignment(value) {
|
|
599
|
+
return VALID_ALIGNMENTS.has(value) ? value : "left";
|
|
600
|
+
}
|
|
601
|
+
function sanitizeCssValue(value) {
|
|
602
|
+
return value.replace(/[;{}()"'<>\\]/g, "");
|
|
603
|
+
}
|
|
604
|
+
function sanitizeFontFamily(value) {
|
|
605
|
+
return value.replace(/[;{}()"<>\\]/g, "");
|
|
606
|
+
}
|
|
607
|
+
function sanitizeCssNumber(value, fallback) {
|
|
608
|
+
if (value === null || value === void 0)
|
|
609
|
+
return String(fallback);
|
|
610
|
+
const num = Number(value);
|
|
611
|
+
return Number.isFinite(num) && num >= 0 ? String(num) : String(fallback);
|
|
612
|
+
}
|
|
613
|
+
function sanitizeUrl(url) {
|
|
614
|
+
const trimmed = url.trim();
|
|
615
|
+
if (!trimmed || trimmed === "#")
|
|
616
|
+
return trimmed || "#";
|
|
617
|
+
try {
|
|
618
|
+
const parsed = new URL(trimmed);
|
|
619
|
+
return SAFE_URL_PROTOCOLS.has(parsed.protocol) ? trimmed : "#";
|
|
620
|
+
} catch {
|
|
621
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("#"))
|
|
622
|
+
return trimmed;
|
|
623
|
+
return "#";
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
var VALID_ALIGNMENTS, SAFE_URL_PROTOCOLS;
|
|
627
|
+
var init_sanitize = __esm({
|
|
628
|
+
"libs/email/src/lib/utils/sanitize.ts"() {
|
|
629
|
+
"use strict";
|
|
630
|
+
VALID_ALIGNMENTS = /* @__PURE__ */ new Set(["left", "center", "right"]);
|
|
631
|
+
SAFE_URL_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// libs/email/src/types.ts
|
|
636
|
+
var DEFAULT_EMAIL_THEME;
|
|
637
|
+
var init_types = __esm({
|
|
638
|
+
"libs/email/src/types.ts"() {
|
|
639
|
+
"use strict";
|
|
640
|
+
DEFAULT_EMAIL_THEME = {
|
|
641
|
+
primaryColor: "#18181b",
|
|
642
|
+
backgroundColor: "#f4f4f5",
|
|
643
|
+
textColor: "#3f3f46",
|
|
644
|
+
mutedColor: "#71717a",
|
|
645
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
646
|
+
borderRadius: "8px"
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// libs/email/src/lib/render/render-blocks.ts
|
|
652
|
+
function renderEmailFromBlocks(template, options) {
|
|
653
|
+
const theme = { ...DEFAULT_EMAIL_THEME, ...template.theme };
|
|
654
|
+
const shouldInline = options?.inlineCss ?? true;
|
|
655
|
+
const blocks2 = options?.variables ? replaceBlockVariables(template.blocks, options.variables) : template.blocks;
|
|
656
|
+
const validBlocks = blocks2.filter((block) => {
|
|
657
|
+
if (!isValidBlock(block)) {
|
|
658
|
+
console.warn("[momentum:email] Skipping invalid email block:", block);
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
return true;
|
|
662
|
+
});
|
|
663
|
+
const blocksHtml = validBlocks.map((block) => renderBlock(block, theme, 0)).join("\n");
|
|
664
|
+
let html = wrapEmailDocument(blocksHtml, theme);
|
|
665
|
+
if (shouldInline) {
|
|
666
|
+
html = inlineCss(html);
|
|
667
|
+
}
|
|
668
|
+
return html;
|
|
669
|
+
}
|
|
670
|
+
function wrapEmailDocument(content, theme) {
|
|
671
|
+
const fontFamily = sanitizeFontFamily(theme.fontFamily);
|
|
672
|
+
const bgColor = sanitizeCssValue(theme.backgroundColor);
|
|
673
|
+
const borderRadius = sanitizeCssValue(theme.borderRadius);
|
|
674
|
+
return `<!DOCTYPE html>
|
|
675
|
+
<html lang="en">
|
|
676
|
+
<head>
|
|
677
|
+
<meta charset="UTF-8">
|
|
678
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
679
|
+
</head>
|
|
680
|
+
<body style="margin: 0; padding: 0; font-family: ${fontFamily}; background-color: ${bgColor}; line-height: 1.6;">
|
|
681
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: ${bgColor};">
|
|
682
|
+
<tr>
|
|
683
|
+
<td style="padding: 40px 20px;">
|
|
684
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px; margin: 0 auto; background-color: #ffffff; border-radius: ${borderRadius}; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
685
|
+
<tr>
|
|
686
|
+
<td style="padding: 40px;">
|
|
687
|
+
${content}
|
|
688
|
+
</td>
|
|
689
|
+
</tr>
|
|
690
|
+
</table>
|
|
691
|
+
</td>
|
|
692
|
+
</tr>
|
|
693
|
+
</table>
|
|
694
|
+
</body>
|
|
695
|
+
</html>`;
|
|
696
|
+
}
|
|
697
|
+
function isValidBlock(block) {
|
|
698
|
+
const rec = block;
|
|
699
|
+
return typeof block === "object" && block !== null && typeof rec["id"] === "string" && rec["id"].length > 0 && typeof rec["type"] === "string" && typeof rec["data"] === "object" && rec["data"] !== null;
|
|
700
|
+
}
|
|
701
|
+
function renderBlock(block, theme, depth) {
|
|
702
|
+
switch (block.type) {
|
|
703
|
+
case "header":
|
|
704
|
+
return renderHeaderBlock(block.data, theme);
|
|
705
|
+
case "text":
|
|
706
|
+
return renderTextBlock(block.data, theme);
|
|
707
|
+
case "button":
|
|
708
|
+
return renderButtonBlock(block.data, theme);
|
|
709
|
+
case "image":
|
|
710
|
+
return renderImageBlock(block.data);
|
|
711
|
+
case "divider":
|
|
712
|
+
return renderDividerBlock(block.data);
|
|
713
|
+
case "spacer":
|
|
714
|
+
return renderSpacerBlock(block.data);
|
|
715
|
+
case "columns":
|
|
716
|
+
if (depth >= MAX_BLOCK_DEPTH) {
|
|
717
|
+
console.warn("[momentum:email] Max nesting depth reached, skipping columns block");
|
|
718
|
+
return "";
|
|
719
|
+
}
|
|
720
|
+
return renderColumnsBlock(block.data, theme, depth);
|
|
721
|
+
case "footer":
|
|
722
|
+
return renderFooterBlock(block.data, theme);
|
|
723
|
+
default:
|
|
724
|
+
return `<!-- unknown block type: ${escapeHtml2(block.type)} -->`;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function renderHeaderBlock(data, theme) {
|
|
728
|
+
const title = escapeHtml2(String(data["title"] ?? ""));
|
|
729
|
+
const subtitle = data["subtitle"] ? escapeHtml2(String(data["subtitle"])) : "";
|
|
730
|
+
const alignment = sanitizeAlignment(String(data["alignment"] ?? "left"));
|
|
731
|
+
return `<h1 style="margin: 0 0 8px; font-size: 24px; font-weight: 600; color: ${sanitizeCssValue(theme.textColor)}; text-align: ${alignment};">${title}</h1>${subtitle ? `<p style="margin: 0 0 16px; font-size: 16px; color: ${sanitizeCssValue(theme.mutedColor)}; text-align: ${alignment};">${subtitle}</p>` : ""}`;
|
|
732
|
+
}
|
|
733
|
+
function renderTextBlock(data, theme) {
|
|
734
|
+
const content = escapeHtml2(String(data["content"] ?? ""));
|
|
735
|
+
const fontSize = sanitizeCssNumber(data["fontSize"], 16);
|
|
736
|
+
const color = sanitizeCssValue(String(data["color"] ?? theme.textColor));
|
|
737
|
+
const alignment = sanitizeAlignment(String(data["alignment"] ?? "left"));
|
|
738
|
+
return `<p style="margin: 0 0 16px; font-size: ${fontSize}px; color: ${color}; text-align: ${alignment}; line-height: 1.6;">${content}</p>`;
|
|
739
|
+
}
|
|
740
|
+
function renderButtonBlock(data, theme) {
|
|
741
|
+
const label = escapeHtml2(String(data["label"] ?? "Click here"));
|
|
742
|
+
const href = escapeHtml2(sanitizeUrl(String(data["href"] ?? "#")));
|
|
743
|
+
const bgColor = sanitizeCssValue(String(data["backgroundColor"] ?? theme.primaryColor));
|
|
744
|
+
const color = sanitizeCssValue(String(data["color"] ?? "#ffffff"));
|
|
745
|
+
const alignment = sanitizeAlignment(String(data["alignment"] ?? "left"));
|
|
746
|
+
return `<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
|
747
|
+
<tr>
|
|
748
|
+
<td style="padding: 0 0 16px;" align="${alignment}">
|
|
749
|
+
<a href="${href}" style="display: inline-block; padding: 12px 24px; background-color: ${bgColor}; color: ${color}; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px;">${label}</a>
|
|
750
|
+
</td>
|
|
751
|
+
</tr>
|
|
752
|
+
</table>`;
|
|
753
|
+
}
|
|
754
|
+
function renderImageBlock(data) {
|
|
755
|
+
const rawSrc = String(data["src"] ?? "").trim();
|
|
756
|
+
if (!rawSrc)
|
|
757
|
+
return "<!-- image block: no src configured -->";
|
|
758
|
+
const src = escapeHtml2(sanitizeUrl(rawSrc));
|
|
759
|
+
const alt = escapeHtml2(String(data["alt"] ?? ""));
|
|
760
|
+
const width = sanitizeCssValue(String(data["width"] ?? "100%"));
|
|
761
|
+
const img = `<img src="${src}" alt="${alt}" width="${width}" style="display: block; max-width: 100%; height: auto; border: 0;">`;
|
|
762
|
+
if (data["href"]) {
|
|
763
|
+
const href = escapeHtml2(sanitizeUrl(String(data["href"])));
|
|
764
|
+
return `<a href="${href}" style="display: block;">${img}</a>`;
|
|
765
|
+
}
|
|
766
|
+
return img;
|
|
767
|
+
}
|
|
768
|
+
function renderDividerBlock(data) {
|
|
769
|
+
const color = sanitizeCssValue(String(data["color"] ?? "#e4e4e7"));
|
|
770
|
+
const margin = sanitizeCssValue(String(data["margin"] ?? "24px 0"));
|
|
771
|
+
return `<hr style="border: none; border-top: 1px solid ${color}; margin: ${margin};">`;
|
|
772
|
+
}
|
|
773
|
+
function renderSpacerBlock(data) {
|
|
774
|
+
const height = sanitizeCssNumber(data["height"], 24);
|
|
775
|
+
return `<div style="height: ${height}px; line-height: ${height}px; font-size: 1px;"> </div>`;
|
|
776
|
+
}
|
|
777
|
+
function renderColumnsBlock(data, theme, depth) {
|
|
778
|
+
const rawColumns = data["columns"];
|
|
779
|
+
const columns = Array.isArray(rawColumns) ? rawColumns : [];
|
|
780
|
+
const width = Math.floor(100 / (columns.length || 1));
|
|
781
|
+
const tds = columns.map((col) => {
|
|
782
|
+
const colObj = (
|
|
783
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- narrowing unknown column objects
|
|
784
|
+
typeof col === "object" && col !== null ? col : {}
|
|
785
|
+
);
|
|
786
|
+
const rawBlocks = colObj["blocks"];
|
|
787
|
+
const colContent = (Array.isArray(rawBlocks) ? rawBlocks : []).filter(isValidBlock).map((b) => renderBlock(b, theme, depth + 1)).join("\n");
|
|
788
|
+
return `<td style="width: ${width}%; vertical-align: top; padding: 0 8px;">${colContent}</td>`;
|
|
789
|
+
}).join("\n");
|
|
790
|
+
return `<table role="presentation" width="100%" cellspacing="0" cellpadding="0"><tr>${tds}</tr></table>`;
|
|
791
|
+
}
|
|
792
|
+
function renderFooterBlock(data, theme) {
|
|
793
|
+
const text2 = escapeHtml2(String(data["text"] ?? ""));
|
|
794
|
+
const color = sanitizeCssValue(String(data["color"] ?? theme.mutedColor));
|
|
795
|
+
return `<p style="margin: 16px 0 0; font-size: 12px; color: ${color}; text-align: center;">${text2}</p>`;
|
|
796
|
+
}
|
|
797
|
+
var MAX_BLOCK_DEPTH;
|
|
798
|
+
var init_render_blocks = __esm({
|
|
799
|
+
"libs/email/src/lib/render/render-blocks.ts"() {
|
|
800
|
+
"use strict";
|
|
801
|
+
init_escape_html();
|
|
802
|
+
init_css_inliner();
|
|
803
|
+
init_replace_variables();
|
|
804
|
+
init_sanitize();
|
|
805
|
+
init_types();
|
|
806
|
+
MAX_BLOCK_DEPTH = 5;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// libs/email/src/lib/utils/blocks-to-plain-text.ts
|
|
811
|
+
function blocksToPlainText(blocks2, depth = 0) {
|
|
812
|
+
const lines = [];
|
|
813
|
+
for (const block of blocks2) {
|
|
814
|
+
const text2 = blockToText(block, depth);
|
|
815
|
+
if (text2) {
|
|
816
|
+
lines.push(text2);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return lines.join("\n\n");
|
|
820
|
+
}
|
|
821
|
+
function blockToText(block, depth) {
|
|
822
|
+
switch (block.type) {
|
|
823
|
+
case "header":
|
|
824
|
+
return headerToText(block.data);
|
|
825
|
+
case "text":
|
|
826
|
+
return String(block.data["content"] ?? "");
|
|
827
|
+
case "button":
|
|
828
|
+
return buttonToText(block.data);
|
|
829
|
+
case "footer":
|
|
830
|
+
return String(block.data["text"] ?? "");
|
|
831
|
+
case "columns":
|
|
832
|
+
if (depth >= MAX_BLOCK_DEPTH2) {
|
|
833
|
+
console.warn("[momentum:email] Max nesting depth reached, skipping columns block");
|
|
834
|
+
return "";
|
|
835
|
+
}
|
|
836
|
+
return columnsToText(block.data, depth);
|
|
837
|
+
case "divider":
|
|
838
|
+
case "spacer":
|
|
839
|
+
case "image":
|
|
840
|
+
return "";
|
|
841
|
+
default:
|
|
842
|
+
return "";
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function headerToText(data) {
|
|
846
|
+
const title = String(data["title"] ?? "");
|
|
847
|
+
const subtitle = data["subtitle"] ? String(data["subtitle"]) : "";
|
|
848
|
+
if (!title && !subtitle)
|
|
849
|
+
return "";
|
|
850
|
+
if (!subtitle)
|
|
851
|
+
return title;
|
|
852
|
+
return `${title}
|
|
853
|
+
${subtitle}`;
|
|
854
|
+
}
|
|
855
|
+
function buttonToText(data) {
|
|
856
|
+
const label = String(data["label"] ?? "");
|
|
857
|
+
const href = data["href"] ? String(data["href"]) : "";
|
|
858
|
+
if (!label)
|
|
859
|
+
return "";
|
|
860
|
+
if (!href)
|
|
861
|
+
return label;
|
|
862
|
+
return `${label}: ${href}`;
|
|
863
|
+
}
|
|
864
|
+
function columnsToText(data, depth) {
|
|
865
|
+
const columns = data["columns"];
|
|
866
|
+
if (!Array.isArray(columns))
|
|
867
|
+
return "";
|
|
868
|
+
const parts = [];
|
|
869
|
+
for (const col of columns) {
|
|
870
|
+
if (col && typeof col === "object" && Array.isArray(col.blocks)) {
|
|
871
|
+
const colText = blocksToPlainText(col.blocks, depth + 1);
|
|
872
|
+
if (colText) {
|
|
873
|
+
parts.push(colText);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return parts.join("\n\n");
|
|
878
|
+
}
|
|
879
|
+
var MAX_BLOCK_DEPTH2;
|
|
880
|
+
var init_blocks_to_plain_text = __esm({
|
|
881
|
+
"libs/email/src/lib/utils/blocks-to-plain-text.ts"() {
|
|
882
|
+
"use strict";
|
|
883
|
+
MAX_BLOCK_DEPTH2 = 5;
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
// libs/email/src/server.ts
|
|
888
|
+
var server_exports = {};
|
|
889
|
+
__export(server_exports, {
|
|
890
|
+
DEFAULT_EMAIL_THEME: () => DEFAULT_EMAIL_THEME,
|
|
891
|
+
blocksToPlainText: () => blocksToPlainText,
|
|
892
|
+
escapeHtml: () => escapeHtml2,
|
|
893
|
+
inlineCss: () => inlineCss,
|
|
894
|
+
isValidBlock: () => isValidBlock,
|
|
895
|
+
renderEmailFromBlocks: () => renderEmailFromBlocks,
|
|
896
|
+
replaceBlockVariables: () => replaceBlockVariables,
|
|
897
|
+
replaceVariables: () => replaceVariables
|
|
898
|
+
});
|
|
899
|
+
var init_server = __esm({
|
|
900
|
+
"libs/email/src/server.ts"() {
|
|
901
|
+
"use strict";
|
|
902
|
+
init_render_blocks();
|
|
903
|
+
init_replace_variables();
|
|
904
|
+
init_blocks_to_plain_text();
|
|
905
|
+
init_css_inliner();
|
|
906
|
+
init_escape_html();
|
|
907
|
+
init_types();
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
|
|
534
911
|
// libs/server-analog/src/index.ts
|
|
535
912
|
var src_exports2 = {};
|
|
536
913
|
__export(src_exports2, {
|
|
@@ -973,6 +1350,31 @@ function validateRowCount(name, label, count, minRows, maxRows, errors) {
|
|
|
973
1350
|
}
|
|
974
1351
|
|
|
975
1352
|
// libs/core/src/lib/collections/media.collection.ts
|
|
1353
|
+
var validateFocalPoint = (value) => {
|
|
1354
|
+
if (value === null || value === void 0)
|
|
1355
|
+
return true;
|
|
1356
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
1357
|
+
return "Focal point must be an object with x and y coordinates";
|
|
1358
|
+
}
|
|
1359
|
+
const fp = Object.fromEntries(Object.entries(value));
|
|
1360
|
+
if (!("x" in fp) || !("y" in fp)) {
|
|
1361
|
+
return "Focal point must have both x and y properties";
|
|
1362
|
+
}
|
|
1363
|
+
const { x, y } = fp;
|
|
1364
|
+
if (typeof x !== "number" || !Number.isFinite(x)) {
|
|
1365
|
+
return "Focal point x must be a finite number";
|
|
1366
|
+
}
|
|
1367
|
+
if (typeof y !== "number" || !Number.isFinite(y)) {
|
|
1368
|
+
return "Focal point y must be a finite number";
|
|
1369
|
+
}
|
|
1370
|
+
if (x < 0 || x > 1) {
|
|
1371
|
+
return `Focal point x must be between 0 and 1 (received ${x})`;
|
|
1372
|
+
}
|
|
1373
|
+
if (y < 0 || y > 1) {
|
|
1374
|
+
return `Focal point y must be between 0 and 1 (received ${y})`;
|
|
1375
|
+
}
|
|
1376
|
+
return true;
|
|
1377
|
+
};
|
|
976
1378
|
var MediaCollection = defineCollection({
|
|
977
1379
|
slug: "media",
|
|
978
1380
|
labels: {
|
|
@@ -1027,6 +1429,14 @@ var MediaCollection = defineCollection({
|
|
|
1027
1429
|
json("focalPoint", {
|
|
1028
1430
|
label: "Focal Point",
|
|
1029
1431
|
description: "Focal point coordinates for image cropping",
|
|
1432
|
+
validate: validateFocalPoint,
|
|
1433
|
+
admin: {
|
|
1434
|
+
hidden: true
|
|
1435
|
+
}
|
|
1436
|
+
}),
|
|
1437
|
+
json("sizes", {
|
|
1438
|
+
label: "Image Sizes",
|
|
1439
|
+
description: "Generated image size variants",
|
|
1030
1440
|
admin: {
|
|
1031
1441
|
hidden: true
|
|
1032
1442
|
}
|
|
@@ -1796,6 +2206,15 @@ function deepEqual(a, b) {
|
|
|
1796
2206
|
(key) => Object.prototype.hasOwnProperty.call(bRec, key) && deepEqual(aRec[key], bRec[key])
|
|
1797
2207
|
);
|
|
1798
2208
|
}
|
|
2209
|
+
function stripTransientKeys(data) {
|
|
2210
|
+
const result = {};
|
|
2211
|
+
for (const [key, value] of Object.entries(data)) {
|
|
2212
|
+
if (!key.startsWith("_")) {
|
|
2213
|
+
result[key] = value;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
return result;
|
|
2217
|
+
}
|
|
1799
2218
|
function flattenWhereClause(where) {
|
|
1800
2219
|
if (!where)
|
|
1801
2220
|
return {};
|
|
@@ -1959,6 +2378,7 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
|
|
|
1959
2378
|
);
|
|
1960
2379
|
}
|
|
1961
2380
|
processedData = await this.runHooks("beforeChange", processedData, "create");
|
|
2381
|
+
processedData = stripTransientKeys(processedData);
|
|
1962
2382
|
const doc = await this.adapter.create(this.slug, processedData);
|
|
1963
2383
|
await this.runHooks("afterChange", doc, "create");
|
|
1964
2384
|
if (hasFieldHooks(this.collectionConfig.fields)) {
|
|
@@ -2019,6 +2439,7 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
|
|
|
2019
2439
|
);
|
|
2020
2440
|
}
|
|
2021
2441
|
processedData = await this.runHooks("beforeChange", processedData, "update", originalDoc);
|
|
2442
|
+
processedData = stripTransientKeys(processedData);
|
|
2022
2443
|
const doc = await this.adapter.update(this.slug, id, processedData);
|
|
2023
2444
|
await this.runHooks("afterChange", doc, "update", originalDoc);
|
|
2024
2445
|
if (hasFieldHooks(this.collectionConfig.fields)) {
|
|
@@ -3566,7 +3987,8 @@ async function handleUpload(config, request) {
|
|
|
3566
3987
|
filesize: file.size,
|
|
3567
3988
|
path: storedFile.path,
|
|
3568
3989
|
url: storedFile.url,
|
|
3569
|
-
alt: alt ?? ""
|
|
3990
|
+
alt: alt ?? "",
|
|
3991
|
+
_file: file
|
|
3570
3992
|
};
|
|
3571
3993
|
const api = getMomentumAPI().setContext({ user });
|
|
3572
3994
|
const doc = await api.collection(collection).create(mediaData);
|
|
@@ -3633,7 +4055,8 @@ async function handleCollectionUpload(globalConfig, request) {
|
|
|
3633
4055
|
mimeType: file.mimeType,
|
|
3634
4056
|
filesize: file.size,
|
|
3635
4057
|
path: storedFile.path,
|
|
3636
|
-
url: storedFile.url
|
|
4058
|
+
url: storedFile.url,
|
|
4059
|
+
_file: file
|
|
3637
4060
|
};
|
|
3638
4061
|
const api = getMomentumAPI().setContext({ user });
|
|
3639
4062
|
const doc = await api.collection(collectionSlug).create(docData);
|
|
@@ -3670,6 +4093,7 @@ function getMimeTypeFromPath(path) {
|
|
|
3670
4093
|
png: "image/png",
|
|
3671
4094
|
gif: "image/gif",
|
|
3672
4095
|
webp: "image/webp",
|
|
4096
|
+
avif: "image/avif",
|
|
3673
4097
|
svg: "image/svg+xml",
|
|
3674
4098
|
pdf: "application/pdf",
|
|
3675
4099
|
json: "application/json",
|
|
@@ -4335,11 +4759,11 @@ SwaggerUIBundle({
|
|
|
4335
4759
|
|
|
4336
4760
|
// libs/server-core/src/lib/preview-renderer.ts
|
|
4337
4761
|
function renderPreviewHTML(options) {
|
|
4338
|
-
const { doc, collection } = options;
|
|
4762
|
+
const { doc, collection, customFieldRenderers } = options;
|
|
4339
4763
|
const titleField = collection.admin?.useAsTitle ?? "id";
|
|
4340
4764
|
const title = escapeHtml(String(doc[titleField] ?? doc["id"] ?? "Untitled"));
|
|
4341
4765
|
const fields = collection.fields ?? [];
|
|
4342
|
-
const fieldHtml = fields.filter((f) => !isHiddenField(f) && !isLayoutField(f) && f.name !== titleField).map((f) => renderField(f, doc)).filter(Boolean).join("\n");
|
|
4766
|
+
const fieldHtml = fields.filter((f) => !isHiddenField(f) && !isLayoutField(f) && f.name !== titleField).map((f) => renderField(f, doc, customFieldRenderers)).filter(Boolean).join("\n");
|
|
4343
4767
|
const richTextFields = fields.filter((f) => f.type === "richText").map((f) => f.name);
|
|
4344
4768
|
return `<!DOCTYPE html>
|
|
4345
4769
|
<html lang="en">
|
|
@@ -4373,6 +4797,7 @@ ${fieldHtml}
|
|
|
4373
4797
|
(function(){
|
|
4374
4798
|
var richTextFields=${JSON.stringify(richTextFields)};
|
|
4375
4799
|
window.addEventListener('message',function(e){
|
|
4800
|
+
if(e.origin!==window.location.origin)return;
|
|
4376
4801
|
if(!e.data||e.data.type!=='momentum-preview-update')return;
|
|
4377
4802
|
var d=e.data.data;if(!d)return;
|
|
4378
4803
|
document.querySelectorAll('[data-field]').forEach(function(el){
|
|
@@ -4393,16 +4818,20 @@ if(titleEl){var tf=titleEl.getAttribute('data-field');if(d[tf]!==undefined)title
|
|
|
4393
4818
|
</body>
|
|
4394
4819
|
</html>`;
|
|
4395
4820
|
}
|
|
4396
|
-
function renderField(field, doc) {
|
|
4821
|
+
function renderField(field, doc, customRenderers) {
|
|
4397
4822
|
const value = doc[field.name];
|
|
4398
4823
|
if (value === void 0 || value === null) {
|
|
4399
4824
|
return renderFieldWrapper(field, "");
|
|
4400
4825
|
}
|
|
4826
|
+
const editorKey = field.admin?.editor;
|
|
4827
|
+
if (editorKey && customRenderers?.[editorKey]) {
|
|
4828
|
+
return renderFieldWrapper(field, customRenderers[editorKey](value, field));
|
|
4829
|
+
}
|
|
4401
4830
|
switch (field.type) {
|
|
4402
4831
|
case "richText":
|
|
4403
4832
|
return renderFieldWrapper(
|
|
4404
4833
|
field,
|
|
4405
|
-
`<div class="field-value rich-text" data-field="${escapeHtml(field.name)}">${String(value)}</div>`
|
|
4834
|
+
`<div class="field-value rich-text" data-field="${escapeHtml(field.name)}">${escapeHtml(String(value))}</div>`
|
|
4406
4835
|
);
|
|
4407
4836
|
case "checkbox":
|
|
4408
4837
|
return renderFieldWrapper(
|
|
@@ -4733,6 +5162,20 @@ function coerceCsvValue(value, fieldType) {
|
|
|
4733
5162
|
}
|
|
4734
5163
|
|
|
4735
5164
|
// libs/server-analog/src/lib/server-analog.ts
|
|
5165
|
+
function getEmailBuilderFieldName(collection) {
|
|
5166
|
+
const field = collection.fields.find(
|
|
5167
|
+
(f) => f.type === "json" && f.admin?.editor === "email-builder"
|
|
5168
|
+
);
|
|
5169
|
+
return field?.name;
|
|
5170
|
+
}
|
|
5171
|
+
async function renderEmailPreviewHTML(doc, blocksFieldName) {
|
|
5172
|
+
const { renderEmailFromBlocks: renderEmailFromBlocks2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
5173
|
+
const blocks2 = doc[blocksFieldName];
|
|
5174
|
+
if (!Array.isArray(blocks2) || blocks2.length === 0) {
|
|
5175
|
+
return '<html><body style="display:flex;align-items:center;justify-content:center;min-height:100vh;color:#666;font-family:sans-serif"><p>No email blocks yet.</p></body></html>';
|
|
5176
|
+
}
|
|
5177
|
+
return renderEmailFromBlocks2({ blocks: blocks2 });
|
|
5178
|
+
}
|
|
4736
5179
|
function nestBracketParams(flat) {
|
|
4737
5180
|
const result = {};
|
|
4738
5181
|
for (const [key, value] of Object.entries(flat)) {
|
|
@@ -5320,6 +5763,63 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
5320
5763
|
}
|
|
5321
5764
|
}
|
|
5322
5765
|
}
|
|
5766
|
+
if (seg2 === "preview" && seg1 && (method === "GET" || method === "POST")) {
|
|
5767
|
+
if (!user) {
|
|
5768
|
+
utils.setResponseStatus(event, 401);
|
|
5769
|
+
return { error: "Authentication required to access preview" };
|
|
5770
|
+
}
|
|
5771
|
+
try {
|
|
5772
|
+
const collectionSlug2 = seg0;
|
|
5773
|
+
const docId = seg1;
|
|
5774
|
+
const collectionConfig = config.collections.find((c) => c.slug === collectionSlug2);
|
|
5775
|
+
if (!collectionConfig) {
|
|
5776
|
+
utils.setResponseStatus(event, 404);
|
|
5777
|
+
return { error: "Collection not found" };
|
|
5778
|
+
}
|
|
5779
|
+
const accessFn = collectionConfig.access?.read;
|
|
5780
|
+
if (accessFn) {
|
|
5781
|
+
const allowed = await Promise.resolve(accessFn({ req: { user } }));
|
|
5782
|
+
if (!allowed) {
|
|
5783
|
+
utils.setResponseStatus(event, 403);
|
|
5784
|
+
return { error: "Access denied" };
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
let docRecord;
|
|
5788
|
+
if (method === "POST") {
|
|
5789
|
+
const body2 = await safeReadBody(event, utils, method);
|
|
5790
|
+
if (body2["data"] && typeof body2["data"] === "object") {
|
|
5791
|
+
docRecord = body2["data"];
|
|
5792
|
+
} else {
|
|
5793
|
+
utils.setResponseStatus(event, 400);
|
|
5794
|
+
return { error: "POST preview requires { data: ... } body" };
|
|
5795
|
+
}
|
|
5796
|
+
} else {
|
|
5797
|
+
const contextApi = getContextualAPI(user);
|
|
5798
|
+
const doc = await contextApi.collection(collectionSlug2).findById(docId);
|
|
5799
|
+
if (!doc) {
|
|
5800
|
+
utils.setResponseStatus(event, 404);
|
|
5801
|
+
return { error: "Document not found" };
|
|
5802
|
+
}
|
|
5803
|
+
docRecord = doc;
|
|
5804
|
+
}
|
|
5805
|
+
const emailField = getEmailBuilderFieldName(collectionConfig);
|
|
5806
|
+
const html = emailField ? await renderEmailPreviewHTML(docRecord, emailField) : renderPreviewHTML({ doc: docRecord, collection: collectionConfig });
|
|
5807
|
+
utils.setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
|
|
5808
|
+
return utils.send(event, html);
|
|
5809
|
+
} catch (error) {
|
|
5810
|
+
const message = sanitizeErrorMessage(error, "Unknown error");
|
|
5811
|
+
if (message.includes("Access denied")) {
|
|
5812
|
+
utils.setResponseStatus(event, 403);
|
|
5813
|
+
return { error: message };
|
|
5814
|
+
}
|
|
5815
|
+
if (message.includes("not found")) {
|
|
5816
|
+
utils.setResponseStatus(event, 404);
|
|
5817
|
+
return { error: message };
|
|
5818
|
+
}
|
|
5819
|
+
utils.setResponseStatus(event, 500);
|
|
5820
|
+
return { error: "Preview failed", message };
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5323
5823
|
if (seg1 && seg2 && method === "POST") {
|
|
5324
5824
|
const collectionSlug2 = seg0;
|
|
5325
5825
|
const docId = seg1;
|
|
@@ -5417,42 +5917,6 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
5417
5917
|
};
|
|
5418
5918
|
}
|
|
5419
5919
|
}
|
|
5420
|
-
if (seg2 === "preview" && seg1 && method === "GET") {
|
|
5421
|
-
try {
|
|
5422
|
-
const collectionSlug2 = seg0;
|
|
5423
|
-
const docId = seg1;
|
|
5424
|
-
const contextApi = getContextualAPI(user);
|
|
5425
|
-
const doc = await contextApi.collection(collectionSlug2).findById(docId);
|
|
5426
|
-
if (!doc) {
|
|
5427
|
-
utils.setResponseStatus(event, 404);
|
|
5428
|
-
return { error: "Document not found" };
|
|
5429
|
-
}
|
|
5430
|
-
const collectionConfig = config.collections.find((c) => c.slug === collectionSlug2);
|
|
5431
|
-
if (!collectionConfig) {
|
|
5432
|
-
utils.setResponseStatus(event, 404);
|
|
5433
|
-
return { error: "Collection not found" };
|
|
5434
|
-
}
|
|
5435
|
-
const html = renderPreviewHTML({
|
|
5436
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- doc type from API
|
|
5437
|
-
doc,
|
|
5438
|
-
collection: collectionConfig
|
|
5439
|
-
});
|
|
5440
|
-
utils.setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
|
|
5441
|
-
return utils.send(event, html);
|
|
5442
|
-
} catch (error) {
|
|
5443
|
-
const message = sanitizeErrorMessage(error, "Unknown error");
|
|
5444
|
-
if (message.includes("Access denied")) {
|
|
5445
|
-
utils.setResponseStatus(event, 403);
|
|
5446
|
-
return { error: message };
|
|
5447
|
-
}
|
|
5448
|
-
if (message.includes("not found")) {
|
|
5449
|
-
utils.setResponseStatus(event, 404);
|
|
5450
|
-
return { error: message };
|
|
5451
|
-
}
|
|
5452
|
-
utils.setResponseStatus(event, 500);
|
|
5453
|
-
return { error: "Preview failed", message };
|
|
5454
|
-
}
|
|
5455
|
-
}
|
|
5456
5920
|
if (seg0 && seg1 && !seg2) {
|
|
5457
5921
|
const customKey = `${method}:${seg0}/${seg1}`;
|
|
5458
5922
|
const customEntry = customEndpointMap.get(customKey);
|
|
@@ -5705,12 +6169,13 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
5705
6169
|
fields[field.name] = field.data.toString("utf-8");
|
|
5706
6170
|
}
|
|
5707
6171
|
}
|
|
6172
|
+
const collectionUpload = postUploadCol.upload ?? {};
|
|
5708
6173
|
const uploadRequest = {
|
|
5709
6174
|
file,
|
|
5710
6175
|
user,
|
|
5711
6176
|
fields,
|
|
5712
6177
|
collectionSlug: seg0,
|
|
5713
|
-
collectionUpload
|
|
6178
|
+
collectionUpload
|
|
5714
6179
|
};
|
|
5715
6180
|
const response2 = await handleCollectionUpload(uploadConfig, uploadRequest);
|
|
5716
6181
|
utils.setResponseStatus(event, response2.status);
|