# Eta v4 is a 3.5 KB template engine with a layout story and a strict size budget

> Eta is an MIT-licensed embedded template engine for Node, Deno and the browser, written in TypeScript with no dependencies and a size-limit check that fails the build past 3.5 KB. Its distinguishing features are layouts, precompilation and quote handling that survives strings containing the tag delimiter.

**bgub/eta** — Embedded JS template engine for Node, Deno, and the browser. Lighweight, fast, and pluggable. Written in TypeScript

- Repository: https://github.com/bgub/eta
- Website: https://eta.js.org
- Stars: 1,765 · Forks: 87
- Language: TypeScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/bgub-eta

## The size limit is enforced by a script, not promised in a README

Eta's headline claim is a bundle size, and unusually for this kind of claim it is wired into the build rather than left as prose. package.json lists a size-limit configuration whose path is dist/core.js with a limit of 3.5 KB, and a script named size that runs size-limit. The README states the same figure as roughly 3.5 KB minzipped.

Note which file that applies to. It is core.js, not the whole package. The package also exposes a root entry point with a separate type declaration pair, so the full build is larger than the core bundle the limit measures. That distinction matters if you are chasing a bundle budget: the number people quote and the number CI enforces are both about the core.

The rest of the toolchain is visible in package.json. Build is tsdown, lint and format go through Biome with check --error-on-warnings, tests run under Vitest with Istanbul coverage, and the release process uses release-please, visible from .release-please-manifest.json and release-please-config.json. There is a jsr.json at the root, which is what backs the JSR publication mentioned in the usage example.

## Zero dependencies and a Node 20 floor

The dependency list is short enough to state precisely: none at runtime. package.json carries no dependencies field, and the devDependencies are all tooling, among them @biomejs/biome at 2.3.5, tsdown, typescript ^5.9.3 and vitest 4.0.8. The README states zero dependencies as a feature alongside the size and the TypeScript source.

The runtime floor is the thing to check before anything else:

```json
"engines": {
  "node": ">=20"
}
```

Node 20 is the minimum. There is no dual-path claim for older runtimes in what the project publishes.

Dual module format is handled properly. The root export maps import to ./dist/index.mjs and require to ./dist/index.cjs, with types split into .d.mts for import and .d.cts for require, and main and module fields pointing at the same two files. The package is marked type module and sideEffects false, which is what lets a bundler drop it when unused. A ./core subpath export is declared separately, pointing at dist/core.js for both require and import.

## Layouts and partials are what separate it from doT

The feature list names plugins, custom delimiters, custom tags, caching, precompilation, partials, async, blocks, capture, and layout support. Most template engines in the lightweight category give you interpolation and little else; the layout support is the item that decides whether Eta can replace a heavier engine in a server-rendered app.

With layouts, a page template can inherit a shell that owns the document head, the script tags and the navigation, and a child template fills named blocks. Blocks and capture give you the other half: a child can capture the output of a partial into a named block and pass it up. Async matters for the same reason, since a real page pulls in a database or an API rather than only in-memory data.

Precompilation is the feature to understand before optimising anything. If you ship templates to a browser or a Lambda, you can compile once at build time and render the resulting function, which removes the parse step from the hot path. Plugins, custom delimiters and custom tags extend the language itself, and caching is the option that keeps a compiled template in memory across renders. All four are configuration surface, so the engine is small but not rigid.

Layouts and async are the two that take real reading in the documentation at eta.js.org, which the README points to as the thorough reference.

## The quote case that fails on EJS and doT

One line in the README explains why the parser was written rather than borrowed. The example given is a template expression containing a string that itself contains the closing tag delimiter, written as an expression that appends a string with a delimiter inside it. The README's claim is that Eta compiles this correctly while doT or EJS fail on it.

That is a narrow claim about narrow parser behaviour, and it matters more than it first appears. Client-side templates routinely interpolate user-controlled strings, and a naive scanner that stops at the first occurrence of the closing delimiter will truncate the expression or, worse, emit half a token into the output. Getting this right is not a feature you should have to think about, but the fact that a competitor fails at it is a reasonable signal that the case is real.

The related claim is error reporting, listed simply as great. The README does not show what an Eta error looks like, so how it compares in practice has to be judged by writing a template that fails and reading the message rather than by reading the feature list.

## Installing Eta and rendering your first template

The whole getting-started path is three steps. Install from npm:

```bash
npm install eta
```

Create templates/simple.eta in the root of your project, with one line of template syntax:

```eta
Hi <%= it.name %>!
```

Then render it. The `it` prefix is how Eta names the data object passed to a template, and it is the one piece of syntax you must know to read any Eta template:

