diff --git a/build.zig b/build.zig
index fed6086672..cfc512ad8d 100644
--- a/build.zig
+++ b/build.zig
@@ -414,6 +414,15 @@ pub fn addBunObject(b: *Build, opts: *BunBuildOptions) *Compile {
}
addInternalPackages(b, obj, opts);
obj.root_module.addImport("build_options", opts.buildOptionsModule(b));
+
+ const translate_plugin_api = b.addTranslateC(.{
+ .root_source_file = b.path("./packages/bun-native-bundler-plugin-api/bundler_plugin.h"),
+ .target = opts.target,
+ .optimize = opts.optimize,
+ .link_libc = true,
+ });
+ obj.root_module.addImport("bun-native-bundler-plugin-api", translate_plugin_api.createModule());
+
return obj;
}
diff --git a/docs/runtime/plugins.md b/docs/runtime/plugins.md
index b6be028120..129d129936 100644
--- a/docs/runtime/plugins.md
+++ b/docs/runtime/plugins.md
@@ -355,7 +355,7 @@ Bun.build({
{% /callout %}
-## Lifecycle callbacks
+## Lifecycle hooks
Plugins can register callbacks to be run at various points in the lifecycle of a bundle:
@@ -363,6 +363,8 @@ Plugins can register callbacks to be run at various points in the lifecycle of a
- [`onResolve()`](#onresolve): Run before a module is resolved
- [`onLoad()`](#onload): Run before a module is loaded.
+### Reference
+
A rough overview of the types (please refer to Bun's `bun.d.ts` for the full type definitions):
```ts
@@ -603,3 +605,98 @@ plugin({
```
Note that the `.defer()` function currently has the limitation that it can only be called once per `onLoad` callback.
+
+## Native plugins
+
+{% callout %}
+**NOTE** — This is an advanced and experiemental API recommended for plugin developers who are familiar with systems programming and the C ABI. Use with caution.
+{% /callout %}
+
+One of the reasons why Bun's bundler is so fast is that it is written in native code and leverages multi-threading to load and parse modules in parallel.
+
+However, one limitation of plugins written in JavaScript is that JavaScript itself is single-threaded.
+
+Native plugins are written as [NAPI](/docs/node-api) modules and can be run on multiple threads. This allows native plugins to run much faster than JavaScript plugins.
+
+In addition, native plugins can skip unnecessary work such as the UTF-8 -> UTF-16 conversion needed to pass strings to JavaScript.
+
+These are the following lifecycle hooks which are available to native plugins:
+
+- [`onBeforeParse()`](#onbeforeparse): Called on any thread before a file is parsed by Bun's bundler.
+
+### Creating a native plugin
+
+Native plugins are NAPI modules which expose lifecycle hooks as C ABI functions.
+
+To create a native plugin, you must export a C ABI function which matches the signature of the native lifecycle hook you want to implement.
+
+#### Example: Rust with napi-rs
+
+First initialize a napi project (see [here](https://napi.rs/docs/introduction/getting-started) for a more comprehensive guide).
+
+Then install Bun's official safe plugin wrapper crate:
+
+```bash
+cargo add bun-native-plugin
+```
+
+Now you can export an `extern "C" fn` which is the implementation of your plugin:
+
+```rust
+#[no_mangle]
+extern "C" fn on_before_parse_impl(
+ args: *const bun_native_plugin::sys::OnBeforeParseArguments,
+ result: *mut bun_native_plugin::sys::OnBeforeParseResult,
+) {
+ let args = unsafe { &*args };
+ let result = unsafe { &mut *result };
+
+ let mut handle = match bun_native_plugin::OnBeforeParse::from_raw(args, result) {
+ Ok(handle) => handle,
+ Err(_) => {
+ return;
+ }
+ };
+
+ let source_code = match handle.input_source_code() {
+ Ok(source_code) => source_code,
+ Err(_) => {
+ handle.log_error("Fetching source code failed!");
+ return;
+ }
+ };
+
+ let loader = handle.output_loader();
+ handle.set_output_source_code(source_code.replace("foo", "bar"), loader);
+```
+
+Use napi-rs to compile the plugin to a `.node` file, then you can `require()` it from JS and use it:
+
+```js
+await Bun.build({
+ entrypoints: ["index.ts"],
+ setup(build) {
+ const myNativePlugin = require("./path/to/plugin.node");
+
+ build.onBeforeParse(
+ { filter: /\.ts/ },
+ { napiModule: myNativePlugin, symbol: "on_before_parse_impl" },
+ );
+ },
+});
+```
+
+### `onBeforeParse`
+
+```ts
+onBeforeParse(
+ args: { filter: RegExp; namespace?: string },
+ callback: { napiModule: NapiModule; symbol: string; external?: unknown },
+): void;
+```
+
+This lifecycle callback is run immediately before a file is parsed by Bun's bundler.
+
+As input, it receives the file's contents and can optionally return new source code.
+
+This callback can be called from any thread and so the napi module implementation must be thread-safe.
diff --git a/packages/bun-build-mdx-rs/.cargo/config.toml b/packages/bun-build-mdx-rs/.cargo/config.toml
new file mode 100644
index 0000000000..06516547bf
--- /dev/null
+++ b/packages/bun-build-mdx-rs/.cargo/config.toml
@@ -0,0 +1,5 @@
+[target.aarch64-unknown-linux-musl]
+linker = "aarch64-linux-musl-gcc"
+rustflags = ["-C", "target-feature=-crt-static"]
+[target.x86_64-pc-windows-msvc]
+rustflags = ["-C", "target-feature=+crt-static"]
\ No newline at end of file
diff --git a/packages/bun-build-mdx-rs/.gitignore b/packages/bun-build-mdx-rs/.gitignore
new file mode 100644
index 0000000000..3bcc3fe03c
--- /dev/null
+++ b/packages/bun-build-mdx-rs/.gitignore
@@ -0,0 +1,202 @@
+# Created by https://www.toptal.com/developers/gitignore/api/node
+# Edit at https://www.toptal.com/developers/gitignore?templates=node
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# End of https://www.toptal.com/developers/gitignore/api/node
+
+# Created by https://www.toptal.com/developers/gitignore/api/macos
+# Edit at https://www.toptal.com/developers/gitignore?templates=macos
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+# End of https://www.toptal.com/developers/gitignore/api/macos
+
+# Created by https://www.toptal.com/developers/gitignore/api/windows
+# Edit at https://www.toptal.com/developers/gitignore?templates=windows
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/windows
+
+#Added by cargo
+
+/target
+Cargo.lock
+
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+
+*.node
+
+dist/
+
+index.js
+index.d.ts
\ No newline at end of file
diff --git a/packages/bun-build-mdx-rs/.npmignore b/packages/bun-build-mdx-rs/.npmignore
new file mode 100644
index 0000000000..ec144db2a7
--- /dev/null
+++ b/packages/bun-build-mdx-rs/.npmignore
@@ -0,0 +1,13 @@
+target
+Cargo.lock
+.cargo
+.github
+npm
+.eslintrc
+.prettierignore
+rustfmt.toml
+yarn.lock
+*.node
+.yarn
+__test__
+renovate.json
diff --git a/packages/bun-build-mdx-rs/Cargo.toml b/packages/bun-build-mdx-rs/Cargo.toml
new file mode 100644
index 0000000000..90c4753237
--- /dev/null
+++ b/packages/bun-build-mdx-rs/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+edition = "2021"
+name = "bun-mdx-rs"
+version = "0.0.0"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
+napi = { version = "2.12.2", default-features = false, features = ["napi4"] }
+napi-derive = "2.12.2"
+mdxjs = "0.2.11"
+bun-native-plugin = { path = "../bun-native-plugin-rs" }
+
+[build-dependencies]
+napi-build = "2.0.1"
+
+[profile.release]
+lto = true
+strip = "symbols"
diff --git a/packages/bun-build-mdx-rs/README.md b/packages/bun-build-mdx-rs/README.md
new file mode 100644
index 0000000000..0c2f01a2ce
--- /dev/null
+++ b/packages/bun-build-mdx-rs/README.md
@@ -0,0 +1,34 @@
+# bun-build-mdx-rs
+
+This is a proof of concept for using a third-party native addon in `Bun.build()`.
+
+This uses `mdxjs-rs` to convert MDX to JSX.
+
+TODO: **This needs to be built & published to npm.**
+
+## Building locally:
+
+```sh
+cargo build --release
+```
+
+```js
+import { build } from "bun";
+import mdx from "./index.js";
+
+// TODO: This needs to be prebuilt for the current platform
+// Probably use a napi-rs template for this
+import addon from "./target/release/libmdx_bun.dylib" with { type: "file" };
+
+const results = await build({
+ entrypoints: ["./hello.jsx"],
+ plugins: [mdx({ addon })],
+ minify: true,
+ outdir: "./dist",
+ define: {
+ "process.env.NODE_ENV": JSON.stringify("production"),
+ },
+});
+
+console.log(results);
+```
diff --git a/packages/bun-build-mdx-rs/__test__/index.spec.mjs b/packages/bun-build-mdx-rs/__test__/index.spec.mjs
new file mode 100644
index 0000000000..1ade4cafe8
--- /dev/null
+++ b/packages/bun-build-mdx-rs/__test__/index.spec.mjs
@@ -0,0 +1,7 @@
+import test from 'ava'
+
+import { sum } from '../index.js'
+
+test('sum from native', (t) => {
+ t.is(sum(1, 2), 3)
+})
diff --git a/packages/bun-build-mdx-rs/build.rs b/packages/bun-build-mdx-rs/build.rs
new file mode 100644
index 0000000000..1f866b6a3c
--- /dev/null
+++ b/packages/bun-build-mdx-rs/build.rs
@@ -0,0 +1,5 @@
+extern crate napi_build;
+
+fn main() {
+ napi_build::setup();
+}
diff --git a/packages/bun-build-mdx-rs/input/index.ts b/packages/bun-build-mdx-rs/input/index.ts
new file mode 100644
index 0000000000..9531975088
--- /dev/null
+++ b/packages/bun-build-mdx-rs/input/index.ts
@@ -0,0 +1,6 @@
+import page1 from "./page1.mdx";
+import page2 from "./page2.mdx";
+import page3 from "./page3.mdx";
+import page4 from "./page4.mdx";
+
+console.log(page1, page2, page3, page4);
diff --git a/packages/bun-build-mdx-rs/input/page1.mdx b/packages/bun-build-mdx-rs/input/page1.mdx
new file mode 100644
index 0000000000..2199a0d985
--- /dev/null
+++ b/packages/bun-build-mdx-rs/input/page1.mdx
@@ -0,0 +1,11 @@
+# Hello World
+
+This is a sample MDX file that demonstrates various MDX features.
+
+## Components
+
+You can use JSX components directly in MDX:
+
+
+
+## Code Blocks
diff --git a/packages/bun-build-mdx-rs/input/page2.mdx b/packages/bun-build-mdx-rs/input/page2.mdx
new file mode 100644
index 0000000000..2199a0d985
--- /dev/null
+++ b/packages/bun-build-mdx-rs/input/page2.mdx
@@ -0,0 +1,11 @@
+# Hello World
+
+This is a sample MDX file that demonstrates various MDX features.
+
+## Components
+
+You can use JSX components directly in MDX:
+
+
+
+## Code Blocks
diff --git a/packages/bun-build-mdx-rs/input/page3.mdx b/packages/bun-build-mdx-rs/input/page3.mdx
new file mode 100644
index 0000000000..2199a0d985
--- /dev/null
+++ b/packages/bun-build-mdx-rs/input/page3.mdx
@@ -0,0 +1,11 @@
+# Hello World
+
+This is a sample MDX file that demonstrates various MDX features.
+
+## Components
+
+You can use JSX components directly in MDX:
+
+
+
+## Code Blocks
diff --git a/packages/bun-build-mdx-rs/input/page4.mdx b/packages/bun-build-mdx-rs/input/page4.mdx
new file mode 100644
index 0000000000..2199a0d985
--- /dev/null
+++ b/packages/bun-build-mdx-rs/input/page4.mdx
@@ -0,0 +1,11 @@
+# Hello World
+
+This is a sample MDX file that demonstrates various MDX features.
+
+## Components
+
+You can use JSX components directly in MDX:
+
+
+
+## Code Blocks
diff --git a/packages/bun-build-mdx-rs/npm/darwin-arm64/README.md b/packages/bun-build-mdx-rs/npm/darwin-arm64/README.md
new file mode 100644
index 0000000000..ad90799c6f
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/darwin-arm64/README.md
@@ -0,0 +1,3 @@
+# `bun-mdx-rs-darwin-arm64`
+
+This is the **aarch64-apple-darwin** binary for `bun-mdx-rs`
diff --git a/packages/bun-build-mdx-rs/npm/darwin-arm64/package.json b/packages/bun-build-mdx-rs/npm/darwin-arm64/package.json
new file mode 100644
index 0000000000..a49f40f89e
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/darwin-arm64/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "bun-mdx-rs-darwin-arm64",
+ "version": "0.0.0",
+ "os": [
+ "darwin"
+ ],
+ "cpu": [
+ "arm64"
+ ],
+ "main": "bun-mdx-rs.darwin-arm64.node",
+ "files": [
+ "bun-mdx-rs.darwin-arm64.node"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+}
\ No newline at end of file
diff --git a/packages/bun-build-mdx-rs/npm/darwin-x64/README.md b/packages/bun-build-mdx-rs/npm/darwin-x64/README.md
new file mode 100644
index 0000000000..53098f4931
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/darwin-x64/README.md
@@ -0,0 +1,3 @@
+# `bun-mdx-rs-darwin-x64`
+
+This is the **x86_64-apple-darwin** binary for `bun-mdx-rs`
diff --git a/packages/bun-build-mdx-rs/npm/darwin-x64/package.json b/packages/bun-build-mdx-rs/npm/darwin-x64/package.json
new file mode 100644
index 0000000000..41bd00cd3f
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/darwin-x64/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "bun-mdx-rs-darwin-x64",
+ "version": "0.0.0",
+ "os": [
+ "darwin"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "main": "bun-mdx-rs.darwin-x64.node",
+ "files": [
+ "bun-mdx-rs.darwin-x64.node"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+}
\ No newline at end of file
diff --git a/packages/bun-build-mdx-rs/npm/linux-arm64-gnu/README.md b/packages/bun-build-mdx-rs/npm/linux-arm64-gnu/README.md
new file mode 100644
index 0000000000..f2613108fe
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/linux-arm64-gnu/README.md
@@ -0,0 +1,3 @@
+# `bun-mdx-rs-linux-arm64-gnu`
+
+This is the **aarch64-unknown-linux-gnu** binary for `bun-mdx-rs`
diff --git a/packages/bun-build-mdx-rs/npm/linux-arm64-gnu/package.json b/packages/bun-build-mdx-rs/npm/linux-arm64-gnu/package.json
new file mode 100644
index 0000000000..6d8fc3cd88
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/linux-arm64-gnu/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "bun-mdx-rs-linux-arm64-gnu",
+ "version": "0.0.0",
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "arm64"
+ ],
+ "main": "bun-mdx-rs.linux-arm64-gnu.node",
+ "files": [
+ "bun-mdx-rs.linux-arm64-gnu.node"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "libc": [
+ "glibc"
+ ]
+}
\ No newline at end of file
diff --git a/packages/bun-build-mdx-rs/npm/linux-arm64-musl/README.md b/packages/bun-build-mdx-rs/npm/linux-arm64-musl/README.md
new file mode 100644
index 0000000000..6a07db8135
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/linux-arm64-musl/README.md
@@ -0,0 +1,3 @@
+# `bun-mdx-rs-linux-arm64-musl`
+
+This is the **aarch64-unknown-linux-musl** binary for `bun-mdx-rs`
diff --git a/packages/bun-build-mdx-rs/npm/linux-arm64-musl/package.json b/packages/bun-build-mdx-rs/npm/linux-arm64-musl/package.json
new file mode 100644
index 0000000000..02344ef80a
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/linux-arm64-musl/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "bun-mdx-rs-linux-arm64-musl",
+ "version": "0.0.0",
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "arm64"
+ ],
+ "main": "bun-mdx-rs.linux-arm64-musl.node",
+ "files": [
+ "bun-mdx-rs.linux-arm64-musl.node"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "libc": [
+ "musl"
+ ]
+}
\ No newline at end of file
diff --git a/packages/bun-build-mdx-rs/npm/linux-x64-gnu/README.md b/packages/bun-build-mdx-rs/npm/linux-x64-gnu/README.md
new file mode 100644
index 0000000000..339193a734
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/linux-x64-gnu/README.md
@@ -0,0 +1,3 @@
+# `bun-mdx-rs-linux-x64-gnu`
+
+This is the **x86_64-unknown-linux-gnu** binary for `bun-mdx-rs`
diff --git a/packages/bun-build-mdx-rs/npm/linux-x64-gnu/package.json b/packages/bun-build-mdx-rs/npm/linux-x64-gnu/package.json
new file mode 100644
index 0000000000..b45a64b866
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/linux-x64-gnu/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "bun-mdx-rs-linux-x64-gnu",
+ "version": "0.0.0",
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "main": "bun-mdx-rs.linux-x64-gnu.node",
+ "files": [
+ "bun-mdx-rs.linux-x64-gnu.node"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "libc": [
+ "glibc"
+ ]
+}
\ No newline at end of file
diff --git a/packages/bun-build-mdx-rs/npm/linux-x64-musl/README.md b/packages/bun-build-mdx-rs/npm/linux-x64-musl/README.md
new file mode 100644
index 0000000000..f37e0555ec
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/linux-x64-musl/README.md
@@ -0,0 +1,3 @@
+# `bun-mdx-rs-linux-x64-musl`
+
+This is the **x86_64-unknown-linux-musl** binary for `bun-mdx-rs`
diff --git a/packages/bun-build-mdx-rs/npm/linux-x64-musl/package.json b/packages/bun-build-mdx-rs/npm/linux-x64-musl/package.json
new file mode 100644
index 0000000000..8a3f90cd98
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/linux-x64-musl/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "bun-mdx-rs-linux-x64-musl",
+ "version": "0.0.0",
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "main": "bun-mdx-rs.linux-x64-musl.node",
+ "files": [
+ "bun-mdx-rs.linux-x64-musl.node"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "libc": [
+ "musl"
+ ]
+}
\ No newline at end of file
diff --git a/packages/bun-build-mdx-rs/npm/win32-x64-msvc/README.md b/packages/bun-build-mdx-rs/npm/win32-x64-msvc/README.md
new file mode 100644
index 0000000000..5e72822a4b
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/win32-x64-msvc/README.md
@@ -0,0 +1,3 @@
+# `bun-mdx-rs-win32-x64-msvc`
+
+This is the **x86_64-pc-windows-msvc** binary for `bun-mdx-rs`
diff --git a/packages/bun-build-mdx-rs/npm/win32-x64-msvc/package.json b/packages/bun-build-mdx-rs/npm/win32-x64-msvc/package.json
new file mode 100644
index 0000000000..738081cb77
--- /dev/null
+++ b/packages/bun-build-mdx-rs/npm/win32-x64-msvc/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "bun-mdx-rs-win32-x64-msvc",
+ "version": "0.0.0",
+ "os": [
+ "win32"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "main": "bun-mdx-rs.win32-x64-msvc.node",
+ "files": [
+ "bun-mdx-rs.win32-x64-msvc.node"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+}
\ No newline at end of file
diff --git a/packages/bun-build-mdx-rs/package.json b/packages/bun-build-mdx-rs/package.json
new file mode 100644
index 0000000000..280221f8c6
--- /dev/null
+++ b/packages/bun-build-mdx-rs/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "bun-mdx-rs",
+ "version": "0.0.0",
+ "main": "index.js",
+ "types": "index.d.ts",
+ "napi": {
+ "name": "bun-mdx-rs",
+ "triples": {
+ "additional": [
+ "aarch64-apple-darwin",
+ "aarch64-unknown-linux-gnu",
+ "aarch64-unknown-linux-musl",
+ "x86_64-unknown-linux-musl"
+ ]
+ }
+ },
+ "license": "MIT",
+ "devDependencies": {
+ "@napi-rs/cli": "^2.18.4",
+ "ava": "^6.0.1"
+ },
+ "ava": {
+ "timeout": "3m"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "scripts": {
+ "artifacts": "napi artifacts",
+ "build": "napi build --platform --release",
+ "build:debug": "napi build --platform",
+ "prepublishOnly": "napi prepublish -t npm",
+ "test": "ava",
+ "universal": "napi universal",
+ "version": "napi version"
+ }
+}
\ No newline at end of file
diff --git a/packages/bun-build-mdx-rs/rustfmt.toml b/packages/bun-build-mdx-rs/rustfmt.toml
new file mode 100644
index 0000000000..cab5731eda
--- /dev/null
+++ b/packages/bun-build-mdx-rs/rustfmt.toml
@@ -0,0 +1,2 @@
+tab_spaces = 2
+edition = "2021"
diff --git a/packages/bun-build-mdx-rs/src/lib.rs b/packages/bun-build-mdx-rs/src/lib.rs
new file mode 100644
index 0000000000..4b93e6037f
--- /dev/null
+++ b/packages/bun-build-mdx-rs/src/lib.rs
@@ -0,0 +1,55 @@
+use bun_native_plugin::{define_bun_plugin, BunLoader, OnBeforeParse};
+use mdxjs::{compile, Options as CompileOptions};
+use napi_derive::napi;
+
+#[macro_use]
+extern crate napi;
+
+define_bun_plugin!("bun-mdx-rs");
+
+#[no_mangle]
+pub extern "C" fn bun_mdx_rs(
+ args: *const bun_native_plugin::sys::OnBeforeParseArguments,
+ result: *mut bun_native_plugin::sys::OnBeforeParseResult,
+) {
+ let args = unsafe { &*args };
+
+ let mut handle = match OnBeforeParse::from_raw(args, result) {
+ Ok(handle) => handle,
+ Err(_) => {
+ return;
+ }
+ };
+
+ let source_str = match handle.input_source_code() {
+ Ok(source_str) => source_str,
+ Err(_) => {
+ handle.log_error("Failed to fetch source code");
+ return;
+ }
+ };
+
+ let mut options = CompileOptions::gfm();
+
+ // Leave it as JSX for Bun to handle
+ options.jsx = true;
+
+ let path = match handle.path() {
+ Ok(path) => path,
+ Err(e) => {
+ handle.log_error(&format!("Failed to get path: {:?}", e));
+ return;
+ }
+ };
+ options.filepath = Some(path.to_string());
+
+ match compile(&source_str, &options) {
+ Ok(compiled) => {
+ handle.set_output_source_code(compiled, BunLoader::BUN_LOADER_JSX);
+ }
+ Err(_) => {
+ handle.log_error("Failed to compile MDX");
+ return;
+ }
+ }
+}
diff --git a/packages/bun-native-bundler-plugin-api/bundler_plugin.h b/packages/bun-native-bundler-plugin-api/bundler_plugin.h
new file mode 100644
index 0000000000..ff10c27ccd
--- /dev/null
+++ b/packages/bun-native-bundler-plugin-api/bundler_plugin.h
@@ -0,0 +1,73 @@
+#ifndef BUN_NATIVE_BUNDLER_PLUGIN_API_H
+#define BUN_NATIVE_BUNDLER_PLUGIN_API_H
+
+#include
+#include
+
+typedef enum {
+ BUN_LOADER_JSX = 0,
+ BUN_LOADER_JS = 1,
+ BUN_LOADER_TS = 2,
+ BUN_LOADER_TSX = 3,
+ BUN_LOADER_CSS = 4,
+ BUN_LOADER_FILE = 5,
+ BUN_LOADER_JSON = 6,
+ BUN_LOADER_TOML = 7,
+ BUN_LOADER_WASM = 8,
+ BUN_LOADER_NAPI = 9,
+ BUN_LOADER_BASE64 = 10,
+ BUN_LOADER_DATAURL = 11,
+ BUN_LOADER_TEXT = 12,
+} BunLoader;
+
+const BunLoader BUN_LOADER_MAX = BUN_LOADER_TEXT;
+
+typedef struct BunLogOptions {
+ size_t __struct_size;
+ const uint8_t *message_ptr;
+ size_t message_len;
+ const uint8_t *path_ptr;
+ size_t path_len;
+ const uint8_t *source_line_text_ptr;
+ size_t source_line_text_len;
+ int8_t level;
+ int line;
+ int lineEnd;
+ int column;
+ int columnEnd;
+} BunLogOptions;
+
+typedef struct {
+ size_t __struct_size;
+ void *bun;
+ const uint8_t *path_ptr;
+ size_t path_len;
+ const uint8_t *namespace_ptr;
+ size_t namespace_len;
+ uint8_t default_loader;
+ void *external;
+} OnBeforeParseArguments;
+
+typedef struct OnBeforeParseResult {
+ size_t __struct_size;
+ uint8_t *source_ptr;
+ size_t source_len;
+ uint8_t loader;
+ int (*fetchSourceCode)(const OnBeforeParseArguments *args,
+ struct OnBeforeParseResult *result);
+ void *plugin_source_code_context;
+ void (*free_plugin_source_code_context)(void *ctx);
+ void (*log)(const OnBeforeParseArguments *args, BunLogOptions *options);
+} OnBeforeParseResult;
+
+typedef enum {
+ BUN_LOG_LEVEL_VERBOSE = 0,
+ BUN_LOG_LEVEL_DEBUG = 1,
+ BUN_LOG_LEVEL_INFO = 2,
+ BUN_LOG_LEVEL_WARN = 3,
+ BUN_LOG_LEVEL_ERROR = 4,
+} BunLogLevel;
+
+const BunLogLevel BUN_LOG_MAX = BUN_LOG_LEVEL_ERROR;
+
+#endif // BUN_NATIVE_BUNDLER_PLUGIN_API_H
diff --git a/packages/bun-native-plugin-rs/.gitignore b/packages/bun-native-plugin-rs/.gitignore
new file mode 100644
index 0000000000..2f7896d1d1
--- /dev/null
+++ b/packages/bun-native-plugin-rs/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/packages/bun-native-plugin-rs/Cargo.lock b/packages/bun-native-plugin-rs/Cargo.lock
new file mode 100644
index 0000000000..202700fa3a
--- /dev/null
+++ b/packages/bun-native-plugin-rs/Cargo.lock
@@ -0,0 +1,286 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+
+[[package]]
+name = "bun-native-plugin"
+version = "0.1.0"
+dependencies = [
+ "bindgen",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.166"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36"
+
+[[package]]
+name = "libloading"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
+dependencies = [
+ "cfg-if",
+ "windows-targets",
+]
+
+[[package]]
+name = "log"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "syn"
+version = "2.0.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/packages/bun-native-plugin-rs/Cargo.toml b/packages/bun-native-plugin-rs/Cargo.toml
new file mode 100644
index 0000000000..bf4d7b784b
--- /dev/null
+++ b/packages/bun-native-plugin-rs/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "bun-native-plugin"
+version = "0.1.0"
+edition = "2021"
+
+[build-dependencies]
+bindgen = "0.70.1"
diff --git a/packages/bun-native-plugin-rs/README.md b/packages/bun-native-plugin-rs/README.md
new file mode 100644
index 0000000000..f235849872
--- /dev/null
+++ b/packages/bun-native-plugin-rs/README.md
@@ -0,0 +1,248 @@
+> ⚠️ Note: This is an advanced and experimental API recommended only for plugin developers who are familiar with systems proramming and the C ABI. Use with caution.
+
+# Bun Native Plugins
+
+This crate provides a Rustified wrapper over the Bun's native bundler plugin C API.
+
+Some advantages to _native_ bundler plugins as opposed to regular ones implemented in JS:
+
+- Native plugins take full advantage of Bun's parallelized bundler pipeline and run on multiple threads at the same time
+- Unlike JS, native plugins don't need to do the UTF-8 <-> UTF-16 source code string conversions
+
+What are native bundler plugins exactly? Precisely, they are NAPI modules which expose a C ABI function which implement a plugin lifecycle hook.
+
+The currently supported lifecycle hooks are:
+
+- `onBeforeParse` (called immediately before a file is parsed, allows you to modify the source code of the file)
+
+## Getting started
+
+Since native bundler plugins are NAPI modules, the easiest way to get started is to create a new [napi-rs](https://github.com/napi-rs/napi-rs) project:
+
+```bash
+bun add -g @napi-rs/cli
+napi new
+```
+
+Then install this crate:
+
+```bash
+cargo add bun-native-plugin
+```
+
+Now, inside the `lib.rs` file, expose a C ABI function which has the same function signature as the plugin lifecycle hook that you want to implement.
+
+For example, implementing `onBeforeParse`:
+
+```rs
+use bun_native_plugin::{define_bun_plugin, OnBeforeParse};
+use napi_derive::napi;
+
+/// Define with the name of the plugin
+define_bun_plugin!("replace-foo-with-bar");
+
+/// This is necessary for napi-rs to compile this into a proper NAPI module
+#[napi]
+pub fn register_bun_plugin() {}
+
+/// Use `no_mangle` so that we can reference this symbol by name later
+/// when registering this native plugin in JS.
+///
+/// Here we'll create a dummy plugin which replaces all occurences of
+/// `foo` with `bar`
+#[no_mangle]
+pub extern "C" fn on_before_parse_plugin_impl(
+ args: *const bun_native_plugin::sys::OnBeforeParseArguments,
+ result: *mut bun_native_plugin::sys::OnBeforeParseResult,
+) {
+ let args = unsafe { &*args };
+
+ // This returns a handle which is a safe wrapper over the raw
+ // C API.
+ let mut handle = OnBeforeParse::from_raw(args, result) {
+ Ok(handle) => handle,
+ Err(_) => {
+ // `OnBeforeParse::from_raw` handles error logging
+ // so it fine to return here.
+ return;
+ }
+ };
+
+ let input_source_code = match handle.input_source_code() {
+ Ok(source_str) => source_str,
+ Err(_) => {
+ // If we encounter an error, we must log it so that
+ // Bun knows this plugin failed.
+ handle.log_error("Failed to fetch source code!");
+ return;
+ }
+ };
+
+ let loader = handle.output_loader();
+ let output_source_code = source_str.replace("foo", "bar");
+ handle.set_output_source_code(output_source_code, loader);
+}
+```
+
+Then compile this NAPI module. If you using napi-rs, the `package.json` should have a `build` script you can run:
+
+```bash
+bun run build
+```
+
+This will produce a `.node` file in the project directory.
+
+With the compiled NAPI module, you can now register the plugin from JS:
+
+```js
+const result = await Bun.build({
+ entrypoints: ["index.ts"],
+ plugins: [
+ {
+ name: "replace-foo-with-bar",
+ setup(build) {
+ const napiModule = require("path/to/napi_module.node");
+
+ // Register the `onBeforeParse` hook to run on all `.ts` files.
+ // We tell it to use function we implemented inside of our `lib.rs` code.
+ build.onBeforeParse(
+ { filter: /\.ts/ },
+ { napiModule, symbol: "on_before_parse_plugin_impl" },
+ );
+ },
+ },
+ ],
+});
+```
+
+## Very important information
+
+### Error handling and panics
+
+It is highly recommended to avoid panicking as this will crash the runtime. Instead, you must handle errors and log them:
+
+```rs
+let input_source_code = match handle.input_source_code() {
+ Ok(source_str) => source_str,
+ Err(_) => {
+ // If we encounter an error, we must log it so that
+ // Bun knows this plugin failed.
+ handle.log_error("Failed to fetch source code!");
+ return;
+ }
+};
+```
+
+### Passing state to and from JS: `External`
+
+One way to communicate data from your plugin and JS and vice versa is through the NAPI's [External](https://napi.rs/docs/concepts/external) type.
+
+An External in NAPI is like an opaque pointer to data that can be passed to and from JS. Inside your NAPI module, you can retrieve
+the pointer and modify the data.
+
+As an example that extends our getting started example above, let's say you wanted to count the number of `foo`'s that the native plugin encounters.
+
+You would expose a NAPI module function which creates this state. Recall that state in native plugins must be threadsafe. This usually means
+that your state must be `Sync`:
+
+```rs
+struct PluginState {
+ foo_count: std::sync::atomic::AtomicU32,
+}
+
+#[napi]
+pub fn create_plugin_state() -> External {
+ let external = External::new(PluginState {
+ foo_count: 0,
+ });
+
+ external
+}
+
+
+#[napi]
+pub fn get_foo_count(plugin_state: External) -> u32 {
+ let plugin_state: &PluginState = &plugin_state;
+ plugin_state.foo_count.load(std::sync::atomic::Ordering::Relaxed)
+}
+```
+
+When you register your plugin from Javascript, you call the napi module function to create the external and then pass it:
+
+```js
+const napiModule = require("path/to/napi_module.node");
+const pluginState = napiModule.createPluginState();
+
+const result = await Bun.build({
+ entrypoints: ["index.ts"],
+ plugins: [
+ {
+ name: "replace-foo-with-bar",
+ setup(build) {
+ build.onBeforeParse(
+ { filter: /\.ts/ },
+ {
+ napiModule,
+ symbol: "on_before_parse_plugin_impl",
+ // pass our NAPI external which contains our plugin state here
+ external: pluginState,
+ },
+ );
+ },
+ },
+ ],
+});
+
+console.log("Total `foo`s encountered: ", pluginState.getFooCount());
+```
+
+Finally, from the native implementation of your plugin, you can extract the external:
+
+```rs
+pub extern "C" fn on_before_parse_plugin_impl(
+ args: *const bun_native_plugin::sys::OnBeforeParseArguments,
+ result: *mut bun_native_plugin::sys::OnBeforeParseResult,
+) {
+ let args = unsafe { &*args };
+
+ let mut handle = OnBeforeParse::from_raw(args, result) {
+ Ok(handle) => handle,
+ Err(_) => {
+ // `OnBeforeParse::from_raw` handles error logging
+ // so it fine to return here.
+ return;
+ }
+ };
+
+ let plugin_state: &PluginState =
+ // This operation is only safe if you pass in an external when registering the plugin.
+ // If you don't, this could lead to a segfault or access of undefined memory.
+ match unsafe { handle.external().and_then(|state| state.ok_or(Error::Unknown)) } {
+ Ok(state) => state,
+ Err(_) => {
+ handle.log_error("Failed to get external!");
+ return;
+ }
+ };
+
+
+ // Fetch our source code again
+ let input_source_code = match handle.input_source_code() {
+ Ok(source_str) => source_str,
+ Err(_) => {
+ handle.log_error("Failed to fetch source code!");
+ return;
+ }
+ };
+
+ // Count the number of `foo`s and add it to our state
+ let foo_count = source_code.matches("foo").count() as u32;
+ plugin_state.foo_count.fetch_add(foo_count, std::sync::atomic::Ordering::Relaxed);
+}
+```
+
+### Concurrency
+
+Your `extern "C"` plugin function can be called _on any thread_ at _any time_ and _multiple times at once_.
+
+Therefore, you must design any state management to be threadsafe
diff --git a/packages/bun-native-plugin-rs/build.rs b/packages/bun-native-plugin-rs/build.rs
new file mode 100644
index 0000000000..fb33bbbae1
--- /dev/null
+++ b/packages/bun-native-plugin-rs/build.rs
@@ -0,0 +1,20 @@
+use std::path::PathBuf;
+
+fn main() {
+ println!("cargo:rustc-link-search=./headers");
+
+ let bindings = bindgen::Builder::default()
+ .header("wrapper.h")
+ // Add absolute path to headers directory
+ .clang_arg("-I./headers")
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+ .rustified_enum("BunLogLevel")
+ .rustified_enum("BunLoader")
+ .generate()
+ .expect("Unable to generate bindings");
+
+ let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap());
+ bindings
+ .write_to_file(out_path.join("bindings.rs"))
+ .expect("Couldn't write bindings!");
+}
diff --git a/packages/bun-native-plugin-rs/copy_headers.ts b/packages/bun-native-plugin-rs/copy_headers.ts
new file mode 100644
index 0000000000..23cc166b8c
--- /dev/null
+++ b/packages/bun-native-plugin-rs/copy_headers.ts
@@ -0,0 +1,6 @@
+import { join } from "node:path";
+
+const dirname = join(import.meta.dir, "../", "bun-native-bundler-plugin-api");
+await Bun.$`rm -rf headers`;
+await Bun.$`mkdir -p headers`;
+await Bun.$`cp -R ${dirname} headers/bun-native-bundler-plugin-api`;
diff --git a/packages/bun-native-plugin-rs/headers/bun-native-bundler-plugin-api/bundler_plugin.h b/packages/bun-native-plugin-rs/headers/bun-native-bundler-plugin-api/bundler_plugin.h
new file mode 100644
index 0000000000..bd84c95a5f
--- /dev/null
+++ b/packages/bun-native-plugin-rs/headers/bun-native-bundler-plugin-api/bundler_plugin.h
@@ -0,0 +1,79 @@
+#ifndef BUN_NATIVE_BUNDLER_PLUGIN_API_H
+#define BUN_NATIVE_BUNDLER_PLUGIN_API_H
+
+#include
+#include
+
+typedef enum {
+ BUN_LOADER_JSX = 0,
+ BUN_LOADER_JS = 1,
+ BUN_LOADER_TS = 2,
+ BUN_LOADER_TSX = 3,
+ BUN_LOADER_CSS = 4,
+ BUN_LOADER_FILE = 5,
+ BUN_LOADER_JSON = 6,
+ BUN_LOADER_TOML = 7,
+ BUN_LOADER_WASM = 8,
+ BUN_LOADER_NAPI = 9,
+ BUN_LOADER_BASE64 = 10,
+ BUN_LOADER_DATAURL = 11,
+ BUN_LOADER_TEXT = 12,
+ BUN_LOADER_BUNSH = 13,
+ BUN_LOADER_SQLITE = 14,
+ BUN_LOADER_SQLITE_EMBEDDED = 15
+} BunLoader;
+
+const BunLoader BUN_LOADER_MAX = BUN_LOADER_SQLITE_EMBEDDED;
+
+typedef struct BunLogOptions {
+ size_t __struct_size;
+ const uint8_t* message_ptr;
+ size_t message_len;
+ const uint8_t* path_ptr;
+ size_t path_len;
+ const uint8_t* source_line_text_ptr;
+ size_t source_line_text_len;
+ int8_t level;
+ int line;
+ int lineEnd;
+ int column;
+ int columnEnd;
+} BunLogOptions;
+
+typedef struct {
+ size_t __struct_size;
+ void* bun;
+ const uint8_t* path_ptr;
+ size_t path_len;
+ const uint8_t* namespace_ptr;
+ size_t namespace_len;
+ uint8_t default_loader;
+ void *external;
+} OnBeforeParseArguments;
+
+typedef struct OnBeforeParseResult {
+ size_t __struct_size;
+ uint8_t* source_ptr;
+ size_t source_len;
+ uint8_t loader;
+ int (*fetchSourceCode)(
+ const OnBeforeParseArguments* args,
+ struct OnBeforeParseResult* result
+ );
+ void* plugin_source_code_context;
+ void (*free_plugin_source_code_context)(void* ctx);
+ void (*log)(const OnBeforeParseArguments* args, BunLogOptions* options);
+} OnBeforeParseResult;
+
+
+typedef enum {
+ BUN_LOG_LEVEL_VERBOSE = 0,
+ BUN_LOG_LEVEL_DEBUG = 1,
+ BUN_LOG_LEVEL_INFO = 2,
+ BUN_LOG_LEVEL_WARN = 3,
+ BUN_LOG_LEVEL_ERROR = 4,
+} BunLogLevel;
+
+const BunLogLevel BUN_LOG_MAX = BUN_LOG_LEVEL_ERROR;
+
+#endif // BUN_NATIVE_BUNDLER_PLUGIN_API_H
diff --git a/packages/bun-native-plugin-rs/src/lib.rs b/packages/bun-native-plugin-rs/src/lib.rs
new file mode 100644
index 0000000000..3e589e3bcd
--- /dev/null
+++ b/packages/bun-native-plugin-rs/src/lib.rs
@@ -0,0 +1,627 @@
+//! > ⚠️ Note: This is an advanced and experimental API recommended only for plugin developers who are familiar with systems proramming and the C ABI. Use with caution.
+//!
+//! # Bun Native Plugins
+//!
+//! This crate provides a Rustified wrapper over the Bun's native bundler plugin C API.
+//!
+//! Some advantages to _native_ bundler plugins as opposed to regular ones implemented in JS:
+//!
+//! - Native plugins take full advantage of Bun's parallelized bundler pipeline and run on multiple threads at the same time
+//! - Unlike JS, native plugins don't need to do the UTF-8 <-> UTF-16 source code string conversions
+//!
+//! What are native bundler plugins exactly? Precisely, they are NAPI modules which expose a C ABI function which implement a plugin lifecycle hook.
+//!
+//! The currently supported lifecycle hooks are:
+//!
+//! - `onBeforeParse` (called immediately before a file is parsed, allows you to modify the source code of the file)
+//!
+//! ## Getting started
+//!
+//! Since native bundler plugins are NAPI modules, the easiest way to get started is to create a new [napi-rs](https://github.com/napi-rs/napi-rs) project:
+//!
+//! ```bash
+//! bun add -g @napi-rs/cli
+//! napi new
+//! ```
+//!
+//! Then install this crate:
+//!
+//! ```bash
+//! cargo add bun-native-plugin
+//! ```
+//!
+//! Now, inside the `lib.rs` file, expose a C ABI function which has the same function signature as the plugin lifecycle hook that you want to implement.
+//!
+//! For example, implementing `onBeforeParse`:
+//!
+//! ```rust
+//! use bun_native_plugin::{OnBeforeParse};
+//!
+//! /// This is necessary for napi-rs to compile this into a proper NAPI module
+//! #[napi]
+//! pub fn register_bun_plugin() {}
+//!
+//! /// Use `no_mangle` so that we can reference this symbol by name later
+//! /// when registering this native plugin in JS.
+//! ///
+//! /// Here we'll create a dummy plugin which replaces all occurences of
+//! /// `foo` with `bar`
+//! #[no_mangle]
+//! pub extern "C" fn on_before_parse_plugin_impl(
+//! args: *const bun_native_plugin::sys::OnBeforeParseArguments,
+//! result: *mut bun_native_plugin::sys::OnBeforeParseResult,
+//! ) {
+//! let args = unsafe { &*args };
+//! let result = unsafe { &mut *result };
+//!
+//! // This returns a handle which is a safe wrapper over the raw
+//! // C API.
+//! let mut handle = OnBeforeParse::from_raw(args, result) {
+//! Ok(handle) => handle,
+//! Err(_) => {
+//! // `OnBeforeParse::from_raw` handles error logging
+//! // so it fine to return here.
+//! return;
+//! }
+//! };
+//!
+//! let input_source_code = match handle.input_source_code() {
+//! Ok(source_str) => source_str,
+//! Err(_) => {
+//! // If we encounter an error, we must log it so that
+//! // Bun knows this plugin failed.
+//! handle.log_error("Failed to fetch source code!");
+//! return;
+//! }
+//! };
+//!
+//! let loader = handle.output_loader();
+//! let output_source_code = source_str.replace("foo", "bar");
+//! handle.set_output_source_code(output_source_code, loader);
+//! }
+//! ```
+//!
+//! Then compile this NAPI module. If you using napi-rs, the `package.json` should have a `build` script you can run:
+//!
+//! ```bash
+//! bun run build
+//! ```
+//!
+//! This will produce a `.node` file in the project directory.
+//!
+//! With the compiled NAPI module, you can now register the plugin from JS:
+//!
+//! ```js
+//! const result = await Bun.build({
+//! entrypoints: ["index.ts"],
+//! plugins: [
+//! {
+//! name: "replace-foo-with-bar",
+//! setup(build) {
+//! const napiModule = require("path/to/napi_module.node");
+//!
+//! // Register the `onBeforeParse` hook to run on all `.ts` files.
+//! // We tell it to use function we implemented inside of our `lib.rs` code.
+//! build.onBeforeParse(
+//! { filter: /\.ts/ },
+//! { napiModule, symbol: "on_before_parse_plugin_impl" },
+//! );
+//! },
+//! },
+//! ],
+//! });
+//! ```
+//!
+//! ## Very important information
+//!
+//! ### Error handling and panics
+//!
+//! It is highly recommended to avoid panicking as this will crash the runtime. Instead, you must handle errors and log them:
+//!
+//! ```rust
+//! let input_source_code = match handle.input_source_code() {
+//! Ok(source_str) => source_str,
+//! Err(_) => {
+//! // If we encounter an error, we must log it so that
+//! // Bun knows this plugin failed.
+//! handle.log_error("Failed to fetch source code!");
+//! return;
+//! }
+//! };
+//! ```
+//!
+//! ### Passing state to and from JS: `External`
+//!
+//! One way to communicate data from your plugin and JS and vice versa is through the NAPI's [External](https://napi.rs/docs/concepts/external) type.
+//!
+//! An External in NAPI is like an opaque pointer to data that can be passed to and from JS. Inside your NAPI module, you can retrieve
+//! the pointer and modify the data.
+//!
+//! As an example that extends our getting started example above, let's say you wanted to count the number of `foo`'s that the native plugin encounters.
+//!
+//! You would expose a NAPI module function which creates this state. Recall that state in native plugins must be threadsafe. This usually means
+//! that your state must be `Sync`:
+//!
+//! ```rust
+//! struct PluginState {
+//! foo_count: std::sync::atomic::AtomicU32,
+//! }
+//!
+//! #[napi]
+//! pub fn create_plugin_state() -> External {
+//! let external = External::new(PluginState {
+//! foo_count: 0,
+//! });
+//!
+//! external
+//! }
+//!
+//!
+//! #[napi]
+//! pub fn get_foo_count(plugin_state: External) -> u32 {
+//! let plugin_state: &PluginState = &plugin_state;
+//! plugin_state.foo_count.load(std::sync::atomic::Ordering::Relaxed)
+//! }
+//! ```
+//!
+//! When you register your plugin from Javascript, you call the napi module function to create the external and then pass it:
+//!
+//! ```js
+//! const napiModule = require("path/to/napi_module.node");
+//! const pluginState = napiModule.createPluginState();
+//!
+//! const result = await Bun.build({
+//! entrypoints: ["index.ts"],
+//! plugins: [
+//! {
+//! name: "replace-foo-with-bar",
+//! setup(build) {
+//! build.onBeforeParse(
+//! { filter: /\.ts/ },
+//! {
+//! napiModule,
+//! symbol: "on_before_parse_plugin_impl",
+//! // pass our NAPI external which contains our plugin state here
+//! external: pluginState,
+//! },
+//! );
+//! },
+//! },
+//! ],
+//! });
+//!
+//! console.log("Total `foo`s encountered: ", pluginState.getFooCount());
+//! ```
+//!
+//! Finally, from the native implementation of your plugin, you can extract the external:
+//!
+//! ```rust
+//! pub extern "C" fn on_before_parse_plugin_impl(
+//! args: *const bun_native_plugin::sys::OnBeforeParseArguments,
+//! result: *mut bun_native_plugin::sys::OnBeforeParseResult,
+//! ) {
+//! let args = unsafe { &*args };
+//! let result = unsafe { &mut *result };
+//!
+//! let mut handle = OnBeforeParse::from_raw(args, result) {
+//! Ok(handle) => handle,
+//! Err(_) => {
+//! // `OnBeforeParse::from_raw` handles error logging
+//! // so it fine to return here.
+//! return;
+//! }
+//! };
+//!
+//! let plugin_state: &PluginState =
+//! // This operation is only safe if you pass in an external when registering the plugin.
+//! // If you don't, this could lead to a segfault or access of undefined memory.
+//! match unsafe { handle.external().and_then(|state| state.ok_or(Error::Unknown)) } {
+//! Ok(state) => state,
+//! Err(_) => {
+//! handle.log_error("Failed to get external!");
+//! return;
+//! }
+//! };
+//!
+//!
+//! // Fetch our source code again
+//! let input_source_code = match handle.input_source_code() {
+//! Ok(source_str) => source_str,
+//! Err(_) => {
+//! handle.log_error("Failed to fetch source code!");
+//! return;
+//! }
+//! };
+//!
+//! // Count the number of `foo`s and add it to our state
+//! let foo_count = source_code.matches("foo").count() as u32;
+//! plugin_state.foo_count.fetch_add(foo_count, std::sync::atomic::Ordering::Relaxed);
+//! }
+//! ```
+//!
+//! ### Concurrency
+//!
+//! Your `extern "C"` plugin function can be called _on any thread_ at _any time_ and _multiple times at once_.
+//!
+//! Therefore, you must design any state management to be threadsafe
+
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+#[repr(transparent)]
+pub struct BunPluginName(*const c_char);
+
+impl BunPluginName {
+ pub const fn new(ptr: *const c_char) -> Self {
+ Self(ptr)
+ }
+}
+
+#[macro_export]
+macro_rules! define_bun_plugin {
+ ($name:expr) => {
+ pub static BUN_PLUGIN_NAME_STRING: &str = $name;
+
+ #[no_mangle]
+ pub static BUN_PLUGIN_NAME: bun_native_plugin::BunPluginName =
+ bun_native_plugin::BunPluginName::new(BUN_PLUGIN_NAME_STRING.as_ptr() as *const _);
+
+ #[napi]
+ fn bun_plugin_register() {}
+ };
+}
+
+unsafe impl Sync for BunPluginName {}
+
+use std::{
+ any::TypeId,
+ borrow::Cow,
+ cell::UnsafeCell,
+ ffi::{c_char, c_void},
+ str::Utf8Error,
+};
+
+pub mod sys {
+ include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+}
+
+#[repr(C)]
+pub struct TaggedObject {
+ type_id: TypeId,
+ pub(crate) object: Option,
+}
+
+struct SourceCodeContext {
+ source_ptr: *mut u8,
+ source_len: usize,
+ source_cap: usize,
+}
+
+extern "C" fn free_plugin_source_code_context(ctx: *mut c_void) {
+ // SAFETY: The ctx pointer is a pointer to the `SourceCodeContext` struct we allocated.
+ unsafe {
+ drop(Box::from_raw(ctx as *mut SourceCodeContext));
+ }
+}
+
+impl Drop for SourceCodeContext {
+ fn drop(&mut self) {
+ if !self.source_ptr.is_null() {
+ // SAFETY: These fields come from a `String` that we allocated.
+ unsafe {
+ drop(String::from_raw_parts(
+ self.source_ptr,
+ self.source_len,
+ self.source_cap,
+ ));
+ }
+ }
+ }
+}
+
+pub type BunLogLevel = sys::BunLogLevel;
+pub type BunLoader = sys::BunLoader;
+
+fn get_from_raw_str<'a>(ptr: *const u8, len: usize) -> Result> {
+ let slice: &'a [u8] = unsafe { std::slice::from_raw_parts(ptr, len) };
+
+ // Windows allows invalid UTF-16 strings in the filesystem. These get converted to WTF-8 in Zig.
+ // Meaning the string may contain invalid UTF-8, we'll have to use the safe checked version.
+ #[cfg(target_os = "windows")]
+ {
+ std::str::from_utf8(slice)
+ .map(Into::into)
+ .or_else(|_| Ok(String::from_utf8_lossy(slice)))
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ {
+ // SAFETY: The source code comes from Zig, which uses UTF-8, so this should be safe.
+
+ std::str::from_utf8(slice)
+ .map(Into::into)
+ .or_else(|_| Ok(String::from_utf8_lossy(slice)))
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum Error {
+ Utf8(Utf8Error),
+ IncompatiblePluginVersion,
+ ExternalTypeMismatch,
+ Unknown,
+}
+
+pub type Result = std::result::Result;
+
+impl From for Error {
+ fn from(value: Utf8Error) -> Self {
+ Self::Utf8(value)
+ }
+}
+
+/// A safe handle for the arguments + result struct for the
+/// `OnBeforeParse` bundler lifecycle hook.
+///
+/// This struct acts as a safe wrapper around the raw C API structs
+/// (`sys::OnBeforeParseArguments`/`sys::OnBeforeParseResult`) needed to
+/// implement the `OnBeforeParse` bundler lifecycle hook.
+///
+/// To initialize this struct, see the `from_raw` method.
+pub struct OnBeforeParse<'a> {
+ args_raw: &'a sys::OnBeforeParseArguments,
+ result_raw: *mut sys::OnBeforeParseResult,
+ compilation_context: *mut SourceCodeContext,
+}
+
+impl<'a> OnBeforeParse<'a> {
+ /// Initialize this struct from references to their raw counterparts.
+ ///
+ /// This function will do a versioning check to ensure that the plugin
+ /// is compatible with the current version of Bun. If the plugin is not
+ /// compatible, it will log an error and return an error result.
+ ///
+ /// # Example
+ /// ```rust
+ /// extern "C" fn on_before_parse_impl(args: *const sys::OnBeforeParseArguments, result: *mut sys::OnBeforeParseResult) {
+ /// let args = unsafe { &*args };
+ /// let result = unsafe { &mut *result };
+ /// let handle = match OnBeforeParse::from_raw(args, result) {
+ /// Ok(handle) => handle,
+ /// Err(()) => return,
+ /// };
+ /// }
+ /// ```
+ pub fn from_raw(
+ args: &'a sys::OnBeforeParseArguments,
+ result: *mut sys::OnBeforeParseResult,
+ ) -> Result {
+ if args.__struct_size < std::mem::size_of::()
+ || unsafe { (*result).__struct_size } < std::mem::size_of::()
+ {
+ let message = "This plugin is not compatible with the current version of Bun.";
+ let mut log_options = sys::BunLogOptions {
+ __struct_size: std::mem::size_of::(),
+ message_ptr: message.as_ptr(),
+ message_len: message.len(),
+ path_ptr: args.path_ptr,
+ path_len: args.path_len,
+ source_line_text_ptr: std::ptr::null(),
+ source_line_text_len: 0,
+ level: BunLogLevel::BUN_LOG_LEVEL_ERROR as i8,
+ line: 0,
+ lineEnd: 0,
+ column: 0,
+ columnEnd: 0,
+ };
+ // SAFETY: The `log` function pointer is guaranteed to be valid by the Bun runtime.
+ unsafe {
+ ((*result).log.unwrap())(args, &mut log_options);
+ }
+ return Err(Error::IncompatiblePluginVersion);
+ }
+
+ Ok(Self {
+ args_raw: args,
+ result_raw: result,
+ compilation_context: std::ptr::null_mut() as *mut _,
+ })
+ }
+
+ pub fn path(&self) -> Result> {
+ get_from_raw_str(self.args_raw.path_ptr, self.args_raw.path_len)
+ }
+
+ pub fn namespace(&self) -> Result> {
+ get_from_raw_str(self.args_raw.namespace_ptr, self.args_raw.namespace_len)
+ }
+
+ /// Get the external object from the `OnBeforeParse` arguments.
+ ///
+ /// The external object is set by the plugin definition inside of JS:
+ /// ```js
+ /// await Bun.build({
+ /// plugins: [
+ /// {
+ /// name: "my-plugin",
+ /// setup(builder) {
+ /// const native_plugin = require("./native_plugin.node");
+ /// const external = native_plugin.createExternal();
+ /// builder.external({ napiModule: native_plugin, symbol: 'onBeforeParse', external });
+ /// },
+ /// },
+ /// ],
+ /// });
+ /// ```
+ ///
+ /// The external object must be created from NAPI for this function to be safe!
+ ///
+ /// This function will return an error if the external object is not a
+ /// valid tagged object for the given type.
+ ///
+ /// This function will return `Ok(None)` if there is no external object
+ /// set.
+ ///
+ /// # Example
+ /// The code to create the external from napi-rs:
+ /// ```rs
+ /// #[no_mangle]
+ /// #[napi]
+ /// pub fn create_my_external() -> External {
+ /// let external = External::new(MyStruct::new());
+ ///
+ /// external
+ /// }
+ /// ```
+ ///
+ /// The code to extract the external:
+ /// ```rust
+ /// let external = match handle.external::() {
+ /// Ok(Some(external)) => external,
+ /// _ => {
+ /// handle.log_error("Could not get external object.");
+ /// return;
+ /// },
+ /// };
+ /// ```
+ pub unsafe fn external(&self) -> Result