npm hook scripts 不执行的问题

2018-05-29 · 14 min read

最近在做一个 node 工具,需要在 npm install 之后做一些事情,然而又不希望在每个包里加 scripts(可以想象成 eslint 和 eslint plugins,我不想在每个 plugin 里写 scripts,而是希望 eslint 在 plugins install 的时候“发现”他们,然后执行一些必要操作)。开始想各种方法,然后在 npm 文档 里看到了一个之前完全没印象的功能:"hook scripts" 。

Hook Scripts#

什么是 Hook Scripts,直接看官方的说明:

If you want to run a specific script at a specific lifecycle event for ALL packages, then you can use a hook script.

Place an executable file at node_modules/.hooks/{eventname}, and it'll get run for all packages when they are going through that point in the package lifecycle for any packages installed in that root.

Hook scripts are run exactly the same way as package.json scripts. That is, they are in a separate child process, with the env described above.

很像 git hooksnode_modules/.hook 中的可执行文件会在所有包的相应阶段执行。

正是我想要的,如获至宝,立刻尝试。然而不论怎么写,不论是 postinstall、preinstall 还是其他的 eventname,都死活不执行(甚至把 npm 切换到 5.6 也不行,看下面就知道有多悲催了)。google、baidu、bing 各种搜索,hook scripts 相关的资料少的可怜。

无奈去看 npm 源码(阅读体验挺差的),找到一个叫 npm-lifecycle 的包。

npm-lifecycle#

npm-lifecycle 是 npm 的 lifecycle script runner,独立出来没多久。这个包文档写的非常粗狂,几乎没有示例,没有 api。甚至没有注释。

找了好一阵子终于发现问题,问题代码在这里,修复的 commit 在这里

function lifecycle(pkg, stage, wd, opts) {
  return new Promise((resolve, reject) => {
    while (pkg && pkg._data) pkg = pkg._data
    if (!pkg) return reject(new Error('Invalid package data'))

    opts.log.info('lifecycle', logid(pkg, stage), pkg._id)
    if (!pkg.scripts) pkg.scripts = {}

    if (stage === 'prepublish' && opts.ignorePrepublish) {
      opts.log.info(
        'lifecycle',
        logid(pkg, stage),
        'ignored because ignore-prepublish is set to true',
        pkg._id,
      )
      delete pkg.scripts.prepublish
    }

    /**
     * pkg.scripts 没有 lifecycle 对应的阶段,直接 return
     * node_moduels/.hooks 下的脚本被直接无视了
     */
    if (!pkg.scripts[stage]) return resolve()

    // 略了,后面我也没看 ...
  })
}

简单来说,原因就是 npm-lifecycle 2.0.2 之前的版本存在 bug,只有当 scripts 中有对应的 lifecycle hook 的时候,才会去执行 .node_modules/.hooks 下的 hook。我本地使用的 node 10.1.0, npm 5.6.0, 而 npm-lifecycle 的版本刚好是 2.0.0

解决办法#

我简单翻了下 npm 各个版本的依赖,发现:

  • npm@5.4.0 -> npm-lifecycle@~1.0.2
  • npm@6.1.0 -> npm-lifecycle@^2.0.3

所以使用 npm 5.4.0 ~ 6.1.0 之间的版本,都可能有问题(具体取决于安装时 npm-lifecycle 的最新版本号和 npm semver version 的范围)

解决办法:

  • npm v5.6 以上的,重装 npm,哪怕还是装原来的版本,依赖也会升级上去
  • npm v5.4 ~ v5.6 的,升级 npm

最后#

最后附上我找到的一篇关于 npm hook scripts 实际应用的文章。作者为了解决某些包从墙外下载执行文件时遇到的网络问题,在项目里添加了 preinstall hook,下载前把地址替换到 cnpm。思路非常棒!

本文为原创文章,2018-05-29 首发于网易 KM 平台