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

> Learn to configure loaders for custom file types in Bun. Create a plugin with the onLoad hook to map your file types to built-in loaders like text, css, or ts for seamless integration.

- Repository: [Bun/bun](https://github.com/oven-sh/bun)
- Tags: how-to-guide
- Published: 2026-02-28

---

**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`](https://github.com/oven-sh/bun/blob/main/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`](https://github.com/oven-sh/bun/blob/main/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`](https://github.com/oven-sh/bun/blob/main/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:

```typescript
// 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:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/oven-sh/bun/blob/main/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`](https://github.com/oven-sh/bun/blob/main/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`](https://github.com/oven-sh/bun/blob/main/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`](https://github.com/oven-sh/bun/blob/main/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`](https://github.com/oven-sh/bun/blob/main/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.