文章目录

  • 失败案例
  • npm
  • yarn
  • 成功案例
  • cnpm
  • 更奇怪的事
  • 原因分析
  • npm/yarn尝试多次却失败,cnpm第一次尝试就安装成功
  • 当 cnpm 安装成功一次后,再次使用 npm/yarn 安装都会成功。
  • install.js 脚本内容


昨晚在本地安装 Electron 一直失败,想着早上使用下载服务的人应该少点。

失败案例

安装之前有设置 npm 和 yarn 的镜像为淘宝

npm  config set registry https://registry.npm.taobao.org/
yarn config set registry https://registry.npm.taobao.org/

npm

等待时间较长,有错误日志(可惜了,我没截图,没日志内容)。

yarn

sudo yarn add electron 卡在这一步: Building fresh packages...

yarn install 用的是本地缓存还是 yarn add和npm install_cnpm

成功案例

cnpm

命令列表

# 全局安装 cnpm
sudo npm i -g cnpm
# 查看 cnpm 的安装路径
where cnpm
# 查看镜像(未修改)
cnpm config get registry
# 使用 cnpm 安装 electron, 失败,对一些目录的访问受限。
cnpm i electron
# sudo 执行,安装成功。。but why?
sudo cnpm i electron
# 查看 package.json
more package.json

yarn install 用的是本地缓存还是 yarn add和npm install_yarn_02

wuyujin1997@mac11 electron-demo % sudo npm i -g cnpm
npm WARN deprecated har-validator@5.1.5: this library is no longer supported
npm WARN deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
npm WARN deprecated request@2.88.2: request has been deprecated, see https:///request/request/issues/3142

added 372 packages in 8s

3 packages are looking for funding
  run `npm fund` for details
wuyujin1997@mac11 electron-demo % where cnpm
/usr/local/bin/cnpm
wuyujin1997@mac11 electron-demo % cnpm config get registry
https://registry.npmmirror.com/
wuyujin1997@mac11 electron-demo % cnpm i electron
✖ Install fail! Error: EACCES: permission denied, unlink '/Users/wuyujin1997/Desktop/Coderepo/quickstart/electron-demo/node_modules/.bin/electron'
Error: EACCES: permission denied, unlink '/Users/wuyujin1997/Desktop/Coderepo/quickstart/electron-demo/node_modules/.bin/electron'
npminstall version: 5.8.1
npminstall args: /usr/local/bin/node /usr/local/lib/node_modules/cnpm/node_modules/npminstall/bin/install.js --fix-bug-versions --china --userconfig=/Users/wuyujin1997/.cnpmrc --disturl=https://npmmirror.com/mirrors/node --registry=https://registry.npmmirror.com electron
wuyujin1997@mac11 electron-demo % sudo cnpm i electron
✔ Installed 1 packages
✔ Linked 1 latest versions
✔ Run 0 scripts
✔ All packages installed (used 21ms(network 20ms), speed 0B/s, json 0(0B), tarball 0B, manifests cache hit 1, etag hit 0 / miss 0)
wuyujin1997@mac11 electron-demo % 
wuyujin1997@mac11 electron-demo % 
wuyujin1997@mac11 electron-demo % 
wuyujin1997@mac11 electron-demo % 
wuyujin1997@mac11 electron-demo % more package.json 
{
  "name": "electron-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cross-env": "^7.0.3",
    "electron": "^18.1.0"
  }
}
wuyujin1997@mac11 electron-demo %

更奇怪的事

当我使用cnpm i electron安装成功一次之后,

又去新建了项目,使用npm i electron,也安装成功了(而且速度很快):

yarn install 用的是本地缓存还是 yarn add和npm install_node_03


再去新建项目,sudo yarn install electron,也是一次成功,而且很快:

yarn install 用的是本地缓存还是 yarn add和npm install_node_04

……

yarn install 用的是本地缓存还是 yarn add和npm install_electron_05

