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

@augeo/smelt

v2.1.0

Published

Library + CLI for building Shopify themes from a colocated component tree

Readme

Smelt

Verify

Build Shopify themes from colocated components. Author your button/ directory once (button.liquid, button.ts, button.css, button.test.ts, button.schema.ts), and Smelt compiles it into the flat, namespaced files Shopify expects.

Why

  • Colocation. Every component lives in one directory. Liquid markup, TypeScript behavior, CSS, tests, and schema are authored side-by-side instead of scattered across sections/, snippets/, and assets/.
  • Layering. Smelt ships with a baseline component library; your theme shadows any file. Drop in a button.css to restyle without touching markup, TS, or schema. Same pattern as Nuxt Layers or Gatsby shadowing.
  • Typed schemas. Author {% schema %} blocks in TypeScript with autocomplete derived from Shopify's authoritative JSON Schemas. The build executes the file and injects the result.
  • Private blocks for free. Nest a blocks/ directory under a section or block, and its children become private theme blocks, emitted with Shopify's _ prefix and auto-merged into the parent's schema. No manual filename mangling.
  • Additive build. Only files prefixed built-- (or _built-- for private blocks) are written or cleaned. Hand-written theme files are preserved, so you can adopt incrementally.

Contents


Example

A consumer theme writes one directory per component:

src/
├── sections/
│   └── hero/
│       ├── hero.liquid
│       ├── hero.css
│       └── hero.schema.ts
└── components/
    └── card/
        ├── card.liquid
        └── card.css

src/sections/hero/hero.liquid:

<section class="tr-hero">
	<h2>{{ section.settings.heading }}</h2>
	{% render '@/components/card', title: 'Hello', body: 'World' %}
</section>

src/sections/hero/hero.schema.ts:

import { defineSchemaSection } from "@augeo/smelt/schema";

export const schema = defineSchemaSection({
	name: "Hero",
	settings: [
		{ type: "text", id: "heading", label: "Heading", default: "Welcome" },
	],
	presets: [{ name: "Hero" }],
});

Compile:

smelt build

Outputs sections/built--sections--hero.liquid, snippets/built--components--card.liquid, etc. They're flat and namespaced, the way Shopify wants.

See the 📄 example/ directory for a working consumer theme.

Install

npm install -D @augeo/smelt

This installs the smelt CLI and the @augeo/smelt/schema import for schema authoring.

Authoring

Each component is a directory under src/<type>/<name>/ where <type> is sections, blocks, or components. Files inside share the directory name:

| File | Role | | ------------------ | -------------------------------------------- | | <name>.liquid | Markup (required; anchors the component) | | <name>.ts | Bundled and injected into {% javascript %} | | <name>.css | Injected into {% stylesheet %} | | <name>.schema.ts | Typed schema (sections + blocks only) | | <name>.test.ts | Colocated test file, picked up by vitest |

Components reference other components with the @/ alias:

{% render '@/components/button', label: 'Continue' %}

@/ resolves through the merged layer tree, so a consumer's button.liquid automatically shadows the baseline.

Schemas

Author schemas in TypeScript with full type inference:

import { defineSchemaSection } from "@augeo/smelt/schema";

export const schema = defineSchemaSection({
	name: "Hero",
	settings: [
		{ type: "text", id: "heading", label: "Heading" },
		{
			type: "select",
			id: "alignment",
			label: "Alignment",
			options: [
				{ value: "left", label: "Left" },
				{ value: "center", label: "Center" },
			],
			default: "center",
		},
	],
});

Types are codegen'd from Shopify's authoritative schemas in theme-liquid-docs. Update via npm run docs:update.

Inline {% schema %} blocks in .liquid files are a build error: a single source of truth.

Private blocks

Shopify treats theme block files prefixed with _ as private: hidden from the merchant's block picker, renderable only via a parent's {% content_for "blocks" %}. Smelt expresses this structurally: a blocks/ directory nested under a section or block emits its children with the _ prefix automatically.

src/sections/hero/
├── hero.liquid
├── hero.schema.ts
└── blocks/
    └── feature/
        ├── feature.liquid
        └── feature.schema.ts

Compiles to:

sections/built--sections--hero.liquid
blocks/_built--sections--hero--blocks--feature.liquid

The parent's schema auto-merges discovered children into its blocks: [] array, sorted by directory name and prepended; any explicit entries you list (e.g. globally-shared block types) appended after. Nesting is recursive: a private block can have its own blocks/ subdir.

