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
onLoadhook and return a customloadername.
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:
- Import Resolution – The optional
onResolvehook can rewrite import paths or handle custom protocols. - Load Phase – When the bundler reaches a file, it executes
onLoadcallbacks stored inBundlerPlugin.onLoad. - Loader Assignment – The plugin callback returns an object containing the
loaderproperty (a string name) and optionallycontentsorexports. - ID Mapping – The bundler looks up the numeric loader ID via
LOADERS_MAP = $LoaderLabelToIdinBundlerPlugin.ts. - Processing – The chosen loader processes the file (e.g., the
cssloader parses CSS, thetsxloader 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
onLoadhook in your plugin to intercept file imports and return aloaderproperty corresponding to a built-in loader name (text,css,ts, etc.). - The bundler validates loader names against an internal
LOADERS_MAP($LoaderLabelToId) insrc/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
onLoadand pass the result to a built-in loader (e.g.,loader: "css") along with thecontentsproperty. - The
onResolvehook 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →