Featured image of post Getting started with esbuild

Getting started with esbuild

Overview

Bundling JavaScript applications takes time and can be complicated. A single bundling process doesn’t take a lot of time, but in your development process, the bundling processes add up and they can add a significant delay to your development pipeline.

On top of that, bundling a JavaScript application usually requires you to write a configuration file. If you bundle a JavaScript application using webpack, you need to write webpack.config.js. It’s a significant cognitive overhead.

That’s where esbuild comes in. Esbuild is a fast and simple JavaScript bundler written in Go.

In this article, you’ll learn how to use esbuild to bundle JavaScript applications. You’ll explore common use cases, from bundling TypeScript , React, image files, and CSS files to serving the bundling process as a server { 将打包过程作为服务器提供服务 }.

Installing esbuild

First, install the bundler using npm:

1
$ npm install -g esbuild

Then you can verify the installation by invoking esbuild:

1
$ esbuild --version 0.13.12

If you don’t want to install esbuild globally, you can do that as well:

1
$ npm install esbuild

But you have to invoke esbuild with a full path:

1
$ ./node_modules/.bin/esbuild --version 0.13.12

Bundling TypeScript with esbuild

The first task you’re going to accomplish using esbuild is bundling a TypeScript file. Create a file named input_typescript.ts and add the following code to it:

1
let message: string = "Hello, esbuild!"; console.log(message);

You can bundle the TypeScript code via CLI:

1
2
3
$ esbuild input_typescript.ts --outfile=output.js --bundle --loader:.ts=ts
output.js 99b
⚡ Done in 7ms

Then, check the content of the bundled file like so:

1
2
3
4
5
(() => {
  // input_typescript.ts
  var message = "Hello, esbuild!";
  console.log(message);
})();

The esbuild command accepts input_typescript.ts as the argument. We’ll refer to this argument as the entry point, because it’s where the application starts.

Then, provide the outfile option as a way to define the output file. If you don’t provide this option, esbuild will send the result to stdout. The loader option is the one that you use to load the TypeScript file extension. You can omit this option, however, because esbuild can decide which loader to use based on the file extension.

With the bundle option, esbuild will inline all dependencies into the output file. Let’s look at a demo to see the difference.

Suppose you have a file named main.ts with the content as follows:

1
2
import { SayHello } from "./library";
SayHello();

The main.ts file imports SayHello from library.ts which has the content as below:

1
2
3
export function SayHello() {
  console.log("Hello, esbuild!");
}

If you don’t use the bundle option, esbuild will just import the dependency in the result:

1
2
3
$ esbuild main.ts
import { SayHello } from "./library";
SayHello();

But if you used the bundle option, esbuild would inline the content of the library in the result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ esbuild main.ts --bundle
(() => {
  // library.ts
  function SayHello() {
    console.log("Hello, esbuild!");
  }

  // main.ts
  SayHello();
})();

With the bundle option, you pack all your code into one file. In other words, two files become one file.

Bundling React with esbuild

Integrating React library into your project is a complicated venture. It even warrants the creation of a Create React App project . If you want to use webpack to add React into your project, you have to endure { 忍受 } the writing process of a complicated webpack.config.js .

But with esbuild, it’s a simple process.

First, install the React library using npm:

1
$ npm install react react-dom

Then create a JavaScript file called App.js. Add the following code to the file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import React from "react";
import ReactDOM from "react-dom";

