Original link: https://blog.rxliuli.com/p/ba9b341a8792405fb86d8fe02a18adfc/
foreword
esbuild is a general-purpose code compiler and build tool, built with golang, it is very fast, and its performance is 1~2 orders of magnitude higher than the existing js toolchain. It’s not currently an out-of-the-box build tool, but with its plugin system, we can already do many things.
Automatically exclude all dependencies
When building lib, we usually don’t want to bundle dependent modules, and hope to exclude all dependencies by default. This plugin is used to achieve this function. It will make all imported modules that do not start with .
external, avoiding bundling into the final build.
import { Plugin } from "esbuild" ; /** * 自动排除所有依赖项* golang 不支持js 的一些正则表达式语法,参考https://github.com/evanw/esbuild/issues/1634 */ export function autoExternal ( ) : Plugin { return { name : "autoExternal" , setup ( build ) { build . onResolve ( { filter : / .* / } , ( args ) => { if ( / ^\.{1,2}\/ / . test ( args . path ) ) { return ; } return { path : args . path , external : true , } ; } ) ; } , } ; }
We can use it like import esbuild, but it won’t be bundled in.
import { build } from "esbuild" ; console . log ( build ) ;
will be compiled into
import { build } from "esbuild" ; console . log ( build ) ;
Use environment variables
Sometimes we need to use environment variables to distinguish between different environments, and this can be done very simply by using plugins.
import { Plugin } from "esbuild" ; /** * @param {string} str */ function isValidId ( str : string ) { try { new Function ( ` var ${ str } ; ` ) ; } catch ( err ) { return false ; } return true ; } /** * Create a map of replacements for environment variables. * @return A map of variables. */ export function defineProcessEnv ( ) { /** * @type */ const definitions : Record < string , string > = { } ; definitions [ "process.env.NODE_ENV" ] = JSON . stringify ( process . env . NODE_ENV || "development" ) ; Object . keys ( process . env ) . forEach ( ( key ) => { if ( isValidId ( key ) ) { definitions [ ` process.env. ${ key } ` ] = JSON . stringify ( process . env [ key ] ) ; } } ) ; definitions [ "process.env" ] = "{}" ; return definitions ; } export function defineImportEnv ( ) { const definitions : Record < string , string > = { } ; Object . keys ( process . env ) . forEach ( ( key ) => { if ( isValidId ( key ) ) { definitions [ ` import.meta.env. ${ key } ` ] = JSON . stringify ( process . env [ key ] ) ; } } ) ; definitions [ "import.meta.env" ] = "{}" ; return definitions ; } /** * Pass environment variables to esbuild. * @return An esbuild plugin. */ export function env ( options : { process ? : boolean ; import ? : boolean } ) : Plugin { return { name : "env" , setup ( build ) { const { platform , define = { } } = build . initialOptions ; if ( platform === "node" ) { return ; } build . initialOptions . define = define ; if ( options . import ) { Object . assign ( build . initialOptions . define , defineImportEnv ( ) ) ; } if ( options . process ) { Object . assign ( build . initialOptions . define , defineProcessEnv ( ) ) ; } } , } ; }
After using the plugin, we can use environment variables in our code
export const NodeEnv = import . meta . env . NODE_ENV ;
compile result
export const NodeEnv = "test" ;
output log at build time
Sometimes we want to build something in watch mode, but esbuild doesn’t output a message after the build, we simply implement one.
import { Plugin , PluginBuild } from "esbuild" ; export function log ( ) : Plugin { return { name : "log" , setup ( builder : PluginBuild ) { let start : number ; builder . onStart ( ( ) => { start = Date . now ( ) ; } ) ; builder . onEnd ( ( result ) => { if ( result . errors . length !== 0 ) { console . error ( "build failed" , result . errors ) ; return ; } console . log ( ` build complete, time ${ Date . now ( ) - start } ms ` ) ; } ) ; } , } ; }
we can test that it works
const mockLog = jest . fn ( ) ; jest . spyOn ( global . console , "log" ) . mockImplementation ( mockLog ) ; await build ( { stdin : { contents : ` export const name = 'liuli' ` , } , plugins : [ log ( ) ] , write : false , } ) ; expect ( mockLog . mock . calls . length ) . toBe ( 1 ) ;
Automatically exclude dependencies starting with node:
Sometimes some dependent modules use the native modules of nodejs, but the writing method starts with node:
which will cause esbuild to not recognize it. We use the following plugins to deal with it
import { Plugin } from "esbuild" ; /** * 排除和替换node 内置模块*/ export function nodeExternal ( ) : Plugin { return { name : "nodeExternals" , setup ( build ) { build . onResolve ( { filter : / (^node:) / } , ( args ) => ( { path : args . path . slice ( 5 ) , external : true , } ) ) ; } , } ; }
Native modules starting with node: in our code below will be excluded
import { path } from "node:path" ; console . log ( path . resolve ( __dirname ) ) ;
compile result
// <stdin> import { path } from "path" ; console . log ( path . resolve ( __dirname ) ) ;
Binding text files via ?raw
If you have used vite, you may be impressed by its ?*
feature, it provides a variety of functions to import files in different ways, and in esbuild, we sometimes want to statically bundle some content, For example readme files.
import { Plugin } from "esbuild" ; import { readFile } from "fs-extra" ; import * as path from "path" ; /** * 通过?raw 将资源作为字符串打包进来* @returns */ export function raw ( ) : Plugin { return { name : "raw" , setup ( build ) { build . onResolve ( { filter : / \?raw$ / } , ( args ) => { return { path : path . isAbsolute ( args . path ) ? args . path : path . join ( args . resolveDir , args . path ) , namespace : "raw-loader" , } ; } ) ; build . onLoad ( { filter : / \?raw$ / , namespace : "raw-loader" } , async ( args ) => { return { contents : await readFile ( args . path . replace ( / \?raw$ / , "" ) ) , loader : "text" , } ; } ) ; } , } ; }
Verify by
const res = await build ( { stdin : { contents : ` import readme from '../../README.md?raw' console.log(readme) ` , resolveDir : __dirname , } , plugins : [ raw ( ) ] , bundle : true , write : false , } ) ; console . log ( res . outputFiles [ 0 ] . text ) ; expect ( res . outputFiles [ 0 ] . text . includes ( "@liuli-util/esbuild-plugins" ) ) . toBeTruthy ( ) ;
rewrite some modules
Sometimes we want to rewrite some modules, such as changing the imported lodash to lodash-es to achieve tree shaking, at this time we can do this with the following plugins
import { build , Plugin } from "esbuild" ; import path from "path" ; /** * 将指定的import 重写为另一个* @param entries * @returns */ export function resolve ( entries : [ from : string , to : string ] [ ] ) : Plugin { return { name : "resolve" , setup ( build ) { build . onResolve ( { filter : / .* / } , async ( args ) => { const findEntries = entries . find ( ( item ) => item [ 0 ] === args . path ) ; if ( ! findEntries ) { return ; } return await build . resolve ( findEntries [ 1 ] ) ; } ) ; } , } ; }
We can replace lodash with lodash-es using the following configuration
build ( { plugins : [ resolve ( [ [ "lodash" , "lodash-es" ] ] ) ] , } ) ;
source code
import { uniq } from "lodash" ; console . log ( uniq ( [ 1 , 2 , 1 ] ) ) ;
compile result
import { uniq } from "lodash-es" ; console . log ( uniq ( [ 1 , 2 , 1 ] ) ) ;
Forcing the specified module to have no side effects
When we use a third-party package, it is possible that the package depends on some other modules. If the module does not declare sideEffect
, then even if it has no side effects and exports the esm package, the dependent module will be bundled in, But we can use the plugin api to force the specified module to have no side effects.
import { Plugin } from "esbuild" ; /** * 设置指定模块为没有副作用的包,由于webpack/esbuild 的配置不兼容,所以先使用插件来完成这件事* @param packages * @returns */ export function sideEffects ( packages : string [ ] ) : Plugin { return { name : "sideEffects" , setup ( build ) { build . onResolve ( { filter : / .* / } , async ( args ) => { if ( args . pluginData || // Ignore this if we called ourselves ! packages . includes ( args . path ) ) { return ; } const { path , ... rest } = args ; rest . pluginData = true ; // Avoid infinite recursion const result = await build . resolve ( path , rest ) ; result . sideEffects = false ; return result ; } ) ; } , } ; }
We use it as follows
build ( { plugins : [ sideEffects ( [ "lib" ] ) ] , } ) ;
At this time, even if some code in lib-a depends on lib-b, as long as your code does not depend on a specific method, it will be properly tree shaking
For example the following code
// main.ts import { hello } from "lib-a" ; console . log ( hello ( "liuli" ) ) ;
// lib-a/src/index.ts export * from "lib-b" ; export function hello ( name : string ) { return ` hello ${ name } ` ; }
compile result
// dist/main.js function hello ( name : string ) { return ` hello ${ name } ` ; } console . log ( hello ( "liuli" ) ) ;
Summarize
At present, many plug-ins of esbuild have been implemented, but it is still weak as a basic construction tool for building applications. Currently, it is only recommended to use it to build some pure JavaScript/TypeScript code. If you need to build a complete web application, then vite may be the most mature build tool based on esbuild.
This article is reproduced from: https://blog.rxliuli.com/p/ba9b341a8792405fb86d8fe02a18adfc/
This site is for inclusion only, and the copyright belongs to the original author.