npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

drip-templates

v0.2.0

Published

Reactive templates compiler and runtime for Liquid/Shopify themes

Readme

drip-templates

Reactive templates compiler and runtime for Shopify/Liquid themes. Write templates once in JSX-like syntax, compile to both server-rendered Liquid and client-side reactive JavaScript.

Features

  • SSR + Hydration - Server-rendered Liquid for fast initial load, client reactivity after
  • Single Source of Truth - One template compiles to both SSR and client code
  • No XSS Vulnerabilities - Uses template cloning and textContent, never innerHTML
  • Lightweight Runtime - ~21kb bundled, no framework dependencies
  • Fine-Grained Updates - Only updates changed text/attributes, DOM nodes persist
  • Keyed List Reconciliation - Efficient add/remove/reorder with stable DOM nodes

Installation

npm install drip-templates

Quick Start

1. Create a Template

Create src/templates/cart-item.jsx:

export default ({ item }) => (
  <div $item={item.key} $scope="cart.items[key]">
    <span $text={item.title} />
    <span $text={money(item.price)} class="price" />
    <button $action="cart.remove" $disabled={item.removing}>
      <span $text={t('cart.remove')} />
    </button>
  </div>
);

2. Compile Templates

npx drip build --input ./src/templates --output .

This generates:

  • snippets/_cart-item.liquid - Server-rendered Liquid snippet
  • assets/_cart-item-template.liquid - Client-side <template> element
  • assets/template-manifest.js - Binding metadata for the runtime

3. Use in Your Theme

In your Liquid template:

{% comment %} Render server-side {% endcomment %}
{% for item in cart.items %}
  {% render '_cart-item', item: item %}
{% endfor %}

{% comment %} Include template element for client-side cloning {% endcomment %}
{% render '_cart-item-template' %}

{% comment %} Initialize state {% endcomment %}
<script id="cart-state" type="application/json">
  {{ cart | json }}
</script>

In your JavaScript:

import { Drip } from '/assets/drip-runtime.js';
import { templates } from '/assets/template-manifest.js';

const app = new Drip({
  manifest: { templates },
  translations: { 'cart.remove': 'Remove' },
  initialState: JSON.parse(document.getElementById('cart-state').textContent)
});

app
  .on('cart.remove', async (event, { key }) => {
    app.set(`cart.items[${key}].removing`, true);
    const response = await fetch(`/cart/change.js`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id: key, quantity: 0 })
    });
    const cart = await response.json();
    app.set('cart', cart);
  })
  .hydrate(document.querySelector('.cart-container'))
  .mount('tmpl-cart-item', document.querySelector('.cart-items'), 'cart.items');

Template Syntax

Templates use JSX-like syntax with $ prefixed binding markers.

Binding Markers

| Syntax | Purpose | Generated Attribute | |--------|---------|---------------------| | $text={expr} | Text content binding | data-bind-text="path" | | $value={expr} | Input value binding | data-bind-value="path" | | $checked={expr} | Checkbox binding | data-bind-checked="path" | | $disabled={expr} | Disabled state | data-bind-disabled="path" | | $hidden={expr} | Visibility toggle | data-bind-hidden="path" | | $attr:name={expr} | Attribute binding | data-bind-attr-name="path" | | $item={key} | List item key | data-item="{{ item.key }}" | | $scope="path" | Scope for nested bindings | data-scope="path" | | $action="handler" | Click handler | data-action="handler" | | $action:event="handler" | Custom event handler | data-action-event="handler" |

SSR Condition Attributes

For server-side rendering, you often need Liquid conditions that differ from client-side state paths. Use $ssr: prefixed attributes to specify Liquid-specific conditions:

| Syntax | Purpose | Generated Output | |--------|---------|------------------| | $ssr:hidden="liquid_condition" | SSR hidden state | {% if condition %} hidden{% endif %} | | $ssr:disabled="liquid_condition" | SSR disabled state | {% if condition %} disabled{% endif %} |

Example:

<p
  $hidden={item.hide_variant}
  $ssr:hidden="item.variant_title == blank or item.variant_title == 'Default Title'"
>
  {item.variant_title}
</p>

<button
  $disabled={item.at_min_quantity}
  $ssr:disabled="item.quantity <= 1"
>
  Decrease
</button>

This generates:

<p data-bind-hidden="hide_variant"{% if item.variant_title == blank or item.variant_title == 'Default Title' %} hidden{% endif %}>
  {{ item.variant_title }}
</p>

<button data-bind-disabled="at_min_quantity"{% if item.quantity <= 1 %} disabled{% endif %}>
  Decrease
</button>

The $ssr: conditions only affect the Liquid SSR output. The client-side runtime uses the regular $hidden/$disabled binding paths.

Helpers

Built-in helper functions for formatting:

{money(item.price)}        // Format as currency: $12.34
{date(item.created)}       // Format date
{plural(count, 'item')}    // Pluralize: "1 item" or "2 items"
{t('cart.remove')}         // Translation lookup

Scope Pattern

The $scope attribute uses [key] as a placeholder that gets replaced with the actual item key:

