首页
/ Storybook @storybook/angular-cm 深度解析:基于热 LanguageService 的进程内 Angular Docgen 元数据解析器

Storybook @storybook/angular-cm 深度解析:基于热 LanguageService 的进程内 Angular Docgen 元数据解析器

2026-09-06 17:34:42作者:冯爽妲Honey

@storybook/angular-cm 是 Storybook 仓库中为 Angular 渲染器提供的进程内(in-process)docgen 引擎:它不再依赖外部 Compodoc 进程和 documentation.json,而是直接用 TypeScript LanguageService 从组件源码里读出输入、输出、方法与 JSDoc 元数据,再转成 Storybook 的 argTypes。读完本文,你将理解这条"源码 → 元数据记录 → argTypes → props 表"的完整数据链路、propsTable 配置项的三档过滤语义,以及该解析器目前已知但未修复的三类边界限制(数字枚举、编译库继承 IO、hostDirectives),从而能够在自定义 Angular 项目集成与上游协作时做出准确判断。

一、包定位:一个不发布的内部包,是 Compodoc 管线的继任者

包的 README 开宗明义:

  • 这是"进程内 Angular docgen"——用热(warm)LanguageService 直接从 TypeScript 源码读取组件元数据;
  • 核心类 AngularComponentMetaManager 为每个匹配到的 tsconfig 保持一个热 LanguageService,产出 Compodoc 形状的(Compodoc-shaped)记录,再由本包自带的 extractArgTypes 把记录转成 argTypes;
  • 包是 Node-only 且 internal 的:它不会独立发布,而是被打进 @storybook/angular-vitepackage.json"private": truesideEffects: false,唯一 peerDependency 是工作区内的 storybookdevDependencies 里只带了 typescript^6.0.3,仅用于测试),生产环境中分析器会在运行时使用宿主项目自己的 TypeScript(见第五节)。

@storybook/angular-compodoc 的关系:有意不同步的 fork

README 专门用一节说明了它与老管线 angular-compodoc 的关系,这是理解本包代码注释的关键背景:

这里的转换是 @storybook/angular-compodoc 中那份的有意 fork。后者将随 Compodoc 管线一起在 Storybook 11 中删除。那份副本被其已提交的基线(baselines)钉死在旧行为上保持冻结;本包是继任者,只携带修正后的规则,因此两者不做同步,修复应该提到这里来。

extract-arg-types.ts 的文件头注释也重复了这一点:老包被基线测试钉住不能改行为,新包只写"修正过的规则"。这意味着阅读新代码时不能拿老包的行为当作参照,反之亦然。

二、核心架构:AngularComponentMetaManager 与每个 tsconfig 一个热 LanguageService

2.1 Manager 的公共 API

manager.ts 中导出两个关键符号:

export class AngularComponentMetaManager extends ComponentMetaManager<
  AngularComponentMetaProject,
  ts.ParsedCommandLine
> {
  constructor(typescript: typeof ts) {
    super(typescript, createAngularProjectFactory(typescript));
  }

  /** 提取单个组件的元数据;文件及其 re-export 目标里都找不到
   *  匹配 `names` 的类时返回 undefined */
  extractComponentMeta(
    componentPath: string,
    names: { exportName: string; localName?: string }
  ): AngularComponentMetaResult | undefined

  /** 堆内存压力下释放缓存项目;没有批量接口时按每次 payload 调用 */
  recycleIfHeapPressured(): void
}
  • 继承自 storybook/internal/component-meta 的通用基类 ComponentMetaManager,项目工厂负责 tsconfig 解析(parseTsconfigCommandLine)、项目创建与回收;
  • 类上的文档注释给出了生命周期契约:热 LanguageService 会常驻内存,因此要用 startWatching() / dispose() 包住 manager 的整个存活期;
  • extractComponentMeta 是唯一面向调用方的提取入口,参数是一个文件路径加 { exportName, localName } 两个名字——localName 用于 re-export 场景(export { X as Y })下按源文件的真实类名兜底。

