Architecture
Understand the registry-build lifecycle, build context, cache model, and extension boundaries.
The design goal is simple: static inputs live in config, the core runner produces deterministic base artifacts, and extensions add opt-in behavior after that base state exists. New non-UI consumers should think in named collections first; legacy UI fields are normalized into that model for compatibility.
Lifecycle
Build context
The runtime context is the shared contract between the config loader, the core phases, and every extension.
It carries:
- resolved config
- resolved collections
- config path and config directory
- output paths
- path registry helpers
- collected build artifacts
- output registration records
- a per-build
ts-morphproject - cache access
- changed-only flags and resolved changed paths
That keeps the pipeline explicit. Phases and extensions do not need to rediscover global state on their own.
Core responsibilities
Collections model
Collections are the domain-neutral input surface. A collection can hold file-backed data, named source trees, and arbitrary metadata. Extensions can read those collections without caring whether the consumer is building UI artifacts, package repositories, or search indexes.
Legacy compatibility layer
Older UI-centric fields such as sources and registries are still supported. During config resolution they are translated into generic collection artifacts so extensions and future pipeline work can target one shared model instead of two unrelated APIs.
Config loader
The loader finds registry-build.config.*, resolves extends, normalizes config-relative paths immediately, materializes external file-backed data, applies defaults, derives compatibility collections, and validates the final config with Zod.
Extension-driven phases
The core runner has no built-in phases. All processing — index building, component generation, validation, colors/themes, component indexes, banners — is performed by extensions registered in the extensions array.
For UI registries, indexBuildExtension() materializes registry entries into index.json and componentsExtension() transforms each indexed item into its generated JSON payload. Both run as afterBuild extensions and are included automatically by uiRegistryPreset().
Extension boundary
The core runner only provides the build context, cache management, and extension execution loop. All domain-specific outputs belong in extensions, not in the runner itself.
Artifact model
The runner exposes two related concepts:
| Concept | Meaning |
|---|---|
artifacts | In-memory values shared between phases and extensions |
outputs | Files or file groups registered as generated outputs |
Typical examples:
- artifact:
collections - artifact:
index - output:
public/r/index.json - output:
public/r/components/*.json - output:
__ui_registry__/index.tsx - output:
dist/arch/repos/core.db.json - output:
public/r/themes.css
This split is what lets extensions stay data-first. They can read artifacts and register outputs without pretending they are part of the core phase graph.
Cache model
The cache stores:
- file hashes
- phase-specific manifest data
The cache is local, incremental state. It is not source of truth and should always be safe to delete.
What the cache is for
- skip rehashing unchanged files
- skip rematerializing unchanged registry items
- skip rewriting identical generated outputs
- support
--changed-onlylocal rebuilds
What the cache is not for
- long-term artifact storage
- cross-machine shared caching
- replacing deterministic generation
Design rules
- Keep config declarative and extensions imperative.
- Keep the core generic and small.
- Make
collectionsthe first stop for new non-UI consumers. - Resolve paths relative to the file that declared them.
- Prefer stable artifacts over framework-specific shortcuts in the core.
- Treat generated output ordering as part of correctness.