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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@bosun-ai/snapify

v0.3.0

Published

Visual regression snapshot runner for Shopify Liquid themes using Playwright.

Readme

snapify

Visual regression snapshots for Shopify themes using Playwright — no running dev server required.

This enables drastic refactoring without the fear of breaking existing themes, and makes it easy to add visual tests for new Liquid templates as you build them.

Key ideas

  • ✨ Render OS 2.0 JSON or Liquid templates entirely in-memory with LiquidJS and custom Shopify helpers.
  • 🧱 Resolves sections, snippets, and local-path includes just like a deployed theme.
  • 🎨 Inlines CSS/JS assets (including {{ 'theme.css' | asset_url | stylesheet_tag }}) so snapshots reflect final storefront styling.
  • 🖼️ Replaces shopify://shop_images/... references with deterministic SVG placeholders (respecting requested width/height) so tests never need the real CDN assets.
  • 🌐 Respects Shopify locale strings: load locales/en.default.json (or pass locale/SNAPIFY_LOCALE) and {{ 'sections.*' | t }} renders with the same copy as production.
  • 📸 Uses Playwright to capture screenshots and pixelmatch to diff against baselines.
  • 🧪 Ships both a programmatic API (render) and a CLI (snapify render).

Installation

npm save --dev @bosun-ai/snapify playwright
npx playwright install --with-deps chromium

You can then write your tests, or run the CLI against the current repository root (which already contains a full theme).

CLI usage

snapify render <template> [options]

Common flags (values from snapify.config.js are used as defaults when present):

  • --theme-root – root of the Shopify theme (defaults to process.cwd()).
  • --layout – override layout file (without .liquid).
  • --data – inline JSON or a path to a JSON file providing Liquid data.
  • --styles/--styles-file – inject additional CSS.
  • --viewport 1440x900 – customize Playwright viewport.
  • --snapshot-dir – where snapshots live (defaults to __snapshots__ in the theme root).
  • --accept / -u – replace the stored snapshot with the newly captured one.

Example:

snapify render index --theme-root .. --viewport 1440x900 --data ./fixtures/home.json

Programmatic API (with assertions)

The assertSnapshot helper makes PNG the source of truth while still surfacing HTML drift for debugging.

import { render, assertSnapshot } from 'snapify';

const snapshot = await render({
  themeRoot: '/path/to/theme',
  template: 'product',
  locale: 'en.default',
  layout: 'checkout',
  data: { product: { title: 'Sample' } },
  styles: '.debug-outline { outline: 1px solid red; }',
  viewport: { width: 1440, height: 900 },
  snapshot: {
    name: 'product-page',
    dir: './__snapshots__',
    accept: process.env.CI ? false : true
  }
});

assertSnapshot(snapshot, { htmlMode: 'warn' });

The resolved object includes:

  • htmlPath / screenshotPath – stored baseline snapshot files.
  • newHtmlPath / newScreenshotPath.new files written only when output differs.
  • htmlChanged / imageChanged – booleans for diff detection.
  • status'matched' | 'updated' | 'changed'.

Extending Liquid constructs

Snapify exposes the underlying LiquidJS engine so you can add your own tags and filters, using the same API Liquid provides:

import { TemplateAssembler } from 'snapify/core/templateAssembler.js';

const assembler = new TemplateAssembler('/path/to/theme');

assembler.extend((engine) => {
  engine.registerFilter('shout', (value) => String(value ?? '').toUpperCase());
  engine.registerTag('hello', {
    parse() {},
    async render() {
      return '<span data-custom="hello">hello</span>';
    }
  });
});

const html = await assembler.compose({ template: 'index', layout: false });

Custom constructs participate in the same render pipeline as built-ins, so they work with snapshots and diagnostics.

Using Snapify in automated tests

Snapify slots into Node's built-in test runner (or Jest/Vitest) so you can assert against baselines inside regular CI suites:

// tests/homepage.test.ts
import assert from 'node:assert/strict';
import test from 'node:test';
import path from 'node:path';
import { render } from 'snapify';

