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

@cardanvil/frame-kit

v0.6.0

Published

The contract Card Anvil frames are authored against: schemas, types, authoring helpers, and the distributable manifest format.

Readme

@cardanvil/frame-kit

The contract Card Anvil frames are authored against: the schemas that define what a frame is, the helpers you author one with, and the format frames are distributed in.

pnpm add -D @cardanvil/frame-kit zod

zod is a peer dependency — the frame types are inferred from zod schemas, so you and this package must share one copy.

Building a frame? Start from Card-Anvil/frame-template instead of wiring this up by hand. This package is the contract underneath it.

A frame is data

A frame is one plain object: box geometry, asset URLs, and a few render knobs. Nothing in it runs at render time. That constraint is the whole reason frames are distributable — a frame can be serialized to a frame.json plus a folder of images and loaded by an app that never compiled it, precisely because there is no behaviour to carry across.

import { type Frame, withCollectorInfoDefaults } from "@cardanvil/frame-kit";

import preview from "./preview.jpg";
import { regularLayoutConfig } from "./regular/config";

export const myFrame = {
  name: "Parchment",
  description: "A hand-inked parchment frame.",
  previewImage: preview,
  tags: ["Custom"],
  config: {
    layouts: {
      normal: withCollectorInfoDefaults(regularLayoutConfig, collectorBounds),
    },
  },
} as const satisfies Frame;

as const satisfies Frame is the idiom: satisfies checks the object against the contract, as const keeps the literal types.

Entry points

| Import | What it gives you | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | @cardanvil/frame-kit | The schemas (FrameSchema, LayoutConfigSchema, FrameAssetsSchema, …), the inferred types (Frame, LayoutConfig, CardBoxes, TextBox, Layout), and the authoring helpers | | @cardanvil/frame-kit/manifest | The distributable format: frameToManifest, manifestToFrame, walkAssets, FrameManifestSchema, isContractCompatible, assertDeclarative | | @cardanvil/frame-kit/packaging | The packaging tools the CLI is built on: validateFrames, buildFrames, FrameIndexSchema. Needs Node and Vite, so it is deliberately not on the main entry point |

The frame-kit command

frame-kit validate                # check every frame, write nothing
frame-kit build --out dist        # check, then pack each one
frame-kit build --out dist --watch  # …and repack whenever a source changes
frame-kit schema meta             # a format as JSON Schema
frame-kit --help

A frame is any directory holding a frame.meta.json, so this works both in a workspace of packages and in a flat repository.

validate loads every frame through Vite, parses it against FrameSchema, resolves every asset to a file that exists, and round-trips it through the manifest in memory. Problems are collected rather than thrown, so one run reports everything; under GitHub Actions they become inline annotations.

build adds hashing, zipping and the release index. Each frame becomes a <slug>-<version>.cardframe, and frame-index.json describes the set.

Exit codes: 0 clean, 1 one or more frames failed, 2 used wrongly.

Frames you do not want to ship

A descriptor can opt its frame out of every build:

{
  "id": "com.example.example",
  "private": true,
  "author": { "name": "Your Name" },
  "license": "CC-BY-4.0"
}

validate still loads and checks it; build reports it as skipped and packs nothing, so it never reaches a bundle, the index, or a release. That is the combination a worked example needs — a reference frame stays in the repository and stays correct, without an id that is permanent identity ending up in somebody's release by accident.

--watch

build --watch builds once, then rebuilds only the frames whose files changed, until you stop it. It is meant to be pointed at a folder Card Anvil is linked to, so saving a source file updates the preview without any manual step.

frame-kit build --out ~/CardAnvilFrames --watch

A failed rebuild leaves the previous bundle in place and prints the problem — the app keeps rendering the last good build rather than losing the frame mid-edit. Bundles are written atomically, so a watcher on the other side never reads a half-written file. --watch refuses to combine with --json or --summary-md, which describe a single run that has finished.

Changes are debounced 200 ms, and a rebuild already in flight is allowed to finish before the next one starts: two builds writing one bundle would race. Output files, dotfiles, node_modules, dist and editor droppings are ignored, so a rebuild cannot trigger itself.

Because bundles are byte-deterministic, saving a file that does not change the built output produces an identical bundle, and a consumer watching for content changes sees nothing happen — which is correct.

Frames are loaded through a long-lived Vite server, and its SSR module cache is invalidated on each pass. Without that a rebuild would re-serve the module Vite loaded first, so an edited layout number would rebuild byte-identically and the watch would appear to do nothing. If you embed the build API yourself, call invalidateFrameModules between passes on a server you reuse.

