vite根目录执行pnpm run dev

212 阅读8分钟

在vite根目录执行pnpm run dev

根目录的指令为

"scripts": {
  "dev": "pnpm -r --parallel --filter='./packages/*' run dev",
},
  • -r--recursive:递归执行命令
  • --parallel:并行执行命令
  • --filter='./packages/*':只对 packages 目录下的所有包执行命令

递归执行命令(-r--recursive)是指在 pnpm 工作空间中,命令会从当前目录开始,递归地应用到所有子包。

--filter 的区别

  • -r 会递归执行所有包
  • --filter='./packages/*' 只执行匹配的包
  • 两者结合使用可以更精确地控制执行范围

整个意思就是 并行的运行,整个package下面包的dev指令 package文件夹下面总共有三个包,依次看一下都发生了什么

  1. create-vite
  2. plugin-legacy
  3. vite

create-vite: pnpm run dev

packages/create-vite

"scripts": {
  "dev": "unbuild --stub",
},
"devDependencies": {
  "unbuild": "^3.5.0"
}

这里用到了第三方包:unbuild 用作 create-vite 本地打包工具(其实有点像vite了,esbuild、ts、Rollup杂交出来的) unbuild的github地址: github.com/unjs/unbuil…

--stub 表示不发生实际的编译,开发过程中比较好用

unbuild会有一个单独的文章介绍使用。这里只要知道是打包用的就可以了。

plugin-legacy:pnpm run dev

packages/plugin-legacy

"scripts": {
  "dev": "unbuild --stub",
},
"devDependencies": {
  "unbuild": "3.4.2",
}

也是使用 unbuild 在本地调试,暂时先略过,重点看一下下面 vite主包的dev执行

vite

packages/vite

  "scripts": {
    "dev": "tsx scripts/dev.ts",
  },
  "peerDependencies": {
    "tsx": "^4.8.1",
  },
  "peerDependenciesMeta": {
    "tsx": {
      "optional": true
    },
  }

执行dev指令,会使用tsx 运行 dev.ts文件。

import {
  mkdirSync,
  readFileSync,
  readdirSync,
  rmSync,
  writeFileSync,
} from 'node:fs'
import { type BuildOptions, context } from 'esbuild'
import packageJSON from '../package.json'

rmSync('dist', { force: true, recursive: true })
mkdirSync('dist/node', { recursive: true })
writeFileSync('dist/node/index.d.ts', "export * from '../../src/node/index.ts'")
writeFileSync(
  'dist/node/module-runner.d.ts',
  "export * from '../../src/module-runner/index.ts'",
)
/* 
首先强制删除 dist 目录及其所有内容
然后创建新的 dist/node 目录结构

index.d.ts: 导出 src/node/index.ts 中的所有内容
module-runner.d.ts: 导出 src/module-runner/index.ts 中的所有内容


*/
  1. 确保构建环境的清洁(通过删除旧的构建文件)
  2. 创建必要的目录结构
  3. 生成 TypeScript 类型声明文件,这对于开发时的类型检查和 IDE 支持很重要

典型的开发环境构建脚本,它确保了在开发过程中能够正确地处理 TypeScript 类型定义,使得开发体验更加流畅。

const serverOptions: BuildOptions = {
  bundle: true,
  platform: 'node',
  target: 'node18',
  sourcemap: true,
  external: [
    ...Object.keys(packageJSON.dependencies),
    ...Object.keys(packageJSON.peerDependencies),
    ...Object.keys(packageJSON.optionalDependencies),
    ...Object.keys(packageJSON.devDependencies),
  ],
}
/*
bundle: true
  将所有代码和依赖打包成一个文件
  这样可以减少文件数量,提高加载效率

platform: 'node'
  指定代码运行环境为 Node.js
  这会影响 esbuild 如何处理某些特定的 API 和模块

target: 'node18'
  指定目标 Node.js 版本为 18
  这确保生成的代码与 Node.js 18 兼容
  esbuild 会根据这个版本进行相应的代码转换

sourcemap: true
  生成 sourcemap 文件
  这对于调试非常重要,可以将编译后的代码映射回源代码
  在开发过程中特别有用

external 配置
  这个配置指定哪些依赖应该被视为外部依赖
    外部依赖不会被打包到最终的构建文件中
  这里包含了 package.json 中所有类型的依赖:
    dependencies: 生产环境必需的依赖
    peerDependencies: 对等依赖,需要宿主环境提供的依赖
    optionalDependencies: 可选依赖,即使安装失败也不会影响主要功能
    devDependencies: 开发环境需要的依赖
*/
  1. 确保代码能够正确运行在 Node.js 环境中
  2. 优化构建过程,只打包必要的代码
  3. 保持外部依赖的独立性
  4. 提供良好的开发调试体验
