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

vinumcms

v0.7.0

Published

Command line tools for Vinum CMS: init wires it into an existing React Router app, doctor checks an install for the mistakes that fail silently.

Readme

vinumcms

Command line tools for Vinum CMS — a block-based CMS delivered as importable packages that mount inside your own React Router v7 or v8 app.

npx vinumcms init      # wire Vinum into an existing React Router app
npx vinumcms doctor    # check an install for the mistakes that fail silently

No installation and no configuration.

init

Against an app you already have — npx create-react-router@latest makes one, and reimplementing that means owning every future change to their template.

npx create-react-router@latest my-site
cd my-site && npm install
npx vinumcms init
npm run dev            # then sign in at /admin

Eight steps, four of which fail silently if you do them by hand and miss one: the Vite ssr.noExternal setting, the Tailwind @source lines, allowImportingTsExtensions in your tsconfig, and — the one that costs the most — an import of your vinum.server module from app/entry.server.tsx, without which registerVinum() never runs and every admin route throws an error naming a call you already wrote.

It also writes app/vinum.server.ts — wired to a store that survives a restart, Deno KV under Deno and fsStore under Node — spreads vinumRoutes() after your own routes, generates a SESSION_SECRET from the CSPRNG, and gitignores .env and data/, because the store holds user password hashes.

It never overwrites a file that exists. Patches go in by insertion, and where it cannot patch confidently it prints the exact snippet and where it goes rather than guessing. Running it twice does nothing the second time and says so, which includes not regenerating your session secret.

| | | |---|---| | --no-install | Patch files but skip npm install | | --mail | Include @vinumcms/mail |

Exits 1 if a step failed, 2 if the directory is not a React Router app — in which case it writes nothing and names what is missing.

doctor

Vinum ships TypeScript with no build step. That trade buys a codebase you can read while you debug it, and it costs every install two configuration lines that nothing will ask you for. Get either wrong and the failure does not name itself:

  • Missing ssr.noExternal — the SSR build hands Node raw TypeScript. You get a syntax error deep inside node_modules, which reads like a published package is broken rather than like a missing line in your own config.
  • Missing optimizeDeps — nothing breaks and nothing is logged; dev is simply slower on every navigation, because the browser fetches Vinum's source one module at a time instead of a few pre-bundled chunks. Measured on this repo's playground: 43 module requests five levels deep, against 10 requests and three levels with it set. That reads as "this CMS is heavy" rather than as a missing config line, which is why it is a check.
  • Missing the Tailwind @source lines — Tailwind v4 does not scan node_modules, so none of Vinum's classes are generated and the admin renders completely unstyled. Nothing is logged. Nothing errors.

There is a third setting with the same shape — allowImportingTsExtensions in your tsconfig.json — which breaks neither dev nor build, because Vite never typechecks. It fails tsc, so it surfaces late and usually in CI.

And one that is not a setting at all: something has to import your vinum.server module. registerVinum() is a module side effect, so if nothing in the server graph imports it the call never runs, and every admin route throws No Vinum instance registered — an error that names a call you have already written, which is why it is the most expensive of the four to find.

doctor checks all four, plus two warnings and one more that only applies once you switch analytics or the consent banner on — see What it checks.

A missing SESSION_SECRET. A warning rather than an error because the server falls back to a development-only default — right for react-router dev and dangerous in production, which is exactly the shape that reaches a deploy unnoticed.

A memoryStore. It forgets everything when the dev server reloads, and a second process (vinumcms mcp) opens its own empty copy rather than seeing yours. Also a warning, because it is a legitimate choice in tests and this command cannot know it is not deliberate. init scaffolded it until fsStore shipped, so installs created before then are quietly losing content.

vinumcms doctor  /Users/you/my-site

  ✓  Vite SSR config    vite.config.ts sets ssr.noExternal to cover @vinumcms/.
  ✗  Tailwind sources   app/app.css does not scan @vinumcms/admin, so those
                        classes are never generated and the UI renders unstyled.
  ✓  tsconfig           tsconfig.json sets allowImportingTsExtensions.
  ✗  Registration       app/vinum.server.ts calls registerVinum() but nothing
                        imports it, so it never runs and every admin route will
                        throw at the first request.
  !  Store             app/vinum.server.ts uses memoryStore, so content is lost
                        on every restart and `vinumcms mcp` sees an empty site.
  !  Session secret     SESSION_SECRET is not set, so admin sessions are signed
                        with a development-only default.

  @vinumcms/[email protected]  @vinumcms/[email protected]