wuyujin1997@mac11 start-electron % npm config get registry
https://registry.npm.taobao.org/
wuyujin1997@mac11 start-electron % yarn config get registry
https://registry.npm.taobao.org/
wuyujin1997@mac11 start-electron % yarn add electron
yarn add v1.22.18
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
error Could not write file "/opt/tmpcode/start-electron/yarn-error.log": "EACCES: permission denied, open '/opt/tmpcode/start-electron/yarn-error.log'"
error An unexpected error occurred: "EACCES: permission denied, unlink '/opt/tmpcode/start-electron/node_modules/@sindresorhus/is/license'".
info Visit https://yarnpkg.com/en/docs/cli/add for documentation about this command.
wuyujin1997@mac11 start-electron % sudo yarn add electron
Password:
yarn add v1.22.18
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 82 new dependencies.
info Direct dependencies
└─ electron@18.1.0
info All dependencies
├─ @electron/get@1.14.1
├─ @sindresorhus/is@0.14.0
├─ @szmarczak/http-timer@1.1.2
├─ @types/node@16.11.31
├─ buffer-crc32@0.2.13
├─ buffer-from@1.1.2
├─ cacheable-request@6.1.0
├─ clone-response@1.0.2
├─ concat-stream@1.6.2
├─ config-chain@1.1.13
├─ core-util-is@1.0.3
├─ debug@4.3.4
├─ decompress-response@3.3.0
├─ defer-to-connect@1.1.3
├─ define-properties@1.1.4
├─ detect-node@2.1.0
├─ duplexer3@0.1.4
├─ electron@18.1.0
├─ encodeurl@1.0.2
├─ end-of-stream@1.4.4
├─ env-paths@2.2.1
├─ es6-error@4.1.1
├─ escape-string-regexp@4.0.0
├─ extract-zip@1.7.0
├─ fd-slicer@1.1.0
├─ fs-extra@8.1.0
├─ get-intrinsic@1.1.1
├─ get-stream@4.1.0
├─ global-agent@3.0.0
├─ global-tunnel-ng@2.7.1
├─ globalthis@1.0.2
├─ got@9.6.0
├─ graceful-fs@4.2.10
├─ has-property-descriptors@1.0.0
├─ has-symbols@1.0.3
├─ has@1.0.3
├─ http-cache-semantics@4.1.0
├─ inherits@2.0.4
├─ ini@1.3.8
├─ isarray@1.0.0
├─ json-buffer@3.0.0
├─ json-stringify-safe@5.0.1
├─ jsonfile@4.0.0
├─ keyv@3.1.0
├─ lodash@4.17.21
├─ lowercase-keys@1.0.1
├─ lru-cache@6.0.0
├─ matcher@3.0.0
├─ minimist@1.2.6
├─ mkdirp@0.5.6
├─ ms@2.1.2
├─ normalize-url@4.5.1
├─ npm-conf@1.1.3
├─ object-keys@1.1.1
├─ once@1.4.0
├─ p-cancelable@1.1.0
├─ pend@1.2.0
├─ pify@3.0.0
├─ prepend-http@2.0.0
├─ process-nextick-args@2.0.1
├─ progress@2.0.3
├─ proto-list@1.2.4
├─ readable-stream@2.3.7
├─ responselike@1.0.2
├─ roarr@2.15.4
├─ safe-buffer@5.1.2
├─ semver-compare@1.0.0
├─ semver@6.3.0
├─ serialize-error@7.0.1
├─ sprintf-js@1.1.2
├─ string_decoder@1.1.1
├─ sumchecker@3.0.1
├─ to-readable-stream@1.0.0
├─ tunnel@0.0.6
├─ type-fest@0.13.1
├─ typedarray@0.0.6
├─ universalify@0.1.2
├─ url-parse-lax@3.0.0
├─ util-deprecate@1.0.2
├─ wrappy@1.0.2
├─ yallist@4.0.0
└─ yauzl@2.10.0
✨  Done in 2.79s.
wuyujin1997@mac11 start-electron % pwd
/opt/tmpcode/start-electron
wuyujin1997@mac11 start-electron %

原因分析

npm/yarn尝试多次却失败,cnpm第一次尝试就安装成功

yarn install 用的是本地缓存还是 yarn add和npm install_npm_06

/usr/local/bin/node
/usr/local/lib/node_modules/cnpm/node_modules/npminstall/bin/install.js
--fix-bug-versions
--china
--userconfig=/Users/wuyujin1997/.cnpmrc
--disturl=https://npmmirror.com/mirrors/node
--registry=https://registry.npmmirror.com
electron

用 node 执行了脚本 install.js ,5个参数,最后指明要安装的包是 electron 。