const clientOptions: BuildOptions = {
  bundle: true,
  platform: 'browser',
  target: 'es2020',
  format: 'esm',
  sourcemap: true,
}
/* 
bundle: true
  与服务器端配置相同,将所有代码打包成一个文件
  减少 HTTP 请求数量,提高加载性能

platform: 'browser'
  指定代码运行环境为浏览器
  这会影响 esbuild 如何处理浏览器特定的 API
  与服务器端的 platform: 'node' 形成对比

target: 'es2020'
  指定目标 ECMAScript 版本为 2020
  这意味着生成的代码会使用 ES2020 的特性
  比服务器端的 node18 更现代,因为现代浏览器支持更多新特性

format: 'esm'
  指定输出格式为 ES 模块(ECMAScript Modules)
  使用 import/export 语法
  这是现代浏览器原生支持的模块系统
  支持静态分析,有利于 tree-shaking

sourcemap: true
  生成 sourcemap 文件
  方便在浏览器中调试源代码
  对于开发环境特别重要

*/
  1. 确保代码能在现代浏览器中运行
  2. 使用现代的模块系统
  3. 提供良好的开发调试体验
  4. 优化了代码的加载性能
const watch = async (options: BuildOptions) => {
  const ctx = await context(options)
  await ctx.watch()
}

/* 
const ctx = await context(options)
  使用 esbuild 的 context API 创建一个构建上下文
  这个上下文包含了构建配置和构建状态
  使用 await 等待上下文创建完成

await ctx.watch()
  启动文件监听模式
  当源文件发生变化时,会自动重新构建
  使用 await 等待监听启动完成
*/
  1. 在开发环境中实现热重载(Hot Reload)
  2. 当源代码文件发生变化时,自动触发重新构建
  3. 提高开发效率,不需要手动重新构建
// envConfig
void watch({
  entryPoints: ['src/client/env.ts'],
  outfile: 'dist/client/env.mjs',
  ...clientOptions,
})
/* 
void watch()
  调用之前定义的 watch 函数
  使用 void 操作符忽略 Promise 返回值
  因为这是一个开发环境的监听任务,不需要等待其完成

entryPoints: ['src/client/env.ts']
  指定构建的入口文件
  只构建一个文件:src/client/env.ts

outfile: 'dist/client/env.mjs'
  指定构建后的输出文件路径
  使用 .mjs 扩展名表示这是一个 ES 模块文件
  输出到 dist/client 目录下

...clientOptions
  展开之前定义的客户端构建选项
*/
  1. 构建客户端环境配置文件
  2. 将环境配置打包成浏览器可用的 ES 模块
  3. 在开发过程中监听文件变化并自动重新构建
// clientConfig
void watch({
  entryPoints: ['src/client/client.ts'],
  outfile: 'dist/client/client.mjs',
  external: ['@vite/env'],
  ...clientOptions,
})

/* 
external: ['@vite/env']
  指定外部依赖
  这里特别将 @vite/env 标记为外部依赖
  这个模块不会被打包到最终文件中
*/
// nodeConfig
void watch({
  ...serverOptions,
  entryPoints: {
    cli: 'src/node/cli.ts',
    constants: 'src/node/constants.ts',
    index: 'src/node/index.ts',
  },
  outdir: 'dist/node',
  format: 'esm',
  splitting: true,
  chunkNames: '_[name]-[hash]',
  // The current usage of require() inside inlined workers confuse esbuild,
  // and generate top level __require which are then undefined in the worker
  // at runtime. To workaround, we move require call to ___require and then
  // back to require on build end.
  // Ideally we should move workers to ESM
  define: { require: '___require' },
  plugins: [
    {
      name: 'log',
      setup(build) {
        let first = true
        build.onEnd(() => {
          for (const file of readdirSync('dist/node')) {
            const path = `dist/node/${file}`
            const content = readFileSync(path, 'utf-8')
            if (content.includes('___require')) {
              writeFileSync(path, content.replaceAll('___require', 'require'))
            }
          }
          if (first) {
            first = false
            console.log('Watching...')
          } else {
            console.log('Rebuilt')
          }
        })
      },
    },
  ],
})

