担心NPM生态系统

2020-06-30 21:10:23

NPM生态系统似乎不太健康。如果您关心安全性、可靠性或长期维护,那么选择一个合适的包来使用几乎是不可能的-这是因为有130万个包可用,即使您找到一个文档记录良好且维护良好的包,它也可能依赖于数百个其他包,依赖关系树延伸到十个或更深的级别-作为一个开发人员,不可能验证所有的包。

我认为这是一个社会问题,而不仅仅是一个技术问题,并提出了一个半社会的解决方案:总登记处的一个由人维护的子集,基于共同的标准,“健康”的一揽子计划可以通过这一标准获得批准。其中一个标准将是只依赖于其他批准的一揽子计划。

我不喜欢用NPM安装软件包时的感觉。选择一个软件包,安装它,发现随它一起安装的93个附加软件包,并希望所有这些软件包也都适用于我的项目…。感觉失控了。

我感觉不开心,因为选择依赖项很难,所以我责怪NPM,这样我的问题就不是我的错了。

有没有什么办法让我衡量一下这件坏事,从而更彻底地逃脱责备呢?

我们能对NPM登记处的健康状况进行有意义的实证测量吗?我想是的;但是在我这样做之前,为了保持一丝一毫的公正性,我想列出我期望一个健康的包注册表应该是什么样子。如果事实证明,作为一个整体,NPM看起来基本上是我期望的健康注册中心的样子,我将不得不把尾巴夹在两腿之间,承担起我的挣扎。如果它看起来大不相同,我仍然有希望指责制度。

实用程序是一个简单的包,没有依赖项,它完成单个小但繁重的任务。例如:Lotash是实用程序的集合(您可以分别安装每个实用程序)。备受争议的是左手垫。

库在抽象方面更上一层楼。它可能依赖于几个实用程序,并且它可以完成一整套相关的任务。例如:urlib只有几个简单的依赖项,虽然它做了很多事情,但它尊重明确的统一原则。

框架为整个项目提供脚手架,并且可能依赖于多个库和实用程序。在您的项目中,您应该只需要其中的一个或零个。例如:反应、角度、表示。

插件通过附加的、特殊用途的功能增强了框架。对于插件,框架应该是对等依赖关系,而不是真正的依赖关系。它还可能直接依赖于一个库或几个实用程序。示例:角度组件或jQuery插件。

很明显,这个世界是乱七八糟的!我并不指望100%的包可以归入如此简单的类别,这些定义中也没有任何一个是严格和不妥协的。但总体而言,我希望许多甚至大多数包基本上符合类似的分类,大多数包是实用程序和库,最少的包是框架。

在包主要适合于该层次结构的情况下,许多包的注册表将具有以下特性:

即使是最深最广的框架也依赖于不到250个包,包括依赖项的依赖关系(3-4步深,每个包3-4个依赖项,<;=4.4 max)。大多数软件包将安装远低于30个其他软件包。这些数字非常慷慨;更小的数字会让我更快乐。

我下载了npmjs.org存储库中所有130万个包的元数据,并尝试处理一些数字。(有关我是如何做到这一点的更多技术细节,请参阅最后一节,标题为“附录:方法”)。

我将使用“依赖于此包的其他包的数量”作为穷人衡量包受欢迎程度的指标。它不是一个好的代理,但另一个常见的选择也不是:下载的数量(请参阅PyPI中的“背景”一节)。下载数量的获取在计算上要昂贵得多,所以我使用了依赖项。

在这130万个软件包中,有1700个直接依赖于它们自己,要么是完全循环的,要么是同一软件包的不同版本。我对此没有任何解释。

那么~500、~125和~25分别是3包、4包和5包周期的一部分。(这些并不总是简单的圆;它可能是任意循环排列中的三个相互依赖的包)。

我的第一反应是,这些数字看起来像是好消息;在一百万个包中,只有很小一部分奇怪的包有循环依赖关系,其余的都很好。对吗?

