173 lines
5.1 KiB
JavaScript
173 lines
5.1 KiB
JavaScript
/* eslint-disable camelcase */
|
|
const fs = require('fs')
|
|
const util = require('util')
|
|
const readdir = util.promisify(fs.readdir)
|
|
const reifyFinish = require('../utils/reify-finish.js')
|
|
const log = require('../utils/log-shim.js')
|
|
const { resolve, join } = require('path')
|
|
const Arborist = require('@npmcli/arborist')
|
|
const runScript = require('@npmcli/run-script')
|
|
const pacote = require('pacote')
|
|
const checks = require('npm-install-checks')
|
|
|
|
const ArboristWorkspaceCmd = require('../arborist-cmd.js')
|
|
class Install extends ArboristWorkspaceCmd {
|
|
static description = 'Install a package'
|
|
static name = 'install'
|
|
|
|
// These are in the order they will show up in when running "-h"
|
|
static params = [
|
|
'save',
|
|
'save-exact',
|
|
'global',
|
|
'install-strategy',
|
|
'legacy-bundling',
|
|
'global-style',
|
|
'omit',
|
|
'strict-peer-deps',
|
|
'package-lock',
|
|
'foreground-scripts',
|
|
'ignore-scripts',
|
|
'audit',
|
|
'bin-links',
|
|
'fund',
|
|
'dry-run',
|
|
...super.params,
|
|
]
|
|
|
|
static usage = ['[<package-spec> ...]']
|
|
|
|
async completion (opts) {
|
|
const { partialWord } = opts
|
|
// install can complete to a folder with a package.json, or any package.
|
|
// if it has a slash, then it's gotta be a folder
|
|
// if it starts with https?://, then just give up, because it's a url
|
|
if (/^https?:\/\//.test(partialWord)) {
|
|
// do not complete to URLs
|
|
return []
|
|
}
|
|
|
|
if (/\//.test(partialWord)) {
|
|
// Complete fully to folder if there is exactly one match and it
|
|
// is a folder containing a package.json file. If that is not the
|
|
// case we return 0 matches, which will trigger the default bash
|
|
// complete.
|
|
const lastSlashIdx = partialWord.lastIndexOf('/')
|
|
const partialName = partialWord.slice(lastSlashIdx + 1)
|
|
const partialPath = partialWord.slice(0, lastSlashIdx) || '/'
|
|
|
|
const isDirMatch = async sibling => {
|
|
if (sibling.slice(0, partialName.length) !== partialName) {
|
|
return false
|
|
}
|
|
|
|
try {
|
|
const contents = await readdir(join(partialPath, sibling))
|
|
const result = (contents.indexOf('package.json') !== -1)
|
|
return result
|
|
} catch (er) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
try {
|
|
const siblings = await readdir(partialPath)
|
|
const matches = []
|
|
for (const sibling of siblings) {
|
|
if (await isDirMatch(sibling)) {
|
|
matches.push(sibling)
|
|
}
|
|
}
|
|
if (matches.length === 1) {
|
|
return [join(partialPath, matches[0])]
|
|
}
|
|
// no matches
|
|
return []
|
|
} catch (er) {
|
|
return [] // invalid dir: no matching
|
|
}
|
|
}
|
|
// Note: there used to be registry completion here,
|
|
// but it stopped making sense somewhere around
|
|
// 50,000 packages on the registry
|
|
}
|
|
|
|
async exec (args) {
|
|
// the /path/to/node_modules/..
|
|
const globalTop = resolve(this.npm.globalDir, '..')
|
|
const ignoreScripts = this.npm.config.get('ignore-scripts')
|
|
const isGlobalInstall = this.npm.global
|
|
const where = isGlobalInstall ? globalTop : this.npm.prefix
|
|
const forced = this.npm.config.get('force')
|
|
const scriptShell = this.npm.config.get('script-shell') || undefined
|
|
|
|
// be very strict about engines when trying to update npm itself
|
|
const npmInstall = args.find(arg => arg.startsWith('npm@') || arg === 'npm')
|
|
if (isGlobalInstall && npmInstall) {
|
|
const npmOptions = this.npm.flatOptions
|
|
const npmManifest = await pacote.manifest(npmInstall, npmOptions)
|
|
try {
|
|
checks.checkEngine(npmManifest, npmManifest.version, process.version)
|
|
} catch (e) {
|
|
if (forced) {
|
|
log.warn(
|
|
'install',
|
|
/* eslint-disable-next-line max-len */
|
|
`Forcing global npm install with incompatible version ${npmManifest.version} into node ${process.version}`
|
|
)
|
|
} else {
|
|
throw e
|
|
}
|
|
}
|
|
}
|
|
|
|
// don't try to install the prefix into itself
|
|
args = args.filter(a => resolve(a) !== this.npm.prefix)
|
|
|
|
// `npm i -g` => "install this package globally"
|
|
if (where === globalTop && !args.length) {
|
|
args = ['.']
|
|
}
|
|
|
|
// throw usage error if trying to install empty package
|
|
// name to global space, e.g: `npm i -g ""`
|
|
if (where === globalTop && !args.every(Boolean)) {
|
|
throw this.usageError()
|
|
}
|
|
|
|
const opts = {
|
|
...this.npm.flatOptions,
|
|
auditLevel: null,
|
|
path: where,
|
|
add: args,
|
|
workspaces: this.workspaceNames,
|
|
}
|
|
const arb = new Arborist(opts)
|
|
await arb.reify(opts)
|
|
|
|
if (!args.length && !isGlobalInstall && !ignoreScripts) {
|
|
const scripts = [
|
|
'preinstall',
|
|
'install',
|
|
'postinstall',
|
|
'prepublish', // XXX(npm9) should we remove this finally??
|
|
'preprepare',
|
|
'prepare',
|
|
'postprepare',
|
|
]
|
|
for (const event of scripts) {
|
|
await runScript({
|
|
path: where,
|
|
args: [],
|
|
scriptShell,
|
|
stdio: 'inherit',
|
|
banner: !this.npm.silent,
|
|
event,
|
|
})
|
|
}
|
|
}
|
|
await reifyFinish(this.npm, arb)
|
|
}
|
|
}
|
|
module.exports = Install
|