所以,npm/yarn 安装失败, cnpm 第一次就安装成功的原因,必定就在这个 install.js 和这5个参数中(尤其是 registry 参数)。
(操作时间也算一个,昨晚下载失败,今天早晨下载成功,请求服务器资源的人数不同)。
install.js 脚本内容会贴在文末。

当 cnpm 安装成功一次后,再次使用 npm/yarn 安装都会成功。

node 全局包管理的地方,应该有一些缓存信息。
比如之前我这台机器下载过 electron ,从哪个URL下载的,应该会有记录。
之后再次下载,要么从记录中成功过的URL下载,
要么从本地已经下载过的文件里拷贝。

install.js 脚本内容

快500行代码:

#!/usr/bin/env node

'use strict';

const debug = require('debug')('npminstall:bin:install');
const npa = require('../lib/npa');
const chalk = require('chalk');
const path = require('path');
const util = require('util');
const execSync = require('child_process').execSync;
const fs = require('mz/fs');
const parseArgs = require('minimist');
const utils = require('../lib/utils');
const globalConfig = require('../lib/config');
const installLocal = require('..').installLocal;
const installGlobal = require('..').installGlobal;
const { parsePackageName } = require('../lib/alias');
const {
  LOCAL_TYPES,
  REMOTE_TYPES,
  ALIAS_TYPES,
} = require('../lib/npa_types');
const Context = require('../lib/context');

const originalArgv = process.argv.slice(2);

// since minimist consider --no-xx is xx:false, we handle it manually here
const argv = { 'no-save': originalArgv.includes('--no-save') };
Object.assign(argv, parseArgs(originalArgv, {
  string: [
    'root',
    'registry',
    'prefix',
    'forbidden-licenses',
    'custom-china-mirror-url',
    // {"http://":"http://"}
    'tarball-url-mapping',
    'proxy',
    // --high-speed-store=filepath
    'high-speed-store',
    'dependencies-tree',
  ],
  boolean: [
    'version',
    'help',
    'production',
    'client',
    'global',
    'save',
    'save-dev',
    'save-optional',
    'save-client',
    'save-build',
    'save-isomorphic',
    // Saved dependencies will be configured with an exact version rather than using npm's default semver range operator.
    'save-exact',
    'china',
    'ignore-scripts',
    // install ignore optionalDependencies
    'optional',
    'detail',
    'trace',
    'engine-strict',
    'flatten',
    'registry-only',
    'cache-strict',
    'fix-bug-versions',
    'prune',
    // disable dedupe mode https://docs.npmjs.com/cli/dedupe, back to npm@2 mode
    // please don't use on frontend project
    'disable-dedupe',
    'save-dependencies-tree',
    'force-link-latest',
  ],
  default: {
    optional: true,
  },
  alias: {
    // npm install [-S|--save|-D|--save-dev|-O|--save-optional] [-E|--save-exact] [-d|--detail]
    S: 'save',
    D: 'save-dev',
    O: 'save-optional',
    E: 'save-exact',
    v: 'version',
    h: 'help',
    g: 'global',
    c: 'china',
    r: 'registry',
    d: 'detail',
  },
})
);

if (argv.version) {
  console.log(`npminstall v${require('../package.json').version}`);
  process.exit(0);
}