不幸的是,我发现几乎150,000个包-超过十分之一-在它们的依赖图中的某个地方至少有一个这样的循环依赖项。这意味着至少有几个“怪胎”实际上是主要的、被高度引用的包。一些例子:

这些不是孤立的实例;我从几十个具有循环依赖关系的高度依赖包的列表中挑选出最容易识别的实例。

我需要澄清的是,从技术上讲,这不是问题!NPM可以很好地安装这些软件包。它们可以循环依赖,不存在技术问题。它们起作用了。很明显。如果巴别塔不起作用,现在应该有人会说些什么了。

但我对此不太满意。当涉及的周期超过两个包时,我尤其不舒服,这使得它们看起来不是故意的。也许有一个很好的理由;这些人很聪明,我总是试着假设做出奇怪的选择是有重要原因的。但是…。

我不是用devDependency来衡量的--tyescript出现在超过12000个“依赖”列表中。

因此,虽然我不太关心具有大而深的devDependency树的包(我仍然担心,但不那么担心),但似乎有很大一部分包从一开始就没有利用依赖项和devDependency之间的区别。这是令人担忧的。

我将包的依赖关系树的深度定义为我能找到的最长的依赖关系链的依赖关系。特别是深层依赖树是一个问题,因为当包含单个新包时,它们使得审核将要安装的所有包变得非常困难。

在npmjs.org中的平均依赖树深度略低于4。这听起来还不错!

然而,就像经常发生的那样,平均数并不能说明全部情况。几乎一半的包完全没有依赖关系--这是一件好事!--但是所有这些零大大降低了确实有依赖关系的包的平均值。

如果我们用图表表示每个依赖关系树深度大于零的包的数量,那么基于我上面想象的理想化注册表,下面是我希望看到的:

请记住,我们希望的主要是2、3和4。但是,仍然有一长串树深超过20的包。20是…。比我预期的要大得多,我还以为会失望呢。

但是,让我们重新使用“古怪”理论:也许所有那些具有极深依赖树的包都很少使用,不值得担心。我们来查一下。

这是一个散点图,每个包的放置位置基于绘制的树深度和有多少其他包引用它。图的右侧是引用最多(~流行)的包;顶部是最深的依赖关系树。再说一遍,我的希望首先是:

即使在最受欢迎的软件包中,也有10多个树深的软件包,少数甚至达到20多个。非常深的树不仅仅是一个“古怪”的包裹问题。

直接依赖项的平均数量(在有任何依赖项的包之间)是5个,这本身看起来并不坏。不过,当它与高大的树木深度结合在一起时,感觉有点令人担忧。这是否意味着其中一些软件包总共有5-10个依赖项?(剧透:没有。)。

下面的图表显示了有多少软件包具有1个依赖项,2个依赖项…。高达30--一个漂亮的、整齐的指数衰减。

这条曲线足够清晰,如果在任何软件包注册表中看到非常相似的东西,我都不会感到惊讶--可能不是完全相同的参数,而是相似的形状。这里没有显示的是一个令人难以置信的长尾巴;有4个包与1000个最直接的依赖项捆绑在一起,并且有一些亚军非常平滑地分布到这个最大值。

知道平均深度和分支因子后,您必须想象一下,计算每个包的总依赖项(包括依赖项的依赖项)不会产生好消息。但是,大型依赖关系树的许多分支都是共享的-树中的多个包都依赖于同一个库。我测量的树木深度是每个包裹的最大深度,而不是平均深度。因此,情况并不一定像最初粗略的计算看起来那样可怕。

现在,我们确信不会看到总共有900万个依赖项的包,让我们看看实际的数字是多少。

这看起来不算太糟!大部分在200以下,这符合我想象的极限。它比我希望的在50-100的范围内稍微胖了一点,这张图表没有显示出通向最终赢家的长尾巴,总共有超过2500个依赖项,但这个图表并没有让事情看起来像他们感觉的那么糟糕。

