Service.js 12 KB

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