if (argv.help) {
  console.log(`
Usage:

  npminstall
  npminstall <pkg>
  npminstall <pkg>@<tag>
  npminstall <pkg>@<version>
  npminstall <pkg>@<version range>
  npminstall <folder>
  npminstall <tarball file>
  npminstall <tarball url>
  npminstall <git:// url>
  npminstall <github username>/<github project>
  npminstall --proxy=http://localhost:8080

Can specify one or more: npminstall ./foo.tgz bar@stable /some/folder
If no argument is supplied, installs dependencies from ./package.json.

Options:

  --production: won't install devDependencies
  --client: install clientDependencies and buildDependencies
  --save, --save-dev, --save-optional, --save-exact, --save-client, --save-build, --save-isomorphic: save installed dependencies into package.json
  --no-save: Prevents saving to dependencies
  -g, --global: install devDependencies to global directory which specified in '$npm config get prefix'
  -r, --registry: specify custom registry
  -c, --china: specify in china, will automatically using chinese npm registry and other binary's mirrors
  -d, --detail: show detail log of installation
  --trace: show memory and cpu usages traces of installation
  --ignore-scripts: ignore all preinstall / install and postinstall scripts during the installation
  --no-optional: ignore all optionalDependencies during the installation
  --forbidden-licenses: forbit install packages which used these licenses
  --engine-strict: refuse to install (or even consider installing) any package that claims to not be compatible with the current Node.js version.
  --flatten: flatten dependencies by matching ancestors' dependencies
  --registry-only: make sure all packages install from registry. Any package is installed from remote(e.g.: git, remote url) cause install fail.
  --cache-strict: use disk cache even on production env.
  --fix-bug-versions: auto fix bug version of package.
  --prune: prune unnecessary files from ./node_modules, such as markdown, typescript source files, and so on.
  --high-speed-store: specify high speed store script to cache tgz files, and so on. Should export '* getStream(url)' function.
  --dependencies-tree: install with dependencies tree to restore the last install.
  --force-link-latest: force link latest version package to module root path.
`
  );
  process.exit(0);
}

const pkgs = [];

if (process.env.NPMINSTALL_BY_UPDATE) {
  // ignore all package names on update
  argv._ = [];
}

const context = new Context();
for (const name of argv._) {

  context.nested.update([ name ]);
  const [
    aliasPackageName,
  ] = parsePackageName(name, context.nested);
  const p = npa(name, { where: argv.root, nested: context.nested });
  pkgs.push({
    name: ,
    // `mozilla/nunjucks#0f8b21b8df7e8e852b2e1889388653b7075f0d09` should be rawSpec
    version: p.fetchSpec || p.rawSpec,
    type: p.type,
    alias: aliasPackageName,
  });
}

let root = argv.root || process.cwd();
if (Array.isArray(root)) {
  // use last one, e.g.: $ npminstall --root=abc --root=def
  root = root[root.length - 1];
}
const production = argv.production || process.env.NODE_ENV === 'production';
let cacheDir = argv.cache === false ? '' : null;
if (production) {
  cacheDir = '';
}
// support npm_config_cache to change default cache dir
if (cacheDir === null && process.env.npm_config_cache) {
  cacheDir = process.env.npm_config_cache;
}

let forbiddenLicenses = argv['forbidden-licenses'];
forbiddenLicenses = forbiddenLicenses ? forbiddenLicenses.split(',') : null;

const flatten = argv.flatten;
const prune = argv.prune;

// if in china, will automatic using chines registry and mirros.
const inChina = argv.china || !!process.env.npm_china;
// if exists, override default china mirror url
const customChinaMirrorUrl = argv['custom-china-mirror-url'];

// example: npminstall --registry xx --registry xxxx
let registry = (Array.isArray(argv.registry) ? argv.registry[0] : argv.registry) || process.env.npm_registry;
if (inChina) {
  registry = registry || globalConfig.chineseRegistry;
}
// for env.npm_config_registry
registry = registry || 'https://registry.npmjs.com';

const proxy = argv.proxy || process.env.npm_proxy || process.env.npm_config_proxy;

const env = {
  npm_config_registry: registry,
  // set npm_config_argv
  // see https:///cnpm/npminstall/issues/121#issuecomment-247836741
  npm_config_argv: JSON.stringify({
    remain: [],
    cooked: originalArgv,
    original: originalArgv,
  }),
  // user-agent
  npm_config_user_agent: globalConfig.userAgent,
};
// https:///npm/npm/blob/2005f4ce11f6cdf142f8a77f4f7ee4996000fb57/lib/utils/lifecycle.js#L67
env.npm_node_execpath = env.NODE = process.env.NODE || process.execPath;
env.npm_execpath = require.main.filename;

// package's npm script can get root from `env.npm_rootpath`
env.npm_rootpath = process.env.npm_rootpath || root;

// npm cli will auto set options to npm_xx env.
for (const key in argv) {
  const value = argv[key];
  if (value && typeof value === 'string') {
    env['npm_config_' + key] = value;
  }
}

debug('argv: %j, env: %j', argv, env);

