组件库工程化引入(3)--打包体系化

115 阅读16分钟

定制打包体系

模块化规范

模块规范>宿主环境>工具集>工具>业务代码

浏览器环境实现了EcmaScript Module(后文简称ESM)规范。

Node v12之前支持CommonJS(后文简称CJS)规范,12之后同时支持CJS与ESM。

宿主环境之上,是基于模块化规范实现的工具集,比如webpackviteVScode生态。

基于工具集提供的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 文件之间的依赖关系
  • 模块化

公共配置提取

构建全量产物

目前,构建出的 umdes 以及 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 产物经过了依赖外部化处理,直接引用会缺少大量依赖,此时需要取消依赖的外部化处理,构建出全量产物。全量产物适用于非构建场景,不必配合包管理器使用。

全量产物应用:

  1. 用户需要这样一个快速上手的途径(可以暂时不折腾构建工具)
  2. 在线演示的沙盒环境也正需要这样的产物。

总结

  • 消除大量重复代码
  • 集中维护构建配置,避免分散管理
  • 提高构建的自动化程度
  • 增强构建能力,支持同时生成不同类型的产物。

“获取所有待构建的子包”、“对子包进行拓扑排序”、“遍历子包执行脚本”。

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
    }
  }
}

注意事项

  1. build 包不引用其他子包内部依赖。

由于加载 vite.config 时,各种路径别名能力——无论是 tsconfig 的 paths 还是 Vite 自身的 alias 都无法作用,内部模块之间的引用无法定位到正确的源码。 image.png

  1. 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.jsontsconfig.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

  • 产物适用于构建场景,必须配合包管理器使用。

  • 构建 umdcjs 格式的产物。

  • 构建完成后要将产物路径回写入 package.json 的入口字段。

  • 所有的依赖都要外部化处理。

  • 不混淆压缩产物代码,不生成 sourcemap。

  • 典型 Vite 配置案例: 全量构建

  • 对应 mode 的值为 fullfull-min

  • 产物适用于非构建场景,无需包管理器,支持浏览器环境直接引入。

  • 只产出 umd 格式产物。

  • 构建完成后不回写 package.json

  • 只外部化 vue 这样的 peerDependencies(上游框架),其他依赖项都要打包进来。

  • modefull-min 时,在全量构建的基础上,混淆压缩产物代码、生成 sourcemap。

产物格式 build.lib--lib.ts

getLib--获取 build.lib 产物相关配置,接受两个参数 packageJsonoptions,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--处理单个预设插件

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';