Copy-In Distribution
Component libraries age badly when the components live in node_modules: every project needs the variant the library didn't ship, and every workaround is a fork you now maintain against upstream. Families takes the position shadcn proved for UI: the components are source files in your project. The package ships the vocabulary (family, el, resolve, evaluateModel); the walls, doors, and rooms are code you own, copied in once and edited freely.
Scaffold
npm create brepjs@latest my-building
cd my-building
npm install
npm startThe scaffold is a working model, not an empty shell: src/main.ts builds a wall with a doorway and prints per-element mesh statistics through evaluateModel. From there:
npx brepjs add room storey slabbrepjs add
add resolves the requested families plus their family dependencies against a registry manifest (dependencies are written first; cycles are detected and refused) and copies their source into src/families/, reporting any npm dependencies your project is missing:
wrote /work/my-building/src/families/wall.ts
wrote /work/my-building/src/families/door.ts
wrote /work/my-building/src/families/room.ts
missing npm deps — run: npm install zodThe behavior around your existing files is strict:
- Unmodified copies are skipped as
up to date;addis idempotent. - Modified files are never clobbered without
--force. The check runs for the whole closure before anything is written: a conflict on the last file aborts with zero files touched, never a partial install. - Writes go through a sibling temp file and an atomic rename, so even an interrupted
--forceleaves every file either old or new, never truncated. --installruns the missingnpm installfor you, with lifecycle scripts suppressed. The default only prints the command.
Every copied file's first line is a machine-managed version marker:
// brepjs-family: wall@1Leave it in place; it is the anchor diff uses.
brepjs diff
npx brepjs diff walldiff compares your copy against the registry: stale version markers are called out (local wall@1, registry wall@2), content drift renders as a git diff, and the exit code is 1 on any difference, which makes it usable as a CI guard for teams that want to know when their copies diverge from upstream.
Renaming what you copied
A copied family is yours, including its name, and Storey in particular is the IFC spelling rather than the one every team uses. Rename it freely; the archetype in the file is what a BIM export routes on, not the name:
export const Level = family('Level', render, { archetype: 'storey', props: storeySchema });Keep the // brepjs-family: storey@2 marker on the first line whatever you call the family. diff anchors on it, and it is how you find out the upstream file moved on.
Self-hosting a registry
The registry is data: a manifest.json plus source files, no server logic.
{
"schemaVersion": 1,
"name": "acme standards registry",
"families": [
{
"name": "wall",
"version": 3,
"description": "Wall per Acme spec 4.2",
"files": ["families/wall.ts"],
"npmDeps": ["brepjs-families", "zod"],
"familyDeps": []
}
]
}Host it on any static server, or point at a directory:
npx brepjs add wall --registry https://standards.acme.example/families
npx brepjs add wall --registry ../company-registryThis is the intended path for firm standards: your wall types, your pset defaults, your classification codes, distributed as reviewable source with the same tooling.
The trust boundary, stated plainly
Adding families from a registry means choosing to run that registry's code in your project, the same trust decision as installing a dependency. The CLI enforces what can be enforced mechanically and leaves the rest visible:
- Remote registries must be
https; plaintexthttpis refused. - Manifest file entries are confined to
families/; entries that try to escape the target directory are refused, and writes never follow symlinks. - Declared npm dependencies must be syntactically valid package names (nothing can smuggle flags into
npm install), and--installsuppresses their install scripts. diffrefuses to read through symlinked files or directories, so a hostile checkout cannot use it to leak file contents into CI logs.
None of that makes an untrusted registry safe to use; it makes the trust decision yours instead of an accident. Point --registry only at sources whose code you would merge.
Next steps
- Why a Family Layer: the boundary table for what belongs in copied source versus the package.
- Props & Validation: the starter families ship with schemas; yours should too.