How to Configure Loaders for Custom File Types in Bun: A Complete Plugin Guide

To configure loaders for custom file types in Bun, create a plugin that implements the onLoad hook and returns a loader property mapping to a built-in loader name like "text", "css", or "ts".

Bun's bundler automatically selects built-in loaders based on file extensions, but handling non-standard formats—such as .scss, .md, or .graphql—requires custom configuration via the plugin API. This guide explains how to leverage src/js/builtins/BundlerPlugin.ts and the internal LOADERS_MAP to route custom file types through Bun's existing transformation pipeline.

Understanding Bun's Loader Architecture

Bun's bundler uses a loader system to determine how a file should be parsed and transformed. The architecture distinguishes between two categories:

  • Built-in loaders are selected automatically from file extensions (e.g., .ts → ts, .json → json, .css → css).
  • Custom loaders are configured through Bun plugins that implement the onLoad hook and return a custom loader name.

According to the source code in src/js/builtins/BundlerPlugin.ts (lines 500–531), the bundler maintains an internal LOADERS_MAP (referenced as $LoaderLabelToId) that maps string loader names to numeric IDs. When a plugin returns a loader name, the bundler validates it against this map before processing the file.

How the Loader Selection Process Works

When you configure a custom loader via a plugin, the bundler follows a specific resolution and transformation flow:

  1. Import Resolution – The optional onResolve hook can rewrite import paths or handle custom protocols.
  2. Load Phase – When the bundler reaches a file, it executes onLoad callbacks stored in BundlerPlugin.onLoad.
  3. Loader Assignment – The plugin callback returns an object containing the loader property (a string name) and optionally contents or exports.
  4. ID Mapping – The bundler looks up the numeric loader ID via LOADERS_MAP = $LoaderLabelToId in BundlerPlugin.ts.
  5. Processing – The chosen loader processes the file (e.g., the css loader parses CSS, the tsx loader transpiles TypeScript-JSX).

Because the loader name must exist in the internal map, you normally reuse an existing built-in loader that can handle the target syntax. For example, you might pre-process SCSS into CSS, then return loader: "css" to let Bun handle the rest.

Configuring Loaders for Custom File Types

To add support for custom file extensions, create a Bun plugin that intercepts the file type and returns the appropriate loader configuration.

Example 1: Loading Markdown Files as Text

The following plugin treats .md files as plain text, allowing you to import markdown files directly as strings:

// markdown-loader.ts
import type { BunPlugin } from "bun";

const markdownPlugin: BunPlugin = {
  name: "markdown-loader",
  setup(build) {
    // Resolve .md imports as normal file URLs
    build.onResolve({ filter: /\.md$/ }, args => ({
      path: args.path,
      namespace: "file",
    }));

    // Load .md files using the built-in "text" loader
    build.onLoad(
      { filter: /\.md$/ },
      async () => {},               // defer not needed here
      ({ path }) => ({
        loader: "text",            // tells Bun to use the text loader
      }),
    );
  },
};

export default markdownPlugin;

Register the plugin in your build configuration:

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

Now import readme from "./README.md" returns the file contents as a string.

Example 2: Preprocessing SCSS to CSS

For files that require transformation before Bun can process them, compile the source in the onLoad hook and return the result with a built-in loader:

import type { BunPlugin } from "bun";
import sass from "sass"; // native npm package; Bun can import it directly

const scssPlugin: BunPlugin = {
  name: "scss-loader",
  setup(build) {
    // Resolve .scss files
    build.onResolve({ filter: /\.scss$/ }, args => ({
      path: args.path,
      namespace: "file",
    }));

    // Load .scss files, compile to CSS, then use the "css" loader
    build.onLoad(
      { filter: /\.scss$/ },
      async () => {},                 // no defer needed
      ({ path }) => {
        const source = Bun.file(path).text();   // read raw SCSS
        const compiled = sass.renderSync({ data: source }).css.toString();
        return {
          loader: "css",           // use built-in CSS loader for the result
          contents: compiled,       // provide the transformed CSS text
        };
      },
    );
  },
};

export default scssPlugin;

This approach leverages Bun's native CSS handling for asset hashing and bundling while supporting SCSS syntax through the Sass compiler.

