text: 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, resolveModule } = require('@vue/cli-shared-utils')
  
  const { defaults, validate } = require('./options')
  const checkWebpack = require('@vue/cli-service/lib/util/checkWebpack')
  
  module.exports = class Service {
    constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
      checkWebpack(context)
  
      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, 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))
            }
          })
  
        // Add the plugin automatically to simplify the webpack-4 tests
        // so that a simple Jest alias would suffice, avoid changing every
        // preset used in the tests
        if (
          process.env.VUE_CLI_TEST &&
          process.env.VUE_CLI_USE_WEBPACK4 &&
          !projectPlugins.some((p) => p.id === '@vue/cli-plugin-webpack-4')
        ) {
          builtInPlugins.push(idToPlugin('@vue/cli-plugin-webpack-4'))
        }
  
        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'
      }
  
      // 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)
      }
    })
  }
  
  /** @type {import('../types/index').defineConfig} */
  module.exports.defineConfig = (config) => config
  