Service.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. const path = require('path')
  2. const debug = require('debug')
  3. const { merge } = require('webpack-merge')
  4. const Config = require('webpack-chain')
  5. const PluginAPI = require('./PluginAPI')
  6. const dotenv = require('dotenv')
  7. const dotenvExpand = require('dotenv-expand')
  8. const defaultsDeep = require('lodash.defaultsdeep')
  9. const { warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg, resolveModule, sortPlugins } = require('@vue/cli-shared-utils')
  10. const { defaults } = require('./options')
  11. const loadFileConfig = require('./util/loadFileConfig')
  12. const resolveUserConfig = require('./util/resolveUserConfig')
  13. // Seems we can't use `instanceof Promise` here (would fail the tests)
  14. const isPromise = p => p && typeof p.then === 'function'
  15. module.exports = class Service {
  16. constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
  17. process.VUE_CLI_SERVICE = this
  18. this.initialized = false
  19. this.context = context
  20. this.inlineOptions = inlineOptions
  21. this.webpackChainFns = []
  22. this.webpackRawConfigFns = []
  23. this.devServerConfigFns = []
  24. this.commands = {}
  25. // Folder containing the target package.json for plugins
  26. this.pkgContext = context
  27. // package.json containing the plugins
  28. this.pkg = this.resolvePkg(pkg)
  29. // If there are inline plugins, they will be used instead of those
  30. // found in package.json.
  31. // When useBuiltIn === false, built-in plugins are disabled. This is mostly
  32. // for testing.
  33. this.plugins = this.resolvePlugins(plugins, useBuiltIn)
  34. // pluginsToSkip will be populated during run()
  35. this.pluginsToSkip = new Set()
  36. // resolve the default mode to use for each command
  37. // this is provided by plugins as module.exports.defaultModes
  38. // so we can get the information without actually applying the plugin.
  39. this.modes = this.plugins.reduce((modes, { apply: { defaultModes } }) => {
  40. return Object.assign(modes, defaultModes)
  41. }, {})
  42. }
  43. resolvePkg (inlinePkg, context = this.context) {
  44. if (inlinePkg) {
  45. return inlinePkg
  46. }
  47. const pkg = resolvePkg(context)
  48. if (pkg.vuePlugins && pkg.vuePlugins.resolveFrom) {
  49. this.pkgContext = path.resolve(context, pkg.vuePlugins.resolveFrom)
  50. return this.resolvePkg(null, this.pkgContext)
  51. }
  52. return pkg
  53. }
  54. init (mode = process.env.VUE_CLI_MODE) {
  55. if (this.initialized) {
  56. return
  57. }
  58. this.initialized = true
  59. this.mode = mode
  60. // load mode .env
  61. if (mode) {
  62. this.loadEnv(mode)
  63. }
  64. // load base .env
  65. this.loadEnv()
  66. // load user config
  67. const userOptions = this.loadUserOptions()
  68. const loadedCallback = (loadedUserOptions) => {
  69. this.projectOptions = defaultsDeep(loadedUserOptions, defaults())
  70. debug('vue:project-config')(this.projectOptions)
  71. // apply plugins.
  72. this.plugins.forEach(({ id, apply }) => {
  73. if (this.pluginsToSkip.has(id)) return
  74. apply(new PluginAPI(id, this), this.projectOptions)
  75. })
  76. // apply webpack configs from project config file
  77. if (this.projectOptions.chainWebpack) {
  78. this.webpackChainFns.push(this.projectOptions.chainWebpack)
  79. }
  80. if (this.projectOptions.configureWebpack) {
  81. this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
  82. }
  83. }
  84. if (isPromise(userOptions)) {
  85. return userOptions.then(loadedCallback)
  86. } else {
  87. return loadedCallback(userOptions)
  88. }
  89. }
  90. loadEnv (mode) {
  91. const logger = debug('vue:env')
  92. const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`)
  93. const localPath = `${basePath}.local`
  94. const load = envPath => {
  95. try {
  96. const env = dotenv.config({ path: envPath, debug: process.env.DEBUG })
  97. dotenvExpand(env)
  98. logger(envPath, env)
  99. } catch (err) {
  100. // only ignore error if file is not found
  101. if (err.toString().indexOf('ENOENT') < 0) {
  102. error(err)
  103. }
  104. }
  105. }
  106. load(localPath)
  107. load(basePath)
  108. // by default, NODE_ENV and BABEL_ENV are set to "development" unless mode
  109. // is production or test. However the value in .env files will take higher
  110. // priority.
  111. if (mode) {
  112. // always set NODE_ENV during tests
  113. // as that is necessary for tests to not be affected by each other
  114. const shouldForceDefaultEnv = (
  115. process.env.VUE_CLI_TEST &&
  116. !process.env.VUE_CLI_TEST_TESTING_ENV
  117. )
  118. const defaultNodeEnv = (mode === 'production' || mode === 'test')
  119. ? mode
  120. : 'development'
  121. if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
  122. process.env.NODE_ENV = defaultNodeEnv
  123. }
  124. if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
  125. process.env.BABEL_ENV = defaultNodeEnv
  126. }
  127. }
  128. }
  129. setPluginsToSkip (args, rawArgv) {
  130. let skipPlugins = args['skip-plugins']
  131. const pluginsToSkip = new Set()
  132. if (skipPlugins) {
  133. // When only one appearence, convert to array to prevent duplicate code
  134. if (!Array.isArray(skipPlugins)) {
  135. skipPlugins = Array.from([skipPlugins])
  136. }
  137. // Iter over all --skip-plugins appearences
  138. for (const value of skipPlugins.values()) {
  139. for (const plugin of value.split(',').map(id => resolvePluginId(id))) {
  140. pluginsToSkip.add(plugin)
  141. }
  142. }
  143. }
  144. this.pluginsToSkip = pluginsToSkip
  145. delete args['skip-plugins']
  146. // Delete all --skip-plugin appearences
  147. let index
  148. while ((index = rawArgv.indexOf('--skip-plugins')) > -1) {
  149. rawArgv.splice(index, 2) // Remove the argument and its value
  150. }
  151. }
  152. resolvePlugins (inlinePlugins, useBuiltIn) {
  153. const idToPlugin = (id, absolutePath) => ({
  154. id: id.replace(/^.\//, 'built-in:'),
  155. apply: require(absolutePath || id)
  156. })
  157. let plugins
  158. const builtInPlugins = [
  159. './commands/serve',
  160. './commands/build',
  161. './commands/inspect',
  162. './commands/help',
  163. // config plugins are order sensitive
  164. './config/base',
  165. './config/assets',
  166. './config/css',
  167. './config/prod',
  168. './config/app'
  169. ].map((id) => idToPlugin(id))
  170. if (inlinePlugins) {
  171. plugins = useBuiltIn !== false
  172. ? builtInPlugins.concat(inlinePlugins)
  173. : inlinePlugins
  174. } else {
  175. const projectPlugins = Object.keys(this.pkg.devDependencies || {})
  176. .concat(Object.keys(this.pkg.dependencies || {}))
  177. .filter(isPlugin)
  178. .map(id => {
  179. if (
  180. this.pkg.optionalDependencies &&
  181. id in this.pkg.optionalDependencies
  182. ) {
  183. let apply = loadModule(id, this.pkgContext)
  184. if (!apply) {
  185. warn(`Optional dependency ${id} is not installed.`)
  186. apply = () => {}
  187. }
  188. return { id, apply }
  189. } else {
  190. return idToPlugin(id, resolveModule(id, this.pkgContext))
  191. }
  192. })
  193. plugins = builtInPlugins.concat(projectPlugins)
  194. }
  195. // Local plugins
  196. if (this.pkg.vuePlugins && this.pkg.vuePlugins.service) {
  197. const files = this.pkg.vuePlugins.service
  198. if (!Array.isArray(files)) {
  199. throw new Error(`Invalid type for option 'vuePlugins.service', expected 'array' but got ${typeof files}.`)
  200. }
  201. plugins = plugins.concat(files.map(file => ({
  202. id: `local:${file}`,
  203. apply: loadModule(`./${file}`, this.pkgContext)
  204. })))
  205. }
  206. debug('vue:plugins')(plugins)
  207. const orderedPlugins = sortPlugins(plugins)
  208. debug('vue:plugins-ordered')(orderedPlugins)
  209. return orderedPlugins
  210. }
  211. async run (name, args = {}, rawArgv = []) {
  212. // resolve mode
  213. // prioritize inline --mode
  214. // fallback to resolved default modes from plugins or development if --watch is defined
  215. const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
  216. // --skip-plugins arg may have plugins that should be skipped during init()
  217. this.setPluginsToSkip(args, rawArgv)
  218. // load env variables, load user config, apply plugins
  219. await this.init(mode)
  220. args._ = args._ || []
  221. let command = this.commands[name]
  222. if (!command && name) {
  223. error(`command "${name}" does not exist.`)
  224. process.exit(1)
  225. }
  226. if (!command || args.help || args.h) {
  227. command = this.commands.help
  228. } else {
  229. args._.shift() // remove command itself
  230. rawArgv.shift()
  231. }
  232. const { fn } = command
  233. return fn(args, rawArgv)
  234. }
  235. resolveChainableWebpackConfig () {
  236. const chainableConfig = new Config()
  237. // apply chains
  238. this.webpackChainFns.forEach(fn => fn(chainableConfig))
  239. return chainableConfig
  240. }
  241. resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {
  242. if (!this.initialized) {
  243. throw new Error('Service must call init() before calling resolveWebpackConfig().')
  244. }
  245. // get raw config
  246. let config = chainableConfig.toConfig()
  247. const original = config
  248. // apply raw config fns
  249. this.webpackRawConfigFns.forEach(fn => {
  250. if (typeof fn === 'function') {
  251. // function with optional return value
  252. const res = fn(config)
  253. if (res) config = merge(config, res)
  254. } else if (fn) {
  255. // merge literal values
  256. config = merge(config, fn)
  257. }
  258. })
  259. // #2206 If config is merged by merge-webpack, it discards the __ruleNames
  260. // information injected by webpack-chain. Restore the info so that
  261. // vue inspect works properly.
  262. if (config !== original) {
  263. cloneRuleNames(
  264. config.module && config.module.rules,
  265. original.module && original.module.rules
  266. )
  267. }
  268. // check if the user has manually mutated output.publicPath
  269. const target = process.env.VUE_CLI_BUILD_TARGET
  270. if (
  271. !process.env.VUE_CLI_TEST &&
  272. (target && target !== 'app') &&
  273. config.output.publicPath !== this.projectOptions.publicPath
  274. ) {
  275. throw new Error(
  276. `Do not modify webpack output.publicPath directly. ` +
  277. `Use the "publicPath" option in vue.config.js instead.`
  278. )
  279. }
  280. if (
  281. !process.env.VUE_CLI_ENTRY_FILES &&
  282. typeof config.entry !== 'function'
  283. ) {
  284. let entryFiles
  285. if (typeof config.entry === 'string') {
  286. entryFiles = [config.entry]
  287. } else if (Array.isArray(config.entry)) {
  288. entryFiles = config.entry
  289. } else {
  290. entryFiles = Object.values(config.entry || []).reduce((allEntries, curr) => {
  291. return allEntries.concat(curr)
  292. }, [])
  293. }
  294. entryFiles = entryFiles.map(file => path.resolve(this.context, file))
  295. process.env.VUE_CLI_ENTRY_FILES = JSON.stringify(entryFiles)
  296. }
  297. return config
  298. }
  299. // Note: we intentionally make this function synchronous by default
  300. // because eslint-import-resolver-webpack does not support async webpack configs.
  301. loadUserOptions () {
  302. const { fileConfig, fileConfigPath } = loadFileConfig(this.context)
  303. if (isPromise(fileConfig)) {
  304. return fileConfig
  305. .then(mod => mod.default)
  306. .then(loadedConfig => resolveUserConfig({
  307. inlineOptions: this.inlineOptions,
  308. pkgConfig: this.pkg.vue,
  309. fileConfig: loadedConfig,
  310. fileConfigPath
  311. }))
  312. }
  313. return resolveUserConfig({
  314. inlineOptions: this.inlineOptions,
  315. pkgConfig: this.pkg.vue,
  316. fileConfig,
  317. fileConfigPath
  318. })
  319. }
  320. }
  321. function cloneRuleNames (to, from) {
  322. if (!to || !from) {
  323. return
  324. }
  325. from.forEach((r, i) => {
  326. if (to[i]) {
  327. Object.defineProperty(to[i], '__ruleNames', {
  328. value: r.__ruleNames
  329. })
  330. cloneRuleNames(to[i].oneOf, r.oneOf)
  331. }
  332. })
  333. }
  334. /** @type {import('../types/index').defineConfig} */
  335. module.exports.defineConfig = (config) => config