angular 提供对ivy库构建中插入的导入路径的控制

5kgi1eie  于 5个月前  发布在  Angular
关注(0)|答案(5)|浏览(50)

与特性请求相关的@angular/*包有哪些?

compiler-cli

描述

目前,ivy库编译器在需要引入新导入时会尝试猜测在哪里找到模块、指令、组件或管道。据我所知,这是基于目标是否为APF入口点来实现的。
这在我们的使用场景中并不奏效,我们的monorepository是通过yarn workspaces管理的。换句话说,本地包安装在repo本身的node_modules中,并将构建成APF npm包并稍后部署。我们在(大多数)tsconfigs中没有paths,因为node的解析算法完全能够处理我们的设置。
但是,由于文件不在APF本地,因此编译器插入了深层次的导入,例如../../node_modules/@scope/pkg/src/my.module而不是@scope/pkg。当使用ng-packagr时,会出现错误,如示例仓库https://github.com/bgotink/angular-repro-20210826所示。错误与#38876中的相同。
我们有自己的(闭源)库管道,可以“纠正”ivy编译器添加的路径,将相对导入运行回裸包指定符。这是通过Program#emit中的customTransformers选项在.d.ts中的一个afterDeclaration转换器来实现的。然而,我们无法在Program中实现相同的行为,因为那里没有afterDeclaration转换器。这导致发布具有无效类型的包。
我已经在上面链接的仓库中包含了我们内部管道的输出。
这个转换器实际上在我们转向ivy之前就已经存在了。它是为了解决#23917而引入的。当我们转向ivy时,我们扩展了它,但没有考虑到新导入路径的根本原因。

建议的解决方案

@angular/compiler-cli的API中提供对插入导入的更多控制。虽然编译器-cli本身不知道它被使用的整个上下文,并且必须做出一些假设,但编译器-cli包的消费者对上下文有更多的了解,可以做出更明智的决定。
在我们的情况下,我们希望编译器始终使用当前正在构建的入口点的非APF导入的裸包指定符。

考虑过的其他方案

  • 将位于node_modules文件夹内的非APF包视为“将在发布时成为APF”,而不是“这是一个本地私有包”。

这种选择是否可行尚存疑虑,因为可能有一些仓库,其中本地私有包是通过yarn workspaces链接的。
这可以通过在编译器-cli API中或通过angularCompilerOptions启用/禁用。

  • Program#emit中提供一个afterDeclaration自定义转换器选项。这将允许我们继续通过将相对导入替换为裸包指定符来解决插入导入的问题。

或者,在angular中什么都不做。我们可以将整个monorepository构建成拓扑顺序(依赖项优先于依赖项),而不是项目出现在angular.json中的顺序,这样就可以使用tsconfig paths加载本地依赖项构建的APF包。
这将起作用,但是...

  • 这将要求所有(传递)依赖项始终构建才能构建任何单个包。这与我们的测试管道冲突,就像nxaffected功能一样,仅针对影响PR的项目构建以大大加快大型项目的管道速度。
  • 如果有人忘记重建依赖项,这会导致令人困惑的行为,因为tsconfig将使用先前的版本。这尤其适用于我们仍然希望在我们的IDE中解析到TypeScript源的情况。此外,生成的@microsoft/api-extractor API报告错误的数量也会激增。
oyjwcjzk

oyjwcjzk1#

这个问题是否与generateDeepReexports编译器选项有关,或者已经通过该选项解决?
https://github.com/angular/angular/blame/master/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts#L264-L293

p1iqtdky

p1iqtdky2#

@petebacondarwin 这个选项似乎没有任何区别。在OP中的例子仓库里,如果我打开或关闭generateDeepExports,ng-packagr仍然会产生相同的错误,我们的内部管道仍然产生相同的输出。
generateDeepExports的文档也让我觉得它可能不太合适:当包被构建时,正在导入的本地依赖项不在APF中,但它将在下游项目中作为依赖项安装时成为APF。

3wabscal

3wabscal3#

我查看了一下仓库,我认为这里的根本原因是 OneModule 是从源代码中消耗的,这使得 OneModule.forRoot 的实现暴露给了 Angular 的静态解释器(而不是仅仅暴露 .d.ts 文件)。当根据 .d.ts 元数据解释函数调用时,编译器使用一个 "foreign function resolver" 来提取一个可以评估的表达式:
angular/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts
第 457 行到第 477 行
| letexpr: ts.Expression |null=null; |
| if(context.foreignFunctionResolver){ |
| expr=context.foreignFunctionResolver(lhs,node.arguments); |
| } |
| if(expr===null){ |
| returnDynamicValue.fromDynamicInput( |
| node,DynamicValue.fromExternalReference(node.expression,lhs)); |
| } |
| |
| // 如果外部表达式出现在不同的文件中,那么假设调用表达式的拥有者模块也应该用于解析外部表达式。 |
| if(expr.getSourceFile()!==node.expression.getSourceFile()&& |
| lhs.bestGuessOwningModule!==null){ |
| context={ |
| ...context, |
| absoluteModuleName: lhs.bestGuessOwningModule.specifier, |
| resolutionContext: lhs.bestGuessOwningModule.resolutionContext, |
| }; |
| } |
| |
| returnthis.visitFfrExpression(expr,context); |
重要的是,如果函数是通过绝对模块规范符导入的,它会努力切换评估上下文以开始跟踪原始导入规范符。对于具有主体的函数,编译器无论如何都会使用当前的评估上下文:
angular/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts
第 480 行到第 497 行
| letres: ResolvedValue=this.visitFunctionBody(node,fn,context); |
| |
| // 如果尝试解析函数体的结果是一个 DynamicValue,尝试使用如果存在的话。这仍然可能产生一个可用的值。 |
| if(resinstanceofDynamicValue&&context.foreignFunctionResolver!==undefined){ |
| constffrExpr=context.foreignFunctionResolver(lhs,node.arguments); |
| if(ffrExpr!==null){ |
| // 这个函数能够从这个函数中提取一个表达式。看看那个表达式是否会导致非动态结果。 |
| constffrRes=this.visitFfrExpression(ffrExpr,context); |
| if(!(ffrResinstanceofDynamicValue)){ |
| // FFR 产生了一个实际的结果,而不是动态结果,所以用那个替代原来的解析。 |
| res=ffrRes; |
| } |
| } |
| } |
当应用与外部函数相同的技术时,如下所示,您的示例使用 @scope/one 作为模块规范符正确编译。我在评估函数体之前添加了以下逻辑:

// If the function occurs in a different file, then assume that the owning module
    // of the call expression should also be used for evaluating the function body.
    if (fn.node.getSourceFile() !== node.expression.getSourceFile() && lhs.bestGuessOwningModule !== null) {
      context = {
        ...context,
        absoluteModuleName: lhs.bestGuessOwningModule.specifier,
        resolutionContext: lhs.bestGuessOwningModule.resolutionContext,
      };
    }
l0oc07j2

l0oc07j24#

这对你来说听起来合理吗?

irtuqstp

irtuqstp5#

我们一直在每隔几个月遇到这个问题,目前我们的解决方案是将 forRoot() 函数替换为另一种设置(例如第二个仅包含根模块)。我们现在开始遇到更难解决的情况。
@JoostK 你提出的修复方案是否可行,或者能否为我们添加必要的钩子来解决这个问题?
编辑:在写这条评论后,我意识到 emitCallback 确实让我们可以访问 typescript Program 上的 afterDeclarations 钩子,所以我已经能够将我们已经为 .js 文件找到的相同解决方法更新为也适用于 .d.ts 文件的方法。

相关问题