Skip to content

[#30] Render templates with Handlebars and release 4.0.0 - #32

Merged
sabind merged 1 commit into
masterfrom
30-handlebars-renderer
Oct 20, 2026
Merged

sabind merged 1 commit into
masterfrom
30-handlebars-renderer

Conversation

@sabind

@sabind sabind commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Why

Closes #30

The hand-written {{ }} renderer needed three fixes in the 3.0.2 batch, including a privately reported re-expansion bug. We don't want to re-invent the wheel: template renderers have lots of risks and loopholes that we don't want to own or champion. Handlebars covers all our features and supersets our renderer.

What

4.0 compiles and renders every template with Handlebars, keeping the 3.x API and {{slot:name}} syntax, and adds escape modes, strict mode, helpers and partials. Image inlining works for the first time, off by default in the CLI, and the CLI can now render a template with sample data as a local test of the email. A migration guide at MIGRATION.md lists every difference from 3.x and ends with a prompt for converting templates with an AI assistant.

Changes

  • Handlebars replaces the custom renderer. src/engine.ts owns every contact with Handlebars: an isolated instance per compile, Handlebars' own escaping behind escape: 'html' | 'none' with the 3.x booleans as aliases, Handlebars' strict mode behind strict: true with the parent-context lookup turned off because the two crash together, {{slot:name}} rewritten in the AST to the built-in lookup helper on the root slots, compile-time validation, and a handlebars option for helpers, partials and other compile options with compat, noEscape, strict and the lookup helper reserved. renderTemplate is one call into it; compileEmailTemplate compiles its three templates once and render calls them. A TypeError during a strict render is rethrown as Strict render failed: ... with a hint and the original as cause, and src/style.ts decodes the three entities Handlebars adds so tone names still match. We don't want to re-invent the wheel because template renderers have lots of risks and loopholes that we don't want to own or champion; Handlebars covers all features and supersets our stuff.
  • Image inlining works, and the CLI inlines only when asked. inlineEmailHtml runs juice's inlineExternal before the CSS pass when any of images, links, scripts or svgs is set, each true, false or a kilobyte cap; an unreachable resource is skipped with juice's warning unless webResources.strict: true. The CLI takes --inlineImages, accepts --noInlineImages as a deprecated no-op with one stderr line, and gains --data <file> and --strict so it renders the finished email with sample data. Inline images improve the performance of emails to clients, since image URLs are opt-in on email clients; the features had to work, so testing confirmed and fixed them. Failed emails are the negative case, so an email with a broken image is preferred by default, and the developer controls it per template: a password reset must go out, a marketing email with broken images should fail. The CLI is meant as a local test of the email, so it has the same features.
  • Migration guide, README, PR template and release. MIGRATION.md opens by saying 4.0 renders with Handlebars, gives a verified before-and-after example for every difference, and ends with a prompt to paste into an AI assistant with your templates and sample data; the README gets a callout at the top, the Handlebars-based template syntax, the new options, the juice.webResources kinds and the CLI flags; .github/pull_request_template.md is ported from the developer handbook with its Closes convention; CONTRIBUTING.md shows the v4 tag; version 4.0.0. We say 4.0 is 100% Handlebars and go that way with a migration guide, written so it also works as an AI prompt. Docs only, so no risk.
  • Tests. test/engine.test.ts is new; the 3.0.2 renderer regression tests are all kept, with two assertions changed for behaviour the spec changes on purpose; inert-value, strict, compile-option, example, CLI and image failure-mode tests are added. These are the tests and they cover all the functions we updated and added; adding them is how so many issues were found.
  • Spec record under .freethinking/specs/handlebars-renderer/: the approved requirements, design and task list this branch was built from. Specs are just docs.

Testing

  • npm ci, npm audit --audit-level=high --omit=dev, npm run build, npm test, npm run examples, npm pack --dry-run: all exit 0 on Node 24. Suite: 7 files, 286 tests passing. Node 20 and 22 are left to CI.
  • Visual snapshots: npm run test:visual passes 5 of 5 against freshly rendered examples; no snapshot changed.
  • Coverage of src/ (measured once with a temporary provider, not added as a dependency): engine.ts and template.ts 100% lines, branches and functions; compile.ts 86.5% lines; cli.ts 64.2% lines, the gaps being argument-error paths unchanged by this branch; total 86.9% lines.
  • Every before-and-after example in MIGRATION.md was run through the 3.0.2 renderer and the 4.0 code.
  • The 22 renderer regression tests from 3.0.2 are present and pass.

Risk and rollout

Breaking change, released as 4.0.0. It could break existing clients depending on how they used the previous library; MIGRATION.md lists every difference. Visible changes for 3.x users: Handlebars' entity set (&#x27;, = and backtick escaped), bare blocks over 0 and "" render, malformed templates throw at compile time, standalone-tag whitespace, object values render [object Object], a row's null field falls through to the enclosing context, the CLI no longer inlines images by default and fails on a malformed template. No rollout steps beyond the major version; roll back by pinning to 3.0.2.

Notes for reviewers

  • Assess how Handlebars is wrapped in src/engine.ts; the rest is straightforward and reduces complexity.
  • Assess the image-inlining degradation modes and say whether the expected outcomes are right: skip and warn by default, fail with webResources.strict: true.
  • Review the MIGRATION.md edits and the AI prompt at its end, and the README.
  • Under strict mode Handlebars names the last path segment it reached, not the missing first one, and a deeper path through a missing object gives the Strict render failed wrapper; both are documented and tested.
  • Node 20 and 22 ran only in CI.

🤖 Generated with Claude Code

Replace the hand-written renderer with Handlebars behind the 3.x API,
add escape modes, strict mode, helpers and partials, make image
inlining work with the CLI inlining only on request, add --data and
--strict to the CLI, and ship MIGRATION.md with a prompt for converting
3.x templates. Closes #30.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@sabind
sabind merged commit 64cd54e into master Oct 20, 2026
4 checks passed
@sabind
sabind deleted the 30-handlebars-renderer branch October 20, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decide whether to keep the custom template renderer or adopt mustache.js

2 participants