<div $item={item.key} $scope="cart.items[key]">
  {/* Bindings inside resolve to cart.items[actualKey].path */}
  <span $text={item.title} />  {/* -> cart.items[abc].title */}
</div>

CLI Reference

npx drip <command> [options]

Commands:
  build    Compile all templates
  watch    Watch mode - recompile on changes

Options:
  --input <path>   Input directory for templates (default: ./src/templates)
  --output <path>  Output directory root (default: .)
  --help, -h       Show help

Examples

# Build templates
npx drip build

# Build with custom paths
npx drip build --input ./templates --output ./theme

# Watch mode
npx drip watch --input ./src/templates

Runtime API

new Drip(options)

Create a new Drip instance.

const app = new Drip({
  manifest: { templates },      // Template manifest from compiler
  translations: {},             // Translation strings
  helpers: {},                  // Additional helper functions
  initialState: {}              // Initial state object
});

app.on(action, callback)

Register an action handler.

app.on('cart.remove', (event, context) => {
  // event: DOM event
  // context: { scope, key, target }
});

app.hydrate(container)

Hydrate server-rendered content by wiring up bindings.

app.hydrate(document.querySelector('.cart-container'));

app.mount(templateId, container, listPath)

Mount a list for dynamic updates. Handles add/remove/reorder.

app.mount('tmpl-cart-item', document.querySelector('.cart-items'), 'cart.items');

app.set(path, value)

Set a value in the store. Triggers reactive updates.

app.set('cart.items[abc].title', 'New Title');
app.set('cart.items', [...newItems]);

app.get(path)

Get a value from the store.

const title = app.get('cart.items[abc].title');
const items = app.get('cart.items');

app.batch(fn)

Batch multiple updates into a single notification cycle.

app.batch(() => {
  app.set('cart.items[abc].title', 'New Title');
  app.set('cart.items[abc].price', 1999);
});

app.subscribe(path, callback)

Subscribe to state changes. Returns unsubscribe function.

const unsubscribe = app.subscribe('cart.items', () => {
  console.log('Cart items changed');
});

Path Syntax

Paths support both dot notation and bracket notation:

// These are equivalent for arrays
app.get('cart.items[0].title')   // Numeric index
app.get('cart.items[abc].title') // Keyed access (finds item where key='abc')

// Nested paths
app.set('user.profile.name', 'John')

Keyed Array Access: When accessing an array with a non-numeric key like cart.items[abc], the runtime finds the item where item.key === 'abc'.

Compiler Output

For a template cart-item.jsx, the compiler generates:

snippets/_cart-item.liquid

Server-rendered Liquid with data attributes:

<div data-item="{{ item.key }}" data-scope="cart.items[{{ item.key }}]">
  <span data-bind-text="title">{{ item.title }}</span>
  ...
</div>

assets/_cart-item-template.liquid

Template element for client-side cloning:

<template id="tmpl-cart-item">
  <div data-item="" data-scope="">
    <span data-bind-text="title"></span>
    ...
  </div>
</template>

assets/template-manifest.js

Binding metadata:

export const templates = {
  'cart-item': {
    id: 'tmpl-cart-item',
    bindings: [
      { selector: '[data-bind-text="title"]', type: 'text', path: 'title' },
      { selector: '[data-bind-text="price"]', type: 'text', path: 'price', helper: 'money' }
    ],
    actions: [
      { selector: '[data-action="cart.remove"]', action: 'cart.remove', event: 'click' }
    ],
    itemKey: 'key',
    scopePattern: 'cart.items[key]'
  }
};

Shopify Integration

Theme Structure

your-theme/
├── src/
│   └── templates/           # Your .jsx templates
│       └── cart-item.jsx
├── snippets/
│   └── _cart-item.liquid    # Generated
├── assets/
│   ├── _cart-item-template.liquid  # Generated
│   ├── template-manifest.js        # Generated
│   └── drip-runtime.js             # Copy from node_modules
├── layout/
│   └── theme.liquid
└── package.json

Setup

  1. Install drip-templates
  2. Copy node_modules/drip-templates/assets/drip-runtime.js to your theme's assets
  3. Add build script to package.json:
    {
      "scripts": {
        "templates:build": "drip build --input ./src/templates --output .",
        "templates:watch": "drip watch --input ./src/templates --output ."
      }
    }

Including Templates in theme.liquid

<!DOCTYPE html>
<html>
<head>...</head>
<body>
  {{ content_for_layout }}

  {% comment %} Include all template elements {% endcomment %}
  {% render '_cart-item-template' %}

  {% comment %} Initialize runtime {% endcomment %}
  <script type="module">
    import { Drip } from '{{ 'drip-runtime.js' | asset_url }}';
    import { templates } from '{{ 'template-manifest.js' | asset_url }}';

    window.drip = new Drip({
      manifest: { templates },
      initialState: {
        cart: {{ cart | json }}
      }
    });

    // Register action handlers
    drip.on('cart.remove', async (e, { key }) => {
      // ... handle remove
    });

    // Hydrate existing content
    drip.hydrate(document.body);
  </script>
</body>
</html>

Programmatic API

For build tools and custom integrations:

import { build } from 'drip-templates/compiler';

await build({
  input: './src/templates',
  output: './dist'
});

License

MIT