工厂 createAngularProjectFactory 中有三个值得注意的工程细节:

  1. 共享的 FileSnapshotCachefsFileSnapshots: Map):所有项目共用同一份文件文本快照,快照比任何单个项目活得都久,所以 recycle/dispose 时必须 fsFileSnapshots.clear()L56-L59);
  2. 共享的 DocumentRegistry:注释写明这是为了让各项目复用已解析的 SourceFile,而不是每个项目各自持有一份 lib.d.ts 的解析结果;
  3. 推断项目(inferred project)的宽松默认值:对于任何 tsconfig 都不覆盖的文件(例如放在工作区外或 include 之外的组件),会创建一个带以下选项的"推断项目"(L37-L48):
{
  strict: false,
  allowJs: true,
  skipLibCheck: true,
  experimentalDecorators: true, // 这些"无主"文件仍在使用旧式装饰器
  target: ts.ScriptTarget.Latest,
  module: ts.ModuleKind.ESNext,
  moduleResolution: ts.ModuleResolutionKind.Bundler,
}

测试 manager.test.ts 中"serves files no tsconfig covers from the inferred project"用例验证了这条兜底路径:一个没有 tsconfig 的临时目录里写入组件文件后,getProjectForFile 返回 configFileNameundefined 的项目,且仍能提取出 @Input() amount

2.2 项目内部:手写 LanguageServiceHost 与文件新鲜度

project.tsAngularComponentMetaProject 继承通用基类 ProgramBackedProject,构造时创建 ts.LanguageService。有几处实现决策都写在注释里:

  • Host 是手写的L47-L68),而不是用 Volar 那套——因为 Angular 组件就是普通 TS 文件,不需要语言插件或 script-id 映射;
  • 刻意不实现 getProjectReferences:注释说明"遵守它会丢弃被引用项目里的文件",这里要的是把它们都拉进来;
  • 实现 realpath:不做 realpath,TS 无法对符号链接包去重,会导致类型身份(type identity)分裂。

extract() 的主流程(L76-L135)是理解"为什么提取永远是新鲜的"的关键:

  1. this.files.ensureFresh([fileName])——即使没有 watcher、或提取落在 watcher 防抖窗口内,也先强制核对目标文件的磁盘状态;
  2. 若文件不在 program 根集里(推断项目、或组件不在 tsconfig 的 include 内),调 ensureFiles 后重新取 program;
  3. 该组件的 import 闭包做一次 ensureFresh 扫描(importClosurenode_modules 为边界——注释的理由是"依赖的源码不会在运行中的 dev server 下变化"),保证基类文件的修改也能让继承来的成员刷新;
  4. 拿到 TypeChecker 后调用 analyzeSourceFile 得到整文件的 AngularFileMeta,再依次尝试:按导出名直查(pickEntry,含 default export 的类名解析)→ 沿模块导出解析 re-export(extractViaModuleExports,处理 export { X as default }、桶文件等,且跳过声明文件里的声明)→ 都失败时打出一条列出该文件实际声明了哪些类的 debug 日志——注释说"把名字不匹配从静默失败变成显眼失败,这是 props 表为空的常见原因"。

2.3 热更新与测试基线

manager.test.ts 用一个共享 manager 覆盖了整条生命周期(与生产环境一致,因为"构建 program 很昂贵")。值得留意的用例:

  • 嵌套 tsconfignested/ 子目录下的组件解析到子 tsconfig 的独立项目,而不是根项目;
  • 继承提取ToggleComponent 的输入同时包含本类的 @Input() label、signal input()size、以及来自 base-toggle.ts 的继承输入 disabled
  • 桶文件 / 别名 re-export / default 导出barrel-star.tsbarrel-named.tsaliased-default.component.ts 分别验证星号桶导出、改名 re-export、export { X as default }
  • 热更新三件套onFilesChanged 之后重提取(含 mtime 被钉在同一秒的极端用例)、基类编辑后继承成员刷新、以及没有任何变更事件时靠 ensureFresh 的 mtime 扫描兜底拾取磁盘变化。

三、分析器:把 SourceFile 解析成 Compodoc 形状的记录

3.1 分类与跳过规则

analyze-file.tsanalyzeSourceFile(ts, sourceFile, checker) 遍历顶层语句:

  • enum / type 声明先喂给 TypeIndex(供第 4 节的枚举与类型别名解析用);
  • 类声明先查两个"静默丢弃"条件:带 JSDoc @ignore 标签的类、以及 @NgModule 类(都只打 debug 日志,L38-L46);
  • 其余类按装饰器名分类(classify):Component / Directive / Pipe / Injectable / 普通 class;
  • 对 component/directive,额外从装饰器对象字面量里静态读出 selectorstandalone;读取时支持"标识符引用其初始化器"这一级求值(resolveInitializer 注释称这是"ngtsc 自己的部分求值器所接受的一小部分")。

