拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Angular 编译器 ngtsc 的 Shims 机制解析:如何把“虚拟源码文件“无缝注入 TypeScript 程序

Angular 编译器 ngtsc 的 Shims 机制解析如何把虚拟源码文件无缝注入 TypeScript 程序【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular本篇技术指南以 Angular 仓库 packages/compiler-cli/src/ngtsc/shims/README.md 为骨架结合该目录下api.ts、src/adapter.ts、src/reference_tagger.ts、src/expando.ts等源码实现与对应单元测试系统讲解 ngtscAngular 新一代编译器核心中的 shim垫片/虚拟文件机制它是什么、两类生成器接口如何定义、ShimAdapter与ShimReferenceTagger如何协同把 shim 纳入ts.Program以及该机制在模板类型检查、扁平模块入口生成等真实编译管线中的落地方式。读完你可以理解 ngtsc 编译管线里磁盘上不存在、却必须出现在 TypeScript 程序中的那批.ngtypecheck.ts文件究竟从哪里来、如何被 TypeScript 发现又如何在编译结束后被干净地清理掉。一、Shim 是什么不属于用户程序却由编译器凭空造出的文件在 ngtsc 编译管线中shim 指一类特殊的文件它们不是用户原始程序的一部分而是编译器根据用户请求或为支撑某些编译特性而额外生成的合成源码。典型动机README 开篇即点明是从 View Engine 向 Ivy 迁移旧版 View Engine 编译器会在用户文件旁产出.ngfactory文件Ivy 本身不再需要它们但如果用户要求编译器仍然可以为其生成 shim 形态的 factory 文件。这段定义在代码中对应着一个可感知的观测点这些文件并不存在于磁盘上也不在用户的tsconfig入口列表中但最终却会出现在ts.Program的源码文件集合里参与类型检查乃至 JS 输出。这正是整个 shims 子包要解决的核心矛盾——让 TypeScript 承认并加载一批不存在的文件。二、两类生成器 APITopLevelShimGenerator与PerFileShimGeneratorshim 的生成对外通过两个接口暴露实现在 packages/compiler-cli/src/ngtsc/shims/api.ts。每个实现这两个接口的类负责产出某一种特定类型的 shim一个或多个。2.1 顶层 shimTop-level shim顶层 shim 相对于整个程序而言是一个单例——无论用户源码有多少个文件它只生成一个文件追加在所有用户文件之外。接口定义api.ts#L15-L25export interface TopLevelShimGenerator { /** 该 shim 是否应在 TypeScript emit 阶段被输出转译为 JS */ readonly shouldEmit: boolean; /** 生成一个携带正确文件名的 ts.SourceFile 作为 shim */ makeTopLevelShim(): ts.SourceFile; }2.2 每文件 shimPer-file shim每文件 shim 则是针对用户程序中每一个原始ts.SourceFile生成的文件名带有固定的扩展前缀——.ngfactory类 shim 正是按每个用户输入文件一个的方式生成的。接口定义api.ts#L31-L53export interface PerFileShimGenerator { /** * shim 使用的扩展前缀。知道它可以让消费该生成器的 ts.CompilerHost 实现预测 shim 文件名 * 这在旧的 ts.Program 已包含该 shim 的旧版本时尤其有用用于增量复用。 */ readonly extensionPrefix: string; /** 该生成器产出的 shim 是否应在 TypeScript emit 阶段输出 */ readonly shouldEmit: boolean; /** * 为给定的原始 ts.SourceFile 生成 shim并指定生成的路径。 * priorShimSf 是旧程序中的同名 shim可被复用而非重新生成。 */ generateShimForFile( sf: ts.SourceFile, genFilePath: AbsoluteFsPath, priorShimSf: ts.SourceFile | null, ): ts.SourceFile; }2.3 可发射emittable与不可发射non-emittableREADME 特别强调了一个关键区分无论来自哪类生成器shim 都可能是emittable或non-emittable的。emittable其ts.SourceFile会被转译为 JS并随用户代码一起输出例如 View Engine 迁移所需的.ngfactory.jsnon-emittable只参与编译过程如类型检查用户几乎感知不到它的存在例如类型检查 shim。在接口上shouldEmit布尔值就是这一语义的载体在 adapter.ts 中shouldEmit false的 shim 会被登记到ignoreForEmit集合见 adapter.ts#L107-L109 与 adapter.ts#L228-L230供上层 host 在 emit 时跳过。README 还指出这套 API 不仅被本包内的 shim 生成器使用也被编译器的其他子系统用来生成各自形态的 shim——这正是第三部分要讲的其他用途。三、如何进入ts.ProgramShimReferenceTagger与ShimAdapter双组件shims 包对外暴露两块与把 shim 集成进ts.Program创建过程相关的具体功能导出见 shims/index.tsShimReferenceTagger在程序创建之前给ts.SourceFile打标签建立每个原始文件 → 需要为该文件生成的全部每文件 shim的引用链接ShimAdapter供ts.CompilerHost的实现使用让通过该 host 创建的任何程序都能把 shim 纳入进来。两者的真实装配点在 packages/compiler-cli/src/ngtsc/core/src/host.tsNgCompilerHost.wrap()收集好顶层与每文件生成器后构造ShimAdapter(delegate, tsRootFiles, topLevelShimGenerators, perFileShimGenerators, oldProgram)并用所有每文件生成器的extensionPrefix构造一个ShimReferenceTagger。3.1ShimAdapter识别 shim 路径并生成对应ts.SourceFileadapter 的核心职责是当 host 被请求加载某个路径时判断它是否对应一个 shim若是则现场生成并返回其ts.SourceFile。其主入口是maybeGenerate(fileName)adapter.ts#L147-L204完整识别流程为快速路径路径已被证实不是 shimnotShims集合或已是已知 shimshims缓存时直接短路返回避免反复跑正则排除.d.ts声明文件不可能是 shimadapter.ts#L157-L160正则匹配对每个PerFileShimGenerator维护一个编译好的正则^(.*)\.${extensionPrefix}\.ts$构造见 adapter.ts#L90-L98。注意它是大小写不敏感的RegExp(pattern, i)捕获组提取出文件名前缀由 shim 名反推源文件名并验证存在源文件既可能是.ts也可能是.tsx于是先尝试prefix .ts失败再尝试prefix .tsxadapter.ts#L173-L180。即使路径匹配了模式只有当真实的源文件确实存在时它才算一个合法 shim。为什么会有路径对但源文件不存在的情况adapter.ts#L181-L194 的注释给出了几种现实场景解析对.ngfactory.d.ts的 import 时模块解析算法会先在其位置寻找.ngfactory.ts用户写了错误的 import以及增量编译中某文件在上一轮存在、这一轮被删除。这类路径不会被写入notShims缓存属边缘情况不常出现。生成并缓存确认是 shim 后进入generateSpecificadapter.ts#L206-L234如果旧程序oldProgram中存在同名 shim取出作为priorShimSf交给生成器决定是否复用增量编译关键随后调用generator.generateShimForFile(inputFile, fileName, priorShimSf)并把fileShim: { extension, generatedFrom }元数据通过 expando见第五节打到生成的ts.SourceFile上最后存入shims缓存。adapter 内部用三张数据结构管理状态shims本次已生成的 shim、priorShims旧程序继承下来的 shim并非全部会被继承、notShims已证实非 shim 的路径用于短路。3.2 工程细节root 文件与extraInputFiles应对noResolveadapter 构造函数还维护一个extraInputFiles列表adapter.ts#L99-L125包含两部分所有顶层 shim 的文件名在构造时即调用makeTopLevelShim()预生成并缓存与每个 root 文件对应的每文件 shim 文件名即每个 root 文件 × 每个生成器后缀。为什么 root 文件的 shim 要显式列入输入adapter.ts#L116-L118 的注释解释在开启noResolve的编译中TS 不会走引用/导入解析而是完全依赖输入文件列表来描述程序——此时仅靠 reference tagging 是失效的必须把 shim 显式塞进输入文件列表。该列表最终被上层NgCompilerHost合并进总输入core/src/host.ts#L160this.inputFiles [...inputFiles, ...shimAdapter.extraInputFiles];。3.3 Shim 在实践中的加载路径不保证先加载源文件再加载 shimTypeScript 从 root 文件出发沿 imports 与 references 遍历发现属于程序的全部新文件。README 明确指出 shim 会以两种方式被发现README.md作为其源文件上的references这些引用正是ShimReferenceTagger加进去的作为用户手写的imports例如用户代码 import 了一个.ngfactory文件。因此完全无法保证源文件一定先于其 shim 被加载。这条约束反过来塑造了 adapter 的设计maybeGenerate必须是无状态可重入的基于文件名与磁盘/委托 host 判断而不能依赖我见过的源文件列表。3.4ShimReferenceTagger劫持/// reference机制加载 shim程序创建期间TypeScript 会枚举磁盘上的.ts文件即原始文件纳入程序但每个原始文件还关联着许多未被引用、磁盘上不存在、却同样必须纳入的 shim。把 shim 塞进程序的手段就是reference tagging由ShimReferenceTagger执行。原理建立在 TypeScript 的一个既有机制上README.mdts.SourceFile有一个referencedFiles属性存放从文件内/// reference注释提取出的路径如果带引用的ts.SourceFile被纳入程序其被引用文件也会被一并加载。ShimReferenceTagger正是借用README 原文措辞是 abuse/复用这一机制为每个原始文件伪造出指向其 shim 的引用。实现细节在 reference_tagger.ts#L45-L78其tag(sf)方法对以下情况直接跳过tagger 已停用、.d.ts声明文件、本身是 shim 的文件、已打标过的文件、以及非.ts声明类路径。对要打标的文件若从未打过标先把sf.referencedFiles的原值存进 expando 的originalReferencedFiles保证后续可恢复、可叠加多个 tagger把原值拷贝出来为每个后缀追加一条{ fileName: makeShimFileName(sfPath, suffix), pos: 0, end: 0 }引用将结果写回sf.referencedFiles并记录到tagged集合。程序创建一旦完成就必须通过cleanup()即 expando 里的untagTsFile/untagAllTsFiles见下节把referencedFiles恢复原值——因为ts.SourceFile会长期存活在各种缓存中其生命周期远超单次编译。同时 tagger 提供finalize()reference_tagger.ts#L83-L86来停用自身并清空tagged集合释放内存core/src/host.ts#L191-L193 的postProgramCreationCleanup()正是调用this.shimTagger.finalize()。四、Expando用 Symbol 属性替代 Map规避ts.SourceFile内存泄漏shim 系统需要为每个ts.SourceFile跟踪若干元数据它是否是 shim若是由哪个生成器创建及其原始源文件若非 shim则记录其原始referencedFiles供日后恢复。实现选型上README 明确排除了以ts.SourceFile为 key 的Map方案理由是可能造成内存泄漏强引用会让本可回收的ts.SourceFile及整棵 AST 常驻。取而代之的是expando 符号属性方案见 packages/compiler-cli/src/ngtsc/shims/src/expando.ts/** 打补丁到 ts.SourceFile 上承载扩展数据的 Symbol */ export const NgExtension: unique symbol Symbol(NgExtension);NgExtension是模块级unique symbol其载荷NgExtensionData结构expando.ts#L21-L34包含四个字段export interface NgExtensionData { /** 是否为顶层 shim */ isTopLevelShim: boolean; /** 每文件 shim 的数据generatedFrom 源文件路径 extension 前缀非 shim 时为 null */ fileShim: NgFileShimData | null; /** ShimReferenceTagger 修改前的原始 referencedFiles */ originalReferencedFiles: ReadonlyArrayts.FileReference | null; /** ShimReferenceTagger 打标后的 referencedFiles */ taggedReferenceFiles: ReadonlyArrayts.FileReference | null; }配套的类型收窄与工具函数让整个 shim 子系统可以安全地判别与操作文件isExtended(sf)判断文件是否已挂上NgExtensionexpando.ts#L58-L60sfExtensionData(sf)获取扩展数据没有则惰性初始化一份默认值expando.ts#L65-L81——这是 adpater 与 tagger 读写元数据的统一入口isShim(sf)判断是顶层 shim或每文件 shimexpando.ts#L110-L112isFileShimSourceFile(sf)进一步收窄为每文件 shimexpando.ts#L103-L105copyFileShimData(from, to)把 shim 归属信息从一个ts.SourceFile拷贝到另一个expando.ts#L117-L122untag/retag 系列untagTsFile用originalReferencedFiles还原referencedFilesretagTsFile用taggedReferenceFiles重新打标expando.ts#L148-L171untagAllTsFiles/retagAllTsFiles则对整个 program 批量操作——这组函数支撑了 README 所说的cleanup()语义。打标的数据采用原始值与打标值分离存储的设计带来两个直接收益均有测试覆盖见第六节多个 tagger 先后打标时总是基于最初的原始值叠加而非叠加已污染的数组打标可无损撤销、可随时重放。五、命名工具makeShimFileName与模块名推导ShimReferenceTagger在构造时把每个扩展前缀转换为形如.${extension}.ts的后缀reference_tagger.ts#L38-L40adapter 则将其规范化为正则的 suffix 部分。生成 shim 文件名的核心逻辑集中在 packages/compiler-cli/src/ngtsc/shims/src/util.tsconst TS_EXTENSIONS /\.tsx?$/i; /** 把文件名的 .ts / .tsx 扩展名替换为 shim 文件后缀 */ export function makeShimFileName(fileName: AbsoluteFsPath, suffix: string): AbsoluteFsPath { return absoluteFrom(fileName.replace(TS_EXTENSIONS, suffix)); }这意味着源文件foo.ts与某生成器前缀ngtypecheck组合会得到foo.ngtypecheck.tsfoo.tsx同样被统一替换为foo.ngtypecheck.ts——这正是同一个 shim 可能对应多个候选源文件名.ts或.tsx这一推论在文件层面的体现。同文件还提供generatedModuleNameutil.ts#L20-L33它把原始模块名 源文件名换算成 shim 的模块名并针对index.ts特判模块名追加/index 后缀从而保证export * from ./index.xxx这类写法能正确映射供生成需要可导入模块身份的 shim 使用。六、源码中的真实落地场景与测试佐证6.1TypeCheckShimGenerator类型检查 shim 是当下最核心的每文件 shim类型检查子系统是 shim 机制最重要的消费者。在 packages/compiler-cli/src/ngtsc/typecheck/src/shim.ts 中TypeCheckShimGenerator实现了PerFileShimGeneratorexport class TypeCheckShimGenerator implements PerFileShimGenerator { readonly extensionPrefix ngtypecheck; readonly shouldEmit false; // 类型检查文件绝不输出 JS ... static shimFor(fileName: AbsoluteFsPath): AbsoluteFsPath { return absoluteFrom(fileName.replace(/\.tsx?$/, .ngtypecheck.ts)); } }该文件头注释shim.ts#L14-L20解释了为何类型检查代码必须以 shim 形式进入主程序TypeScript 只有在两轮程序文件集合完全一致时才会在主程序与类型检查程序之间复用信息为了让类型检查程序能高效增量创建主程序也必须在自身文件集合中包含这些合成类型检查文件。generateShimForFile在有priorShimSf旧程序 shim时会直接复用旧版本——即便其内容已过期先保证主程序文件形状稳定以走 TS 最快的增量路径后续类型检查阶段再替换/复用shim.ts#L29-L37。在NgCompilerHost.wrap()中perFileShimGenerators.push(new TypeCheckShimGenerator())core/src/host.ts#L210使其成为本仓库中默认装配的唯一每文件生成器。整个 ngtsc 范围内所有extensionPrefix实现只有两处另一处是测试用的testshim见下文佐证了这一事实。6.2FlatIndexGenerator扁平模块入口以顶层 shim 实现README其他用途中提到的entry_point生成扁平模块 index延续 View Engine 时代的用法正是FlatIndexGenerator——它实现TopLevelShimGenerator见 packages/compiler-cli/src/ngtsc/entry_point/src/generator.ts。当 Angular 编译选项配置了flatModuleOutFile时core/src/host.ts#L222-L252 会先校验并定位扁平入口要求恰好一个.ts文件或取最高层级的index.ts否则报CONFIG_FLAT_MODULE_NO_INDEX错误随后把FlatIndexGenerator压入topLevelShimGenerators——它作为程序级单例shim 被预生成并追加为额外的输入文件。6.3 关于 factory 与 summary shim 的历史背景与当前状态README 的Usage章节对两类 shim 生成做了详细说明需要在此补充与当前仓库状态对齐的说明Factory shim 生成catch-22生成的 factory 文件在 ngtsc 中存在鸡生蛋困境——其内容依赖对当前程序的静态分析可它本身又可被当前程序 import可被 import意味着其内容必须在程序创建之前可知而分析要等程序创建之后才能进行。ngc旧版能绕开是因为其分析阶段不依赖程序创建而是依赖元数据收集/全局分析ngtsc 只能另辟蹊径用一条不依赖ts.TypeChecker因而能在程序创建前运行的轻量分析管线去估计生成文件的内容以支持建程再由一个 transformer 在 emit 阶段对估计文件操作、用精确内容替换之。README 特别强调这个估计宁超勿欠overestimate类型检查始终基于估计文件运行必须在若用精确内容本来能通过的所有情形下都通过。Summary shim 生成比 factory 简单得多可从ts.SourceFile直接生成且事后无需清理。需要说明的是在本仓库当前代码中.ngfactory字样仅残留于 packages/compiler-cli/src/transformers/api.ts#L41 的注释Dont produce .ngfactory.js or .ngstyle.js filesngtsc 中也不再装配 factory/summary 生成器。因此 README 中这两节更应理解为该 shim 机制的设计动机与历史沿革正是按需为每个文件旁产出 factory 类文件的迁移诉求催生了这套通用化、可复用的 shim 生成框架而其当前最活跃的产出是上述类型检查 shim 与扁平入口 shim。6.4 单元测试行为契约一览该包的行为契约由两套测试锁定均通过runInEachFileSystem在多种文件系统实现下运行packages/compiler-cli/src/ngtsc/shims/test/reference_tagger_spec.ts验证给file.ts打标后referencedFiles变为[/file.test1.ts, /file.test2.ts]不打标.d.ts、.js与 shim 文件finalize()之后不再打标不覆盖原始referencedFiles已有/other.ts引用时打标结果是[/other.ts, /file.test.ts]多个 tagger 相继打标时总是基于最初原始值第二个 tagger 结果不含第一个 tagger 的痕迹以及 untag→retag 往返可逆。packages/compiler-cli/src/ngtsc/shims/test/adapter_spec.ts验证ShimAdapter能识别基本 shim 名、不把普通文件当作 shim、对路径像 shim 但没有对应源文件的文件识别失败、并能从旧程序探测到 prior shim。测试用TestShimGeneratorextensionPrefix testshim、shouldEmit false见 packages/compiler-cli/src/ngtsc/shims/test/util.ts展示了实现一个最小PerFileShimGenerator的全部要点有 prior shim 时直接复用否则ts.createSourceFile生成带路径常量的极简源码。七、总结Shim 机制在 ngtsc 架构中的定位把以上内容拼合起来可以看到一条清晰的脉络抽象用TopLevelShimGenerator/PerFileShimGenerator两个接口统一所有编译器合成的源码shouldEmit区分其是否对用户可见注入ShimReferenceTagger在程序创建前借用/// reference机制为源文件挂上 shim 引用ShimAdapter在 host 层识别 shim 路径并即时生成/缓存再配合extraInputFiles兜底noResolve场景元数据与清理NgExtensionexpando 符号把是否 shim、谁生成、原始引用等元数据直接挂在ts.SourceFile上规避 Map 内存泄漏编译结束通过 untag 操作把被劫持的referencedFiles完整还原保证长期驻留缓存的源文件不被污染消费类型检查.ngtypecheck、扁平模块入口、以及历史动机中的 factory/summary 场景均建立在同一套机制之上。对想要深入 ngtsc 管线的读者建议从 core/src/host.ts 的NgCompilerHost.wrap()出发沿ShimAdapter与ShimReferenceTagger的构造参数逆推各生成器来源再结合本目录下 adapter_spec.ts 与 reference_tagger_spec.ts 的测试用例验证你对各阶段行为的理解——这套设计可视为如何在不让磁盘感知的前提下让第三方语言工具TypeScript完整编译一张含合成文件的程序图的一个典范实现。【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门