LiquidJS

A TypeScript Liquid template engine for Node.js, browsers, and the CLI — Shopify, Jekyll, and GitHub Pages compatible.

Library
npm
v10.30.0
1,882 stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum. How we score it →
83 /100 Excellent
Development Activity 72
Maintenance 84
Community 76
Maturity 60
Momentum 40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation. How we score it →
88 /100 Excellent
Architecture 88
Code Quality 90
Innovation 82
Learning Curve 90

LiquidJS is a full reimplementation of Shopify’s Liquid template language in pure TypeScript, built to run identically in Node.js, in the browser (via a UMD/ESM bundle), and as a standalone CLI. It targets the same syntax used by Shopify themes, Jekyll, and GitHub Pages, so existing Liquid templates and filters generally port over without rewriting.

The engine exposes both async and sync render APIs backed by a single generator-based implementation per feature, so plugins, custom tags, and filters never need to be written twice for each execution mode. It supports partials and layouts with pluggable file-system resolution, an Express view-engine adapter, static template analysis (for extracting referenced variables/partials without executing a render), and a sandboxed evaluation model that avoids arbitrary JavaScript execution inside templates.

It’s the template engine underneath tools like Eleventy (11ty) and is used directly by Shopify’s own CLI and checkout tooling, alongside adopters like GitHub Docs, Kibana, and Directus — spanning static site generation, themeable emails, and safely rendering user- or merchant-authored templates in multi-tenant products.

What You Get

  • A Liquid engine class with parse/render/parseAndRender methods in both async and Sync variants, all driven by one shared generator implementation per operation
  • The full standard Liquid tag set (if, for, case, assign, capture, include, render, layout, tablerow, cycle, and more) plus the standard filter library (string, array, math, date, URL, HTML, base64, crypto)
  • Pluggable custom tags and filters via registerTag/registerFilter, and a plugin() hook for packaging reusable extensions
  • Partial and layout resolution through a configurable FS interface, supporting custom file-system or virtual-file lookups instead of the Node fs module
  • An Express view-engine adapter (engine.express()) for dropping LiquidJS straight into app.set('view engine', ...)
  • Static analysis (analyze/analyzeSync) that walks a parsed template to list referenced variables and partials without executing a render — useful for linting or dependency graphs
  • Typed error classes (ParseError, RenderError, TokenizationError, UndefinedVariableError) that carry the offending token and surrounding template context in the message
  • Prebuilt browser bundles (UMD and ESM) alongside the Node.js build, so the same engine runs client-side without a bundler-specific build step

Common Use Cases

  • Static site generators and build pipelines (e.g. Eleventy) rendering Markdown/HTML pages through Liquid layouts and includes
  • Multi-tenant SaaS products letting merchants or customers author their own templates (emails, checkout pages, storefront themes) without exposing arbitrary JS execution
  • Server-rendered Express applications using .liquid views via the built-in Express engine adapter
  • Porting or maintaining Shopify theme code, Jekyll sites, or GitHub Pages content in a Node.js/TypeScript toolchain
  • Client-side template rendering in the browser for previewing or live-editing Liquid templates (e.g. the project’s own playground)

Under The Hood

Architecture

The engine is organized as a pipeline of clearly separated stages: src/tokens turns raw template strings into a token stream, src/parser (Parser/Tokenizer) turns tokens into a Template[] AST, and src/render (Render, Expression, Operators) walks that AST against a Context/Scope (src/context) to produce output through src/emitters. src/tags and src/filters register into a per-Liquid-instance registry (this.tags, this.filters) rather than a global one, so multiple engine instances can carry independent extension sets; src/fs abstracts partial/layout file resolution behind a FS/LookupType interface rather than hard-coding Node’s fs. The defining architectural choice, documented in the repo’s own contributor guide, is that every feature is written as a single function* generator consumed by either toPromise() (async driver) or toValueSync() (sync driver) — Liquid.render()/renderSync() and parseFile()/parseFileSync() are thin wrappers over the same generator body, eliminating the duplicate async/sync implementations common in comparable engines. Swapping this generator-driver core would ripple through essentially every tag, filter, and the Context value-resolution path, since all of them yield through it. Tech Stack The engine is authored entirely in TypeScript (target ES6, strict: true) with a single runtime dependency (commander, for the CLI binary) — the render/parse core has no external dependencies. Rollup (rollup.config.mjs) builds four output targets from one source tree: CommonJS and ESM for Node, plus UMD and browser ESM bundles, published together under dist/ with package.json’s browser field swapping in the browser build automatically for bundlers. TypeDoc generates API docs, and a separate Hexo-based documentation site (docs/, navy theme) is built and deployed alongside the library. The bundled CLI (bin/liquid.js) and an Express integration are both layered on top of the same public Liquid class rather than living in separate packages. Code Quality Testing is extensive and split by concern: Jest-based integration tests (test/integration) covering tags, filters, context, drops, and static analysis import from src/ against current TypeScript; a separate test/e2e suite imports only from the built dist/ package root, guaranteeing the published bundle behaves like the source. Error handling is explicit and typed rather than swallowed: a LiquidError base class captures the offending Token, the original wrapped error, and renders a message with surrounding template context, with dedicated subclasses (ParseError, RenderError, TokenizationError, UndefinedVariableError) rather than generic Error throws. ESLint (TypeScript-ESLint, eslint-plugin-deprecation) and strict-mode TypeScript run in CI alongside build, test, coverage, and a performance-regression check (perf:diff) on every pull request, and the project’s own contributor guide enforces minimal, narrowly scoped diffs and non-narrative code comments. What Makes It Unique The unified generator-based sync/async core is the standout technical decision: rather than hand-maintaining parallel synchronous and asynchronous code paths (a common source of drift in template engines that offer both), every tag and filter is implemented once as a generator and driven by whichever driver the caller needs, including for genuinely async operations like resolving a partial from a remote FS implementation. Combined with static analysis that extracts a template’s referenced variables and partials without rendering it, and a sandboxed evaluation model that never executes arbitrary host-language code, this makes LiquidJS well suited to the specific niche of safely rendering user-authored templates in multi-tenant systems — a harder requirement than most general-purpose JavaScript templating libraries take on.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers