ProjectPackageManager.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. const fs = require('fs-extra')
  2. const path = require('path')
  3. const minimist = require('minimist')
  4. const LRU = require('lru-cache')
  5. const {
  6. chalk,
  7. execa,
  8. semver,
  9. request,
  10. resolvePkg,
  11. loadModule,
  12. hasYarn,
  13. hasProjectYarn,
  14. hasPnpm3OrLater,
  15. hasPnpmVersionOrLater,
  16. hasProjectPnpm,
  17. hasProjectNpm,
  18. isOfficialPlugin,
  19. resolvePluginId,
  20. log,
  21. warn,
  22. error
  23. } = require('@vue/cli-shared-utils')
  24. const { loadOptions } = require('../options')
  25. const { executeCommand } = require('./executeCommand')
  26. const registries = require('./registries')
  27. const shouldUseTaobao = require('./shouldUseTaobao')
  28. const metadataCache = new LRU({
  29. max: 200,
  30. maxAge: 1000 * 60 * 30 // 30 min.
  31. })
  32. const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
  33. const SUPPORTED_PACKAGE_MANAGERS = ['yarn', 'pnpm', 'npm']
  34. const PACKAGE_MANAGER_PNPM4_CONFIG = {
  35. install: ['install', '--reporter', 'silent', '--shamefully-hoist'],
  36. add: ['install', '--reporter', 'silent', '--shamefully-hoist'],
  37. upgrade: ['update', '--reporter', 'silent'],
  38. remove: ['uninstall', '--reporter', 'silent']
  39. }
  40. const PACKAGE_MANAGER_PNPM3_CONFIG = {
  41. install: ['install', '--loglevel', 'error', '--shamefully-flatten'],
  42. add: ['install', '--loglevel', 'error', '--shamefully-flatten'],
  43. upgrade: ['update', '--loglevel', 'error'],
  44. remove: ['uninstall', '--loglevel', 'error']
  45. }
  46. const PACKAGE_MANAGER_CONFIG = {
  47. npm: {
  48. install: ['install', '--loglevel', 'error'],
  49. add: ['install', '--loglevel', 'error'],
  50. upgrade: ['update', '--loglevel', 'error'],
  51. remove: ['uninstall', '--loglevel', 'error']
  52. },
  53. pnpm: hasPnpmVersionOrLater('4.0.0') ? PACKAGE_MANAGER_PNPM4_CONFIG : PACKAGE_MANAGER_PNPM3_CONFIG,
  54. yarn: {
  55. install: [],
  56. add: ['add'],
  57. upgrade: ['upgrade'],
  58. remove: ['remove']
  59. }
  60. }
  61. // extract the package name 'xx' from the format 'xx@1.1'
  62. function stripVersion (packageName) {
  63. const nameRegExp = /^(@?[^@]+)(@.*)?$/
  64. const result = packageName.match(nameRegExp)
  65. if (!result) {
  66. throw new Error(`Invalid package name ${packageName}`)
  67. }
  68. return result[1]
  69. }
  70. class PackageManager {
  71. constructor ({ context, forcePackageManager } = {}) {
  72. this.context = context || process.cwd()
  73. if (forcePackageManager) {
  74. this.bin = forcePackageManager
  75. } else if (context) {
  76. if (hasProjectYarn(context)) {
  77. this.bin = 'yarn'
  78. } else if (hasProjectPnpm(context)) {
  79. this.bin = 'pnpm'
  80. } else if (hasProjectNpm(context)) {
  81. this.bin = 'npm'
  82. }
  83. }
  84. // if no package managers specified, and no lockfile exists
  85. if (!this.bin) {
  86. this.bin = loadOptions().packageManager || (hasYarn() ? 'yarn' : hasPnpm3OrLater() ? 'pnpm' : 'npm')
  87. }
  88. if (!SUPPORTED_PACKAGE_MANAGERS.includes(this.bin)) {
  89. log()
  90. warn(
  91. `The package manager ${chalk.red(this.bin)} is ${chalk.red('not officially supported')}.\n` +
  92. `It will be treated like ${chalk.cyan('npm')}, but compatibility issues may occur.\n` +
  93. `See if you can use ${chalk.cyan('--registry')} instead.`
  94. )
  95. PACKAGE_MANAGER_CONFIG[this.bin] = PACKAGE_MANAGER_CONFIG.npm
  96. }
  97. // Plugin may be located in another location if `resolveFrom` presents.
  98. const projectPkg = resolvePkg(this.context)
  99. const resolveFrom = projectPkg && projectPkg.vuePlugins && projectPkg.vuePlugins.resolveFrom
  100. // Logically, `resolveFrom` and `context` are distinct fields.
  101. // But in Vue CLI we only care about plugins.
  102. // So it is fine to let all other operations take place in the `resolveFrom` directory.
  103. if (resolveFrom) {
  104. this.context = path.resolve(context, resolveFrom)
  105. }
  106. }
  107. // Any command that implemented registry-related feature should support
  108. // `-r` / `--registry` option
  109. async getRegistry () {
  110. if (this._registry) {
  111. return this._registry
  112. }
  113. const args = minimist(process.argv, {
  114. alias: {
  115. r: 'registry'
  116. }
  117. })
  118. if (args.registry) {
  119. this._registry = args.registry
  120. } else if (!process.env.VUE_CLI_TEST && await shouldUseTaobao(this.bin)) {
  121. this._registry = registries.taobao
  122. } else {
  123. try {
  124. this._registry = (await execa(this.bin, ['config', 'get', 'registry'])).stdout
  125. } catch (e) {
  126. // Yarn 2 uses `npmRegistryServer` instead of `registry`
  127. this._registry = (await execa(this.bin, ['config', 'get', 'npmRegistryServer'])).stdout
  128. }
  129. }
  130. return this._registry
  131. }
  132. async setRegistryEnvs () {
  133. const registry = await this.getRegistry()
  134. process.env.npm_config_registry = registry
  135. process.env.YARN_NPM_REGISTRY_SERVER = registry
  136. this.setBinaryMirrors()
  137. }
  138. // set mirror urls for users in china
  139. async setBinaryMirrors () {
  140. const registry = await this.getRegistry()
  141. if (registry !== registries.taobao) {
  142. return
  143. }
  144. try {
  145. // node-sass, chromedriver, etc.
  146. const binaryMirrorConfig = await this.getMetadata('binary-mirror-config')
  147. const mirrors = binaryMirrorConfig.mirrors.china
  148. for (const key in mirrors.ENVS) {
  149. process.env[key] = mirrors.ENVS[key]
  150. }
  151. // Cypress
  152. const cypressMirror = mirrors.cypress
  153. const defaultPlatforms = {
  154. darwin: 'osx64',
  155. linux: 'linux64',
  156. win32: 'win64'
  157. }
  158. const platforms = cypressMirror.newPlatforms || defaultPlatforms
  159. const targetPlatform = platforms[require('os').platform()]
  160. // Do not override user-defined env variable
  161. // Because we may construct a wrong download url and an escape hatch is necessary
  162. if (targetPlatform && !process.env.CYPRESS_INSTALL_BINARY) {
  163. // We only support cypress 3 for the current major version
  164. const latestCypressVersion = await this.getRemoteVersion('cypress', '^3')
  165. process.env.CYPRESS_INSTALL_BINARY =
  166. `${cypressMirror.host}/${latestCypressVersion}/${targetPlatform}/cypress.zip`
  167. }
  168. } catch (e) {
  169. // get binary mirror config failed
  170. }
  171. }
  172. // https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
  173. async getMetadata (packageName, { full = false } = {}) {
  174. const registry = await this.getRegistry()
  175. const metadataKey = `${this.bin}-${registry}-${packageName}`
  176. let metadata = metadataCache.get(metadataKey)
  177. if (metadata) {
  178. return metadata
  179. }
  180. const headers = {}
  181. if (!full) {
  182. headers.Accept = 'application/vnd.npm.install-v1+json'
  183. }
  184. const url = `${registry.replace(/\/$/g, '')}/${packageName}`
  185. try {
  186. metadata = (await request.get(url, { headers })).body
  187. metadataCache.set(metadataKey, metadata)
  188. return metadata
  189. } catch (e) {
  190. error(`Failed to get response from ${url}`)
  191. throw e
  192. }
  193. }
  194. async getRemoteVersion (packageName, versionRange = 'latest') {
  195. const metadata = await this.getMetadata(packageName)
  196. if (Object.keys(metadata['dist-tags']).includes(versionRange)) {
  197. return metadata['dist-tags'][versionRange]
  198. }
  199. const versions = Array.isArray(metadata.versions) ? metadata.versions : Object.keys(metadata.versions)
  200. return semver.maxSatisfying(versions, versionRange)
  201. }
  202. getInstalledVersion (packageName) {
  203. // for first level deps, read package.json directly is way faster than `npm list`
  204. try {
  205. const packageJson = loadModule(`${packageName}/package.json`, this.context, true)
  206. return packageJson.version
  207. } catch (e) {}
  208. }
  209. async runCommand (command, args) {
  210. await this.setRegistryEnvs()
  211. return await executeCommand(
  212. this.bin,
  213. [
  214. ...PACKAGE_MANAGER_CONFIG[this.bin][command],
  215. ...(args || [])
  216. ],
  217. this.context
  218. )
  219. }
  220. async install () {
  221. if (process.env.VUE_CLI_TEST) {
  222. try {
  223. await this.runCommand('install', ['--offline'])
  224. } catch (e) {
  225. await this.runCommand('install')
  226. }
  227. }
  228. return await this.runCommand('install')
  229. }
  230. async add (packageName, {
  231. tilde = false,
  232. dev = true
  233. } = {}) {
  234. const args = dev ? ['-D'] : []
  235. if (tilde) {
  236. if (this.bin === 'yarn') {
  237. args.push('--tilde')
  238. } else {
  239. process.env.npm_config_save_prefix = '~'
  240. }
  241. }
  242. return await this.runCommand('add', [packageName, ...args])
  243. }
  244. async remove (packageName) {
  245. return await this.runCommand('remove', [packageName])
  246. }
  247. async upgrade (packageName) {
  248. const realname = stripVersion(packageName)
  249. if (
  250. isTestOrDebug &&
  251. (packageName === '@vue/cli-service' || isOfficialPlugin(resolvePluginId(realname)))
  252. ) {
  253. // link packages in current repo for test
  254. const src = path.resolve(__dirname, `../../../../${realname}`)
  255. const dest = path.join(this.context, 'node_modules', realname)
  256. await fs.remove(dest)
  257. await fs.symlink(src, dest, 'dir')
  258. return
  259. }
  260. return await this.runCommand('add', [packageName])
  261. }
  262. }
  263. module.exports = PackageManager