/* 
入口文件配置
entryPoints: {
  cli: 'src/node/cli.ts',        // 命令行工具入口
  constants: 'src/node/constants.ts',  // 常量定义
  index: 'src/node/index.ts',    // 主入口文件
}

指定了三个入口文件• 每个入口文件会生成对应的输出文件

输出配置
outdir: 'dist/node',  // 输出目录
format: 'esm',        // ES 模块格式
splitting: true,      // 启用代码分割
chunkNames: '_[name]-[hash]'  // 分块文件命名

  所有文件输出到 dist/node 目录
  使用 ES 模块格式
  启用代码分割以优化加载性能
  分块文件使用 _[name]-[hash] 格式命名

define: { require: '___require' }
  这是一个特殊处理,用于解决内联 worker 中的 require 问题
  将代码中的 require 临时替换为 ___require
  在构建结束后再替换回来

自定义插件
  plugins: [{
  name: 'log',

  在每次构建结束时执行
  处理所有输出文件中的 ___require 替换
  输出构建状态信息
*/
// moduleRunnerConfig
void watch({
  ...serverOptions,
  entryPoints: ['./src/module-runner/index.ts'],
  outfile: 'dist/node/module-runner.js',
  format: 'esm',
})
// cjsConfig
void watch({
  ...serverOptions,  // 展开服务器端构建选项
  entryPoints: ['./src/node/publicUtils.ts'],  // 入口文件
  outfile: 'dist/node-cjs/publicUtils.cjs',    // 输出文件
  format: 'cjs',     // 输出格式为 CommonJS
  banner: {          // 文件头部注入的代码
    js: `
const { pathToFileURL } = require("node:url")
const __url = pathToFileURL(__filename)`.trimStart(),
  },
  define: {          // 定义替换
    'import.meta.url': '__url',  // 将 import.meta.url 替换为 __url
  },
})

/* 
entryPoints: ['./src/node/publicUtils.ts'],  // 入口文件
outfile: 'dist/node-cjs/publicUtils.cjs',    // 输出文件
format: 'cjs',     // CommonJS 格式

指定入口文件为 publicUtils.ts
输出到 dist/node-cjs 目录
使用 CommonJS 模块格式(.cjs 扩展名)

文件头部注入
banner: {
  js: `
const { pathToFileURL } = require("node:url")
const __url = pathToFileURL(__filename)`.trimStart(),
}

*/
  1. ESM 到 CommonJS 的转换
  2. import.meta.url 的兼容性处理
  3. 文件路径的正确解析

总结一下

在vite的根目录下面,执行pnpm run dev,会并行的执行 packages/create-vite packages/plugin-legacy packages/vite 三个包下面的dev指令

create-vite包是用来

  • 创建新项目
  • 提供项目模板
  • 初始化项目配置
  • 安装依赖

plugin-legacy包是用来

  • 为旧浏览器提供兼容性支持
  • 生成传统浏览器可用的代码
  • 自动添加 polyfills
  • 处理现代 JavaScript 特性

packages/vite是Vite 的核心包

  • 开发服务器(Dev Server)
  • 构建工具(Build Tool)
  • 插件系统(Plugin System)
  • 模块解析(Module Resolution)
  • 热更新(HMR)
  • 依赖预构建(Dependency Pre-bundling)

这三个包的关系:

  1. vite 是核心包,提供基础功能
  2. create-vite 使用 vite 创建新项目
  3. plugin-legacy 扩展 vite 的功能,提供兼容性支持

执行dev命令后,会在三个包目录下面各自生成对应的dist文件夹