3.2 记录的数据模型

types.ts 定义了全部形状,注释自述"原始形状贴近 Compodoc 的 JSON schema,分析器自有的字段则使用更严格的契约"。要点:

  • Directive(含 type: 'component' | 'directive')记录被拆成四个桶:propertiesClass / methodsClass / inputsClass / outputsClass;普通 class、PipeInjectable 则是 properties / methods(普通类也会附带 inputsClass? / outputsClass?,提取器对非指令条目忽略它们);
  • Property 上几个对下游规则至关重要的字段(L58-L78):
    • visibility?: 'private' | 'protected'——public 成员省略;
    • internal?: boolean——是否带 JSDoc @internal 标签;
    • type?——分析器无法定型的成员(如 @HostBinding)省略;
    • required?——仅 signal input 与 @Input({ required }) 有;
    • initializer?: PropertyInitializer——语法分类后的源码初始化器,分 literal(含 string/number/boolean/null/undefined/bigint/enum/composite 八种 literalKind)与 expression 两种,注释明确"表达式是元数据,不是展示用默认值";
    • line?: number——成员声明的行号(1-based),这是 model() 结构性识别的基础(见 4.5);
  • AngularFileMeta 把整文件按 components/directives/pipes/injectables/classes 分桶,外加 miscellaneous: { typealiases, enumerations }
  • AngularComponentMetaResultextractComponentMeta 的返回值:entry(解析到的类记录)+ json(整文件记录,供提取器做别名/枚举解析)+ 可选 jsDocInfo(类级 JSDoc 描述与标签)。

3.3 继承合并与类型渲染

inheritance.tsresolveClassMembers 把基类成员折叠进子类:沿 checker 的 getBaseTypes 递归走继承链(带环保护),处理泛型基类的类型参数替换(extends 子句省略的参数会回退到参数默认值,默认值本身也会做替换),最后才应用本类自己的 @Directive({ inputs }) 元数据映射——注释强调"递归在这里完成而不是分开导出各 pass,就是为了保证这个顺序不需要调用者记住"。

其中有一段代码直接对应 README 的第二条已知限制(见第六节):

// 声明文件不记录装饰器或 signal 调用,所以来自它的基类
// 对 IO 桶没有任何可贡献的内容。
if (!declaration.getSourceFile().isDeclarationFile) {
  mergeBucket(ctx, members, baseMembers, 'inputs');
  mergeBucket(ctx, members, baseMembers, 'outputs');
}
mergeInto(members.properties, baseMembers.properties, members);
mergeInto(members.methods, baseMembers.methods, members);

L84-L91)——来自 .d.ts 的基类,属性和方法照常合并,但 IO 桶被跳过。

type-index.tsTypeIndex 负责"渲染类型文本 + 记录文本提及的命名类型",注释解释了为什么两半必须绑在一起:提取器仅凭名字把成员 type 字符串对照 miscellaneous 解析,如果登记的名字和渲染文本用的名字不一致,就永远查不到。其 render 是刻意语法化的(L37-L104):字符串字面量双引号(JSON.stringify,保证内嵌引号/反斜杠仍可被提取器的 JSON.parse 兜底解析)、联合用 | 连接、函数/构造函数渲染为真实签名 (...) => T / new (...) => T——后者对应提取器里 isFunctionTypeStringnew 前缀和泛型签名的放宽(早期只认开头 ( 会把这两种都打进 empty-enum 兜底)。

四、extractArgTypesFromData:从记录到 argTypes 的转换规则

extract-arg-types.ts 是本包导出面的一部分(见 index.ts),签名:

extractArgTypesFromData(
  componentData: Class | Directive | Injectable | Pipe,
  { metadataJson, propsTable, logger }: ExtractArgTypesOptions
): StrictArgTypes

其中 propsTable 没有默认值,注释写明"故意设为必传,让宿主继承不到静默默认"。

4.1 PropsTableMode:一条严格的档位阶梯

L35-L48 定义了核心概念:

/**
 * 哪些成员进入 props 表,作为一条严格阶梯:`all` ⊃ `api` ⊃ `inputs`。
 *
 * - `all`:所有分区的每个成员。
 * - `api`:组件面向模板的表面——无论 TypeScript 可见性如何声明的输入与输出,
 *          加上所有非 TS `private`、非 ES `#`、且不带 JSDoc `internal` 标签
 *          的属性和方法。
 * - `inputs`:inputs 分区,外加一个被文档化的 `model()` 为了让双向绑定成立
 *          而必需的 `${name}Change` 输出。
 */
export type PropsTableMode = 'all' | 'api' | 'inputs';

过滤函数 documentedInModeL405-L417)对 inputs/outputs 分区只按 @internal 过滤,注释解释了一个容易被误解的点:TypeScript private 与 ES # 只挡代码与本组件模板的访问,消费方模板绑定 input/output 不看修饰符(Angular 只在 opt-in 的 strictInputAccessModifiers 下才看),所以 inputs/outputs 区不因可见性被隐藏;而属性和方法分区则要求非 private

