chore: server-and-serverless template
This commit is contained in:
211
packages/functions/.pikku/email/pikku-emails.gen.ts
Normal file
211
packages/functions/.pikku/email/pikku-emails.gen.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.54
|
||||
*/
|
||||
type EmailPrimitive = string | number | boolean | null | undefined
|
||||
type EmailTemplateValue = EmailPrimitive | Record<string, unknown> | Array<unknown>
|
||||
|
||||
const EMAIL_THEME = {
|
||||
"appName": "Pikku Starter",
|
||||
"fonts": {
|
||||
"body": "Inter, Arial, sans-serif"
|
||||
},
|
||||
"colors": {
|
||||
"canvas": "#0b1020",
|
||||
"surface": "#11182d",
|
||||
"border": "#263252",
|
||||
"text": "#f7f8fb",
|
||||
"muted": "#a8b0c5",
|
||||
"accent": "#7dd3fc",
|
||||
"button": "#f59e0b",
|
||||
"buttonText": "#111827"
|
||||
}
|
||||
} as const
|
||||
const EMAIL_LOCALES = {
|
||||
"en": {
|
||||
"common": {
|
||||
"footer": "If you did not create this account, you can safely ignore this email."
|
||||
},
|
||||
"confirmEmail": {
|
||||
"subject": "Confirm your email for {{appName}}",
|
||||
"preview": "Confirm your email address to finish setting up your account.",
|
||||
"heading": "Confirm your email",
|
||||
"intro": "Thanks for signing up for {{appName}}! Confirm {{email}} to finish setting up your account.",
|
||||
"cta": "Confirm email",
|
||||
"fallback": "If the button does not work, copy and paste this URL into your browser:",
|
||||
"expiry": "This link expires in 24 hours."
|
||||
}
|
||||
}
|
||||
} as const
|
||||
const EMAIL_PARTIALS = {
|
||||
"footer": "<p style=\"margin:32px 0 0;color:{{theme.colors.muted}};font-size:13px;line-height:1.6;\">\n {{t.common.footer}}\n</p>\n",
|
||||
"layout": "<!doctype html>\n<html lang=\"{{locale}}\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>{{subject}}</title>\n </head>\n <body style=\"margin:0;padding:0;background:{{theme.colors.canvas}};font-family:{{theme.fonts.body}};\">\n <div style=\"max-width:560px;margin:0 auto;padding:32px 16px;\">\n {{content}}\n </div>\n </body>\n</html>\n"
|
||||
} as const
|
||||
const EMAIL_TEMPLATES = {
|
||||
"confirm-email": {
|
||||
"html": "<p style=\"margin:0 0 12px;color:{{theme.colors.accent}};font-size:12px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;\">\n {{appName}}\n</p>\n<h1 style=\"margin:0 0 16px;color:{{theme.colors.text}};font-size:28px;line-height:1.1;\">\n {{t.confirmEmail.heading}}\n</h1>\n<p style=\"margin:0 0 24px;color:{{theme.colors.muted}};font-size:16px;line-height:1.6;\">\n {{t.confirmEmail.intro}}\n</p>\n<p style=\"margin:0 0 24px;\">\n <a\n href=\"{{confirmUrl}}\"\n style=\"display:inline-block;background:{{theme.colors.button}};color:{{theme.colors.buttonText}};text-decoration:none;padding:14px 22px;border-radius:999px;font-weight:700;\"\n >\n {{t.confirmEmail.cta}}\n </a>\n</p>\n<p style=\"margin:0 0 8px;color:{{theme.colors.text}};font-size:14px;line-height:1.6;\">\n {{t.confirmEmail.fallback}}\n</p>\n<p style=\"margin:0;color:{{theme.colors.muted}};font-size:14px;line-height:1.6;word-break:break-all;\">\n {{confirmUrl}}\n</p>\n<p style=\"margin:24px 0 0;color:{{theme.colors.muted}};font-size:13px;line-height:1.6;\">\n {{t.confirmEmail.expiry}}\n</p>\n{{> footer}}\n",
|
||||
"subject": "{{t.confirmEmail.subject}}\n",
|
||||
"text": "{{t.confirmEmail.heading}}\n\n{{t.confirmEmail.intro}}\n\n{{confirmUrl}}\n\n{{t.confirmEmail.expiry}}\n",
|
||||
"variables": [
|
||||
"appName",
|
||||
"confirmUrl",
|
||||
"email"
|
||||
],
|
||||
"hashes": {
|
||||
"en": {
|
||||
"contentHash": "fa3ef0d0ff652703fc0586c5c78b4c4820206a09bd47651c56a9f1c26a5d2cfd",
|
||||
"htmlHash": "c4b9403e096b1f6c439e120f701bf8cbc6bdba9fdb8e4f21fbad524961343a39",
|
||||
"subjectHash": "209d4ac15c8f7e70e14de04c81724beb00dac7f4ea450a6e8ce527faf4a0ad75",
|
||||
"textHash": "4b494839407f2817290b9b08103cd84103e53a6339ff4e4f24e71dd89ebb7d13"
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
export type EmailTemplateName = keyof typeof EMAIL_TEMPLATES
|
||||
export type EmailLocale = keyof typeof EMAIL_LOCALES
|
||||
|
||||
type TemplateVariableMap = {
|
||||
"confirm-email": {
|
||||
"appName"?: EmailTemplateValue
|
||||
"confirmUrl"?: EmailTemplateValue
|
||||
"email"?: EmailTemplateValue
|
||||
}
|
||||
}
|
||||
|
||||
export type EmailTemplateVariables<TName extends EmailTemplateName> =
|
||||
TemplateVariableMap[TName]
|
||||
|
||||
export type RenderEmailInput<TName extends EmailTemplateName> = {
|
||||
name: TName
|
||||
locale?: EmailLocale
|
||||
data: EmailTemplateVariables<TName>
|
||||
}
|
||||
|
||||
export type RenderedEmail<TName extends EmailTemplateName> = {
|
||||
name: TName
|
||||
locale: EmailLocale
|
||||
subject: string
|
||||
html: string
|
||||
text?: string
|
||||
variables: ReadonlyArray<string>
|
||||
hash: (typeof EMAIL_TEMPLATES)[TName]['hashes'][EmailLocale]['contentHash']
|
||||
}
|
||||
|
||||
function getNestedValue(source: Record<string, unknown>, path: string): string {
|
||||
const segments = path.split('.')
|
||||
let current: unknown = source
|
||||
for (const segment of segments) {
|
||||
if (!current || typeof current !== 'object' || !(segment in current)) {
|
||||
return ''
|
||||
}
|
||||
current = (current as Record<string, unknown>)[segment]
|
||||
}
|
||||
return typeof current === 'string' || typeof current === 'number'
|
||||
? String(current)
|
||||
: ''
|
||||
}
|
||||
|
||||
function applyTemplate(source: string, context: Record<string, unknown>): string {
|
||||
return source.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, rawKey) => {
|
||||
const key = String(rawKey).trim()
|
||||
if (key === 'content') {
|
||||
return typeof context.content === 'string' ? context.content : ''
|
||||
}
|
||||
if (key.startsWith('>')) {
|
||||
return ''
|
||||
}
|
||||
return getNestedValue(context, key)
|
||||
})
|
||||
}
|
||||
|
||||
function renderTemplate(source: string, context: Record<string, unknown>): string {
|
||||
let rendered = source
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
const next = applyTemplate(rendered, context)
|
||||
if (next === rendered) break
|
||||
rendered = next
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
function renderPartial(name: string, context: Record<string, unknown>): string {
|
||||
const partial = EMAIL_PARTIALS[name as keyof typeof EMAIL_PARTIALS]
|
||||
return partial ? renderTemplate(partial, context) : ''
|
||||
}
|
||||
|
||||
export const EMAILS = EMAIL_TEMPLATES
|
||||
|
||||
export function renderEmailTemplate<TName extends EmailTemplateName>(
|
||||
input: RenderEmailInput<TName>
|
||||
): RenderedEmail<TName> {
|
||||
const locale = (input.locale ?? 'en') as EmailLocale
|
||||
const template = EMAIL_TEMPLATES[input.name]
|
||||
if (!template) {
|
||||
throw new Error(`Unknown email template: ${String(input.name)}`)
|
||||
}
|
||||
|
||||
const strings = EMAIL_LOCALES[locale]
|
||||
if (!strings) {
|
||||
throw new Error(`Unknown email locale: ${String(locale)}`)
|
||||
}
|
||||
|
||||
const data = (input.data ?? {}) as Record<string, unknown>
|
||||
const appName =
|
||||
(typeof data.appName === 'string' && data.appName) ||
|
||||
getNestedValue(EMAIL_THEME as Record<string, unknown>, 'appName')
|
||||
|
||||
const baseContext = {
|
||||
...data,
|
||||
locale,
|
||||
theme: EMAIL_THEME,
|
||||
t: strings,
|
||||
appName,
|
||||
}
|
||||
|
||||
const subject = renderTemplate(template.subject, baseContext).trim()
|
||||
const htmlWithPartials = template.html.replace(
|
||||
/\{\{\s*>\s*([a-zA-Z0-9-_/.]+)\s*\}\}/g,
|
||||
(_match, partialName) => `@@PARTIAL:${String(partialName).trim()}@@`
|
||||
)
|
||||
|
||||
let htmlBody = renderTemplate(htmlWithPartials, {
|
||||
...baseContext,
|
||||
subject,
|
||||
})
|
||||
|
||||
const partialMatches = [...htmlBody.matchAll(/@@PARTIAL:([^@]+)@@/g)]
|
||||
for (const match of partialMatches) {
|
||||
const rendered = renderPartial(match[1], {
|
||||
...baseContext,
|
||||
subject,
|
||||
})
|
||||
htmlBody = htmlBody.replace(match[0], rendered)
|
||||
}
|
||||
|
||||
const html = EMAIL_PARTIALS.layout
|
||||
? renderTemplate(EMAIL_PARTIALS.layout, {
|
||||
...baseContext,
|
||||
subject,
|
||||
content: htmlBody,
|
||||
})
|
||||
: htmlBody
|
||||
|
||||
const text = template.text
|
||||
? renderTemplate(template.text, {
|
||||
...baseContext,
|
||||
subject,
|
||||
}).trim()
|
||||
: undefined
|
||||
const hash = (template.hashes[locale]?.contentHash ??
|
||||
'') as RenderedEmail<TName>['hash']
|
||||
|
||||
return {
|
||||
name: input.name,
|
||||
locale,
|
||||
subject,
|
||||
html,
|
||||
...(text ? { text } : {}),
|
||||
variables: template.variables,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user