Build System
Frame-Master uses a singleton builder that merges configurations from every plugin into one coordinated build pipeline.
📦 Overview
- Single shared
builderinstance 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 configbuilder.isBuilding()→ check if a build is in progressbuilder.awaitBuildFinish()→ wait for the current build, ornullif idlebuilder.getConfig()→ current mergedBuildConfigbuilder.analyzeBuild()→ size/kind stats for last buildbuilder.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 + concatenatedplugins: 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 --verboseFlow: 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");
}
},
},
};
}