Bun als Bundler: Plugins und Tree-Shaking

Build-Prozesse optimieren ohne Webpack oder Rollup

6 Minuten
Bun als Bundler: Plugins und Tree-Shaking
#Bun #Bundler #JavaScript #TypeScript

Bun ist nicht nur Runtime, sondern auch Bundler. Kein Webpack, kein Rollup, keine separate Konfigurationsdatei. Der Bundler ist in die Runtime integriert und nutzt dieselbe schnelle Zig-basierte Architektur.

Grundlegende Nutzung

Ein einfacher Build:

bun build ./src/index.ts --outdir ./dist

Oder programmatisch:

const result = await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
});

if (!result.success) {
  console.error("Build failed:", result.logs);
}

Das Ergebnis ist ein optimiertes JavaScript-Bundle.

Build-Optionen

Die wichtigsten Konfigurationsoptionen:

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  
  // Zielumgebung
  target: "browser", // oder "bun", "node"
  
  // Optimierungen
  minify: true,
  sourcemap: "external",
  
  // Splitting für Code-Splitting
  splitting: true,
  
  // Externe Pakete nicht bundlen
  external: ["react", "react-dom"],
  
  // Umgebungsvariablen inlinen
  define: {
    "process.env.NODE_ENV": JSON.stringify("production"),
  },
});

Plugins entwickeln

Plugins erweitern den Build-Prozess. Die API ist einfach:

import type { BunPlugin } from "bun";

const myPlugin: BunPlugin = {
  name: "my-plugin",
  setup(build) {
    // Hooks registrieren
  },
};

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  plugins: [myPlugin],
});

onLoad Hook

Der häufigste Hook transformiert Dateien:

const textPlugin: BunPlugin = {
  name: "text-loader",
  setup(build) {
    build.onLoad({ filter: /\.txt$/ }, async (args) => {
      const content = await Bun.file(args.path).text();
      return {
        contents: `export default ${JSON.stringify(content)}`,
        loader: "js",
      };
    });
  },
};

Jetzt können .txt-Dateien importiert werden:

import readme from "./README.txt";
console.log(readme);

onResolve Hook

Für Custom Module Resolution:

const aliasPlugin: BunPlugin = {
  name: "alias",
  setup(build) {
    build.onResolve({ filter: /^@components\// }, (args) => {
      const path = args.path.replace("@components/", "./src/components/");
      return { path };
    });
  },
};

Praktisches Beispiel: YAML-Loader

import { parse } from "yaml";

const yamlPlugin: BunPlugin = {
  name: "yaml-loader",
  setup(build) {
    build.onLoad({ filter: /\.ya?ml$/ }, async (args) => {
      const text = await Bun.file(args.path).text();
      const data = parse(text);
      return {
        contents: `export default ${JSON.stringify(data)}`,
        loader: "js",
      };
    });
  },
};

Tree-Shaking

Tree-Shaking ist standardmäßig aktiviert. Der Bundler entfernt ungenutzte Exports automatisch.

ESM-Module

Bei ES-Modulen funktioniert Tree-Shaking optimal:

// utils.ts
export const used = () => "wird genutzt";
export const unused = () => "wird entfernt";

// index.ts
import { used } from "./utils";
console.log(used());
// unused wird aus dem Bundle entfernt

CommonJS-Einschränkungen

Bei CommonJS ist statische Analyse schwieriger:

// Problematisch - dynamischer Export
module.exports = require("./dynamic");

// Besser - statischer Export
module.exports = {
  foo: require("./foo"),
  bar: require("./bar"),
};

Bun konvertiert CommonJS zu ESM wo möglich, aber dynamische Patterns verhindern Tree-Shaking.

Seiteneffekte markieren

Für optimales Tree-Shaking in package.json:

{
  "sideEffects": false
}

Oder spezifische Dateien mit Seiteneffekten:

{
  "sideEffects": ["./src/polyfills.js", "*.css"]
}

Code-Splitting

Mit splitting: true erzeugt Bun separate Chunks:

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  splitting: true,
});

Dynamische Imports werden automatisch in separate Dateien extrahiert:

// Wird als separater Chunk gebundelt
const module = await import("./heavy-module");

Sourcemaps

Für Debugging in Production:

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  sourcemap: "external", // oder "inline", "none"
  minify: true,
});

Externe Sourcemaps landen als .js.map-Dateien im Output-Verzeichnis.

Build-Artefakte

Bun.build gibt ein Array von BuildArtifact-Objekten zurück:

const result = await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
});

for (const artifact of result.outputs) {
  console.log(artifact.path);      // Ausgabepfad
  console.log(artifact.kind);      // "entry-point", "chunk", "asset"
  console.log(artifact.loader);    // "js", "css", etc.
  
  // Inhalt lesen
  const text = await artifact.text();
}

Vergleich mit anderen Bundlern

FeatureBunWebpackRollupesbuild
Zero-ConfigJaNeinNeinJa
TypeScriptNativeLoaderPluginNative
Tree-ShakingJaJaJaJa
Code-SplittingJaJaJaJa
Plugin-APIEinfachKomplexMittelEinfach
SpeedSehr schnellLangsamMittelSehr schnell

Bun und esbuild sind ähnlich schnell. Buns Vorteil: Es ist gleichzeitig Runtime, Bundler und Package Manager.


Quellen