@lynstack/shadcn-registry
v0.3.0
Published
Builds a shadcn registry from the source of a TypeScript component library: items, files, dependencies and theme, read from the code itself.
Maintainers
Readme
@lynstack/shadcn-registry
Builds a shadcn registry from the source of a TypeScript component library. You say where items live; the build reads everything else from the code, refuses what the shadcn CLI would install wrongly, and reports every problem at once, with its file and line where it has one.
What it reads
Files and targets: every file of an item, under its folder or the one file its rule names, except the ignored ones, with the path the shadcn CLI writes it to.
npm dependencies: every package the files import, with the range from
package.json.catalog:andworkspace:ranges become the rangespnpm publishwould write. Peer dependencies are left out, since the app has them. A type-only import may name a development dependency, which the item lists in itsdevDependencies.Registry dependencies: every other item the files import, named by
@namespace/nameor by URL, never by a bare name, which the CLI would look up in shadcn's own registry.Details: an item's
descriptionandcategoriescome from the file-level TSDoc comment of its entry, the file itself for a one-file item andindex.*for a folder: the first doc comment that carries@packageDocumentation(TSDoc) or@module(TypeDoc). The description is its@summary, or else the first paragraph of its summary; each@categoryadds a category, once. Tag names read in any case.{@link Button | a button}reads as its text, code spans keep their backticks, other inline tags and HTML tags are left out, and a block tag, even one TSDoc does not define, ends the text. Other tags are API documentation and are ignored. Thetitleis the item's name in title case./** * Shows a short status next to other content. * * @category display * @packageDocumentation */Themes:
:root,.darkand@theme inlinebecomecssVars; every other rule and at-rule becomescss, merged as the CLI merges it into the app.@source,@theme inline referenceand an@importor@pluginoftailwindcssor a subpath of it, which serve only the library's own Tailwind CSS, are left out. A package@importor@pluginadds its package as a dependency; an@importof a stylesheet on the web, such as a font's, is kept as it is.
Files and stylesheets are read with \n line endings, and items, files,
dependencies and item fields are sorted by code unit, never by locale, so
every checkout on every machine builds the same output.
What it refuses
The build replays how the CLI rewrites each import, including its lookup
of an import statement among the files one install writes (the item's,
and those of every item it depends on through its imports or its rule's
registryDependencies). It refuses:
Imports
- an import of a file no item ships, or one that cannot be resolved;
- an import of a package
package.jsondoes not declare, or of a development dependency at run time; - a range an app cannot install: a
catalog:range no catalog ofpnpm-workspace.yamldefines, aworkspace:package that is not linked, or afile:,link:,git:,git+…:,github:,http:,https:orportal:range; - a relative import that would point elsewhere once installed;
- an alias import in a
registry:file, a JavaScript,.mtsor.d.tsfile, whose aliases the CLI does not map, or one it would resolve to another module, and a local import that is neither relative nor@/or#/; - an
import()whose module name is computed; - CommonJS, whose imports the build cannot follow: a
.cjsor.ctsfile,require()of a module orimport x = require(); - a file that is missing, a folder, not UTF-8 text, or cannot be parsed.
Themes
- a block nested in a rule, a declaration outside a rule or
@utility, or a key given two values; - in
:rootand.dark, anything but custom properties; in@theme inline, anything but custom properties and@keyframes; - any
@themebut@theme inlineand@theme inline reference; - an
@importor@pluginthat names a file or a URL instead of a package (an@importof a stylesheet on the web is kept), and a@pluginwith options; - the Tailwind CSS v3 at-rules
@tailwindand@config; - a stylesheet that cannot be parsed.
Configuration
- a field that is missing, of the wrong type, or that the configuration
does not know, such as a misspelled
outdir; - two items with one name, or a name that is not lowercase letters and digits joined by single hyphens;
- an item
sourcewith backslashes, outside the root, without{item}once in its last segment, or naming a file without an extension; - a
targetthat is empty, leaves the app, or does not start with@ui,@components,@lib,@hooksor~/(only aregistry:fileorregistry:pagemay name a plain path); - a file directly in the folder of a folder rule, which belongs to no item of it;
- a rule's
registryDependenciesentry that is a bare name but no item of this registry; - a
namespaceorurlof the wrong shape, or anamethat makes a namespace of the wrong shape when neither is set, and aninstallPeersentry thatpackage.jsondoes not list inpeerDependencies; - a library that has Tailwind CSS older than 4 installed;
- an item, or
registry.json, that theshadcn/schemaof the installed CLI does not accept.
Requirements
Node.js 24.11 or later, a library of ES modules, TypeScript 7 for its
own tsconfig.json, Tailwind CSS 4 or later, and shadcn 4.21.4 or a
later 4.x, whose schema checks every item.
Install
pnpm add -D @lynstack/shadcn-registry shadcnConfigure
Create shadcn-registry.config.ts next to the library's package.json;
.mts, .js and .mjs work too. Node.js runs it directly, so it uses
only erasable TypeScript syntax.
import { defineConfig } from "@lynstack/shadcn-registry";
export default defineConfig({
name: "acme",
homepage: "https://acme.dev",
outDir: "public/r",
items: [
// One item per folder: src/components/ui/button/** is the item "button".
{
source: "src/components/ui/{item}",
type: "registry:ui",
target: "@ui",
registryDependencies: ["style"],
},
// One item per file: src/hooks/use-media.ts is the item "use-media".
{ source: "src/hooks/{item}.ts", type: "registry:hook", target: "@hooks" },
],
themes: [
{
name: "style",
type: "registry:style",
extends: "none",
sources: ["src/theme/tokens.css", "src/theme/theme.css"],
},
],
});itemsare tried in order, and a file goes to the first rule whosesourceholds it: listsrc/components/ui/{item}beforesrc/components/{item}. A file directly in the folder of a folder rule, such assrc/components/ui/index.ts, belongs to no item of it: move it into an item's folder, give it a file rule, or add it toignore.sourceis a path with forward slashes, not a glob: a folder such asapp/[locale]or(shop)matches itself. A file rule may name more than the item, such assrc/hooks/use-{item}.ts.targetis the folder the CLI writes into. It starts with@ui,@components,@lib,@hooksor~/, the folders the CLI knows; aregistry:fileorregistry:pagemay also name a path from the app's root. Each file keeps its path below the part ofsourcebefore{item}:src/components/ui/button/button.tsxgoes to@ui/button/button.tsx.registryDependenciesnames items every item of the rule depends on, beyond those its imports name: an item of this registry by its name, or another registry's item by its@namespace/nameor URL.describe(item)returns an item'stitle,description,author,docs,categoriesandmeta. It receives the item's name, its files and the details its documentation gives, and each field it returns replaces the one the documentation gives.- Items name each other
@<name>/<item>by default;namespacechanges the prefix, andurl, such as"https://acme.dev/r/{name}.json", names them by URL instead, which needs noregistriesentry in the app. - A theme's
extends: "none"makesshadcn initwith it replace shadcn's own style. installPeersnames peer dependencies an app may lack: items list them as dependencies with the peer range. Other peers, such asreact, are left out.tsconfigis the TypeScript configuration that resolves imports,tsconfig.jsonby default. Paths in the configuration start from the folder of the configuration file.ignorelists the files that never ship, as globs from the root. Tests, specs, type tests, stories and snapshots are ignored by default; settingignorereplaces that list.outDir,public/rby default, is where the build writes. As Vite empties itsoutDir, the build deletes the JSON files in it that it does not write;emptyOutDir: falsekeeps them. AnoutDirthat is the root or a folder above it draws a warning: emptied, it losespackage.json.test.templatesnames the templates ofshadcn init --templatethatshadcn-registry teststarts an app from, one app each:astro,next,react-router,startorvite, which is the default.laravelis not one, as the shadcn CLI needs an app fromlaravel new.test.dependenciesadds packages to each app.
Run
shadcn-registry build # write registry.json and <item>.json into outDir
shadcn-registry check # fail when outDir differs from a fresh build (for CI)
shadcn-registry watch # build again whenever a file under the root changes
shadcn-registry test # install every item into a new app per template (needs the network and git)Each command takes --config <file> (-c) to name the configuration
file; by default it is the first of shadcn-registry.config.ts, .mts,
.js and .mjs in the current folder. watch skips outDir,
node_modules and .git, and reads the configuration once: restart it
after changing the configuration.
test builds the registry onto a local server, then tests each template
of test.templates in turn, or of --template (-t), which replaces
them and may be repeated: shadcn-registry test -t vite -t next. A
template named twice is tested once. For
each, it sets an app up with the shadcn CLI as a user would: init
--template creates the app with shadcn's default style (the nova
preset on Base UI) and installs it with the package manager that runs the
script, registry add names the registry's namespace, init installs
the first theme whose extends is none in place of shadcn's style, and
add installs every other item, replacing any file init wrote at the
same path. Before the items, an app that pnpm installed gains the
library's pnpm build and release-age policy, and every app gains the
library's peer dependencies it lacks, at the versions the library has
installed. Then the app must build and
typecheck with its template's own build and typecheck scripts, and
the stylesheet its components.json names must hold every token of the
themes. Run it from a package.json script.
From code
buildRegistry(config, root), writeOutput and compareOutput do the
same as build and check, and testRegistry does what test does
with what loadConfig returns, and with { templates } in place of
--template. A registry with problems throws a
RegistryError that lists them all. testRegistry throws that
RegistryError, or an InstallTestError when it runs outside a
package.json script, before it writes any app. Past that, it throws an
InstallTestError when a command or a check of the app fails, or an
Error whose cause is any other failure, and either names the folder
it kept the app in.
License
MIT
