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 |
|
12 | const { defaults, validate } = require('./options')
|
13 |
|
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 |
|
25 | this.pkgContext = context
|
26 |
|
27 | this.pkg = this.resolvePkg(pkg)
|
28 |
|
29 |
|
30 |
|
31 |
|
32 | this.plugins = this.resolvePlugins(plugins, useBuiltIn)
|
33 |
|
34 | this.pluginsToSkip = new Set()
|
35 |
|
36 |
|
37 |
|
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 |
|
63 | if (mode) {
|
64 | this.loadEnv(mode)
|
65 | }
|
66 |
|
67 | this.loadEnv()
|
68 |
|
69 |
|
70 | const userOptions = this.loadUserOptions()
|
71 | this.projectOptions = defaultsDeep(userOptions, defaults())
|
72 |
|
73 | debug('vue:project-config')(this.projectOptions)
|
74 |
|
75 |
|
76 | this.plugins.forEach(({ id, apply }) => {
|
77 | if (this.pluginsToSkip.has(id)) return
|
78 | apply(new PluginAPI(id, this), this.projectOptions)
|
79 | })
|
80 |
|
81 |
|
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 |
|
102 | if (err.toString().indexOf('ENOENT') < 0) {
|
103 | error(err)
|
104 | }
|
105 | }
|
106 | }
|
107 |
|
108 | load(localPath)
|
109 | load(basePath)
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | if (mode) {
|
115 |
|
116 |
|
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 |
|
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 |
|
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 |
|
207 |
|
208 |
|
209 | const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
|
210 |
|
211 |
|
212 | this.setPluginsToSkip(args)
|
213 |
|
214 |
|
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()
|
227 | rawArgv.shift()
|
228 | }
|
229 | const { fn } = command
|
230 | return fn(args, rawArgv)
|
231 | }
|
232 |
|
233 | resolveChainableWebpackConfig () {
|
234 | const chainableConfig = new Config()
|
235 |
|
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 |
|
245 | let config = chainableConfig.toConfig()
|
246 | const original = config
|
247 |
|
248 | this.webpackRawConfigFns.forEach(fn => {
|
249 | if (typeof fn === 'function') {
|
250 |
|
251 | const res = fn(config)
|
252 | if (res) config = merge(config, res)
|
253 | } else if (fn) {
|
254 |
|
255 | config = merge(config, fn)
|
256 | }
|
257 | })
|
258 |
|
259 |
|
260 |
|
261 |
|
262 | if (config !== original) {
|
263 | cloneRuleNames(
|
264 | config.module && config.module.rules,
|
265 | original.module && original.module.rules
|
266 | )
|
267 | }
|
268 |
|
269 |
|
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 |
|
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 |
|
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 |
|
375 | ensureSlash(resolved, 'publicPath')
|
376 | if (typeof resolved.publicPath === 'string') {
|
377 | resolved.publicPath = resolved.publicPath.replace(/^\.\//, '')
|
378 | }
|
379 | removeSlash(resolved, 'outputDir')
|
380 |
|
381 |
|
382 | validate(resolved, msg => {
|
383 | error(
|
384 | `Invalid options in ${chalk.bold(resolvedFrom)}: ${msg}`
|
385 | )
|
386 | })
|
387 |
|
388 | return resolved
|
389 | }
|
390 | }
|
391 |
|
392 | function ensureSlash (config, key) {
|
393 | const val = config[key]
|
394 | if (typeof val === 'string') {
|
395 | config[key] = val.replace(/([^/])$/, '$1/')
|
396 | }
|
397 | }
|
398 |
|
399 | function removeSlash (config, key) {
|
400 | if (typeof config[key] === 'string') {
|
401 | config[key] = config[key].replace(/\/$/g, '')
|
402 | }
|
403 | }
|
404 |
|
405 | function 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 | }
|