Void 代码编辑器 TypeScript Web/WASM 语言服务器:浏览器内类型获取与虚拟 node_modules 架构解析
【免费下载链接】void开源AI代码编辑器,Cursor的替代方案。项目地址: https://gitcode.com/GitHub_Trending/void2/void
导读
本文深入剖析 Void 代码编辑器(VS Code 开源分支)中 TypeScript 语言服务器在 Web/WASM 环境下运行的实现方案,聚焦 extensions/typescript-language-features/web/README.md 所描述的两大核心技术:基于 tsserver 自身Automatic Type Acquisition(ATA)的全局类型缓存,以及用AutoInstallerFs 虚拟文件系统按需构造node_modules的模拟依赖解析。读完本文,你将掌握:如何在浏览器中完整跑起 TS/JS 智能感知(IntelliSense)开发环境,如何开启TypeScript > Experimental > Tsserver > Web: Enable Project Wide Intellisense等实验性配置,以及vscode-global-typings、vscode-node-modules、memfs等虚拟文件系统 scheme 之间的映射与回退原理。
一、背景:为什么浏览器里的 TypeScript 语言服务器需要一套全新文件系统
传统的 tsserver 进程运行在 Node.js 环境中,可以直接访问磁盘上的node_modules、读取package.json、监听文件变化。但在浏览器/Web Worker 环境中,这些能力统统不存在:没有进程、没有本地磁盘、没有fs模块。Void 的 TypeScript 扩展因此在 Web 端采用了WASM 化的 tsserver——把typescript/lib/tsserverlibrary编译进浏览器,通过同步 API(@vscode/sync-api-client)桥接扩展主线程与 Web Worker,让 tsserver 的ServerHost所有文件系统回调都重定向到虚拟文件系统提供方(FileSystemProvider)。
这段架构可以从 web/src/serverHost.ts 看到:createServerHost接收ts、apiClient、args等参数,构建出一个实现ts.server.ServerHost接口的对象,其readFile、fileExists、directoryExists、readDirectory、getModifiedTime等全部经由apiClient.vscode.workspace.fileSystem转发到扩展侧注册的虚拟文件系统。而整个 worker 的入口在 web/src/webServer.ts:它监听message事件获取args与三个MessagePort(sync、tsserver、watcher),解析启动参数后创建PathMapper、FileWatcherManager与createSys,最终setSys(sys)并startWorkerSession(...)启动会话。
文档给出三条快速上手命令,用于在本地同时启动 VS Code 本体、Web 侧编译产物与浏览器开发服务器:
npm i # 安装 vscode 本身(仓库根目录) npm run watch-web # 编译 Web 侧产物 node <root>/scripts/code-web.js --coi # 以 COOP/COEP 启动 Web 服务最后一条命令会打开浏览器窗口,此时需要在地址后追加?vscode-coi=以启用Shared Array Buffer(跨源隔离),完整地址形如:
http://localhost:8080/?vscode-coi=--coi(Cross-Origin-Isolated)与?vscode-coi=是 Web Worker 场景下共享内存通信的前提,缺失会导致同步文件读取与类型获取无法正常工作。
二、全局类型缓存:基于 tsserver ATA 的vscode-global-typings
2.1 适用场景与开启方式
Web 端项目级智能感知默认不开启,需要手动打开实验性开关。在 VS Code 设置(Ctrl-,)中搜索:
TypeScript > Experimental > Tsserver > Web: Enable Project Wide Intellisense对应底层配置项为typescript.tsserver.web.projectWideIntellisense.enabled,默认值为true(见 package.json),但其真正生效还依赖启动参数--enableProjectWideIntelliSenseOnWeb的传递。修改后可能需要刷新页面(reload)才能生效。
该功能主要服务于普通.js文件中对"没有类型声明依赖"的补全场景。文档给出的例子是:在一个依赖未内置类型的项目里新建file.js,并在文件顶部写入:
import lodash from 'lodash';稍等片刻后,lodash的补全、跳转定义(Go To Def / Source Def)等智能感知能力即可正常工作。这个场景完全依托 tsserver 自身的Automatic Type Acquisition能力:tsserver 在 Node 端会调用 npm 安装@types/*包,而在 Web 端,这一安装动作被替换为"下载类型包并写入内存文件系统"。
2.2 类型缓存的落盘位置与 scheme 设计
ATA 产出的类型包被模拟成一个"全局"类型缓存,存储位置为:
/vscode-global-typings/ts-nul-authority/project这是一个以vscode-global-typings为 scheme 的虚拟文件系统路径,背后由内存文件系统MemFs支撑。注册代码位于 src/filesystems/ata.ts:registerAtaSupport会先检查浏览器是否支持ReadableByteStreams(不支持则直接禁用),然后通过conditionalRegistration依赖typescript.tsserver.web.typeAcquisition.enabled配置(默认true,见 package.json),满足条件后同时注册两个FileSystemProvider:
vscode-global-typings→new MemFs('global-typings', logger):ATA 写入的全局类型缓存;vscode-node-modules→new AutoInstallerFs(logger):模拟node_modules的按需安装文件系统(下文详述)。
MemFs的实现位于 src/filesystems/memFs.ts,它维护一棵以FsDirectoryEntry(目录)与FsFileEntry(文件)为节点的内存树,提供stat、readDirectory、readFile、writeFile、createDirectory、delete、watch等完整的FileSystemProvider接口。文件内容以Uint8Array形式保存,mtime/ctime用Date.now() / 1000打时间戳,所有写操作都会通过_emitter派发onDidChangeFile事件——这正是文档所说的"in-memoryMemFsFileSystemProvider"。
2.3 tsserver 与 ATA 的桥接细节
从 web/src/serverHost.ts 的createSys可以看到两个关键启动参数:
--enableProjectWideIntelliSenseOnWeb:开启 Web 项目级智能感知,才会创建ClientConnection与ApiClient并注入fs;--experimentalTypeAcquisition:开启实验性类型获取,此时serverHost的readFile、fileExists、getFileSize、directoryExists、getModifiedTime、readDirectory等文件系统回调在常规 URI 读取失败后,都会回退到mapUri(uri, 'vscode-node-modules')映射的虚拟路径再读一次(见 serverHost.ts 等处的 try/catch 回退逻辑)。
这两个参数的传递在 src/tsServer/serverProcess.browser.ts 完成:当配置webTypeAcquisitionEnabled且浏览器支持ReadableByteStreams时追加--experimentalTypeAcquisition,否则追加--disableAutomaticTypingAcquisition。
而 tsserver 侧的类型安装请求由 web/src/typingsInstaller/typingsInstaller.ts 的enqueueInstallTypingsRequest承接,接收 tsserver 传出的Project、TypeAcquisition配置与unresolvedImports列表——即哪些import需要触发类型获取,完全复用了 tsserver 内置的 ATA 机制。
三、模拟node_modules:AutoInstallerFs 的按需虚拟依赖安装
3.1 为什么.ts文件不直接走 ATA
对于普通.ts文件(以及依赖解析过程中的常规路径),文档明确说明不会走 tsserver 的 ATA,而是使用一个独立的AutoInstallerFs来创建"虚拟"的node_modules,按需把需要的包解压/提取到内存MemFs中。触发条件是:工作区中任何项目在任何时间对node_modules目录发起的文件系统操作。依赖树的解析使用"真实"的package.json,并在存在时结合package-lock.json锁定版本。
3.2 URI 重定向的完整链路
文档给出的关键映射链如下:当访问
memfs:/path/to/node_modules/lodash/lodash.d.ts时,该 URI 会被重定向为
vscode-node-modules:/ts-nul-authority/memfs/ts-nul-authority/path/to/node_modules/lodash/lodash.d.ts并最终送达AutoInstallerFs处理。这个映射由 web/src/pathMapper.ts 的mapUri完成:把任意 scheme 的 URI 统一改写成目标 scheme(如vscode-node-modules),若原 URI 没有 authority 则补上占位ts-nul-authority,路径部分则拼接为/${原scheme}/${原authority}${原path},从而完整保留原始 URI 的三段信息。
在 tsserver 侧的反向映射由filePathToResourceUri(pathMapper.ts)完成:tsserver 暴露的路径形如/vscode-node-modules/ts-nul-authority/...,会被解析回scheme/authority/path三段;ts-nul-authority会被特殊处理为"空 authority"。AutoInstallerFs内部的MappedUri类(src/filesystems/autoInstallerFs.ts)同样负责这种双向解析,并从扁平路径中还原出原始 URI。
3.3 按需安装的触发与缓存
AutoInstallerFs实现了完整的FileSystemProvider,但writeFile、rename、delete、createDirectory四个写操作直接抛出not implemented(autoInstallerFs.ts)——它是只读的,写入工作由内部MemFs与PackageManager完成。核心逻辑在ensurePackageContents(autoInstallerFs.ts):
- 快速路径过滤:路径不包含
node_modules直接抛FileNotFound;标准库相关路径(node_modules/@typescript、node_modules/@types/typescript__)同样直接拒绝,这些文件由扩展自身的 lib 文件(dist/browser/typescript/*.d.ts,见 pathMapper.ts)提供,不走虚拟安装。 - 定位项目根:
getProjectRoot通过正则^(.*?)\/node_modules截取项目根,并在该根下验证真实存在package.json(autoInstallerFs.ts)。 - 并发去重:
_projectCache(Map<root, Promise>)按项目根缓存"正在进行的安装",同一项目根的并发读取会复用同一个安装 Promise,避免重复解析。 - 解析并还原:用
@vscode/ts-package-manager的PackageManager.resolveProject(root, opts)读取真实package.json(必需)、package-lock.kdl(可选)、package-lock.json(可选,见getInstallOpts,autoInstallerFs.ts),随后调用proj.restore()把整个依赖树解压进内存MemFs。
3.4 "假装存在"的目录技巧
AutoInstallerFs.stat还有一个细节(autoInstallerFs.ts):凡是路径 basename 为node_modules或@types的目录,一律无条件返回一个目录 stat(大小 0、类型 Directory),也就是说"假装"这些目录总是存在。这是为了让 tsserver 的模块解析器在尚未真正安装任何包之前就能把node_modules当作可遍历目录,从而顺畅地发起对内部条目的访问,触发后续的按需安装。
四、文件监听在 Web 端如何工作
浏览器没有fs.watch,因此 tsserver 的文件监听也被替换为FileWatcherManager(web/src/fileWatcherManager.ts)。tsserver 的watchFile/watchDirectory回调被转成watchFile/watchDirectory/dispose消息通过watcherMessagePort 投递到扩展主线程;主线程收到事件后回调updateWatch,把create/change/delete事件翻译成ts.FileWatcherEventKind(Created=0 / Changed=1 / Deleted=2),再按注册的 file/directory watcher 分发回调。
值得注意的两点:
- lib 文件不监听:
looksLikeLibDtsPath的路径(/lib.*.d.ts)直接返回 noop watcher,因为 Web 上这些只读文件不会变化(fileWatcherManager.ts)。 - 类型获取双监听:当
enabledExperimentalTypeAcquisition开启且路径命中node_modules(且非vscode-global-typingsscheme)时,watchFile会对同一个文件同时注册两个 watcher——原始 URI 与映射后的vscode-global-typingsURI(fileWatcherManager.ts),从而让全局类型缓存里的变更也能驱动 tsserver 的增量更新。
五、从配置到运行的完整参数链路
综合 package.json、src/configuration/configuration.ts 与 web/src/serverHost.ts,Web 类型获取相关配置可归纳如下:
| 配置项 | 默认值 | scope | 说明 |
|---|---|---|---|
typescript.tsserver.web.projectWideIntellisense.enabled | true | window | 启用/禁用 Web 端项目级智能感知,需 VS Code 运行在可信上下文中;对应启动参数--enableProjectWideIntelliSenseOnWeb |
typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors | false | window | 即使开启项目级智能感知也压制语义错误;在未开启/不可用时总是生效 |
typescript.tsserver.web.typeAcquisition.enabled | true | window | 启用/禁用 Web 端包获取(对 import 的包提供智能感知);依赖projectWideIntellisense.enabled;当前 Safari 不支持 |
配置的读取链路在 configuration.ts:readProjectWideIntellisenseEnabled返回configuration.get('typescript.tsserver.web.projectWideIntellisense.enabled', true);readWebTypeAcquisition读取typescript.tsserver.web.typeAcquisition.enabled(默认true);而suppressSemanticErrors的最终值等于webTypeAcquisitionEnabled && configuration.get('...suppressSemanticErrors', false)——即语义错误压制只在类型获取开启时按用户意愿生效。
这些配置最终在 serverProcess.browser.ts 与 webServer.ts 汇合:主线程根据配置拼装启动参数,worker 侧读取--enableProjectWideIntelliSenseOnWeb与--experimentalTypeAcquisition两个标志,前者决定是否建立同步文件系统桥接,后者决定是否在常规读取失败后回退到vscode-node-modules虚拟文件系统,二者共同决定 Web 类型获取整条链路是否激活。
六、两条类型获取路径的分工总结
| 场景 | 触发机制 | 落盘位置 | 底层文件系统 |
|---|---|---|---|
.js文件 + 无类型声明的依赖(如import lodash from 'lodash') | tsserver 内置 Automatic Type Acquisition(ATA) | /vscode-global-typings/ts-nul-authority/project | MemFs(vscode-global-typingsprovider) |
.ts文件 + 任何对node_modules的文件系统操作 | AutoInstallerFs按需解析package.json/package-lock.json并还原依赖树 | 内存中的虚拟node_modules | AutoInstallerFs(vscode-node-modulesprovider,内部仍是MemFs) |
两条路径均以"按需"为核心:前者由 tsserver 的 ATA 自行决定拉取哪些@types包,后者由实际的文件系统访问触发解析并缓存到_projectCache。它们共同解决了浏览器端没有 npm registry 安装能力的前提下,TypeScript/JavaScript 智能感知对第三方类型声明的基本需求。读者可在 extensions/typescript-language-features/web/README.md 之外,结合 src/filesystems/ata.ts、src/filesystems/autoInstallerFs.ts、src/filesystems/memFs.ts 与 web/src/serverHost.ts 四个文件完整还原该机制的每一处细节。
【免费下载链接】void开源AI代码编辑器,Cursor的替代方案。项目地址: https://gitcode.com/GitHub_Trending/void2/void
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考