opsinjs
IntroductionStart hereInstallation

components.json

Every field opsinjs reads, what it does, and the one entry you have to add by hand.

Overview

components.json is the shadcn CLI's configuration file, and opsinjs reuses it rather than introducing one of its own. If you already have shadcn/ui in your project, you already have this file and adding opsinjs is one entry.

The URL below is not serving yet

opsinjs.pensievelabs.org is the intended canonical host and does not resolve today, so an add against the entry below fails before the CLI reaches a registry. The sixty built components resolve from any host that is serving the registry items, which are /r/status-pill.json and the rest. Point the entry at one, and everything on this page applies unchanged. Troubleshooting.

Add the @opsinjs entry

Run npx shadcn@latest init first if this project has never had it. That is what writes components.json, and it is also what writes lib/utils.ts, which exports the cn helper every opsinjs component imports. The registry does not ship that file on purpose. A third copy would collide with the one you already have and may have extended, which is ADR 0010. So a project that skips init gets components that land in the right place and then every one of them fails to typecheck on the same missing module.

The registries entry is then the only change opsinjs strictly requires, and the only one you have to make by hand:

components.json
{
  "registries": {
    "@opsinjs": "https://opsinjs.pensievelabs.org/r/{name}.json"
  }
}

{name} is substituted by the CLI, so @opsinjs/result-card resolves to https://opsinjs.pensievelabs.org/r/result-card.json. The URL is guessable on purpose: an agent that knows a component id can fetch its registry item without discovering an index first.

Two safety notes about this line, because a registry entry is a remote code source:

  • opsinjs.pensievelabs.org is the only canonical registry host. Anything else serving @opsinjs is not us. The canonical domain, npm scope and registry URL are published in Official resources precisely so impersonation is checkable.
  • shadcn add writes source files into your repository. Review the diff the first time you add from any registry, exactly as you would review a dependency.

Know what the other fields do

The rest of the file is shadcn's, but opsinjs reads several fields and gets them wrong quietly if they are wrong.

FieldWhat it doesWhat opsinjs needs from it
$schemaEditor validationNothing, but keep it
styleSelects a style variant within a registryRequired: the CLI validates against a schema that demands it, so a file without it is rejected whole. It reaches opsinjs only through the URL template. The CLI substitutes it wherever a registry URL contains {style}. So it matters if you point the entry at /r/styles/{style}/{name}.json, which Registry documents, and not with the /r/{name}.json entry this page publishes
rscWhether the project uses server componentsControls whether a "use client" directive is written into files that need one. Wrong value, wrong build errors
tsxTypeScript or JavaScript outputopsinjs source is TypeScript; false is not supported
tailwind.cssPath to your main stylesheetWhere the token layer import is written
tailwind.baseColorThe neutral rampshadcn's, not opsinjs's. The opsinjs token layer defines its own neutrals
tailwind.cssVariablesVariables versus utility classesMust be true. The whole token system is CSS custom properties
tailwind.prefixUtility prefixSupported, but set it before you copy anything in. Changing it later means editing copied source
iconLibraryWhich icon setopsinjs targets lucide. A different set means substituting icons in copied source by hand
aliases.componentsWhere components goMust match your tsconfig.json paths
aliases.uiWhere primitives goSame
aliases.utilsWhere cn livesCopied components import it from here
aliases.lib, aliases.hooksSupporting codeSame rule: must match tsconfig
registriesNamespaced registriesWhere @opsinjs goes

The alias rule that causes the most trouble

aliases and your tsconfig.json paths are two independent statements of the same fact, and nothing reconciles them. The CLI writes files to the alias; your bundler and typechecker resolve the tsconfig path. When they disagree, files land somewhere real and imports fail somewhere else, and the error message names neither file.

In a workspace, both must be correct in the app being written to, not in the repository root. Monorepo has the details.

A complete example

A consumer project's file, with the opsinjs entry in place. It is complete, and the style key is the reason it has to be: the CLI validates the whole file against its schema before it fetches anything, style is required by that schema, and a components.json it rejects writes nothing at all rather than failing on the item you asked for. new-york is what shadcn init writes; keep whatever value it wrote for you rather than copying this one over it.

components.json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "iconLibrary": "lucide",
  "aliases": {
    "components": "@/components",
    "ui": "@/components/ui",
    "utils": "@/lib/utils",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "registries": {
    "@opsinjs": "https://opsinjs.pensievelabs.org/r/{name}.json"
  }
}

Verify it worked

  1. A namespaced add resolves. @opsinjs/status-pill fetches rather than erroring on an unknown registry.
  2. The file lands where the alias says. If it does not, the alias and the tsconfig path disagree.
  3. The cn import inside the copied file resolves. It comes from aliases.utils and it breaks two ways: the alias points somewhere real but wrong, or lib/utils.ts was never written because init was never run. Identical TS2307 errors on every copied file at once is the second one; a single file resolving to the wrong place is the first.
  4. "use client" appears only where expected. If it is on everything, rsc is probably false when it should be true.

Troubleshooting

Unknown registry @opsinjs. The registries entry is missing, or the key is missing its @. The key includes the @; the URL template must include {name}.

Components land in the wrong folder. aliases versus tsconfig paths.

cn is not exported, or Cannot find module '@/lib/utils'. aliases.utils points somewhere that does not export it, or shadcn init was never run and the file is not there at all. Nothing in the registry creates it.

Icons are missing or wrong. iconLibrary is not lucide. There is no automatic substitution; you are editing copied source.

Everything is a client component. rsc is false.

Next

  • Monorepo covers the same fields, one directory level harder.
  • Registry says what the namespace resolves to and what a registry item contains.
  • Official resources has the canonical hosts, so you can tell a real registry from a lookalike.

On this page