377 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			377 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
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 { warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg, resolveModule, sortPlugins } = require('@vue/cli-shared-utils')
 | 
						|
 | 
						|
const { defaults } = require('./options')
 | 
						|
const loadFileConfig = require('./util/loadFileConfig')
 | 
						|
const resolveUserConfig = require('./util/resolveUserConfig')
 | 
						|
 | 
						|
// Seems we can't use `instanceof Promise` here (would fail the tests)
 | 
						|
const isPromise = p => p && typeof p.then === 'function'
 | 
						|
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()
 | 
						|
    const loadedCallback = (loadedUserOptions) => {
 | 
						|
      this.projectOptions = defaultsDeep(loadedUserOptions, 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)
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (isPromise(userOptions)) {
 | 
						|
      return userOptions.then(loadedCallback)
 | 
						|
    } else {
 | 
						|
      return loadedCallback(userOptions)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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, rawArgv) {
 | 
						|
    let skipPlugins = args['skip-plugins']
 | 
						|
    const pluginsToSkip = new Set()
 | 
						|
    if (skipPlugins) {
 | 
						|
      // When only one appearence, convert to array to prevent duplicate code
 | 
						|
      if (!Array.isArray(skipPlugins)) {
 | 
						|
        skipPlugins = Array.from([skipPlugins])
 | 
						|
      }
 | 
						|
      // Iter over all --skip-plugins appearences
 | 
						|
      for (const value of skipPlugins.values()) {
 | 
						|
        for (const plugin of value.split(',').map(id => resolvePluginId(id))) {
 | 
						|
          pluginsToSkip.add(plugin)
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    this.pluginsToSkip = pluginsToSkip
 | 
						|
 | 
						|
    delete args['skip-plugins']
 | 
						|
    // Delete all --skip-plugin appearences
 | 
						|
    let index
 | 
						|
    while ((index = rawArgv.indexOf('--skip-plugins')) > -1) {
 | 
						|
      rawArgv.splice(index, 2) // Remove the argument and its value
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  resolvePlugins (inlinePlugins, useBuiltIn) {
 | 
						|
    const idToPlugin = (id, absolutePath) => ({
 | 
						|
      id: id.replace(/^.\//, 'built-in:'),
 | 
						|
      apply: require(absolutePath || id)
 | 
						|
    })
 | 
						|
 | 
						|
    let plugins
 | 
						|
 | 
						|
    const builtInPlugins = [
 | 
						|
      './commands/serve',
 | 
						|
      './commands/build',
 | 
						|
      './commands/inspect',
 | 
						|
      './commands/help',
 | 
						|
      // config plugins are order sensitive
 | 
						|
      './config/base',
 | 
						|
      './config/assets',
 | 
						|
      './config/css',
 | 
						|
      './config/prod',
 | 
						|
      './config/app'
 | 
						|
    ].map((id) => idToPlugin(id))
 | 
						|
 | 
						|
    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 = loadModule(id, this.pkgContext)
 | 
						|
            if (!apply) {
 | 
						|
              warn(`Optional dependency ${id} is not installed.`)
 | 
						|
              apply = () => {}
 | 
						|
            }
 | 
						|
 | 
						|
            return { id, apply }
 | 
						|
          } else {
 | 
						|
            return idToPlugin(id, resolveModule(id, this.pkgContext))
 | 
						|
          }
 | 
						|
        })
 | 
						|
 | 
						|
      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)
 | 
						|
      })))
 | 
						|
    }
 | 
						|
    debug('vue:plugins')(plugins)
 | 
						|
 | 
						|
    const orderedPlugins = sortPlugins(plugins)
 | 
						|
    debug('vue:plugins-ordered')(orderedPlugins)
 | 
						|
 | 
						|
    return orderedPlugins
 | 
						|
  }
 | 
						|
 | 
						|
  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, rawArgv)
 | 
						|
 | 
						|
    // load env variables, load user config, apply plugins
 | 
						|
    await 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
 | 
						|
  }
 | 
						|
 | 
						|
  // Note: we intentionally make this function synchronous by default
 | 
						|
  // because eslint-import-resolver-webpack does not support async webpack configs.
 | 
						|
  loadUserOptions () {
 | 
						|
    const { fileConfig, fileConfigPath } = loadFileConfig(this.context)
 | 
						|
 | 
						|
    if (isPromise(fileConfig)) {
 | 
						|
      return fileConfig
 | 
						|
        .then(mod => mod.default)
 | 
						|
        .then(loadedConfig => resolveUserConfig({
 | 
						|
          inlineOptions: this.inlineOptions,
 | 
						|
          pkgConfig: this.pkg.vue,
 | 
						|
          fileConfig: loadedConfig,
 | 
						|
          fileConfigPath
 | 
						|
        }))
 | 
						|
    }
 | 
						|
 | 
						|
    return resolveUserConfig({
 | 
						|
      inlineOptions: this.inlineOptions,
 | 
						|
      pkgConfig: this.pkg.vue,
 | 
						|
      fileConfig,
 | 
						|
      fileConfigPath
 | 
						|
    })
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
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)
 | 
						|
    }
 | 
						|
  })
 | 
						|
}
 | 
						|
 | 
						|
/** @type {import('../types/index').defineConfig} */
 | 
						|
module.exports.defineConfig = (config) => config
 |