const THEME_ROOT = path.resolve('tests/theme');
const SNAPSHOT_DIR = path.join(THEME_ROOT, '__snapshots__');
const ACCEPT = Boolean(process.env.SNAPIFY_UPDATE_BASELINES);

test('index template matches stored baseline', async () => {
  const snapshot = await render({
    themeRoot: THEME_ROOT,
    template: 'index',
    data: { hero: { headline: 'Golden hour' } },
    viewport: { width: 1280, height: 720 },
    snapshot: {
      name: 'index',
      dir: SNAPSHOT_DIR,
      accept: ACCEPT
    }
  });

  if (ACCEPT) {
    // Baselines refreshed locally; fail fast if this ever happens on CI.
    assert.equal(snapshot.status, 'updated');
    return;
  }

  assert.equal(snapshot.imageChanged, false, `Snapshot drift detected. Inspect ${snapshot.newScreenshotPath ?? 'n/a'} for details.`);
  assert.equal(snapshot.htmlChanged, false, 'Rendered HTML should match the stored baseline');
});

Tips:

  • SNAPIFY_UPDATE_BASELINES=1 npm test refreshes every snapshot in bulk.
  • Keep __snapshots__/*.png/.html under version control; ignore *.new.*.
  • The README code samples and the Jest example are exercised by the automated test suite, so they stay in sync.

Jest example

Using Snapify inside Jest with TypeScript just requires enabling ESM support and invoking render + assertSnapshot within a test:

/**
 * @jest-environment node
 */
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { render, assertSnapshot } from 'snapify';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const THEME_ROOT = path.resolve(__dirname, '../theme');

describe('product template', () => {
  const snapshotDir = path.join(THEME_ROOT, '__snapshots__');
  const accept = process.env.SNAPIFY_UPDATE_BASELINES === '1';

  it('matches the stored baseline', async () => {
    const snapshot = await render({
      themeRoot: THEME_ROOT,
      template: 'product',
      snapshot: {
        name: 'product',
        dir: snapshotDir,
        accept
      }
    });

    if (accept) {
      expect(snapshot.status).toBe('updated');
      return;
    }

    assertSnapshot(snapshot, { htmlMode: 'warn' });
  });
});

// snapify.config.js is picked up automatically if present:
// export default { snapshot: { dir: '__snapshots__' }, browser: 'chromium' };
// See examples/jest/homepage.test.ts in this repository for a complete, runnable example.

Set up Jest with "type": "module" (or transform rules for CommonJS), run SNAPIFY_UPDATE_BASELINES=1 npx jest locally to refresh baselines, and npx jest in CI to verify snapshots.

How rendering works

  1. Liquid + sections. TemplateAssembler configures LiquidJS with Shopify-like defaults, resolves JSON templates (sections, block order, custom_css) and plain .liquid templates.
  2. Inline assets. Filters such as asset_url, stylesheet_tag, and script_tag are re-implemented to read from assets/ and inline their contents directly into the <head>.
  3. Head injection. Anything coming from filters or user-provided styles is piped through content_for_header (or injected at the top of <head> if a layout omits it) so the snapshot matches storefront styling.
  4. Playwright capture. HTML is handed to a headless Chromium page via page.setContent, and the resulting screenshot is compared with the baseline using pixelmatch.

Local-path includes

Snapify keeps Liquid's relativeReference behavior enabled, so you can co-locate fixtures next to the template you are testing:

{%- comment -%}sections/__snapify__/hero.liquid{%- endcomment -%}
<section class="hero">
  {% render './partials/cta', label: 'Book a demo' %}
</section>

Place sections/__snapify__/partials/cta.liquid next to it and the renderer will resolve the relative include without needing to copy files into snippets/.

Testing multiple templates

This repository uses the Node test runner plus the SNAPIFY_UPDATE_BASELINES flag shown above. Run the following from the repo root:

# Refresh baselines locally
SNAPIFY_UPDATE_BASELINES=1 npm test

# Validate without touching stored baselines
npm test

Artifacts land under __snapshots__/ inside your theme root so they can be reviewed or committed; .new.* files are transient and should stay untracked.