源码解析
index.cjs
js
/* eslint-disable no-restricted-globals */
// 禁用 ESLint 的 no-restricted-globals 规则,不进行全局变量的限制检查
// type utils
module.exports.defineConfig = (config) => config
// proxy cjs utils (sync functions)
// 通过 Object.assign 将 ./dist/node-cjs/publicUtils.cjs 模块中的函数挂载到模块的导出上
Object.assign(module.exports, require('./dist/node-cjs/publicUtils.cjs'))
// async functions, can be redirect from ESM build
// 异步函数,可以从ESM构建中重定向
// 这样做的目的是将这些异步函数调用时动态地从 ./dist/node/index.js 模块中导入,实现了按需加载的效果
const asyncFunctions = [
'build',
'createServer',
'preview',
'transformWithEsbuild',
'resolveConfig',
'optimizeDeps',
'formatPostcssSourceMap',
'loadConfigFromFile',
'preprocessCSS',
]
asyncFunctions.forEach((name) => {
module.exports[name] = (...args) =>
import('./dist/node/index.js').then((i) => i[name](...args))
})
// some sync functions are marked not supported due to their complexity and uncommon usage
// 一些同步函数由于其复杂性和不常见的使用方式而被标记为不支持
// 简单来说,就是这两个函数不支持commonjs,当在commonjs中使用时需要报错提示,提示使用ESM或者动态导入
const unsupportedCJS = ['resolvePackageEntry', 'resolvePackageData']
unsupportedCJS.forEach((name) => {
module.exports[name] = () => {
// ESM是JavaScript的官方模块系统,使用import和export关键字来导入和导出模块。
// 动态导入(dynamic imports)是一种在运行时根据条件或需要来动态加载模块的方式。它使用import()函数来实现,函数返回一个Promise,在Promise解析后可以访问导入的模块。
// 两种方式的区别在于使用时机和语法形式。ESM适用于在模块的顶层直接导入使用,而动态导入适用于在运行时根据需要动态加载模块。
throw new Error(
`"${name}" is not supported in CJS build of Vite 4.\nPlease use ESM or dynamic imports \`const { ${name} } = await import('vite')\`.`,
)
}
})
/* eslint-disable no-restricted-globals */
// 禁用 ESLint 的 no-restricted-globals 规则,不进行全局变量的限制检查
// type utils
module.exports.defineConfig = (config) => config
// proxy cjs utils (sync functions)
// 通过 Object.assign 将 ./dist/node-cjs/publicUtils.cjs 模块中的函数挂载到模块的导出上
Object.assign(module.exports, require('./dist/node-cjs/publicUtils.cjs'))
// async functions, can be redirect from ESM build
// 异步函数,可以从ESM构建中重定向
// 这样做的目的是将这些异步函数调用时动态地从 ./dist/node/index.js 模块中导入,实现了按需加载的效果
const asyncFunctions = [
'build',
'createServer',
'preview',
'transformWithEsbuild',
'resolveConfig',
'optimizeDeps',
'formatPostcssSourceMap',
'loadConfigFromFile',
'preprocessCSS',
]
asyncFunctions.forEach((name) => {
module.exports[name] = (...args) =>
import('./dist/node/index.js').then((i) => i[name](...args))
})
// some sync functions are marked not supported due to their complexity and uncommon usage
// 一些同步函数由于其复杂性和不常见的使用方式而被标记为不支持
// 简单来说,就是这两个函数不支持commonjs,当在commonjs中使用时需要报错提示,提示使用ESM或者动态导入
const unsupportedCJS = ['resolvePackageEntry', 'resolvePackageData']
unsupportedCJS.forEach((name) => {
module.exports[name] = () => {
// ESM是JavaScript的官方模块系统,使用import和export关键字来导入和导出模块。
// 动态导入(dynamic imports)是一种在运行时根据条件或需要来动态加载模块的方式。它使用import()函数来实现,函数返回一个Promise,在Promise解析后可以访问导入的模块。
// 两种方式的区别在于使用时机和语法形式。ESM适用于在模块的顶层直接导入使用,而动态导入适用于在运行时根据需要动态加载模块。
throw new Error(
`"${name}" is not supported in CJS build of Vite 4.\nPlease use ESM or dynamic imports \`const { ${name} } = await import('vite')\`.`,
)
}
})
rollup.config.ts
ts
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import nodeResolve from '@rollup/plugin-node-resolve'
import typescript from '@rollup/plugin-typescript'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import MagicString from 'magic-string'
import type { Plugin, RollupOptions } from 'rollup'
import { defineConfig } from 'rollup'
import licensePlugin from './rollupLicensePlugin'
// 读取当前在目录下的 package.json 文件,并将其内容解析为 JSON 对象
const pkg = JSON.parse(
// new URL() 是 JavaScript 中的一个内置函数,用于创建 URL 对象。它接受一个 URL 字符串作为参数,并返回一个代表该 URL 的 URL 对象
// windows系统URL打印结果:
// URL {
// href: 'file:///D:/node/1.txt',
// origin: 'null',
// protocol: 'file:',
// username: '',
// password: '',
// host: '',
// hostname: '',
// port: '',
// pathname: '/D:/node/1.txt',
// search: '',
// searchParams: URLSearchParams {},
// hash: ''
// }
// import.meta.url 是在 ECMAScript 模块中可用的一个元数据属性,用于获取当前模块文件的 URL 地址。它返回一个包含模块文件路径的字符串
// new URL('./package.json', import.meta.url) 的作用是创建一个 URL 对象,用于定位相对于当前模块文件的 package.json 文件的路径
// readFileSync 读取文件时,如果没有指定编码方式,那么返回的是一个 Buffer 对象。因此,为了将文件内容以字符串的形式进行处理,需要使用 toString() 方法进行转换
readFileSync(new URL('./package.json', import.meta.url)).toString(),
)
// __dirname 变量是 CommonJS 模块系统中提供的一个特殊变量,用于表示当前模块文件所在的目录路径。而在 ECMAScript 模块中,没有类似的内置变量来获取当前模块的目录路径
// 因此,在 ECMAScript 模块中,可以使用 import.meta.url 获取当前模块文件的 URL,然后通过 new URL() 创建一个 URL 对象,并使用 fileURLToPath() 将 URL 转换为文件路径字符串,以获得当前模块文件所在的目录路径,模拟类似于 CommonJS 中的 __dirname 的功能
const __dirname = fileURLToPath(new URL('.', import.meta.url))
// rollup打包配置
const envConfig = defineConfig({
// 打包的输入文件路径
input: path.resolve(__dirname, 'src/client/env.ts'),
// 使用 typescript 插件,并通过 tsconfig 属性指定 TypeScript 配置文件的路径
plugins: [
typescript({
tsconfig: path.resolve(__dirname, 'src/client/tsconfig.json'),
}),
],
// 打包的输出文件配置
output: {
// 输出文件的路径
file: path.resolve(__dirname, 'dist/client', 'env.mjs'),
// 生成源映射文件
sourcemap: true,
// 自定义源映射文件中源文件路径的转换。relativeSourcePath 表示源文件相对于构建输出目录的路径,通过 path.basename() 函数获取源文件的基本文件名
sourcemapPathTransform(relativeSourcePath) {
return path.basename(relativeSourcePath)
},
// 哪些源文件不包含在生成的源映射文件中。return true 表示所有源文件都不包含在源映射文件中
sourcemapIgnoreList() {
return true
},
},
})
const clientConfig = defineConfig({
input: path.resolve(__dirname, 'src/client/client.ts'),
// 排除(不打包)的外部依赖项。./env 和 @vite/env 这两个模块将作为外部依赖项,不会被打包进最终的输出文件中
external: ['./env', '@vite/env'],
plugins: [
typescript({
tsconfig: path.resolve(__dirname, 'src/client/tsconfig.json'),
}),
],
output: {
file: path.resolve(__dirname, 'dist/client', 'client.mjs'),
sourcemap: true,
sourcemapPathTransform(relativeSourcePath) {
return path.basename(relativeSourcePath)
},
sourcemapIgnoreList() {
return true
},
},
})
const sharedNodeOptions = defineConfig({
treeshake: {
// 对于外部依赖项,不考虑其副作用,可以进行 Tree-shaking
moduleSideEffects: 'no-external',
// 不考虑属性读取操作的副作用,可以进行 Tree-shaking
propertyReadSideEffects: false,
// 表示禁用针对 try-catch 语句的优化,以避免产生副作用,可以进行 Tree-shaking
tryCatchDeoptimization: false,
},
output: {
// 指定输出目录的路径
dir: './dist',
// 指定生成的入口文件名的格式,'node/[name].js' 表示生成的入口文件放在 node 目录下,并以原始文件名命名
entryFileNames: `node/[name].js`,
// 指定生成的分块文件名的格式,'node/chunks/dep-[hash].js' 表示生成的分块文件放在 node/chunks 目录下,并以 dep-[hash].js 格式命名,其中 [hash] 是根据文件内容生成的哈希值。
chunkFileNames: 'node/chunks/dep-[hash].js',
// 指定模块的导出方式,'named' 表示使用命名导出
exports: 'named',
// 指定输出的模块格式,'esm' 表示输出为 ES 模块。
format: 'esm',
// 表示禁用外部依赖项的实时绑定,即不会在运行时绑定外部依赖项
externalLiveBindings: false,
// 表示禁用对象冻结,即不会冻结输出的对象
freeze: false,
},
// 处理构建过程中的警告信息。如果警告信息中包含 'Circular dependency'(循环依赖)则忽略该警告,否则调用 warn 函数处理警告
onwarn(warning, warn) {
if (warning.message.includes('Circular dependency')) {
return
}
warn(warning)
},
})
function createNodePlugins(
isProduction: boolean,
sourceMap: boolean,
declarationDir: string | false,
): (Plugin | false)[] {
// 返回由插件组成的数组,用于 Rollup 的配置
return [
// rollup 解析 Node.js 模块的插件,通过 preferBuiltins: true 选项设置偏好使用内置模块
nodeResolve({ preferBuiltins: true }),
// 处理 TypeScript 代码的插件,通过提供的 tsconfig 指定 TypeScript 配置文件路径,并根据参数配置是否生成源映射和声明文件
typescript({
tsconfig: path.resolve(__dirname, 'src/node/tsconfig.json'),
sourceMap,
declaration: declarationDir !== false,
declarationDir: declarationDir !== false ? declarationDir : undefined,
}),
// Some deps have try...catch require of optional deps, but rollup will
// generate code that force require them upfront for side effects.
// Shim them with eval() so rollup can skip these calls.
// 某些依赖项在尝试使用可选的依赖项时会使用 try...catch require 的方式,但是 Rollup 会生成代码,强制要求提前引入这些依赖项,以实现副作用。使用 eval() 来进行替代,以便让 Rollup 能够跳过这些调用。
// (仅在生产环境下):处理一些依赖项的插件
isProduction &&
shimDepsPlugin({
// chokidar -> fsevents
'fsevents-handler.js': {
src: `require('fsevents')`,
replacement: `__require('fsevents')`,
},
// postcss-import -> sugarss
'process-content.js': {
src: 'require("sugarss")',
replacement: `__require('sugarss')`,
},
'lilconfig/dist/index.js': {
pattern: /: require,/g,
replacement: `: __require,`,
},
// postcss-load-config calls require after register ts-node
'postcss-load-config/src/index.js': {
pattern: /require(?=\((configFile|'ts-node')\))/g,
replacement: `__require`,
},
'json-stable-stringify/index.js': {
pattern: /^var json = typeof JSON.+require\('jsonify'\);$/gm,
replacement: 'var json = JSON',
},
// postcss-import uses the `resolve` dep if the `resolve` option is not passed.
// However, we always pass the `resolve` option. Remove this import to avoid
// bundling the `resolve` dep.
'postcss-import/index.js': {
src: 'const resolveId = require("./lib/resolve-id")',
replacement: 'const resolveId = (id) => id',
},
}),
// 将 CommonJS 模块转换为 ES6 模块的插件,配置了扩展名为 .js 的文件,并忽略了 bufferutil 和 utf-8-validate 这两个可选的 ws 依赖
commonjs({
extensions: ['.js'],
// Optional peer deps of ws. Native deps that are mostly for performance.
// Since ws is not that perf critical for us, just ignore these deps.
ignore: ['bufferutil', 'utf-8-validate'],
}),
// 解析 JSON 文件的插件
json(),
//(仅在生产环境下):用于生成版权信息的插件,根据根目录版权文件路径和参数生成许可证信息
isProduction &&
licensePlugin(
path.resolve(__dirname, 'LICENSE.md'),
'Vite core license',
'Vite',
),
// 修复 CommonJS 模块中的一些问题的插件
cjsPatchPlugin(),
]
}
function createNodeConfig(isProduction: boolean) {
return defineConfig({
...sharedNodeOptions,
input: {
index: path.resolve(__dirname, 'src/node/index.ts'),
cli: path.resolve(__dirname, 'src/node/cli.ts'),
constants: path.resolve(__dirname, 'src/node/constants.ts'),
},
output: {
...sharedNodeOptions.output,
sourcemap: !isProduction,
},
external: [
'fsevents',
...Object.keys(pkg.dependencies),
...(isProduction ? [] : Object.keys(pkg.devDependencies)),
],
plugins: createNodePlugins(
isProduction,
!isProduction,
// in production we use api-extractor for dts generation
// in development we need to rely on the rollup ts plugin
isProduction ? false : './dist/node',
),
})
}
function createCjsConfig(isProduction: boolean) {
return defineConfig({
...sharedNodeOptions,
input: {
publicUtils: path.resolve(__dirname, 'src/node/publicUtils.ts'),
},
output: {
dir: './dist',
entryFileNames: `node-cjs/[name].cjs`,
chunkFileNames: 'node-cjs/chunks/dep-[hash].js',
exports: 'named',
format: 'cjs',
externalLiveBindings: false,
freeze: false,
sourcemap: false,
},
external: [
'fsevents',
...Object.keys(pkg.dependencies),
...(isProduction ? [] : Object.keys(pkg.devDependencies)),
],
plugins: [...createNodePlugins(false, false, false), bundleSizeLimit(120)],
})
}
export default (commandLineArgs: any): RollupOptions[] => {
const isDev = commandLineArgs.watch
const isProduction = !isDev
return defineConfig([
envConfig,
clientConfig,
createNodeConfig(isProduction),
createCjsConfig(isProduction),
])
}
// #region ======== Plugins ========
interface ShimOptions {
src?: string
replacement: string
pattern?: RegExp
}
function shimDepsPlugin(deps: Record<string, ShimOptions>): Plugin {
const transformed: Record<string, boolean> = {}
return {
name: 'shim-deps',
transform(code, id) {
for (const file in deps) {
if (id.replace(/\\/g, '/').endsWith(file)) {
const { src, replacement, pattern } = deps[file]
const magicString = new MagicString(code)
if (src) {
const pos = code.indexOf(src)
if (pos < 0) {
this.error(
`Could not find expected src "${src}" in file "${file}"`,
)
}
transformed[file] = true
magicString.overwrite(pos, pos + src.length, replacement)
console.log(`shimmed: ${file}`)
}
if (pattern) {
let match
while ((match = pattern.exec(code))) {
transformed[file] = true
const start = match.index
const end = start + match[0].length
magicString.overwrite(start, end, replacement)
}
if (!transformed[file]) {
this.error(
`Could not find expected pattern "${pattern}" in file "${file}"`,
)
}
console.log(`shimmed: ${file}`)
}
return {
code: magicString.toString(),
map: magicString.generateMap({ hires: true }),
}
}
}
},
buildEnd(err) {
if (!err) {
for (const file in deps) {
if (!transformed[file]) {
this.error(
`Did not find "${file}" which is supposed to be shimmed, was the file renamed?`,
)
}
}
}
},
}
}
/**
* Inject CJS Context for each deps chunk
*/
/**
* 为每个依赖项(deps)的代码块注入 CommonJS(CJS)的上下文
*/
// 这是一个 rollup 插件函数。作用是为每个依赖项的代码块注入 CJS 的上下文,以模拟在 CJS 环境下的运行。这样可以确保依赖项在 Rollup 打包后在 CJS 环境中正常运行
function cjsPatchPlugin(): Plugin {
const cjsPatch = `
import { fileURLToPath as __cjs_fileURLToPath } from 'node:url';
import { dirname as __cjs_dirname } from 'node:path';
import { createRequire as __cjs_createRequire } from 'node:module';
const __filename = __cjs_fileURLToPath(import.meta.url);
const __dirname = __cjs_dirname(__filename);
const require = __cjs_createRequire(import.meta.url);
const __require = require;
`.trimStart()
// trimStart() 方法被用于移除 cjsPatch 字符串开头的空格。这样做是为了确保在将 cjsPatch 插入到代码块中时不会出现多余的空格或缩进,以保持代码的格式化和可读性
return {
// 插件的名称
name: 'cjs-chunk-patch',
// renderChunk 钩子用于在生成每个块(chunk)的过程中进行自定义的代码转换和处理操作。它在 Rollup 构建过程中的特定阶段被调用
// 具体调用时机如下:
// 在 Rollup 进行代码分块(chunking)的阶段,对每个块进行处理时调用
// 可能会在 Rollup 执行输出(output)阶段之前的某个时刻被调用,具体取决于配置和构建流程
// 在每个块的处理过程中,renderChunk 方法会接收两个参数:
// code:当前块的原始代码。
// chunk:表示当前块的对象,包含有关块的信息,如文件名、模块依赖等
renderChunk(code, chunk) {
// 如果依赖块的文件名不包含 chunks/dep-,则不用插入
if (!chunk.fileName.includes('chunks/dep-')) return
// 匹配 code 字符串中以 import 开头的一连串 import 语句
// /^(?:import[\s\S]*?;\s*)+/ 是一个正则表达式,具体含义如下:
// ^ 表示匹配字符串的开头
// (?:import[\s\S]*?;\s*)+ 是一个非捕获型分组,用来匹配一个或多个以 import 开头的一连串 import 语句,后面跟着零个或多个空白字符和分号
// import 匹配实际的字符串 "import"
// [\s\S]*? 匹配任意的空白字符或非空白字符,使用非贪婪模式
// ; 匹配分号
// \s* 匹配零个或多个空白字符
// match 是一个数组,其中存储了匹配到的结果。如果匹配成功,数组的第一个元素是与正则表达式匹配的字符串,后面的元素是与正则表达式中的捕获组匹配的字符串(如果有的话)
const match = code.match(/^(?:import[\s\S]*?;\s*)+/)
// index 是一个变量,用来存储匹配到的字符串在 code 中的起始位置。如果匹配成功,index 的值为匹配到的字符串在 code 中的索引位置。如果没有匹配到,则 index 的值为 0。
// 通过匹配 import 语句,可以获取到最后一个 import 语句在 code 中的位置。这个位置信息用于后续的操作,插入特定的代码片段到最后一个 import 语句之后。
const index = match ? match.index! + match[0].length : 0
// s 是一个 MagicString 的实例,它用于处理字符串并进行插入、删除和替换等操作。
const s = new MagicString(code)
// inject after the last `import`
// 插入到最后一个import语句之后
s.appendRight(index, cjsPatch)
console.log('patched cjs context: ' + chunk.fileName)
return {
// 返回修改后的代码字符串
code: s.toString(),
// 生成源映射,其中 { hires: true } 表示生成高分辨率的源映射
map: s.generateMap({ hires: true }),
}
},
}
}
/**
* Guard the bundle size
*
* @param limit size in KB
*/
/**
* 限制打包大小
*
* @param limit 大小(以 KB 为单位)
*/
// 这是一个用于限制打包大小的 rollup 插件函数。它接收一个大小限制参数(以 KB 为单位)
function bundleSizeLimit(limit: number): Plugin {
return {
// 插件的名称
name: 'bundle-limit',
// 生成打包文件的钩子函数,当打包过程完成后调用
generateBundle(options, bundle) {
// Object.values(bundle) 返回一个打包文件对象(bundle)中所有值的数组。然后,通过 .map() 方法遍历数组的每个元素,检查元素是否具有 code 属性。如果有 code 属性,则取其值,否则返回空字符串。这样可以过滤掉那些没有 code 属性的元素。
// 接下来,使用 .join('') 方法将过滤后的字符串数组拼接成一个单独的字符串,表示所有打包文件的代码内容。
// 最后,Buffer.byteLength() 函数计算该字符串的字节长度。这里使用 'utf-8' 编码来计算字符串的字节长度。
// 因此,size 变量将保存打包文件的大小(以字节为单位),用于后续的大小比较和限制。
const size = Buffer.byteLength(
Object.values(bundle)
.map((i) => ('code' in i ? i.code : ''))
.join(''),
'utf-8',
)
// 大小换算成kb单位
const kb = size / 1024
if (kb > limit) {
throw new Error(
`Bundle size exceeded ${limit}kb, current size is ${kb.toFixed(
2,
)}kb.`,
)
}
},
}
}
// #endregion
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import nodeResolve from '@rollup/plugin-node-resolve'
import typescript from '@rollup/plugin-typescript'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import MagicString from 'magic-string'
import type { Plugin, RollupOptions } from 'rollup'
import { defineConfig } from 'rollup'
import licensePlugin from './rollupLicensePlugin'
// 读取当前在目录下的 package.json 文件,并将其内容解析为 JSON 对象
const pkg = JSON.parse(
// new URL() 是 JavaScript 中的一个内置函数,用于创建 URL 对象。它接受一个 URL 字符串作为参数,并返回一个代表该 URL 的 URL 对象
// windows系统URL打印结果:
// URL {
// href: 'file:///D:/node/1.txt',
// origin: 'null',
// protocol: 'file:',
// username: '',
// password: '',
// host: '',
// hostname: '',
// port: '',
// pathname: '/D:/node/1.txt',
// search: '',
// searchParams: URLSearchParams {},
// hash: ''
// }
// import.meta.url 是在 ECMAScript 模块中可用的一个元数据属性,用于获取当前模块文件的 URL 地址。它返回一个包含模块文件路径的字符串
// new URL('./package.json', import.meta.url) 的作用是创建一个 URL 对象,用于定位相对于当前模块文件的 package.json 文件的路径
// readFileSync 读取文件时,如果没有指定编码方式,那么返回的是一个 Buffer 对象。因此,为了将文件内容以字符串的形式进行处理,需要使用 toString() 方法进行转换
readFileSync(new URL('./package.json', import.meta.url)).toString(),
)
// __dirname 变量是 CommonJS 模块系统中提供的一个特殊变量,用于表示当前模块文件所在的目录路径。而在 ECMAScript 模块中,没有类似的内置变量来获取当前模块的目录路径
// 因此,在 ECMAScript 模块中,可以使用 import.meta.url 获取当前模块文件的 URL,然后通过 new URL() 创建一个 URL 对象,并使用 fileURLToPath() 将 URL 转换为文件路径字符串,以获得当前模块文件所在的目录路径,模拟类似于 CommonJS 中的 __dirname 的功能
const __dirname = fileURLToPath(new URL('.', import.meta.url))
// rollup打包配置
const envConfig = defineConfig({
// 打包的输入文件路径
input: path.resolve(__dirname, 'src/client/env.ts'),
// 使用 typescript 插件,并通过 tsconfig 属性指定 TypeScript 配置文件的路径
plugins: [
typescript({
tsconfig: path.resolve(__dirname, 'src/client/tsconfig.json'),
}),
],
// 打包的输出文件配置
output: {
// 输出文件的路径
file: path.resolve(__dirname, 'dist/client', 'env.mjs'),
// 生成源映射文件
sourcemap: true,
// 自定义源映射文件中源文件路径的转换。relativeSourcePath 表示源文件相对于构建输出目录的路径,通过 path.basename() 函数获取源文件的基本文件名
sourcemapPathTransform(relativeSourcePath) {
return path.basename(relativeSourcePath)
},
// 哪些源文件不包含在生成的源映射文件中。return true 表示所有源文件都不包含在源映射文件中
sourcemapIgnoreList() {
return true
},
},
})
const clientConfig = defineConfig({
input: path.resolve(__dirname, 'src/client/client.ts'),
// 排除(不打包)的外部依赖项。./env 和 @vite/env 这两个模块将作为外部依赖项,不会被打包进最终的输出文件中
external: ['./env', '@vite/env'],
plugins: [
typescript({
tsconfig: path.resolve(__dirname, 'src/client/tsconfig.json'),
}),
],
output: {
file: path.resolve(__dirname, 'dist/client', 'client.mjs'),
sourcemap: true,
sourcemapPathTransform(relativeSourcePath) {
return path.basename(relativeSourcePath)
},
sourcemapIgnoreList() {
return true
},
},
})
const sharedNodeOptions = defineConfig({
treeshake: {
// 对于外部依赖项,不考虑其副作用,可以进行 Tree-shaking
moduleSideEffects: 'no-external',
// 不考虑属性读取操作的副作用,可以进行 Tree-shaking
propertyReadSideEffects: false,
// 表示禁用针对 try-catch 语句的优化,以避免产生副作用,可以进行 Tree-shaking
tryCatchDeoptimization: false,
},
output: {
// 指定输出目录的路径
dir: './dist',
// 指定生成的入口文件名的格式,'node/[name].js' 表示生成的入口文件放在 node 目录下,并以原始文件名命名
entryFileNames: `node/[name].js`,
// 指定生成的分块文件名的格式,'node/chunks/dep-[hash].js' 表示生成的分块文件放在 node/chunks 目录下,并以 dep-[hash].js 格式命名,其中 [hash] 是根据文件内容生成的哈希值。
chunkFileNames: 'node/chunks/dep-[hash].js',
// 指定模块的导出方式,'named' 表示使用命名导出
exports: 'named',
// 指定输出的模块格式,'esm' 表示输出为 ES 模块。
format: 'esm',
// 表示禁用外部依赖项的实时绑定,即不会在运行时绑定外部依赖项
externalLiveBindings: false,
// 表示禁用对象冻结,即不会冻结输出的对象
freeze: false,
},
// 处理构建过程中的警告信息。如果警告信息中包含 'Circular dependency'(循环依赖)则忽略该警告,否则调用 warn 函数处理警告
onwarn(warning, warn) {
if (warning.message.includes('Circular dependency')) {
return
}
warn(warning)
},
})
function createNodePlugins(
isProduction: boolean,
sourceMap: boolean,
declarationDir: string | false,
): (Plugin | false)[] {
// 返回由插件组成的数组,用于 Rollup 的配置
return [
// rollup 解析 Node.js 模块的插件,通过 preferBuiltins: true 选项设置偏好使用内置模块
nodeResolve({ preferBuiltins: true }),
// 处理 TypeScript 代码的插件,通过提供的 tsconfig 指定 TypeScript 配置文件路径,并根据参数配置是否生成源映射和声明文件
typescript({
tsconfig: path.resolve(__dirname, 'src/node/tsconfig.json'),
sourceMap,
declaration: declarationDir !== false,
declarationDir: declarationDir !== false ? declarationDir : undefined,
}),
// Some deps have try...catch require of optional deps, but rollup will
// generate code that force require them upfront for side effects.
// Shim them with eval() so rollup can skip these calls.
// 某些依赖项在尝试使用可选的依赖项时会使用 try...catch require 的方式,但是 Rollup 会生成代码,强制要求提前引入这些依赖项,以实现副作用。使用 eval() 来进行替代,以便让 Rollup 能够跳过这些调用。
// (仅在生产环境下):处理一些依赖项的插件
isProduction &&
shimDepsPlugin({
// chokidar -> fsevents
'fsevents-handler.js': {
src: `require('fsevents')`,
replacement: `__require('fsevents')`,
},
// postcss-import -> sugarss
'process-content.js': {
src: 'require("sugarss")',
replacement: `__require('sugarss')`,
},
'lilconfig/dist/index.js': {
pattern: /: require,/g,
replacement: `: __require,`,
},
// postcss-load-config calls require after register ts-node
'postcss-load-config/src/index.js': {
pattern: /require(?=\((configFile|'ts-node')\))/g,
replacement: `__require`,
},
'json-stable-stringify/index.js': {
pattern: /^var json = typeof JSON.+require\('jsonify'\);$/gm,
replacement: 'var json = JSON',
},
// postcss-import uses the `resolve` dep if the `resolve` option is not passed.
// However, we always pass the `resolve` option. Remove this import to avoid
// bundling the `resolve` dep.
'postcss-import/index.js': {
src: 'const resolveId = require("./lib/resolve-id")',
replacement: 'const resolveId = (id) => id',
},
}),
// 将 CommonJS 模块转换为 ES6 模块的插件,配置了扩展名为 .js 的文件,并忽略了 bufferutil 和 utf-8-validate 这两个可选的 ws 依赖
commonjs({
extensions: ['.js'],
// Optional peer deps of ws. Native deps that are mostly for performance.
// Since ws is not that perf critical for us, just ignore these deps.
ignore: ['bufferutil', 'utf-8-validate'],
}),
// 解析 JSON 文件的插件
json(),
//(仅在生产环境下):用于生成版权信息的插件,根据根目录版权文件路径和参数生成许可证信息
isProduction &&
licensePlugin(
path.resolve(__dirname, 'LICENSE.md'),
'Vite core license',
'Vite',
),
// 修复 CommonJS 模块中的一些问题的插件
cjsPatchPlugin(),
]
}
function createNodeConfig(isProduction: boolean) {
return defineConfig({
...sharedNodeOptions,
input: {
index: path.resolve(__dirname, 'src/node/index.ts'),
cli: path.resolve(__dirname, 'src/node/cli.ts'),
constants: path.resolve(__dirname, 'src/node/constants.ts'),
},
output: {
...sharedNodeOptions.output,
sourcemap: !isProduction,
},
external: [
'fsevents',
...Object.keys(pkg.dependencies),
...(isProduction ? [] : Object.keys(pkg.devDependencies)),
],
plugins: createNodePlugins(
isProduction,
!isProduction,
// in production we use api-extractor for dts generation
// in development we need to rely on the rollup ts plugin
isProduction ? false : './dist/node',
),
})
}
function createCjsConfig(isProduction: boolean) {
return defineConfig({
...sharedNodeOptions,
input: {
publicUtils: path.resolve(__dirname, 'src/node/publicUtils.ts'),
},
output: {
dir: './dist',
entryFileNames: `node-cjs/[name].cjs`,
chunkFileNames: 'node-cjs/chunks/dep-[hash].js',
exports: 'named',
format: 'cjs',
externalLiveBindings: false,
freeze: false,
sourcemap: false,
},
external: [
'fsevents',
...Object.keys(pkg.dependencies),
...(isProduction ? [] : Object.keys(pkg.devDependencies)),
],
plugins: [...createNodePlugins(false, false, false), bundleSizeLimit(120)],
})
}
export default (commandLineArgs: any): RollupOptions[] => {
const isDev = commandLineArgs.watch
const isProduction = !isDev
return defineConfig([
envConfig,
clientConfig,
createNodeConfig(isProduction),
createCjsConfig(isProduction),
])
}
// #region ======== Plugins ========
interface ShimOptions {
src?: string
replacement: string
pattern?: RegExp
}
function shimDepsPlugin(deps: Record<string, ShimOptions>): Plugin {
const transformed: Record<string, boolean> = {}
return {
name: 'shim-deps',
transform(code, id) {
for (const file in deps) {
if (id.replace(/\\/g, '/').endsWith(file)) {
const { src, replacement, pattern } = deps[file]
const magicString = new MagicString(code)
if (src) {
const pos = code.indexOf(src)
if (pos < 0) {
this.error(
`Could not find expected src "${src}" in file "${file}"`,
)
}
transformed[file] = true
magicString.overwrite(pos, pos + src.length, replacement)
console.log(`shimmed: ${file}`)
}
if (pattern) {
let match
while ((match = pattern.exec(code))) {
transformed[file] = true
const start = match.index
const end = start + match[0].length
magicString.overwrite(start, end, replacement)
}
if (!transformed[file]) {
this.error(
`Could not find expected pattern "${pattern}" in file "${file}"`,
)
}
console.log(`shimmed: ${file}`)
}
return {
code: magicString.toString(),
map: magicString.generateMap({ hires: true }),
}
}
}
},
buildEnd(err) {
if (!err) {
for (const file in deps) {
if (!transformed[file]) {
this.error(
`Did not find "${file}" which is supposed to be shimmed, was the file renamed?`,
)
}
}
}
},
}
}
/**
* Inject CJS Context for each deps chunk
*/
/**
* 为每个依赖项(deps)的代码块注入 CommonJS(CJS)的上下文
*/
// 这是一个 rollup 插件函数。作用是为每个依赖项的代码块注入 CJS 的上下文,以模拟在 CJS 环境下的运行。这样可以确保依赖项在 Rollup 打包后在 CJS 环境中正常运行
function cjsPatchPlugin(): Plugin {
const cjsPatch = `
import { fileURLToPath as __cjs_fileURLToPath } from 'node:url';
import { dirname as __cjs_dirname } from 'node:path';
import { createRequire as __cjs_createRequire } from 'node:module';
const __filename = __cjs_fileURLToPath(import.meta.url);
const __dirname = __cjs_dirname(__filename);
const require = __cjs_createRequire(import.meta.url);
const __require = require;
`.trimStart()
// trimStart() 方法被用于移除 cjsPatch 字符串开头的空格。这样做是为了确保在将 cjsPatch 插入到代码块中时不会出现多余的空格或缩进,以保持代码的格式化和可读性
return {
// 插件的名称
name: 'cjs-chunk-patch',
// renderChunk 钩子用于在生成每个块(chunk)的过程中进行自定义的代码转换和处理操作。它在 Rollup 构建过程中的特定阶段被调用
// 具体调用时机如下:
// 在 Rollup 进行代码分块(chunking)的阶段,对每个块进行处理时调用
// 可能会在 Rollup 执行输出(output)阶段之前的某个时刻被调用,具体取决于配置和构建流程
// 在每个块的处理过程中,renderChunk 方法会接收两个参数:
// code:当前块的原始代码。
// chunk:表示当前块的对象,包含有关块的信息,如文件名、模块依赖等
renderChunk(code, chunk) {
// 如果依赖块的文件名不包含 chunks/dep-,则不用插入
if (!chunk.fileName.includes('chunks/dep-')) return
// 匹配 code 字符串中以 import 开头的一连串 import 语句
// /^(?:import[\s\S]*?;\s*)+/ 是一个正则表达式,具体含义如下:
// ^ 表示匹配字符串的开头
// (?:import[\s\S]*?;\s*)+ 是一个非捕获型分组,用来匹配一个或多个以 import 开头的一连串 import 语句,后面跟着零个或多个空白字符和分号
// import 匹配实际的字符串 "import"
// [\s\S]*? 匹配任意的空白字符或非空白字符,使用非贪婪模式
// ; 匹配分号
// \s* 匹配零个或多个空白字符
// match 是一个数组,其中存储了匹配到的结果。如果匹配成功,数组的第一个元素是与正则表达式匹配的字符串,后面的元素是与正则表达式中的捕获组匹配的字符串(如果有的话)
const match = code.match(/^(?:import[\s\S]*?;\s*)+/)
// index 是一个变量,用来存储匹配到的字符串在 code 中的起始位置。如果匹配成功,index 的值为匹配到的字符串在 code 中的索引位置。如果没有匹配到,则 index 的值为 0。
// 通过匹配 import 语句,可以获取到最后一个 import 语句在 code 中的位置。这个位置信息用于后续的操作,插入特定的代码片段到最后一个 import 语句之后。
const index = match ? match.index! + match[0].length : 0
// s 是一个 MagicString 的实例,它用于处理字符串并进行插入、删除和替换等操作。
const s = new MagicString(code)
// inject after the last `import`
// 插入到最后一个import语句之后
s.appendRight(index, cjsPatch)
console.log('patched cjs context: ' + chunk.fileName)
return {
// 返回修改后的代码字符串
code: s.toString(),
// 生成源映射,其中 { hires: true } 表示生成高分辨率的源映射
map: s.generateMap({ hires: true }),
}
},
}
}
/**
* Guard the bundle size
*
* @param limit size in KB
*/
/**
* 限制打包大小
*
* @param limit 大小(以 KB 为单位)
*/
// 这是一个用于限制打包大小的 rollup 插件函数。它接收一个大小限制参数(以 KB 为单位)
function bundleSizeLimit(limit: number): Plugin {
return {
// 插件的名称
name: 'bundle-limit',
// 生成打包文件的钩子函数,当打包过程完成后调用
generateBundle(options, bundle) {
// Object.values(bundle) 返回一个打包文件对象(bundle)中所有值的数组。然后,通过 .map() 方法遍历数组的每个元素,检查元素是否具有 code 属性。如果有 code 属性,则取其值,否则返回空字符串。这样可以过滤掉那些没有 code 属性的元素。
// 接下来,使用 .join('') 方法将过滤后的字符串数组拼接成一个单独的字符串,表示所有打包文件的代码内容。
// 最后,Buffer.byteLength() 函数计算该字符串的字节长度。这里使用 'utf-8' 编码来计算字符串的字节长度。
// 因此,size 变量将保存打包文件的大小(以字节为单位),用于后续的大小比较和限制。
const size = Buffer.byteLength(
Object.values(bundle)
.map((i) => ('code' in i ? i.code : ''))
.join(''),
'utf-8',
)
// 大小换算成kb单位
const kb = size / 1024
if (kb > limit) {
throw new Error(
`Bundle size exceeded ${limit}kb, current size is ${kb.toFixed(
2,
)}kb.`,
)
}
},
}
}
// #endregion