4.2 required 语义与分区

  • required 判定(L85-L88):required 跟随 @Input({...}) 键的存在与否,optional 必须与之一致;键缺失时看初始化器——带默认值的输入永远不是必绑的(item.required ?? item.initializer === undefined) && !item.optional
  • 分区顺序固定为 SECTION_ORDERL71-L80):properties → inputs → outputs → methods → view child → view children → content child → content children。带 @ViewChild/@ViewChildren/@ContentChild/@ContentChildren 装饰器的属性被分流到对应分区,而不是堆在 properties 里。

4.3 枚举、联合与类型别名的控制(control)推导

extractTypeL244-L275)决定一个 input 显示什么控件:

  1. string/boolean/number 直接对应同名 SB 类型;
  2. 函数类型字符串function 或以 (new <...>( 开头的箭头/构造签名 → function 控件;
  3. 类型别名递归解析resolveTypealias 沿 miscellaneous.typealiasesrawtype 递归,seen 集合防 type A = B; type B = A 互引用例——注释说这不是优化,而是"递归会把整个同步 docgen worker 拖垮,而不是只影响一个组件";
  4. 纯原语联合:联合成员全是 boolean/number/string 时,控件取最窄成员(boolean 优先于 number 优先于 string),但 summary 保留完整联合——这是为了 coercion 变换产生的 boolean | string、可选的 string | undefined 之类不再掉进 JSON object 控件;
  5. 枚举extractEnumValuesmiscellaneous.enumerations 里按名字找,且所有 child 都必须带值才成立,否则回落到对联合字面量的 JSON.parse,再失败就是 { name: 'other', value: 'empty-enum' } 兜底。

多声明同名时由 pickDeclarationL143-L161)做确定性裁决:优先选与组件同文件的声明,再按文件路径 localeCompare 排序取第一个——注释直指 Compodoc 的痼疾:"Compodoc 每次运行枚举条目的顺序都不同,而像 Size 这种名字通常每个组件文件夹都声明一次,first-wins 会让控件类型在源码没变的情况下漂移。"

4.4 默认值:绝不发明数值

extractDefaultValue 的取值优先级是:JSDoc @default/@defaultvalue(裸标签不可用,多标签取最后一个,字符串会去掉成对引号)→ 源码初始化器(仅 literal 六种 literalKind 直接取值,bigint/enum/composite 保留文本,expression 走 debug 日志"non-literal default not shown")→ undefinedcastDefaultValueL294-L316)对 boolean/number 做安全转型,EventEmitter 一律不给默认值,注释原则是"绝不发明数值:缺失的默认保持缺失,而不是变成 NaN/false"。

4.5 model() 的结构性识别与 Change 合成

这是与老 Compodoc 管线共享、且被 angular-compodoc README 专门记录的一条规则:Angular 的 model() 同时是输入 foo 与输出 fooChange,但元数据只在两个数组里以裸名 foo 各出现一次,从不发 fooChange。JSON 里没有任何 model() 标记,所以检测是结构性的:同名 input 与 output 出现在同一声明行。仅凭同名不够——@Input('shared')@Output('shared') 会形成同样的名字碰撞但没有 model(),把这种当双向绑定会删掉真实输出、发明出不存在的 sharedChange。这正是 Property.line 字段存在的理由("两个生产者因此都必须在能被该规则匹配的成员上记录 line")。

