Skip to content

第9章 JavaScript 与 TypeScript 转换

开篇引言

当浏览器请求一个 .ts.tsx 文件时,开发服务器不能直接返回源文件——浏览器不理解 TypeScript 类型注解,也无法处理 JSX 语法。Vite 需要在毫秒级的时间内完成代码转换,同时还要处理 import.meta.globimport.meta.env 等 Vite 特有的语法扩展,以及重写所有导入路径为浏览器可理解的 URL。

这条从源码到浏览器可执行代码的路径,就是 Vite 的 JavaScript 转换管线。

与传统的 Webpack loader 链不同,Vite 的 JS 转换管线是由多个专职插件组成的流水线。每个插件只负责一种特定的转换,它们通过 Vite 的插件容器按顺序执行。这种设计让每个环节都可以独立优化和替换——最显著的例子就是 Vite 正在从 esbuild 迁移到基于 Oxc 的转换器。

本章将深入 plugins/ 目录中与 JS/TS 转换相关的六个核心插件,揭示代码从 TypeScript 源文件到浏览器可执行模块的完整转换过程。

本章要点

  • Vite 从 esbuild 到 Oxc 的转换器迁移路径
  • plugins/oxc.ts 中 Oxc 转换插件的完整实现
  • plugins/esbuild.ts 的历史角色和向后兼容
  • plugins/importAnalysis.ts 如何重写导入路径并注入 HMR 代码
  • plugins/define.ts 的编译时变量替换机制
  • plugins/importMetaGlob.ts 的 glob 导入展开策略
  • JSX 处理的跨框架支持设计

JS 转换管线全景

一个 .tsx 文件从磁盘到浏览器,需要经过以下转换阶段:

这些插件按照 Vite 内部插件排序规则依次执行。每个 transform 钩子接收前一个插件的输出作为输入,形成一条处理链。下面我们逐一深入每个环节。

Oxc 转换插件(plugins/oxc.ts)

Oxc(Oxidation Compiler)是一套用 Rust 编写的高性能 JavaScript 工具链。在 Vite 最新版本中,vite:oxc 已取代 vite:esbuild 成为默认的 TypeScript/JSX 转换器。

transformWithOxc 核心函数

所有 Oxc 转换都通过 transformWithOxc 函数执行,它是对 Rolldown 内置的 transformSync 的封装:

typescript
import { transformSync } from 'rolldown/utils'

export async function transformWithOxc(
  code: string,
  filename: string,
  options?: OxcTransformOptions,
  inMap?: object,
  config?: ResolvedConfig,
  watcher?: FSWatcher,
): Promise<Omit<OxcTransformResult, 'errors'>> {
  let lang = options?.lang
  if (!lang) {
    const ext = path.extname(
      validExtensionRE.test(filename) ? filename : cleanUrl(filename)
    ).slice(1)

    if (ext === 'cjs' || ext === 'mjs') {
      lang = 'js'
    } else if (ext === 'cts' || ext === 'mts') {
      lang = 'ts'
    } else {
      lang = ext as 'js' | 'jsx' | 'ts' | 'tsx'
    }
  }

  const result = transformSync(
    filename,
    code,
    { sourcemap: true, ...options, lang },
    getTSConfigResolutionCache(config),
  )

  if (result.errors.length > 0) {
    // 构建友好的错误信息
    throw new Error(summary)
  }
  return result
}

几个关键设计点:

  1. 语言自动检测:根据文件扩展名自动选择转换模式,支持 .ts.tsx.jsx.mts.cts
  2. TSConfig 缓存:通过 getTSConfigResolutionCache 在多次转换之间复用 tsconfig 的解析结果
  3. 同步执行:使用 transformSync 而非异步版本,因为 Rust 层的转换速度极快,同步调用避免了 Promise 调度开销

oxcPlugin 的两种模式

oxcPlugin 函数根据 config.isBundled 标志选择不同的实现策略:

Bundled 模式(生产构建):直接使用 Rolldown 的原生转换插件 nativeTransformPlugin,它在 Rust 层完成所有转换,避免了 JS/Rust 边界的序列化开销:

typescript
if (config.isBundled) {
  return perEnvironmentPlugin('native:transform', (environment) => {
    return nativeTransformPlugin({
      root: environment.config.root,
      include,
      exclude,
      jsxRefreshInclude,
      jsxRefreshExclude,
      isServerConsumer: environment.config.consumer === 'server',
      jsxInject,
      transformOptions,
    })
  })
}

非 Bundled 模式(开发服务器):使用 JS 层的 transform 钩子,提供更细粒度的控制:

typescript
return {
  name: 'vite:oxc',
  async transform(code, id) {
    if (filter(id) || filter(cleanUrl(id)) || jsxRefreshFilter?.(id)) {
      const modifiedOxcTransformOptions = getModifiedOxcTransformOptions(
        oxcTransformOptions, id, code, this.environment,
      )
      const result = await transformWithOxc(
        code, id, modifiedOxcTransformOptions,
        undefined, config, server?.watcher,
      )
      if (jsxInject && jsxExtensionsRE.test(id)) {
        result.code = jsxInject + ';' + result.code
      }
      return {
        code: result.code,
        map: result.map,
        moduleType: 'js',
      }
    }
  },
}

JSX 处理

Oxc 插件的 JSX 处理具有丰富的灵活性。getRollupJsxPresets 函数提供了预设配置:

typescript
export function getRollupJsxPresets(
  preset: 'react' | 'react-jsx',
): OxcJsxOptions {
  switch (preset) {
    case 'react':
      return {
        runtime: 'classic',         // React.createElement
        pragma: 'React.createElement',
        pragmaFrag: 'React.Fragment',
        importSource: 'react',
      }
    case 'react-jsx':
      return {
        runtime: 'automatic',       // jsx-runtime(React 17+)
        pragma: 'React.createElement',
        importSource: 'react',
      }
  }
}

JSX Refresh(React Fast Refresh)的启用条件经过精心设计,遵循 @vitejs/plugin-react 的相同逻辑:

typescript
const getModifiedOxcTransformOptions = (
  oxcTransformOptions, id, code, environment
) => {
  const [filepath] = id.split('?')
  const isJSX = filepath.endsWith('x')

  // 在以下情况禁用 JSX Refresh:
  // 1. 服务端环境
  // 2. 被 jsxRefreshFilter 排除
  // 3. 非 JSX 文件且不包含 jsx-runtime 导入
  if (
    jsxOptions.refresh &&
    (environment.config.consumer === 'server' ||
     (jsxRefreshFilter && !jsxRefreshFilter(id)) ||
     !(isJSX || code.includes(jsxImportRuntime) || ...))
  ) {
    result.jsx = { ...jsxOptions, refresh: false }
  }
  return result
}

从 esbuild 迁移的兼容层

对于仍然使用 config.esbuild 配置的项目,convertEsbuildConfigToOxcConfig 函数提供了自动转换:

typescript
export function convertEsbuildConfigToOxcConfig(
  esbuildConfig: ESBuildOptions,
  logger: Logger,
): OxcOptions {
  const oxcOptions: OxcOptions = {
    jsxInject: esbuildConfig.jsxInject,
    include: esbuildConfig.include,
    exclude: esbuildConfig.exclude,
  }

  // 将 esbuild 的 jsx 选项映射到 Oxc 格式
  switch (esbuildTransformOptions.jsx) {
    case 'automatic':
      jsxOptions.runtime = 'automatic'
      break
    case 'transform':
      jsxOptions.runtime = 'classic'
      break
  }

  // 映射 define、jsxDev 等其他选项
  // ...
  return oxcOptions
}

基于 VitePress 构建