如何在npm install安装依赖包及下级依赖包时使^x.y.z不要向上匹配最大版本号

简书福利¥2000 阿里云红包http://wz4.in/1k2nC

虽然多数npm依赖包开发者能严格遵守SemVer版本规范,但有时在实际项目中为了可靠,我们不希望未经审核评估就轻易升级一个项目依赖包,以免带来意外的风险,尤其是依赖包下更多第三方依赖包往往也指定是“^x.y.z”格式的版本。我们可以通过修改npm源码来禁止其在安装依赖包过程中匹配最新版本。

标签: npm SemVer 不要自动升级 ^版本符号

-2017.9.12-

(原创文章,未经本人许可不得以任何形式转载 。bytethinker(at)163.com)


package.json

一、背景

基于npm管理的项目根目录下通常会有一个package.json文件,简单如下:


{

  "name": "ism-client",

  "version": "0.2.18",

  "private": true,

  "author": "ism",

  "license": "ISC",

  "dependencies": {

    "global-modules": "^0.2.1"

  }

}

里面包含的dependencies、devDependencies等节点用于定义项目需要的依赖包及版本。而这些下级依赖包也会有自己的package.json,定义了自己需要的依赖包及版本,如此可以一级级地依赖下去。依赖包可以是自己开发的,也可以是第三方开发的,如npm的官方仓库中就提供了大量的第三方依赖包。官方仓库中的每个依赖包一般会有一个依赖包信息文件,里面包含了该依赖包的最新版本可用版本以及包下载地址。 通过类似https://registry.npmjs.org/global-modules的URL可以访问到:


{

    ...

    "name": "global-modules",

    ...

    "dist-tags":

    {

        "latest": "1.0.0"

    },

    ...

    "versions":

    {

        "0.1.0":

        {

            ...

            "dist":

            {

                "shasum": "9e8011fdede4f811047f76b155495e7786d27e02",

                "tarball": "https://registry.npmjs.org/global-modules/-/global-modules-0.1.0.tgz"

            },

            ...

        },

        "0.2.0":

        {

            ...

            "dist":

            {

                "shasum": "efb4f2d0c9a6856c106f1bba3bc0568e59c197b1",

                "tarball": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.0.tgz"

            },

            ...

        },

        "0.2.1":

        {

            ...

            "dist":

            {

                "shasum": "c2720c3b36a4565d21b08a5322f8edd46a9e4d38",

                "tarball": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.1.tgz"

            },

            ...

        },

        "0.2.2":

        {

            ...

            "dist":

            {

                "shasum": "c7e589646bf8bec457d71049553adc72e36c6346",

                "tarball": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.2.tgz"

            },

            ...

        },

        "0.2.3":

        {

            ...

            "dist":

            {

                "shasum": "ea5a3bed42c6d6ce995a4f8a1269b5dae223828d",

                "tarball": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz"

            },

            ...

        },

        "1.0.0":

        {

            ...

            "dist":

            {

                "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",

                "shasum": "6d770f0eb523ac78164d72b5e71a8877265cc3ea",

                "tarball": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz"

            },

            ...

        }

    },

    ...

}

在项目根目录下执行npm install命令即可安装项目依赖包及其更多下级依赖包。安装过程中,npm会根据SemVer版本规范,自动从各依赖库的版本清单地址中匹配尽量新的版本,然后下载并安装到node_modules子目录。如上面package.json中的“global-modules”依赖包的版本为“^0.2.1” ,根据SemVer版本规范最终会在依赖信息文件的中匹配到最新的“0.2.3”版本。

虽然多数依赖包开发者能严格遵守SemVer版本规范,但有时在实际项目中为了可靠,我们不希望未经审核评估就轻易升级一个项目依赖包,以免带来意外的风险,尤其是依赖包下更多第三方依赖包往往也指定是“^x.y.z”格式的版本。著名的left-pad事件就因为一个第三方依赖包的撤回,导致依赖它的React、Babel、Ember依赖包无法安装,继而影响大量使用这些包的项目。这两天我也因为项目中随意指定angular版本为"angular":"^1.5.11",npm install命令实际安装后的版本是"angular":"1.6.6",但这个版本中将导航书签地址的默认前缀从空字符串""变成了"!",导致项目中原有的导航路径无法解析页面无法显示,调试控制台也没有任何错误信息,耗费了大量精力才定位出问题。

