Installer

Install compiled registry components into a consumer codebase.

Connectors

A connector tells the installer where to read the registry from.

import { HttpRegistryConnector } from "fuma-cli/registry/connector";

// the base URL you serve the registry output at
const connector = new HttpRegistryConnector("https://acme-ui.com/registry");

Implement RegistryConnector for other sources, it has two methods: fetchManifest(registry?) and fetchFile(file, registry?).

Installer

It accepts a config object, it is recommended to implement your own config system, then pass the config to installer.

import { ComponentInstaller } from "fuma-cli/registry/installer";
import { HttpRegistryConnector } from "fuma-cli/registry/connector";

const installer = new ComponentInstaller(
  new HttpRegistryConnector("https://registry.example.com"),
  {
    // options
  },
);

const plan = await installer.install("button");

// or from a sub registry:
await installer.install("chart", "acme/charts");

const deps = await plan.deps();
if (deps.hasRequired()) await deps.installRequired(); // runs detected package manager

Plan & Apply

install() is a shorthand of two steps:

const plan = await installer.plan([{ name: "button" }, { name: "chart", registry: "acme/charts" }]);

for (const file of plan.files) {
  // "create" | "overwrite" | "merge" | "unchanged"
  console.log(file.status, file.output);
}

await installer.apply(plan);
  • plan() links the components against the consumer's codebase, and downloads the files to install. Nothing is written.
  • apply() writes the files, asking io.confirmFileOverride() for the ones with overwrite status.
StatusMeaning
createThe file doesn't exist.
overwriteThe file exists with different content.
mergeA tree-shaken file exists, and lacks some declarations in use.
unchangedNothing to write.

A merge only adds code, so it needs no confirmation:

  • Statements are skipped when the file already declares one of their names, along with what only they reference.
  • Imports are inserted after the last import, other statements are appended.
  • Statements that declare nothing, like "use client", are only written when the file is created.

Route handlers are installed by a built-in plugin, using the same hooks available to you.

How preserved imports are linked

An import of a preserved file points to the local file when it is part of the plan, or already exists in the consumer's codebase. Otherwise, it stays as a package import.

Options

OptionDescription
cwdThe consumer's project, defaults to process.cwd().
frameworkSee below.
outDirDirectory of each file type, base is prepended to all. src is detected.
ioSee below.
registryAliasesSee below.
pluginsSee Plugins.

framework

Fuma CLI generates code based on consumer's framework (e.g. next or tanstack-start).

Instead of detecting automatically, you can specify it:

const installer = new ComponentInstaller(connector, {
  // 'none' = framework-agnostic
  framework: "none",
});

io

Customise the IO interface, useful for integrating with your own TUI.

const installer = new ComponentInstaller(connector, {
  io: {
    confirmFileOverride(file) {
      return true;
    },
    onFileWritten(file) {},
  },
});

registryAliases

Install files of another registry with the same file structure, like the same UI built on a different headless library.

const installer = new ComponentInstaller(connector, {
  registryAliases: {
    "acme/radix-ui": "acme/base-ui",
  },
});

Files referencing acme/radix-ui:button.tsx now install acme/base-ui:button.tsx.

Last updated on

On this page