Replacing Images
intro
In the realm of front-end development, managing dependencies has long been a challenge, particularly with JavaScript’s lack of a standardized approach to importing and exporting dependencies. To address this, bundlers like Webpack and Vite have emerged as crucial tools for consolidating your front-end code and its dependencies into a single file. Esbuild, a relatively new entrant in the bundler landscape, has garnered attention for its exceptional speed and performance. In this article, we’ll explore Esbuild’s capabilities and delve into the process of creating a custom plugin to handle image bundling.
Initial setup
Before embarking on our journey, let’s establish the groundwork. To illustrate the fundamental aspects of Esbuild, we’ll dive into the process of bundling a React application. For this endeavor, we’ll utilize a very simple setup of React and npm, a powerful tool that ensures the seamless management of our project’s dependencies.
import React from 'react';
import totodile from './crocodilo.png';
// We're going to turn totodile up here to into a lugia.
function App() {
return (
<div className="App">
<header className="App-header">
<img src={totodile} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
</header>
</div>
);
}
export default App;
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
With this we already have pretty much all of the React needed to be written in order to make our first simple bundle.
Setting up Esbuild:
To begin setting up our bundler and creating a custom plugin, we’ll first need to install Esbuild into our Node project. Open your command-line interface (CLI) and navigate to the directory containing your project. Once there, execute the following command:
npm install --save-dev esbuild
Now that we have that installed lets take a look at the main method we’ll be configuring and using to bundle our react app!
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['./index.js'],
bundle: true,
outdir: 'bundle/js',
loader:{
".js": "jsx"
}
plugins: [],
});
The .build
method, provided by Esbuild, serves as the primary means of interacting with Esbuild. It offers a range of parameters, but we’ll focus on those used in the code snippet above. For a more comprehensive overview, refer to the Esbuild documentation here.
Within our .build
invocation, we’ve specified two key parameters:
-
entryPoints
: This parameter indicates the location where Esbuild should begin the bundling process. In our case, we’ve specifiedindex.js
, which contains ourroot.render
function responsible for rendering React components to the DOM. -
bundle
: Settingbundle
totrue
instructs Esbuild to bundle the specifiedentryPoints
.
Additionally, we’ve included three other parameters:
-
outdir
: This parameter defines the directory where the bundled code should be placed. -
loader
: This parameter informs Esbuild how to handle specific file types. -
plugins
: This parameter, takes an array of functions which enables the extensibility of Esbuild. It’s where we’ll introduce our custom plugin later in the process.
Esbuild also offers a collection of community-developed plugins available for installation through npm
. You can find them on the Esbuild GitHub repository (https://github.com/esbuild/community-plugins).
Our first bundle.
So before we get to bundling our react well need a just a couple things to make running this a bit smoother. In our package.json let add "build": "node ./customBundler/build.js",
to the scripts object, after that make sure you have an image to use, in the code snippets above you can see that I’m using a picture of Totodile. In addition, In order to actually render our react we’ll need to have a file, index.html
within our output directory which contains a script tag pointing to the index.js
file generated by Esbuild.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="our first bundle app!"
/>
<link rel="apple-touch-icon" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
<script src="./js/index.js"></script>
</html>
Once we build the application our script tag should be able to load the contents of index.js
within our js folder. Now lets run our first build!
Uh oh 👀 what happened?
Well thankfully Esbuild provides some great logging. As we can see here we don’t have a loader configured to handle the .png file type so lets enable that next!
Going back into build.js
, were going to add the .png
loader into our build method.
await esbuild.build({
entryPoints: ['./index.js'],
bundle: true,
outdir: 'bundle/js',
loader:{
".js": "jsx",
".png": "dataurl"
}
plugins: [],
});
Now lets try and build again.
And success, our first bundle is now complete!
Our First Plugin
Now that we have gotten Esbuild to produce bundles for us it is time to learn a bit about writing plugins for it! In this coming section we’ll write a custom plugin that will allow us to on build, replace any and all esm imports of .png's
in our react code with an image of our choosing. In this case Ill be using an image of lugia. In Esbuild plugins are objects containing a name property and a function.
const customPlugin = {
name: 'customPLugin',
setup(build) {
build.onLoad({ filter: /\.png$/ }, async (args) => {
return;
})
}
};
Within our setup function is where we will be telling Esbuild to perform certain actions. The onLoad method will be called for each path/namespace pair and in the snippet above it will stop each time it finds a path with a .png
extension. Let’s add some logic here to return a png of lugia instead of totodile.
const customPlugin = {
name: 'customPLugin',
setup(build) {
build.onLoad({ filter: /\.png$/ }, async (args) => {
const encoding = await fs.promises.readFile('custom-bundler/lugia.png', { encoding: 'base64' }).catch((err) => {
console.log(err)
});
const jsContents = `data:image/png;base64,${encoding}`
return{
contents: jsContents,
loader:'text'
}
});
}
};
Here we are reading the contents of the lugia image in base64 encoding appending it to data:image/png;base64,
creating a dataurl writing over the original import of the totodile image. Now with our custom plugin complete all we have to do is pass it to the plugins array within our Esbuild configuration and rebuild.
await esbuild.build({
entryPoints: ['./index.js'],
bundle: true,
outdir: 'www/js',
loader: {
".js": "jsx",
},
plugins: [customPlugin]
});
Now after re building and refreshing the page we are left with the legendary bird!
I hope creating your first esbuild plugin has been an enlightening journey into the world of JavaScript bundling and transformation.
Even more incredible is being able to evolve Totodile, into Lugia 😀.
Throughout this guide, we’ve gotten a taste of the essentials for building esbuild plugins, from understanding the plugin structure to implementing the custom hook onLoad.
We’ve seen how esbuild hooks allow us to manipulate and extend the build process, making esbuild a highly flexible and powerful tool for modern web development