Example 3: Creating Custom Protocol Handlers

You can also use onResolve to handle custom URL schemes before the load phase:

const protoPlugin: BunPlugin = {
  name: "myproto-resolver",
  setup(build) {
    // Intercept imports that start with "myproto:"
    build.onResolve({ filter: /^myproto:/ }, args => ({
      path: args.path.replace(/^myproto:/, "./src/proto/"),
      namespace: "file",
    }));

    // Load the rewritten files as normal TypeScript modules
    build.onLoad(
      { filter: /\.ts$/ },
      async () => {},
      ({ path }) => ({
        loader: "ts",
      }),
    );
  },
};

Key Implementation Files in the Bun Repository

Understanding the internal architecture helps when debugging custom loader configurations. The following files in the oven-sh/bun repository define how loaders are registered and executed:

File Purpose
docs/bundler/plugins.mdx Documents the universal plugin API, the shape of onLoad, and the list of valid built-in loader names.
docs/bundler/loaders.mdx Lists every built-in loader (js, tsx, json, text, css, etc.) and demonstrates how to select one with import attributes.
src/js/builtins/BundlerPlugin.ts Core implementation that stores onLoad callbacks, executes them, validates the returned {loader, contents} object, and maps string names to internal numeric IDs via LOADERS_MAP.
src/js/builtins/BundlerPlugin.ts (lines 500–531) Contains the runtime path that processes custom onLoad results and forwards them to the internal loader lookup ($LoaderLabelToId).

These files demonstrate that Bun's plugin system acts as a bridge between user-defined file extensions and the internal loader pipeline defined in the C++/Zig runtime.

Summary

  • Bun selects built-in loaders automatically based on file extensions, but custom file types require a Bun plugin to configure the loader mapping.
  • Use the onLoad hook in your plugin to intercept file imports and return a loader property corresponding to a built-in loader name (text, css, ts, etc.).
  • The bundler validates loader names against an internal LOADERS_MAP ($LoaderLabelToId) in src/js/builtins/BundlerPlugin.ts, so you must reuse existing loader names for the return value.
  • For files requiring transformation (like SCSS), pre-process the content in onLoad and pass the result to a built-in loader (e.g., loader: "css") along with the contents property.
  • The onResolve hook can rewrite import paths or handle custom URL schemes before the load phase begins.

Frequently Asked Questions

Can I create entirely new loader types not built into Bun?

No, you cannot create entirely new loader types with unique parsing logic using only the plugin API. According to the source code in src/js/builtins/BundlerPlugin.ts, the bundler maintains a fixed internal LOADERS_MAP that maps string names to numeric loader IDs. Your plugin must return a loader property that exists in this map (such as "text", "css", "ts", or "json"). If you need truly new parsing behavior, you would need to extend the core C++/Zig loader table and rebuild Bun itself.

How do I handle binary files or assets in Bun plugins?

For binary files or static assets, use the built-in "file" loader or "dataurl" loader in your onLoad return object. The "file" loader emits the asset to the output directory and returns a public URL string, while "dataurl" inlines the file as a base64 data URL. Since these are built-in loader names recognized by the LOADERS_MAP in BundlerPlugin.ts, you can return loader: "file" or loader: "dataurl" from your plugin to handle images, fonts, or other binary assets seamlessly.

What is the difference between onResolve and onLoad in Bun plugins?

The onResolve hook runs during the import resolution phase and determines which file path or virtual module should be loaded when encountering an import statement. It can rewrite paths, handle custom URL schemes (like myproto:, or redirect imports to different locations. The onLoad hook runs during the load phase after resolution is complete, reading the file content and determining which loader (transpiler) should process it. While onResolve answers "where is the module?", onLoad answers "how do we parse it?".

Can I use existing npm packages like Sass in Bun plugins?

Yes, you can import and use existing npm packages directly within Bun plugins because Bun supports native Node.js module resolution and can run many npm packages without modification. For example, you can import sass from "sass" inside your plugin's onLoad callback to compile SCSS files before passing the result to the built-in CSS loader. Bun's runtime handles the execution of these packages within the plugin context, allowing you to leverage the full npm ecosystem for file transformation while still integrating with Bun's native bundling pipeline.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →