news 2026/10/15 17:58:36

使用 vue-tsc 对 VitePress Markdown 进行类型检查:vitePressExtensions 配置与 file-md 插件实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 vue-tsc 对 VitePress Markdown 进行类型检查:vitePressExtensions 配置与 file-md 插件实现解析
  • 开发工具
  • IDE
  • 语言服务

【免费下载链接】language-tools

⚡ High-performance Vue language tooling based-on Volar.js

项目地址:https://gitcode.com/gh_mirrors/la/language-tools
点击查看免费下载

本篇技术指南以 language-tools 仓库中的真实测试工作区 test-workspace/tscBuild/vitepress 为核心,讲解如何通过vueCompilerOptions.vitePressExtensions让 vue-tsc 对 VitePress 的.md文件执行完整的 TypeScript 类型检查(包括<script setup lang="ts">脚本与{{ msg }}模板插值)。读完本文,你将掌握vitePressExtensions的配置方法、vue-tsc --build的项目级校验流程,以及 @vue/language-core 内部file-md插件把 Markdown 转换为 Vue SFC 的底层实现原理。

从一个最小工作区认识 VitePress 类型检查

仓库的tscBuild目录是一个专用于验证vue-tsc --build行为的测试工作区,其中 vitepress 子项目只有三个文件:

main.md(待类型检查的 Markdown 文件):

# Title <script setup lang="ts"> const msg = 1; </script> {{ msg }}

main.vue(作为对照的等价 Vue 单文件组件):

<template> <div>{{ msg }}</div> </template> <script setup lang="ts"> const msg = 1; </script>

tsconfig.json(启用 VitePress 扩展的关键配置):

{ "extends": "../../tsconfig.base.json", "vueCompilerOptions": { "vitePressExtensions": [".md"] }, "include": ["**/*"] }

这个最小工作区清晰地展示了一个事实:在 Vue 生态中,{{ msg }}这类插值表达式不只出现在.vue文件的<template>里,也大量出现在 VitePress 文档站点的 Markdown 正文中。VitePress 文档页面常常在 Markdown 顶部编写<script setup lang="ts">定义状态与逻辑,并在正文中通过插值渲染。只要在tsconfig.json的vueCompilerOptions中声明"vitePressExtensions": [".md"],vue-tsc就会把这些.md文件也纳入类型检查范围,让脚本中的类型错误和模板插值中的错误在 CI 阶段即可暴露。

在你的项目中启用.md类型检查

安装依赖

vue-tsc是tsc的包装器(wrapper),使用前需要同时安装 TypeScript(peerDependencies 要求typescript >= 5.0.0,见 packages/tsc/package.json):

npm install vue-tsc typescript --save-dev

配置vitePressExtensions

在项目根目录的tsconfig.json(或 VitePress 专用的 tsconfig)中加入:

{ "extends": "./tsconfig.base.json", "vueCompilerOptions": { "vitePressExtensions": [".md"] }, "include": ["**/*"] }

参数说明(对照 @vue/language-core README 与默认值实现 packages/language-core/lib/compilerOptions.ts):

选项类型默认值说明
vitePressExtensionsstring[][]需要被当作 VitePress Markdown 处理的文件扩展名列表
extensionsstring[]['.vue']需要被当作 Vue SFC 处理的扩展名
petiteVueExtensionsstring[][]需要被当作 Petite Vue HTML 处理的扩展名(如['.html'])

vitePressExtensions的默认值为空数组,即默认情况下.md文件不会被类型检查;只有显式声明后才会生效。仓库在 test-workspace/tscBuild/petiteVue/tsconfig.json 中展示了同族配置petiteVueExtensions: [".html"]的用法,可对照理解。

在 package.json 中接入脚本

参照 packages/tsc/README.md 的推荐写法:

{ "scripts": { "type-check": "vue-tsc --noEmit", "build:types": "vue-tsc --declaration --emitDeclarationOnly" } }

运行npm run type-check即可对包括.md在内的所有声明扩展名文件执行纯类型检查(--noEmit不产出任何文件);--build模式则配合项目引用(references)做增量构建,仓库的tscBuild工作区正是后者的验证场景。

底层原理:file-md插件如何把 Markdown 变成 SFC

vue-tsc之所以能理解.md文件,靠的是 @vue/language-core 内置的文件解析插件file-md,实现位于 packages/language-core/lib/plugins/file-md.ts。

第一步:语言 ID 识别

在 getLanguageId 中,插件通过文件名后缀匹配vitePressExtensions:

getLanguageId(fileName) { if (vueCompilerOptions.vitePressExtensions.some(ext => fileName.endsWith(ext))) { return 'markdown'; } }

随后isValidFile校验语言 ID 是否为markdown。也就是说,file-md只负责vitePressExtensions中声明的那批文件,与处理.vue的 file-vue.ts、处理.html的 file-html.ts 三者互不干扰。

第二步:掩码(mask)无关内容,提取脚本块

parseSFC2 是核心逻辑,它用一组精心设计的正则表达式先把“不应当被当作模板内容解析”的部分替换为等长空格,再抽取script/style块:

  • frontmatterRE:---包裹的 Frontmatter 区域(#L8);
  • codeblockRE:``` 或$$包裹的代码块(#L9);
  • codeSnippetImportRE:<<<形式的代码片段导入(#L10);
  • sfcBlockRE:<script>/<style>及其闭合标签(#L11);
  • inlineCodeRE/angleBracketRE:行内代码与xxx:xxx形式的尖括号标签,用于标记“歧义区间”,避免把 Markdown 语法误判为 HTML(#L14-L15);
  • htmlTagRE/interpolationRE:真正要保留语义的 HTML 标签与{{ }}插值(#L12-L13)。

流程分三步:先掩掉 frontmatter、代码块与代码导入(#L36-L38);再从matchAll(sfcBlockRE)中取出script/style块并同样掩掉(#L56-L63);最后把剩下的正文用<template>\n…\n</template>包裹起来(#L72-L74),交给 parseSfc.ts 走标准的 SFC 解析。此时最初的:

# Title {{ msg }}

就被转换成了一个「模板 + 脚本」结构,等价于前面看到的那种带<script setup lang="ts">的 Vue SFC。

第三步:SourceMap 回映射

由于虚拟代码(转换后的 SFC 文本)与源 Markdown 的字节偏移并不一致,file-md会用buildMappings构建映射表,再通过SourceMap把每个 block 的loc起始/结束偏移转换回源文件位置(#L76-L110)。这正是类型诊断(error)能正确报告到main.md原始行列的关键——你在编辑器或 CI 输出里看到main.md(3,1)这类定位,就来自这一步。

在 vue-tsc 命令行中如何生效

vue-tsc的入口 packages/tsc/index.ts 展示了扩展名配置如何被动态接管:

  1. 启动时维护一个runExtensions集合,初始只有['.vue'];
  2. 运行runTsc前先解析 tsconfig:从configFilePath读取vueCompilerOptions(若没有 tsconfig 则按空配置解析,index.ts#L17-L20);
  3. 调用 getAllExtensions,把extensions、vitePressExtensions、petiteVueExtensions三组配置去重合并;
  4. 若合并结果中出现新的扩展名,就将其加入runExtensions并抛出一个标记性的retryToken重新运行(index.ts#L31-L36),外层while (true)循环捕获后重试,直到扩展名集合稳定;
  5. 之后通过createVueLanguagePlugin创建 Vue 语言插件,把.vue/.md/.html文件统一转换为 TypeScript 虚拟代码再交给tsc。

这也是 packages/tsc/README.md 中“vue-tsc自动从vueCompilerOptions.extensions读取文件类型,默认['.vue'];若配置了vitePressExtensions或petiteVueExtensions,这些扩展名也会被一并处理”这句话的实现出处。

测试验证:tscBuild工作区与--build

tscBuild项目用「项目引用」把三个独立 tsconfig 组合起来(见 test-workspace/tscBuild/tsconfig.json):

{ "references": [ { "path": "./petiteVue/tsconfig.json" }, { "path": "./vitepress/tsconfig.json" }, { "path": "./vue/tsconfig.json" } ], "files": [] }

配套的测试用例在 packages/tsc/tests/build.spec.ts:

test(`vue-tsc --build`, () => { expect(() => runTsc('tscBuild')).not.toThrow(); });

它通过 packages/tsc/tests/utils.ts 以--build <tscBuild目录> --pretty false的形式真实调用vue-tsc,校验整套 Markdown / HTML / Vue 混合工作区在--build模式下不会抛错。注意tscBuild各子项目都继承自 test-workspace/tsconfig.base.json,后者统一设置了"vueCompilerOptions": { "target": 99 }(即对最新 Vue 版本目标做代码生成)。也就是说,仓库用自动化测试持续保证:配置了vitePressExtensions后,.md文件在--build与--noEmit两种模式下都能被正确解析与类型检查。

在真实 VitePress 项目中的实战建议

基于上述配置与实现,把这项能力迁移到自己的 VitePress 项目时,只需三点:

  1. 在 tsconfig 中声明"vitePressExtensions": [".md"](与"include": ["docs/**/*.md"]配合可缩小检查范围);
  2. 把vue-tsc --noEmit或vue-tsc --build接入package.json的 scripts,并纳入 CI;
  3. 注意file-md的解析边界:Frontmatter、代码块、<<<代码片段导入会被掩码,不会进入模板解析;行内代码与xxx:xxx形式的内容会被识别为歧义区间而跳过;真正被类型检查的是 Markdown 正文中的 HTML 标签与{{ }}插值,以及<script setup lang="ts">脚本本身。

这样,VitePress 文档里的插值表达式、<script setup>中的类型定义乃至事件绑定,都能与.vue组件一样获得完整、可定位到源文件行列的类型诊断,而不必等到构建运行时才发现问题。

参考文件

  • 测试工作区:test-workspace/tscBuild/vitepress/main.md、test-workspace/tscBuild/vitepress/main.vue、test-workspace/tscBuild/vitepress/tsconfig.json
  • 底层实现:packages/language-core/lib/plugins/file-md.ts、packages/language-core/lib/languagePlugin.ts、packages/language-core/lib/compilerOptions.ts
  • CLI 入口:packages/tsc/index.ts、packages/tsc/README.md
  • 测试用例:packages/tsc/tests/build.spec.ts、packages/tsc/tests/utils.ts
  • 开发工具
  • IDE
  • 语言服务

【免费下载链接】language-tools

⚡ High-performance Vue language tooling based-on Volar.js

项目地址:https://gitcode.com/gh_mirrors/la/language-tools
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 17:58:29

Spark实时模式深度解析:微批次与Continuous Processing实战

老实说&#xff0c;当我第一次在技术交流群里看到“Apache Spark Real-Time Mode”这个说法时&#xff0c;心里是有点别扭的。Spark在很多人心中早就被贴上了“微批次、准实时、延迟比不上Flink”的标签&#xff0c;哪怕Structured Streaming已经发布多年&#xff0c;不少团队做…

作者头像 李华
网站建设 2026/10/15 17:57:42

微信小程序自定义顶部栏与搜索栏吸顶实战:胶囊对齐与高度计算

1. 为什么小程序顶部栏总让人头疼 做过微信小程序的人多半都踩过这个坑&#xff1a;辛辛苦苦设计好的页面&#xff0c;一进真机就被系统导航栏劈成两半——上面是微信自带的白色&#xff08;或黑色&#xff09;标题栏&#xff0c;下面才是你自己的内容。想放个搜索框在顶部&…

作者头像 李华
网站建设 2026/10/15 17:56:25

低功耗测试实战:从M601看物联网设备的电流测量与续航估算

拿到M601这块样机的时候&#xff0c;配套资料上只写了“低功耗测试”四个字。做这行的都清楚&#xff0c;这几个字背后往往是电池供电产品的命门&#xff1a;待机电流、平均功耗、峰值电流&#xff0c;每一个数都直接决定产品能跑多久、电源要配多大、PCB散热扛不扛得住。我接这…

作者头像 李华
网站建设 2026/10/15 17:56:03

JACOB 1.20实战:Java调用Windows COM组件驱动Word自动化的完整指南

简介&#xff1a;面向需要在Java应用中操作Office文档的开发者&#xff0c;jacob 1.20是一套通过COM桥接调用MS Office的经典解决方案&#xff0c;可将Word、Excel、PPT等文件快捷转换为PDF。压缩包共88个文件&#xff0c;约495KB&#xff0c;包含jacob.jar、32位与64位两个dll…

作者头像 李华