为了验证,让我们再次按使用频率进行划分;在此图表中,更经常依赖(更受欢迎)的包位于右侧,而将安装更多总依赖项的包位于顶部。

这里的坏消息和以前一样。事实上,长尾中的许多包都相当频繁地使用。即使在最常用的包中,也有几个点代表总共超过1000个依赖项的包。

当然,NPM并不符合我所希望的“健康”品质。你可以认为这意味着我的愿望是不切实际的,或者说有些事情真的是错的。

作为读者的作业:反驳我的分析的一个简单方法是显示其他包库也有同样的问题:周期、高深度、高间接依赖、很大比例的未维护包、来自最常用包的统计尾部的包等等。

对PyPI.org或rubymems.org执行我为NPM所做的操作,看看结果是相似还是非常不同。

JavaScript很流行。它是GitHub上最流行的语言,已经有5年多了。

NPM使用与定义包相同的文件来定义项目。如果您在副项目中添加了一些依赖项,则将其发布为包基本上是免费的。将这与ruby bundler的gefile与gespec以及PyPI的requments.txt与setup.py进行对比。

NPM包是永久性的;您可以拖出一个版本,也可以弃用该包,但不能否认或删除该项目。(几个月来,请求已被正式否决-但近5万个包仍依赖于它。)。

没有标准的图书馆。您可能想要用javascript做的所有事情,您都需要自己编写,或者使用第三方软件包。

这些都不是坏事!我很高兴javascript很受欢迎;我很高兴npm提供了一种深思熟虑和简单的方式来发布软件包;我很高兴我们修复了Left-Pad如此戏剧性地暴露出来的非永久性软件包危险。

但是收集这些观察结果,再加上人类的本性,导致npmjs.org拥有的包比PyPI多5倍,其中大量的包是未记录的、维护的、未使用的或毫无意义的。即使在流行且经常维护的包中,您也会发现包含大量依赖项的包,包括存在安全问题的依赖项、不推荐使用的依赖项、循环依赖项等等。

因为这被认为是正常的,所以它将继续发生;随着它的继续发生,将有更多的库基于那些已经存在的库,从而进一步增加膨胀。

理想情况下,我应该在这篇文章的这一部分介绍我的新的自动化软件包-存储库-Better-Maker,它将解决我们所有的问题。相反,我只能提供一些有趣的东西供你想象。

想象一下定义“健康”包的一组规则。它们可能看起来有点像。

健康的包是积极维护的-无论是最近的提交,还是每年一次的“此包仍在按预期执行”消息。

一个健康的软件包有一个记录在案的备份维护员;如果维护员中了彩票,他就会接手。

一个健康的包有一种报告错误的方法,这些错误至少会定期分类。

现在,从最流行的零依赖包开始,开始收集健康包的列表。然后,您可以开始查看依赖于它们的包,向后遍历依赖图,并标记什么不仅是安全的,而且是递归的。最终,(有时间和足够的社会压力使其“健康”),您可以构建类似于标准图书馆的东西。当像Request这样的包决定弃用时,这对依赖它的任何已批准的包都有一定的意义-他们必须找到替代方案,否则自己就会失去已批准的状态。

不需要更改NPM,所有这些都可以工作!人们仍然可以发布愚蠢的软件包和个人软件包,以及他们不打算维护的软件包--但是对于专业环境中的开发人员来说,很容易认识到其中的不同之处。

这是一幅令人难以想象的美丽图画。显然,这是行不通的。最流行的javascript包,特别是大型框架,不会仅仅因为西密歇根的一些人画了一些图表并说这会很好,就推动他们戏剧性地改变自己的做法。

任何一套规则都会有赌博的风险;例如,如果你说一个健康的软件包及时响应错误报告,你就是在允许沮丧的错误用户大喊大叫,并威胁说如果他们被忽视就会报告一个项目。

