Service.js 13 KB

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