实现见 getModelPropertiesL514-L541:识别出 model() 后丢弃裸名输出副本,并合成 ${name}Change 输出(类型为 (e: T) => void、无 defaultValuerequired)。该合成在 inputs 档下也会发生——注释说明这一档"收窄分区但不能拆散一对被文档化的绑定",只有当该档把 model 本身藏掉时才跳过。

五、接入点:@storybook/angular-vite 的 docgen 管线

@storybook/angular-cm 被打包进 code/frameworks/angular-vite 使用,接入点有两层:

1. worker 中间件docgen-worker.ts):

const createManager = async (): Promise<AngularComponentMetaManager | undefined> => {
  try {
    // 首次使用时才导入,让分析器看到的是项目构建所用的 TypeScript 版本,
    // 而这个版本正是本包刻意不随包发布的。
    const typescript = await import('typescript');
    const manager = new AngularComponentMetaManager(typescript.default ?? typescript);
    manager.startWatching();
    return manager;
  } catch (error) {
    logger.warn(`Angular docgen is unavailable: ...`);
    return undefined;
  }
};

L15-L31)——manager 在整个 worker 生命周期内只建一次,LanguageService 因此在组件之间保持热状态;创建失败时降级为把请求放行给下一个 provider,而不是让 docgen 整体报错。createDocgenProvider 返回的中间件对每个 story 条目:非 story 文件直接放行 → 懒建 manager → buildDocgenPayload → 每次提取后调 recycleIfHeapPressured()(注释:"没有批量表面可挂这个钩子,所以堆压力检查按每次提取做一次")→ 我们的 payload 若带 error不覆盖链上其他 provider 的结果("用我们自己的错误替换别人的 payload,等于让我们这条链对一个自己不了解的组件行使一票否决权")。

2. payload 构建build-docgen.ts):buildDocgenPayload 从 story 文件解析出 meta.component 指向的类,然后依次处理几类失败——组件表达式无法解析、import 无法解析(提示检查 import specifier 与 tsconfig 路径别名)、extractComponentMeta 抛异常(AngularComponentMetaExtractionFailed)、提取结果为空(AngularComponentMetaNotFound,提示"确认文件导出组件类且被其目录或上层的 tsconfig.json 覆盖")。成功路径上:

  • propsTable 配置调用 extractArgTypesFromData 得到 argTypes
  • 另跑一遍 propsTable: 'api' 生成 apiArgTypesapiDescription 使用(L220-L229)——注释说明这是为 Agent 文档固定的:"无论用户给 props 表选了什么,Agent 文档都钉在 apiall 会把 Agent 无法绑定的私有布线交出去,inputs 又会让 Outputs 分区变空";
  • metaToSnippetMetaL79-L104)把记录压成组件片段元数据(name/selector/standalone/inputs/outputs/enums),其中 output 绑定名对 model()Change 后缀,与 4.5 的合成规则一致。

六、配置面:propsTable 的三档取值与兼容映射

面向用户的开关只有一个,解析逻辑在 props-table.ts

export const resolvePropsTable = (
  frameworkOptions: PropsTableInput,
  features: Features
): PropsTableMode => {
  const deprecatedFlag = features?.angularFilterNonInputControls;
  const inherited = deprecatedFlag === undefined ? undefined : deprecatedFlag ? 'inputs' : 'all';
  return configuredMode(frameworkOptions) ?? inherited ?? 'api';
};

L27-L35)语义为:

  1. @storybook/angular-vite framework 选项里的 propsTable'all' | 'api' | 'inputs')优先;拼错的值会被 configuredMode 拦下——注释解释"拼错的值来自无类型 JS 配置,放任它会半生效,各管线各读各的垃圾字符串",并由 warnAboutPropsTable 打印 "Ignoring the unknown propsTable value ... expected 'all', 'api' or 'inputs'";
  2. 未配置时回落到已废弃features.angularFilterNonInputControlstrue'inputs'false'all',并打 deprecation 提示"将在 Storybook 11 移除",同时告知 propsTable 框架选项优先级更高;
  3. 两者都没有 → 默认 'api'

一个典型配置形态(framework 选项传入 options 对象,与 resolvePropsTable 读取的 frameworkOptions.propsTable 对应):

// .storybook/main.ts
export default {
  framework: '@storybook/angular-vite',
  options: {
    propsTable: 'api', // 'all' | 'api' | 'inputs',缺省即 'api'
  },
  features: {
    experimentalDocgenServer: true, // 'api' 档依赖该特性开启,见下
  },
};

