记一次npm run问题排查的详细过程

1. 前言

今天遇到一个特别难整的问题, 在执行npm run a_task 出错, 报错信息特别简单, 对于查找问题没有任何帮助. 尝试过几种方法来定位问题, 收效很差, 最后不得不祭出npm的大杀器npm debug来一探究竟.

首先我要说说我遇到的问题和我尝试了哪集中解决问题的方法, 也是我惯常解决问题的思路.

问题是这样的, 我在执行npm run buildDev时报如下错误, buildDev是一个自定义的npm任务. 其内容就是编译angular应用, 并进行prerendering一些页面. 具体内容如下ng build --configuration development && ng run xxx_project:prerender --configuration development

1
2
3
4
5
6
7
Prerendering routes to path_to_dist\xxx\browser complete.
Service worker generation failed.
EISDIR: illegal operation on a directory, read
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! xxx_project@1.0.0 buildDev: `ng build --configuration development && ng run xxx_project:prerender --configuration development`
npm ERR! Exit status 1

其报错特别简单EISDIR: illegal operation on a directory, read 对某个目录操作不合法.

为什么说特别简单, 特别没有帮助, 首先它不告诉你是哪个目录, 如何非法, 在哪一步非法. 这就是我遇到的问题.

接着聊聊我做了哪些方面的尝试

1.1. 尝试用搜索引擎了解问题和解决方案

首先当然是根据报错信息一顿搜索, 百度, bing, 某歌. 搜索出来的答案五花八门, 有说是版本不兼容问题的, 有所是.npmrc配置问题的, 等等等. 而且我都做了尝试, 没有任何效果. 但是在搜索的过程中, 我清楚了一点, 如果我关闭应用的PWA功能就不会出现类似的错误. 所以问题应该是angular的prerendering 和 pwa模块可能存在兼容性问题, 也就是在生成ngsw-worker.js时候出错了.

1.2. 查询官方文档

由于通过搜索以及探索, 发现了是prerendering 和 pwa兼容性问题, 我就去官网了解到底preredering和pwa一起运行有哪些注意事项. 官网上有独立介绍prerendering和PWA的文章, 但是两种结合使用方面的文章几乎没有. 也就是这两个模块应该是独立运行的, 应该不存在很强的关联性. 可能只是在prerendering的时候也需要执行service worker的过程, 碰巧遇到了service worker generation的bug. 于是我尝试其它过程例如ssr, build, serve等task的 service worker generation. 而这些任务的service worker generation都是正常的, 都能看到”Service worker generation completed” 打印信息. 参照官方文档, 我再次检查了prerendering的配置和pwa的配置, 确保没有漏到重要细节, 并对一些依赖包的版本进行了调整避免兼容性问题. 至此我仍然没有找打问题的具体原因, 但是对问题是如何发生的有了一些了解, 排除了各种配置问题, 将问题定性为bug.

1.3. 查看源码

在有了一些对异常的认识之后, 在搜索与官网仍然找不到原因的情况下, 我只能开始着手研究源代码. 这里多啰嗦两句. 走到这一步的时候, 我脑海里面在想什么?

  1. 幸好我采用的是开源技术, 如果是闭源技术, 那么此时只能找客服了, 需要经过一个漫长的等待过程, 而且要将问题描述清楚需要很强的文字功底, 如果是国外的闭源产品, 可能还需要很强的英文基础. 而开源的产品我们可以着手研究源码, debug, 顺手一个PR就将问题永久解决了, 或者去github上去提issue. 这就是开源产品的生命力如此旺盛的原因.

  2. 在设计系统时, 错误处理部分一定要注意. 像这种错误太泛化, illegal operation on a directory 如果在”案发现场”(错误发生的地方),能将错误信息详细的保存非常的重要, 比如将a directory替换成一个具体的目录, 将不能读取的原因告知用户, 而不是一个简单的read, 让用户去猜测去. 说实在的我现在正在写的这边博文, 以及整个探索过程其实都是在浪费生命. 写博文也是为了让更少的人去继续浪费生命, 俗话说我不入地狱谁入地狱, 我入了地狱是为了让更少的人入地域, 而且一定要将地狱咒骂一番. 面对一个毫无帮助的错误信息, 感觉就是在做一次侦探的过程, 看起来很高大上实际上一点意义都没有, 它可能是因为开发人员的一次偷懒, 测试人员的不重视造成的. 尤其是底层依赖库, 如果吞掉了错误详细信息, 破坏了案发现场. 而二次开发者, 调用者很难去enhance错误消息. 幸运的是它还打印出了那么一条错误信息, 如果遇到哪种自以为是直接将错误信息吞没的库, 我想我现在心中有一万投草泥马飘过.

闲话少叙, 首先根据错误信息定位问题, 我首先根据Service worker generation failed 定位到是在@nguniversal/buiders/src/prerender下面

