Documentation

Build System

Frame-Master uses a singleton builder that merges configurations from every plugin into one coordinated build pipeline.

📦 Overview

  • Single shared builder instance that includes configs from all loaded plugins
  • beforeBuild/afterBuild lifecycle hooks for custom logic
  • Build analytics, history, and report generation
  • Concurrent build protection to prevent race conditions

🚀 Using the Builder

Basic Usage

import { builder } from "frame-master/build";
 
export function myPlugin(): FrameMasterPlugin {
  return {
    name: "my-plugin",
    version: "1.0.0",
    serverStart: {
      main: async () => {
        const result = await builder.build("/src/client.ts", "/src/entry.tsx");
        if (result.success) {
          console.log("Build successful!", result.outputs.length, "files");
        } else {
          console.error("Build failed:", result.logs);
        }
      },
    },
  };
}

Builder Methods

  • builder.build(...entrypoints) → run build with merged config
  • builder.isBuilding() → check if a build is in progress
  • builder.awaitBuildFinish() → wait for the current build, or null if idle
  • builder.getConfig() → current merged BuildConfig
  • builder.analyzeBuild() → size/kind stats for last build
  • builder.generateReport("text" | "json") → formatted report

⚙️ Build Configuration

Static Configuration

export function myPlugin(): FrameMasterPlugin {
  return {
    name: "my-plugin",
    version: "1.0.0",
    build: {
      buildConfig: {
        external: ["react", "react-dom"],
        target: "browser",
        minify: true,
        sourcemap: "external",
        define: { "process.env.NODE_ENV": JSON.stringify("production") },
      },
    },
  };
}

Dynamic Configuration

build: {
  buildConfig: async () => {
    const isDev = process.env.NODE_ENV !== "production";
    return {
      external: ["react", "react-dom"],
      minify: !isDev,
      sourcemap: isDev ? "inline" : "external",
      define: {
        "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
        "__DEBUG__": isDev.toString(),
        "__BUILD_TIME__": JSON.stringify(new Date().toISOString()),
      },
      plugins: [myCustomBunPlugin({ debug: isDev })],
    };
  },
}

Type-Safe Configuration

import { defineBuildConfig } from "frame-master/build";
 
build: {
  buildConfig: defineBuildConfig({
    target: "browser",
    external: ["react"],
    minify: true,
    splitting: true,
  }),
}

🔀 Configuration Merging

  • entrypoints, external: deduplicated + concatenated
  • plugins: concatenated (order preserved)
  • define, loader: deep merged (later wins)
  • primitives: last plugin wins (warns)

Example merge:

// Plugin A
buildConfig: { external: ["react"], define: { __A__: "true" } }
 
// Plugin B
buildConfig: { external: ["lodash"], define: { __B__: "true" } }
 
// Result
external: ["react", "lodash"]
define: { __A__: "true", __B__: "true" }

🪝 Lifecycle Hooks

beforeBuild

build: {
  beforeBuild: async (config) => {
    console.log("Starting build", config.entrypoints.length, "entries");
    if (!config.outdir) throw new Error("Output directory not specified");
    await Bun.write(
      ".frame-master/build-info.json",
      JSON.stringify({ timestamp: new Date().toISOString(), entrypoints: config.entrypoints, target: config.target })
    );
  },
}

afterBuild

build: {
  afterBuild: async (config, result, builder) => {
    if (!result.success) return;
 
    const manifestPath = `${config.outdir}/manifest.json`;
    await Bun.write(
      manifestPath,
      JSON.stringify({ files: result.outputs.map((o) => o.path), buildTime: new Date().toISOString() }, null, 2)
    );
 
    // Register custom files so cleanup keeps them
    result.outputs.push({ path: manifestPath, kind: "asset", hash: "", loader: "file" } as Bun.BuildArtifact);
 
    const analysis = builder.analyzeBuild();
    if (analysis.totalSize > 1_000_000) console.warn("Bundle exceeds 1MB", analysis.totalSize);
  },
}