我可以想象一个由javascript中受人尊敬的声音组成的委员会-知名的开发人员、流行项目的维护者、来自棱角分明和反应激烈的项目领导-为一套规则争论不休,最终就一套规则达成一致,并用他们的名字支持这一过程。

我可以想象一种折中方案,包得到类似PageRank的分数,这既取决于它们自身的质量,也取决于它们所有依赖项的分数,而不是二进制的好包/不好包。然后,团队可以说“我们只使用健康得分在65分以上的套餐”,但仍然觉得他们承担了一些责任。也许分数甚至会鼓励更多的参与--人们确实喜欢让数字上升。

读者,这一切都会让你大失所望。成千上万的文字、图表和数字,我甚至没有一个完整的解决方案!什么给予?

我不能给出一个包含电池的答案,但我希望你带着这些结论离开:

实用的解决方案不是更改包的发布方式,而是更改包的选择使用方式。

该解决方案不仅需要考虑包,还需要考虑它的所有直接和间接依赖关系。

而且,要获得批准和采用,它将需要该领域主要名称的社会影响力。

如果你有兴趣讨论这个问题,潜在的解决方案,或者责备我把一切都搞错了,请随时联系:[email protected]

在replicate.npmjs.org上没有用于批量下载整个注册表的有文档记录的端点;一段时间以来,我假设我需要发出130万个单独的请求才能获得所需的数据。但是,复制API实际上是一个原始CouchDB实例。所以(不要点击这个链接!)。https://replicate.npmjs.com/registry/_all_docs?include_docs=true是一个50 GB的JSON文件的咒语,它包含的信息足以完成这项任务。

您需要能够解析那个50 GB的文件,如果您不属于RAM丰富的1%,这意味着要使用流式解析器。我并不期望在javascript中完成这项任务,但是Oboe.js做的正是我们需要的工作-它允许您将json文件解析为流,并在使用解析的节点后将其丢弃。

我将相关数据从该json文件推入Postgres数据库。请注意,如果您尝试使用node-postrges或任何其他异步接口,使用oboe的所有工作都将化为乌有-您将创建数万个挂起的异步调用,并且仍然会耗尽内存。我使用的是PG-Native的同步API。

我的数据模式很简单:一个用于包的表,一个用于依赖项的连接表(从包到包)(我还为devDependency、peerDependency等创建和填充表,但最终没有将它们用于此分析)。

插入130万个包和410万个依赖项将需要几个小时。确保您优雅地处理错误,这样您就不会被迫重新启动进程。一些保证唯一性的索引会对您有所帮助,如果您被迫这样做的话。

要查找循环依赖项,请首先查找并标记所有依赖于其自身的包。然后,剔除这些,找出两步循环。然后,剔除这些,找出3个阶梯循环。等。

类似地,要计算依赖关系树的深度,请找到所有没有依赖关系的包,并将它们标记为深度0。然后查找依赖于标记为0的包的所有包,并将其标记为1。然后查找依赖于标记为1的包的所有包,并将其标记为2。继续操作,直到没有新的包被标记。(请确保您不会被之前检测到的循环捕获!)。

最大的挑战是计算总的间接依赖项。单个软件包可能在许多方面依赖于另一个单个软件包-作为直接依赖关系,以及作为间接依赖关系在其依赖关系树中的多个位置。如果忽略这一事实,特别是对于具有深树的包,您可能会得到类似“此包有900,000个间接依赖项”的结果。

因此,这里没有归纳验证风格的All-SQL解决方案可供选择。您必须将另一个连接表从包添加到包上,这一次跟踪间接依赖关系。它需要一个复合唯一密钥。而且,您需要迭代每个包,从Depth=1开始,然后逐步递增,填写直接依赖关系表中的间接依赖关系,以及前一个深度中的所有间接依赖关系。

像以前一样,向连接表中插入4000万个条目将需要数小时的计算,因此请尝试在第一次就做对。