Packaging needs Vite — it is what turns import w from "./w.png" into a file — so it is an optional peer dependency, loaded only when packaging runs. The schemas above work under plain Node and in a browser without it.

frame-kit studio

frame-kit studio [--root <dir>] [--port <n>] [--frame <slug>] [--no-write]

A visual box editor, served from dist/studio and opened at http://localhost:4620. It loads frames through the same Vite SSR loader validate uses, so what it draws is the validated frame, and it reloads when a source file changes.

The card name and type line are previewed in Beleren at the authored size and flagged when they overflow — one line each, with no wrapping, symbols or auto-shrink, so the app remains the authority on a full render.

Working on the studio's own UI: --dev serves it from source with hot reloading, on the same port as the API, so one command covers both. It needs the UI's source, which only exists inside the frames workspace.

Dragging a box rewrites the literal it came from, in place — nothing is reprinted, so comments and formatting survive. Editing needs typescript installed (an optional peer); without it the studio still runs read-only. Values that are not written as literals are reported as such rather than edited.

@cardanvil/frame-kit/layout

How a frame's art is chosen and placed, as pure functions:

  • selectFrameLayers(details, options) — the base frame and section overlays a set of FrameDetails selects. Split out of Card Anvil's getFrameLayers, so the studio and the renderer cannot drift on two-colour splits, hybrid halves or the coloured-artifact fallback.
  • frameDetailsFor(shape) — those details from a plain description of a card (colours plus artifact / vehicle / land / enchantment / hybrid / devoid), for callers with no Scryfall card to parse.
  • placeAsset(path, config) — where a piece of art is drawn. Most frame art is full-sheet and centres, but crowns, nicknames and PT plates come from the layout's own render config; centring those puts a legendary crown in the middle of the card.
  • resolveFrameAsset / resolveBaseFrame — exact per-family lookup, and the base-frame fallback chain.

Card Anvil still owns the other half — reading a Scryfall card into FrameDetails — because that is where the Scryfall types live.

Type it, then validate it

Typing a config as Frame is weaker than the schema a loading app enforces. The clearest example: passing a whole asset barrel to a typed field type-checks even when the barrel exports extra keys, because excess property checks do not apply to namespace objects — and those extra keys then enter the frame contract where nothing can resolve them.

import { FrameSchema } from "@cardanvil/frame-kit";

const result = FrameSchema.safeParse(myFrame);
expect(result.error?.issues ?? []).toEqual([]);

The coordinate space

Boxes are absolute pixels on a 3264 × 4440 canvas.

A Magic card face is 63 × 88 mmnot 2.5 × 3.5 inches, which is the poker-card size and is wrong by 0.5 mm across and 0.9 mm tall, roughly 24 × 43 pixels at 1200 DPI. In inches the card is 2.48 × 3.46.

The canvas is not the card face: it is the full printed sheet including bleed — 2.72 × 3.7 in (69.09 × 93.98 mm) at 1200 DPI, about 3 mm of bleed per edge. A box at y: 0 sits in the bleed, above the top of the card. Override the canvas per frame with config.canvas.

fontSize is in points, not pixels; the renderer converts.

The distributable format

@cardanvil/frame-kit/manifest converts a Frame into something an app can install: a frame.json with every asset URL rewritten to a package-relative path, plus the files themselves.

Asset positions are found by walking the schema, not from a hard-coded list of field paths, so a new asset field in the contract is picked up automatically.

import { frameToManifest } from "@cardanvil/frame-kit/manifest";

const manifest = frameToManifest(myFrame, {
  id: "com.example.parchment",
  version: "1.0.0",
  resolveAsset: (url) => ({ path: `assets/${hashOf(url)}.png` }),
});

manifestToFrame reverses it, turning package-relative paths back into URLs the renderer can load — an object URL for a zip, an asset:// URL for a directory on disk, an absolute URL for a remote package.

A manifest carries a contractVersion of the form MAJOR.MINOR, independent of this package's version. A differing major is refused; a newer minor loads with a reduced-fidelity warning, because added fields are always optional. A layout the loader does not recognise is dropped with a degraded note rather than failing the frame, so one new layout cannot cost an author every other one. Packaging preserves everything the contract defines and drops everything it does not — keys outside the schema do not survive the round trip.

Licence

MIT. See LICENSE.