enableLoging

build: {
  enableLoging: process.env.NODE_ENV !== "production",
}

📊 Build Analysis

const result = await builder.build("/src/client.ts");
if (result.success) {
  const analysis = builder.analyzeBuild();
  console.log("Total size", analysis.totalSize);
  console.log("Average size", analysis.averageSize);
  console.log("Largest", analysis.largestFiles.slice(0, 5));
  console.log("By kind", analysis.byKind);
}

History and reports:

const history = builder.getBuildHistory();
const successRate = history.filter((b) => b.success).length / history.length;
builder.clearBuildHistory();
 
const textReport = builder.generateReport("text");
const jsonReport = builder.generateReport("json");

🔒 Concurrent Build Protection

if (builder.isBuilding()) await builder.awaitBuildFinish();
await builder.build("/src/index.ts");

Warning: The builder throws if you start a build while another is running—check first.

🧹 Output Cleanup

After each successful build, files not listed in result.outputs are deleted from outdir.

Register custom files:

afterBuild: async (config, result) => {
  if (!result.success) return;
  const path = `${config.outdir}/my-custom-file.json`;
  await Bun.write(path, JSON.stringify({ data: "value" }));
  result.outputs.push({
    path,
    kind: "asset",
    hash: "",
    loader: "file",
  } as Bun.BuildArtifact);
};

🛠️ Helper Utilities

import Builder from "frame-master/build";
 
// Build regex for Bun plugins
const filter = Builder.pluginRegexMake({
  path: ["src", "components"],
  ext: ["ts", "tsx"],
});
 
// Return empty stubs for server-only modules
build.onLoad({ filter: /\.server\.(ts|tsx)$/ }, async (args) => {
  const mod = await import(args.path);
  return Builder.returnEmptyFile("tsx", mod);
});

💻 CLI Build Command

NODE_ENV=production frame-master build
NODE_ENV=production frame-master build --verbose

Flow: sets env, initializes plugins, loads builder, runs build, prints summary.

📝 Complete Example

import { builder, defineBuildConfig, Builder } from "frame-master/build";
import type { FrameMasterPlugin } from "frame-master";
 
export function myBuildPlugin(): FrameMasterPlugin {
  return {
    name: "my-build-plugin",
    version: "1.0.0",
    build: {
      buildConfig: async () => {
        const isDev = process.env.NODE_ENV !== "production";
        return defineBuildConfig({
          target: "browser",
          external: ["react", "react-dom"],
          minify: !isDev,
          sourcemap: isDev ? "inline" : "external",
          splitting: true,
          define: {
            "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
            __VERSION__: JSON.stringify("1.0.0"),
          },
          plugins: [
            {
              name: "server-stub",
              setup(build) {
                build.onLoad({ filter: /\.server\.tsx?$/ }, async (args) => {
                  const mod = await import(args.path);
                  return Builder.returnEmptyFile("tsx", mod);
                });
              },
            },
          ],
        });
      },
      beforeBuild: async (config) => {
        console.log("🔨 Starting build...", config.entrypoints.length);
        await Bun.write(
          ".frame-master/build-time.txt",
          new Date().toISOString(),
        );
      },
      afterBuild: async (config, result, builder) => {
        if (!result.success) return;
        const analysis = builder.analyzeBuild();
        await Bun.write(
          `${config.outdir}/manifest.json`,
          JSON.stringify(
            {
              buildTime: new Date().toISOString(),
              files: analysis.artifacts.map((a) => ({
                path: a.path,
                size: a.size,
                kind: a.kind,
              })),
              totalSize: analysis.totalSize,
            },
            null,
            2,
          ),
        );
      },
      enableLoging: process.env.DEBUG_BUILD === "true",
    },
    serverStart: {
      main: async () => {
        if (process.env.NODE_ENV === "production") {
          const result = await builder.build();
          if (!result.success) throw new Error("Production build failed");
        }
      },
    },
  };
}

🎯 Next Steps