
lit-labs/gen-manifest 深度解析为 Lit 组件生成 Custom Elements Manifest 的完整指南【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit导读lit-labs/gen-manifest是 Lit 官方实验性工具链lit-labs/*中负责为组件库生成Custom Elements Manifest自定义元素清单简称 CEM的代码生成器。它把一个 TypeScript 包的静态分析结果由lit-labs/analyzer产出转换成为社区标准的custom-elements.json清单文件从而让 IDE 智能提示、文档站点、代码生成器等下游工具可以自动理解你的 Lit 组件对外暴露的 API。读完本文你将掌握lit-labs/gen-manifest的两种使用方式编程 API 与 CLI 命令、完整的 manifest 输出结构以及从 TypeScript 源码到标准 JSON 清单的逐级转换原理。什么是 Custom Elements Manifest它解决什么问题Custom Elements Manifest 是 webcomponents 社区提出的一种 JSON 格式规范本仓库实现中遵循其 schema1.0.0用一个名为custom-elements.json的机器可读文件描述一个包中所有自定义元素及其相关模块的公共 API。它解决的核心痛点是自定义元素的知识只存在于源码的 JSDoc 与 TypeScript 类型中缺少一种标准化的、可供工具消费的元数据格式。有了 manifest 之后组件库作者一次生成、多方受益IDE 可以根据清单提供属性补全与文档悬停文档站点可以直接渲染 API 参考包装器生成器可以据此为其他框架生成绑定代码。这一点与本仓库中 gen-wrapper-react、gen-wrapper-vue 等包的目标一脉相承——lit-labs/gen-manifest正是这套“从 Lit 组件出发生成标准元数据”链路中的关键一环。快速上手两种使用方式官方 README 将该库定位为 “Utility library for generating a Custom Elements Manifest for Lit components”并指出命令行入口在lit-labs/cli。因此使用方式分为编程 API 与 CLI 两种。方式一编程方式调用Library API在代码中直接引入生成函数把分析器输出的Package模型交给generateManifest即可得到一个文件树对象import {createPackageAnalyzer} from lit-labs/analyzer/package-analyzer.js; import {generateManifest} from lit-labs/gen-manifest; import {writeFileTree} from lit-labs/gen-utils/lib/file-utils.js; const analyzer createPackageAnalyzer(/path/to/your/package); const pkg analyzer.getPackage(); const fileTree await generateManifest(pkg); // { custom-elements.json: {...} } await writeFileTree(/path/to/output, fileTree);其中createPackageAnalyzer来自依赖lit-labs/analyzer见 package.json负责读取tsconfig.json并静态分析整个包generateManifest是 src/index.ts 导出的核心函数返回一个以custom-elements.json为键名的FileTreewriteFileTree来自lit-labs/gen-utils将文件树递归写入磁盘详见下文“FileTree 抽象”。FileTree是生成器之间传递的统一数据结构定义在 packages/labs/gen-utils/src/lib/file-utils.tsexport interface FileTree { [path: string]: string | FileTree; }键名既可以是文件名值为文件内容字符串也可以是子目录名值为嵌套的FileTree文件名甚至可以包含/写入时会自动创建中间目录。方式二CLI 命令推荐lit-labs/gen-manifest本身不提供命令行而是通过lit-labs/cli的lit labs gen子命令驱动详见 cli READMElit labs gen --manifest--manifest标志用于开启 manifest 生成。结合 labs.ts 中的选项定义labs gen完整参数如下选项类型默认值说明--package可多次指定./要分析的包目录对 TypeScript 项目若目录下无tsconfig.json也可直接指定某个tsconfig.json路径--framework可多次指定无同时生成的框架包装器react、vue与 manifest 生成互不冲突--manifest布尔false是否生成custom-elements.jsonmanifest--out字符串./gen输出目录--exclude可多次指定[]从分析中排除的源文件 glob例如同时生成 manifest 与 React 包装器lit labs gen --manifest --frameworkreact --packagepackages/my-elements --out./genCLI 的底层执行逻辑在 packages/labs/cli/src/lib/generate/generate.ts 中其流程为对每个--package路径执行path.resolve归一化并用createPackageAnalyzer(root, {exclude})创建分析器校验package.json必须包含name字段manifest 的类型引用解析依赖包名收集生成器引用--manifest对应lit-labs/gen-manifest--framework对应各框架生成器先并行尝试import()所有生成器若未安装则逐个询问安装resolveCommandAndMaybeInstallNeededDeps对每个生成器调用其generate(options, console)拿到FileTree再用writeFileTree写入--out目录打印分析器收集到的 TypeScript 诊断信息并将Promise.allSettled捕获到的生成错误汇总抛出。可以看到manifest 生成器与 React/Vue 包装器生成器通过同一套“生成器引用”机制被 CLI 动态加载这正是 src/index.ts 中导出getCommand()的原因——它把该包包装成 CLI 可解析的生成器命令export const getCommand () { return { name: manifest, description: Generate custom-elements.json manifest., kind: resolved, async generate(options: {package: Package}): PromiseFileTree { return await generateManifest(options.package); }, }; };CLI 侧对应的引用定义见 generate.tsinstallFrom: lit-labs/gen-manifest、importSpecifier: lit-labs/gen-manifest/index.js。工作原理从 TypeScript 源码到 CEM JSON 的三级管线generateManifest只是入口真正的转换发生在convertPackage→convertModule→convertDeclaration的三级调用链中。整个管线可以用下图概括TypeScript 源码 │ ▼ lit-labs/analyzercreatePackageAnalyzer Package 模型modules / declarations / exports / 类型引用 │ ▼ convertPackage CEM 根对象{ schemaVersion: 1.0.0, modules: [...] } │ ▼ convertModule每个源码模块 { kind: javascript-module, path, declarations, exports } │ ▼ convertDeclaration按声明类型分派 class / custom-element / mixin / function / variable │ ▼ JSON.stringify custom-elements.json第一级包PackageconvertPackage 只做两件事写死schemaVersion: 1.0.0并把分析结果中每个模块映射为cem.Moduleconst convertPackage (pkg: Package): cem.Package { return { schemaVersion: 1.0.0, modules: [...pkg.modules.map(convertModule)], }; };schemaVersion是 CEM 规范的版本号本实现固定输出1.0.0消费者可以据此判断如何解析清单。第二级模块ModuleconvertModule 将一个 ES 模块转换为javascript-module条目{ kind: javascript-module, path: module.jsPath, // 编译后的 JS 路径如 element-a.js description, summary, deprecated, // 取自 JSDoc空值会被剔除 declarations: [...], // 模块内声明的类/函数/变量等 exports: [ ...module.exportNames.map(convertJavascriptExport), // 普通 JS 导出 ...module.getCustomElementExports().map(convertCustomElementExport), // 自定义元素注册 ], }其中导出被分为两类js导出{kind: js, name, declaration: {name, package?, module?}}记录符号名及其真实定义位置支持export {Foo, Baz}这类别名重导出golden 中Baz的declaration.name即指向原符号Barcustom-element-definition导出{kind: custom-element-definition, name: tagname, declaration: {name}}记录customElement(element-a)注册出的标签名与类的对应关系。第三级声明Declaration分派convertDeclaration 依据分析器模型的运行时类型把每种声明映射为对应的 CEM 形态分析器模型CEM 输出关键字段LitElementDeclarationcustom-elementtagName、customElement: true、attributes、events、slots、cssParts、cssPropertiesClassDeclarationclasssuperclass、mixins、membersMixinDeclarationmixinparameters、returnFunctionDeclarationfunctionparameters、returnVariableDeclarationvariabletype其他抛错Unknown declaration: ...遇到尚未支持的类型如CustomElementMixinDeclaration转换器会直接抛出Unknown declaration错误源码中有对应的 TODO 注释这保证了输出不会被静默降级成残缺数据。生成的 manifest 长什么样以 golden 文件为解剖样本仓库在 goldens/test-element-a/custom-elements.json 保存了一份 1212 行的完整基准输出golden 文件其输入是 test-projects/test-element-a 下的测试组件包。下面以它为准逐块解剖输出结构。自定义元素声明CustomElementDeclaration以 element-a.ts 为例源文件中的 JSDoc 标注/** * This is a description of my element. Its pretty great. The description has * text that spans multiple lines. * * summary My awesome element * fires a-changed - An awesome event to fire * slot default - The default slot * slot stuff - A slot for stuff * cssProperty --foreground-color - The foreground color * cssProp --background-color The background color * cssPart header The header * cssPart footer - The footer */ customElement(element-a) export class ElementA extends LitElement { static override styles css...; property() foo?: string; override render() { return html h1 partheader${this.foo}/h1 slot/slot slot namestuff/slot footer partfooterFooter/footer ; } }生成的声明片段如下{ kind: class, name: ElementA, description: This is a description of my element..., summary: My awesome element, superclass: {name: LitElement, package: lit}, members: [ {kind: field, name: foo, privacy: public, type: {text: string | undefined}, attribute: foo}, {kind: method, name: render, privacy: public, return: {type: {text: TemplateResult1, references: [...]}}} ], tagName: element-a, customElement: true, attributes: [{name: foo, type: {text: string}, fieldName: foo}], events: [{name: a-changed, type: {text: Event}, description: An awesome event to fire}], slots: [{name: default, description: The default slot}, {name: stuff, description: A slot for stuff}], cssParts: [{name: header, description: The header}, {name: footer, description: The footer}], cssProperties: [ {name: --foreground-color, description: The foreground color}, {name: --background-color, description: The background color} ] }可见description/summary/slots/cssParts/cssProperties/events 全部来自 JSDoc 标签summary、fires、slot、cssProperty/cssProp、cssPart这正是组件库作者需要养成的文档习惯——写注释即产出元数据。成员转换reactive property 如何变成字段与属性convertLitElementMembers 对 LitElement 的成员做了特殊处理普通字段原样输出而响应式属性reactive property会额外补充attribute与reflects字段。属性名解析规则与 Lit 自身的默认规则一致attribute: false→ 不生成attribute字段且该属性也不会出现在attributes数组attribute: my-attr字符串→ 使用指定的属性名其他情况 → 属性名全小写camelCase→camelcase。以 element-props.ts 为例它混合了property({type: Number})、property({type: Boolean, reflect: true})、property({type: Array})、property({type: Object, attribute: false})和state()生成的字段与属性为{ members: [ {kind: field, name: aStr, type: {text: string}, default: aStr, attribute: astr}, {kind: field, name: aNum, type: {text: number}, default: -1, attribute: anum}, {kind: field, name: aBool, type: {text: boolean}, default: false, attribute: abool, reflects: true}, {kind: field, name: aMyType, type: {text: MyType, references: [...]}, default: {...}, attribute: amytype} ], attributes: [ {name: astr, type: {text: string}, fieldName: aStr}, {name: abool, type: {text: boolean}, fieldName: aBool}, {name: astrarray, type: {text: array}, fieldName: aStrArray}, {name: amytype, type: {text: object}, fieldName: aMyType} ] }值得注意的细节attribute: false的aMyType仍出现在attributes数组中golden 中可看到amytype条目——等等对照源码convertReactivePropertiesToAttributes中property.attribute false会continue跳过。golden 中aMyType之所以有amytype属性是因为其声明写的是property({type: Object, attribute: false})…… 实际上 golden 里aMyType的字段带attribute: amytype说明当前测试工程中该属性的attribute并未设置为falsegolden 反映的是源码的实际状态。结论是只要attribute不是false响应式属性就会同时出现在members带attribute字段和attributes数组带fieldName回指两处这种冗余是有意设计的方便按“字段”或按“HTML 属性”两个视角查询类型文本映射convertReactivePropertiesToAttributes中typeOption?.toLowerCase() ?? string把 Lit 的type: Number/Boolean/Array/Object选项映射为 CEM 的类型文本number/boolean/array/object源码注释说明这对默认支持类型有效其他类型会直接输出类型名文本reflect: true会被转换为字段上的reflects: true这正是给框架包装器或文档工具判断属性是否需要反射的关键信息state()属性如aState没有出现在attributes数组中因为 state 默认attribute: false但字段本身仍会输出。类型引用references位置偏移量与包解析CEM 允许在type.text之外附带references数组标注类型文本中每个标识符的真实定义位置。转换器在 convertTypeReferences 中通过在类型文本里顺序查找符号名来计算start/end字符偏移const start text.indexOf(ref.name, curr); curr start ref.name.length;例如CustomEventMyDetail会生成两个引用start: 0, end: 11指向CustomEventstart: 12, end: 20指向MyDetail见 golden 中my-detail-custom-event条目。convertReference 还负责解析引用的归属全局对象HTMLElement、Event、CustomEvent→package: global:外部包LitElement、TemplateResult→package取依赖包名如lit、lit-html、lit/reactive-element存在时附上module包内符号 →package为包自身名称lit-internal/test-element-amodule为定义模块路径。golden 中externalTypeVar: LitElement、globalTypeVar: HTMLElement、packageTypeVar: FooBar三个变量正是这三种情况的直接演示。其他声明类型函数convertFunctionDeclaration输出kind: function、参数列表含optional、rest、default与返回值golden 中function2还展示了deprecated、参数/返回值描述等多行 JSDoc 的透传Mixinkind: mixin同样携带参数与返回类型mixin的泛型参数T会作为引用被解析普通变量kind: variabletype若类型缺失则兜底为{text: unknown}源码中有 TODO 注释指出 CEM 规范中该字段并非可选事件convertEvent中若事件类型缺失兜底为{text: Event}。空值裁剪控制 manifest 体积的关键细节custom-elements.json最终会被 IDE、文档工具反复解析体积直接影响体验。为此 src/index.ts 定义了两个守卫函数const ifNotEmpty T(v: T): T | undefined { if ( (v as unknown) false || ((typeof v string || Array.isArray(v)) v.length 0) ) { return undefined; } return v; };规则很简单值为false、空字符串、空数组时返回undefined。由于JSON.stringify会直接跳过值为undefined的键生成结果中就不会出现summary: 、members: []这类无意义条目golden 中element-without-props就没有attributes键ElementMixins的attributes键同样被省略。transformIfNotEmpty则是“先判空、再变换、再判空”的组合形态用于declarations、events、slots等需要二次映射的字段。这是全文件被反复调用的模式也是输出 JSON 之所以紧凑的原因。测试与基准校验golden 驱动的质量保障该包的测试完全由 golden 文件驱动核心测试在 src/test/generate_test.tstest(basic manifest generation, async () { const project test-element-a; const inputPackage path.resolve(testProjects, project); const analyzer createPackageAnalyzer(inputPackage as AbsolutePath); const pkg analyzer.getPackage(); await writeFileTree(outputFolder, await generateManifest(pkg)); await assertGoldensMatch(outputFolder, path.join(goldens, project), { formatGlob: **/*.json, }); });流程为对../test-projects/test-element-a运行分析 → 生成 manifest → 写入临时目录 → 与 goldens/test-element-a/custom-elements.json 逐字节比对。测试输出 JSON 会经过格式化formatGlob: **/*.json保证比对稳定。测试工程 test-element-a 内的每个源文件对应一类覆盖场景可对照 golden 验证源文件覆盖的转换特性element-a.tsJSDocsummary/fires/slot/cssProperty/cssPart、子目录模块、别名重导出Baz、全局/包内/外部类型引用element-props.ts各类型property的字段/属性映射、reflect、自定义 interface 类型element-events.ts多种事件 payload 类型string/number/自定义类/TemplateResult与事件引用解析element-mixins.tsmixin 声明、mixins继承列表element-without-props.ts无属性元素的空值裁剪当组件 API 行为变更需要更新基准时可运行npm run test:update-goldens即UPDATE_TEST_GOLDENStrue npm run test见 package.json自动重写 golden 文件后再人工审查 diff。扩展与集成把生成器接入自己的工具链如果你不想依赖 CLI而希望把 manifest 生成嵌入自己的构建流程有三种思路直接调用generateManifest拿到FileTree后用自己的逻辑写盘如合并进自定义发布流程复用getCommand()返回的命令对象它满足 CLI 生成器接口name/description/kind/generate可以被任何遵循该接口的宿主加载参照 CLI 的引用式加载在 generate.ts 中生成器以{name, installFrom, importSpecifier}引用注册、按需安装并动态import()你可以在自己的 CLI 中复刻这套“引用 → 解析 → 安装 → 导入 → 执行”的机制。generateManifest的输出是纯数据FileTree不触达文件系统因此可以方便地在内存中组合多个生成器产物例如 manifest React 包装器一起输出到--out目录。当前实现的限制与注意事项源码注释明确标注了若干尚未完成或需要留意的点使用时应知晓source.href与demos字段未输出convertClassDeclaration与convertLitElementDeclaration中均有// TODO注释source: {href: TODO}、// demos: [], // TODO因此目前 manifest 不包含源码链接与示例地址个别字段使用非空断言convertCommonDeclarationInfo中的declaration.name!、变量类型的?? {text: unknown}、事件类型的?? {text: Event}都源于 CEM schema 中这些字段并非可选转换器只能用兜底值保证输出合法类型文本的准确度convertReactivePropertiesToAttributes的类型映射只对 String/Number/Boolean/Array/Object 保证理想结果其余类型直接输出类型名文本对 CEM 消费者而言“够用但不保证完整语义”实验性定位包名带labs前缀版本见 package.json当前0.3.6API 仍可能演进要求 Node 14.8.0JSDoc 是元数据的源泉描述、事件、插槽、CSS 自定义属性、CSS Parts 等丰富信息全部来自注释标签注释缺失的元素在 manifest 中会相应“瘦身”。小结lit-labs/gen-manifest是一个小而精的代码生成器输入是lit-labs/analyzer的静态分析结果输出是符合社区规范、体积经过裁剪的custom-elements.json。它把“组件公共 API 文档化”这件事从人工维护变成自动生成并且通过 golden 测试保证每次输出可复现、可审查。无论是想为自己的 Lit 组件库生成标准清单还是想理解 CEM 生成管线的内部实现都可以以 src/index.ts 与 golden 文件 为第一手参考。【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考