Spaces:
Sleeping
Sleeping
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 } = 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) | |
// 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 => ({ | |
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/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]) | |
// --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' | |
} | |
if (resolved.css && typeof resolved.css.modules !== 'undefined') { | |
if (typeof resolved.css.requireModuleExtension !== 'undefined') { | |
warn( | |
`You have set both "css.modules" and "css.requireModuleExtension" in ${chalk.bold('vue.config.js')}, ` + | |
`"css.modules" will be ignored in favor of "css.requireModuleExtension".` | |
) | |
} else { | |
warn( | |
`"css.modules" option in ${chalk.bold('vue.config.js')} ` + | |
`is deprecated now, please use "css.requireModuleExtension" instead.` | |
) | |
resolved.css.requireModuleExtension = !resolved.css.modules | |
} | |
} | |
// 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) | |
} | |
}) | |
} | |