I'm still here...

Well, it's been several months since my last post. And I want to apologize to the bots that crawl this website for not giving you anything new to index. And to the human that might read this someday, welcome; sorry there's not more here (at the time of writing). Not only did my wife and I welcome our third child into the world, but I've also been heads down working on a different project rather than updating my blog with all the things that I still don't like. I've got some improvements to the architecture in mind, AND Bun 1.4 just dropped, which means I can drop some dependencies.

That said, there's a lot about what has been built for this blog that one post really doesn't showcase. So I figured I might write a short article about something I stumbled across while working on my other project.

Bun Patch

In short, Bun Patch is functionality built into the Bun Package Manager (part of the Bun Runtime) that allows us to modify node_modules whenever we run 'bun install' to fetch our project dependencies. Reading the friendly manual is always a great way to learn about something, so I encourage you to read the documentation for bun patch here: https://bun.com/docs/pm/cli/patch

How is this Helpful?

Some needed background

In my other project, I'm using a web framework called ElysiaJS. This is a phenomenal framework that is performant and full-featured. On July 30th, the creator of Elysia published a blog post introducing the beta for Elysia 2. There were quite a few performance improvements, ergonomic adjustments, and some breaking changes introduced, but reportedly, it's already quite stable. I, of course, encourage you to read the full blog post though it's not quite applicable to this post.

What matters is that I decided to upgrade so that I could build on this new foundation, but I immediately ran into an error. My project uses Turborepo to segment apps and packages, and when I first started the project, 'hoisting' was the default package management strategy. This means that if 'App A' declared a dependency, it would be stored in the root node_modules folder alongside dependencies for 'App B' and other packages.

root/
├── package.json (root)
├── node_modules/ (HOISTED - all dependencies here)
│   ├── react/
│   ├── axios/
│   └── ... (other dependencies from App A, App B, etc.)
└── apps/
    ├── app-a/
    └── app-b/

I noticed this was causing dependencies to be leaked that weren't properly declared in the app/packages manifest file, package.json. To solve this, I updated to Isolated Installs, which prevents phantom dependencies by requiring they be declared in the manifest for which they are being used. You can read how this works at the previous link, but the key takeaway is that when I try to pull in a package that exists in the monorepo but not a specific app, there is now an error that gets thrown. This is good because it helps ensure things are properly compartmentalized.

The Problem

Elysia has several plugins that add helpful tooling. One such package is @elysia/openapi which generates documentation based on the types defined in your elysia instance(s). When I updated to Elyisa 2, I started getting an error.

error: Cannot find module 
    '../node_modules/typebox/build/type/script/script.mjs' from 
    '/project/node_modules/.bun/@[email protected]+6c8eef7d7158fc74/node_modules/@elysia/openapi/dist/gen/index.mjs'

By navigating into the node_modules folder that throws the error, we see that we have a hardcoded relative path import. This may have worked fine with 'hoisted installs' because of the relative pathing, but because I had switched to 'isolated installs', the package failed to resolve. If we inspect the following package.json snippet from the @elysia/openapi plugin, we see that typebox is listed under the devDependencies.

"devDependencies": {
    "@apidevtools/swagger-parser": "^12.0.0",
    "@scalar/types": "^0.2.13",
    "@types/bun": "1.2.20",
    "effect": "^3.17.13",
    "elysia": "2.0.0-beta.1",
    "eslint": "9.6.0",
    "file-type": "^22.0.0",
    "openapi-types": "^12.1.3",
    "tsdown": "^0.22.3",
    "typebox": "1.3.0",
    "typescript": "^5.9.2",
    "zod": "^4.2.1"
},

This means that when Bun install runs, it does not include Typebox as a sibling in order for a relative path to resolve properly. Not only is the package missing, even if Typebox was included, the '@elysia` namespace actually nests the package one directory further which would have still likely resulted in a failed module resolution.

The Solution

The solution is fundamentally rather simple. Instead of using a relative path, lean on the package manager to properly resolve the package for us. This means that with 'isolated installs', Bun can traverse what it needs to, in order to resolve the module properly.

-const require_script = require('../node_modules/typebox/build/type/script/script.js');
-require('../node_modules/typebox/build/type/index.js');
+const require_script = require('typebox/type');

While it's a simple fix, keep in mind that this is a node_module, meaning a dependency managed by a package manager. Not internal code tracked by git. The moment bun install is run, these changes will be overwritten, and those running it for the first time, such as when first cloning the repo, won't have the change at all. This also would be a weird thing to include in a README that no one will read. Additionally, waiting for an upstream fix to be accepted will take an indeterminate amount of time.

This is where the beauty of Bun Patch comes into play. By defining what is essentially a git diff we instruct Bun to make those changes every time bun install is executed. In fact, my previous snippet was pulled from the patch file. Here's the full version.

diff --git a/dist/gen/index.js b/dist/gen/index.js
index 500863734b896bafa4522c9d5f4500b7d9a3d883..a1781e5c316df4f8cb021e897e4e0888ef1299e4 100644
--- a/dist/gen/index.js
+++ b/dist/gen/index.js
@@ -1,6 +1,5 @@
 Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
-const require_script = require('../node_modules/typebox/build/type/script/script.js');
-require('../node_modules/typebox/build/type/index.js');
+const require_script = require('typebox/type');
 
 //#region src/gen/index.ts
 const matchRoute = /: Elysia<(.*)>/gs;
diff --git a/dist/gen/index.mjs b/dist/gen/index.mjs
index 27baa3f4900963856809d0b13dc49e91f454a1d3..ded1c2166dd124dd95d7b4821a0aa9a628ef478e 100644
--- a/dist/gen/index.mjs
+++ b/dist/gen/index.mjs
@@ -1,5 +1,4 @@
-import { Script } from "../node_modules/typebox/build/type/script/script.mjs";
-import "../node_modules/typebox/build/type/index.mjs";
+import { Script } from "typebox/type";
 
 //#region src/gen/index.ts
 const matchRoute = /: Elysia<(.*)>/gs;

This means that I can get started using the plugin immediately without having to wait for a fix to be accepted and deployed upstream. Not only that, but I can reliably reproduce the fix programmatically. Anyone pulling down the codebase for the first time doesn't even need to know this exists in the codebase as a fix. It just works, reliably and consistently while being tracked by version control.