Appearance
第9章 JavaScript 与 TypeScript 转换
开篇引言
当浏览器请求一个 .ts 或 .tsx 文件时,开发服务器不能直接返回源文件——浏览器不理解 TypeScript 类型注解,也无法处理 JSX 语法。Vite 需要在毫秒级的时间内完成代码转换,同时还要处理 import.meta.glob、import.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
}几个关键设计点:
- 语言自动检测:根据文件扩展名自动选择转换模式,支持
.ts、.tsx、.jsx、.mts、.cts等 - TSConfig 缓存:通过
getTSConfigResolutionCache在多次转换之间复用 tsconfig 的解析结果 - 同步执行:使用
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
}