Skip to main content

Recipes

Practical registry-build patterns for docs sites, custom manifests, external data files, and framework-specific output.

Recipe 1: Docs-site registry

This is the common shape for a docs app that needs:

  • index.json
  • per-item component payloads
  • a generated component index file
  • theme and color outputs
import {
  bannerExtension,
  colorsExtension,
  componentIndexExtension,
  componentsExtension,
  defineConfig,
  indexBuildExtension,
  validateExtension,
} from '@gentleduck/registry-build'
 
export default defineConfig({
  sources: {
    'registry:ui': {
      path: '../../packages/registry-ui/src',
      packageName: '@example/registry-ui',
      referencePath: '/registry-ui/src',
    },
    'registry:example': {
      path: '../../packages/registry-examples/src',
      packageName: '@example/registry-examples',
      referencePath: '/registry-examples/src',
      indexStrategy: 'file',
    },
  },
  registries: {
    uis: [...],
    examples: [...],
  },
  output: {
    dir: '.',
  },
  targetPaths: {
    'registry:ui': 'components/ui',
    'registry:example': 'components',
  },
  extensions: [
    bannerExtension({ name: 'My Docs' }),
    validateExtension(),
    indexBuildExtension(),
    componentsExtension(),
    componentIndexExtension({
      framework: 'nextjs',
    }),
    colorsExtension(),
  ],
})
import {
  bannerExtension,
  colorsExtension,
  componentIndexExtension,
  componentsExtension,
  defineConfig,
  indexBuildExtension,
  validateExtension,
} from '@gentleduck/registry-build'
 
export default defineConfig({
  sources: {
    'registry:ui': {
      path: '../../packages/registry-ui/src',
      packageName: '@example/registry-ui',
      referencePath: '/registry-ui/src',
    },
    'registry:example': {
      path: '../../packages/registry-examples/src',
      packageName: '@example/registry-examples',
      referencePath: '/registry-examples/src',
      indexStrategy: 'file',
    },
  },
  registries: {
    uis: [...],
    examples: [...],
  },
  output: {
    dir: '.',
  },
  targetPaths: {
    'registry:ui': 'components/ui',
    'registry:example': 'components',
  },
  extensions: [
    bannerExtension({ name: 'My Docs' }),
    validateExtension(),
    indexBuildExtension(),
    componentsExtension(),
    componentIndexExtension({
      framework: 'nextjs',
    }),
    colorsExtension(),
  ],
})

Recipe 2: Arch-style package repository

This pattern treats registry-build as a generic build core plus a custom extension. The core UI phases are disabled, and the custom extension emits repository metadata, package-file manifests, and search artifacts.

import { defineConfig } from '@gentleduck/registry-build'
import { archRepositoryExtension } from './arch-repository.extension'
 
export default defineConfig({
  collections: {
    packages: {
      data: './data/packages.json',
      metadata: {
        repoOrder: ['core', 'extra'],
      },
      sources: {
        pkgbuilds: {
          glob: '**/PKGBUILD',
          path: './pkgbuilds',
        },
      },
    },
  },
  extensions: [
    archRepositoryExtension({
      collection: 'packages',
    }),
  ],
  output: {
    dir: './dist',
  },
})
import { defineConfig } from '@gentleduck/registry-build'
import { archRepositoryExtension } from './arch-repository.extension'
 
export default defineConfig({
  collections: {
    packages: {
      data: './data/packages.json',
      metadata: {
        repoOrder: ['core', 'extra'],
      },
      sources: {
        pkgbuilds: {
          glob: '**/PKGBUILD',
          path: './pkgbuilds',
        },
      },
    },
  },
  extensions: [
    archRepositoryExtension({
      collection: 'packages',
    }),
  ],
  output: {
    dir: './dist',
  },
})

This is the direction for non-UI consumers: collections plus extensions, not empty componentIndex or themes objects.

See the in-repo example in packages/registry-build/examples/arch-package-index.

If you want the full guided build, continue to Course and Arch Package Index.


Recipe 3: Custom manifest extension

If another tool needs a derived manifest after the registry build finishes, expose it through an afterBuild extension:

{
  name: 'manifest',
  stage: 'afterBuild',
  async run(api) {
    const manifest = {
      outputs: api.listOutputs().map((output) => output.name),
      itemCount: (api.getArtifact('index') as Array<unknown> | undefined)?.length ?? 0,
    }
 
    // write file, register output, set artifact
  },
}
{
  name: 'manifest',
  stage: 'afterBuild',
  async run(api) {
    const manifest = {
      outputs: api.listOutputs().map((output) => output.name),
      itemCount: (api.getArtifact('index') as Array<unknown> | undefined)?.length ?? 0,
    }
 
    // write file, register output, set artifact
  },
}

Use this pattern instead of patching the core runner.


Recipe 4: External static data files

Use root config file paths when colors/themes or registry entries live in static JSON files:

export default defineConfig({
  registrySource: './data/registry-entries.json',
  colors: {
    data: './data/colors.json',
  },
  themes: {
    data: './data/themes.json',
  },
  output: {
    dir: '.',
  },
})
export default defineConfig({
  registrySource: './data/registry-entries.json',
  colors: {
    data: './data/colors.json',
  },
  themes: {
    data: './data/themes.json',
  },
  output: {
    dir: '.',
  },
})

This keeps static data portable and avoids importing giant objects into every consumer config file manually.


Recipe 5: Extend a base config

File-based composition:

export default defineConfig({
  extends: ['./registry-build.base.ts', './registry-build.theme.ts'],
  output: {
    dir: '.',
  },
})
export default defineConfig({
  extends: ['./registry-build.base.ts', './registry-build.theme.ts'],
  output: {
    dir: '.',
  },
})

Code-based composition:

import { defineConfig, mergeRegistryBuildConfigs } from '@gentleduck/registry-build'
import { base } from './registry-build.base'
import { theme } from './registry-build.theme'
 
export default defineConfig(mergeRegistryBuildConfigs(base, theme))
import { defineConfig, mergeRegistryBuildConfigs } from '@gentleduck/registry-build'
import { base } from './registry-build.base'
import { theme } from './registry-build.theme'
 
export default defineConfig(mergeRegistryBuildConfigs(base, theme))

Use file-based composition when the config should stay mostly declarative. Use code-based composition when you need parameterized factories.


Recipe 6: Custom framework component index

If the built-in framework adapters are not enough, provide a custom generator:

componentIndexExtension({
  generator(items) {
    return [
      '// custom generated file',
      `export const items = ${JSON.stringify(items, null, 2)}`,
    ].join('\n')
  },
})
componentIndexExtension({
  generator(items) {
    return [
      '// custom generated file',
      `export const items = ${JSON.stringify(items, null, 2)}`,
    ].join('\n')
  },
})

That keeps the rest of the build intact while taking full control over the generated file.


Recipe 7: Fast local rebuild loop

Use split scripts so the raw CLI path remains available:

{
  "scripts": {
    "build:reg": "bun run build:reg:cli && bun run format:reg",
    "build:reg:cli": "registry-build build",
    "format:reg": "bunx biome format --write ./__ui_registry__/index.tsx"
  }
}
{
  "scripts": {
    "build:reg": "bun run build:reg:cli && bun run format:reg",
    "build:reg:cli": "registry-build build",
    "format:reg": "bunx biome format --write ./__ui_registry__/index.tsx"
  }
}

Then for targeted local updates:

bun run build:reg:cli -- --changed-only --changed ../../packages/registry-ui/src/button/button.tsx
bun run build:reg:cli -- --changed-only --changed ../../packages/registry-ui/src/button/button.tsx

Rule of thumb