UNPKG

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