Build a library with esbuild (vol. 2)

Photo by Joseph Greve on Unsplash

​​A year ago I shared a post ​that explains how to build a library with esbuild. While it remains a valid solution, I developed some improvements in my tooling since its publication.
Here are these few add-ons that I hope, will be useful for your projects too.

​Source and output
It can be sometimes useful to define more than one entry point for the library - i.e. not just use a unique ​index.ts​ file as entry but multiple sources that provides logically-independent groups of code. esbuild supports such option through the parameter entryPoints.
For example, in my projects, I often list all the TypeScript files present in my ​src​ folder and use these as separate entries.
import { readdirSync, statSync } from "fs"; import { join } from "path"; // Select all typescript files of src directory as entry points const entryPoints = readdirSync(join(process.cwd(), "src")) .filter( (file) => file.endsWith(".ts") && statSync(join(process.cwd(), "src", file)).isFile() ) .map((file) => `src/${file}`);
​As the output folder before each build might have been deleted, I also like to ensure it exists by creating it before proceeding.
import { existsSync, mkdirSync } from "fs"; import { join } from "path"; // Create dist before build if not exist const dist = join(process.cwd(), "dist"); if (!existsSync(dist)) { mkdirSync(dist); } // Select entryPoints and build

​Global is not defined

​Your library might use some dependency that leads to a build error "Uncaught ReferenceError: global is not defined" when building ESM target. Root cause being the dependency expecting a ​global​ object (as in NodeJS) while you would need ​window​ for the browser.
To overcome the issue, esbuild has a define option that can be use to replace global identifiers with constant expression.
import esbuild from "esbuild"; esbuild .build({ entryPoints, outdir: "dist/esm", bundle: true, sourcemap: true, minify: true, splitting: true, format: "esm", define: { global: "window" }, target: ["esnext"], }) .catch(() => process.exit(1));

​Both esm and cjs

To ship a library that supports both CommonJS (cjs) and ECMAScript module (esm), I output the bundles in two sub-folders of the distribution directory - e.g. ​dist/cjs​ and ​dist/esm​. With esbuild, this can be achieved by specifying the options outdir or outfile to these relative paths.
import esbuild from "esbuild"; import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync, } from "fs"; import { join } from "path"; const dist = join(process.cwd(), "dist"); if (!existsSync(dist)) { mkdirSync(dist); } const entryPoints = readdirSync(join(process.cwd(), "src")) .filter( (file) => !file.endsWith(".ts") && statSync(join(process.cwd(), "src", file)).isFile() ) .map((file) => `src/${file}`); // esm output bundles with code splitting esbuild .build({ entryPoints, outdir: "dist/esm", bundle: true, sourcemap: true, minify: true, splitting: true, format: "esm", define: { global: "window" }, target: ["esnext"], }) .catch(() => process.exit(1)); // cjs output bundle esbuild .build({ entryPoints: ["src/index.ts"], outfile: "dist/cjs/index.cjs.js", bundle: true, sourcemap: true, minify: true, platform: "node", target: ["node16"], }) .catch(() => process.exit(1)); // an entry file for cjs at the root of the bundle writeFileSync(join(dist, "index.js"), "export * from './esm/index.js';"); // an entry file for esm at the root of the bundle writeFileSync( join(dist, "index.cjs.js"), "module.exports = require('./cjs/index.cjs.js');" );
​​As distributing two distinct folders leads to having no more entry files in the ​dist path of the library, I also like to add two files that re-export the code. It can be useful when importing the library in a project.
In addition, the ​package.json​ entries should be updated accordingly as well.
{ "name": "mylibary", "version": "0.0.1", "main": "dist/cjs/index.cjs.js", "module": "dist/esm/index.js", "types": "dist/types/index.d.ts", }

​CSS and SASS

Did you know that ​esbuild can bundle CSS files too? There is even a SASS plugin that makes it easy to build ​.scss​ files 😃.
npm i -D esbuild-sass-plugin postcss autoprefixer postcss-preset-env
​In following example, I bundle two different SASS files - ​src/index.scss​ and ​src/doc/index.scss​. I use the plugin to transform the code - i.e. to prefix the CSS - and I also use the option metafile which tells esbuild to produce some metadata about the build in JSON format.
Using it, I can retrieve the paths and names of the generated CSS files to e.g. include these in my HTML files later on.
import esbuild from 'esbuild'; import {sassPlugin} from 'esbuild-sass-plugin'; import postcss from 'postcss'; import autoprefixer from 'autoprefixer'; import postcssPresetEnv from 'postcss-preset-env'; const buildCSS = async () => { const {metafile} = await esbuild.build({ entryPoints: ['src/index.scss', 'src/doc/index.scss'], bundle: true, minify: true, format: 'esm', target: ['esnext'], outdir: 'dist/build', metafile: true, plugins: [ sassPlugin({ async transform(source, resolveDir) { const {css} = await postcss([autoprefixer, postcssPresetEnv() ]).process(source, { from: undefined }); return css; } }) ] }); const {outputs} = metafile; return Object.keys(outputs); };

​Conclusion

esbuild is still slick!
To infinity and beyond
David​

For more adventures, follow me on Twitter
Made with Papyrs