```js
import { Eta } from "eta";
// or use https://jsr.io/@bgub/eta

const eta = new Eta({ views: path.join(__dirname, "templates") });

const res = eta.render("./simple", { name: "Ben" });
console.log(res); // Hi Ben!
```

Two details are worth copying exactly rather than improvising. The `views` option is how the engine resolves a relative template path such as ./simple, and the file extension is omitted from that path in the render call. Deno users load the same package from JSR at jsr.io/@bgub/eta instead of npm, which is why a jsr.json sits in the repository root.

For a fuller try without writing files, the README links a playground at eta.js.org/playground.

## Integrations are community-built, and Webpack is the gap

The integration list is worth reading closely, because most entries are third-party packages rather than official releases, and the one missing integration is the one a large share of users will look for.

Several entries are community work rather than official. The Visual Studio Code extension is eta-vscode by shadowtime2000. The ESLint support is eslint-plugin-eta, created to provide an ESLint processor so you can lint Eta templates. For Vite there is vite-plugin-eta, and for Koa there is koa-eta. Node-RED has node-red-contrib-eta.

Webpack is the notable absence, and the README does not pretend otherwise. It states there is currently no official Webpack integration, and shares a loader configuration instead:

```javascript
{
  loader: 'html-loader',
  options: {
    preprocessor(content, loaderContext) {
      return eta.render(content, {}, { filename: loaderContext.resourcePath });
    },
  },
}
```

The third argument carries the filename through, which is what makes error messages point at the right file. That is a useful detail, and it is also the sort of thing you will want to reproduce yourself if you maintain a Webpack setup.

Existing users listed in the README include Docusaurus v2, which uses Eta to generate an SSR build, swagger-typescript-api as a code generator templated by Eta, html-bundler-webpack-plugin, SmartDeno on Deno and Oak, and stc for OpenAPI to API conversion.

## Release pace and what v4 means for adoption

The repository is not archived and the last push was on 2026-04-25, which is also the date of release v4.6.0. Before that came v4.5.1 on 2026-02-05 and v4.5.0 on 2025-12-12. The version in package.json is 4.6.0, matching the tag.

One caveat for readers arriving at this repository: the README carries a banner saying you are viewing the source for Eta v4. v3 was the long-lived line, and a codebase or tutorial written against it may not match. The major version exists because the module and export layout changed, so read the current documentation rather than an archived v3 snippet.

Licence is MIT, with LICENSE at the root and funding through GitHub Sponsors. One credit detail matters for compliance if you fork the code: async support, file handling and error formatting were based on EJS, which is licensed Apache-2.0, and the README notes the code was modified and refactored. Syntax and parts of compilation are based on EJS, Nunjucks and doT. A derived work that carries EJS-derived code inherits that lineage, which is not a blocker under either licence but should be recorded in a NOTICE if you redistribute.

## Conclusion

Eta fits a TypeScript project that wants EJS-shaped template syntax with layout inheritance and a core bundle within its 3.5 KB size limit, and a Deno codebase that wants the same templates on both runtimes. It does not fit a CommonJS project on Node 18, because package.json declares engines node >=20, and it does not fit anyone who needs autoescaping by default, since the README documents neither an escape function nor a default escaping mode. Before committing, install eta, render one template containing a string that itself contains the closing tag delimiter, and check that eta.render returns what you expect rather than a parse error.

## FAQ

### How do I install and use the Eta template engine?

Run npm install eta, create a templates/simple.eta file containing Hi <%= it.name %>!, then import the Eta class, pass a views directory, and call eta.render with your data object. The rendered result for the example data is the string Hi Ben!.

### Does Eta support layouts, partials and async templates?

The README lists precompilation, partials, async, blocks, capture and layout support among the engine's features. Layouts are what let a page template inherit a shared shell and fill named blocks.

### What is the minimum Node version for Eta?

package.json declares engines node >=20. The package itself ships both ESM and CommonJS builds, with type declarations resolved separately for import and require.

### Is there an official Webpack integration for Eta?

No. The README states there is currently no official Webpack integration and instead shares a configuration using html-loader with an Eta preprocessor that passes the resource path as the filename.

### What licence is Eta released under?

Eta is MIT licensed. The README also credits EJS, licensed Apache-2.0, as the basis for the async support, file handling and error formatting code, and notes that syntax and parts of compilation are based on EJS, Nunjucks and doT.

## Sources

- [bgub/eta on GitHub](https://github.com/bgub/eta)
- [License: MIT](https://github.com/bgub/eta/blob/main/LICENSE)
- [Project website](https://eta.js.org)
- [README](https://github.com/bgub/eta/blob/main/README.md)
- [Releases](https://github.com/bgub/eta/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bgub-eta
