439 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			439 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const fs = require('fs')
 | |
| const path = require('path')
 | |
| const debug = require('debug')
 | |
| const merge = require('webpack-merge')
 | |
| const Config = require('webpack-chain')
 | |
| const PluginAPI = require('./PluginAPI')
 | |
| const dotenv = require('dotenv')
 | |
| const dotenvExpand = require('dotenv-expand')
 | |
| const defaultsDeep = require('lodash.defaultsdeep')
 | |
| const { chalk, warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg } = require('@vue/cli-shared-utils')
 | |
| 
 | |
| const { defaults, validate } = require('./options')
 | |
| 
 | |
| module.exports = class Service {
 | |
|   constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
 | |
|     process.VUE_CLI_SERVICE = this
 | |
|     this.initialized = false
 | |
|     this.context = context
 | |
|     this.inlineOptions = inlineOptions
 | |
|     this.webpackChainFns = []
 | |
|     this.webpackRawConfigFns = []
 | |
|     this.devServerConfigFns = []
 | |
|     this.commands = {}
 | |
|     // Folder containing the target package.json for plugins
 | |
|     this.pkgContext = context
 | |
|     // package.json containing the plugins
 | |
|     this.pkg = this.resolvePkg(pkg)
 | |
|     // If there are inline plugins, they will be used instead of those
 | |
|     // found in package.json.
 | |
|     // When useBuiltIn === false, built-in plugins are disabled. This is mostly
 | |
|     // for testing.
 | |
|     this.plugins = this.resolvePlugins(plugins, useBuiltIn)
 | |
|     // pluginsToSkip will be populated during run()
 | |
|     this.pluginsToSkip = new Set()
 | |
|     // resolve the default mode to use for each command
 | |
|     // this is provided by plugins as module.exports.defaultModes
 | |
|     // so we can get the information without actually applying the plugin.
 | |
|     this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
 | |
|       return Object.assign(modes, defaultModes)
 | |
|     }, {})
 | |
|   }
 | |
| 
 | |
|   resolvePkg (inlinePkg, context = this.context) {
 | |
|     if (inlinePkg) {
 | |
|       return inlinePkg
 | |
|     }
 | |
|     const pkg = resolvePkg(context)
 | |
|     if (pkg.vuePlugins && pkg.vuePlugins.resolveFrom) {
 | |
|       this.pkgContext = path.resolve(context, pkg.vuePlugins.resolveFrom)
 | |
|       return this.resolvePkg(null, this.pkgContext)
 | |
|     }
 | |
|     return pkg
 | |
|   }
 | |
| 
 | |
