You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
376 lines
12 KiB
376 lines
12 KiB
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
|
|
|