In Vite ESbuild

A lot of Vite uses ESbuild, for example

  • translationtsType configuration file
  • requestts,jsx,tsxFile when it is compiled intojsfile
  • Automatically searches the list of precompiled modules
  • Precompiled module

So it is necessary to learn ESbuild before looking at Vite source code

Why is ESbuild fast

  1. Js is single-threaded serial, ESbuild is a new process, and then multi-threaded parallel, give full play to the advantages of multi-core
    • Generating the final file and generating the Source maps are all parallelized
  2. Go compiles directly to machine code and is certainly faster than JIT
  3. The construction process is optimized to make full use of CPU resources

What are the disadvantages of ESbuild

  • ESbuild does not support conversion from ES6 + to ES5.Refer to JavaScript notes. To ensure efficient compilation of esbuilds,ESbuild does not provide the operation capability of AST. So some babel-plugins that process code through AST don’t have a good way to transition into ESbuild. Such asbabel-plugin-import
  • Important functionality for building applications is still under continuous development — in particularThe code segmentandCSSIn terms of
  • The ESbuild community is a bit different than the Webpack community

How to determine if ESbuild can be used in the current project

  1. Not using some custom babel-plugin (e.gbabel-plugin-import)
  2. No need to be compatible with older browsers (ESbuild can only convert code to ES6)

For Vite, ESbuild is used for prebuild and file compilation in the development environment, while Rollup is used in the production environment. This is because some of ESbuild’s most important features for building applications are still under continuous development — particularly code splitting and CSS handling. For now, Rollup is more mature and flexible when it comes to application packaging.

ESbuild use

ESbuild can be used in command line, JS call, and go call.

The command line

# import file esbuild index.js
# --outfile output file
# --define:TEST=12
# --format= CJS compiled module specification
# --bundle bundles third-party libraries together
# --platform=[node/browser] specifies the compiled runtime environment
# --target=esnext
PNG = dataURL Converts PNG to base64 and needs to be used with --bundle
Copy the code

JavaScript way

ESbuild throws three apis, which are

  • transform API
  • build API
  • service

transform API

Transform /transformSync Operates on a single string without accessing the file system. Ideal for use in environments without file systems or as part of another tool chain, it provides two parameters:

transformSync(str: string, options? : Config): Result transform(str: string, options? : Config): Promise<Result>Copy the code
  1. str: A string (mandatory), indicating the code to be converted
  2. options: Configuration item (optional), which is required for transformation

Config Refer to the official website for details about the configuration

interface Config {
  define: object # Keyword substitution
  format: string Output specification (IIFE/CJS/ESM)
  loader: string | object # transform API can only use string
  minify: boolean Zip code, including removing whitespace, renaming variables, and modifying syntax to make syntax more concise
  # Configure the above functionality separately in the following way
  minifyWhitespace: boolean # delete space
  minifyIdentifiers: boolean Rename variables
  minifySyntax: boolean Modify the syntax to make the syntax more concise
  sourcemap: boolean | string
  target: string[] # Set the target environment, default is ESNext (using the latest ES features)
}
Copy the code

The return value:

  • Synchronous method (transformSync) returns an object
  • Asynchronous methods (transform) returns the valuePromiseobject
interface Result {
	warnings: string[] # Warning message
	code: string # Compiled code
	map: string # source map
}
Copy the code

For example,

require('esbuild').transformSync('let x: number = 1', {
    loader: 'ts',})//   =>
/ / {
// code: 'let x = 1; \n',
// map: '',
// warnings: []
/ /}
Copy the code

build API

Build API calls operate on one or more files in the file system. This allows files to reference each other and be compiled together (bundle: true required)

buildSync(options? : Config): Result build(options? : Config): Promise<Result>Copy the code
  • options: Configuration item (optional), which is required for transformation

Config Refer to the official website for details about the configuration

