@lit-labs/analyzer 能力演进全解析:Lit 静态分析器的架构设计与模板解析实现
【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit
导读
本文以 @lit-labs/analyzer 的 CHANGELOG 为主体脉络,系统梳理这个面向 Lit 生态的静态分析库从 0.1.0 到 0.14.0 的完整演进过程。你将了解它如何通过 TypeScript AST 与类型系统识别 LitElement、ReactiveElement、原生自定义元素乃至 lit-html 模板,如何对外暴露Analyzer/createPackageAnalyzer编程接口,以及它如何支撑 linter、IDE 插件(tsserver-plugin)和代码生成器(React/Angular wrapper 生成器)等下游工具。读完后,你既能掌握该库的 API 用法与分析能力边界,也能从源码层面理解模板解析器、声明模型与引用解析的底层原理。
一、这个包是什么:面向 Lit 的静态分析基础设施
@lit-labs/analyzer是 Lit 仓库packages/labs/analyzer目录下的一个实验性(Lit Labs)包,其 README 给出的定位是:
包含用于分析包含 Lit 模板和元素的源代码的静态分析工具,可用于 linter、IDE 插件、代码生成器等下游程序。
从仓库结构看,该包被设计为纯 TypeScript/JavaScript 静态分析库,而非运行时库:它读取源码文件、解析出结构化模型,供其他工具消费。它与仓库中其他 Labs 包的关系可以从生成器侧反推——例如 gen-wrapper-react、gen-wrapper-angular、gen-wrapper-vue 等框架 wrapper 生成器,以及 custom-elements-manifest 类工具 都以它产出的模型为基础。
[!IMPORTANT] 依据 README 中的警告,该包属于 Lit Labs 系列,发布目的是收集设计反馈,可能包含破坏性变更或停止维护,生产环境使用前请先阅读 Labs 文档。
二、快速上手:两种编程入口
2.1 Node 环境:createPackageAnalyzer
最常见的用法是基于文件系统路径创建"包级分析器",源码实现位于 package-analyzer.ts:
import {createPackageAnalyzer} from '@lit-labs/analyzer/package-analyzer.js'; import * as path from 'path'; const packagePath = path.resolve('./my-package'); const analyzer = createPackageAnalyzer(packagePath); const module = analyzer.getModule( path.resolve(packagePath, 'src/my-element.ts') );createPackageAnalyzer的入参解析逻辑(源码 L36-L88)值得注意:
- 传入路径可以是包根目录,也可以是某个具体的 tsconfig 文件;
- 若传入目录且目录下存在
tsconfig.json,则按 TypeScript 工程分析,读取配置并通过ts.parseJsonConfigFileContent构造ParsedCommandLine; - 若传入目录但没有
tsconfig.json,控制台会打印No tsconfig.json found; assuming package is JavaScript.,随后以硬编码的 JS 编译器选项(module: es2021、allowJs: true、typeRoots: []等)将工程当作 JavaScript 分析——这正是 CHANGELOG 0.3.0 "Added support for analyzing JavaScript files" 的落地实现; - 传入的既不是目录也不是 tsconfig 文件时,会抛出
The specified path '...' was not a folder or a tsconfig file.错误。
它还接收一个AnalyzerOptions(源码 L12-L20),目前只有exclude?: string[]一个选项,用于排除工程中不应参与分析的源文件:
const analyzer = createPackageAnalyzer(packagePath, { exclude: ['**/test/**', '**/*_test.ts'], });这正是 CHANGELOG 0.6.0 中 "--exclude options(对排除测试文件以生成 manifest 或 wrapper 至关重要)"的实现所在——排除 glob 会被合并进 tsconfig 的exclude数组(L43-L45)。
内部流程(L90-L114)还会用ts.createCompilerHost(options, /* setParentNodes */ true)创建带父节点指针的编译器宿主,因为getText()等 API 需要向上回溯 AST;随后基于解析出的文件名与编译选项创建ts.Program,并把program.getSyntacticDiagnostics()收集进 analyzer 的诊断队列。
2.2 浏览器环境:底层Analyzer
Analyzer类(analyzer.ts)不依赖 Node 文件系统,而是通过构造参数注入依赖,因此可以在浏览器中与 bundler 配合使用。它的构造参数(AnalyzerInit,源码 L19-L25)包括:
export interface AnalyzerInit { typescript: TypeScript; getProgram: () => ts.Program; fs: AnalyzerInterface['fs']; path: AnalyzerInterface['path']; basePath?: AbsolutePath; }由于要传入一个ts.Program,在浏览器中运行必须先用 bundler 打包 TypeScript(README 建议使用 Rollup 配合 CommonJS 插件)。在 Rollup 配置中需要忽略os、fs、inspector等 Node 内建库:
// rollup.config.js import commonjs from '@rollup/plugin-commonjs'; // ... plugins: [ commonjs({ ignore: (id) => ['fs', 'os', 'inspector'].includes(id), }), ], // ...并且可能需要安装path包(npm i path)。随后按 README 所示引入:
import {Analyzer} from '@lit-labs/analyzer/lib/analyzer.js'; import {AbsolutePath} from '@lit-labs/analyzer/lib/paths.js'; import ts from 'typescript'; import * as path from 'path'; // TODO: show constructing an Analyzer in browser contexts依据 package.json 的
exports字段,该包公开了"."、"./package-analyzer.js"与"./lib/*.js"三个入口;其中package-analyzer.js入口(即createPackageAnalyzer)依赖 Node API,是 0.9.0 中"Add separate entrypoint for createPackageAnalyzer() which requires Node APIs"的结果。
2.3 公共导出与包级模型
src/index.ts 导出Analyzer及一系列模型类型:Package、Module、Reference、Type、Event、Declaration、VariableDeclaration、ClassDeclaration、ClassField、ClassMethod、Parameter、Return、LitElementDeclaration、MixinDeclaration、CustomElementDeclaration、FunctionDeclaration等,以及getImportsStringForReferences工具函数。
其中Package模型(model.ts)提供getLitElementModules(),返回"包含 LitElement 声明的模块 + 过滤后的声明列表",这是下游代码生成器最常用的入口之一。
三、能力演进时间线:从最小骨架到模板级分析
以下是依据 CHANGELOG 整理的完整能力演进脉络。该包遵循 changesets 语义化版本:Minor 为新增能力,Patch 为缺陷修复。
0.1.x(2022 年初):初代骨架与 LitElement 发现
- 0.1.0:新增初始
Analyzer类(PR #2676),这是整个库的地基。 - 0.1.1:三个关键补丁——"Initial support for finding LitElement declarations"(在源码中定位 LitElement 声明,PR #2796)、"Refactor LitElement-specific utilities into separate module"(将 LitElement 相关工具拆成独立模块,PR #2798)、"Add minimal class declaration gathering"(最基础的类声明收集,PR #2789)。
从源码看,"LitElement 专用工具"最终沉淀为 src/lib/lit/lit-element.ts:其中isLitElementSubclass()(L112-L130)通过类型检查器取基类型并逐层判定是否最终指向规范的 LitElement 声明;而_isLitElementModule()(L86-L96)则通过文件路径特征(node_modules/lit-element/lit-element.d.ts、monorepo 下的packages/lit-element/lit-element.d.ts等)识别 LitElement 本源文件。
0.2.x:属性选项、事件与 React wrapper 雏形
- 0.2.0:CLI 增加 React wrapper 的基本生成能力(PR #2822)——这是
@lit-labs/gen-wrapper-react方向的最早萌芽。 - 0.2.1:模型新增
Type、Reference、VariableDeclaration(PR #2976)。 - 0.2.2:TypeScript 升级至 ~4.7.4(PR #3116)。
- 补丁:"Read property options from decorated properties"(PR #2804,从
@property()装饰器中读取选项)、"Read events from class JSDoc @fires tags"(PR #2812,从类 JSDoc 的@fires标签读取事件)、"Add utilities for getting LitElement declarations"(PR #2896)。
属性选项的读取在 src/lib/lit/properties.ts 中有完整实现:getProperties()遍历类成员,区分"带@property装饰器的属性"(从装饰器参数对象字面量解析attribute/type/reflect/converter等选项,对应源码 L58-L72 与 decorators.ts 的getPropertyOptions)、"静态properties块"(L73-L78,JS 用户常用写法)与"无装饰器普通字段"(L79-L85,用于类型推断)。
0.3.x:JavaScript 支持与 Analyzer 重构
- 0.3.0:三件事——"Added support for analyzing JavaScript files"(PR #3304);"Refactored Analyzer into better fit for use in plugins"(PR #3288):
Analyzer类改为接收ts.Program,新增PackageAnalyzer接收包路径并在文件系统上创建 program;修复 CLI 全局安装导致 analyzer 跨包不兼容的 bug(PR #3254)。
"Analyzer 接收 Program、PackageAnalyzer 接收路径"这一分层一直保持到今天,正是我们在第二节看到的两个入口的由来。JS 分析能力则体现在createPackageAnalyzer的 tsconfig 缺失回退路径(见 2.1 节)。
0.4.x:缓存与 custom elements manifest 生成器
- 0.4.0:"Cache Module models based on dependencies"(PR #3333)——按依赖关系缓存 Module 模型,避免重复解析。
- 补丁:"Added initial implementation of custom elements manifest generator (WIP)"(PR #2990)——自定义元素清单生成器的初始实现(进行中状态)。
模块缓存在 analyzer.ts 中体现为readonly moduleCache = new Map<AbsolutePath, Module>(),注释明确说明"当源文件或其任一依赖变化时失效"。而 custom elements manifest 生成逻辑的入口在 src/lib/custom-elements/custom-elements.ts:除了@customElement装饰器,它还能从 JSDoc 的@customelement标签以及customElements.define('x-foo', XFoo)命令式调用中提取 tag 名(L70-L100 之后)。
0.5.x:超类分析、导出查询与引用解引用
- 0.5.0(PR #3507):
export、slot、cssPart、cssProperty进入 analyzer 与 manifest 生成器;同时改善 JS 工程分析性能;ClassDeclaration新增 superclass 分析,Module新增getExport()/getResolvedExport(),Reference新增dereference()。
引用解析在 model.ts 中有清晰呈现:
getExport(name)(L193-L205):返回本模块定义的Declaration,或指向其他模块的Reference(重导出场景);getResolvedExport(name)(L226-L232):沿着重导出链循环dereference()直到拿到具体声明;getExportReference(name)(L213-L220):统一返回Reference形式。
而超类继承链的查询方式在 CHANGELOG 中有明确示例:classDeclaration.heritage.superClass.dereference()——heritage.superClass返回Reference,解引用后得到超类的ClassDeclaration模型。ClassDeclaration.heritage的实现在 model.ts,继承信息由getHeritage工厂函数惰性计算。
0.6.x:分析覆盖面大幅扩展
- 0.6.0的 Minor 变更最为密集:
- "Added analysis of vanilla custom elements that extend HTMLElement"(PR #3621)——原生(非 Lit)自定义元素分析,对应 custom-elements.ts 中的
isCustomElementSubclass():通过基类型链查找HTMLElement接口声明判定; - 支持
const变量初始化为类表达式/函数表达式时按ClassDeclaration/FunctionDeclaration分析(PR #3662); - JSDoc 类型在 TS 文件中对输出无影响(与 TS 自身行为一致,PR #3658);
- 函数重载支持(PR #3702):以字符串 key 查询的方法将返回重载函数的"实现签名"声明,该声明新增
overloads字段,内含每个重载签名的FunctionOverloadDeclaration——对应 model.ts 中FunctionDeclaration.overloads与FunctionOverloadDeclaration的设计; - 静态类成员支持:按名称分别存储到独立的 map(PR #3648),即 model.ts 中的
staticFieldMap/staticMethodMap,与普通fieldMap/methodMap分离; - 函数声明分析(PR #3655);
- CLI 增加
--exclude选项;分析器与 manifest 输出新增:TS 枚举类型变量、所有模型的description/summary/deprecated、模块级 description & summary、ClassField和ClassMethod(PR #3529)。
- "Added analysis of vanilla custom elements that extend HTMLElement"(PR #3621)——原生(非 Lit)自定义元素分析,对应 custom-elements.ts 中的
至此,分析器的"模型宇宙"基本成型:Declaration抽象基类(model.ts)提供isVariableDeclaration()、isClassDeclaration()、isLitElementDeclaration()、isFunctionDeclaration()、isMixinDeclaration()、isClassField()、isClassMethod()、isCustomElementDeclaration()等类型守卫。
0.7.x:CSS 自定义属性回退值
- 0.7.0(PR #3812):manifest 中新增 CSS 自定义属性回退(默认)值。这为设计系统文档生成等场景补充了
--my-color: red中默认值red的语义信息。
0.8.x:从"崩溃"走向"尽力而为"
- 0.8.0(PR #3866):在遇到意外语法或尚未处理的场景时,analyzer 不再大面积崩溃;custom elements manifest 生成器会记录分析过程中收集到的 diagnostics,能生成 manifest 就尽量生成。这一"容错优先"的哲学对下游代码生成工具的健壮性至关重要——单个不支持的语法不再阻塞整个包的清单输出。诊断的收集与读取实现在 analyzer.ts:
addDiagnostic()入队、getDiagnostics()经sortAndDeduplicateDiagnostics去重排序后产出。
0.9.x:TypeScript 5.0 与 API 收敛
- 0.9.0:TypeScript 升级至 ~5.0(PR #4030);
createPackageAnalyzer()拆出独立入口(PR #3980,即今天的@lit-labs/analyzer/package-analyzer.js);构造Analyzer必须传入 TypeScript 对象(PR #4029)。 - 补丁(PR #4006):当
tsconfig.json通过extends继承其他配置时也能正确检测源文件。
"必须传入 TypeScript 对象"是重要的设计决策——它保证 analyzer 使用与分析者相同的 TypeScript 版本,避免多版本 TypeScript 并存导致的类型不兼容。这一点在 0.10.0 的补丁中被进一步强化(见下节)。
0.10.x:TypeScript 5.2 与消费者优先
- 0.10.0:TypeScript 升级至 ~5.2.0(PR #4141)。
- 补丁:
- "Always use consumer's typescript rather than analyzer's dependency to avoid version mismatches"(PR #4252,感谢 @43081j)——始终使用消费者侧安装的 TypeScript,避免版本失配,与 0.9.0 的 API 变更一脉相承;
- TypeScript v5.0 更新(PR #3814);
- 移除对 Node 专有库的依赖,
absoluteToPackage()需显式传入路径分隔符(0.11.0,PR #4322); - 分析器模型对象上新增 TypeScript 节点引用(0.11.0,PR #4260)。
0.12.x ~ 0.13.x:Mixin 与模块解析修复
- 0.12.0:新增
lib/lit-html/template.js模块,提供初始模板工具(PR #4261)——这是模板分析能力的先声。 - 0.12.1:支持将mixin 类/函数作为被分析类的超类(PR #4147,感谢 @43081j)。
- 0.13.1:
- 正确忽略分析 LitElement 响应式属性时的类私有字段(
#field语法,PR #4746); - 修复 TypeScript
NodeNext模块解析下的类型解析与 Lit 模块检测 bug(PR #4744)。
- 正确忽略分析 LitElement 响应式属性时的类私有字段(
- 0.13.2:README 添加 Lit Labs 提示(PR #4903)——即我们在开头引用的那段实验性警告。
从 properties.ts 可以看到私有字段处理的相关代码:仅当属性名为普通标识符或私有标识符(ts.isPrivateIdentifier)时才继续分析,否则产生UNSUPPORTED警告诊断并跳过。
0.14.0(当前版本):模板解析器与类型检查
Minor Changes:
- tsserver-plugin 中支持对 lit-html 属性绑定做类型检查(PR #5056)——分析能力开始反哺 IDE 体验(对应仓库中的 tsserver-plugin)。
- 新增模板解析器(PR #4267 与 PR #4805)——能够把
html\...`模板解析成结构化的LitTemplate(parse5DocumentFragment` 的扩展),模板中的子节点绑定、属性绑定、事件绑定、属性绑定、布尔属性绑定都被识别为可查询的 part。 - 声明的联合类型(union types)不再被拓宽为基类型(PR #5177,感谢 @ClaudioHoffmann)——修复了 Angular wrapper 生成器生成的属性访问器中的意外类型错误。
Patch Changes:
- 调整模板解析器中属性的源码位置(PR #5057);
- TypeScript 依赖升级至5.8,并同步处理
ARIAMixin相关变更(ariaColIndexText、ariaRelevant、ariaRowIndexText,PR #4984,感谢 @kyubisation)。
四、源码深潜:模板解析器是如何工作的
0.14.0 引入的模板解析器位于 src/lib/lit/template.ts,是整个分析器目前最有技术含量也最有代表性的模块,值得单独展开。
4.1 识别"真正的" lit-html 模板
isLitHtmlTaggedTemplateExpression()(L58-L77)负责判定一个 AST 节点是否为 lit-html 模板。它递归调用isResolvedIdentifierLitHtmlTemplate()(L101-L140):把 tag 标识符解析回符号声明,校验它确实是来自lit或lit-html的html命名导入。注释中的例子很直观:
import {html as h} from 'lit'; h``; // ✅ 是 lit-html 模板(支持别名导入)import {html} from 'lit-html/static.js'; html`false`; // ❌ 不是(static 模板不算可编译模板)4.2 Part 类型体系
解析结果中的绑定(part)按PartType枚举分类(L142-L149):
export const PartType = { ATTRIBUTE: 1, // 属性绑定 <div foo=${x}> CHILD: 2, // 子节点绑定 <div>${x}</div> PROPERTY: 3, // 属性绑定(. 前缀)<div .foo=${x}> BOOLEAN_ATTRIBUTE: 4, // 布尔属性绑定(? 前缀)<div ?hidden=${x}> EVENT: 5, // 事件绑定(@ 前缀)<button @click=${onClick}> ELEMENT: 6, // 元素绑定 <div ${directive}> } as const;SinglePartInfo覆盖 CHILD / ELEMENT 两类(携带单个ts.Expression),AttributePartInfo覆盖 ATTRIBUTE / PROPERTY / BOOLEAN_ATTRIBUTE / EVENT(携带prefix、绑定名与表达式数组)。
4.3 解析流程与源码位置映射
parseLitTemplate()(L306-L574)的流程是:
- 从
ts.TaggedTemplateExpression提取模板字符串数组与插值表达式(getTemplateStrings,L582-L612); - 调用 lit-html 内部的
_$LH.getTemplateHtml(strings, 1)生成"已准备"的 HTML(含 marker),再交给 parse5 的parseFragment(source, {sourceCodeLocationInfo: true})解析成 DOM 树; - 深度优先遍历 parse5 树,遇到 marker 注释节点(子绑定)、以 marker 开头的属性(元素绑定)或带
boundAttributeSuffix后缀的属性(各类属性绑定)时,把对应ts.Expression挂到节点的litPart上,并记录valueIndex; - 由于
${表达式}在准备 HTML 中被替换为等长的 marker,解析器通过lineAdjust/colAdjust/offsetAdjust三组游标把 parse5 的行列偏移精确映射回 TypeScript 源码位置——这正是 0.14.0 Patch 中"Adjust attribute source locations"的基础; - 结果以
WeakMap<ts.TaggedTemplateExpression, LitTemplate>缓存(L277),同一模板节点重复解析直接命中缓存。
最终产物LitTemplate(L236-L253)同时携带tsNode(原始 TS 节点)、strings(模板字符串数组)和parts(全部绑定信息)。getLitTemplateExpressions()(L282-L298)则负责遍历整个源文件收集所有 lit-html 模板表达式,供 linter 规则或 tsserver-plugin 逐模板分析。
五、源码深潜:LitElement 声明的完整解剖
以@customElement('my-element')装饰的 LitElement 类为例,getLitElementDeclaration()(lit-element.ts)会产出包含以下信息的LitElementDeclaration:
- tagname:
getTagName()(L137-L157)优先读@customElement('x-foo')装饰器的字符串参数;否则回退到原生自定义元素的 tag 检测(JSDoc@customelement标签或customElements.define(...)调用); - reactiveProperties:由 properties.ts 的
getProperties()产出——装饰器属性从@property({...})对象字面量中解析attribute、type、reflect、converter等选项,静态properties块与构造函数赋值路径则用于 JS 工程中的类型推断; - heritage:
getHeritage()惰性计算超类与 mixin 引用; - 类成员:
getClassMembers()收集字段与方法(含static/privacy/readonly等元数据); - JSDoc 数据:description、summary、deprecated。
判定一个类"是 LitElement 子类"的逻辑(isLitElementSubclass,L112-L130)走类型系统:取类的基类型链,逐个判断是否最终命中规范的LitElement声明;遇到 mixin 产生的交叉类型则递归检查交叉成员。0.12.1 "支持 mixin 类/函数作为超类"正是这一逻辑的受益者。
六、JSDoc 驱动的声明元数据:面向文档生成器的细节
0.5.0 与 0.9.2 等版本密集补充了 JSDoc 解析能力,这些细节对 API 文档生成器、设计系统 catalog 工具价值极高:
- 类/方法/字段级别的
description、summary、deprecated; @fires {EventType} name事件声明(0.9.2 起支持类型在前的写法);@cssprop/@cssproperty/@csspart小写标签(0.9.2);@cssProperty {<color>} --my-color带语法元数据的 CSS 自定义属性(0.9.2);@readonly标签与 TypeScriptreadonly关键字(0.9.2,针对非响应式类字段);- 类访问器(成对 / 只读 / 仅 setter,0.9.2);
- 非响应式、构造函数赋值的类字段(0.9.2);
- ECMAScript 私有方法的
privacy字段正确设置为 private(0.9.2); - CSS 自定义属性的回退(默认)值进入 manifest(0.7.0)。
对应解析实现在 src/lib/javascript/jsdoc.ts(命名/类型化 JSDoc 信息解析)、src/lib/custom-elements/events.ts(事件收集)以及 src/lib/custom-elements/custom-elements.ts。
七、工程视角:测试、构建与依赖
- 测试组织:源码树 src/test 分为
server(node 原生 test runner,覆盖 JavaScript 分析、Lit 属性/事件/模板、原生元素、类型解析等)与browser(经 web-test-runner 在浏览器中运行,验证脱离 Node 环境的可行性);依据 package.json 的 wireit 配置,浏览器测试前会用 Rollup 把 TypeScript 打包成test/browser/typescript.js供浏览器加载。 - 依赖:
typescript ~5.9.0、parse5 ^7.3.0、@parse5/tools、lit-html ^3.1.2(复用其_$LH私有模板准备逻辑)、package-json-type。 - 构建脚本:
npm run build(tsc --build),依赖../../lit:build,即需要先构建核心lit包。
八、总结:从 changelog 读出的设计哲学
回看 CHANGELOG 的完整脉络,可以提炼出@lit-labs/analyzer的几条关键设计主线:
- 分层解耦:
Analyzer(接收ts.Program,可嵌入任意宿主)与PackageAnalyzer(接收路径,面向文件系统)的分层,让同一套分析逻辑既能跑在 CLI / Node 工具链里,也能跑在浏览器或编辑器插件进程里。 - 版本对齐优先:从"必须传入 TypeScript 对象"(0.9.0)到"始终使用消费者的 TypeScript"(0.10.0),再到 NodeNext 模块解析修复(0.13.1)与 TypeScript 5.8 跟进(0.14.0),版本兼容性始终是最高优先级。
- 容错优于崩溃:0.8.0 起"能生成就生成"的策略,使下游代码生成器面对不完整源码时依然可用。
- 覆盖面持续外扩:分析对象从 LitElement 类 → JavaScript 工程 → 原生自定义元素 → 函数/重载/静态成员 → mixin 超类 →lit-html 模板本身,最终形成从"元素级"到"模板级"的完整静态分析栈,为 linter、IDE 插件(tsserver-plugin)和 React/Angular/Vue wrapper 生成器提供了统一的信息底座。
如果你正在构建面向 Lit 生态的代码生成器、lint 规则或 IDE 增强,建议从 README 的两个入口示例起步,对照 model.ts 的模型定义理解输出结构,再以 src/test/server 下的测试用例作为行为参考。注意该包仍处于 Labs 阶段,接口可能随版本演进发生破坏性变更,锁定版本使用并在升级时查阅 changelog 是更稳妥的做法。
【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考