2 errors, 2 warnings.

Each failure prints the exact snippet to paste and where it goes.

Options

| | | |---|---| | --cwd=<dir> | Directory to check. Defaults to the working directory. | | --json | Machine-readable output on stdout. | | --version, -v | Print the version. | | --help, -h | Print usage. |

Exit code is 1 when a check fails and 0 otherwise; warnings do not fail the command. 2 means it could not run at all — no command given, or no Vinum install where it looked.

For agents

Nothing prompts, check order is fixed, and --json gives you the same report as a structure:

{
  "ok": false,
  "runnable": true,          // false when there was no install to check at all
  "root": "/Users/you/my-site",
  "install": {
    "declared": { "@vinumcms/core": "^0.2.0" },
    "installed": { "@vinumcms/core": "0.2.0" }
  },
  "checks": [
    {
      "id": "ssr-noexternal",     // ssr-noexternal | dev-prebundle |
                                  // tailwind-source | ts-extensions |
                                  // registration | store | session-secret |
                                  // extras
      "title": "Vite SSR config",
      "status": "ok",          // ok | error | warn | unknown | skipped
      "detail": "…",
      "fix": null              // a pasteable snippet when there is something to do
    }
  ]
}

status: "unknown" is deliberate and worth handling. doctor reads a host repo it cannot execute, and a vite config is TypeScript that may import, spread or compute its options — so every check is a regex over source, and a regex can be wrong. Where it cannot prove something either way it says so and names the file to open, rather than guessing confidently in either direction.

What it checks

The build settings above, that something imports your vinum.server module, your SESSION_SECRET, your store — and, if you switched analytics or the consent banner on, that any route of your own calling loadCmsPage also renders CmsPageExtras.

That last one is the newest and the most recently expensive. A host route that owns its own CMS page renders the blocks and nothing else, so measurement is configured, mounted and completely silent. It happened to a real site, and to this repo's own playground, which is why it is a check rather than a paragraph.

mcp

vinumcms mcp hands off to @vinumcms/mcp, which serves your content model and pages as MCP tools — read-only unless --write, and every write through the same sanitizers a form post gets.

export and import

The backup round trip, from a terminal. Vinum's leaving guide promises you can take everything and go; this is the version of that promise you can actually run without writing code.

npx vinumcms export                     # → vinum-backup.json
npx vinumcms export --out=mine.json
npx vinumcms export --force             # replace an existing file

npx vinumcms import mine.json --dry-run # what is in it; writes nothing
npx vinumcms import mine.json           # merge (default)
npx vinumcms import mine.json --mode=overwrite --yes

Merge writes each key and leaves anything not mentioned alone. Overwrite clears every covered prefix first — which is the one genuinely destructive thing here, so it refuses to run without --yes and tells you what it would clear.

Import goes through the same sanitizer the admin uses, so a file from somewhere else cannot store a shape the editor could never have made: unknown block types are dropped, URLs are neutralised, and a record's identity comes from its key rather than its body.

Media bytes are not in the JSON. Copy your media directory alongside it.

Exporting from a Deno site

[email protected] VINUM_PASSWORD=… \
  npx vinumcms export --url=https://your-site.example

--url asks the running site instead of opening its store, and for a Deno host that is the only way in: its content lives in Deno KV, which only the process that already has it open can read, and Deno refuses to compile Vinum's TypeScript out of node_modules so no second process can join. The site has no such difficulty — it serves this JSON to a signed-in admin already.

Credentials come from the environment, never a flag. A password in a flag is a password in ps, in your shell history, and in whatever CI log captured the command line.

It refuses plain http to anything but localhost, because it is sending an admin password, and it checks the output file before it sends anything — a mistyped --out should not cost you a round trip with your credentials on it.

Without --url, on a Deno site, init scaffolds a store that picks Deno KV when the runtime is Deno and a file when it is Node — and the local path runs under Node, so it would open the wrong database. It detects that and refuses rather than handing you a backup of a site you do not have. Use --url above, or /admin/backup in the browser.

Two things worth knowing:

  • An empty export is reported loudly, because it usually means the install uses memoryStore(). This command is its own process, so it opens its own empty database rather than the one your dev server is holding in RAM.
  • It needs tsx, because it loads your vinum.server module and the packages ship TypeScript. It is not a dependency of this CLI — it is resolved when you run it, from your node_modules first, then npx. If neither works, npm i -D tsx.

What else is coming

Read-only stays the default throughout. Anything destructive needs an explicit flag and prints what it intends to touch first.

Licence

MIT