Quick Start

Getting started with Fuma CLI.

Getting Started

Installation

You can install the package to get started:

npm i fuma-cli

Define Registry

Specify the base directory for your registry.

my-repo/registry.ts
import type { Registry } from "fuma-cli/compiler";

// my-repo/src
const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), "./src");

export const registry: Registry = {
  name: "acme-ui",
  dir,
  tsconfigPath: "../tsconfig.json",
  packageJson: "../package.json",
  components: [
    {
      name: "button",
      files: [
        {
          // `type` specifies how the file should be installed
          type: "ui",
          path: "src/button.tsx",

          // optional: change the installed path, <dir> refers to the default destination
          target: "<dir>/primitives/button.tsx",
        },
      ],
    },
  ],
};

Common pitfalls

The same file cannot belong to two different top-level components.

Now compile & write the output via a script:

my-repo/scripts/compile.ts
import { compile, writeRegistry } from "fuma-cli/compiler";
import { registry } from "../registry";

const out = await compile({ root: registry });

await writeRegistry(out, {
  dir: "./public",

  // optional: clean previous outputs
  cleanDir: true,
});
node ./scripts/compile.ts

You need to serve the output files in public folder, here we assume all files under public are already served via https://acme-ui.com.

Implement Installer

Initialize your own CLI package and install dependencies:

npm i fuma-cli @clack/prompts

Fuma CLI installer is headless, we will use @clack/prompts for creating TUI.

my-cli/src/installer.ts
import { ComponentInstaller } from "fuma-cli/registry/installer";
import { HttpRegistryConnector } from "fuma-cli/registry/connector";
import type { LoadedConfig } from "@/config";
import { confirm, isCancel } from "@clack/prompts";
import picocolors from "picocolors";

export class MyComponentInstaller extends ComponentInstaller {
  constructor() {
    const connector = new HttpRegistryConnector("https://acme-ui.com");

    super(connector, {
      // optional: where to write components
      outDir: {
        base: "src",
      },

      // use our own TUI
      io: {
        onWarn(message) {
          console.warn(message);
        },
        async confirmFileOverride(options) {
          const value = await confirm({
            message: `Do you want to override ${options.path}?`,
            initialValue: false,
          });

          if (isCancel(value)) {
            outro("Installation terminated");
            process.exit(0);
          }

          return value;
        },
        onFileDownloaded(options) {
          console.log(`downloaded ${options.path}`);
        },
      },
    });
  }
}

Create Command

Create a command that installs your button component.

my-cli/src/commands/install.ts
import { isCancel, confirm, box } from "@clack/prompts";
import { MyComponentInstaller } from "../installer";

export async function install(targets: string[]) {
  const installer = new MyComponentInstaller();

  for (const target of targets) {
    const deps = await installer.install(target).then((res) => res.deps());

    if (deps.hasRequired()) {
      box([...deps.dependencies, ...deps.devDependencies].join("\n"), "New Dependencies");
      const value = await confirm({
        message: `Do you want to install dependencies?`,
      });

      if (isCancel(value)) {
        outro("Installation terminated");
        process.exit(0);
      }

      if (value) {
        await deps.installRequired();
      } else {
        await deps.writeRequired();
      }
    }
  }
}

The consumer can now execute the command to install button, e.g.

scripts/test-install.ts
import { install } from "./commands/install";

await install(["button"]);
node scripts/test-install.ts

Examples

Interactive Install UI

Allow user to select components to install.

import { isCancel, autocompleteMultiselect, outro, spinner } from "@clack/prompts";
import picocolors from "picocolors";
import { RegistryConnector } from "fuma-cli/registry/connector";
import { install } from "@/commands/install";

interface AddOption {
  label: string;
  value: string;
  hint?: string;
}

export async function add() {
  // add your connector
  const connector: RegistryConnector;

  const spin = spinner();
  spin.start("fetching registry");
  const info = await connector.fetchRegistryInfo();

  spin.stop(picocolors.bold(picocolors.greenBright("registry fetched")));
  const value = await autocompleteMultiselect({
    message: "Select components to install",
    options: info.indexes.map<AddOption>((item) => ({
      label: item.title ?? item.name,
      value: item.name,
      hint: item.description,
    })),
  });

  if (isCancel(value)) {
    outro("Ended");
    return;
  }

  await install(value);
  outro(picocolors.bold(picocolors.greenBright("Successful")));
}

On this page