diff --git a/.changeset/25190-retire-render-template-export.md b/.changeset/25190-retire-render-template-export.md new file mode 100644 index 00000000000..682d045fc41 --- /dev/null +++ b/.changeset/25190-retire-render-template-export.md @@ -0,0 +1,18 @@ +--- +'@objectstack/plugin-email': patch +--- + +fix(plugin-email)!: retire `renderTemplate`, the render export that takes an email template body with no save moment + +The hole-placement rule for mail templates is judged where a template is authored or published: `os validate`, `os build`, `os lint` and the runtime publish gate. The `renderTemplate` function on the package entry took a template body inline and rendered its html face at once, with no save moment, so the rule could not see it. It leaves the published entry of `@objectstack/plugin-email`. Nothing in this repository imported it from that entry. The renderer stays inside the package: `services.email` still renders every stored `email_template` through it, unchanged. + +Clause-②: no (narrowing) +Metadata-protocol: none + + + +**BREAKING** (a runtime export is removed). It ships as `patch` because the metadata protocol does not move. There is no staged window. + +- FROM `renderTemplate` TO an `email_template` item for the name and locale, rendered by `services.email` through `renderTemplate({ template, data, locale })` (render only) or `sendTemplate` (render and send). + +Unchanged: `IEmailService.renderTemplate` (the render-by-name method of the same name), `requireVars`, `htmlToText` and the template loader; and how every stored template renders. diff --git a/packages/plugins/plugin-email/src/index.ts b/packages/plugins/plugin-email/src/index.ts index c3aa51380e3..6f2d1cd7de3 100644 --- a/packages/plugins/plugin-email/src/index.ts +++ b/packages/plugins/plugin-email/src/index.ts @@ -85,7 +85,12 @@ export { type AttachmentReclaimOutcome, type ReclaimAttachmentContentOptions, } from './attachment-reclaim.js'; -export { renderTemplate, requireVars, htmlToText } from './template-engine.js'; +// [#25190] `renderTemplate` is not on this entry: it took a template body +// inline and rendered its html face with no save moment, so the hole-placement +// rule, judged where a template is authored or published, could not see it +// (ruling 6335348115 on #25082, F2 B3). A template is an `email_template` +// item, rendered by `services.email` (`sendTemplate` / `renderTemplate`). +export { requireVars, htmlToText } from './template-engine.js'; export { ResendTransport, PostmarkTransport, diff --git a/packages/plugins/plugin-email/src/render-export-retired.pin.test.ts b/packages/plugins/plugin-email/src/render-export-retired.pin.test.ts new file mode 100644 index 00000000000..686b2b616d5 --- /dev/null +++ b/packages/plugins/plugin-email/src/render-export-retired.pin.test.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #25190 — `renderTemplate`, the module function that took a template body + * inline, is retired from this package's published entry (ruling 6335348115 + * on #25082, F2 B3; ADR-0087 `runtime-export-retired`). The hole-placement + * rule judges a template where it is authored or published, and a body handed + * straight to the function was never either, so it was the one door the rule + * could not see. `IEmailService.renderTemplate` — the render-by-name method of + * the same name — is a different door and stays. + * + * Pinned both ways: the entry no longer names it (the helpers beside it stay), + * and a stored template still renders through the service, by name — the html + * face with every hole encoded, the subject (a plain-text face) verbatim — + * for both the render-only and the sending door. + */ + +import { describe, it, expect } from 'vitest'; +import type { IEmailTransport, NormalizedEmailMessage, TransportSendResult } from '@objectstack/spec/contracts'; +import * as entry from './index.js'; +import type { EmailTemplateRow, TemplateLoader } from './index.js'; +import { EmailService } from './email-service.js'; + +const row: EmailTemplateRow = { + name: 'deal.won', + locale: 'en-US', + subject: 'Won {{deal.name}}', + body_html: '