interface Config {
  bundle: boolean Package all the source code together
  entryPoints: string[] | object # entry file, through the object can specify the output file name, similar to Webpack
  outdir: string The output folder cannot be used at the same time as outfile. Use outdir for multi-entry files
  outfile: string The output filename,, cannot be used with outdir; Single-entry files use outfile
  outbase: string # each entry file is built to a different directory
  define: object # define = {K: V} replace K with V when parsing code
  platform: string # specify the output environment, default is browser and a value is node,
  format: string # js output specification (iIFE/CJS/ESM), if platform is browser, default to IIFE; If platform is Node, the default is CJS
  splitting: boolean # Code split (currently esM mode only)
  loader: string | object # transform API can only use string
  minify: boolean Zip code, including removing whitespace, renaming variables, and modifying syntax to make syntax more concise
  # Configure the above functionality separately in the following way
  minifyWhitespace: boolean # delete space
  minifyIdentifiers: boolean Rename variables
  minifySyntax: boolean Modify the syntax to make the syntax more concise
  sourcemap: boolean | string
  target: string[] # Set the target environment, default is ESNext (using the latest ES features)
  jsxFactory: string # specify the function to call for each JSX element
  jsxFragment: string Specifies the function that aggregates a list of child elements
  assetNames: string # Static resource output file name (default name plus hash)
  chunkNames: string The name of the file output after the code is split
  entryNames: string # import file name
  treeShaking: string Annotations /* @__pure__ */ and package.json sideEffects properties are ignored if 'ignore-Annotations' is configured
  tsconfig: string # specify tsconfig file
  publicPath: string CDN # specified static files, such as https://www.example.com/v1 () takes effect for the static file set the loader to the file
  write: boolean For cli and JS apis, write to the file system by default. When set to true, write to the memory buffer
  inject: string[] Import the files from the array into all output files
  metafile: boolean Generate a dependency graph
}
Copy the code

The build return value is a Promise object

interface BuildResult { warnings: Message[] outputFiles? : OutputFile[]# output only if write is false, which is a Uint8Array
}
Copy the code

For example,

require('esbuild').build({
    entryPoints: ['index.js'].bundle: true.metafile: true.format: 'esm'.outdir: 'dist'.plugins: [],
}).then(res= > {
    console.log(res)
})
Copy the code

Commonly used configuration

outbase

outbase: string
Copy the code

When multiple entry files are in different directories, the directory structure will be copied to the output directory relative to the outbase directory

require('esbuild').buildSync({
  entryPoints: [
    'src/pages/home/index.ts'.'src/pages/about/index.ts',].bundle: true.outdir: 'out'.outbase: 'src',})Copy the code

SRC /home/index.ts; SRC /about/index.ts; SRC /home/index.ts; And set outbase to SRC, which is packaged relative to the SRC directory; Ts and out/about/index.ts respectively

bundle

Only build API is supported

bundle: boolean
Copy the code

If true, the dependency is inlined into the file itself. This process is recursive, so the dependencies of the dependencies will also be merged. By default, ESbuild does not bundle input files, which is false. Dynamic module names are not merged with the source code, as follows:

// Static imports (will be bundled by esbuild)
import 'pkg';
import('pkg');
require('pkg');

// Dynamic imports (will not be bundled by esbuild)
import(`pkg/${foo}`);
require(`pkg/${foo}`);
['pkg'].map(require);
Copy the code

If there are multiple entry files, separate files are created and dependencies are merged.

sourcemap

sourcemap: boolean | string
Copy the code
  • trueGenerated by:.js.mapAnd the generated file is added//# sourceMappingURL=
  • false: Do not use sourcemap
  • 'external'Generated by:.js.mapThe generated file is not added//# sourceMappingURL=
  • 'inline': don’t generate.js.map.source map Information is inlined to the file
  • 'both':'inline' + 'external'Mode. generate.js.mapBut the generated file information is not added//# sourceMappingURL=

define

Keyword substitution

