mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 08:13:23 +00:00
Merge pull request #16 from xtr-dev/dev
Replace Handlebars with flexible template engine system
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/payload-mailing",
|
"name": "@xtr-dev/payload-mailing",
|
||||||
"version": "0.0.8",
|
"version": "0.0.12",
|
||||||
"description": "Template-based email system with scheduling and job processing for PayloadCMS",
|
"description": "Template-based email system with scheduling and job processing for PayloadCMS",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
@@ -47,9 +47,12 @@
|
|||||||
"payload": "^3.37.0"
|
"payload": "^3.37.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"handlebars": "^4.7.8",
|
|
||||||
"nodemailer": "^6.9.8"
|
"nodemailer": "^6.9.8"
|
||||||
},
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"liquidjs": "^10.19.0",
|
||||||
|
"mustache": "^4.2.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@payloadcms/db-mongodb": "^3.37.0",
|
"@payloadcms/db-mongodb": "^3.37.0",
|
||||||
|
|||||||
66
pnpm-lock.yaml
generated
66
pnpm-lock.yaml
generated
@@ -8,9 +8,6 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
handlebars:
|
|
||||||
specifier: ^4.7.8
|
|
||||||
version: 4.7.8
|
|
||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^6.9.8
|
specifier: ^6.9.8
|
||||||
version: 6.10.1
|
version: 6.10.1
|
||||||
@@ -117,6 +114,13 @@ importers:
|
|||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.1.2
|
specifier: ^3.1.2
|
||||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(sass@1.77.4)(tsx@4.20.3)
|
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(sass@1.77.4)(tsx@4.20.3)
|
||||||
|
optionalDependencies:
|
||||||
|
liquidjs:
|
||||||
|
specifier: ^10.19.0
|
||||||
|
version: 10.21.1
|
||||||
|
mustache:
|
||||||
|
specifier: ^4.2.0
|
||||||
|
version: 4.2.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -2637,6 +2641,10 @@ packages:
|
|||||||
colorette@2.0.20:
|
colorette@2.0.20:
|
||||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||||
|
|
||||||
|
commander@10.0.1:
|
||||||
|
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
commander@2.20.3:
|
commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
|
|
||||||
@@ -3540,11 +3548,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==}
|
resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==}
|
||||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||||
|
|
||||||
handlebars@4.7.8:
|
|
||||||
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
|
|
||||||
engines: {node: '>=0.4.7'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
has-bigints@1.1.0:
|
has-bigints@1.1.0:
|
||||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3922,6 +3925,11 @@ packages:
|
|||||||
lines-and-columns@1.2.4:
|
lines-and-columns@1.2.4:
|
||||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
|
|
||||||
|
liquidjs@10.21.1:
|
||||||
|
resolution: {integrity: sha512-NZXmCwv3RG5nire3fmIn9HsOyJX3vo+ptp0yaXUHAMzSNBhx74Hm+dAGJvscUA6lNqbLuYfXgNavRQ9UbUJhQQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
locate-path@5.0.0:
|
locate-path@5.0.0:
|
||||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4186,6 +4194,10 @@ packages:
|
|||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
mustache@4.2.0:
|
||||||
|
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
nanoid@3.3.11:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@@ -4202,9 +4214,6 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
neo-async@2.6.2:
|
|
||||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
|
||||||
|
|
||||||
new-find-package-json@2.0.0:
|
new-find-package-json@2.0.0:
|
||||||
resolution: {integrity: sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==}
|
resolution: {integrity: sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==}
|
||||||
engines: {node: '>=12.22.0'}
|
engines: {node: '>=12.22.0'}
|
||||||
@@ -5170,11 +5179,6 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
uglify-js@3.19.3:
|
|
||||||
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
|
||||||
engines: {node: '>=0.8.0'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
uint8array-extras@1.5.0:
|
uint8array-extras@1.5.0:
|
||||||
resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
|
resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -5377,9 +5381,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
wordwrap@1.0.0:
|
|
||||||
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
|
||||||
|
|
||||||
wrap-ansi@7.0.0:
|
wrap-ansi@7.0.0:
|
||||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -8540,6 +8541,9 @@ snapshots:
|
|||||||
|
|
||||||
colorette@2.0.20: {}
|
colorette@2.0.20: {}
|
||||||
|
|
||||||
|
commander@10.0.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
commander@2.20.3: {}
|
commander@2.20.3: {}
|
||||||
|
|
||||||
commander@6.2.1: {}
|
commander@6.2.1: {}
|
||||||
@@ -9689,15 +9693,6 @@ snapshots:
|
|||||||
|
|
||||||
graphql@16.11.0: {}
|
graphql@16.11.0: {}
|
||||||
|
|
||||||
handlebars@4.7.8:
|
|
||||||
dependencies:
|
|
||||||
minimist: 1.2.8
|
|
||||||
neo-async: 2.6.2
|
|
||||||
source-map: 0.6.1
|
|
||||||
wordwrap: 1.0.0
|
|
||||||
optionalDependencies:
|
|
||||||
uglify-js: 3.19.3
|
|
||||||
|
|
||||||
has-bigints@1.1.0: {}
|
has-bigints@1.1.0: {}
|
||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
@@ -10044,6 +10039,11 @@ snapshots:
|
|||||||
|
|
||||||
lines-and-columns@1.2.4: {}
|
lines-and-columns@1.2.4: {}
|
||||||
|
|
||||||
|
liquidjs@10.21.1:
|
||||||
|
dependencies:
|
||||||
|
commander: 10.0.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
locate-path@5.0.0:
|
locate-path@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 4.1.0
|
p-locate: 4.1.0
|
||||||
@@ -10431,6 +10431,9 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
mustache@4.2.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
napi-postinstall@0.3.3: {}
|
napi-postinstall@0.3.3: {}
|
||||||
@@ -10439,8 +10442,6 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
neo-async@2.6.2: {}
|
|
||||||
|
|
||||||
new-find-package-json@2.0.0:
|
new-find-package-json@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
@@ -11571,9 +11572,6 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.9.2: {}
|
typescript@5.9.2: {}
|
||||||
|
|
||||||
uglify-js@3.19.3:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
uint8array-extras@1.5.0: {}
|
uint8array-extras@1.5.0: {}
|
||||||
|
|
||||||
unbox-primitive@1.1.0:
|
unbox-primitive@1.1.0:
|
||||||
@@ -11820,8 +11818,6 @@ snapshots:
|
|||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
wordwrap@1.0.0: {}
|
|
||||||
|
|
||||||
wrap-ansi@7.0.0:
|
wrap-ansi@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Payload } from 'payload'
|
import { Payload } from 'payload'
|
||||||
import Handlebars from 'handlebars'
|
import { Liquid } from 'liquidjs'
|
||||||
import nodemailer, { Transporter } from 'nodemailer'
|
import nodemailer, { Transporter } from 'nodemailer'
|
||||||
import {
|
import {
|
||||||
MailingPluginConfig,
|
MailingPluginConfig,
|
||||||
SendEmailOptions,
|
SendEmailOptions,
|
||||||
MailingService as IMailingService,
|
MailingService as IMailingService,
|
||||||
EmailTemplate,
|
EmailTemplate,
|
||||||
QueuedEmail,
|
QueuedEmail,
|
||||||
@@ -18,19 +18,19 @@ export class MailingService implements IMailingService {
|
|||||||
private transporter!: Transporter | any
|
private transporter!: Transporter | any
|
||||||
private templatesCollection: string
|
private templatesCollection: string
|
||||||
private emailsCollection: string
|
private emailsCollection: string
|
||||||
|
private liquid: Liquid | null | false = null
|
||||||
|
|
||||||
constructor(payload: Payload, config: MailingPluginConfig) {
|
constructor(payload: Payload, config: MailingPluginConfig) {
|
||||||
this.payload = payload
|
this.payload = payload
|
||||||
this.config = config
|
this.config = config
|
||||||
|
|
||||||
const templatesConfig = config.collections?.templates
|
const templatesConfig = config.collections?.templates
|
||||||
this.templatesCollection = typeof templatesConfig === 'string' ? templatesConfig : 'email-templates'
|
this.templatesCollection = typeof templatesConfig === 'string' ? templatesConfig : 'email-templates'
|
||||||
|
|
||||||
const emailsConfig = config.collections?.emails
|
const emailsConfig = config.collections?.emails
|
||||||
this.emailsCollection = typeof emailsConfig === 'string' ? emailsConfig : 'emails'
|
this.emailsCollection = typeof emailsConfig === 'string' ? emailsConfig : 'emails'
|
||||||
|
|
||||||
this.initializeTransporter()
|
this.initializeTransporter()
|
||||||
this.registerHandlebarsHelpers()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeTransporter(): void {
|
private initializeTransporter(): void {
|
||||||
@@ -62,39 +62,49 @@ export class MailingService implements IMailingService {
|
|||||||
return fromEmail || ''
|
return fromEmail || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlebarsHelpers(): void {
|
private async ensureLiquidJSInitialized(): Promise<void> {
|
||||||
Handlebars.registerHelper('formatDate', (date: Date, format?: string) => {
|
if (this.liquid !== null) return // Already initialized or failed
|
||||||
if (!date) return ''
|
|
||||||
const d = new Date(date)
|
try {
|
||||||
if (format === 'short') {
|
const liquidModule = await import('liquidjs')
|
||||||
return d.toLocaleDateString()
|
const { Liquid: LiquidEngine } = liquidModule
|
||||||
}
|
this.liquid = new LiquidEngine()
|
||||||
if (format === 'long') {
|
|
||||||
return d.toLocaleDateString('en-US', {
|
// Register custom filters (equivalent to Handlebars helpers)
|
||||||
year: 'numeric',
|
if (this.liquid && typeof this.liquid !== 'boolean') {
|
||||||
month: 'long',
|
this.liquid.registerFilter('formatDate', (date: any, format?: string) => {
|
||||||
day: 'numeric'
|
if (!date) return ''
|
||||||
|
const d = new Date(date)
|
||||||
|
if (format === 'short') {
|
||||||
|
return d.toLocaleDateString()
|
||||||
|
}
|
||||||
|
if (format === 'long') {
|
||||||
|
return d.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return d.toLocaleString()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.liquid.registerFilter('formatCurrency', (amount: any, currency = 'USD') => {
|
||||||
|
if (typeof amount !== 'number') return amount
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency
|
||||||
|
}).format(amount)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.liquid.registerFilter('capitalize', (str: any) => {
|
||||||
|
if (typeof str !== 'string') return str
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return d.toLocaleString()
|
} catch (error) {
|
||||||
})
|
console.warn('LiquidJS not available. Falling back to simple variable replacement. Install liquidjs or use a different templateEngine.')
|
||||||
|
this.liquid = false // Mark as failed to avoid retries
|
||||||
Handlebars.registerHelper('formatCurrency', (amount: number, currency = 'USD') => {
|
}
|
||||||
if (typeof amount !== 'number') return amount
|
|
||||||
return new Intl.NumberFormat('en-US', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: currency
|
|
||||||
}).format(amount)
|
|
||||||
})
|
|
||||||
|
|
||||||
Handlebars.registerHelper('ifEquals', function(this: any, arg1: any, arg2: any, options: any) {
|
|
||||||
return (arg1 === arg2) ? options.fn(this) : options.inverse(this)
|
|
||||||
})
|
|
||||||
|
|
||||||
Handlebars.registerHelper('capitalize', (str: string) => {
|
|
||||||
if (typeof str !== 'string') return str
|
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendEmail(options: SendEmailOptions): Promise<string> {
|
async sendEmail(options: SendEmailOptions): Promise<string> {
|
||||||
@@ -104,7 +114,7 @@ export class MailingService implements IMailingService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await this.processEmailItem(emailId)
|
await this.processEmailItem(emailId)
|
||||||
|
|
||||||
return emailId
|
return emailId
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,14 +126,14 @@ export class MailingService implements IMailingService {
|
|||||||
|
|
||||||
if (options.templateSlug) {
|
if (options.templateSlug) {
|
||||||
const template = await this.getTemplateBySlug(options.templateSlug)
|
const template = await this.getTemplateBySlug(options.templateSlug)
|
||||||
|
|
||||||
if (template) {
|
if (template) {
|
||||||
templateId = template.id
|
templateId = template.id
|
||||||
const variables = options.variables || {}
|
const variables = options.variables || {}
|
||||||
const renderedContent = await this.renderEmailTemplate(template, variables)
|
const renderedContent = await this.renderEmailTemplate(template, variables)
|
||||||
html = renderedContent.html
|
html = renderedContent.html
|
||||||
text = renderedContent.text
|
text = renderedContent.text
|
||||||
subject = this.renderHandlebarsTemplate(template.subject, variables)
|
subject = await this.renderTemplate(template.subject, variables)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Email template not found: ${options.templateSlug}`)
|
throw new Error(`Email template not found: ${options.templateSlug}`)
|
||||||
}
|
}
|
||||||
@@ -164,7 +174,7 @@ export class MailingService implements IMailingService {
|
|||||||
|
|
||||||
async processEmails(): Promise<void> {
|
async processEmails(): Promise<void> {
|
||||||
const currentTime = new Date().toISOString()
|
const currentTime = new Date().toISOString()
|
||||||
|
|
||||||
const { docs: pendingEmails } = await this.payload.find({
|
const { docs: pendingEmails } = await this.payload.find({
|
||||||
collection: this.emailsCollection as any,
|
collection: this.emailsCollection as any,
|
||||||
where: {
|
where: {
|
||||||
@@ -348,7 +358,7 @@ export class MailingService implements IMailingService {
|
|||||||
},
|
},
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
return docs.length > 0 ? docs[0] as EmailTemplate : null
|
return docs.length > 0 ? docs[0] as EmailTemplate : null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Template with slug '${templateSlug}' not found:`, error)
|
console.error(`Template with slug '${templateSlug}' not found:`, error)
|
||||||
@@ -356,14 +366,62 @@ export class MailingService implements IMailingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderHandlebarsTemplate(template: string, variables: Record<string, any>): string {
|
private async renderTemplate(template: string, variables: Record<string, any>): Promise<string> {
|
||||||
try {
|
// Use custom template renderer if provided
|
||||||
const compiled = Handlebars.compile(template)
|
if (this.config.templateRenderer) {
|
||||||
return compiled(variables)
|
try {
|
||||||
} catch (error) {
|
return await this.config.templateRenderer(template, variables)
|
||||||
console.error('Handlebars template rendering error:', error)
|
} catch (error) {
|
||||||
return template
|
console.error('Custom template renderer error:', error)
|
||||||
|
return template
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const engine = this.config.templateEngine || 'liquidjs'
|
||||||
|
|
||||||
|
// Use LiquidJS if configured
|
||||||
|
if (engine === 'liquidjs') {
|
||||||
|
try {
|
||||||
|
await this.ensureLiquidJSInitialized()
|
||||||
|
if (this.liquid && typeof this.liquid !== 'boolean') {
|
||||||
|
return await this.liquid.parseAndRender(template, variables)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('LiquidJS template rendering error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Mustache if configured
|
||||||
|
if (engine === 'mustache') {
|
||||||
|
try {
|
||||||
|
const mustacheResult = await this.renderWithMustache(template, variables)
|
||||||
|
if (mustacheResult !== null) {
|
||||||
|
return mustacheResult
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Mustache not available. Falling back to simple variable replacement. Install mustache package.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to simple variable replacement
|
||||||
|
return this.simpleVariableReplacement(template, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async renderWithMustache(template: string, variables: Record<string, any>): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const mustacheModule = await import('mustache')
|
||||||
|
const Mustache = mustacheModule.default || mustacheModule
|
||||||
|
return Mustache.render(template, variables)
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private simpleVariableReplacement(template: string, variables: Record<string, any>): string {
|
||||||
|
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
||||||
|
const value = variables[key]
|
||||||
|
return value !== undefined ? String(value) : match
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async renderEmailTemplate(template: EmailTemplate, variables: Record<string, any> = {}): Promise<{ html: string; text: string }> {
|
private async renderEmailTemplate(template: EmailTemplate, variables: Record<string, any> = {}): Promise<{ html: string; text: string }> {
|
||||||
@@ -375,11 +433,11 @@ export class MailingService implements IMailingService {
|
|||||||
let html = serializeRichTextToHTML(template.content)
|
let html = serializeRichTextToHTML(template.content)
|
||||||
let text = serializeRichTextToText(template.content)
|
let text = serializeRichTextToText(template.content)
|
||||||
|
|
||||||
// Apply Handlebars variables to the rendered content
|
// Apply template variables to the rendered content
|
||||||
html = this.renderHandlebarsTemplate(html, variables)
|
html = await this.renderTemplate(html, variables)
|
||||||
text = this.renderHandlebarsTemplate(text, variables)
|
text = await this.renderTemplate(text, variables)
|
||||||
|
|
||||||
return { html, text }
|
return { html, text }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export interface EmailObject {
|
|||||||
|
|
||||||
export type EmailWrapperHook = (email: EmailObject) => EmailObject | Promise<EmailObject>
|
export type EmailWrapperHook = (email: EmailObject) => EmailObject | Promise<EmailObject>
|
||||||
|
|
||||||
|
export type TemplateRendererHook = (template: string, variables: Record<string, any>) => string | Promise<string>
|
||||||
|
|
||||||
|
export type TemplateEngine = 'liquidjs' | 'mustache' | 'simple'
|
||||||
|
|
||||||
export interface MailingPluginConfig {
|
export interface MailingPluginConfig {
|
||||||
collections?: {
|
collections?: {
|
||||||
templates?: string | Partial<CollectionConfig>
|
templates?: string | Partial<CollectionConfig>
|
||||||
@@ -28,6 +32,8 @@ export interface MailingPluginConfig {
|
|||||||
retryAttempts?: number
|
retryAttempts?: number
|
||||||
retryDelay?: number
|
retryDelay?: number
|
||||||
emailWrapper?: EmailWrapperHook
|
emailWrapper?: EmailWrapperHook
|
||||||
|
templateRenderer?: TemplateRendererHook
|
||||||
|
templateEngine?: TemplateEngine
|
||||||
richTextEditor?: RichTextField['editor']
|
richTextEditor?: RichTextField['editor']
|
||||||
onReady?: (payload: any) => Promise<void>
|
onReady?: (payload: any) => Promise<void>
|
||||||
initOrder?: 'before' | 'after'
|
initOrder?: 'before' | 'after'
|
||||||
|
|||||||
7
src/types/mustache.d.ts
vendored
Normal file
7
src/types/mustache.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
declare module 'mustache' {
|
||||||
|
interface MustacheStatic {
|
||||||
|
render(template: string, view?: any, partials?: any, tags?: string[]): string
|
||||||
|
}
|
||||||
|
const mustache: MustacheStatic
|
||||||
|
export = mustache
|
||||||
|
}
|
||||||
160
template-syntax-migration.md
Normal file
160
template-syntax-migration.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Template Engine Options
|
||||||
|
|
||||||
|
The plugin now supports flexible template rendering with multiple options:
|
||||||
|
|
||||||
|
1. **String-based Configuration** (easy setup with built-in engines)
|
||||||
|
2. **Custom Template Renderer Hook** (maximum flexibility)
|
||||||
|
3. **Simple Variable Replacement** (fallback, no dependencies)
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### String-based Template Engine Configuration
|
||||||
|
Easy setup using built-in template engines:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Using LiquidJS (default, requires: npm install liquidjs)
|
||||||
|
mailingPlugin({
|
||||||
|
templateEngine: 'liquidjs'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Using Mustache (requires: npm install mustache)
|
||||||
|
mailingPlugin({
|
||||||
|
templateEngine: 'mustache'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Using simple variable replacement (no dependencies)
|
||||||
|
mailingPlugin({
|
||||||
|
templateEngine: 'simple'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Template Renderer Hook
|
||||||
|
```typescript
|
||||||
|
// Example with Handlebars
|
||||||
|
import Handlebars from 'handlebars'
|
||||||
|
|
||||||
|
mailingPlugin({
|
||||||
|
templateRenderer: async (template: string, variables: Record<string, any>) => {
|
||||||
|
const compiled = Handlebars.compile(template)
|
||||||
|
return compiled(variables)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Example with Mustache
|
||||||
|
import Mustache from 'mustache'
|
||||||
|
|
||||||
|
mailingPlugin({
|
||||||
|
templateRenderer: async (template: string, variables: Record<string, any>) => {
|
||||||
|
return Mustache.render(template, variables)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Example with Nunjucks
|
||||||
|
import nunjucks from 'nunjucks'
|
||||||
|
|
||||||
|
mailingPlugin({
|
||||||
|
templateRenderer: async (template: string, variables: Record<string, any>) => {
|
||||||
|
return nunjucks.renderString(template, variables)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using LiquidJS (Optional)
|
||||||
|
Install the optional dependency:
|
||||||
|
```bash
|
||||||
|
npm install liquidjs
|
||||||
|
# or
|
||||||
|
pnpm add liquidjs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fallback Mode
|
||||||
|
If no custom renderer is provided and neither LiquidJS nor Mustache are installed, simple `{{variable}}` replacement is used.
|
||||||
|
|
||||||
|
## Template Syntax Reference
|
||||||
|
|
||||||
|
### Mustache Syntax (Logic-less)
|
||||||
|
```mustache
|
||||||
|
Hello {{user.name}},
|
||||||
|
|
||||||
|
{{#user.isPremium}}
|
||||||
|
Welcome to premium! Your balance is {{balance}}.
|
||||||
|
{{/user.isPremium}}
|
||||||
|
|
||||||
|
{{#orders}}
|
||||||
|
Order: {{id}} - {{date}}
|
||||||
|
{{/orders}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### LiquidJS Syntax (With Logic)
|
||||||
|
```liquid
|
||||||
|
Hello {{user.name}},
|
||||||
|
|
||||||
|
{% if user.isPremium %}
|
||||||
|
Welcome to premium! Your balance is {{balance | formatCurrency}}.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for order in orders %}
|
||||||
|
Order: {{order.id}} - {{order.date | formatDate: "short"}}
|
||||||
|
{% endfor %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Simple Variable Replacement
|
||||||
|
```
|
||||||
|
Hello {{user.name}},
|
||||||
|
Your balance is {{balance}}.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Handlebars
|
||||||
|
|
||||||
|
### Variables
|
||||||
|
- **Handlebars**: `{{variable}}`
|
||||||
|
- **LiquidJS**: `{{variable}}` (same)
|
||||||
|
|
||||||
|
### Conditionals
|
||||||
|
- **Handlebars**: `{{#if condition}}content{{/if}}`
|
||||||
|
- **LiquidJS**: `{% if condition %}content{% endif %}`
|
||||||
|
|
||||||
|
### Loops
|
||||||
|
- **Handlebars**: `{{#each items}}{{this}}{{/each}}`
|
||||||
|
- **LiquidJS**: `{% for item in items %}{{item}}{% endfor %}`
|
||||||
|
|
||||||
|
### Filters/Helpers
|
||||||
|
- **Handlebars**: `{{formatDate date "short"}}`
|
||||||
|
- **LiquidJS**: `{{date | formatDate: "short"}}`
|
||||||
|
|
||||||
|
### Available Filters
|
||||||
|
- `formatDate` - Format dates (short, long, or default)
|
||||||
|
- `formatCurrency` - Format currency amounts
|
||||||
|
- `capitalize` - Capitalize first letter
|
||||||
|
|
||||||
|
### Comparison Operations (LiquidJS Advantage)
|
||||||
|
- **Handlebars**: Required `{{#ifEquals}}` helper
|
||||||
|
- **LiquidJS**: Built-in: `{% if user.role == "admin" %}`
|
||||||
|
|
||||||
|
## Example Migration
|
||||||
|
|
||||||
|
### Before (Handlebars)
|
||||||
|
```handlebars
|
||||||
|
Hello {{user.name}},
|
||||||
|
|
||||||
|
{{#if user.isPremium}}
|
||||||
|
Welcome to premium! Your balance is {{formatCurrency balance}}.
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#each orders}}
|
||||||
|
Order: {{this.id}} - {{formatDate this.date "short"}}
|
||||||
|
{{/each}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (LiquidJS)
|
||||||
|
```liquid
|
||||||
|
Hello {{user.name}},
|
||||||
|
|
||||||
|
{% if user.isPremium %}
|
||||||
|
Welcome to premium! Your balance is {{balance | formatCurrency}}.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for order in orders %}
|
||||||
|
Order: {{order.id}} - {{order.date | formatDate: "short"}}
|
||||||
|
{% endfor %}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user