|   init (mode = process.env.VUE_CLI_MODE) {
 | |
|     if (this.initialized) {
 | |
|       return
 | |
|     }
 | |
|     this.initialized = true
 | |
|     this.mode = mode
 | |
| 
 | |
|     // load mode .env
 | |
|     if (mode) {
 | |
|       this.loadEnv(mode)
 | |
|     }
 | |
|     // load base .env
 | |
|     this.loadEnv()
 | |
| 
 | |
|     // load user config
 | |
|     const userOptions = this.loadUserOptions()
 | |
|     this.projectOptions = defaultsDeep(userOptions, defaults())
 | |
| 
 | |
|     debug('vue:project-config')(this.projectOptions)
 | |
| 
 | |
|     // apply plugins.
 | |
|     this.plugins.forEach(({ id, apply }) => {
 | |
|       if (this.pluginsToSkip.has(id)) return
 | |
|       apply(new PluginAPI(id, this), this.projectOptions)
 | |
|     })
 | |
| 
 | |
|     // apply webpack configs from project config file
 | |
|     if (this.projectOptions.chainWebpack) {
 | |
|       this.webpackChainFns.push(this.projectOptions.chainWebpack)
 | |
|     }
 | |
|     if (this.projectOptions.configureWebpack) {
 | |
|       this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   loadEnv (mode) {
 | |
|     const logger = debug('vue:env')
 | |
|     const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`)
 | |
|     const localPath = `${basePath}.local`
 | |
| 
 | |
|     const load = envPath => {
 | |
|       try {
 | |
|         const env = dotenv.config({ path: envPath, debug: process.env.DEBUG })
 | |
|         dotenvExpand(env)
 | |
|         logger(envPath, env)
 | |
|       } catch (err) {
 | |
|         // only ignore error if file is not found
 | |
|         if (err.toString().indexOf('ENOENT') < 0) {
 | |
|           error(err)
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     load(localPath)
 | |
|     load(basePath)
 | |
| 
 | |
|     // by default, NODE_ENV and BABEL_ENV are set to "development" unless mode
 | |
|     // is production or test. However the value in .env files will take higher
 | |
|     // priority.
 | |
|     if (mode) {
 | |
|       // always set NODE_ENV during tests
 | |
|       // as that is necessary for tests to not be affected by each other
 | |
|       const shouldForceDefaultEnv = (
 | |
|         process.env.VUE_CLI_TEST &&
 | |
|         !process.env.VUE_CLI_TEST_TESTING_ENV
 | |
|       )
 | |
|       const defaultNodeEnv = (mode === 'production' || mode === 'test')
 | |
|         ? mode
 | |
|         : 'development'
 | |
|       if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
 | |
|         process.env.NODE_ENV = defaultNodeEnv
 | |
|       }
 | |
|       if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
 | |
|         process.env.BABEL_ENV = defaultNodeEnv
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   setPluginsToSkip (args) {
 | |
|     const skipPlugins = args['skip-plugins']
 | |
|     const pluginsToSkip = skipPlugins
 | |
|       ? new Set(skipPlugins.split(',').map(id => resolvePluginId(id)))
 | |
|       : new Set()
 | |
| 
 | |
|     this.pluginsToSkip = pluginsToSkip
 | |
|   }
 | |
| 
 | |
|   resolvePlugins (inlinePlugins, useBuiltIn) {
 | |
|     const idToPlugin = id => ({
 | |
|       id: id.replace(/^.\//, 'built-in:'),
 | |
|       apply: require(id)
 | |
|     })
 | |
| 
 | |
|     let plugins
 | |
| 
 | |
|     const builtInPlugins = [
 | |
|       './commands/serve',
 | |
|       './commands/build',
 | |
|       './commands/inspect',
 | |
|       './commands/help',
 | |
|       // config plugins are order sensitive
 | |
|       './config/base',
 | |
|       './config/css',
 | |
|       './config/prod',
 | |
|       './config/app'
 | |
|     ].map(idToPlugin)
 | |
| 
 | |
|     if (inlinePlugins) {
 | |
|       plugins = useBuiltIn !== false
 | |
|         ? builtInPlugins.concat(inlinePlugins)
 | |
|         : inlinePlugins
 | |
|     } else {
 | |
|       const projectPlugins = Object.keys(this.pkg.devDependencies || {})
 | |
|         .concat(Object.keys(this.pkg.dependencies || {}))
 | |
|         .filter(isPlugin)
 | |
|         .map(id => {
 | |
|           if (
 | |
|             this.pkg.optionalDependencies &&
 | |
|             id in this.pkg.optionalDependencies
 | |
|           ) {
 | |
|             let apply = () => {}
 | |
|             try {
 | |
|               apply = require(id)
 | |
|             } catch (e) {
 | |
|               warn(`Optional dependency ${id} is not installed.`)
 | |
|             }
 | |
| 
 | |
|             return { id, apply }
 | |
|           } else {
 | |
|             return idToPlugin(id)
 | |
|           }
 | |
|         })
 | |
|       plugins = builtInPlugins.concat(projectPlugins)
 | |
|     }
 | |
| 
 | |
|     // Local plugins
 | |
|     if (this.pkg.vuePlugins && this.pkg.vuePlugins.service) {
 | |
|       const files = this.pkg.vuePlugins.service
 | |
|       if (!Array.isArray(files)) {
 | |
|         throw new Error(`Invalid type for option 'vuePlugins.service', expected 'array' but got ${typeof files}.`)
 | |
|       }
 | |
|       plugins = plugins.concat(files.map(file => ({
 | |
|         id: `local:${file}`,
 | |
|         apply: loadModule(`./${file}`, this.pkgContext)
 | |
|       })))
 | |
|     }
 | |
| 
 | |
|     return plugins
 | |
|   }
 | |
| 
 | |
|   async run (name, args = {}, rawArgv = []) {
 | |
|     // resolve mode
 | |
|     // prioritize inline --mode
 | |
|     // fallback to resolved default modes from plugins or development if --watch is defined
 | |
|     const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
 | |
| 
 | |
|     // --skip-plugins arg may have plugins that should be skipped during init()
 | |
|     this.setPluginsToSkip(args)
 | |
| 
 | |
|     // load env variables, load user config, apply plugins
 | |
|     this.init(mode)
 | |
| 
 | |
|     args._ = args._ || []
 | |
|     let command = this.commands[name]
 | |
|     if (!command && name) {
 | |
|       error(`command "${name}" does not exist.`)
 | |
|       process.exit(1)
 | |
|     }
 | |
|     if (!command || args.help || args.h) {
 | |
|       command = this.commands.help
 | |
|     } else {
 | |
|       args._.shift() // remove command itself
 | |
|       rawArgv.shift()
 | |
|     }
 | |
|     const { fn } = command
 | |
|     return fn(args, rawArgv)
 | |
|   }
 | |
| 
 | |
|   resolveChainableWebpackConfig () {
 | |
|     const chainableConfig = new Config()
 | |
|     // apply chains
 | |
|     this.webpackChainFns.forEach(fn => fn(chainableConfig))
 | |
|     return chainableConfig
 | |
|   }
 | |
| 
 | |
|   resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {
 | |
|     if (!this.initialized) {
 | |
|       throw new Error('Service must call init() before calling resolveWebpackConfig().')
 | |
|     }
 | |
|     // get raw config
 | |
|     let config = chainableConfig.toConfig()
 | |
|     const original = config
 | |
|     // apply raw config fns
 | |
|     this.webpackRawConfigFns.forEach(fn => {
 | |
|       if (typeof fn === 'function') {
 | |
|         // function with optional return value
 | |
|         const res = fn(config)
 | |
|         if (res) config = merge(config, res)
 | |
|       } else if (fn) {
 | |
|         // merge literal values
 | |
|         config = merge(config, fn)
 | |
|       }
 | |
|     })
 | |
| 
 | |
|     // #2206 If config is merged by merge-webpack, it discards the __ruleNames
 | |
|     // information injected by webpack-chain. Restore the info so that
 | |
|     // vue inspect works properly.
 | |
|     if (config !== original) {
 | |
|       cloneRuleNames(
 | |
|         config.module && config.module.rules,
 | |
|         original.module && original.module.rules
 | |
|       )
 | |
|     }
 | |
| 
 | |
|     // check if the user has manually mutated output.publicPath
 | |
|     const target = process.env.VUE_CLI_BUILD_TARGET
 | |
|     if (
 | |
|       !process.env.VUE_CLI_TEST &&
 | |
|       (target && target !== 'app') &&
 | |
|       config.output.publicPath !== this.projectOptions.publicPath
 | |
|     ) {
 | |
|       throw new Error(
 | |
|         `Do not modify webpack output.publicPath directly. ` +
 | |
|         `Use the "publicPath" option in vue.config.js instead.`
 | |
|       )
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       !process.env.VUE_CLI_ENTRY_FILES &&
 | |
|       typeof config.entry !== 'function'
 | |
|     ) {
 | |
|       let entryFiles
 | |
|       if (typeof config.entry === 'string') {
 | |
|         entryFiles = [config.entry]
 | |
|       } else if (Array.isArray(config.entry)) {
 | |
|         entryFiles = config.entry
 | |
|       } else {
 | |
|         entryFiles = Object.values(config.entry || []).reduce((allEntries, curr) => {
 | |
|           return allEntries.concat(curr)
 | |
|         }, [])
 | |
|       }
 | |
| 
 | |
|       entryFiles = entryFiles.map(file => path.resolve(this.context, file))
 | |
|       process.env.VUE_CLI_ENTRY_FILES = JSON.stringify(entryFiles)
 | |
|     }
 | |
| 
 | |
|     return config
 | |
|   }
 | |
| 
 | |
|   loadUserOptions () {
 | |
|     // vue.config.c?js
 | |
|     let fileConfig, pkgConfig, resolved, resolvedFrom
 | |
|     const esm = this.pkg.type && this.pkg.type === 'module'
 | |
| 
 | |
|     const possibleConfigPaths = [
 | |
|       process.env.VUE_CLI_SERVICE_CONFIG_PATH,
 | |
|       './vue.config.js',
 | |
|       './vue.config.cjs'
 | |
|     ]
 | |
| 
 | |
|     let fileConfigPath
 | |
|     for (const p of possibleConfigPaths) {
 | |
|       const resolvedPath = p && path.resolve(this.context, p)
 | |
|       if (resolvedPath && fs.existsSync(resolvedPath)) {
 | |
|         fileConfigPath = resolvedPath
 | |
|         break
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (fileConfigPath) {
 | |
|       if (esm && fileConfigPath === './vue.config.js') {
 | |
|         throw new Error(`Please rename ${chalk.bold('vue.config.js')} to ${chalk.bold('vue.config.cjs')} when ECMAScript modules is enabled`)
 | |
|       }
 | |
| 
 | |
|       try {
 | |
|         fileConfig = loadModule(fileConfigPath, this.context)
 | |
| 
 | |
|         if (typeof fileConfig === 'function') {
 | |
|           fileConfig = fileConfig()
 | |
|         }
 | |
| 
 | |
|         if (!fileConfig || typeof fileConfig !== 'object') {
 | |
|           // TODO: show throw an Error here, to be fixed in v5
 | |
|           error(
 | |
|             `Error loading ${chalk.bold(fileConfigPath)}: should export an object or a function that returns object.`
 | |
|           )
 | |
|           fileConfig = null
 | |
|         }
 | |
|       } catch (e) {
 | |
|         error(`Error loading ${chalk.bold(fileConfigPath)}:`)
 | |
|         throw e
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // package.vue
 | |
|     pkgConfig = this.pkg.vue
 | |
|     if (pkgConfig && typeof pkgConfig !== 'object') {
 | |
|       error(
 | |
|         `Error loading vue-cli config in ${chalk.bold(`package.json`)}: ` +
 | |
|         `the "vue" field should be an object.`
 | |
|       )
 | |
|       pkgConfig = null
 | |
|     }
 | |
| 
 | |
|     if (fileConfig) {
 | |
|       if (pkgConfig) {
 | |
|         warn(
 | |
|           `"vue" field in package.json ignored ` +
 | |
|           `due to presence of ${chalk.bold('vue.config.js')}.`
 | |
|         )
 | |
|         warn(
 | |
|           `You should migrate it into ${chalk.bold('vue.config.js')} ` +
 | |
|           `and remove it from package.json.`
 | |
|         )
 | |
|       }
 | |
|       resolved = fileConfig
 | |
|       resolvedFrom = 'vue.config.js'
 | |
|     } else if (pkgConfig) {
 | |
|       resolved = pkgConfig
 | |
|       resolvedFrom = '"vue" field in package.json'
 | |
|     } else {
 | |
|       resolved = this.inlineOptions || {}
 | |
|       resolvedFrom = 'inline options'
 | |
|     }
 | |
| 
 | |
|     if (resolved.css && typeof resolved.css.modules !== 'undefined') {
 | |
|       if (typeof resolved.css.requireModuleExtension !== 'undefined') {
 | |
|         warn(
 | |
|           `You have set both "css.modules" and "css.requireModuleExtension" in ${chalk.bold('vue.config.js')}, ` +
 | |
|           `"css.modules" will be ignored in favor of "css.requireModuleExtension".`
 | |
|         )
 | |
|       } else {
 | |
|         warn(
 | |
|           `"css.modules" option in ${chalk.bold('vue.config.js')} ` +
 | |
|           `is deprecated now, please use "css.requireModuleExtension" instead.`
 | |
|         )
 | |
|         resolved.css.requireModuleExtension = !resolved.css.modules
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // normalize some options
 | |
|     ensureSlash(resolved, 'publicPath')
 | |
|     if (typeof resolved.publicPath === 'string') {
 | |
|       resolved.publicPath = resolved.publicPath.replace(/^\.\//, '')
 | |
|     }
 | |
|     removeSlash(resolved, 'outputDir')
 | |
| 
 | |
|     // validate options
 | |
|     validate(resolved, msg => {
 | |
|       error(
 | |
|         `Invalid options in ${chalk.bold(resolvedFrom)}: ${msg}`
 | |
|       )
 | |
|     })
 | |
| 
 | |
|     return resolved
 | |
|   }
 | |
| }
 | |
| 
 | |
| function ensureSlash (config, key) {
 | |
|   const val = config[key]
 | |
|   if (typeof val === 'string') {
 | |
|     config[key] = val.replace(/([^/])$/, '$1/')
 | |
|   }
 | |
| }
 | |
| 
 | |
| function removeSlash (config, key) {
 | |
|   if (typeof config[key] === 'string') {
 | |
|     config[key] = config[key].replace(/\/$/g, '')
 | |
|   }
 | |
| }
 | |
| 
 | |
| function cloneRuleNames (to, from) {
 | |
|   if (!to || !from) {
 | |
|     return
 | |
|   }
 | |
|   from.forEach((r, i) => {
 | |
|     if (to[i]) {
 | |
|       Object.defineProperty(to[i], '__ruleNames', {
 | |
|         value: r.__ruleNames
 | |
|       })
 | |
|       cloneRuleNames(to[i].oneOf, r.oneOf)
 | |
|     }
 | |
|   })
 | |
| }
 | 
