Files
vue-cli/packages/@vue/cli-service/lib/Service.js
Haoqun Jiang e41ef41540 chore!: drop webpack-4 support in v5 (#6598)
1. I don't think I'll ever have the capacity to keep maintaining this
compat plugin. It's better to drop it now than later.
2. The plugin is needed at the time of alpha.0 because many ecosystem
plugins does not support webpack 5. But now I don't think it will be a
major hurdle.
3. The implementation was too hacky.
2021-07-30 14:03:54 +08:00

361 lines
11 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) {
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))
}
})
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)
// 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