bun2nix
bun2nix is a fast Rust-based tool for converting lock-files generated by the JavaScript Bun (v1.2+) package manager to Nix expressions, which allows them to be consumed to build Bun packages reproducibly.
Advantages
Here are some of the advantages of using bun2nix over the alternatives:
- Much faster than other similar
lang2nixtools for the JavaScript ecosystem - a full install will only take around 50ms for a medium project with 2k packages - Build AOT-compiled binaries easily with Bun that fit the Nix model much better than NPM scripts
- Quality error messages because of the static types in Rust
Alternatives
Here are some alternatives to bun2nix in the JavaScript ecosystem which fulfill a similar purpose:
Installation
Bun2nix is primarily distributed as a nix flake library that contains everything you need to package your bun application with the tool.
This can be set up in two ways:
From A Template
By default a handful of templates are offered for starting off a new project with bun2nix enabled by default:
- A minimal hello world binary program - the default.
- A basic react website and server setup.
- And the rest in
templates/
Notable files
The main files of note are:
flake.nix⇒ Contains basic project setup for a nix flake forbun2nixdefault.nix⇒ Contains build instructions for this bun packagebun.nix⇒ Generated bun expression frombun.lockpackage.json⇒ Standard JavaScriptpackage.jsonwith apostinstallscript pointing tobun2nix
Default - Minimal Setup
To produce the default minimal sample, run:
nix flake init -t github:nix-community/bun2nix
This is a bare-bones project created via bun init which produces a simple hello world binary packaged via bun2nix.
React Website
To start with the React website template run
nix flake init -t github:nix-community/bun2nix#react
This is a simple example of a basic React app built through bun2nix.
In a pre-existing flake
To install bun2nix in an already existing bun project with a flake.nix, the following steps are recommended:
1. Source the bun2nix repo
Add the bun2nix flake to your inputs as follows:
bun2nix.url = "github:nix-community/bun2nix";
bun2nix.inputs.nixpkgs.follows = "nixpkgs";
1.5. (Optional) Use the binary cache
The bun2nix executable typically takes a while to compile, which is typical for many Rust programs, hence, hence, it may be a good idea to use the nix-community cache.
To add it include the following in your flake.nix.
nixConfig = {
extra-substituters = [
"https://cache.nixos.org"
"https://nix-community.cachix.org"
];
extra-trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
};
2. Get a bun2nix binary
Recommended: Use the NPM package
bun2nix has a cross-platform web-assembly based NPM package available for usage. This should
be the default choice as it allows those who do not have nix installed to work on your project
and keep the generated bun.nix file up to date.
Simply install and run it with:
bun install --dev bun2nix
bunx bun2nix
Add the binary to your environment with nix
Alternatively, add the native bun2nix CLI program into your developer environment by adding it to your dev-shell.
devShells.default = pkgs.mkShell {
packages = with pkgs; [
bun
bun2nix.packages.${system}.default
];
};
NOTE: the
systemvariable can be gotten in a variety of convenient ways - including flake-utils or nix-systems.
3. Use the binary in a bun postinstall script
To keep the generated bun.nix file produced by bun2nix up to date, add bun2nix as a postinstall script to run it after every bun operation that modifies the packages in some way.
Add the following to package.json:
"scripts": {
"postinstall": "bun2nix -o bun.nix"
}
4. Build your package with Nix
Finally, a convenient package builder is exposed inside bun2nix - mkDerivation.
Add the following to flake.nix:
A nice way to do this might be with the overlay
my-package = pkgs.callPackage ./default.nix {
inherit bun2nix.packages.${system}.default;
};
And place this in a file called default.nix
{ bun2nix, ... }:
bun2nix.mkDerivation {
pname = "bun2nix-example";
version = "1.0.0";
src = ./.;
bunNix = ./bun.nix;
module = "index.ts";
}
A list of available options for mkDerivation can be seen at the building packages page, along with other useful things for building bun packages.
Overlay
bun2nix provides an overlay which places the bun2nix binary (along with it's passthru functions) into pkgs for ease of use, where you can then use it in your devShell or with pkgs.callPackage, etc.
Usage
Set up the bun2nix overlay as follows:
Add the overlay
Instantiate pkgs with the bun2nix attribute included:
{ bun2nix, nixpkgs, ... }:
let
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ inputs.bun2nix.overlays.default ];
};
in
{ ... }
Use bun2nix from pkgs
Use your freshly overlaid pkgs to build a bun2nix project:
{ pkgs, ... }:
pkgs.stdenv.mkDerivation {
pname = "react-website";
version = "1.0.0";
src = ./.;
nativeBuildInputs = [
pkgs.bun2nix.hook
];
bunDeps = pkgs.bun2nix.fetchBunDeps {
bunNix = ./bun.nix;
};
buildPhase = ''
bun run build \
--minify
'';
installPhase = ''
mkdir -p $out/dist
cp -R ./dist $out
'';
}
Or, to add the bun2nix binary to your devShell:
# In your flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
bun2nix.url = "github:nix-community/bun2nix";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, bun2nix, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ bun2nix.overlays.default ];
};
in
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
bun
bun2nix
];
};
}
);
}
The Command Line Tool
Recommended Usage
The recommended way to use bun2nix is by adding it to your package.json to automatically run after any package management option:
"scripts": {
"postinstall": "bun2nix -o bun.nix"
}
However, if you run without the -o flag it will produce text output over stdout similar to other lang2nix tools. Hence, if you enforce formatting rules in your repository, it is likely a good idea to pass it through a formatter before writing the file.
Choosing between the WASM CLI and the native CLI
You should use the WASM CLI if you are:
- On a team in environments where Nix may not be installed (i.e. working with Windows users, etc.)
- Not using any exotic dependency types (tarball, git, etc.)
You should use the native CLI if you are:
- All using Nix anyway (less overhead because it is not ran in a JavaScript runtime)
- Using dependency types which require a prefetch (tarball, git, etc.)
Options
WASM CLI
Currently, the options available from the web assembly command line tool are as follows:
Description
Convert Bun (v1.2+) packages to Nix expressions
Usage
$ bun2nix [options]
Options
-l, --lock-file The Bun (v1.2+) lockfile to use to produce the Nix expression (default bun.lock)
-o, --output-file The output file to write to - if no file location is provided, print to stdout instead
-v, --version Displays current version
-h, --help Displays this message
Native CLI
Currently, the options available from the native command line tool are as follows:
Convert Bun (v1.2+) packages to Nix expressions
Usage: bun2nix [OPTIONS]
Options:
-l, --lock-file <LOCK_FILE> The Bun (v1.2+) lockfile to use to produce the Nix expression [default: ./bun.lock]
-o, --output-file <OUTPUT_FILE> The output file to write to - if no file location is provided, print to stdout instead
-h, --help Print help
-V, --version Print version
Building Packages
bun2nix provides a number of functions to aid in building Bun-related packages:
All of these functions are available as attributes on the
bun2nixpackage itself via itspassthru.
Building with bun2nix.mkDerivation
mkDerivation is provided as a library function to build Bun packages from the file generated by the bun2nix command line tool.
This function is intended for use building binaries with Bun. If you need to build something like a compiled React website, the more idiomatic choice is probably the bun2nix hook.
Example
Currently, basic usage would look something like:
{bun2nix, ...}:
bun2nix.mkDerivation {
pname = "simple-bun-app";
version = "1.0.0";
src = ./.;
bunDeps = bun2nix.fetchBunDeps {
bunNix = ./bun.nix;
};
module = "index.ts";
}
or, in the more implicit style:
{bun2nix, ...}:
bun2nix.mkDerivation {
packageJson = ./package.json;
src = ./.;
bunDeps = bun2nix.fetchBunDeps {
bunNix = ./bun.nix;
};
}
NOTE: building with the implicit
package.jsonvalues makes a number of basic assumptions about your project that it expects to hold true in the name of convenience. Approximately, these are:
nameis a field that is acceptable for use as the name of the binary produced by your package.versionis a field denoting your package version in proper semantic versioning.moduleis a field pointing towards yourindex.tsfile or equivalent. If you notice any strange errors while using the implicit build scheme try specifying the values manually and contribute a new descriptive assert message tomkDerivation.
Arguments
The full list of accepted arguments is:
Additionally, the full range of config options from the hook is available too.
| Argument | Purpose |
|---|---|
packageJson | (Optional) Your project's package.json. If supplied can be used to complete pname, version and module instead of requiring them manually. |
pname | The name of the package to build. Required if packageJson is not given. |
version | Your package version. Required if packageJson is not given. |
module | The index.{js,ts} file entry point to your Bun application. This should be a string containing the relative path from src. Required if packageJson is not given. |
Building with bun2nix.hook
The bun2nix hook provides a simple way to extend an existing derivation with Bun dependencies by way of a setup hook.
This is especially useful for building things like websites or other artifacts that have a build artifact that is not an executable, or for building polyglot projects that need Bun dependencies on hand.
mkDerivationis a thin wrapper over this withstdenv.mkDerivationwith some extra goodies for building Bun executables.
Example
You can use the bun2nix hook to integrate with an existing stdenv.mkDerivation style function by adding it to nativeBuildInputs like so:
{ stdenv, bun2nix, ... }:
stdenv.mkDerivation {
pname = "my-react-website";
version = "1.0.0";
src = ./.;
nativeBuildInputs = [
bun2nix.hook
];
bunDeps = bun2nix.fetchBunDeps {
bunNix = ./bun.nix;
};
buildPhase = ''
bun run build \
--minify
'';
installPhase = ''
mkdir -p $out/dist
cp -R ./dist $out
'';
}
Troubleshooting
The default behavior of bun2nix is to hard-link installs from the Nix store. Unfortunately, this is not guaranteed to work the same on all systems - if you see strange permissions errors from bun install try setting bunInstallFlags to --backend=symlink, which works but may be marginally slower.
Useful Functional Information
The bun2nix hook installs the fake Bun install cache created by fetchBunDeps at $BUN_INSTALL_CACHE_DIR.
This is then installed into your repo via a regular bun install during bunNodeModulesInstallPhase, which runs before the buildPhase.
Arguments
The full list of extra arguments bun2nix.hook adds to a derivation are:
| Argument | Purpose |
|---|---|
bunDeps | The output of fetchBunDeps (or any other Nix derivation which produces a Bun-compatible install cache). This is required. |
bunBuildFlags | Flags to pass to Bun in the default Bun build phase |
bunCheckFlags | Flags to pass to Bun in the default Bun check phase |
bunInstallFlags | Flags to pass to bun install. If not set these default to "--linker=isolated --backend=symlink" on aarch64-darwin or "--linker=isolated" on other systems |
dontRunLifecycleScripts | By default, after bunNodeModulesInstallPhase runs bun install --ignore-scripts, bunLifecycleScriptsPhase runs any missing lifecycle scripts after making the node_modules directory writable and executable. This attribute can be used to disable running bunLifecycleScriptsPhase |
dontUseBunPatch | Don't patch any shebangs in your src directory to use Bun as their interpreter |
dontUseBunBuild | Disable the default build phase |
dontUseBunCheck | Disable the default check phase |
dontUseBunInstall | Disable the default install phase |
New Build Phases
The bun2nix hook introduces a number of new build phases which are worth knowing about:
These all have
preandpostrun hooks available
| Phase | Purpose |
|---|---|
bunPatchPhase | Before doing anything, patch shebangs of your local scripts to use Bun as their interpreter |
bunNodeModulesInstallPhase | Runs bun install in your src repo |
bunLifecycleScriptsPhase | Runs any Bun lifecycle scripts (i.e., "install", etc.) after making node_modules writable |
Fetching with bun2nix.fetchBunDeps
fetchBunDeps is a handy function responsible for creating a bun compatible cache for doing offline installs.
Example
You should use fetchBunDeps in conjunction with the rest of bun2nix to build your Bun packages like so:
{
bun2nix,
...
}:
bun2nix.mkDerivation {
pname = "workspace-test-app";
version = "1.0.0";
src = ./.;
bunDeps = bun2nix.fetchBunDeps {
bunNix = ./bun.nix;
};
module = "packages/app/index.js";
}
Arguments
fetchBunDeps is designed to offer a number of flexible options for customizing your Bun install process:
| Argument | Purpose |
|---|---|
bunNix | The bun.nix file as created by the bun2nix CLI |
overrides | Allows for modifying packages before install in the Nix store to patch any broken dependencies. See the overriding section below |
useFakeNode | By default, bun2nix patches any scripts that use Node in your dependencies to use bun as its executable instead. Turning this off will patch them to use node instead. This might be useful, if, for example, you need to link to actual Node v8 while building a native addon. Defaults to true. |
patchShebangs | If scripts in your dependencies should have their shebangs patched or not. Defaults to true. |
Overrides
fetchBunDeps provides an overrides api for modifying packages in the Nix store before they become a part of Bun's install cache and ultimately your project's node_modules.
You may want to use this to patch a dependency which, for example, makes a network request during install and fails because it's sandboxed by Nix.
Type
Each override attribute name must be a key that exists in your bun.nix file, and the attribute value must be a function that takes a derivation and returns another one.
Example
bunDeps = bun2nix.fetchBunDeps {
bunNix = ./bun.nix;
overrides = {
"typescript@5.7.3" =
pkg:
runCommandLocal "override-example" { } ''
mkdir $out
cp -r ${pkg}/. $out
echo "hello world" > $out/my-override.txt
'';
};
};
# Assertion will not fail
postBunNodeModulesInstallPhase = ''
if [ ! -f "node_modules/typescript/my-override.txt" ]; then
echo "Text file created with override does not exist."
exit 1
fi
'';
Operating Details
As mentioned above, the bun2nix.fetchBunDeps function produces a bun compatible cache, which allows bun to do offline installs through files available in the Nix store.
In general, for a given package, the installation process looks something like:
# bun.nix contents
"@types/bun@1.2.4" = fetchurl {
url = "https://registry.npmjs.org/@types/bun/-/bun-1.2.4.tgz";
hash = "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA==";
};
1. Download the package via a fetcher function
First, bun2nix deps are downloaded via a Nix FOD fetching function.
For most packages this is
pkgs.fetchurl, and the hash can be taken directly from thebun.locktextual lock-file, meaning they don't need to be prefetched.
2. Extract the package
By default, fetching packages will produce either a tarball or a flat directory with the contents.
If the package is a tarball, it should be extracted first. Then, both types have their permissions normalized to make sure that scripts are properly executable/readable by the nixbld users.
Any references to node or bun binaries are also fixed up at this stage.
Unfortunately, NPM dependencies cannot use
builtins.fetchTarball, which would do the fetching and extraction in one build step because it would produce a hash that differs from the one in the Bun lockfile.
3. Creating a cache entry
After this point, the packages are all the same shape (a patched flat directory). These are then passed into the bun2nix.cache-entry-creator binary, which creates symlinks to where Bun expects to find its packages in its cache.
This binary is a Zig-implemented command-line utility, because Bun uses a very specific version of Wyhash, which is vendored into their project, to patch package names before putting them into their cache.
If a package is in a sub-directory like @types, it must have appropriate parent directories created too.
# Input
"@types/bun@1.2.4"
# Output location
$out/share/bun-cache/@types/bun@1.2.4@@@1
# Input
"tailwindcss@4.0.0-beta.9"
# Output location
$out/share/bun-cache/tailwindcss@4.0.0-73c5c46324e78b9b@@@1
4. Forcing use of the cache
Our Bun cache is then forced to be used in the build process by the hook, which sets $BUN_INSTALL_CACHE_DIR to $out/share/bun-cache.
Building with bun2nix.writeBunApplication
writeBunApplication is provided as a library function as a convenient extension over
mkDerivation. If you are looking to use writeBunApplication, please take a look over there first to familiarize yourself with its arguments.
This function is intended for use building non-compilable programs that run under bun.
Note:
writeBunApplicationtakes heavy inspiration frompkgs.writeShellApplication. Think of it as an alternative which also managesbundependencies.
When to use:
- If you need something like a
nextjsserver that needs the original source directory and a validnode_modulesto run - If you want to run a
bunxbinary as if it is a regular system executable withbun2nixmanaged dependencies.
When to avoid:
- You should avoid this at all if you can bundle your application in a way where it produces a standalone artifact. This could be something like a website, a bun binary, or anything else that doesn't care about the original source. Use the hook or the
mkDerivationto build those kinds of projects instead.
Example
Currently, basic usage would look something like (to package a nextjs server):
{bun2nix, ...}:
bun2nix.writeBunApplication {
# Could also be `pname` and `version`
# See the `mkDerivation` docs if this
# looks unfamiliar to you
packageJson = ./package.json;
src = ./.;
# Use the `build` script from
# `package.json` instead of
# bun's bundler accessed via
# `bun build`
#
# Confusing, isn't it?
buildPhase = ''
bun run build
'';
# Start script to use to launch
# your project at runtime
startScript = ''
bun run start
'';
bunDeps = bun2nix.fetchBunDeps {
bunNix = ./bun.nix;
};
}
Or, to package an arbitrary bunx binary (as long as it's in bun.nix somewhere)
{bun2nix, ...}:
bun2nix.writeBunApplication {
packageJson = ./package.json;
src = ./.;
dontUseBunBuild = true;
dontUseBunCheck = true;
startScript = ''
bunx cowsay "$@"
'';
bunDeps = bun2nix.fetchBunDeps {
bunNix = ./bun.nix;
};
}
Or, to package a simple web server that uses cowsay and a runtime environment variable:
# In your default.nix
{ pkgs, bun2nix, ... }:
bun2nix.writeBunApplication {
pname = "cowsay-server";
version = "1.0.0";
src = ./.;
startScript = ''
bun run index.ts
'';
runtimeInputs = [ pkgs.cowsay ];
runtimeEnv = {
USER = "Nix";
};
bunDeps = bun2nix.fetchBunDeps {
bunNix = ./bun.nix;
};
}
And the corresponding index.ts:
import { $ } from "bun";
const user = process.env.USER || "World";
const server = Bun.serve({
port: 3000,
async fetch(req) {
const message = `Hello, ${user}!`;
const cowsay = await $`cowsay ${message}`.text();
return new Response(cowsay);
},
});
console.log(`Listening on http://localhost:${server.port} ...`);
Arguments
The full list of accepted arguments is:
Additionally, the full range of config options from
mkDerivationis available too.
| Argument | Purpose |
|---|---|
startScript | The script to start your application with. |
runtimeInputs | Runtime nix dependencies your application requires. Defaults to []. |
runtimeEnv | Runtime environment variables. Defaults to {}. |
excludeShellChecks | shellcheck checks to exclude while checking your startScript. Defaults to []. |
extraShellCheckFlags | Extra shellcheck flags to run when checking your startScript. Defaults to []. |
bashOptions | Bash Options to set for your script. Defaults to [ "errexit", "nounset", "pipefail" ]. |
inheritPath | If your script should use the external $PATH when begin ran. Defaults to false for purity. |
Creating scripts with bun2nix.writeBunScriptBin
writeBunScriptBin is useful for creating once off $ Shell scripts, similarly to writeShellScriptBin in nixpkgs.
Example
An example bun script for printing "Hello World" might look like:
writeBunScriptBin {
name = "hello-world";
text = ''
import { $ } from "bun";
await $`echo "Hello World!"`;
'';
};
Arguments
The full list of accepted arguments is:
| Argument | Purpose |
|---|---|
name | The name to give the binary of the script |
text | Textual contents of the script |
V2 Release Guide
bun2nix V2 comes with many improvements, including:
- Guaranteed correct bun installs via building the bun cache instead of creating the
node_modules/directory manually - Faster incremental builds via better caching
- Rewritten nix API designed to be both more idiomatic and flexible
- New NPM packaged web assembly CLI - run the
bun2nixCLI with nothing butbunx bun2nix- see the NPM page. - Now supports more of bun's dependency types, including:
- Files and symlinks
- Tarballs
- Git Dependencies
This work was done under Fleek. Please, check us out, especially if you are interested in Nix!
Updating
Below is a guide to all the breaking changes:
mkBunDerivation
mkBunDerivationhas been renamed and moved frominputs.bun2nix.${system}.mkBunDerivationtoinputs.bun2nix.packages.${system}.default.mkDerivation(or more simplybun2nix.mkDerivation)- The
indexattribute has been renamed tomoduleto be inline with how it's defined inpackages.json. - Instead of specifying
bunNixdirectly, specifybunDepsusingfetchBunDepsinstead. - See
mkDerivationfor other new features and examples
mkBunNodeModules
mkBunNodeModuleshas been removed entirely in favor offetchBunDepsto build a bun compatible cache.- Still need to add
node_modules/to an arbitrary derivation? Have a look at the newbun2nixhook, which provides a much nicer API.
writeBunScriptBin
- This has been moved from
inputs.bun2nix.lib.${system}.writeBunScriptBintoinputs.bun2nix.packages.${system}.default.writeBunScriptBin
New bun.nix schema
- Now consumable by
pkgs.callPackageandbun2nix.fetchBunDeps. - Don't forget to rewrite your
bun.nixfile by runningbun2nix -o bun.nixbefore use!