PluginAPI.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. const path = require('path')
  2. const hash = require('hash-sum')
  3. const semver = require('semver')
  4. const { matchesPluginId } = require('@vue/cli-shared-utils')
  5. // Note: if a plugin-registered command needs to run in a specific default mode,
  6. // the plugin needs to expose it via `module.exports.defaultModes` in the form
  7. // of { [commandName]: mode }. This is because the command mode needs to be
  8. // known and applied before loading user options / applying plugins.
  9. class PluginAPI {
  10. /**
  11. * @param {string} id - Id of the plugin.
  12. * @param {Service} service - A vue-cli-service instance.
  13. */
  14. constructor (id, service) {
  15. this.id = id
  16. this.service = service
  17. }
  18. get version () {
  19. return require('../package.json').version
  20. }
  21. assertVersion (range) {
  22. if (typeof range === 'number') {
  23. if (!Number.isInteger(range)) {
  24. throw new Error('Expected string or integer value.')
  25. }
  26. range = `^${range}.0.0-0`
  27. }
  28. if (typeof range !== 'string') {
  29. throw new Error('Expected string or integer value.')
  30. }
  31. if (semver.satisfies(this.version, range)) return
  32. throw new Error(
  33. `Require @vue/cli-service "${range}", but was loaded with "${this.version}".`
  34. )
  35. }
  36. /**
  37. * Current working directory.
  38. */
  39. getCwd () {
  40. return this.service.context
  41. }
  42. /**
  43. * Resolve path for a project.
  44. *
  45. * @param {string} _path - Relative path from project root
  46. * @return {string} The resolved absolute path.
  47. */
  48. resolve (_path) {
  49. return path.resolve(this.service.context, _path)
  50. }
  51. /**
  52. * Check if the project has a given plugin.
  53. *
  54. * @param {string} id - Plugin id, can omit the (@vue/|vue-|@scope/vue)-cli-plugin- prefix
  55. * @return {boolean}
  56. */
  57. hasPlugin (id) {
  58. if (id === 'router') id = 'vue-router'
  59. if (['vue-router', 'vuex'].includes(id)) {
  60. const pkg = this.service.pkg
  61. return ((pkg.dependencies && pkg.dependencies[id]) || (pkg.devDependencies && pkg.devDependencies[id]))
  62. }
  63. return this.service.plugins.some(p => matchesPluginId(id, p.id))
  64. }
  65. /**
  66. * Register a command that will become available as `vue-cli-service [name]`.
  67. *
  68. * @param {string} name
  69. * @param {object} [opts]
  70. * {
  71. * description: string,
  72. * usage: string,
  73. * options: { [string]: string }
  74. * }
  75. * @param {function} fn
  76. * (args: { [string]: string }, rawArgs: string[]) => ?Promise
  77. */
  78. registerCommand (name, opts, fn) {
  79. if (typeof opts === 'function') {
  80. fn = opts
  81. opts = null
  82. }
  83. this.service.commands[name] = { fn, opts: opts || {}}
  84. }
  85. /**
  86. * Register a function that will receive a chainable webpack config
  87. * the function is lazy and won't be called until `resolveWebpackConfig` is
  88. * called
  89. *
  90. * @param {function} fn
  91. */
  92. chainWebpack (fn) {
  93. this.service.webpackChainFns.push(fn)
  94. }
  95. /**
  96. * Register
  97. * - a webpack configuration object that will be merged into the config
  98. * OR
  99. * - a function that will receive the raw webpack config.
  100. * the function can either mutate the config directly or return an object
  101. * that will be merged into the config.
  102. *
  103. * @param {object | function} fn
  104. */
  105. configureWebpack (fn) {
  106. this.service.webpackRawConfigFns.push(fn)
  107. }
  108. /**
  109. * Register a dev serve config function. It will receive the express `app`
  110. * instance of the dev server.
  111. *
  112. * @param {function} fn
  113. */
  114. configureDevServer (fn) {
  115. this.service.devServerConfigFns.push(fn)
  116. }
  117. /**
  118. * Resolve the final raw webpack config, that will be passed to webpack.
  119. *
  120. * @param {ChainableWebpackConfig} [chainableConfig]
  121. * @return {object} Raw webpack config.
  122. */
  123. resolveWebpackConfig (chainableConfig) {
  124. return this.service.resolveWebpackConfig(chainableConfig)
  125. }
  126. /**
  127. * Resolve an intermediate chainable webpack config instance, which can be
  128. * further tweaked before generating the final raw webpack config.
  129. * You can call this multiple times to generate different branches of the
  130. * base webpack config.
  131. * See https://github.com/mozilla-neutrino/webpack-chain
  132. *
  133. * @return {ChainableWebpackConfig}
  134. */
  135. resolveChainableWebpackConfig () {
  136. return this.service.resolveChainableWebpackConfig()
  137. }
  138. /**
  139. * Generate a cache identifier from a number of variables
  140. */
  141. genCacheConfig (id, partialIdentifier, configFiles = []) {
  142. const fs = require('fs')
  143. const cacheDirectory = this.resolve(`node_modules/.cache/${id}`)
  144. // replace \r\n to \n generate consistent hash
  145. const fmtFunc = conf => {
  146. if (typeof conf === 'function') {
  147. return conf.toString().replace(/\r\n?/g, '\n')
  148. }
  149. return conf
  150. }
  151. const variables = {
  152. partialIdentifier,
  153. 'cli-service': require('../package.json').version,
  154. 'cache-loader': require('cache-loader/package.json').version,
  155. env: process.env.NODE_ENV,
  156. test: !!process.env.VUE_CLI_TEST,
  157. config: [
  158. fmtFunc(this.service.projectOptions.chainWebpack),
  159. fmtFunc(this.service.projectOptions.configureWebpack)
  160. ]
  161. }
  162. if (!Array.isArray(configFiles)) {
  163. configFiles = [configFiles]
  164. }
  165. configFiles = configFiles.concat([
  166. 'package-lock.json',
  167. 'yarn.lock',
  168. 'pnpm-lock.yaml'
  169. ])
  170. const readConfig = file => {
  171. const absolutePath = this.resolve(file)
  172. if (!fs.existsSync(absolutePath)) {
  173. return
  174. }
  175. if (absolutePath.endsWith('.js')) {
  176. // should evaluate config scripts to reflect environment variable changes
  177. try {
  178. return JSON.stringify(require(absolutePath))
  179. } catch (e) {
  180. return fs.readFileSync(absolutePath, 'utf-8')
  181. }
  182. } else {
  183. return fs.readFileSync(absolutePath, 'utf-8')
  184. }
  185. }
  186. for (const file of configFiles) {
  187. const content = readConfig(file)
  188. if (content) {
  189. variables.configFiles = content.replace(/\r\n?/g, '\n')
  190. break
  191. }
  192. }
  193. const cacheIdentifier = hash(variables)
  194. return { cacheDirectory, cacheIdentifier }
  195. }
  196. }
  197. module.exports = PluginAPI