Oftentimes, it feels like we overengineer the solutions we build. Patching packages never feels this way.
When I use patch-tool, it's often the best solution.
It may not be the best long-term solution, but it's so darn fast. And at a startup, speed of development directly translates into product velocity.
So let me tell you how to use it. With an example: Patching Datadog.
When working with javascript, you naturally have file access to the contents of your imported NPM packages. So you can modify them locally. If your code needs a dependency changed, you can make that change, rebuild, and try it out.
What patch-package does then, is it lets you check those patches into version control. So when your teammates or CI systems pull the repo, as part of running yarn
to update deps, they reapply those patches. That way you can modify your dependencies, without having to take on the weighty burden of forking them.
So here's the context. I work at a company called Jam, and we make the best QA tool for websites and webapps. We care about our users' experience, so we want to keep track of unexpected errors that users hit. We use Datadog for this.
The problem is that a portion of our Chrome1 extension is a content-script, which gets loaded alongside pages that users report bugs on. When our content script sends updates to Datadog, these updates live in the context of the host page. So they pollute the user's network requests tab in devtools. And when a user closes the page, requests that were buffered could be lost!
So that's the issue. We don't want Datadog requests to go be sent by the pages that our users browse. But Datadog's browser-logs
and browser-rum
packages don't let you proxy messages with a proxy function! You can set a URL to proxy them through, but that doesn't help in our case. What do? Fork it?
No! Patch-package to the rescue!
The Datadog packages we use have all of their requests go through one file:
@datadog/browser-core
, which is the package I patched directly, after elevating it to a top-level dependency.What we need to do now is modify this file to allow us to proxy these requests. After that, we'll be able to proxy these requests over postMessage, from our content script to our background script.
But first, we need to figure out what version of the file we're modifying! For this file, there's a cjs
and an esm
version. Easiest way to figure out which one we're importing? Just sprinkle some log statements in, clear webpack's cache, and reload!
THIS IS ESM!
We've found our file! Next step: modify it!
Since Chrome extension content scripts live in a separate context from their host page (window
objects aren't shared), we can simply have our patched package read from the window
object, to figure out where should send the message data, without worrying about intereference with/from the host page.
'DATADOG_REQUEST_PROXY'
function to the window
, our logger falls back gracefully. This is useful, because we'll be using this dependency in our background script as well, where we want to use default behavior!Amazing! Last step, proxy the messages and execute them on the background script.
And in the background script, we execute the messages as regular network requests:
HttpRequest
? Imported from the same file we patched, which thankfully already exported it.The end result: Datadog logs are smoothly sent through our background script!
Now that we're satsified with our changes, we run yarn patch-package
to generate a patch file, and commit it to version control:
Next time you consider forking a package to change a few lines, don't forget you have another option!
Patch-package even makes it easy to contribute back, by providing an easy way to submit your patch as a PR!
One more thing: care as much about user experience as we do? Hate how long it takes to file bugs?
Try Jam today! Our users file bugs in seconds, directly to their issue tracker, with all the details engineers need. Learn more here!