}>
  registry-build doesn't attach default plugins. For validation, component-index
  generation, colors/themes, an Arch-style repository emitter, or any other derived
  output, add the extension explicitly in config.

## Extension model

Extensions run in one of two stages:

* `beforeBuild`
* `afterBuild` (default when `stage` is omitted)

They receive a typed API with access to:

* the resolved config
* build artifacts
* output registration helpers
* path helpers
* custom artifact storage

Optional behavior stays composable — the core runner doesn't guess which outputs
each consumer wants.

***

## Built-in extensions

The built-in extensions are mostly UI/docs helpers. Useful, but not the definition
of the core package.

| Extension | Stage | Purpose |
| --- | --- | --- |
| `bannerExtension()` | `beforeBuild` | Print a compact build banner |
| `validateExtension()` | `beforeBuild` | Validate source paths and registry entries |
| `indexBuildExtension()` | `afterBuild` | Materialize registry entries and write `index.json` |
| `componentsExtension()` | `afterBuild` | Generate per-item JSON component payloads |
| `componentIndexExtension()` | `afterBuild` | Generate framework-specific import index files |
| `colorsExtension()` | `afterBuild` | Generate colors, themes, and CSS artifacts |

```ts
import {
  bannerExtension,
  colorsExtension,
  componentIndexExtension,
  componentsExtension,
  defineConfig,
  indexBuildExtension,
  validateExtension,
} from '@gentleduck/registry-build'

export default defineConfig({
  extensions: [
    bannerExtension({ name: 'Duck UI' }),
    validateExtension(),
    indexBuildExtension(),
    componentsExtension(),
    componentIndexExtension({
      framework: 'nextjs',
      excludeTypes: ['registry:ui', 'registry:hook'],
    }),
    colorsExtension(),
  ],
})
```

***

## Collections-first custom extensions

For new non-UI builds, the key pattern is reading named collections from the build
context.

```ts
{
  name: 'archRepository',
  stage: 'afterBuild',
  async run(api) {
    const collections = api.getArtifact('collections') ?? api.config.collections
    const packages = collections.packages?.data

    // validate input
    // derive outputs
    // register emitted files
  },
}
```

The extension stays generic:

* Config declares data and paths.
* The extension reads collections and metadata.
* The extension emits files and artifacts on purpose.

***

## Arch-style repository pattern

An Arch-like package repository extension usually does four things:

1. read `collections.packages.data`
2. group packages by repo such as `core`, `extra`, or `community`
3. emit repo-specific database and file-manifest outputs
4. emit a search or lookup artifact for downstream consumers

That is exactly the pattern used in the in-repo example under `packages/registry-build/examples/arch-package-index`.

Read the full walkthrough in [Course](/duck-registry-build/course) and [Arch Package Index](/duck-registry-build/course-arch-package-index).

***

## `componentIndexExtension`

This extension is how consumers opt into framework-facing generated import maps.

Typical options:

```ts
componentIndexExtension({
  framework: 'nextjs',
  packageMappings: {
    'registry:ui': '@example/registry-ui',
    'registry:example': '@example/registry-examples',
  },
  excludeTypes: ['registry:ui'],
  ssr: false,
  header: '// Auto-generated component index',
  generator: undefined, // set a custom (items) => string to replace the built-in adapter entirely
})
```

Use a custom `generator` when none of the built-in adapters match your target runtime.

***

## `colorsExtension`

This extension consumes resolved colors/themes data and writes derived artifacts such as:

* `colors/index.json`
* per-theme color payloads
* `themes/*.json`
* `themes.css`

When options are passed directly to the extension, use loaded objects rather than string file paths. File-path loading belongs at the root config level.

***

## Writing a custom extension

```ts
import fs from 'node:fs/promises'
import path from 'node:path'
import { defineConfig } from '@gentleduck/registry-build'

export default defineConfig({
  extensions: [
    {
      name: 'manifest',
      stage: 'afterBuild',
      async run(api) {
        const collections = api.getArtifact('collections') ?? api.config.collections
        const manifest = {
          generatedAt: new Date().toISOString(),
          collections: Object.keys(collections),
          outputs: api.listOutputs().map((output) => output.name),
        }
        const manifestPath = path.join(api.getPath('registryDir'), 'manifest.json')

        await fs.mkdir(path.dirname(manifestPath), { recursive: true })
        await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')

        api.setArtifact('manifest', manifest)
        api.registerOutput('manifest', manifestPath, {
          kind: 'custom',
        })

        return {
          itemCount: 1,
          name: 'manifest',
          outputFiles: [manifestPath],
        }
      },
    },
  ],
})
```

***

## Design guidance

}>
  Keep extensions data-first. Let the core build produce stable artifacts, then let extensions derive extra files from those artifacts. Avoid hiding base-path assumptions or hardcoded package names inside extension code.

## When to use an extension vs config

Use config when the value is static and declarative:

* collections and collection metadata
* source folders
* output directories
* package mappings
* target paths
* theme data

Use an extension when you need to execute logic:

* generate a derived file
* validate a custom contract
* publish a custom manifest
* bridge the build into another system

***

## Related pages

} className="[&_ul]:my-0">
  * [Configuration](/duck-registry-build/configuration)
  * [CLI](/duck-registry-build/cli)
  * [Performance](/duck-registry-build/performance)