123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- const fs = require('fs')
- const path = require('path')
- const debug = require('debug')
- const chalk = require('chalk')
- const readPkg = require('read-pkg')
- 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 { warn, error, isPlugin, loadModule } = 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)
- // 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
- } else if (fs.existsSync(path.join(context, 'package.json'))) {
- const pkg = readPkg.sync({ cwd: context })
- if (pkg.vuePlugins && pkg.vuePlugins.resolveFrom) {
- this.pkgContext = path.resolve(context, pkg.vuePlugins.resolveFrom)
- return this.resolvePkg(null, this.pkgContext)
- }
- return pkg
- } else {
- return {}
- }
- }
- 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 }) => {
- 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 = path => {
- try {
- const env = dotenv.config({ path, debug: process.env.DEBUG })
- dotenvExpand(env)
- logger(path, 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
- }
- }
- }
- 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/dev',
- './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])
- // 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 (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.js
- let fileConfig, pkgConfig, resolved, resolvedFrom
- const configPath = (
- process.env.VUE_CLI_SERVICE_CONFIG_PATH ||
- path.resolve(this.context, 'vue.config.js')
- )
- if (fs.existsSync(configPath)) {
- try {
- fileConfig = require(configPath)
- if (typeof fileConfig === 'function') {
- fileConfig = fileConfig()
- }
- if (!fileConfig || typeof fileConfig !== 'object') {
- error(
- `Error loading ${chalk.bold('vue.config.js')}: should export an object or a function that returns object.`
- )
- fileConfig = null
- }
- } catch (e) {
- error(`Error loading ${chalk.bold('vue.config.js')}:`)
- 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 (typeof resolved.baseUrl !== 'undefined') {
- if (typeof resolved.publicPath !== 'undefined') {
- warn(
- `You have set both "baseUrl" and "publicPath" in ${chalk.bold('vue.config.js')}, ` +
- `in this case, "baseUrl" will be ignored in favor of "publicPath".`
- )
- } else {
- warn(
- `"baseUrl" option in ${chalk.bold('vue.config.js')} ` +
- `is deprecated now, please use "publicPath" instead.`
- )
- resolved.publicPath = resolved.baseUrl
- }
- }
- // normalize some options
- ensureSlash(resolved, 'publicPath')
- if (typeof resolved.publicPath === 'string') {
- resolved.publicPath = resolved.publicPath.replace(/^\.\//, '')
- }
- // for compatibility concern, in case some plugins still rely on `baseUrl` option
- resolved.baseUrl = resolved.publicPath
- removeSlash(resolved, 'outputDir')
- // deprecation warning
- // TODO remove in final release
- if (resolved.css && resolved.css.localIdentName) {
- warn(
- `css.localIdentName has been deprecated. ` +
- `All css-loader options (except "modules") are now supported via css.loaderOptions.css.`
- )
- }
- // validate options
- validate(resolved, msg => {
- error(
- `Invalid options in ${chalk.bold(resolvedFrom)}: ${msg}`
- )
- })
- return resolved
- }
- }
- function ensureSlash (config, key) {
- let val = config[key]
- if (typeof val === 'string') {
- if (!/^https?:/.test(val)) {
- val = val.replace(/^([^/.])/, '/$1')
- }
- 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)
- }
- })
- }
|