虽然可以通过npm的package-lock.js、shrinkwrap.json,或者通过yarn.lock等锁定文件实现版本号锁定,但它们只是在一个包被首次安装时把当时实际匹配到的版本号写入到锁定文件中。也就是说,如果一个子依赖包的版本号是“^0.2.1”,往往它被写入到锁定文件中的不是“0.2.1”,而是当时匹配的最新版本如“0.2.3”。想想实际发布包的过程中,这个“^0.2.1”是怎么来的呢?它实际时作者在开发时通过npm install xxx命令安装时自动生成的,然后集成调用这个版本的子依赖包进行大理的开发、测试,然后发布了自己的包。所以这个子依赖包的“0.2.1”版本是作者在发布自己的包过程中实际使用的版本,对作者这次发布的包来说,这个子依赖包的版本是稳定可靠的,我们应该优先使用,我们可以修改npm,禁止其根据SemVer规则自动升级该版本。

二、解决方法

修改npm源码,禁止npm根据SemVer规则自动升级依赖包及其下级依赖包的版本。

1. 升级npm到5.0以上

不升级也可以,但后面第4、第5步对应修改点的位置可能不一样,需要自行分析对应的修改点。另外,npm 5.4.1版本容易出现权限不足无法创建文件链接、无法删除目录之类的错误,最好不要使用。


D:\project\mynpm>npm install npm@5.3.0 -g

2. 测试1(npm会自动升级依赖包版本号)

(1) 新建目录d:\project\mynpm

(2) 添加文件d:\project\mynpm\package.json


{

  "name": "ism-client",

  "version": "0.2.18",

  "private": true,

  "scripts": {

    "start": "pm2 startOrGracefulReload ./pm2.json --no-daemon",

    "production": "pm2 startOrGracefulReload ./pm2.json --no-daemon",

    "debug": "npm build && node dist/app.js",

    "build": "gulp build",

    "test": "gulp build --test"

  },

  "author": "ism",

  "license": "ISC",

  "dependencies": {

    "global-modules": "^0.2.1"

  }

}

(3) 添加d:\project\mynpm\npm-test.bat,用于清除安装结果并再次安装。也可以不使用批处理文件,直接使用npm install命令,加--verbose是为了看到更详细的安装过程。


rd /s/q node_modules

del /s/f/q package-lock.json

cls

call npm install --verbose



(4) cmd中执行批处理文件安装依赖包


D:\project\mynpm>npm-test.bat

安装过程略...

(5) 通过package-lock.json或者node_modules目录下各依赖包的package.json,观察定义的版本和实际安装的版本。此时(2017-9-11)实际安装了8个依赖包,以下例举部分。可以发现所有^x.y.z格式定义的依赖包和子依赖包版本都被npm自动升级了:

项目本身:

    package.json定义的依赖:

    "global-modules": "^0.2.1" //实际安装版本0.2.3(被升级了)



node_modules/global-modules (0.2.3):

    package.json定义的依赖:

    "global-prefix": "^0.1.1", //实际安装版本0.1.5(被升级了)

    "is-windows": "^0.1.1" //实际安装版本0.2.0(被升级了)



node_modules/global-prefix (0.1.5):

    package.json定义的依赖:

    "homedir-polyfill": "1.0.1", //实际安装版本1.0.1

    "ini": "1.3.4", //实际安装版本1.3.4

    "is-windows": "0.2.0", //实际安装版本0.2.0

    "which": "1.3.0" //实际安装版本1.3.0



node_modules/is-windows (0.2.0):

    package.json定义的依赖:

    无依赖



其它5个依赖包...

3. 找到npm的安装目录

其中prefix后面的C:\Users\administrator\AppData\Roaming\npm就是你npm的安装位置,打开该文件夹后就可以看到npm安装后的源码了。


D:\project\mynpm>npm config list

