typescript-eslint 浏览器端构建:解读 website-eslint 如何在浏览器中运行 ESLint 与 TypeScript Lint
【免费下载链接】typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-eslint
website-eslint是 typescript-eslint 仓库中一个不起眼却关键的基础包:它把 ESLint 的Linter与内置规则、typescript-eslint 全家桶以及 TypeScript 编译器能力捆绑成一个可在浏览器中运行的 CommonJS 产物,专门服务于website包中的在线 Playground。本文将以packages/website-eslint/README.md为核心骨架,结合build.mts、src/index.js等源码,完整解析它的构建流程、打包内容、浏览器适配技巧与对外 API,帮你理解"ESLint 上浏览器"这条工程链路的真实实现。
一、这个包是什么:为 Playground 而生的浏览器版 ESLint
website-eslint的定位非常纯粹——它是一个bundled version of ESLint plustypescript-eslint, made to work in a browser,即"能在浏览器里工作的 ESLint + typescript-eslint 打包版"。它只被用于website包中的在线 Playground,不参与任何 Node.js 侧的 lint 流程,也不面向最终用户单独发布(package.json中"private": true明确标记为私有包)。
也就是说,当你在 typescript-eslint 官网上打开 Playground 输入一段 TypeScript 代码并得到即时 lint 反馈时,背后执行 lint 的并不是常见的 Node 进程,而是被 esbuild 打包进浏览器 bundle 的这个website-eslint产物。
从源码结构看,整个包非常精简:
packages/website-eslint/build.mts:构建脚本(README 中写作build.ts,实际仓库中的文件名是build.mts,由package.json中 nx 配置"command": "tsx build.mts"驱动);packages/website-eslint/src/index.js:运行时入口,负责组装 Linter、配置与规则;packages/website-eslint/src/mock/:一组 Node 专属模块的浏览器桩实现(path、util、assert、lru-cache等)。
二、构建流程:一条命令产出浏览器 bundle
构建命令非常简单:
pnpm run buildpackage.json中该脚本为pnpm exec nx build,而 nx target 最终执行tsx build.mts,即用tsx直接运行build.mts。脚本内部调用esbuild(版本约~0.28.0)完成打包,核心配置如下:
- 格式与目标:
format: 'cjs'、platform: 'browser'、target: 'es2020',输出到./dist/,同时开启minify: true(压缩)与treeShaking: true(摇树); - 入口:
./src/index.js,产物名index(对应dist/index.js),并生成 linked sourcemap; - 外部依赖:
external: [],即所有依赖全部打进 bundle,不保留任何运行时 require; - 输出约定:
package.json的exports字段将"."指向./dist/index.js,类型声明指向./dist/index.d.ts。
构建过程在build.mts的buildPackage函数中完成,函数末尾通过console.time('building eslint for web')记录整体耗时,便于在 CI 中观测打包性能。
三、打包内容三件套:Linter、TypeScript、typescript-eslint
README 明确指出这个 bundle 包含三大块内容,下面逐一结合源码展开。
1. ESLint 的 Linter 类与内置规则
bundle 包含 ESLint 的Linter类(通过eslint/lib/linter/linter.js直接引入)以及其全部内置规则(eslint/lib/rules/index.js)。
在src/index.js中,对外暴露了工厂方法:
exports.createLinter = function () { return new Linter(); };同时内置规则通过eslint/use-at-your-own-risk的builtinRules原样导出(exports.builtinRules = builtinRules;)。Playground 可以据此拿到与 Node 端一致的全量内置规则集,而不必为浏览器单独裁剪 ESLint 核心功能。
2. TypeScript:按需在线加载的 "Monaco web" 兼容 bundle
这是最巧妙的工程点。由于typescript包体积庞大,不能直接打包进页面,website-eslint采用了一个 wrapper:让typescript与typescript/lib/tsserverlibrary两个模块入口被"按需下载"(downloaded on the fly)。
具体机制藏在build.mts的 esbuildalias配置中:typescript与typescript/lib/tsserverlibrary都会被 alias 到./src/mock/typescript.js,而后者只有一行代码:
module.exports = window.ts;即运行时直接读取浏览器全局对象window.ts。这个window.ts由网站侧在需要时加载(与 TypeScript Playground 使用同一套源码方案),从而得到 README 所说的"Monaco web" compatible bundle——即与 Monaco Editor 的 TypeScript 语言服务兼容的浏览器版 TypeScript。这让 Playground 既能完整使用 TypeScript 的类型系统能力(解析、分析、报错),又不把数 MB 的编译器塞进首屏 bundle。
3. typescript-eslint 全家桶
bundle 同时打包进 typescript-eslint 的核心包,包括:
@typescript-eslint/eslint-plugin:全部 configs 与规则;@typescript-eslint/parser与@typescript-eslint/typescript-estree:负责把 TypeScript 源码解析为 ESLint 可处理的 AST。
这背后的打包细节非常讲究。构建脚本通过 esbuild 插件把相关包全部解析到源码的index.ts(见build.mts的onResolve逻辑):
- 形如
@typescript-eslint/<pkg>/<sub>的导入被重定向到<pkg>/src/<sub>/index.ts; - 形如
@typescript-eslint/<pkg>的导入被重定向到<pkg>/src/index.ts; @typescript-eslint/typescript-estree及其/use-at-your-own-risk深链接则被重定向到use-at-your-own-risk.ts。
也就是说,打包的是各包未经编译的 TypeScript 源码而非已构建的 dist 产物,这一步正是实现精细摇树的关键前提。
四、深链接 /use-at-your-own-risk 与 tree-shaking 的工程哲学
README 专门强调:构建文件刻意使用各包内部的深层/use-at-your-own-risk导入(deep imports)。这是有明确收益的设计:
This is so that esbuild can properly tree-shake and only include the necessary code. This saves us having to mock unnecessary things and reduces our website bundle size.
即:通过深链接直接指向源码内部模块,esbuild 才能做精细的 tree-shaking,只把真正被用到的代码打进 bundle,从而避免为无关代码编写 mock,并显著压缩网站 bundle 体积。
在src/index.js中可以直观看到这种导入风格:
import rawPlugin from '@typescript-eslint/eslint-plugin/use-at-your-own-risk/raw-plugin'; import { analyze } from '@typescript-eslint/scope-manager'; import { astConverter, getScriptKind, } from '@typescript-eslint/typescript-estree/use-at-your-own-risk'; import { visitorKeys } from '@typescript-eslint/visitor-keys'; import { Linter } from 'eslint'; import { builtinRules } from 'eslint/use-at-your-own-risk';入口只挑选 Playground 运行时真正需要的能力,如 AST 转换器astConverter、脚本类型判断getScriptKind、作用域分析analyze、访问者键表visitorKeys与查询库esquery,其余未被引用到的主包代码(如getParsedConfigFile、ESLint、RuleTester、CLIEngine等 Node 专用入口)都会被 esbuild 摇树剔除。
五、浏览器环境的"去 Node 化":define 与 mock 模块
ESLint 及其依赖链包含大量 Node 专属模块,直接打包必然在浏览器里爆炸。website-eslint用两层手段解决:
1. 全局 define 替换
在build.mts中,esbuild 的define配置把 Node 环境变量在编译期全部替换为浏览器值:
define: { 'define.amd': 'false', global: 'window', 'process.emitWarning': 'console.warn', 'process.env.DEBUG': 'false', 'process.env.IGNORE_TEST_WIN32': 'true', 'process.env.NODE_DEBUG': 'false', 'process.env.NODE_ENV': '"production"', 'process.env.TIMING': 'undefined', 'process.platform': '"browser"', }比如process.platform被固定为"browser",避免代码里出现基于平台的require('child_process')等路径。
2. mock 模块(alias 桩)
对于无法用 define 解决的模块,build.mts通过alias把它们全部指向packages/website-eslint/src/mock/下的桩实现:
- Node 内置的
util、assert、path(每个都同时注册xx与node:xx两种写法); - npm 包
lru-cache——桩实现是一个基于Map的极简LruCache(src/mock/lru-cache.js); typescript与typescript/lib/tsserverlibrary——桩为window.ts(src/mock/typescript.js)。
此外,esbuild 插件还通过onLoad对若干关键文件做了内容级替换(build.mts):
getParsedConfigFile.ts、ts-eslint/ESLint.ts、ts-eslint/RuleTester.ts、ts-eslint/CLIEngine.ts全部替换为空模块(src/mock/empty.js);- ESLint 的
lib/api.js被替换为src/mock/eslint.js,其中RuleTester、SourceCode是空壳类,Linter则通过vt:eslint/linter虚拟标识符重定向到真实的eslint/lib/linter/linter.js; - ESLint 的
unsupported-api.js被替换为src/mock/eslint-rules.js,builtinRules通过vt:eslint/rules指向真实的eslint/lib/rules/index.js; @typescript-eslint/parser/src/parser.ts被替换为src/mock/parser.js,其parseForESLint为空操作——因为在 Playground 场景下,AST 转换实际由typescript-estree的astConverter直接完成,不需要 parser 重新走一遍解析流程。
这套组合拳保证了 bundle 内不会残留任何fs、child_process、os等浏览器不存在的模块引用。
六、运行时 API 与配置的浏览器化处理
src/index.js最终对外暴露的完整 API 如下:
exports.getScriptKind // 来自 typescript-estree,判断脚本类型 exports.analyze // 来自 @typescript-eslint/scope-manager,作用域分析 exports.visitorKeys // 来自 @typescript-eslint/visitor-keys exports.astConverter // 来自 typescript-estree,TS AST → ESTree exports.esquery // esquery,供 Playground 做 AST 查询 exports.createLinter // () => new Linter() exports.configs // js/* 与 @typescript-eslint/* 全套配置 exports.plugin // rawPlugin.plugin(typescript-eslint 插件本体) exports.builtinRules // ESLint 内置规则其中configs的组装有一个非常实用的细节(src/index.js):
const stripParser = config => { if (!config.languageOptions?.parser) { return config; } const { parser: _parser, ...languageOptions } = config.languageOptions; return { ...config, languageOptions }; };源码注释解释了原因:部分配置自带languageOptions.parser(默认的 Node 版 parser),这会把 Playground 使用的浏览器兼容 parser 覆盖掉并导致报错,因此在导出配置前统一剥离parser字段。随后:
for (const [name, value] of Object.entries(js.configs)) { configs[`js/${name}`] = stripParserFromConfig(value); } for (const [name, value] of Object.entries(rawPlugin.flatConfigs)) { configs[`@typescript-eslint/${name}`] = stripParserFromConfig(value); }最终得到以js/<name>与@typescript-eslint/<name>命名的全套扁平配置(flat config),供 Playground 按预设选择 lint 规则组合。
另外值得一提的是 esbuild 的banner/footer配置(build.mts):
banner: { js: `define(['exports', 'vs/language/typescript/tsWorker'], function (exports) {` }, footer: { js: `});` },这是针对 esbuild#819 的已知 workaround:将产物包进一个 AMD 风格的define调用中,使其能无缝对接 Monaco/vs/language/typescript/tsWorker的加载环境,这正是"Monaco web 兼容"在产物形态上的具体体现。
七、构建产物如何被 Playground 使用
构建完成后,dist/index.js由website包(网站/Playground 应用)在浏览器侧加载。package.json中exports["."]的default指向./dist/index.js,配合tsx build.mts产出的类型声明dist/index.d.ts,使得网站侧可以像使用普通 npm 包一样获得类型提示。
整个依赖关系是单向的:website-eslint依赖@typescript-eslint/eslint-plugin、@typescript-eslint/parser、@typescript-eslint/scope-manager、@typescript-eslint/typescript-estree、@typescript-eslint/visitor-keys以及eslint、@eslint/js、esquery等(见package.json的devDependencies,全部为workspace:*或catalog:版本),它自身则只被website引用,不参与任何发布给用户的产物链路。
八、小结:一条可复用的"ESLint 上浏览器"工程范式
纵观README.md与其实现,website-eslint给出了一个非常典型的浏览器化 Node 工具链方案,其要点可归纳为:
- 构建期裁剪:esbuild
alias+onLoad替换 +define,把 Node 专属模块在编译期替换为桩或常量; - 源码级深链接:通过
/use-at-your-own-risk直接引用包内源码,让 tree-shaking 精准到函数粒度,最小化 bundle; - 按需加载重型依赖:TypeScript 编译器不打包,而是暴露
window.ts由宿主按需注入,与 Monaco/TS Playground 方案共用同一套加载机制; - 运行时收口:入口文件只暴露 Playground 真正需要的少量 API(
createLinter、configs、plugin、builtinRules等),并针对浏览器环境对配置做必要的"净化"(剥离 Node parser)。
如果你需要在自己的项目中让 ESLint + typescript-eslint 跑在浏览器里(例如在线 IDE、文档站 Playground、代码沙箱),website-eslint的这套build.mts+src/mock+src/index.js组合是可直接参照的完整参考实现——从构建脚本、桩模块到运行时 API,每一个环节都能在本文引用的仓库文件里找到一一对应的代码依据。
【免费下载链接】typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-eslint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考