warnAboutPropsTableL44-L72)还有一条与特性开关联动的警告:propsTable: 'api' 需要 experimentalDocgenServer 特性开启,否则"props 表仍会显示所有成员"(因为该特性关闭时 docgen preset 整体被跳过)——即配置了 'api' 但特性关闭时实际效果等价于 'all',需显式开特性或改设为 'all'

七、已知限制:继承自 Compodoc 的三类缺口(当前未修复)

README 的 "Known limitations" 一节列出了三条从 Compodoc 行为继承、尚未修复的限制。结合源码可以看清每条的成因与边界:

7.1 数字枚举产生不了控件

数字枚举产生不了控件。 enum Size { Small, Medium }enum Size { Small = 1, Medium } 都表现为 empty-enum 而非 select,因为自增成员没有可读的初始化器。字符串枚举与联合类型别名能正确解析。TypeChecker 可以通过 getConstantValue 回答这个问题,所以这在本文的分析器里以 Compodoc 做不到的方式可修。

成因链路:TypeIndex.enumMemberValueL344-L360)只在成员有字符串字面量或数字字面量初始化器时产出 value,注释特意说明"数字初始化器保持为 number,所以 0 成员是 falsy,能正确禁用提取器的枚举路径";自增成员没有初始化器 → value 缺失 → extractEnumValues 要求 childs.every(hasEnumValue) 不成立 → 回落到 empty-enum。字符串枚举每个成员都有字面量,所以正常;type Size = 'small' | 'medium' 走联合字面量的 JSON.parse 路径也正常。README 同时指出修复方向:checker.getConstantValue 能算出自增成员的实际值——这是 Compodoc 静态解析做不到、而本包(持有 TypeChecker)做得到的一点。

7.2 从编译库继承的输入/输出被跳过

从编译库继承的 inputs 和 outputs 被跳过。 来自 .d.ts 的基类会贡献其普通属性与方法,但不贡献其 IO,因为装饰器已被擦除;Angular 把它们记录在 ɵɵDirectiveDeclaration 的类型参数里,而本分析器不读它。

对应 inheritance.ts L84-L91isDeclarationFile 判断:声明文件里 @Input() 装饰器与 input() 调用都不存在(编译产物只留下类型签名),分析器选择只合并属性与方法、放弃 IO,而不是猜测。后果是:基类在 npm 包(如某个组件库)里、只以 .d.ts 形式进入本项目时,其输入/输出不会出现在 props 表里。

7.3 hostDirectives 绑定缺席

hostDirectives 绑定缺席。 组件通过指令组合(directive composition)暴露的输入和输出不是类成员,因此永远进不了 props 表。

分析器的工作单位是"类成员 + 装饰器",@Component({ hostDirectives: [...] }) 引入的绑定发生在 Angular 编译期的宿主合成上,源码里没有任何类字段可分析,属于结构性盲区。

八、小结

@storybook/angular-cm 展示了"用编译器替代文档生成器"的一条完整工程路线:

  • 进程内:热 LanguageService + 共享 DocumentRegistry/快照缓存,替代 Compodoc 外部进程与 JSON 产物;manager.ts 负责多 tsconfig 项目划分与生命周期,project.ts 负责文件新鲜度与 re-export 解析;
  • 确定性:Compodoc 形状的记录(types.ts)刻意保留 line 等强契约字段,pickDeclaration 的同文件优先裁决与 seen 防环,都是针对老管线非确定性行为的定点修正;
  • 可控的文档表面propsTable: 'all' | 'api' | 'inputs' 三档阶梯(extract-arg-types.ts + props-table.ts),默认 api,且 Agent 侧文档固定使用 api 档;
  • 已知边界:数字枚举、.d.ts 基类 IO、hostDirectives 三类缺口均有明确成因(见第七节),且 README 明确它们是"继承而来、尚未修复",其中数字枚举一项已被标注为在本包中可修。

相关入口文件:code/lib/angular-cm/src/index.tsextractArgTypesFromData / AngularComponentMetaManager 及类型的公共导出)、code/frameworks/angular-vite/src/docgen/build-docgen.ts(payload 构建)、code/lib/angular-compodoc/README.md(将被删除的旧管线及其 model() 规则的背景)。

登录后查看全文
热门项目推荐
相关项目推荐