Behive Tech Component Registry

Guide

Adopting the registry into a monorepo you already have

Adding the registry to a monorepo you already have.

npx create-bht-component-registry gives you a repo whose only job is the registry. If you already have a pnpm monorepo and want the registry and its docs site to live inside it, this guide lists what to copy, where it has to sit, and how to express the build order with whatever runs your builds today. The trade-off up front: create and update assume they own the repo root, so adoption is by hand and so are later template updates (last section).

What to copy

Get a fresh template with npx create-bht-component-registry tmp --scope <yours> --no-install (or clone this repo), then copy into your monorepo:

PathWhat it isNeeded
registry/your scope folder with scope.json and one package per componentyes
registry.config.json, registry.config.schema.jsonscope, registry URL, site brandingyes
apps/docs/the docs site (Next.js static export)yes
packages/enveverything a component builds, lints, type-checks and tests with (tsup, tsconfig, ESLint, Vitest config + setup, SCSS-module typings) and the bht-registry-docgen bin — see The env packageyes
packages/class-namesthe getClassName helper the component template usesyes
packages/scopescope naming rules used by the generatoryes
packages/eslint-config, packages/typescript-configthe configs the packages above extendyes
plopfile.ts, scripts/generators/pnpm gen and the shipped generators — see Generatorsyes
vitest.config.tsa one-line re-export of the env's Vitest config, found by component tests walking up from their folder (see below)yes
.changeset/config.jsonChangesets config, if you do not have oneoptional
Dockerfile, nginx.conf, .github/workflows/*.ymlthe container image and CI/release workflowsoptional

Keep the @bht-component-registry/* package names. The component template, the shared tsup config (which bundles them into each component) and the docs app's next.config.ts all reference them.

Where things have to sit

  • Components must be at registry/<scope>/<category>/<name> under the repo root. The catalog, the generator and the publish filter all read that tree.
  • The docs app resolves the repo root as two directories above itself (apps/docs/scripts/generate-catalog.ts, the REPO_ROOT constant) and reads <root>/registry and <root>/registry.config.json from there. You can rename the folder (apps/registry-docs) as long as it stays two levels deep; otherwise adjust that constant.
  • The app's package name is docs. If that collides with something you have, rename it in apps/docs/package.json and in the root scripts below.

pnpm-workspace.yaml

Add the folders to packages and make sure the catalog: entries the copied packages use exist. The shipped file is the reference; the parts that matter:

1packages:
2 - "apps/*"
3 - "packages/*"
4 - "registry/*/*/*"
5
6catalog:
7 "@types/node": ^22.20.4
8 "@types/react": 19.2.17
9 "@types/react-dom": 19.2.3
10 esbuild-sass-plugin: ^3.7.0
11 eslint: ^9.39.5
12 postcss-modules: ^9.0.1
13 react: ^19.2.8
14 react-dom: ^19.2.8
15 tsup: ^8.5.1
16 tsx: ^4.23.11
17 typescript: ^5.9.3
18 vitest: ^4.1.10
19
20allowBuilds:
21 esbuild: true
22 unrs-resolver: true
23 "@parcel/watcher": true

If you do not use pnpm catalogs, replace each catalog: in the copied package.json files with the range you want. On pnpm 9 or 10, allowBuilds is onlyBuiltDependencies.

Root devDependencies

Component packages and packages/env resolve a few tools from the repo root rather than declaring them each time. Copy these from the template's root package.json into yours:

sass, postcss, postcss-modules, esbuild-sass-plugin, jsdom, @testing-library/jest-dom, @testing-library/react, @testing-library/user-event, vitest, tsup, typescript, prettier, and for pnpm gen: plop, tsx, cross-env, handlebars. Add @changesets/cli if you publish with Changesets.

Build order

Three levels, and the order matters:

  1. packages/class-names builds to dist/.
  2. Every registry/*/*/* package runs tsup && bht-registry-docgen, bundling class-names and writing dist/ and docs.json.
  3. The docs app's prebuild regenerates the catalog, which imports each registry package's dist/, then next build writes apps/docs/out.

check-types and test in the registry packages and the docs app need the upstream dist/ too. lint needs nothing built.

pnpm orders a recursive run by workspace:* dependencies, so the template's root scripts are enough on their own:

1"dev": "pnpm --filter \"docs^...\" run build && pnpm --filter docs run dev",
2"build": "pnpm -r run build",
3"lint": "pnpm -r run lint",
4"check-types": "pnpm -r run check-types",
5"test": "pnpm -r run test",
6"verify": "pnpm build && pnpm lint && pnpm check-types && pnpm test",
7"gen": "cross-env NODE_OPTIONS=\"--import tsx\" plop --plopfile=plopfile.ts",
8"release": "pnpm --filter \"{./registry/**}...\" --filter create-bht-component-registry run build && changeset publish"

If your root already has build, test and friends that cover other apps, these still work: pnpm -r run build builds everything in dependency order, and pnpm --filter "{./registry/**}..." scopes a run to the registry packages and what they depend on. To build only the docs site and what it needs: pnpm --filter "docs..." run build.

If your monorepo uses Turborepo

Turborepo expresses the same order with dependsOn: ["^build"]. Add these tasks to your turbo.json (merge with what you have):

1{
2 "tasks": {
3 "build": {
4 "dependsOn": ["^build"],
5 "outputs": [
6 "dist/**",
7 "docs.json",
8 "out/**",
9 ".next/**",
10 "!.next/cache/**",
11 ],
12 },
13 // The docs site reads the whole registry and the repo config, which live
14 // outside its own folder, so declare them as inputs or its cache goes stale.
15 "docs#build": {
16 "dependsOn": ["^build"],
17 "inputs": [
18 "$TURBO_DEFAULT$",
19 "../../registry/**",
20 "../../registry.config.json",
21 ],
22 "env": ["REGISTRY_READ_TOKEN"],
23 },
24 "dev": { "dependsOn": ["^build"], "cache": false, "persistent": true },
25 "lint": {},
26 "check-types": { "dependsOn": ["^build"] },
27 "test": { "dependsOn": ["^build"] },
28 },
29}

Then the root scripts are turbo run build, turbo run dev --filter=docs, and so on. REGISTRY_READ_TOKEN is the only environment variable the docs build reads, and only if you set it (Publishing).

The root Vitest config

Component packages run vitest run with no config of their own. Vitest walks up from the package folder to the nearest vitest.config.*, which is how they pick up globals: true, environment: "jsdom" and the setup file with the jest-dom matchers and jsdom stubs. All of that lives in packages/env (vitest.config.mjs, vitest.setup.ts); the root vitest.config.ts only re-exports it, so copy that one line to your root. If you already have a root Vitest config, spread componentTest from @bht-component-registry/env/vitest.config.mjs into its test block, or scope it to registry/** with Vitest's projects option.

The generator

pnpm gen is plop, launched the way plop's own documentation describes for a TypeScript plopfile: NODE_OPTIONS="--import tsx" plop --plopfile=plopfile.ts (cross-env makes the variable work on Windows). On Node 22.18 or newer, which strips types natively, plain plop works too. The component generator reads registry.config.json (scope, npmRegistry) and the root package.json#repository.url, and writes into <root>/registry/<scope>/<category>/<name>. If your repo already has a plopfile, keep ours as the entry point and move your generators into generators/<name>/index.ts — they are loaded automatically and update never touches that folder. See Generators.

Publishing and CI in a shared repo

  • Changesets. If the rest of your monorepo is not versioned with Changesets, list those packages under ignore in .changeset/config.json so changeset version and changeset publish only see the registry. The release script above builds only the registry packages and the CLI; drop the CLI filter, since you will not have it.
  • CI. pnpm verify or your runner's equivalent. The build command hosts need stays pnpm build (or pnpm --filter "docs..." run build) with the output directory apps/docs/out. See Deploy.
  • Docker. The shipped Dockerfile copies the whole repo into the build stage, which is fine; only apps/docs/out reaches the runtime image.

What create and update do not cover here

create only scaffolds a whole repo. update replaces every path in template.manifest.json#owned, which includes the root package.json, pnpm-workspace.yaml, .gitignore, CLAUDE.md, the Dockerfile and the workflows. In a shared root that is destructive, so do not run it.

To take template updates by hand:

  1. Keep a scratch checkout of the template: git clone https://github.com/behivetech/bht-component-registry.git.
  2. Diff between the version you copied and the latest: git diff v<old>..v<new> -- apps/docs packages scripts docs vitest.config.ts vitest.setup.ts.
  3. Apply what applies. Your components under registry/ are never part of that diff.

The e2e spec names the example components; point it at yours, as Testing and CI describes.

Related