Blu for developers

Claude Code plugin · v1.2.1

/plugin marketplace add https://developers.blu-ce.horizonmedia.com/plugins/claude-code/marketplace.json
/plugin install blu@blu

Download blu-1.2.1.zip

Front end, inline (React + Vite, Module Federation)

Inside Blu, the shell loads the module's federated entry into its own page and renders the exposed component. The same component also runs standalone through @blu/sdk/dev.

1. One exposed page component

Expose one component (by convention ./Page) that renders the module. It takes no props: everything comes from hooks.

// src/Page.tsx
import { useEffect, useState } from "react";
import { useApiClient, useClient, useRoute, useTheme, useUser } from "@blu/sdk";

export default function Page() {
  const user = useUser();
  const client = useClient();             // changes when the user switches client in Blu's header
  const api = useApiClient();             // your API only, with a token for it
  const route = useRoute();               // where the user is inside your module
  const theme = useTheme();

  const [rows, setRows] = useState<Campaign[] | null>(null);
  useEffect(() => {
    setRows(null);
    api.get<Campaign[]>("/api/campaigns").then(setRows);
  }, [api, client.id]);                   // refetch per client

  return route.path.startsWith("/campaigns/")
    ? <CampaignDetail id={route.path.split("/")[2]} onBack={() => route.navigate("/campaigns")} />
    : <CampaignList rows={rows} onOpen={(id) => route.navigate(`/campaigns/${id}`)} />;
}

2. Vite + Module Federation

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { federation } from "@module-federation/vite";

export default defineConfig({
  base: "./",                                   // one build for every environment
  plugins: [
    react(),
    federation({
      name: "pacing",                           // = manifest remote.name
      filename: "remoteEntry.js",
      manifest: true,                           // emits mf-manifest.json (the manifest's remoteEntry URL)
      dts: false,
      exposes: { "./Page": "./src/Page.tsx" },  // = manifest remote.exposed
      shared: {
        react: { singleton: true },
        "react-dom": { singleton: true },
        "@blu/sdk": { singleton: true },        // REQUIRED: the shell provides it; two copies = no Blu context
      },
    }),
  ],
  build: { target: "esnext", outDir: "dist" },
});

Dev dependencies: vite, @vitejs/plugin-react, @module-federation/vite. Deploy dist/ to the URL the manifest's remoteEntry points at, with CORS allowing the Blu shell's origins.

3. Standalone (keep the app working on its own)

// src/standalone.tsx: the entry of your own index.html
import { mountStandalone } from "@blu/sdk/dev";
import Page from "./Page";
mountStandalone({ Page });

It reads two files served next to index.html at runtime:

"Sign in with Blu" returns only to addresses listed in the manifest's standaloneRedirectUris.

If the app already has its own standalone shell and sign-in, keep it, and wrap Page yourself only where it needs Blu's context.