; cli configs

metrics-registry = "https://registry.npmjs.org/"

scope = ""

user-agent = "npm/5.3.0 node/v7.8.0 win32 x64"



; builtin config undefined

prefix = "C:\\Users\\administrator\\AppData\\Roaming\\npm"



; node bin location = C:\Program Files\nodejs\node.exe

; cwd = D:\project\mynpm

; HOME = C:\Users\administrator

; "npm config ls -l" to show all defaults.

4. 修改npm安装目录中的源代码

说是修改npm的源码,其实升级版本的逻辑在npm下下级依赖包npm-pick-manifest中也有,需要一并修改。npm整个代码量非常大,依赖非常多,要定位到这些逻辑真是困难!如果你的开发环境必须使用较早前的npm版本,可以把这里的修改作为参考,自行定位修改点。

(1) 修改npm-pick-manifest包的index.js文件

文件路径:

C:\Users\administrator\AppData\Roaming\npm\node_modules\npm\node_modules\pacote\node_modules\npm-pick-manifest\index.js


function pickManifest (packument, wanted, opts) {

  ...



  /* --禁止自动升级版本

  if (

    !target &&

    tagVersion &&

    packument.versions[tagVersion] &&

    semver.satisfies(tagVersion, wanted, true)

  ) {

    target = tagVersion

  }



  if (!target) {

    target = semver.maxSatisfying(versions, wanted, true)

  }

  */

  // ++禁止自动升级版本

  if (!target) {

    target = semver.minSatisfying(versions, wanted, true)

  }



  ...

}

(2) 修改npm包的pick-manifest-from-registry-metadata.js文件

文件路径:

C:\Users\administrator\AppData\Roaming\npm\node_modules\npm\lib\utils\pick-manifest-from-registry-metadata.js


function pickManifestFromRegistryMetadata (spec, tag, versions, metadata) {

  ...



  /* --禁止自动升级版本

  // if the tagged version satisfies, then use that.

  var tagged = metadata['dist-tags'][tag]

  if (tagged &&

      metadata.versions[tagged] &&

      semver.satisfies(tagged, spec, true)) {

    return {resolvedTo: tag, manifest: metadata.versions[tagged]}

  }

  // find the max satisfying version.

  var ms = semver.maxSatisfying(versions, spec, true)

  */

  // ++禁止自动升级版本

  var ms = semver.minSatisfying(versions, spec, true)



  if (ms) {

    return {resolvedTo: ms, manifest: metadata.versions[ms]}

  } else if (spec === '*' && versions.length && tagged && metadata.versions[tagged]) {

    return {resolvedTo: tag, manifest: metadata.versions[tagged]}

  } else {

    return

  }

}

6. 测试2(禁止npm自动升级依赖包版本号后)

(1) 复制d:\project\mynpm中的package.jsonnpm-test.bat两个文件到d:\project\mynpm2目录

(1) cmd中跳转到mynpm2执行批处理文件安装依赖包


D:\project\mynpm>cd ../mynpm2

D:\project\mynpm2>npm-test.bat

安装过程略...

(2) 通过package-lock.json或者node_modules目录下各依赖包的package.json,观察定义的版本和实际安装的版本。现在,所有依赖包以及依赖包下的依赖的版本都没有被自动升级了,相当于忽略了SemVer版本规范的^标识符号,直接安装的是各个包作者发布版本时使用的依赖包版本。总共安装了3个依赖包(早期版本依赖较少):

项目本身:

    package.json定义的依赖:

    "global-modules": "^0.2.1" //实际安装版本0.2.1



node_modules/global-modules (0.2.1):

    package.json定义的依赖:

    "global-prefix": "^0.1.1", //实际安装版本0.1.1

    "is-windows": "^0.1.1" //实际安装版本0.1.1



node_modules/global-prefix (0.1.1):

    package.json定义的依赖:

    "is-windows": "0.1.1", //实际安装版本0.1.1



node_modules/is-windows (0.1.1):

    package.json定义的依赖:

    无依赖

<完>
简书福利¥2000 阿里云红包http://wz4.in/1k2nC

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335