esbuild can inject arbitrary JavaScript code into your bundles, which is a surprisingly powerful way to polyfill global variables that might not be available in older JavaScript environments.
Let’s see it in action. Imagine you’re using a new Web API, like the navigator.clipboard API, but you need to support browsers that don’t have it yet.
// main.js
console.log(navigator.clipboard);
If you bundle this with esbuild without any special configuration, and you run it in an older browser, navigator.clipboard will be undefined, and any subsequent calls will throw an error.
Here’s how we can fix that with esbuild’s inject option. We’ll create a small JavaScript file that defines navigator.clipboard if it doesn’t exist, and then we’ll tell esbuild to inject this file into our bundle.
First, create a file named clipboard.polyfill.js:
// clipboard.polyfill.js
if (typeof navigator.clipboard === 'undefined') {
Object.defineProperty(navigator, 'clipboard', {
value: {
readText: () => Promise.reject(new Error('Clipboard API not available')),
writeText: () => Promise.reject(new Error('Clipboard API not available')),
// Add other methods if needed for your specific use case
},
writable: false,
configurable: false,
});
console.log('Clipboard polyfill applied');
}
Now, we’ll use esbuild to bundle main.js and inject clipboard.polyfill.js. The inject option takes an array of file paths.
esbuild main.js --bundle --outfile=bundle.js --inject:clipboard.polyfill.js
When you run bundle.js in an environment where navigator.clipboard doesn’t exist, esbuild will first execute the code from clipboard.polyfill.js. Our polyfill checks if navigator.clipboard is undefined. If it is, it dynamically defines navigator.clipboard with a basic implementation that rejects promises, mimicking the behavior of the real API when it’s not supported. A message "Clipboard polyfill applied" will also be logged to the console, confirming the polyfill ran.
This inject mechanism is essentially a global prepend. esbuild takes all the files listed in inject and places their contents at the very beginning of the output bundle, before any of your application’s code. This ensures that any variables or functions defined in the injected files are available in the global scope when your main code runs.
The Object.defineProperty is crucial here. It allows us to define navigator.clipboard as a non-writable and non-configurable property, making it behave more closely to a native API. If we just did navigator.clipboard = {...}, it could be overwritten by other scripts or even the browser later on.
This approach isn’t limited to just polyfilling browser APIs. You can use it to:
- Inject environment variables: Define global constants that are set during the build process but need to be accessible in client-side JavaScript.
- Provide custom utility functions: Make common helper functions globally available without needing to import them everywhere.
- Conditional logic at runtime: Inject code that checks certain conditions and sets up global variables accordingly, allowing for different behaviors based on the execution environment.
- Mocking for testing: Inject mock implementations of global objects or functions for easier unit testing.
The power comes from the fact that esbuild treats these injected files as part of the bundle’s execution flow, not just as external dependencies. They are processed, minified (if enabled), and concatenated into the final output, ensuring they are present and correctly scoped.
What most people don’t realize is that the order of files in the inject array matters. esbuild processes them sequentially. If one injected file depends on something defined in a previous injected file, you need to list them in the correct order. For example, if you had a polyfill for URL and then another polyfill that used URL but needed to be applied after the URL polyfill, you’d list them like inject:['url.polyfill.js', 'other.polyfill.js'].
The next hurdle you’ll likely encounter is managing more complex polyfills that might have dependencies on each other or require specific ordering.