(async () => {
  let binaryMirrors = {};

  if (inChina) {
    binaryMirrors = await utils.getBinaryMirrors(registry, { proxy });
    if (customChinaMirrorUrl) {
      for (const key in binaryMirrors) {
        const item = binaryMirrors[key];
        if (item.host) {
          item.host = item.host.replace(globalConfig.chineseMirrorUrl, customChinaMirrorUrl);
        }
      }
    }

    // set env
    for (const key in binaryMirrors.ENVS) {
      env[key] = binaryMirrors.ENVS[key];
      if (customChinaMirrorUrl) {
        env[key] = env[key].replace(globalConfig.chineseMirrorUrl, customChinaMirrorUrl);
      }
    }
  }

  const config = {
    root,
    registry,
    pkgs,
    production,
    cacheStrict: argv['cache-strict'],
    cacheDir,
    env,
    binaryMirrors,
    forbiddenLicenses,
    flatten,
    proxy,
    prune,
    disableDedupe: argv['disable-dedupe'],
  };
  config.strictSSL = getStrictSSL();
  config.ignoreScripts = argv['ignore-scripts'] || getIgnoreScripts();
  config.ignoreOptionalDependencies = !argv.optional;
  config.detail = argv.detail;
  config.forceLinkLatest = !!argv['force-link-latest'];
  config.trace = argv.trace;
  config.engineStrict = argv['engine-strict'];
  config.registryOnly = argv['registry-only'];
  if (config.production || argv.global) {
    // make sure show detail on production install or global install
    config.detail = true;
  }
  config.client = argv.client;

  if (argv['tarball-url-mapping']) {
    const tarballUrlMapping = JSON.parse(argv['tarball-url-mapping']);
    config.formatNpmTarbalUrl = function formatNpmTarbalUrl(url) {
      for (const fromUrl in tarballUrlMapping) {
        const toUrl = tarballUrlMapping[fromUrl];
        url = url.replace(fromUrl, toUrl);
      }
      return url;
    };
  }

  if (argv['fix-bug-versions']) {
    const packageVersionMapping = await utils.getBugVersions(registry, { proxy });
    config.autoFixVersion = function autoFixVersion(name, version) {
      const fixVersions = packageVersionMapping[name];
      return fixVersions && fixVersions[version] || null;
    };
  }

  const dependenciesTree = argv['dependencies-tree'];
  if (dependenciesTree) {
    try {
      const content = fs.readFileSync(dependenciesTree);
      config.dependenciesTree = JSON.parse(content);
    } catch (err) {
      console.warn(chalk.yellow('npminstall WARN load dependencies tree %s error: %s'), dependenciesTree, err.message);
    }
  }
  if (argv['save-dependencies-tree']) {
    config.saveDependenciesTree = true;
  }

  if (argv['high-speed-store']) {
    config.highSpeedStore = require(argv['high-speed-store']);
  }

  // -g install to npm's global prefix
  if (argv.global) {
    // support custom prefix for global install
    const meta = utils.getGlobalInstallMeta(argv.prefix);
    config.targetDir = meta.targetDir;
    config.binDir = meta.binDir;
    await installGlobal(config, context);
  } else {
    if (pkgs.length === 0) {
      if (config.production) {
        // warning when `${root}/node_modules` exists
        const nodeModulesDir = path.join(root, 'node_modules');
        if (await fs.exists(nodeModulesDir)) {
          const dirs = await fs.readdir(nodeModulesDir);
          // ignore [ '.bin', 'node' ], it will install first by https:///cnpm/nodeinstall
          if (!(dirs.length === 2 && dirs.indexOf('.bin') >= 0 && dirs.indexOf('node') >= 0)) {
            console.error(chalk.yellow(`npminstall WARN node_modules exists: ${nodeModulesDir}, contains ${dirs.length} dirs`));
          }
        }
      }
      const pkgFile = path.join(root, 'package.json');
      const exists = await fs.exists(pkgFile);
      if (!exists) {
        console.warn(chalk.yellow(`npminstall WARN package.json not exists: ${pkgFile}`));
      } else {
        // try to read npminstall config from package.json
        const pkg = await utils.readJSON(pkgFile);
        pkg.config = pkg.config || {};
        pkg.config.npminstall = pkg.config.npminstall || {};
        // {
        //   "config": {
        //     "npminstall": {
        //       "prune": true
        //     }
        //   }
        // }
        if (pkg.config.npminstall.prune === true) {
          config.prune = true;
        }
        if (pkg.config.npminstall.disableDedupe === true) {
          config.disableDedupe = true;
        }
        // env config
        // {
        //   "config": {
        //     "npminstall": {
        //       "env:production": {
        //         "disableDedupe": true
        //       }
        //     }
        //   }
        // }
        // production
        if (config.production && pkg.config.npminstall['env:production']) {
          const envConfig = pkg.config.npminstall['env:production'];
          if (envConfig.prune === true) {
            config.prune = true;
          }
          if (envConfig.disableDedupe === true) {
            config.disableDedupe = true;
          }
        }
        // development
        if (!config.production && pkg.config.npminstall['env:development']) {
          const envConfig = pkg.config.npminstall['env:development'];
          if (envConfig.prune === true) {
            config.prune = true;
          }
          if (envConfig.disableDedupe === true) {
            config.disableDedupe = true;
          }
        }
      }
    }
    await installLocal(config, context);
    if (pkgs.length > 0) {
      // support --save, --save-dev, --save-optional, --save-client, --save-build and --save-isomorphic
      const map = {
        save: 'dependencies',
        'save-dev': 'devDependencies',
        'save-optional': 'optionalDependencies',
        'save-client': 'clientDependencies',
        'save-build': 'buildDependencies',
        'save-isomorphic': 'isomorphicDependencies',
      };

      //    install saves any specified packages into dependencies by default.
      if (Object.keys(map).every(key => !argv[key]) && !argv['no-save']) {
        await updateDependencies(root, pkgs, map.save, argv['save-exact'], config.remoteNames);
      } else {
        for (const key in map) {
          if (argv[key]) await updateDependencies(root, pkgs, map[key], argv['save-exact'], config.remoteNames);
        }
      }

    }
  }

  process.on('exit', code => {
    if (code !== 0) {
      fs.writeFileSync(path.join(root, 'npminstall-debug.log'), util.inspect(config, { depth: 2 }));
    }
  });
})().catch(err => {
  console.error((err.stack));
  console.error(chalk.yellow('npminstall version: %s'), require('../package.json').version);
  console.error(chalk.yellow('npminstall args: %s'), process.argv.join(' '));
  process.exit(1);
});

