原文链接: https://blog.rxliuli.com/p/ba9b341a8792405fb86d8fe02a18adfc/
前言
esbuild 是一个通用的代码编译器和构建工具,使用 golang 构建,它非常快,性能上比现有的 js 工具链高 1~2 个数量级。它目前还不是一个开箱即用的构建工具,但通过它的插件系统,我们已经可以做到许多事情。
自动排除所有依赖项
在构建 lib 时,我们通常都不希望捆绑依赖的模块,希望能够默认排除掉所有的依赖项,这个插件就是用来实现这个功能的。它会将所有不是以 .
开头的导入模块设置为外部的,避免捆绑到最终构建产物中。
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, }; }); }, };}
我们可以这样使用,例如 import esbuild,但它不会被捆绑进来。
import { build } from "esbuild";console.log(build);
会被编译成
import { build } from "esbuild";console.log(build);
使用环境变量
有时候我们会需要针对不同的环境使用环境变量来区分,而使用插件也可以很简单的做到这一点。
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()); } }, };}
使用插件之后,我们就可以在代码中使用环境变量了
export const NodeEnv = import.meta.env.NODE_ENV;
编译结果
export const NodeEnv = "test";
在构建时输出日志
有时候我们希望以监视模式构建某些东西,但 esbuild 不会在构建后输出消息,我们简单实现一个。
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`); }); }, };}
我们可以测试它是有效的
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);
自动排除 node: 开头的依赖
有时候某些依赖的模块使用了 nodejs 的原生模块,但是以 node:
开头的的写法,这会导致 esbuild 无法识别,我们使用以下插件处理它
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, })); }, };}
我们的以下代码中的 node: 开头的原生模块会被排除
import { path } from "node:path";console.log(path.resolve(__dirname));
编译结果
// <stdin>import { path } from "path";console.log(path.resolve(__dirname));
通过 ?raw 捆绑文本文件
如果使用过 vite 的话,可能会对它的 ?*
功能印象深刻,它提供了各种各样的功能来以不同的方式导入文件,而在 esbuild 中,我们有时候也希望静态捆绑某些内容,例如 readme 文件。
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", }; } ); }, };}
通过以下方式验证
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();
重写一些模块
有时候我们希望重写一些模块,例如将导入的 lodash 更改为 lodash-es 以实现 tree shaking,这时候我们可以通过以下插件办到这件事
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]); }); }, };}
我们可以使用以下配置将 lodash 替换为 lodash-es
build({ plugins: [resolve([["lodash", "lodash-es"]])],});
源代码
import { uniq } from "lodash";console.log(uniq([1, 2, 1]));
编译结果
import { uniq } from "lodash-es";console.log(uniq([1, 2, 1]));
强制指定模块没有副作用
当我们的使用一个第三方的包时,有可能这个包依赖了一些其他的模块,如果这个模块没有声明 sideEffect
,那么即便它没有副作用并且导出了 esm 的包,也会将依赖的模块 bundle 进来,但我们可以使用插件 api 强制指定模块没有副作用。
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; }); }, };}
我们以如下方式使用它
build({ plugins: [sideEffects(["lib"])],});
这时候,即便 lib-a 中某些代码依赖了 lib-b,但只要你的代码没有依赖到特定方法,那么它就会被正确的 tree shaking
例如下面的代码
// main.tsimport { hello } from "lib-a";console.log(hello("liuli"));
// lib-a/src/index.tsexport * from "lib-b";export function hello(name: string) { return `hello ${name}`;}
编译结果
// dist/main.jsfunction hello(name: string) { return `hello ${name}`;}console.log(hello("liuli"));
总结
目前已经实现了 esbuild 的许多插件,但它作为构建应用的基础构建工具仍然稍显薄弱,目前仅推荐使用它构建一些纯 JavaScript/TypeScript 的代码。如果需要构建完整的 web 应用,那么 vite 可能是目前基于 esbuild 的最成熟的构建工具。
本文转自: https://blog.rxliuli.com/p/ba9b341a8792405fb86d8fe02a18adfc/
本站仅做收录,版权归原作者所有。