Top-level src/blocks/* files remain public (no _ prefix).

Block faces

A src/components/* component compiles to a snippet — reusable through {% render %}, but invisible to the theme editor. A block face also exposes that same component as a theme block a merchant can drop onto a page, without duplicating it. Declare one with a singular block/ directory inside the component:

src/components/card/
├── card.liquid          # the reusable snippet
├── card.css
└── block/
    └── card.schema.ts   # the block face's schema

Compiles to both:

snippets/built--components--card.liquid   # the snippet, unchanged
blocks/built--components--card.liquid      # the block face

With just a schema (no block/card.liquid), the face is mechanical: the build synthesizes the wrapper for you — rendering the component, mapping each setting to a render arg of the same name, and passing shopify_attributes through. So name your component's props to match the setting ids.

{% # blocks/built--components--card.liquid (generated) %}
{% render 'built--components--card',
	title: block.settings.title,
	body: block.settings.body,
	shopify_attributes: block.shopify_attributes
%}
{% schema %}…{% endschema %}

When the wrapper isn't mechanical — derived props, conditional logic, or passing children: block.blocks — add a block/card.liquid and the build uses it as the body verbatim (renders rewritten, schema injected). The face is then just a normal block component living in block/, so it can carry its own block/card.ts / block/card.css too.

Faces are components-only: sections and blocks don't get them. A block face can own private child blocks by nesting a blocks/ directory inside block/ — see docs/build-spec.md for that and the full rules.

Layering

Smelt walks an ordered list of layers and merges them per-file. The default is:

  1. Consumer: your theme's src/ (process.cwd()).
  2. @augeo/smelt: the package's baseline src/.

For each component slot (liquid, ts, css, schema), the first layer that has the file wins. Drop just a button.css in your consumer to override styles; the baseline's button.liquid and button.ts are inherited.

Build

smelt build

Run from the theme root. Outputs go to sections/built--*.liquid, blocks/built--*.liquid, and snippets/built--*.liquid, prefixed so they coexist with hand-written files in the same directories.

Watch mode

smelt dev

Runs an initial build, then watches each layer's src/ and rebuilds on file changes (add, edit, delete). Build failures log and keep the watcher alive. Pair with shopify theme dev (in another terminal or via concurrently): smelt dev writes the built files; shopify theme dev uploads them.

Committing the output

Shopify imports themes from git, so built--* files (and _built--* for private blocks) need to be committed alongside source. Two configs keep the noise down:

.gitattributes marks output as generated so GitHub collapses it in PR diffs and excludes it from language stats:

sections/built--*.liquid linguist-generated=true
snippets/built--*.liquid linguist-generated=true
blocks/built--*.liquid linguist-generated=true
blocks/_built--*.liquid linguist-generated=true

.prettierignore skips the output so Prettier doesn't reformat it between builds:

sections/built--*.liquid
snippets/built--*.liquid
blocks/built--*.liquid
blocks/_built--*.liquid

CI: verify the build is committed

Because the built--* files are committed, they can drift from source: someone edits a component but forgets to rebuild, or commits a stale build. Catch it in CI by rebuilding and failing if the working tree is dirty: a clean tree means the committed output already matches source.

.github/workflows/verify.yml:

name: Verify

on:
  push:
    branches: [main]
  pull_request:

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: actions/setup-node@v5
        with:
          node-version-file: "package.json"
          cache: "npm"

      - run: npm ci

      - name: Build
        run: npm run build # → smelt build

      - name: Check built files match source
        run: |
          if [ -n "$(git status --porcelain)" ]; then
            echo "::error::Build produced uncommitted changes. Did you forget to commit a rebuild?"
            git status --porcelain
            git diff
            exit 1
          fi

The build is additive and deterministic: it only writes built--* files, so a rebuild on a current tree produces no diff. Any change to git status means the commit is missing a rebuild.

If you run other checks (tests, shopify theme check), put the build step first so the job fails fast when the committed output is stale. There's no point linting and testing a tree you already know is out of date. See this repo's own verify.yml for the full pattern, including git submodules and a Playwright browser for the test suite.

Learn More

Future Plans

  • smelt.config.ts: consumer-defined layer list, enabling N-layer composition (e.g., a community component pack between consumer and baseline).
  • More baseline components. Currently just button demo. Expanding to a real default set.