function App() {
  return (
    <div>Hello, esbuild!</div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

Create an HTML file called index.html so React can render your application into the div with an ID root. Add the following code to the file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>Hello, esbuild!</title>
</head>

<body>
  <div id="root"></div>
  <script src="AppBundle.js"></script>
</body>

</html>

In the HTML file, we are using AppBundle.js. This is the name of the bundled JavaScript file.

Now, bundle App.js to AppBundle.js:

1
2
3
$ esbuild App.js --bundle --outfile=AppBundle.js --loader:.js=jsx
AppBundle.js  890.8kb
⚡ Done in 46ms

You’ve learned all the options in the previous section. You use the bundle option because, well, you want to bundle the JavaScript file. Then, give the output file the name you want using the outfile option.

The last option, loader, is not actually optional. Tell esbuild to use the JSX loader for files with the .js extension, because JSX syntax is inside App.js. If you don’t use the JSX loader, esbuild will throw an error. You can omit the loader option if the extension of the input file is .jsx, not .js. So if you name the JavaScript file App.jsx, then you can omit the loader option.

Now that you have AppBundle.js, let’s open index.html to check whether your bundling process works or not. You must open index.html using the http protocol, not the file protocol.

Then, you can serve the HTML file using http-server:

1
$ npx http-server

Finally, open http://localhost:8080/index.html . You should see the screen below:

The React app bundled with esbuild

Using the build API

While you can bundle your JavaScript file with CLI, you also have an option to use the build API .

Suppose you want to bundle input_typescript.ts into output.js. This is the command you would use:

1
$ esbuild input_typescript.ts --outfile=output.js --bundle --loader:.ts=ts

Let’s try the build API. Write a JavaScript file called build.js and add the following code:

1
2
3
4
5
6
7
8
require("esbuild").build({
  entryPoints: ["input_typescript.ts"],
  outfile: "output.js",
  bundle: true,
  loader: {".ts": "ts"}
})
.then(() => console.log("⚡ Done"))
.catch(() => process.exit(1));

Import the esbuild library and use the build method from it. The argument is an object that has keys and values similar to the options in the esbuild command.

Then you can execute the bundling process with Node.js:

1
2
$ node build.js
⚡ Done

You can treat the build file as a configuration file. It’s like webpack.config.js, but for esbuild.

Bundling CSS with esbuild

Let’s try bundling something else, such as CSS files. Create a CSS file named color.css and add the following code to it:

1
2
3
.beautiful {
  color: rgb(0,0,255);
}

Then, create another CSS file that imports the CSS file above. Name it style.css and add the following code to it:

1
2
3
4
5
@import 'color.css';

p {
  font-weight: bold;
}

To bundle these two CSS files, you can use esbuild as shown below:

1
2
3
4
5
$ esbuild style.css --outfile=out.css --bundle

  out.css  100b

⚡ Done in 7ms

The content of out.css will be the combination of the two CSS files:

1
2
3
4
5
6
7
8
9
/* color.css */
.beautiful {
  color: rgb(0, 0, 255);
}

/* style.css */
p {
  font-weight: bold;
}

Now, you can include only this one file in your HTML file.

You can also minify the CSS file using the minify option:

1
2
3
4
5
$ esbuild style.css --outfile=out.css --bundle --minify

  out.css  42b

⚡ Done in 3ms

The content of the CSS file will be compact, as shown below:

1
beautiful{color:#00f}p{font-weight:bold}

As you can see, the bundler even changed the way you specify the color. The input file uses the rgb syntax, but the output file uses hexadecimal code, which is more compact.

Bundling images

You can also bundle images with esbuild. You have two options for bundling images: the first is to load the image as an external file in the JavaScript file, and the second is to embed the image as a Base64-encoded data URL in a JavaScript file.

Let’s look at the difference. First, put one JPG file and one PNG file into the project directory. You need two images with different extensions because you want to load both images in different ways. Name the PNG image image.png and the JPG image image.jpg.

Create an HTML file named images.html and add the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>Hello, esbuild!</title>
</head>

<body>
  <div id="root">
    <div>
      <img id="image_png" />
    </div>
    <div>
      <img id="image_jpg" />
    </div>
  </div>
  <script src="out_image.js"></script>
</body>

</html>

Then, you need to create a JavaScript file. Name it input_image.js and add the following code:

1
2
3
4
5
6
7
import png_url from './image.png'
const png_image = document.getElementById("image_png");
png_image.src = png_url;

import jpg_url from './image.jpg'
const jpg_image = document.getElementById("image_jpg");
jpg_image.src = jpg_url

Next, load the image using the import statement inside the JavaScript file. Unlike bundling CSS files, you don’t bundle images directly, but bundle images by bundling the JavaScript files that refer to the images.

Now, bundle the JavaScript files:

1
2
3
4
5
6
$ esbuild input_image.js --bundle --loader:.png=dataurl --loader:.jpg=file --outfile=out_image.js

  out_image.js        20.1kb
  image-UKQOKISI.jpg  10.1kb

⚡ Done in 11ms

Notice that you used two loaders. The .png extension uses the dataurl loader and the .jpg extension uses the file loader. Instead of image-UKQOKISI.jpg, you will get a different name.

If you peek inside out_image.js, you’ll see the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(() => {
  // image.png
  var image_default = "data:image/png;base64,iVBORw0KGgoAAAANSU..."

  // image.jpg
  var image_default2 = "./image-UKQOKISI.jpg";

  // input_image.js
  var png_image = document.getElementById("image_png");
  png_image.src = image_default;
  var jpg_image = document.getElementById("image_jpg");
  jpg_image.src = image_default2;
})();

As you can see, the first image uses a Based64-encoded data URL format. The second image uses the file path format. For the second image, you also have an external file called image-UKQOKISI.jpg.

You can check the images by opening images.html:

1
$ npx http-server

Open http://localhost:8080/images.html and you would get the following screen:

The HTML file loads images

Using Plugin

Esbuild is not a complete solution for bundling. It has default supports for React, CSS, and images, but it doesn’t support SASS. If you want to bundle SASS files, you need to install an esbuild plugin. The list of the esbuild plugins can be found here .

There are a couple of plugins that bundle SASS files. In this tutorial, you’ll use esbuild-plugin-sass. Install the plugin using npm like so:

1
$ npm install esbuild-plugin-sass

Let’s create an SCSS file named style.scss. Add the following content to it:

1
2
3
4
5
6
7
$font: Roboto;
$color: rgb(0, 0, 255);

#root {
  font: 1.2em $font;
  color: $color;
}

To use the esbuild-plugin-sass plugin, you need to use the build API. Create a file called sass_build.js and add the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const sassPlugin = require("esbuild-plugin-sass");

require("esbuild").build({
  entryPoints: ["style.scss"],
  outfile: "bundle.css",
  bundle: true,
  plugins: [sassPlugin()]
})
.then(() => console.log("⚡ Done"))
.catch(() => process.exit(1));

Notice that you use the plugin using the plugins key. The entry is the SCSS file, but you can also fill the entry with the JavaScript file, which imports the SCSS file. The output is the CSS file.

Execute this build file:

1
2
$ node sass_build.js
⚡ Done

You can check the result by opening the bundle.css file:

1
2
3
4
5
/* ../../../../../../tmp/tmp-234680-cl7EYSZ4C0qM/esbuild_demo/style.css */
#root {
  font: 1.2em Roboto;
  color: blue;
}

Watch Mode

It’s not fun to execute the bundling process every time you modify the input file. There should be a way to bundle the input files automatically. For this case, esbuild has the watch mode.

Create a file called watch_build.js and add the following content:

1
2
3
4
5
6
7
8
9
require("esbuild").build({
  entryPoints: ["input_typescript.ts"],
  outfile: "output.js",
  bundle: true,
  loader: {".ts": "ts"},
  watch: true
})
.then(() => console.log("⚡ Done"))
.catch(() => process.exit(1));

The input_typescript.ts file is the same as the previous example. This is the content of the file:

1
2
let message: string = "Hello, esbuild!";
console.log(message);

Execute the build file like so:

1
2
$ node watch_build.js
⚡ Done

The process hangs up. Check the content of output.js:

1
2
3
4
5
(() => {
  // input_typescript.ts
  var message = "Hello, esbuild!";
  console.log(message);
})();

While the build process is still alive, change the content of input_typescript.ts to the content shown below:

1
2
3
4
let message: string = "Hello, esbuild!";
let x: number = 3;
console.log(message);
console.log(x);

Finally, check the content of output.js again:

1
2
3
4
5
6
7
(() => {
  // input_typescript.ts
  var message = "Hello, esbuild!";
  var x = 3;
  console.log(message);
  console.log(x);
})();

The output file is updated automatically. watch watches the file system so esbuild can bundle the input files when it detects that the file changes.

Serve mode

There is another way to bundle files automatically called serve mode. It means that you launch a server to serve { 提供 } the output file. If someone requests the output file from the browser, the server will bundle the input files automatically if the files have been changed.

Let’s create an HTML file called index_ts.html and add the following code to it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>Hello, esbuild!</title>
</head>

<body>
  <script src="output.js"></script>
</body>

</html>

The output file is output.js, and the user requests it indirectly by accessing index_ts.html. The input file is the same as before, input_typescript.ts. The content of the file is as follows:

1
2
let message: string = "Hello, esbuild!";
console.log(message);

This is how you bundle the file using the serve mode:

1
2
3
$ esbuild input_typescript.ts --outfile=output.js --bundle --loader:.ts=ts --serve=localhost:8000 --servedir=.

 > Local: http://127.0.0.1:8000/

The serve option is used to define the server and the port. The servedir option defines the directory the server serves.

Now, open http://127.0.0.1/8000/index_ts.html and check the console:

The bundler in serve mode

Modify input_typescript.ts into the following code:

1
2
3
4
let message: string = "Hello, esbuild!";
let x: number = 5;
console.log(message);
console.log(x);

Now, refresh the browser or open http://127.0.0.1/8000/index_ts.html again. You will see the following screen:

The bundler in the serve mode

As you can see, the bundling process happened automatically.

Conclusion

In this article, you’ve learned how to use esbuild to bundle TypeScript, React, CSS, image files, and SCSS files. You used esbuild tool via CLI and the build API. You executed esbuild with different options according to your needs.

This article only scratches the surface of esbuild { 本文仅涉及 esbuild 的皮毛 }. There are many sides of esbuild that we haven’t covered, such as using sourcemap, injecting functions, and naming the assets. Please check the documentation to learn more. The code for this article is available on this GitHub repository .

Reference

Licensed under CC BY-NC-SA 4.0
Last updated on May 05, 2023 20:26 CST
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy