VUE源码分析-打包命令《dev.js》

335 阅读4分钟

重要命令

  1. npm run 命令会将 -- 之后的参数传递给脚本 正确的参数传入命令 npm run dev -- -fesm -p -i reactivity

详细代码分析

// 导入esbuild和其他Node.js内置模块,以及第三方插件esbuild-plugin-polyfill-node。
import esbuild from 'esbuild'
import fs from 'node:fs'
import { dirname, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import { parseArgs } from 'node:util'
import { polyfillNode } from 'esbuild-plugin-polyfill-node'
// 创建一个require函数,用于动态加载模块。
const require = createRequire(import.meta.url)
// 获取当前文件的目录路径。
const __dirname = dirname(fileURLToPath(import.meta.url))
// npm run 命令会将 -- 之后的参数传递给脚本 正确的参数传入命令  npm run dev -- -fesm -p -i reactivity
// 使用parseArgs解析命令行参数,提取构建格式(format)、是否为生产环境(prod)和是否内联依赖(inlineDeps)。
// positionals存储位置参数,即目标包的名称。
// 配置选项:定义了三个命令行选项 format、prod 和 inline,分别对应 -f、-p 和 -i 短选项,并设置了默认值。
// 解析参数:使用 parseArgs 函数解析命令行参数,允许位置参数(即不带选项的参数)。
// 提取结果:从解析结果中提取 format、prod 和 inline 的值,并将其赋值给 rawFormat、prod 和 inlineDeps 变量。
// parseArgs 默认返回的数据中包含了位置参数(positionals)、值(values-对应的是options传入的参数)。
const {
  values: { format: rawFormat, prod, inline: inlineDeps },
  positionals,
} = parseArgs({
  // allowPositionals 是一个配置选项,通常用于命令行参数解析库(如 yargs 或 commander)。它的作用是允许解析位置参数(即不带选项的参数)。
  // node script.js --format=global --prod file1 file2
  // 在这个例子中,file1 和 file2 就是位置参数。由于 allowPositionals 被设置为 true,这些位置参数会被正确解析并存储在 positionals 中。
  allowPositionals: true,
  options: {
    format: {
      type: 'string',
      short: 'f',
      default: 'global',
    },
    prod: {
      type: 'boolean',
      short: 'p',
      default: false,
    },
    inline: {
      type: 'boolean',
      short: 'i',
      default: false,
    },
  },
})
// 确定构建格式,默认为global。
// 确定目标包,默认为vue。
const format = rawFormat || 'global' // 默认打包模式
const targets = positionals.length ? positionals : ['vue']
console.log(`targets: ${targets.join(', ')},  模式: ${format}, 是否内联依赖:${inlineDeps},存储位置参数,即目标包的名称:${positionals}`)

// 输出文件路径解析:根据目标包和构建格式生成输出文件路径。
// 根据构建格式确定输出格式(iife, cjs, esm)。
const outputFormat = format.startsWith('global')
  ? 'iife'
  : format === 'cjs'
    ? 'cjs'
    : 'esm'
// 根据构建格式生成后缀名。
const postfix = format.endsWith('-runtime')
  ? `runtime.${format.replace(/-runtime$/, '')}`
  : format

// 读取私有包目录。
const privatePackages = fs.readdirSync('packages-private')

// 遍历目标包,确定包的基础路径和包信息。
for (const target of targets) {
  const pkgBase = privatePackages.includes(target)
    ? `packages-private`
    : `packages`
  const pkgBasePath = `../${pkgBase}/${target}`
  const pkg = require(`${pkgBasePath}/package.json`)
  const outfile = resolve(
    __dirname,
    `${pkgBasePath}/dist/${
      target === 'vue-compat' ? `vue` : target
    }.${postfix}.${prod ? `prod.` : ``}js`,
  )
  // 生成输出文件路径,并转换为相对于当前工作目录的路径。
  const relativeOutfile = relative(process.cwd(), outfile)

  // 如果不内联依赖,根据构建格式和目标包,确定需要外部化的依赖项。
  // TODO this logic is largely duplicated from rollup.config.js
  /** @type {string[]} */
  let external = []
  if (!inlineDeps) {
    // cjs & esm-bundler: external all deps
    if (format === 'cjs' || format.includes('esm-bundler')) {
      external = [
        ...external,
        ...Object.keys(pkg.dependencies || {}),
        ...Object.keys(pkg.peerDependencies || {}),
        // for @vue/compiler-sfc / server-renderer
        'path',
        'url',
        'stream',
      ]
    }

    if (target === 'compiler-sfc') {
      const consolidatePkgPath = require.resolve(
        '@vue/consolidate/package.json',
        {
          paths: [resolve(__dirname, `../packages/${target}/`)],
        },
      )
      const consolidateDeps = Object.keys(
        require(consolidatePkgPath).devDependencies,
      )
      external = [
        ...external,
        ...consolidateDeps,
        'fs',
        'vm',
        'crypto',
        'react-dom/server',
        'teacup/lib/express',
        'arc-templates/dist/es5',
        'then-pug',
        'then-jade',
      ]
    }
  }
  console.log(external,'external')
  /** @type {Array<import('esbuild').Plugin>} */
//   添加日志插件,记录构建完成的信息。
  const plugins = [
    {
      name: 'log-rebuild',
      setup(build) {
        build.onEnd(() => {
          console.log(`built: ${relativeOutfile}`)
        })
      },
    },
  ]
// 如果构建格式不是cjs且包配置中启用了非浏览器分支,则添加Node环境的polyfill插件。
  if (format !== 'cjs' && pkg.buildOptions?.enableNonBrowserBranches) {
    plugins.push(polyfillNode())
  }
  // 使用esbuild的context方法配置构建任务,包括入口文件、输出文件、外部依赖、源码映射、格式、平台、插件和定义的全局变量。
  esbuild
    .context({
      entryPoints: [resolve(__dirname, `${pkgBasePath}/src/index.ts`)],
      outfile,
      bundle: true,
      external,
      sourcemap: true,
      format: outputFormat,
      globalName: pkg.buildOptions?.name,
      platform: format === 'cjs' ? 'node' : 'browser',
      plugins,
      define: {
        __COMMIT__: `"dev"`,
        __VERSION__: `"${pkg.version}"`,
        __DEV__: prod ? `false` : `true`,
        __TEST__: `false`,
        __BROWSER__: String(
          format !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches,
        ),
        __GLOBAL__: String(format === 'global'),
        __ESM_BUNDLER__: String(format.includes('esm-bundler')),
        __ESM_BROWSER__: String(format.includes('esm-browser')),
        __CJS__: String(format === 'cjs'),
        __SSR__: String(format !== 'global'),
        __COMPAT__: String(target === 'vue-compat'),
        __FEATURE_SUSPENSE__: `true`,
        __FEATURE_OPTIONS_API__: `true`,
        __FEATURE_PROD_DEVTOOLS__: `false`,
        __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: `true`,
      },
    })
    .then(ctx => ctx.watch()) //启动esbuild的监听模式,以便在文件变化时自动重新构建。
}

执行流程图

graph TD
    A[开始] --> B[导入依赖]
    B --> C[创建require函数和目录路径]
    C --> D[解析命令行参数]
    D --> E[确定构建格式和目标包]
    E --> F[解析输出文件路径]
    F --> G[处理外部依赖]
    G --> H[配置插件]
    H --> I[配置构建任务]
    I --> J[启动esbuild监听模式]
    J --> K[结束]

详细步骤

  1. 导入依赖

    • 导入esbuild和其他Node.js内置模块,以及第三方插件esbuild-plugin-polyfill-node
  2. 创建require函数和目录路径

    • 创建一个require函数,用于动态加载模块。
    • 获取当前文件的目录路径。
  3. 解析命令行参数

    • 使用parseArgs解析命令行参数,提取构建格式、是否为生产环境和是否内联依赖。
    • positionals存储位置参数,即目标包的名称。
  4. 确定构建格式和目标包

    • 确定构建格式,默认为global
    • 确定目标包,默认为vue
  5. 解析输出文件路径

    • 根据构建格式确定输出格式(iifecjsesm)。
    • 根据构建格式生成后缀名。
    • 读取私有包目录。
    • 遍历目标包,确定包的基础路径和包信息。
    • 生成输出文件路径,并转换为相对于当前工作目录的路径。
  6. 处理外部依赖

    • 如果不内联依赖,根据构建格式和目标包,确定需要外部化的依赖项。
  7. 配置插件

    • 添加日志插件,记录构建完成的信息。
    • 如果构建格式不是cjs且包配置中启用了非浏览器分支,则添加Node环境的polyfill插件。
  8. 配置构建任务

    • 使用esbuild的context方法配置构建任务,包括入口文件、输出文件、外部依赖、源码映射、格式、平台、插件和定义的全局变量。
    • 启动esbuild的监听模式,以便在文件变化时自动重新构建。
  9. 结束

    • 构建过程结束。