@aemvite/vite-plugin-aem-handlebars
v0.7.0
Published
Vite plugin that precompiles `.template.hbs` files into Handlebars runtime functions and stubs Storybook/non-template `.hbs` partials, mirroring webpack handlebars-loader + IgnorePlugin for AEM clientlib builds.
Maintainers
Readme
@aemvite/vite-plugin-aem-handlebars
Vite plugin that precompiles
*.template.hbsfiles into runtime Handlebars functions and stubs Storybook-only / non-template.hbspartials. Drop-in replacement for the legacy webpackhandlebars-loader+IgnorePluginpair used by AEM clientlib builds.
Part of the @aemvite/* toolchain.
Install
npm i -D @aemvite/vite-plugin-aem-handlebars handlebars- Peer dependencies:
vite^8,handlebars^4.7 - Engines: Node
^20.19.0 || ^22.18.0 || >=24.11.0 - Consumers must install
handlebarsthemselves so they control the templating runtime version.
What it does
Two concerns under one plugin, both mirroring legacy webpack behavior:
Precompile templates. Every
*.template.hbsfile is read at build time, run throughHandlebars.precompile, and replaced with a tiny ESM module that importshandlebars/runtimeand exports the compiled template function. Soimport Foo from "./foo.template.hbs"; Foo({ data })keeps returning an HTML string.Stub Storybook-only modules. Storybook stories (
*.stories.{js,ts,tsx}) and non-template*.hbspartials are resolved to an empty ESM module so any transitive dynamic imports that reach them do not break the production build.
Usage
Direct (any Vite project)
// vite.config.ts
import { defineConfig } from "vite";
import { aemHandlebars } from "@aemvite/vite-plugin-aem-handlebars";
export default defineConfig({
plugins: [aemHandlebars()],
});Through @aemvite/aem-config (recommended)
Set the handlebars flag in your aem.config.{ts,mjs} and the plugin is
auto-wired into every clientlib build:
import { defineAemConfig } from "@aemvite/aem-config";
export default defineAemConfig({
clientLibRoot: "../ui.apps/.../clientlibs",
handlebars: true, // or: { precompileOptions: { strict: true } }
clientlibs: [
/* ... */
],
});The same field is available per-clientlib and takes precedence over the
global value. Set handlebars: false per-clientlib to opt out when the
global is enabled.
API reference
Plugin
| Export | Signature | Notes |
|---|---|---|
| aemHandlebars | (options?: AemHandlebarsOptions) => Plugin | Vite plugin (also the package default export). enforce: "pre". |
Types
interface AemHandlebarsOptions {
/** Default: ".template.hbs". */
templateSuffix?: string;
/** Forwarded verbatim to `Handlebars.precompile`. Default: { strict: false }. */
precompileOptions?: Record<string, unknown>;
/** Module specifier for the emitted import. Default: "handlebars/runtime". */
runtime?: string;
/**
* Built-in stubbing always covers `*.stories.{js,ts,tsx}` and non-template
* `*.hbs` partials. Pass an array of RegExp / pattern strings to add
* ADDITIONAL stub patterns. Pass `false` to disable stubbing entirely.
*/
ignore?: false | readonly (RegExp | string)[];
}Emitted module shape
For a file foo.template.hbs:
import Handlebars from "handlebars/runtime";
export default Handlebars.template(<precompiled program>);So consumers keep the webpack-era ergonomics:
import Foo from "./foo.template.hbs";
const html = Foo({ data });Make sure the Handlebars helpers your templates depend on are registered on
the same handlebars/runtime module before the templates execute (a
typical pattern is a small handlebars-helpers.ts imported early in the
clientlib entry).
Behavior
- Only
*.template.hbsfiles are transformed. All other module IDs pass through thetransformhook untouched. - Stubbing is
enforce: "pre"so the empty module is loaded before any other plugin (e.g. an asset loader) tries to parse the.hbssource. handlebarsis a peer dependency. The plugin imports it once and usesHandlebars.precompileat build time; consumers shiphandlebars/runtimeat runtime.- Idempotent and pure. No filesystem mutations, no temp files; only
the
transform/resolveId/loadhooks run.
Notes & caveats
- The default
ignorelist does not include project-specific paths (e.g. legacyvendors/tabs.js). Add them viaignore: [/vendors\/tabs\.js$/]or layer a small project-local plugin in youraem.config.{ts,mjs}plugins:array. - Source maps are emitted as
nullfor the transformed templates — the precompiled output bears no useful relationship to the source.hbssyntax. Storybook-stub modules also have no map.
License
MIT © Luca Nerlich
Repository
https://github.com/LucaNerlich/aem-vite (this package lives in
packages/vite-plugin-aem-handlebars).
