mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-01-13 02:29:40 -06:00
Avoid empty `node_modules` when the user has set `NODE_ENV` to `production` in the shell environment. In the long run we should have a more comprehensive preflight check for execution environment though.
434 lines
13 KiB
JavaScript
434 lines
13 KiB
JavaScript
const fs = require('fs-extra')
|
|
const path = require('path')
|
|
|
|
const ini = require('ini')
|
|
const minimist = require('minimist')
|
|
const LRU = require('lru-cache')
|
|
|
|
const stripAnsi = require('strip-ansi')
|
|
|
|
const {
|
|
chalk,
|
|
execa,
|
|
semver,
|
|
request,
|
|
|
|
resolvePkg,
|
|
loadModule,
|
|
|
|
hasYarn,
|
|
hasProjectYarn,
|
|
hasPnpm3OrLater,
|
|
hasPnpmVersionOrLater,
|
|
hasProjectPnpm,
|
|
hasProjectNpm,
|
|
|
|
isOfficialPlugin,
|
|
resolvePluginId,
|
|
|
|
log,
|
|
warn,
|
|
error
|
|
} = require('@vue/cli-shared-utils')
|
|
|
|
const { loadOptions } = require('../options')
|
|
const { executeCommand } = require('./executeCommand')
|
|
|
|
const registries = require('./registries')
|
|
const shouldUseTaobao = require('./shouldUseTaobao')
|
|
|
|
const metadataCache = new LRU({
|
|
max: 200,
|
|
maxAge: 1000 * 60 * 30 // 30 min.
|
|
})
|
|
|
|
const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
|
|
|
|
const SUPPORTED_PACKAGE_MANAGERS = ['yarn', 'pnpm', 'npm']
|
|
const PACKAGE_MANAGER_PNPM4_CONFIG = {
|
|
install: ['install', '--reporter', 'silent', '--shamefully-hoist'],
|
|
add: ['install', '--reporter', 'silent', '--shamefully-hoist'],
|
|
upgrade: ['update', '--reporter', 'silent'],
|
|
remove: ['uninstall', '--reporter', 'silent']
|
|
}
|
|
const PACKAGE_MANAGER_PNPM3_CONFIG = {
|
|
install: ['install', '--loglevel', 'error', '--shamefully-flatten'],
|
|
add: ['install', '--loglevel', 'error', '--shamefully-flatten'],
|
|
upgrade: ['update', '--loglevel', 'error'],
|
|
remove: ['uninstall', '--loglevel', 'error']
|
|
}
|
|
const PACKAGE_MANAGER_CONFIG = {
|
|
npm: {
|
|
install: ['install', '--loglevel', 'error'],
|
|
add: ['install', '--loglevel', 'error'],
|
|
upgrade: ['update', '--loglevel', 'error'],
|
|
remove: ['uninstall', '--loglevel', 'error']
|
|
},
|
|
pnpm: hasPnpmVersionOrLater('4.0.0') ? PACKAGE_MANAGER_PNPM4_CONFIG : PACKAGE_MANAGER_PNPM3_CONFIG,
|
|
yarn: {
|
|
install: [],
|
|
add: ['add'],
|
|
upgrade: ['upgrade'],
|
|
remove: ['remove']
|
|
}
|
|
}
|
|
|
|
// extract the package name 'xx' from the format 'xx@1.1'
|
|
function stripVersion (packageName) {
|
|
const nameRegExp = /^(@?[^@]+)(@.*)?$/
|
|
const result = packageName.match(nameRegExp)
|
|
|
|
if (!result) {
|
|
throw new Error(`Invalid package name ${packageName}`)
|
|
}
|
|
|
|
return result[1]
|
|
}
|
|
|
|
// extract the package scope from the full package name
|
|
// the result includes the initial @ character
|
|
function extractPackageScope (packageName) {
|
|
const scopedNameRegExp = /^(@[^/]+)\/.*$/
|
|
const result = packageName.match(scopedNameRegExp)
|
|
|
|
if (!result) {
|
|
return undefined
|
|
}
|
|
|
|
return result[1]
|
|
}
|
|
|
|
class PackageManager {
|
|
constructor ({ context, forcePackageManager } = {}) {
|
|
this.context = context || process.cwd()
|
|
this._registries = {}
|
|
|
|
if (forcePackageManager) {
|
|
this.bin = forcePackageManager
|
|
} else if (context) {
|
|
if (hasProjectYarn(context)) {
|
|
this.bin = 'yarn'
|
|
} else if (hasProjectPnpm(context)) {
|
|
this.bin = 'pnpm'
|
|
} else if (hasProjectNpm(context)) {
|
|
this.bin = 'npm'
|
|
}
|
|
}
|
|
|
|
// if no package managers specified, and no lockfile exists
|
|
if (!this.bin) {
|
|
this.bin = loadOptions().packageManager || (hasYarn() ? 'yarn' : hasPnpm3OrLater() ? 'pnpm' : 'npm')
|
|
}
|
|
|
|
if (this.bin === 'npm') {
|
|
// npm doesn't support package aliases until v6.9
|
|
const MIN_SUPPORTED_NPM_VERSION = '6.9.0'
|
|
const npmVersion = stripAnsi(execa.sync('npm', ['--version']).stdout)
|
|
|
|
if (semver.lt(npmVersion, MIN_SUPPORTED_NPM_VERSION)) {
|
|
throw new Error(
|
|
'You are using an outdated version of NPM.\n' +
|
|
'It does not support some core functionalities of Vue CLI.\n' +
|
|
'Please upgrade your NPM version.'
|
|
)
|
|
}
|
|
|
|
if (semver.gte(npmVersion, '7.0.0')) {
|
|
this.needsPeerDepsFix = true
|
|
}
|
|
}
|
|
|
|
if (!SUPPORTED_PACKAGE_MANAGERS.includes(this.bin)) {
|
|
log()
|
|
warn(
|
|
`The package manager ${chalk.red(this.bin)} is ${chalk.red('not officially supported')}.\n` +
|
|
`It will be treated like ${chalk.cyan('npm')}, but compatibility issues may occur.\n` +
|
|
`See if you can use ${chalk.cyan('--registry')} instead.`
|
|
)
|
|
PACKAGE_MANAGER_CONFIG[this.bin] = PACKAGE_MANAGER_CONFIG.npm
|
|
}
|
|
|
|
// Plugin may be located in another location if `resolveFrom` presents.
|
|
const projectPkg = resolvePkg(this.context)
|
|
const resolveFrom = projectPkg && projectPkg.vuePlugins && projectPkg.vuePlugins.resolveFrom
|
|
|
|
// Logically, `resolveFrom` and `context` are distinct fields.
|
|
// But in Vue CLI we only care about plugins.
|
|
// So it is fine to let all other operations take place in the `resolveFrom` directory.
|
|
if (resolveFrom) {
|
|
this.context = path.resolve(context, resolveFrom)
|
|
}
|
|
}
|
|
|
|
// Any command that implemented registry-related feature should support
|
|
// `-r` / `--registry` option
|
|
async getRegistry (scope) {
|
|
const cacheKey = scope || ''
|
|
if (this._registries[cacheKey]) {
|
|
return this._registries[cacheKey]
|
|
}
|
|
|
|
const args = minimist(process.argv, {
|
|
alias: {
|
|
r: 'registry'
|
|
}
|
|
})
|
|
|
|
let registry
|
|
if (args.registry) {
|
|
registry = args.registry
|
|
} else if (!process.env.VUE_CLI_TEST && await shouldUseTaobao(this.bin)) {
|
|
registry = registries.taobao
|
|
} else {
|
|
try {
|
|
if (scope) {
|
|
registry = (await execa(this.bin, ['config', 'get', scope + ':registry'])).stdout
|
|
}
|
|
if (!registry || registry === 'undefined') {
|
|
registry = (await execa(this.bin, ['config', 'get', 'registry'])).stdout
|
|
}
|
|
} catch (e) {
|
|
// Yarn 2 uses `npmRegistryServer` instead of `registry`
|
|
registry = (await execa(this.bin, ['config', 'get', 'npmRegistryServer'])).stdout
|
|
}
|
|
}
|
|
|
|
this._registries[cacheKey] = stripAnsi(registry).trim()
|
|
return this._registries[cacheKey]
|
|
}
|
|
|
|
async getAuthConfig (scope) {
|
|
// get npmrc (https://docs.npmjs.com/configuring-npm/npmrc.html#files)
|
|
const possibleRcPaths = [
|
|
path.resolve(this.context, '.npmrc'),
|
|
path.resolve(require('os').homedir(), '.npmrc')
|
|
]
|
|
if (process.env.PREFIX) {
|
|
possibleRcPaths.push(path.resolve(process.env.PREFIX, '/etc/npmrc'))
|
|
}
|
|
// there's also a '/path/to/npm/npmrc', skipped for simplicity of implementation
|
|
|
|
let npmConfig = {}
|
|
for (const loc of possibleRcPaths) {
|
|
if (fs.existsSync(loc)) {
|
|
try {
|
|
// the closer config file (the one with lower index) takes higher precedence
|
|
npmConfig = Object.assign({}, ini.parse(fs.readFileSync(loc, 'utf-8')), npmConfig)
|
|
} catch (e) {
|
|
// in case of file permission issues, etc.
|
|
}
|
|
}
|
|
}
|
|
|
|
const registry = await this.getRegistry(scope)
|
|
const registryWithoutProtocol = registry
|
|
.replace(/https?:/, '') // remove leading protocol
|
|
.replace(/([^/])$/, '$1/') // ensure ending with slash
|
|
const authTokenKey = `${registryWithoutProtocol}:_authToken`
|
|
const authUsernameKey = `${registryWithoutProtocol}:username`
|
|
const authPasswordKey = `${registryWithoutProtocol}:_password`
|
|
|
|
const auth = {}
|
|
if (authTokenKey in npmConfig) {
|
|
auth.token = npmConfig[authTokenKey]
|
|
}
|
|
if (authPasswordKey in npmConfig) {
|
|
auth.username = npmConfig[authUsernameKey]
|
|
auth.password = Buffer.from(npmConfig[authPasswordKey], 'base64').toString()
|
|
}
|
|
return auth
|
|
}
|
|
|
|
async setRegistryEnvs () {
|
|
const registry = await this.getRegistry()
|
|
|
|
process.env.npm_config_registry = registry
|
|
process.env.YARN_NPM_REGISTRY_SERVER = registry
|
|
|
|
this.setBinaryMirrors()
|
|
}
|
|
|
|
// set mirror urls for users in china
|
|
async setBinaryMirrors () {
|
|
const registry = await this.getRegistry()
|
|
|
|
if (registry !== registries.taobao) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
// chromedriver, etc.
|
|
const binaryMirrorConfigMetadata = await this.getMetadata('binary-mirror-config', { full: true })
|
|
const latest = binaryMirrorConfigMetadata['dist-tags'] && binaryMirrorConfigMetadata['dist-tags'].latest
|
|
const mirrors = binaryMirrorConfigMetadata.versions[latest].mirrors.china
|
|
for (const key in mirrors.ENVS) {
|
|
process.env[key] = mirrors.ENVS[key]
|
|
}
|
|
|
|
// Cypress
|
|
const cypressMirror = mirrors.cypress
|
|
const defaultPlatforms = {
|
|
darwin: 'osx64',
|
|
linux: 'linux64',
|
|
win32: 'win64'
|
|
}
|
|
const platforms = cypressMirror.newPlatforms || defaultPlatforms
|
|
const targetPlatform = platforms[require('os').platform()]
|
|
// Do not override user-defined env variable
|
|
// Because we may construct a wrong download url and an escape hatch is necessary
|
|
if (targetPlatform && !process.env.CYPRESS_INSTALL_BINARY) {
|
|
const projectPkg = resolvePkg(this.context)
|
|
if (projectPkg && projectPkg.devDependencies && projectPkg.devDependencies.cypress) {
|
|
const wantedCypressVersion = await this.getRemoteVersion('cypress', projectPkg.devDependencies.cypress)
|
|
process.env.CYPRESS_INSTALL_BINARY =
|
|
`${cypressMirror.host}/${wantedCypressVersion}/${targetPlatform}/cypress.zip`
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// get binary mirror config failed
|
|
}
|
|
}
|
|
|
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
|
async getMetadata (packageName, { full = false } = {}) {
|
|
const scope = extractPackageScope(packageName)
|
|
const registry = await this.getRegistry(scope)
|
|
|
|
const metadataKey = `${this.bin}-${registry}-${packageName}`
|
|
let metadata = metadataCache.get(metadataKey)
|
|
|
|
if (metadata) {
|
|
return metadata
|
|
}
|
|
|
|
const headers = {}
|
|
if (!full) {
|
|
headers.Accept = 'application/vnd.npm.install-v1+json;q=1.0, application/json;q=0.9, */*;q=0.8'
|
|
}
|
|
|
|
const authConfig = await this.getAuthConfig(scope)
|
|
if ('password' in authConfig) {
|
|
const credentials = Buffer.from(`${authConfig.username}:${authConfig.password}`).toString('base64')
|
|
headers.Authorization = `Basic ${credentials}`
|
|
}
|
|
if ('token' in authConfig) {
|
|
headers.Authorization = `Bearer ${authConfig.token}`
|
|
}
|
|
|
|
const url = `${registry.replace(/\/$/g, '')}/${packageName}`
|
|
try {
|
|
metadata = (await request.get(url, { headers }))
|
|
if (metadata.error) {
|
|
throw new Error(metadata.error)
|
|
}
|
|
metadataCache.set(metadataKey, metadata)
|
|
return metadata
|
|
} catch (e) {
|
|
error(`Failed to get response from ${url}`)
|
|
throw e
|
|
}
|
|
}
|
|
|
|
async getRemoteVersion (packageName, versionRange = 'latest') {
|
|
const metadata = await this.getMetadata(packageName)
|
|
if (Object.keys(metadata['dist-tags']).includes(versionRange)) {
|
|
return metadata['dist-tags'][versionRange]
|
|
}
|
|
const versions = Array.isArray(metadata.versions) ? metadata.versions : Object.keys(metadata.versions)
|
|
return semver.maxSatisfying(versions, versionRange)
|
|
}
|
|
|
|
getInstalledVersion (packageName) {
|
|
// for first level deps, read package.json directly is way faster than `npm list`
|
|
try {
|
|
const packageJson = loadModule(`${packageName}/package.json`, this.context, true)
|
|
return packageJson.version
|
|
} catch (e) {}
|
|
}
|
|
|
|
async runCommand (command, args) {
|
|
const prevNodeEnv = process.env.NODE_ENV
|
|
// In the use case of Vue CLI, when installing dependencies,
|
|
// the `NODE_ENV` environment variable does no good;
|
|
// it only confuses users by skipping dev deps (when set to `production`).
|
|
delete process.env.NODE_ENV
|
|
|
|
await this.setRegistryEnvs()
|
|
await executeCommand(
|
|
this.bin,
|
|
[
|
|
...PACKAGE_MANAGER_CONFIG[this.bin][command],
|
|
...(args || [])
|
|
],
|
|
this.context
|
|
)
|
|
|
|
if (prevNodeEnv) {
|
|
process.env.NODE_ENV = prevNodeEnv
|
|
}
|
|
}
|
|
|
|
async install () {
|
|
const args = []
|
|
|
|
if (this.needsPeerDepsFix) {
|
|
args.push('--legacy-peer-deps')
|
|
}
|
|
|
|
if (process.env.VUE_CLI_TEST) {
|
|
args.push('--silent', '--no-progress')
|
|
}
|
|
|
|
return await this.runCommand('install', args)
|
|
}
|
|
|
|
async add (packageName, {
|
|
tilde = false,
|
|
dev = true
|
|
} = {}) {
|
|
const args = dev ? ['-D'] : []
|
|
if (tilde) {
|
|
if (this.bin === 'yarn') {
|
|
args.push('--tilde')
|
|
} else {
|
|
process.env.npm_config_save_prefix = '~'
|
|
}
|
|
}
|
|
|
|
if (this.needsPeerDepsFix) {
|
|
args.push('--legacy-peer-deps')
|
|
}
|
|
|
|
return await this.runCommand('add', [packageName, ...args])
|
|
}
|
|
|
|
async remove (packageName) {
|
|
return await this.runCommand('remove', [packageName])
|
|
}
|
|
|
|
async upgrade (packageName) {
|
|
// manage multiple packages separated by spaces
|
|
const packageNamesArray = []
|
|
|
|
for (const packname of packageName.split(' ')) {
|
|
const realname = stripVersion(packname)
|
|
if (
|
|
isTestOrDebug &&
|
|
(packname === '@vue/cli-service' || isOfficialPlugin(resolvePluginId(realname)))
|
|
) {
|
|
// link packages in current repo for test
|
|
const src = path.resolve(__dirname, `../../../../${realname}`)
|
|
const dest = path.join(this.context, 'node_modules', realname)
|
|
await fs.remove(dest)
|
|
await fs.symlink(src, dest, 'dir')
|
|
} else {
|
|
packageNamesArray.push(packname)
|
|
}
|
|
}
|
|
|
|
if (packageNamesArray.length) return await this.runCommand('add', packageNamesArray)
|
|
}
|
|
}
|
|
|
|
module.exports = PackageManager
|