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

@fmaplabs/meta-manifest

v0.9.0

Published

Zero-dependency, zod-style builder for Shopify metaobject definitions, plus a CLI (mm) that syncs them to a store via pull → diff → push.

Downloads

136

Readme

meta-manifest

A zero-dependency, zod-style builder for Shopify metaobject definitions, plus a CLI (mm / meta-manifest) that keeps a store's definitions in sync with schema declared in code. Think tento, but scoped to metaobject-definition schema/migrations rather than a runtime query client (see Roadmap below).

meta-manifest is not a runtime client for querying metaobject entries — it declares definitions, validates values against them, and syncs the definitions themselves (create/update fields) to a store via pulldiffpush. It can also declare seed entries in code and upsert them on push (see Seed entries) — but never enumerates, queries, or deletes store data.

Install

npm i -D @fmaplabs/meta-manifest
# or
pnpm add -D @fmaplabs/meta-manifest

Library usage

Declare a metaobject with defineMetaobject and the m field builders. Implements Standard Schema.

import { defineMetaobject, m, type Infer } from "@fmaplabs/meta-manifest";

export const Author = defineMetaobject("author", {
  name: "Author",
  displayName: "name",
  access: { storefront: "public_read" },
  fields: {
    name: m.text({ required: true, max: 120 }),
    bio: m.multilineText(),
    rating: m.rating({ min: 1, max: 5 }),
  },
});

type AuthorValue = Infer<typeof Author.fields>;

Author.type;                 // "$app:author"
Author.toDefinitionInput();  // MetaobjectDefinitionCreateInput (for metaobjectDefinitionCreate)
Author.parse(fields);        // Shopify {key, jsonValue}[] -> typed, validated
Author.encode({ name: "Ursula" }); // typed -> [{ key, value }] for metaobjectUpsert

References between metaobjects are declared with m.ref(...) / m.list(m.ref(...)). Use m.mixedRef([...]) (and m.list(m.mixedRef([...]))) for a field that may point at several metaobject types:

export const Book = defineMetaobject("book", {
  name: "Book",
  fields: {
    title: m.text({ required: true }),
    author: m.ref(Author),                        // → one Author
    related: m.list(m.ref(Author)),               // → many Authors
    feature: m.mixedRef([Author, Publisher]),     // → one Author *or* Publisher
  },
});

m.ref maps to Shopify's metaobject_reference (a metaobject_definition_type validation); m.mixedRef maps to mixed_reference (a metaobject_definition_types validation). Targets are referenced by type, so push orders creates so a referenced definition exists first — and a reference cycle is created two-pass (the cycle-breaking fields are added by a follow-up update once every member exists). Pass a thunk (m.ref(() => Book), m.mixedRef([() => Book])) for forward/circular references.

Configuration options

Beyond fields, a metaobject definition accepts these options — all optional, all reconciled by diff/push against a live store:

export const Author = defineMetaobject("author", {
  name: "Author",
  displayName: "name",              // optional; omit → Shopify auto-generates
  scope: "merchant",                // optional; overrides config.scope for this metaobject
  access: {
    admin: "merchant_read_write",   // "merchant_read" | "merchant_read_write" (app scope only)
    storefront: "public_read",      // "none" | "public_read"
    customerAccount: "read",        // "none" | "read"
  },
  capabilities: {
    publishable: true,                                              // active/draft status
    translatable: true,                                            // translations
    renderable: { metaTitleKey: "name", metaDescriptionKey: "bio" }, // SEO metadata; also accepts `true`
    onlineStore: { urlHandle: "authors", createRedirects: true },  // publish entries as web pages
  },
  fields: {
    name: m.text({ required: true, filterable: true }),  // filterable → "use as filter" in the admin
    bio: m.multilineText(),
  },
});

| Option | Maps to | Notes | | --- | --- | --- | | scope (config or per-metaobject) | app ($app:<handle>) vs merchant (<handle>) type | default "app"; resolved at sync time — Author.type stays "$app:author" | | merchantEditable (config) | access.admin default | falsemerchant_read, truemerchant_read_write (app scope only) | | access.admin | access.admin | per-metaobject override; invalid on merchant scope | | access.storefront | access.storefront | Storefront API access | | access.customerAccount | access.customerAccount | Customer Account API access (none | read) | | capabilities.publishable | capabilities.publishable | active/draft status | | capabilities.translatable | capabilities.translatable | translations | | capabilities.renderable | capabilities.renderable | true or { metaTitleKey?, metaDescriptionKey? } (SEO) | | capabilities.onlineStore | capabilities.onlineStore | publish as web pages; GraphQL-only (not shopify.app.toml) | | displayName | displayNameKey | omit to let Shopify auto-generate | | field filterable | field capabilities.adminFilterable | expose the field as an admin filter |

For the full pulldiffpush sync model (how local schema and a live store are reconciled, destructive-change gating, dependency ordering, error handling), see docs/SYNC.md.

Declaring schemas across multiple files

The schema module doesn't have to hold every definition. Declare each metaobject in its own file as the module's default export, import them into the main schema module (the file schema points at in the config), and list them in the schemas array. That array is the manifest diff/push read — a definition file that isn't imported there is invisible to the CLI.

// src/metaobjects/author.ts
import { defineMetaobject, m } from "@fmaplabs/meta-manifest";

export default defineMetaobject("author", {
  name: "Author",
  fields: {
    name: m.text({ required: true, max: 120 }),
    bio: m.multilineText(),
  },
});
// src/metaobjects/book.ts
import { defineMetaobject, m } from "@fmaplabs/meta-manifest";
import Author from "./author";

export default defineMetaobject("book", {
  name: "Book",
  fields: {
    title: m.text({ required: true }),
    author: m.ref(Author),
  },
});
// src/schema.ts — the module `schema` in the config points at
import author from "./metaobjects/author";
import book from "./metaobjects/book";

