定制打包体系
模块化规范
模块规范>宿主环境>工具集>工具>业务代码
浏览器环境实现了EcmaScript Module
(后文简称ESM
)规范。
Node v12之前支持CommonJS(后文简称CJS)规范,12之后同时支持CJS与ESM。
在宿主环境之上,是基于模块化规范实现的工具集,比如webpack
、vite
、VScode
生态。
基于工具集提供的API
,可以实现各种工程化工具。
再往上,就是开发者自己编写的业务代码。
- Commonjs:简称 cjs,是 nodejs 中默认采用的模块化规范 ,不能直接在浏览器中运行。模块为同步的运行时加载,同时导出变量为值拷贝。
const axios = require("axios")
- AMD:模块异步加载,使用
requirejs
库后可在浏览器端和服务端运行,同时导出变量也为值拷贝。
var requirejs = require('requirejs');
requirejs.config({
paths: {
lib1: 'module/index1',
lib2: 'module/index2'
}
})
require(["lib1","lib2"], function(l1,l2) {
l1.doSomething();
l2.doSomething();
})
- CMD:CMD 的基本逻辑跟 AMD 是一致的,需要使用sea.js库且 CMD 仅支持浏览器端使用。
- ESM:ECMAscript 的模块化规范,该规范在浏览器端和服务端都得到了支持,在非 default 导出的情况下,导出变量为值的引用,否之亦为值的拷贝。
import { value, getValue, Obj, name } from "./module/index.js"
为什么要打包
- 生活在 script 标签中--如果一个网站功能很多,我们要按照功能划分写多个 js 文件,那就要添加多个
<script src="">
去引用这些 js 文件,还需要注意不同 js 文件之间的依赖关系 - 模块化
公共配置提取
构建全量产物
目前,构建出的 umd
、es
以及 d.ts
类型声明产物,称这种产物为常规产物。常规产物适用于构建场景,必须配合包管理器使用。
默认情况下,Vite
会为我们生成 .mjs
和 .umd.js
后缀的产物,可以满足绝大多数情况下对于产物格式的要求。其中 .mjs
对应 esm
格式的可用产物,.umd.js
对应 cjs
格式的可用产物。
- UMD 格式的产物一般是一个合并后的、自包含的 JavaScript 文件,可以直接在浏览器环境中使用,也可以通过模块加载器(如 RequireJS)在其他环境中引入和使用。
- ES 模块格式的产物会将代码拆分为多个模块,并生成符合 ES 模块规范的文件。这种格式适用于在现代浏览器中直接使用、按需加载模块,或在支持 ES 模块的 Node.js 环境中作为依赖引入。
- 类型声明文件是一种用于描述 JavaScript 代码中类型信息的文件,用于静态类型检查和编辑器智能提示
产物如果直接通过 <script src="xxxx"></script>
的方式引入,是无法正常工作的。umd
产物经过了依赖外部化处理,直接引用会缺少大量依赖,此时需要取消依赖的外部化处理,构建出全量产物。全量产物适用于非构建场景,不必配合包管理器使用。
全量产物应用:
- 用户需要这样一个快速上手的途径(可以暂时不折腾构建工具)
- 在线演示的沙盒环境也正需要这样的产物。
总结
- 消除大量重复代码
- 集中维护构建配置,避免分散管理
- 提高构建的自动化程度
- 增强构建能力,支持同时生成不同类型的产物。
“获取所有待构建的子包”、“对子包进行拓扑排序”、“遍历子包执行脚本”。
Vite
的基础上增强构建能力,演化出自己的打包体系
构建配置相关的封装集中于build
{
"name": "@elephant4vue/shared",
"version": "0.0.0",
"description": "",
"keywords": [
"vue",
"ui",
"component library"
],
"author": "OrzMiku",
"license": "MIT",
"scripts": {
"build": "vite build"
},
"main": "./dist/elephant4vue-shared.umd.js",
"module": "./dist/elephant4vue-shared.mjs",
"exports": {
".": {
"require": "./dist/elephant4vue-shared.umd.js",
"module": "./dist/elephant4vue-shared.mjs",
"types": "./dist/index.d.ts"
}
},
"types": "./dist/index.d.ts",
"dependencies": {
"@types/lodash": "^4.14.197",
"lodash": "^4.17.21"
},
"peerDependencies": {
"vite": ">=3.0.0",
"vue": ">=3.0.0"
},
"peerDependenciesMeta": {
"vue": {
"optional": true
}
}
}
注意事项
build
包不引用其他子包内部依赖。
由于加载
vite.config
时,各种路径别名能力——无论是tsconfig
的paths
还是Vite
自身的alias
都无法作用,内部模块之间的引用无法定位到正确的源码。
build
包谨慎安装只提供esm
产物的外部依赖。
.ts
后缀的配置文件在package.json
中不声明"type": "module"
的情况下无法使用esm
模块。能把配置文件改成
vite.config.mts
来适应这些纯esm
产物的外部依赖。 能把配置文件改成vite.config.mts
来适应这些纯esm
产物的外部依赖。
Vite是如何对我们写的(vite.config.x)进行解析?
- 加载配置文件-大概思路是首先加载,解析配置文件,然后 合并命令行的配置
- 解析用户插件-根据apply参数,剔除不生效的插件, 给插件排好顺序.有些插件只在开发阶段生效,或者说只在生产环境生效,我们可以通过 apply: 'serve' 或 'build' 来指定它们,同时也可以将apply配置为一个函数,来自定义插件生效的条件
- 调用 插件的 config 钩子,进行配置合并
- 解析root参数,alias参数-如果在配置文件内没有指定的话,默认root解析的是 process.cwd(),解析alias时,需要加上一些内置的 alias 规则,如@vite/env、@vite/client这种直接重定向到 Vite 内部的模块.
- 加载环境变量
- 定义路径解析器工厂
规划
// packages/build/vite.config.ts
import { generateConfig } from './src';
export default generateConfig(/** ... */);
Vite
解析配置文件 vite.config
的方式却相对简单直接,本质上是先用 esbuild
将 ts
配置解析成 js
,再通过 Node.js
原生的机制加载,并不像构建代码那样时做了很多兼容性处理。
文件作用
📦src
┣ 📂generateConfig # 实现生成构建配置的主体方法
┃ ┣ 📜external.ts # 依赖外部化相关,获取构建配置的 build.rollupOptions.external 字段
┃ ┣ 📜index.ts # 模块出口,主题方法实现,整合构建配置
┃ ┣ 📜lib.ts # 产物相关,获取构建配置的 build.lib 字段
┃ ┣ 📜options.ts # 配置项对象声明
┃ ┣ 📜pluginMoveDts.ts # 移动 d.ts 产物的自定义插件
┃ ┣ 📜pluginSetPackageJson.ts # 自动将产物路径写入 package.json 的自定义插件
┃ ┗ 📜plugins.ts # 插件相关,获取构建配置的 plugins 字段
┃
┣ 📂utils # 存放本模块用到的公共方法
┃ ┣ 📜formatVar.ts # 变量名格式转换方法,如驼峰式,连字符式等
┃ ┣ 📜index.ts # 公共方法统一出口
┃ ┣ 📜json.ts # JSON 文件的读写
┃ ┣ 📜resolvePath.ts # 路径的处理方法
┃ ┗ 📜typeCheck.ts # 判断对象类型的方法
┃
┗ 📜index.ts # 模块出口
获取构建配置的主方法
-
首先要处理自定义的构建选项
options
,并且读取子包的package.json
。它们将决定生成构建配置的具体行为。 -
生成构建配置的整体过程是比较复杂的,于是我们将其拆分成三部分:
- 与产物相关的配置:
build.lib
- 与依赖相关的配置:
build.rollupOptions.external
- 与插件相关的配置:
plugins
- 与产物相关的配置:
-
初步生成的构建配置与用户自定义的
Vite
配置做一个深合并,得到最终构建配置
构建选项定义--generateConfig/options.ts
UMD 格式的代码可以在浏览器中直接使用,也可以通过 CommonJS 或 AMD 规范进行引用。
export interface GenerateConfigOptions extends GenerateConfigPluginsOptions
自定义构建选项接口GenerateConfigOptions扩展GenerateConfigPluginsOptions(配置构建中的插件的选项集合)
GenerateConfigOptions {
代码入口(entry)
产物输出路径(outDir)
生成的文件名称(fileName-默认取package包名,产物为umd格式时驼峰化后作为全局变量名)
打包模式(mode—1.package常规构建将所有依赖外部化处理,打包出es和umd产物,构建结束后将产物路径回写入package.json入口字段中2.full全量构建,打包出umd产物,不参与d.ts移动,和构建完成后的产物路径回写。3.full-min全量构建基础上,产物代码混淆压缩,生成sourcemap 文件)
是否移动dts文件(dts)
}
defaultOptions-构建选项的默认值 getOptions-解析构建选项:使用一个默认的配置作为基础,以及用户提供的选项进行覆盖,最终返回一个完整的配置对象
// packages/build/src/generateConfig/options.ts
import { PackageJson } from 'type-fest';
import type { GenerateConfigPluginsOptions } from './plugins';
/** 自定义构建选项 */
export interface GenerateConfigOptions extends GenerateConfigPluginsOptions {
/**
* 代码入口
* @default 'src/index.ts'
*/
entry?: string;
/**
* 产物输出路径,同:https://cn.vitejs.dev/config/build-options.html#build-outdir
* @default 'dist'
*/
outDir?: string;
/**
* 生成的文件名称,
*
* 默认情况下取 package 包名,转换为 kebab-case,如:@openx/request -> openx-request
*
* 当产物为 umd 格式时,驼峰化后的 fileName 会作为全局变量名,如:openx-request -> openxRequest
*/
fileName?: string;
/**
* 打包模式
* - package - 常规构建。会将所有依赖外部化处理,打包出适用于构建场景的 `es`、`umd` 格式产物。并在构建结束后将产物路径回写入 package.json 的入口字段中。
* - full - 全量构建。大部分依赖都不做外部化处理,打包出适用于非构建场景的 `umd` 格式产物。不参与 d.ts 的移动;不参与构建完成后的产物路径回写。
* - full-min - 在全量构建的基础上,将产物代码混淆压缩,并生成 sourcemap 文件。
* @default 'package'
*/
mode?: 'package' | 'full' | 'full-min';
/**
* 是否将 d.ts 类型声明文件的产物从集中目录移动到产物目录,并将类型入口回写到 package.json 的 types 字段。
*
* 必须在 mode 为 packages 时生效。
*
* 输入 tsc 编译生成 d.ts 文件时所读取的 tsconfig 文件的路径。
* @default ''
*/
dts?: string;
/**
* 完成构建后,准备回写 package.json 文件前对其对象进行更改的钩子。
*
* 必须在 mode 为 packages 时生效。
*/
onSetPkg?: (pkg: PackageJson) => void | Promise<void>;
}
/** 构建选项的默认值 */
export function defaultOptions(): Required<GenerateConfigOptions> {
return {
entry: 'src/index.ts',
outDir: 'dist',
fileName: '',
mode: 'package',
dts: '',
onSetPkg: () => {},
pluginVue: false,
pluginInspect: false,
pluginVisualizer: false,
pluginReplace: false,
};
}
/** 解析构建选项 */
export function getOptions(options?: GenerateConfigOptions): Required<GenerateConfigOptions> {
return {
...defaultOptions(),
...options,
};
}
预设插件相关配置选项--plugins.ts
GenerateConfigPluginsOptions
/** 预设插件相关配置选项 */
export interface GenerateConfigPluginsOptions {
/**
* 是否启用 @vitejs/plugin-vue 进行 vue 模板解析。配置规则如下,对于其他插件也适用。
* - false / undefined 不启用该插件
* - true 启用该插件,采用默认配置
* - Options 启用该插件,应用具体配置
* @default false
*/
pluginVue?: boolean | VueOptions;
/**
* 是否启用 vite-plugin-inspect 进行产物分析。
* @default false
*/
pluginInspect?: boolean | InspectOptions;
/**
* 是否启用 rollup-plugin-visualizer 进行产物分析。
* @default false
*/
pluginVisualizer?: boolean | PluginVisualizerOptions;
/**
* 是否启用 @rollup/plugin-replace 进行产物内容替换。
* @default false
*/
pluginReplace?: boolean | RollupReplaceOptions;
}
实现 package.json 的读写
package.json
的读写并不复杂- 读写可以集成 npm 包 read-pkg-up 以及 write-pkg 分别处理。
readJsonFile
readFile(filePath, 'utf-8'); - 读取指定路径的文件内容,使用
'utf-8'
编码格式writeJsonFile
writeFile(filePath, JSON.stringify(...rests), 'utf-8');
// packages/build/src/utils/json.ts
import {
readFile,
writeFile,
} from 'node:fs/promises';
/**
* 从文件中读取出 JSON 对象
* @param filePath 文件路径
* @returns JSON 对象
*/
export async function readJsonFile<
T extends Record<string, any> = Record<string, any>,
>(filePath: string): Promise<T> {
const buffer = await readFile(filePath, 'utf-8');
return JSON.parse(buffer);
}
/**
* 将 JSON 对象写入文件
* @param filePath 文件路径
* @param rests {@link JSON.stringify} 的参数
*/
export async function writeJsonFile(filePath: string, ...rests: Parameters<typeof JSON.stringify>) {
await writeFile(filePath, JSON.stringify(...rests), 'utf-8');
}
package.json
配置对象的类型定义
npm 包 type-fest,它是一个纯类型库,除了包含 package.json
、tsconfig.json
这种复杂配置对象的类型声明,还包括了大量的 TypeScript
类型运算方法。
pnpm --filter @openxui/build i -S type-fest
import { PackageJson } from 'type-fest';
对象类型检查的方法--typeCheck.ts
export function isObjectLike(val: unknown): val is Record<any, any> {
return val !== null && typeof val === 'object';
}
export function isFunction(val: unknown): val is Function {
return typeof val === 'function';
}
相对路径与绝对路径的计算-resolvePath.ts
usePathAbs--予一个基础路径,获取到一个以此为基准计算绝对路径的方法
absCwd--获取相对于当前脚本执行位置的绝对路径
usePathRel--给予一个基础路径,获取到一个以此为基准计算相对路径的方法 relCwd--获取相对于当前脚本执行位置的相对路径
normalizePath --抹平 Win 与 Linux 系统路径分隔符之间的差异
import { relative, resolve, sep } from 'node:path';
/** 给予一个基础路径,获取到一个以此为基准计算绝对路径的方法 */
export function usePathAbs(basePath: string) {
return (...paths: string[]) => normalizePath(resolve(basePath, ...paths));
}
/** 获取相对于当前脚本执行位置的绝对路径 */
export const absCwd = usePathAbs(process.cwd());
/** 给予一个基础路径,获取到一个以此为基准计算相对路径的方法 */
export function usePathRel(basePath: string) {
return (path: string, ignoreLocalSignal: boolean = true) => {
const result = normalizePath(relative(basePath, path));
if (result.slice(0, 2) === '..') {
return result;
}
return ignoreLocalSignal ? result : `./${result}`;
};
}
/** 获取相对于当前脚本执行位置的相对路径 */
export const relCwd = usePathRel(process.cwd());
/** 抹平 Win 与 Linux 系统路径分隔符之间的差异 */
function normalizePath(path: string) {
if (sep === '/') {
return path;
}
return path.replace(new RegExp(`\\${sep}`, 'g'), '/');
}
变量名转换的工具方法
camelCase
varName
表示要转换的变量名字符串,isFirstWordUpperCase
表示首个单词是否大写(默认为小写)splitVar
[A-Z]{2,}(?=[A-Z][a-z]+|[0-9]|[^a-zA-Z0-9]):匹配连续出现两个或多个大写字母,并且后面跟随的是大写字母和小写字母的组合、数字或非字母数字字符。
[A-Z]?[a-z]+:匹配一个可选的大写字母(用于匹配首个单词的首字母大写情况),后面跟随一个或多个小写字母。
[A-Z]:匹配单个大写字母。
[0-9]:匹配单个数字。
function splitVar(varName: string) {
const reg = /[A-Z]{2,}(?=[A-Z][a-z]+|[0-9]|[^a-zA-Z0-9])|[A-Z]?[a-z]+|[A-Z]|[0-9]/g;
return varName.match(reg) || <string[]>[];
}
/** 将变量名转换为肉串形式:@openxui/build -> openxui-build */
export function kebabCase(varName: string) {
const nameArr = splitVar(varName);
return nameArr.map((item) => item.toLowerCase()).join('-');
}
/** 将变量名转换为驼峰形式:@openxui/build -> openxuiBuild */
export function camelCase(varName: string, isFirstWordUpperCase = false) {
const nameArr = splitVar(varName);
return nameArr.map((item, index) => {
if (index === 0 && !isFirstWordUpperCase) {
return item.toLowerCase();
}
return item.charAt(0).toUpperCase() + item.slice(1).toLowerCase();
}).join('');
}
常规构建与全量构建
常规构建:
-
对应
mode
的值为package
。 -
产物适用于构建场景,必须配合包管理器使用。
-
构建
umd
、cjs
格式的产物。 -
构建完成后要将产物路径回写入
package.json
的入口字段。 -
所有的依赖都要外部化处理。
-
不混淆压缩产物代码,不生成 sourcemap。
-
典型
Vite
配置案例: 全量构建: -
对应
mode
的值为full
或full-min
。 -
产物适用于非构建场景,无需包管理器,支持浏览器环境直接引入。
-
只产出
umd
格式产物。 -
构建完成后不回写
package.json
。 -
只外部化
vue
这样的peerDependencies
(上游框架),其他依赖项都要打包进来。 -
当
mode
为full-min
时,在全量构建的基础上,混淆压缩产物代码、生成 sourcemap。
产物格式 build.lib--lib.ts
getLib--获取 build.lib 产物相关配置,接受两个参数
packageJson
和options
,finalName:name 字段转换成 kebab-case。libOptions中调用getOutFileName
函数来生成文件名。getOutFileName-获取产物文件名称。判断是否是量构建,从而修改文件名字 EntryInfo(子包源码入口文件的绝对路径,子包源码入口文件相对于脚本执行位置的路径,子包源码入口是不是文件)
resolveEntry(解析子包源码入口,绝对路径)
import { PackageJson } from 'type-fest';
import { LibraryOptions, LibraryFormats, BuildOptions } from 'vite';
import { statSync } from 'node:fs';
import { join } from 'node:path';
import {
kebabCase,
camelCase,
absCwd,
relCwd,
} from '../utils';
import { getOptions, GenerateConfigOptions } from './options';
/**
* 获取 build.lib 产物相关配置
* @param packageJson package.json 文件内容
* @param options 构建选项
*/
export function getLib(
packageJson: PackageJson = {},
options: GenerateConfigOptions = {},
): Pick<BuildOptions, 'lib' | 'minify' | 'sourcemap' | 'outDir' | 'emptyOutDir'> {
const {
entry,
outDir,
mode,
fileName,
} = getOptions(options);
// 文件名称,默认取 package.json 的 name 字段转换成 kebab-case:@openxui/build => openxui-build
const finalName = fileName || kebabCase(packageJson.name || '');
const libOptions: LibraryOptions = {
entry,
// 全量构建只生产 umd 产物
formats: mode === 'package' ? ['es', 'umd'] : ['umd'],
name: camelCase(finalName),
fileName: (format) => {
const formatName = format as LibraryFormats;
return getOutFileName(finalName, formatName, mode);
},
};
return {
lib: libOptions,
// full-min 模式下全量构建,需要混淆代码,生成 sourcemap 文件,且不清空产物目录
minify: mode === 'full-min' ? 'esbuild' : false,
sourcemap: mode === 'full-min',
emptyOutDir: mode === 'package',
outDir,
};
}
/**
* 获取产物文件名称
* @param fileName 文件名称
* @param format 产物格式
* @param buildMode 构建模式
*/
export function getOutFileName(fileName: string, format: LibraryFormats, buildMode: GenerateConfigOptions['mode']) {
const formatName = format as ('es' | 'umd');
const ext = formatName === 'es' ? '.mjs' : '.umd.js';
let tail: string;
// 全量构建时,文件名后缀的区别
if (buildMode === 'full') {
tail = '.full.js';
} else if (buildMode === 'full-min') {
tail = '.full.min.js';
} else {
tail = ext;
}
return `${fileName}${tail}`;
}
interface EntryInfo {
/** 子包源码入口文件的绝对路径 */
abs: string;
/** 子包源码入口文件相对于脚本执行位置的路径 */
rel: string;
/** 子包源码入口是不是文件 */
isFile: boolean;
}
/**
* 解析子包源码入口
* @param entry 源码入口路径
* @returns 子包源码入口信息,解析结果
*/
export function resolveEntry(entry: string): EntryInfo {
/** 入口绝对路径 */
const absEntry = absCwd(entry);
/** 入口是否为文件 */
const isEntryFile = statSync(absEntry).isFile();
/** 入口文件夹绝对路径 */
const absEntryFolder = isEntryFile ? join(absEntry, '..') : absEntry;
return {
abs: absEntry,
rel: relCwd(absEntryFolder),
isFile: isEntryFile,
};
}
依赖外部化 build.rollupOptions.external
在常规构建中,getExternal
会将 package.json
中所有的依赖项都推入 build.rollupOptions.external
;全量构建中,则只推入 peerDependencies
部分。
getExternal
创建一个名为
defaultExternal
的数组,其中包含一个正则表达式/^node:.*/
,用于匹配所有以 "node:" 开头的字符串,这样的字符串表示对 Node.js 原生模块的外部化处理。这段代码用于生成一个外部化的模块列表,这些模块将在打包过程中被排除在外。它处理了
packageJson
中的依赖和对 Node.js 原生模块的处理,并根据mode
的值来确定是否将依赖包括在打包结果中。
import { PackageJson } from 'type-fest';
import { getOptions, GenerateConfigOptions } from './options';
/**
* 获取 build.rollupOptions.external 依赖外部化相关的配置
* @param packageJson package.json 文件内容
* @param options 构建选项
*/
export function getExternal(
packageJson: PackageJson = {},
options: GenerateConfigOptions = {},
) {
const {
dependencies = {},
peerDependencies = {},
} = packageJson;
const { mode } = getOptions(options);
const defaultExternal: (string | RegExp)[] = [
// 将所有 node 原生模块都进行外部化处理
/^node:.*/,
];
const toReg = (item: string) => new RegExp(`^${item}`);
return defaultExternal.concat(
Object.keys(peerDependencies).map(toReg),
// 全量构建时,依赖不进行外部化,一并打包进来
mode === 'package' ? Object.keys(dependencies).map(toReg) : [],
);
}
插件管理
getPresetPlugins--用于获取一组预设插件选项,并将这些选项保存在一个数组中,然后将数组作为结果返回。每个插件选项的获取是通过调用
getPresetPlugin
函数,并传递相应的参数来完成的。getPlugins--获取完整的插件配置
getPresetPlugin--处理单个预设插件
-
@vitejs/plugin-vue:提供 Vue 3 单文件组件支持。
-
vite-plugin-inspect:检查
Vite
构建过程的中间状态,为调试Vite
配置提供支持。 -
rollup-plugin-visualizer:可视化分析构建结果中各个模块的空间占用情况。
-
@rollup/plugin-replace:在构建过程中替换源码中的内容。
import inspect, { Options as InspectOptions } from 'vite-plugin-inspect';
import { visualizer, PluginVisualizerOptions } from 'rollup-plugin-visualizer';
import vue, { Options as VueOptions } from '@vitejs/plugin-vue';
import replace, { RollupReplaceOptions } from '@rollup/plugin-replace';
import { PluginOption } from 'vite';
import { PackageJson } from 'type-fest';
import { isObjectLike } from '../utils';
import type { GenerateConfigOptions } from './options';
import { pluginSetPackageJson } from './pluginSetPackageJson';
import { pluginMoveDts } from './pluginMoveDts';
/** 预设插件相关配置选项 */
export interface GenerateConfigPluginsOptions {
/**
* 是否启用 @vitejs/plugin-vue 进行 vue 模板解析。配置规则如下,对于其他插件也适用。
* - false / undefined 不启用该插件
* - true 启用该插件,采用默认配置
* - Options 启用该插件,应用具体配置
* @default false
*/
pluginVue?: boolean | VueOptions;
/**
* 是否启用 vite-plugin-inspect 进行产物分析。
* @default false
*/
pluginInspect?: boolean | InspectOptions;
/**
* 是否启用 rollup-plugin-visualizer 进行产物分析。
* @default false
*/
pluginVisualizer?: boolean | PluginVisualizerOptions;
/**
* 是否启用 @rollup/plugin-replace 进行产物内容替换。
* @default false
*/
pluginReplace?: boolean | RollupReplaceOptions;
}
/**
* 获取预设插件配置
* @param options 预设插件相关配置选项
*/
export function getPresetPlugins(options: GenerateConfigPluginsOptions = {}) {
const result: PluginOption[] = [];
result.push(
getPresetPlugin(options, 'pluginVue', vue),
getPresetPlugin(options, 'pluginInspect', inspect),
getPresetPlugin(options, 'pluginVisualizer', visualizer),
getPresetPlugin(options, 'pluginReplace', replace),
);
return result;
}
/**
* 获取完整的插件配置
* @param packageJson package.json 文件内容
* @param options 构建选项
*/
export function getPlugins(
packageJson: PackageJson = {},
options: GenerateConfigOptions = {},
) {
const { mode, dts } = options;
const result = getPresetPlugins(options);
if (mode === 'package') {
// 常规构建的情况下,集成自定义插件,回写 package.json 的入口字段
result.push(pluginSetPackageJson(packageJson, options));
if (dts) {
// 常规构建的情况下,集成自定义插件,移动 d.ts 产物
result.push(pluginMoveDts(options));
}
}
return result;
}
/**
* 处理单个预设插件
* @param options 预设插件相关配置选项
* @param key 目标选项名称
* @param plugin 对应的插件函数
* @param defaultOptions 插件默认选项
*/
export function getPresetPlugin<K extends keyof GenerateConfigPluginsOptions>(
options: GenerateConfigPluginsOptions,
key: K,
plugin: (...args: any[]) => PluginOption,
defaultOptions?: GenerateConfigPluginsOptions[K],
) {
const value = options[key];
if (!value) {
return null;
}
return plugin(
isObjectLike(value) ? value : defaultOptions,
);
}
Vite
的各种钩子
pluginSetPackageJson.ts
构建配置的主方法
import { mergeConfig, UserConfig } from 'vite';
import { PackageJson } from 'type-fest';
import { readJsonFile, absCwd } from '../utils';
import { getOptions, GenerateConfigOptions } from './options';
import { getPlugins } from './plugins';
import { getExternal } from './external';
import { getLib } from './lib';
/**
* 生成 Vite 构建配置
* @param customOptions 自定义构建选项
* @param viteConfig 自定义 vite 配置
*/
export async function generateConfig(
customOptions?: GenerateConfigOptions,
viteConfig?: UserConfig,
) {
/** 获取配置选项 */
const options = getOptions(customOptions);
// 获取每个子包的 package.json 对象
const packageJson = await readJsonFile<PackageJson>(absCwd('package.json'));
// 生成产物相关配置 build.lib
const libOptions = getLib(packageJson, options);
// 生成依赖外部化相关配置 build.rollupOptions.external
const external = getExternal(packageJson, options);
// 插件相关,获取构建配置的 plugins 字段
const plugins = getPlugins(packageJson, options);
// 拼接各项配置
const result: UserConfig = {
plugins,
build: {
...libOptions,
rollupOptions: {
external,
},
},
};
// 与自定义 Vite 配置深度合并,生成最终配置
return mergeConfig(result, viteConfig || {}) as UserConfig;
}
// 导出其他模块
export * from './plugins';
export * from './options';
export * from './lib';
export * from './external';
export * from './pluginMoveDts';
export * from './pluginSetPackageJson';