function getVersionSavePrefix() {
  try {
    return execSync('npm config get save-prefix').toString().trim();
  } catch (err) {
    debug(`exec npm config get save-prefix ERROR: ${err.message}`);
    return '^';
  }
}

function getStrictSSL() {
  try {
    const strictSSL = execSync('npm config get strict-ssl').toString().trim();
    return strictSSL !== 'false';
  } catch (err) {
    debug(`exec npm config get strict-ssl ERROR: ${err.message}`);
    return true;
  }
}

function getIgnoreScripts() {
  try {
    const ignoreScripts = execSync('npm config get ignore-scripts').toString().trim();
    return ignoreScripts === 'true';
  } catch (err) {
    debug(`exec npm config get ignore-scripts ERROR: ${err.message}`);
    return false;
  }
}

async function updateDependencies(root, pkgs, propName, saveExact, remoteNames) {
  const savePrefix = saveExact ? '' : getVersionSavePrefix();
  const pkgFile = path.join(root, 'package.json');
  const pkg = await utils.readJSON(pkgFile);
  const deps = pkg[propName] = pkg[propName] || {};
  for (const item of pkgs) {
    if (REMOTE_TYPES.includes(item.type)) {
      // if install from remote or git and don't specified name
      // get package's name from `remoteNames`
      item.name
        ? deps[item.name] = item.version
        : deps[remoteNames[item.version]] = item.version;
    } else if (item.type === ALIAS_TYPES) {
      deps[item.name] = item.version;
    } else {
      const pkgDir = LOCAL_TYPES.includes(item.type) ? item.version : path.join(root, 'node_modules', item.name);
      const itemPkg = await utils.readJSON(path.join(pkgDir, 'package.json'));
      deps[itemPkg.name] = `${savePrefix}${itemPkg.version}`;
    }
  }
  // sort pkg[propName]
  const newDeps = {};
  for (const key of Object.keys(deps).sort()) {
    newDeps[key] = deps[key];
  }
  pkg[propName] = newDeps;
  await fs.writeFile(pkgFile, JSON.stringify(pkg, null, 2) + '\n');
}