In Vite ESbuild
A lot of Vite uses ESbuild, for example
- translation
ts
Type configuration file - request
ts
,jsx
,tsx
File when it is compiled intojs
file - 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
- 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
- Go compiles directly to machine code and is certainly faster than JIT
- 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 as
babel-plugin-import
- Important functionality for building applications is still under continuous development — in particular
The code segment
andCSS
In 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
- Not using some custom babel-plugin (e.g
babel-plugin-import
) - 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
str
: A string (mandatory), indicating the code to be convertedoptions
: 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 valuePromise
object
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
true
Generated by:.js.map
And the generated file is added//# sourceMappingURL=
false
: Do not use sourcemap'external'
Generated by:.js.map
The 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.map
But 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 elementjsxFragment
Fragments 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 tooutbase
The 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 tooutbase
The 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 item
bundle
forfalse
, the generated dependency graph contains only import files and import files in import files - If the configuration item
bundle
fortrue
The 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 namesetup
Function: runs once each time the Build API is calledbuild
Contains 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
filter
Each 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, infilter
If the module namespace is the same, the callback is triggered. Can pass the previous oneonResolve
The 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