Component Packs and Contract
The component-pack and component-contract interfaces that connect hosts to Composer.
The contract separates data from executable components
@zudo-composer/component-contract defines how a host supplies components to Composer. A pack contains a serializable manifest and a trusted runtime registry. The manifest describes component IDs, schema versions, source imports, defaults, fields, and slots. The runtime supplies actual component functions and optional rendering or inline-editor adapters.
Persisted documents contain component IDs, versions, JSON props, and named slots. They never contain the component functions or adapters. This same separation lets headless validation and source generation use a manifest while the preview iframe renders trusted functions.
| Module | Responsibility |
|---|---|
| file: | Public contract exports, including authoring helpers, schemas, and runtime resolution. |
| file: | Manifest/runtime types and the typed authoring-definition contract. |
| file: | Validate serializable definitions, public source imports, JSON values, and manifest/runtime parity. |
| file: | Resolve a persisted component node against the trusted pack. |
| file: | Resolve and load the host's pack, then check its declared source modules. |
| file: | Adapt a validated pack into the catalog and runtime entries used by Composer. |
Slots, accepts, and bounds
A SlotDefinition names a persisted slot id, the real component prop it projects to, a display label, and a cardinality of "single" or "many". Two further fields narrow what that slot holds:
accepts— an ordered list of component ids the slot allows. Omitted means every component in the pack is accepted; present, it is a hard restriction: consumers reject an insert whose component id is not listed, exactly like a cycle or a missing component.min/max— optional non-negative integer bounds on the slot's direct children.cardinality: "single"already holds at most one node, somin > 1ormax > 1is rejected when the manifest is validated (INVALID_SLOT_BOUNDS).
max is a hard cap: an insert that would exceed it is rejected the same way an insert outside accepts is. min is soft — a completeness requirement surfaced as a diagnostic (describeSlotCompleteness in src/) that never blocks rendering, export, or a published Global template outlet from staying empty in its own source. A consumer bound to that outlet is judged against its own roots instead; see Page kinds for the end-to-end mechanism and packages/ for the exact bound rules. Omitted min/max leave behavior unchanged from before these bounds existed.
Sidecars and authoring helpers
A sidecar is a provider-owned module that pairs an existing component with its Composer metadata. defineComponent<Props>()(Component, definition) keeps the definition's prop names and values tied to the component's props. defineComponentPack projects those definitions into a JSON manifest and a runtime registry, then validates their identities, versions, and adapters.
import {
defineComponent,
defineComponentPack,
} from "@zudo-composer/component-contract";
export function Banner({ headline }: { headline: string }) {
return <h2>{headline}</h2>;
}
const banner = defineComponent<{ headline: string }>()(Banner, {
id: "site.banner",
schemaVersion: 1,
title: "Banner",
category: "Content",
description: "A headline for the host site.",
source: {
module: "my-site/components",
exportKind: "named",
exportName: "Banner",
},
defaults: { headline: "Welcome" },
fields: [
{ prop: "headline", label: "Headline", schema: { type: "string" }, editor: { kind: "text" } },
],
});
export const componentPack = defineComponentPack({
packId: "my-site",
packVersion: "1.0.0",
components: [banner],
});This example belongs behind the host's my-site/components export. A pack entry must export the named componentPack value. The provider adapter calls validateRuntimeParity; a loose list of components is not the contract.
The internal parseSource function in validation.ts restricts source modules to public bare-package imports and rejects any src path segment. It validates named/default export metadata and local identifiers. It is not a public export to import from the package; public manifest validation runs it for callers.
Two pack shapes, one resolver
| Shape | Configured pack | Component source.module in the example | Repository fixture |
|---|---|---|---|
| Installed themeset | @acme/ | @acme/themeset | fixtures/themeset-host/ |
| Host self-reference | my-site/components | my-site/components | fixtures/self-host/ |
A self-reference uses the host's own package name and an exports entry such as ".. An installed themeset supplies the same public export contract through a dependency. Neither configuration uses a relative filesystem path for pack.
resolveComponentPack uses createRequire anchored at the host's package.json. assertPackSourcesResolvable checks every distinct source.module from that same root because generated composition modules live in the host tree. Pack and component-source exports must be resolvable under that require-based lookup: use a string target or a suitable default condition, not only import/types conditions. An unresolved pack or source fails explicitly; there is no bundled fallback pack.
plugins/ publishes the chosen entry through virtual:zudo-composer-pack. src/ is the production selection point used by the feature layer.
Styles belong to the host and provider
The host's configured stylesheet, normally styles/, imports pack CSS and declares the pack's Tailwind @source locations. The tool loads it through virtual:zudo-composer-host-styles. file:
Tool chrome styles do not import a particular pack. Their custom-property defaults live in file:
The owned pack proof
@zudo-composer/ui in packages/ui is this repository's ordinary dogfood pack. It owns its component sidecars, runtime pack, and canonical component CSS. The tool never bundles it or falls back to it. The pack's focused @takazudo/zfb-md-wasm dependency supports ProseMd rendering.
The provider proof has several distinct parts:
src/checks the advertised twelve-component pack and its schema identities.features/ composer/ _ _ tests_ _ / active- pack. test. ts scripts/checks thecheck- provider- boundary. mjs workspace:*pack dependency, its lockfile link, and sidecar/runtime-list correspondence.src/exercises real component rendering, named slots, and ProseMd WASM output;app/ _ _ tests_ _ / provider- render. test. tsx provider-css.test.tschecks stylesheet ownership and integration.scripts/checks emitted resources, including exactly one focused render WASM file and its glue.check- dist- artifact. mjs
The pack's package metadata version, its pack protocol identity, and the contract package version are separate values:
| Identity | Current handoff |
|---|---|
| Pack package | @zudo- |
| Component-pack identity | @zudo- |
| Contract API/package | @zudo- |
file: contract-handoff.json records the contract's external package commit and root Git spec, and file: ui-handoff.json records the pack's. This monorepo uses workspace:* for both. See Provider and Contract Handoffs for update procedures.