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) | |
| } | |
| }) | |
| } | |