@yahoo/uds-create-config
v5.68.1
Published
Keywords
Readme
@yahoo/uds-create-config
The UDS config layer. A uds.config.ts authors one Config — the single surface both the
author (new System().register({ … })) and every consumer (codegen, Studio, the pipeline)
read. There are no per-entity derived getters and no CodegenConfig subclass: group
enumeration, the dependency graph, the folder/file tree, validation, and the flat wire format
are all first-class on Config, and derived projections are free functions that work on any
config.
What this fixes from v1
"v1" is the config layer this replaced — a bespoke class per entity, projections hanging off
.derived getters, parallel *Group entities. The differences:
- Projections are free functions, not per-entity
.derivedgetters. In v1 every entity (Token,Component,Mode,StyleProp, …) exposed a.derivedsub-object of engine-computed projections that reached cross-entity state through an injected sibling lookup / parent-Configback-ref and recomputed on each access. All of that is now free functions over a plainConfig(cssVar,className,resolveTokenValue) — no.derivedon entities, no injected back-refs, and they work on any config. - A group is a path prefix, not a Group class. v1 had explicit
TokenGroup,ComponentGroup,AssetGroupclasses (each with its own.derived). Now a kind stores one flat, path-keyed record and a group is simply a prefix of it — membership is emergent, so there are no parallel Group entities to keep in sync. A group node carries only the folder's own metadata, and a prefix with no metadata needs no node at all. - A kind is one zod schema, not a hand-written class. v1 had a bespoke class per
entity plus
define*factories and caller-path capture.defineEntity({ kind, fields })derives the typed constructors, the kind-boundref, the create/update schemas, and the operation set from a single field schema. A config TYPE names the kinds it owns, so what a config can hold depends on what was registered into it rather than on import order. The baseEntityowns hydration and serialization, so a new kind adds no constructor ortoJSON. - Identity is the qualified path.
color/brandis the identity; no caller-path capture or separately-tracked namespace. A bare value at an item key is shorthand for the kind's primary field, soToken.item('#1167f4')skips the object wrapper. Configis the single home for derived structure. Emergent grouping, the dependency graph, the folder/file tree, validation, and the flat wire format are first-class methods onConfigrather than spread across entity getters and helper modules.- Downstream projections read the live config. The pipeline owns no operation
definitions and no build-time snapshots — CRUD, MCP tools, and the AI prompt are derived
at runtime from the current
Config, so a mutation is reflected immediately (seeapps/studio/src/ai/config-v2).
Authoring
import { System, Token } from '@yahoo/uds-create-config';
export default new System()
.register({
settings: { css: { prefix: 'uds' } }, // one body, no path: settings is a singleton kind
tokens: {
color: Token.group({
label: 'Color',
items: {
brand: Token.item('#1167f4'), // bare value = the primary field
primary: Token.item(Token.ref('color/brand')), // a ref, chased at projection time
},
}),
},
});A deep path is authored as a group, with item leaves. The stored form is flat and
path-keyed — identity IS the qualified path (color/brand) — but the authored form nests,
because that is what says which prefixes are folders and lets one carry a label or a
cssPrefix. A flat 'color/brand' key registers the same item and leaves the folder
emergent, which is a different artifact. Two shapes keep the flat key, since a group cannot
express either: an owner path (Card/Header, a PART of the Card item) and a name that
contains a slash (w/1/2, the fraction). The full rule, with the failure modes, is
.claude/rules/config-authoring.md.
Defining an entity
defineEntity({ kind, fields, groupFields }) is the whole authoring toolkit for one kind,
derived from its z.object field schema. Declare the fields; get the typed item/group
constructors, the kind-bound ref, and a typed get. Registration lives on
Config.register, so the config file reads as a fluent chain.
export const Token = defineEntity({
kind: 'token',
fields: z.object({ value: z.string() /* … */ }),
});
export const { ref: token } = Token;The kind's metadata — field names, create/update schemas, operation set — is built once and
hangs off the returned class as kindDef. A config TYPE names the kind in its entities
map, and register copies that metadata into the config instance it is loading, which is
what Config hydrates and validates against. Token.extend({ … }) makes a VARIANT that
takes the base's slot, so a config authors through whichever variant it registers.
Entities
Concrete kinds live in src/entities/system/: Token, StyleProperty, Component,
Composite, Mode, Modifier, Motion, Font, Icon, Package, Guidance, CanvasRole,
GlobalStyle, Device, LinkedSystem, plus the derived Operation and Tool.
src/entities/native/ holds the native variants a ReactNativeSystem registers in their place
(NativeToken, NativeMode, NativeModifier, NativeStyleProperty).
Native component style rules accept literal arrays such as fontVariant: ['tabular-nums'],
including in linked component extensions. Register a NativeStyleProperty mapping to route a
named prop into native style, or put the array inside the layer's native style object.
Arrays contain strings, numbers or booleans; element trees remain anatomy. Arrays are admitted by the platform-neutral authoring schema; Web editing operations
continue to reject them as CSS style values. Linked size/auto tokens preserve React Native's auto keyword,
while CSS-only min-content, max-content and fit-content remain unsupported.
Components are authored with the exported defineComponent
(src/entities/system/defineComponent.ts) — see packages/minimal-template/components/.
src/authoring/define-component.ts is an unexported prototype of a different chaining
shape for the same job; nothing consumes it.
defineComponent(...) and Component.ref(...).extend(...) reject anatomy, renderProps,
file, export, and tokenRefs in types and at runtime. Declare source structure with .render().
Component.item(...) remains the typed stored-body form, including anatomy for Create drafts and
snapshots. Public editing operations and stored schemas keep their existing behavior.
Projections
CSS var names, class names, and chased token literals are exported as free functions
over a plain Config (cssVar, cssVarRef, className, resolveTokenValue) — not methods
on a subclass. So codegen and Studio get projections that work on any config without wrapping,
and the base Config stays clean. A projection shared by more than one surface is memoized on
the snapshot SLICES it reads (createSliceMemo), never on the config — a Config is immutable,
so keying on one misses on every edit.
What this package owns vs. what reads it
This package owns the config model and its projections. It does not own operations
downstream: @yahoo/uds-create-codegen emits CSS from a Config, and
apps/studio/src/ai/config-v2 derives the CRUD facade, MCP tools, and AI prompt from it.
Both just read a Config.
Develop
cd packages/config
bun test # bun:test
bun run typecheck # tsc --noEmit