Skip to content
索引

源码解析

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

Released under the MIT License.