Plugin UI pages are distributed as a shadcn v4 registry so consumers can eject and customize the UI layer via npx shadcn@latest add.
Key files
| File | Purpose |
|---|---|
packages/stack/scripts/build-registry.ts |
Build script — globs plugin components, rewrites imports, outputs JSON |
packages/stack/scripts/test-registry.sh |
E2E test — packs @btst/stack, installs via shadcn, builds Next.js project |
packages/stack/registry/btst-{name}.json |
Per-plugin registry item (committed, regenerated by build script) |
packages/stack/registry/registry.json |
Combined registry collection (committed, regenerated) |
.github/workflows/registry.yml |
CI — rebuilds registry and runs E2E on PRs and main |
Design rules
- Hooks are excluded — components import hooks from
@btst/stack/plugins/{name}/client/hooks. Only the view layer is ejectable. - Routable pages — wire back via
pageComponentson the client plugin config when the plugin supports page overrides. @workspace/uiimports are rewritten: standard shadcn components →registryDependencies; custom components (page-wrapper,empty, etc.) → embedded asregistry:componentfiles frompackages/ui/src/.- Directory structure is preserved — source files land at
src/components/btst/{name}/client/{relative}so all relative imports remain valid. - Workspace component API compatibility — the workspace version must match the standard shadcn API. Never add custom props that standard shadcn doesn't have.
EXTERNAL_REGISTRY_COMPONENTS
Multi-file @workspace/ui components (directories, not single files) must be mapped to an external registry URL:
// In packages/stack/scripts/build-registry.ts
const EXTERNAL_REGISTRY_COMPONENTS: Record<string, string> = {
"auto-form": "https://shadcn-registry.example.com/auto-form",
"minimal-tiptap": "https://shadcn-registry.example.com/minimal-tiptap",
// add new multi-file components here
}
If a new component imports from @workspace/ui/components/my-dir/... (a directory), add it here.
Adding a plugin — 4 steps
- Add a
PluginConfigentry toPLUGINSinpackages/stack/scripts/build-registry.ts - Run
pnpm --filter @btst/stack build-registry - Run
pnpm --filter @btst/stack test-registrylocally - Commit the updated
registry/*.jsonfiles
Gotchas
- Registry not rebuilt after plugin changes — always run
build-registryand commit the updated JSON. CI rejects stale output; commit it before final verification. Do not auto-commit or use skip-CI commits after the checks. @workspace/uisub-path components not found — if a new component imports from a directory, add it toEXTERNAL_REGISTRY_COMPONENTS.- Hooks accidentally included — check that
hooks/files show asskipin the build output. UpdateshouldExclude()inbuild-registry.tsif a plugin has hooks in a non-standard location. - Custom prop on workspace component — remove the custom prop and use the standard shadcn API instead. The shadcn CLI will overwrite embedded components when installing other components that share the same dependency (e.g.
popover).