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:
| Path | What it is | Needed |
|---|---|---|
registry/ | your scope folder with scope.json and one package per component | yes |
registry.config.json, registry.config.schema.json | scope, registry URL, site branding | yes |
apps/docs/ | the docs site (Next.js static export) | yes |
packages/env | everything 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 package | yes |
packages/class-names | the getClassName helper the component template uses | yes |
packages/scope | scope naming rules used by the generator | yes |
packages/eslint-config, packages/typescript-config | the configs the packages above extend | yes |
plopfile.ts, scripts/generators/ | pnpm gen and the shipped generators — see Generators | yes |
vitest.config.ts | a one-line re-export of the env's Vitest config, found by component tests walking up from their folder (see below) | yes |
.changeset/config.json | Changesets config, if you do not have one | optional |
Dockerfile, nginx.conf, .github/workflows/*.yml | the container image and CI/release workflows | optional |
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, theREPO_ROOTconstant) and reads<root>/registryand<root>/registry.config.jsonfrom 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 inapps/docs/package.jsonand 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/*/*/*"56catalog:7 "@types/node": ^22.20.48 "@types/react": 19.2.179 "@types/react-dom": 19.2.310 esbuild-sass-plugin: ^3.7.011 eslint: ^9.39.512 postcss-modules: ^9.0.113 react: ^19.2.814 react-dom: ^19.2.815 tsup: ^8.5.116 tsx: ^4.23.1117 typescript: ^5.9.318 vitest: ^4.1.101920allowBuilds:21 esbuild: true22 unrs-resolver: true23 "@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:
packages/class-namesbuilds todist/.- Every
registry/*/*/*package runstsup && bht-registry-docgen, bundlingclass-namesand writingdist/anddocs.json. - The docs app's
prebuildregenerates the catalog, which imports each registry package'sdist/, thennext buildwritesapps/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 live14 // 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
ignorein.changeset/config.jsonsochangeset versionandchangeset publishonly see the registry. Thereleasescript above builds only the registry packages and the CLI; drop the CLI filter, since you will not have it. - CI.
pnpm verifyor your runner's equivalent. The build command hosts need stayspnpm build(orpnpm --filter "docs..." run build) with the output directoryapps/docs/out. See Deploy. - Docker. The shipped
Dockerfilecopies the whole repo into the build stage, which is fine; onlyapps/docs/outreaches 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:
- Keep a scratch checkout of the template:
git clone https://github.com/behivetech/bht-component-registry.git. - 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. - 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.