let js = 'DEBUG && require("hooks")'
require('esbuild').transformSync(js, {
  define: { DEBUG: 'true'}})/ / {
// code: 'require("hooks"); \n',
// map: '',
// warnings: []
// }

require('esbuild').transformSync('id, str', {
    define: { id: 'text'.str: '"text"'}})/ / {
// code: 'text, "text"; \n',
// map: '',
// warnings: []
/ /}
Copy the code

Double quotes contain strings, indicating that compiled code is replaced with strings, while no double quotes contain compiled code replaced with keywords

loader

loader: string | object
Optional value # are: 'js' |' JSX '|' ts' | 'benchmark' | 'CSS' | 'json' | 'text' | 'base64' | 'file' | 'dataurl' | 'binary'
Copy the code

For example,

// Build API uses the file system. You need to use the loader based on the file name extension
require('esbuild').buildSync({
  loader: {
    '.png': 'dataurl'.'.svg': 'text',}})// The transform API does not use file systems and does not need suffix names. Only one Loader can be used because the Transform API only operates on one string
let ts = 'let x: number = 1'
require('esbuild').transformSync(ts, {
  loader: 'ts'
})
Copy the code

jsxFactory&jsxFragment

  • jsxFactory: specifies the function to call for each JSX element
  • jsxFragmentFragments allow you to aggregate a list of child elements without adding additional nodes to the DOM
require('esbuild').transformSync('<div/>', {
  jsxFactory: 'h'.// The default value is react. CreateElement, which is customizable. If you want to use Vue JSX, change this value to Vue.CreateElement
  loader: 'jsx'.// Set loader to JSX to compile JSX code
})

React.Fragment (default); Vue.Fragment (default);
require('esbuild').transformSync('<>x</>', {
  jsxFragment: 'Fragment'.loader: 'jsx',})Copy the code

If it is a TSX file, you can configure JSX for TypeScript by adding this to tsConfig. ESbuild picks it up automatically without configuration

{
  "compilerOptions": {
    "jsxFragmentFactory": "Fragment"."jsxFactory": "h"}}Copy the code

assetNames

If the loader of a static resource is set to file, the location and name of the static resource can be redefined using the subproperty

require('esbuild').buildSync({
  entryPoints: ['app.js'].assetNames: 'assets/[name]-[hash]'.loader: { '.png': 'file' }, / / must
  bundle: true.outdir: 'out',})Copy the code

If 3.png is introduced, the location of the packaged image is.png with out/assets/3-hash value

Three placeholders are provided

  • [name]: the name of the file
  • [dir]: from the directory containing static files tooutbaseThe relative path of a directory
  • [hash]Hash value: Hash value generated based on the content

chunkNames

Controls the file name of the shared code block that is automatically generated when code splitting is enabled

require('esbuild').buildSync({
  entryPoints: ['app.js'].chunkNames: 'chunks/[name]-[hash]'.bundle: true.outdir: 'out'.splitting: true./ / must
  format: 'esm'./ / must
})
Copy the code

There are two placeholders

  • [name]: the name of the file
  • [hash]Hash value: Hash value generated based on the content

Note: You do not need to include a suffix. This property can only change the file name of the code split output, not the entry file name.

Now, there is a problem that if two entry files reference the same image, code split and assetNames will package a JS file and an image file. The image file will be placed in the assetNames directory, and the JS file will be placed in the chunkNames directory. The js file internally exports the image file, as shown below

// 3.jpg
var __default = ".. /assets/3-FCRZLGZY.jpg";

export {
  __default
};
Copy the code

entryNames

Specifies the location and name of the entry file

require('esbuild').buildSync({
  entryPoints: ['src/main-app/app.js'].entryNames: '[dir]/[name]-[hash]'.outbase: 'src'.bundle: true.outdir: 'out',})Copy the code

Three placeholders are provided

  • [name]: the name of the file
  • [dir]: from the directory containing static files tooutbaseThe relative path of a directory
  • [hash]Hash value: Hash value generated based on the content

metafile

Generate dependency diagrams for the packaged files and store them in res.metafile below

  • If the configuration itembundleforfalse, the generated dependency graph contains only import files and import files in import files
  • If the configuration itembundlefortrueThe packaged files are included in the dependency diagram, as shown below
require('esbuild').build({
    entryPoints: ['index.js'].bundle: true.// Set to true
    metafile: true.format: 'esm'.outdir: 'dist',
}).then(res= > {
    console.log(res);
})

/* metafile: { "inputs": { "b.js": { "bytes": 18, "imports": [] }, "a.js": { "bytes": 54, "imports": [{ "path": "b.js", "kind": "import-statement" }] }, "index2.js": { "bytes": 146, "imports": [{ "path": "a.js", "kind": Outputs: {dist/index2.js": {"imports": [], "exports": {"imports": [], "exports": [], "entryPoint": "index2.js", "inputs": { "b.js": { "bytesInOutput": 78 }, "a.js": { "bytesInOutput": 193 }, "index2.js": { "bytesInOutput": 184 } }, "bytes": 1017 } } } */
Copy the code

If afile introduces a third-party library, the generated res.metafile will also contain the address of the third-party library. There is a plugin implemented in Vite that does not package the third-party library into the bundle, but still loads it through import

const externalizeDep = {
  name: 'externalize-deps'.setup(build) {
    // If the return value is undefined, the next onResolve registered callback will be called, otherwise it will not proceed
    build.onResolve({ filter: /. * / }, (args) = > {
      const id = args.path
      // For external modules
      if (id[0]! = ='. ' && !path.isAbsolute(id)) {
        return {
          external: true.// Set this to true to mark the module as a third-party module, which means it will not be included in the package but imported at run time}})}}Copy the code

ESbuild hot update

github

The plug-in

The plug-in API, which is part of the API calls mentioned above, allows you to inject code into various parts of the build process. Unlike the rest of the API, it is not available from the command line. You must write JavaScript or Go code to use the plug-in API.

The plugin API can only be used with the Build API, not the Transform API

If you are looking for an existing ESbuild plug-in, you should look at the list of existing ESbuild plug-ins. The plugins in this list were deliberately added by the authors to be used by others in the ESbuild community.

How to write a plug-in

An ESbuild plug-in is an object containing the name and setup functions

export default {
  name: "env".setup(build){}};Copy the code
  • name: Plug-in name
  • setupFunction: runs once each time the Build API is called
  • buildContains some hook functions
onStart # trigger at start
onResolve Run when an import path is encountered, intercept the import path
onLoad Trigger after parsing is complete
onEnd Trigger when packing is complete
Copy the code

onResolve

Run on every import path for every module that ESbuild builds. OnResolve registered callbacks can be customized to ESbuild

type Cb = (args: OnResolveArgs) = > OnResolveResult

type onResolve = ({}: OnResolveOptions, cb: Cb) = > {}
Copy the code

OnResolve registers the callback function with matching arguments and a callback that returns an object of type OnResolveResult

Let’s look at the matching parameters

interface OnResolveOptions {
  filter: RegExp;
  namespace? : string; }Copy the code
  • filterEach callback must provide a filter, which is a regular expression. When the path does not match this filter, the current callback is skipped.
  • namespace: Optional, infilterIf the module namespace is the same, the callback is triggered. Can pass the previous oneonResolveThe hook function returns, by defaultflie

The argument received by the callback function

interface OnResolveArgs {
  path: string; # import file path, as in the code import path
  importer: string; The absolute path to which the file was imported
  namespace: string; The default namespace for importing files is 'file'.
  resolveDir: string; Absolute path, the directory in which the file was imported
  kind: ResolveKind; # import mode
  pluginData: any; # Properties passed by the previous plug-in
}

type ResolveKind =
  | 'entry-point' # import file
  | 'import-statement' # ESM import
  | 'require-call'
  | 'dynamic-import' # dynamic import import ('')
  | 'require-resolve'
  | 'import-rule' # CSS @import import
  | 'url-token'
Copy the code

The value returned by the callback function

If the return value is undefined, the next onResolve registered callback will be called; otherwise, execution will not proceed.

interface OnResolveResult { errors? : Message[]; external? : boolean;# Set this to true to mark the module as external, which means it will not be included in the package but will be imported at run timenamespace? : string;# file namespace, which defaults to 'file', indicating that esbuild will take the default treatmentpath? : string;The file path after plug-in parsingpluginData? : any;# Data passed to the next plug-inpluginName? : string; warnings? : Message[]; watchDirs? : string[]; watchFiles? : string[]; } interface Message { text: string; location: Location | null; detail: any; // The original error from a JavaScript plugin,if applicable
}

interface Location {
  file: string;
  namespace: string;
  line: number; // 1-based
  column: number; // 0-based, in bytes
  length: number; // in bytes
  lineText: string;
}
Copy the code

Demo

const externalizeDep = {
  name: 'externalize-deps'.setup(build) {
    // If the return value is undefined, the next onResolve registered callback will be called, otherwise it will not proceed
    build.onResolve({ filter: /. * / }, (args) = > {
      console.log(args);
      const id = args.path
      // For external modules
      if (id[0]! = ='. ' && !path.isAbsolute(id)) {
        return {
          external: true.// Set this to true to mark the module as a third-party module, which means it will not be included in the package but imported at run time}})}}Copy the code

onLoad

The onLoad registered callback function is triggered when the non-external file is loaded

type Cb = (args: OnLoadArgs) = > OnLoadResult
type onLoad = ({}: OnLoadOptions, cb: Cb) = > {}


// Same as onResolve
interface OnLoadOptions {
  filter: RegExp;
  namespace? : string; } // Interface OnLoadArgs {path: string; // The absolute path to the loaded file
  namespace: string; // pluginData: any; Interface OnLoadResult {contents? :string | Uint8Array; // Specify the contents of the module. If this is set, no more load callbacks are run for this resolution path. If not, esBuild will continue to run load callbacks registered after the current callback. Then, if the content is still not set, esBuild will load the content from the file system by default if the namespace of the resolved path is 'file'errors? : Message[]; loader? : Loader;// Set the loader for this module, default is 'js'pluginData? :any; pluginName? :string; resolveDir? :string; // The file system directory to use when resolving the import path in this module to the real path on the file system. For modules in the 'file' namespace, this value defaults to the directory part of the module path. Otherwise, this value defaults to null unless the plug-in provides one. If the plug-in does not provide it, esBuild's default behavior will not resolve any imports in this module. This directory will be passed to any parse callbacks running on unresolved import paths in this module.warnings? : Message[]; watchDirs? :string[]; watchFiles? :string[];
}
Copy the code

The plug-in for

Suppose that if loDash’s add method is introduced through the CDN, the code in LoDash is added to the bundle at package time

import add from 'https://unpkg.com/[email protected]/add.js'

console.log(add(1.1))
Copy the code

A plugin

const axios = require('axios')
const httpUrl = {
    name: 'httpurl'.setup(build) {
        build.onResolve({ filter: /^https? : \ \ / / / }, (args) = > {
            return {
                path: args.path,
                namespace: 'http-url',
            }
        })
        build.onResolve({ filter: /. * /, namespace: 'http-url' }, (args) = > {
            return {
                path: new URL(args.path, args.importer).toString(),
                namespace: 'http-url',
            }
        })
        build.onLoad({ filter: /. * /, namespace: 'http-url' }, async (args) => {
            const res = await axios.get(args.path)
            return {
                contents: res.data,
            }
        })
    },
}

require('esbuild').build({
    entryPoints: ['index.js'].outdir: 'dist'.bundle: true.format: 'esm'.plugins: [httpUrl],
})
Copy the code

Vite handwritten plug-in, js, TS code import.meta. Url, __dirname, __filename into absolute path output

const replaceImportMeta = {
  name: 'replace-import-meta'.setup(build) {
    build.onLoad({ filter: /\.[jt]s$/ }, async (args) => {
      const contents = await fs.promises.readFile(args.path, 'utf8')
      return {
        loader: args.path.endsWith('.ts')?'ts' : 'js'.contents: contents
        .replace(
          /\bimport\.meta\.url\b/g.JSON.stringify(`file://${args.path}`)
        )
        .replace(
          /\b__dirname\b/g.JSON.stringify(path.dirname(args.path))
        )
        .replace(/\b__filename\b/g.JSON.stringify(args.path))
      }
    })
  }
}
Copy the code

conclusion

This is the common configuration of ESbuild and how to implement custom plug-ins. In Vite, the prebuild process and the compile process are all esbuilds. This is also one of Vite’s faster speeds.

Now that you know how to use ESbuild, start parsing Vite source code