1
2
3
4
5
6
7
8
9
10
11
12
if (browserOptions.serviceWorker) {
spinner.start('Generating service worker...');
try {
await (0, service_worker_1.augmentAppWithServiceWorker)(projectRoot, context.workspaceRoot, (0, core_1.normalize)(outputPath), browserOptions.baseHref || '/', browserOptions.ngswConfigPath);
}
catch (error) {
spinner.fail('Service worker generation failed.');
return { success: false, error: error.message };
}
spinner.succeed('Service worker generation complete.');
}

通过阅读源码, 发现这个prerender整合了pwa, 这也验证了在为什么在angular.json中将serviceWorker设置为false就不报错的原因.

接下来, 我在catch语句中打印了error的详细信息, 得到了如下错误信息

1
2
3
4
5
6
7
8
9

[AsyncFunction: augmentAppWithServiceWorker]
in nguniveral builder prerender
[Error: EISDIR: illegal operation on a directory, read] {
errno: -4068,
code: 'EISDIR',
syscall: 'read'
}

依然没有获得更多信息, 只是通过errno 4068了解到使用某种第三方依赖包时会出现类似的状况. 所以可以肯定的是第三方库吞掉了错误的详细信息.

我再次通过illegal operation on a directory, read 错误信息搜索源码, 由于相关代码经过层层封装, 逻辑十分的绕, 所以我打算使用npm的debug功能深入到底层代码, 去一探究竟.

1.4. npm debug

1.4.1. 配置npm debug

打开vscode左侧 run and debug视图, 点击左上方 run and debug下拉列表. 选中 add config (your project),
或者用文本编辑器在项目的.vscode/launch.json文件中添加如下内容.

1
2
3
4
5
6
7
8
9
10
11
{
"version": "0.2.0",
"configurations": [
{
"name": "My Backend",
"command": "npm run buildDev",
"request": "launch",
"type": "node-terminal"
}
]
}

注意: 将command替换为合适的command
commands 可以在 package.json 的 “scripts” 节点找到

设置断点, 点击开始debug按钮(绿色三角图标)

1.4.2. 问题排查

经过debug发现是@nguniversal 和@angular-devkit/build-angular这个包的兼容性问题

@nguniversal调用augmentAppWithServiceWorker传入的是五个参数, 而@angular-devkit/build-angular接受的是4个参数, 而且参数位置发生了错位.

1
2
3

await (0, service_worker_1.augmentAppWithServiceWorker)(projectRoot, context.workspaceRoot, (0, core_1.normalize)(outputPath), browserOptions.baseHref || '/', browserOptions.ngswConfigPath);

node_modules@angular-devkit\build-angular\src\utils\service-worker.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

async function augmentAppWithServiceWorker(appRoot, outputPath, baseHref, ngswConfigPath) {
const distPath = (0, core_1.getSystemPath)((0, core_1.normalize)(outputPath));
// Determine the configuration file path
const configPath = ngswConfigPath
? (0, core_1.getSystemPath)((0, core_1.normalize)(ngswConfigPath))
: path.join((0, core_1.getSystemPath)(appRoot), 'ngsw-config.json');
// Read the configuration file
let config;
try {
const configurationData = await fs_1.promises.readFile(configPath, 'utf-8');
config = JSON.parse(configurationData);
}
catch (error) {
if (error.code === 'ENOENT') {
throw new Error('Error: Expected to find an ngsw-config.json configuration file' +
` in the ${(0, core_1.getSystemPath)(appRoot)} folder. Either provide one or` +
' disable Service Worker in the angular.json configuration file.');
}
else {
throw error;
}
}

而错误EISDIR: illegal operation on a directory, read 是从fs_1.promises.readFile(configPath, 'utf-8')位置抛出来的.
而fs_1实际就是node的一个核心包, 抛出了一个非常简单的错误, 而angular universal得出来非常偷懒, 直接使用了一个throw error,

1
const fs_1 = require("fs");

而跟踪该文件得版本变化, 该函数被反复的修改, 接口参数反复变化, 参数位置任意调整, 丝毫没有考虑兼容性问题. 所以在像google这样的大公司里面, 也有很多素质很差的程序员.

经过比较发现在universal版本v13.0.0和v13.1.1之间augmentAppWithServiceWorker的参数发生了变化详细参考universal-compare 中文件modules/builders/src/prerender/index.ts的差异部分.

参考了universal的package.json文件, v13.1.1依赖的是”@angular-devkit/build-angular”: “13.3.4”, 而我当前使用的是"@angular-devkit/build-angular": "13.3.1", 由于依赖的Angular包较多, 不敢贸然升级Angular, 所以这里将universal降级为合适的版本13.1.0.

重新执行npm run buildDev 问题解决.

2. Nodejs 系列文章

最新更新以及更多Nodejs相关文章请访问 鹏叔的技术博客 - Nodejs

3. 参考文章

Can I add a debug script to NPM?

VS Code - debug with npm run