好文推荐
localStorage的别样用法
借助npm包统一包管理器
前言
前端的打包工具层出不穷,老牌的webpack、rollup、gulp,新生代的esbuild,vite、rspack、tsup,他们都很好的解决了某一领域的痛点,但由于其配置的繁杂导致其学习成本较大,直到看到unbuild,突然有种这双手不要也不要紧的感觉......
作者对该库的介绍是
A unified javascript build system
我的理解是:一个零配置或低配置的javascript构建工具,该结论主要源自对源码的阅读与理解,若有偏差还望评论指正
源码阅读方式
1.理清整体的实现思路
2.对我认为比较亮眼的代码做进一步分析
核心流程梳理
通过其scripts配置,我们可以知道其入口在src/cli.ts(jiti是一个typescript运行时,放到下一遍文章详谈)
"scripts": {
"build": "pnpm unbuild",
"unbuild": "jiti ./src/cli"
}
进入cli.ts文件,其核心做了两件事
- 获取脚本传入的配置和项目根目录
const args = mri(process.argv.splice(2));
const rootDir = resolve(process.cwd(), args._[0] || ".");
- 调用build函数
await build(rootDir, args.stub).catch((error) => {
console.error(`Error building ${rootDir}: ${error}`);
throw error;
});
进入build.ts文件
- 配置合并
配置的来源有:package.json、用户自定义配置文件、内建的静态配置、智能分析
// 来源一:用户自定义配置文件
const buildConfig: BuildConfig = tryRequire("./build.config", rootDir) || {};
// 来源二:package.json
const pkg: PackageJson & Record<"unbuild" | "build", BuildConfig> =
tryRequire("./package.json", rootDir);
// 来源三:智能分析
await ctx.hooks.callHook("build:prepare", ctx);
// 来源四:内建的静态配置
.....略
- 创建上下文
上下文是npm包开发中常用到的方式,能有效避免不同文件之间依赖的相互引用
const ctx: BuildContext = {
options,
warnings: new Set(),
pkg,
buildEntries: [],
usedImports: new Set(),
hooks: createHooks(),
};
- 清空dist文件夹
if (options.clean) {
for (const dir of new Set(options.entries.map((e) => e.outDir).sort())) {
await rmdir(dir!);
await fsp.mkdir(dir!, { recursive: true });
}
}
- 执行打包
此时,预准备工作基本完成,开始进入构建流程,这其实就是使用了rollup的编程式api来进行打包
// 获取rollup配置
const rollupOptions = getRollupOptions(ctx);
// 之间rollup打包
const buildResult = await rollup(rollupOptions);
// 输出打包文件
await buildResult.write(outputOptions);
- 总结
目前为止,从流程上看,一切都还是中规中矩的,它就是套了壳的rollup
亮点一:零配置
在流程梳理的配置合并阶段,我们提到了合并来源包含了智能分析分支,这其实也是unbuild支持零配置的关键点之一
- 递归读取用户文件
export function listRecursively(path: string) {
const filenames = new Set<string>();
const walk = (path: string) => {
const files = readdirSync(path);
for (const file of files) {
const fullPath = resolve(path, file);
if (statSync(fullPath).isDirectory()) {
filenames.add(fullPath + "/");
walk(fullPath);
} else {
filenames.add(fullPath);
}
}
};
walk(path);
return [...filenames];
}
- 推断打包输出格式
1.根据exports的语法进行格式化
export function inferExportType(
condition: string,
previousConditions: string[] = [],
filename = ""
): "esm" | "cjs" {
if (filename) {
if (filename.endsWith(".d.ts")) {
return "esm";
}
if (filename.endsWith(".mjs")) {
return "esm";
}
if (filename.endsWith(".cjs")) {
return "cjs";
}
}
switch (condition) {
case "import":
return "esm";
case "require":
return "cjs";
default: {
if (previousConditions.length === 0) {
// TODO: Check against type:module for default
return "esm";
}
const [newCondition, ...rest] = previousConditions;
return inferExportType(newCondition, rest, filename);
}
}
}
2.读取pkg中配置的bin、main、module、types|typings字段
if (pkg.main) {
outputs.push({ file: pkg.main });
}
3.推测用户期望输出的文件格式
const isESMPkg = pkg.type === "module";
for (const output of outputs.filter((o) => !o.type)) {
const isJS = output.file.endsWith(".js");
if ((isESMPkg && isJS) || output.file.endsWith(".mjs")) {
output.type = "esm";
} else if ((!isESMPkg && isJS) || output.file.endsWith(".cjs")) {
output.type = "cjs";
}
}
- 推断打包入口
正向时,我们通过构建工具从入口开始执行打包,然后在pkg.json中将对应的打包好的文件设置为出口
理论上,入口也可以从出口中反向解析出来
// possiblePaths = ['dist/index', 'index']
const input = possiblePaths.reduce((source, d) => {
if (source) {
return source;
}
const SOURCE_RE = new RegExp(`${d}${isDir ? "" : "\\.\\w+"}$`);
return sourceFiles.
find((i) => i.match(SOURCE_RE))?.
replace(/(\.d\.ts|\.\w+)$/, "");
}, undefined);
- 补充或矫正文件输出类型
在推断打包出口的文件类型时,判断的依据是是否设置了type字段且值为module,这只适用于无exports和types配置的情况,因此还需要对这两种情况做处理记录
if (res.cjs) {
ctx.options.rollup.emitCJS = true;
}
if (res.dts) {
ctx.options.declaration = res.dts;
}
亮点二:externals与external的使用
一个重要的前提条件是,unbuild作为构建工具,本身就只在开发环境中使用,这意味着相关依赖总是可得的,而这一部分不应该占用对用户工程的打包编译
1.排除node相关
import Module from "node:module";
externals: [
...Module.builtinModules,
...Module.builtinModules.map((m) => "node:" + m),
],
2.排除依赖包
首先,需要向pkg收集dependencies、peerDependencies、pkg.devDependencies信息
options.dependencies = Object.keys(pkg.dependencies || {});
options.peerDependencies = Object.keys(pkg.peerDependencies || {});
options.devDependencies = Object.keys(pkg.devDependencies || {});
options.externals.push(...options.dependencies, ...options.peerDependencies);
接着在external钩子中判断当前import的值是否在名单中,在就跳过
const isExplicitExternal = arrayIncludes(ctx.options.externals, pkg);
if (isExplicitExternal) {
return true;
}
亮点三:bundless
我们知道,现代主流浏览器都对esm格式提供了天然的支持,换言之,我们没有必要像webpack那样从一开始就将所有文件一股脑打包成一个bundle,而应该更加靠近bundless,两者的对比可参考如下
![](https://img.haomeiwen.com/i22517122/2dee0e5ca1a4f39c.png)
在unbuild中,通过mkdist来实现,这个放到后边单独分享
import { mkdist, MkdistOptions } from "mkdist";
const mkdistOptions: MkdistOptions = {};
await mkdist(mkdistOptions);
亮点四:js to ts
如果现在有一个需求:为一个使用js开发的npm包添加类型注释,你会怎么做?应该大部分人都会老老实实的读源码,并在理解其含义的基础上一个一个进行定义;牛逼一些的,可能会想到去将js转ast,然后再transform到ts;而unbuild已经给我们了标准答案:untyped
import { resolveSchema, generateTypes } from "untyped";
const schema = await resolveSchema(srcConfig, defaults);
generateTypes(schema)
亮点五:watch
不管是webpack又或是vite,他们的热更新套路都需要先对对变更的源文件进行编译
现在unbuild为我们启发了新大路:为源代码添加运行时环境,即jiti
output + ".cjs",
`${shebang}module.exports = require(${JSON.stringify()})
(null, { interopDefault: true, esmResolve: true })(${JSON.stringify()})`
网友评论