Service.js 13 KB

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