export const schemas = [author, book];

mm init scaffolds this layout. Cross-file references work exactly like same-file ones — including thunks for forward/circular references (m.ref(() => Book)). The loader validates every element of schemas: a file that forgets its export default imports as undefined and fails fast naming the offending index, and two files declaring the same metaobject type are rejected as a duplicate. One caveat: mm pull codegens a single file, so re-pulling overwrites the main schema module with all definitions inlined.

Seed entries

Beyond definitions, specific metaobject entries (data instances) can be declared by handle and kept in sync. Declare them with defineEntries in a module that exports an entries array, and point entries in the config at it:

// src/entries.ts
import { defineEntries, entryRef } from "@fmaplabs/meta-manifest";
import { Author, Book } from "./schema";

export const bookEntries = defineEntries(Book, {
  persuasion: { title: "Persuasion" },
});

export const authorEntries = defineEntries(Author, {
  "jane-austen": {
    name: "Jane Austen",
    favoriteBook: entryRef(Book, "persuasion"),      // reference another declared entry
    portrait: "gid://shopify/MediaImage/123",        // raw GIDs pass through untouched
  },
}, { status: "active" });                            // optional publishable status for the set

export const entries = [bookEntries, authorEntries];

Entry sets can be split across files the same way as schemas — each file default-exports one defineEntries(...) set, and the main entries module imports them into the entries array:

// src/entries/authors.ts
import { defineEntries } from "@fmaplabs/meta-manifest";
import Author from "../metaobjects/author";

export default defineEntries(Author, {
  "jane-austen": { name: "Jane Austen" },
  "ursula-le-guin": { name: "Ursula K. Le Guin" },
});
// src/entries.ts — the module `entries` in the config points at
import authorEntries from "./entries/authors";
import bookEntries from "./entries/books";

export const entries = [authorEntries, bookEntries];

Entry values are typed against the schema's InferInput, validated at plan time (before any network call), and upserted via metaobjectUpsert when mm push runs — after definitions.

The model is upsert-only seed data:

  • Only declared (type, handle) pairs are ever touched. Merchant-created entries, and fields you don't declare on a declared entry, are never compared, written, or deleted.
  • entryRef(Target, "handle") references another declared entry; push creates referenced entries first and resolves the reference to its GID. Reference cycles (and self-references) are handled with a two-pass upsert. One caveat: a required reference field inside an entry cycle may fail the first pass with a Shopify userError.
  • To reference products, files, or entries meta-manifest doesn't manage, use a raw gid://shopify/... string.
  • mm diff previews entry changes the same way it previews definition changes.

CLI

The CLI drives sync against a real store using an Admin API access token. For a step-by-step walk-through (install → token → initpull/diff/push, with example output and CI usage), see the CLI quick start & usage guide.

Config

meta-manifest.config.ts (safe to commit — the token comes from the environment):

import { defineConfig } from "@fmaplabs/meta-manifest";

export default defineConfig({
  shop: "my-store.myshopify.com",
  accessToken: process.env.SHOPIFY_ADMIN_TOKEN!,
  apiVersion: "2026-07",           // optional; defaults to DEFAULT_API_VERSION
  schema: "./src/schema.ts",       // where `pull` writes, `diff`/`push` read
  entries: "./src/entries.ts",     // optional; seed entries to upsert on push
  scope: "app",                    // optional; "app" (default) | "merchant" — applies to all metaobjects
  merchantEditable: false,         // optional; default admin access for app-scoped metaobjects
});

Set SHOPIFY_ADMIN_TOKEN before running pull, diff, or push — either export it into your shell or put it in a .env file in the project root, which the CLI loads automatically (real environment variables take precedence). The token needs the read_metaobject_definitions scope for pull/diff, and write_metaobject_definitions (which implies read) for push. When entries is configured, it additionally needs read_metaobjects for diff and write_metaobjects for push.

Commands

| Command | Behavior | Exit | |----------|----------|------| | mm init | Scaffold meta-manifest.config.ts + a starter schema (src/schema.ts aggregating src/metaobjects/author.ts). No network. | 0 / 1 | | mm pull | Enumerate the store's app-owned metaobject definitions and codegen schema.ts (tento-style — writes/overwrites the schema source file). | 0 / 1 | | mm diff | Load schema.ts, compare it against the store, and print the plan (definitions, then declared entries when configured). Read-only. | 0 / 1 | | mm push | Diff, then apply: topologically ordered (referenced types created first) and destructive-gatedremoveField/changeFieldType are skipped unless you pass --allow-destructive. Declared entries are upserted after definitions. | 0 / 1 / 2 |

npx mm init                    # scaffold config + schema
npx mm pull                    # bootstrap schema.ts from an existing store
npx mm diff                    # preview what push would do
npx mm push                    # apply non-destructive changes
npx mm push --allow-destructive  # also apply field removals/type changes
npx mm pull --force             # overwrite an existing schema.ts without the warning
npx mm diff --config ./staging.config.ts  # use a non-default config file

Flags

  • --config <path> — use a non-default config file instead of meta-manifest.config.ts.
  • --allow-destructive — apply destructive changes (removeField/changeFieldType) on push.
  • --force — overwrite the schema file on pull without the "overwriting" warning.

mm push exits 2 if any operation failed or was blocked (e.g. a reference cycle among the definitions being created in that push — so CI can detect a partial failure), 1 on a config/transport error, and 0 otherwise — including when destructive ops were skipped.

Roadmap: runtime query client

Definitions (schema sync) and declared seed entries are covered. A runtime client for querying/enumerating metaobject entries — the tento-style query API — is not implemented yet and is tracked as a follow-up.