UNPKG

npminstall

Version:

Make npm install fast and handy.

638 lines (595 loc) 21.7 kB
#!/usr/bin/env node const debug = require('node:util').debuglog('npminstall:bin:install'); const path = require('node:path'); const util = require('node:util'); const { execSync } = require('node:child_process'); const os = require('node:os'); const fs = require('node:fs/promises'); const { writeFileSync } = require('node:fs'); const chalk = require('chalk'); const parseArgs = require('minimist'); const { installLocal, installGlobal } = require('..'); const npa = require('../lib/npa'); const utils = require('../lib/utils'); const globalConfig = require('../lib/config'); const { parsePackageName } = require('../lib/alias'); const { LOCAL_TYPES, REMOTE_TYPES, ALIAS_TYPES, } = require('../lib/npa_types'); const Context = require('../lib/context'); const { lockfileConverter } = require('../lib/lockfile_resolver'); 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://a.com":"http://b.com"} 'tarball-url-mapping', 'proxy', 'dependencies-tree', // npminstall foo --workspace=aa // npminstall foo -w aa 'workspace', /** * set package-lock.json path * * 1. only support package lock v2 and v3. * 2. npminstall doesn't inspect <cwd>/package-lock.json by default. * 3. because arborist doesn't support client/build/isomorphic dependencies, * these kinds of dependencies will all be ignored. * 4. this option doesn't do extra check for the equivalence of package-lock.json and package.json * simply behaves like `npm ci` but doesn't remove the node_modules in advance. * 5. you're not supposed to install extra dependencies along with a lockfile. */ 'lockfile-path', ], 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', // run scripts on foreground, default is background 'foreground-scripts', // install ignore optionalDependencies 'optional', 'detail', 'trace', 'engine-strict', 'flatten', 'registry-only', 'cache-strict', 'fix-bug-versions', 'prune', // don't link latest version to <root>/node_modules/.store/node_modules 'disable-fallback-store', 'save-dependencies-tree', 'force-link-latest', 'workspaces', 'offline', ], default: { optional: true, }, alias: { // npm install [-S|--save|-D|--save-dev|-O|--save-optional] [-E|--save-exact] [-d|--detail] [-w|--workspace] S: 'save', D: 'save-dev', O: 'save-optional', E: 'save-exact', v: 'version', h: 'help', g: 'global', c: 'china', r: 'registry', d: 'detail', w: 'workspace', }, }) ); 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> --workspace=<workspace> npminstall <pkg> -w <workspace> npminstall <pkg> --workspaces 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 npminstall --lockfile-path=</path/to/package-lock.json> 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 -w, --workspace: install on one workspace only, e.g.: npminstall koa -w a --workspaces: install new package on all workspaces, e.g: npminstall foo --workspaces --trace: show memory and cpu usages traces of installation --ignore-scripts: ignore all preinstall / install and postinstall scripts during the installation --foreground-scripts: scripts run in the background by default, to see the output, run with: --foreground-scripts --no-optional: ignore all optionalDependencies during the installation --forbidden-licenses: forbidden 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: automatically fix bug version of packages. --prune: prune unnecessary files from ./node_modules, such as markdown, typescript source files, and so on. --dependencies-tree: install with dependencies tree to restore the last install. --force-link-latest: force link latest version package to module root path. --offline: offline mode. If a package won't be found locally, the installation will fail. ` ); 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: p.name, // `mozilla/nunjucks#0f8b21b8df7e8e852b2e1889388653b7075f0d09` should be rawSpec version: p.fetchSpec || p.rawSpec, type: p.type, alias: aliasPackageName, arg: p, }); } 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]; } let installOnAllWorkspaces = argv.workspaces; let installWorkspaceNames = utils.formatWorkspaceNames(argv); const production = argv.production || process.env.NODE_ENV === 'production'; const cacheStrict = argv['cache-strict']; // support npm_config_cache to change default cache dir const defaultCacheDir = process.env.npm_config_cache || path.join(os.homedir(), '.npminstall_tarball'); let cacheDir = defaultCacheDir; if (!cacheStrict && (production || argv.cache === false)) { cacheDir = ''; } if (process.env.npminstall_cache) { cacheDir = process.env.npminstall_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 chinese registry and mirror. 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 offline = !!argv.offline; const env = { npm_config_registry: registry, // set npm_config_argv // see https://github.com/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://github.com/sass/node-sass/blob/master/lib/extensions.js#L270 // make sure npm_config_cache env exists npm_config_cache: defaultCacheDir, }; // https://github.com/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; // 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 () => { const { workspaceRoots, workspacesMap } = await utils.readWorkspaces(root); if (workspacesMap.size > 0) { for (const info of workspacesMap.values()) { // link to root/node_modules const linkDir = path.join(root, 'node_modules', info.package.name); await utils.forceSymlink(info.root, linkDir); debug('add workspace %s on %s', info.package.name, info.root); } } // don't enable workspace on global install const enableWorkspace = !argv.global && workspacesMap.size > 0; if (!enableWorkspace) { installOnAllWorkspaces = false; installWorkspaceNames = []; } let binaryMirrors = {}; if (inChina) { binaryMirrors = await utils.getBinaryMirrors(registry, { proxy, offline, cacheDir }); 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, disableFallbackStore: argv['disable-fallback-store'], workspacesMap, // don't enable workspace on global install enableWorkspace, workspaceRoot: root, // install on workspaces root isWorkspaceRoot: true, // install on one workspace package isWorkspacePackage: false, offline, }; config.strictSSL = getStrictSSL(); // when ignore-scripts is set to `false` by user, npminstall will still // get config from npm settings instead of following user's specification, // should migrate to ?? or typeof. config.ignoreScripts = argv['ignore-scripts'] || getIgnoreScripts(); config.foregroundScripts = argv['foreground-scripts']; 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.formatNpmTarballUrl = function formatNpmTarballUrl(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, offline, cacheDir }); config.autoFixVersion = function autoFixVersion(name, version) { const fixVersions = packageVersionMapping[name]; return fixVersions && fixVersions[version] || null; }; } const lockfilePath = argv['lockfile-path']; if (lockfilePath) { try { const lockfileData = await fs.readFile(lockfilePath, 'utf8'); config.dependenciesTree = lockfileConverter(JSON.parse(lockfileData), { ignoreOptionalDependencies: true, }); } catch (error) { console.warn(chalk.yellow('npminstall WARN load lockfile from %s error :%s'), lockfilePath, error.message); } } const dependenciesTree = argv['dependencies-tree']; if (dependenciesTree) { try { const content = await fs.readFile(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; } process.on('exit', code => { if (code !== 0) { writeFileSync(path.join(root, 'npminstall-debug.log'), util.inspect(config, { depth: 2 })); } }); if (config.offline) { console.warn(chalk.yellow('npminstall WARN running on offline mode')); } // -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; // package's npm script can get root from `env.npm_rootpath` config.env.npm_rootpath = process.env.npm_rootpath || root; config.env.INIT_CWD = process.env.INIT_CWD || root; await installGlobal(config, context); console.log(''); return; } if (pkgs.length === 0) { if (config.production) { // warning when `${root}/node_modules` exists const nodeModulesDir = path.join(root, 'node_modules'); if (await utils.exists(nodeModulesDir)) { const dirs = await fs.readdir(nodeModulesDir); // ignore [ '.bin', 'node' ], it will install first by https://github.com/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 utils.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; } // production if (config.production && pkg.config.npminstall['env:production']) { const envConfig = pkg.config.npminstall['env:production']; if (envConfig.prune === true) { config.prune = true; } } // development if (!config.production && pkg.config.npminstall['env:development']) { const envConfig = pkg.config.npminstall['env:development']; if (envConfig.prune === true) { config.prune = true; } } } } const installRootConfigs = []; if (config.enableWorkspace) { if (installOnAllWorkspaces) { // npm i --workspaces for (const workspaceRoot of workspaceRoots) { installRootConfigs.push({ ...config, root: workspaceRoot, isWorkspaceRoot: false, isWorkspacePackage: true, }); } } else if (installWorkspaceNames.length > 0) { // npm i --w foo const installWorkspaceInfos = await utils.getWorkspaceInfos(root, installWorkspaceNames, workspacesMap); if (installWorkspaceInfos.length === 0) { throw new Error(`No workspaces found: --workspace=${installWorkspaceNames.join(',')}`); } for (const { root: workspaceRoot } of installWorkspaceInfos) { installRootConfigs.push({ ...config, root: workspaceRoot, isWorkspaceRoot: false, isWorkspacePackage: true, }); } } else { if (pkgs.length === 0) { // workspace: npm i for (const workspaceRoot of workspaceRoots) { installRootConfigs.push({ ...config, root: workspaceRoot, isWorkspaceRoot: false, isWorkspacePackage: true, }); } } // workspace: npm i // workspace: npm i <name> installRootConfigs.push({ ...config, root, isWorkspaceRoot: true, isWorkspacePackage: false, }); } } else { // normal: npm i // normal: npm i <name> installRootConfigs.push({ ...config, root, isWorkspaceRoot: true, isWorkspacePackage: false, }); } // main installation logic for (const installConfig of installRootConfigs) { installConfig.env.npm_rootpath = process.env.npm_rootpath || installConfig.root; installConfig.env.INIT_CWD = process.env.INIT_CWD || installConfig.root; await installLocal(installConfig, context); console.log(''); 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(installConfig.root, pkgs, map.save, argv['save-exact'], installConfig.remoteNames); } else { for (const key in map) { if (argv[key]) { await updateDependencies(installConfig.root, pkgs, map[key], argv['save-exact'], installConfig.remoteNames); } } } } } })().catch(err => { utils.exitWithError('npminstall', err); }); let _versionSavePrefix = null; function getVersionSavePrefix() { if (_versionSavePrefix === null) { try { _versionSavePrefix = execSync('npm config get save-prefix').toString().trim(); } catch (err) { debug(`exec npm config get save-prefix ERROR: ${err.message}`); _versionSavePrefix = '^'; } } return _versionSavePrefix; } 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 pkgFile = path.join(root, 'package.json'); const pkg = await utils.readJSON(pkgFile); const deps = pkg[propName] = pkg[propName] || {}; console.log('%s:', chalk.cyanBright(propName)); for (const item of pkgs) { let saveName; let saveSpec; if (REMOTE_TYPES.includes(item.type)) { // if install from remote or git and don't specified name // get package's name from `remoteNames` if (item.name) { saveName = item.name; saveSpec = item.version; } else { saveName = remoteNames[item.version]; saveSpec = item.version; } } else if (item.type === ALIAS_TYPES) { saveName = item.name; saveSpec = item.version; } else { let saveVersion; if (item.workspacePackage) { saveName = item.workspacePackage.name; saveVersion = item.workspacePackage.version || 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')); saveName = itemPkg.name; saveVersion = itemPkg.version; } // If install with `cnpm i foo`, the type is tag but rawSpec is empty string if (item.arg.type === 'tag' && item.arg.rawSpec) { saveSpec = item.arg.rawSpec; } else { const savePrefix = saveExact ? '' : getVersionSavePrefix(); saveSpec = `${savePrefix}${saveVersion}`; } } deps[saveName] = saveSpec; console.log('%s %s %s', chalk.green('+'), chalk.bold(saveName), chalk.gray(saveSpec)); } console.log(''); // 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'); }