const path = require('path') const hash = require('hash-sum') const semver = require('semver') const { matchesPluginId } = require('@vue/cli-shared-utils') // Note: if a plugin-registered command needs to run in a specific default mode, // the plugin needs to expose it via `module.exports.defaultModes` in the form // of { [commandName]: mode }. This is because the command mode needs to be // known and applied before loading user options / applying plugins. class PluginAPI { /** * @param {string} id - Id of the plugin. * @param {Service} service - A vue-cli-service instance. */ constructor (id, service) { this.id = id this.service = service } get version () { return require('../package.json').version } assertVersion (range) { if (typeof range === 'number') { if (!Number.isInteger(range)) { throw new Error('Expected string or integer value.') } range = `^${range}.0.0-0` } if (typeof range !== 'string') { throw new Error('Expected string or integer value.') } if (semver.satisfies(this.version, range)) return throw new Error( `Require @vue/cli-service "${range}", but was loaded with "${this.version}".` ) } /** * Current working directory. */ getCwd () { return this.service.context } /** * Resolve path for a project. * * @param {string} _path - Relative path from project root * @return {string} The resolved absolute path. */ resolve (_path) { return path.resolve(this.service.context, _path) } /** * Check if the project has a given plugin. * * @param {string} id - Plugin id, can omit the (@vue/|vue-|@scope/vue)-cli-plugin- prefix * @return {boolean} */ hasPlugin (id) { if (id === 'router') id = 'vue-router' if (['vue-router', 'vuex'].includes(id)) { const pkg = this.service.pkg return ((pkg.dependencies && pkg.dependencies[id]) || (pkg.devDependencies && pkg.devDependencies[id])) } return this.service.plugins.some(p => matchesPluginId(id, p.id)) } /** * Register a command that will become available as `vue-cli-service [name]`. * * @param {string} name * @param {object} [opts] * { * description: string, * usage: string, * options: { [string]: string } * } * @param {function} fn * (args: { [string]: string }, rawArgs: string[]) => ?Promise */ registerCommand (name, opts, fn) { if (typeof opts === 'function') { fn = opts opts = null } this.service.commands[name] = { fn, opts: opts || {}} } /** * Register a function that will receive a chainable webpack config * the function is lazy and won't be called until `resolveWebpackConfig` is * called * * @param {function} fn */ chainWebpack (fn) { this.service.webpackChainFns.push(fn) } /** * Register * - a webpack configuration object that will be merged into the config * OR * - a function that will receive the raw webpack config. * the function can either mutate the config directly or return an object * that will be merged into the config. * * @param {object | function} fn */ configureWebpack (fn) { this.service.webpackRawConfigFns.push(fn) } /** * Register a dev serve config function. It will receive the express `app` * instance of the dev server. * * @param {function} fn */ configureDevServer (fn) { this.service.devServerConfigFns.push(fn) } /** * Resolve the final raw webpack config, that will be passed to webpack. * * @param {ChainableWebpackConfig} [chainableConfig] * @return {object} Raw webpack config. */ resolveWebpackConfig (chainableConfig) { return this.service.resolveWebpackConfig(chainableConfig) } /** * Resolve an intermediate chainable webpack config instance, which can be * further tweaked before generating the final raw webpack config. * You can call this multiple times to generate different branches of the * base webpack config. * See https://github.com/mozilla-neutrino/webpack-chain * * @return {ChainableWebpackConfig} */ resolveChainableWebpackConfig () { return this.service.resolveChainableWebpackConfig() } /** * Generate a cache identifier from a number of variables */ genCacheConfig (id, partialIdentifier, configFiles = []) { const fs = require('fs') const cacheDirectory = this.resolve(`node_modules/.cache/${id}`) // replace \r\n to \n generate consistent hash const fmtFunc = conf => { if (typeof conf === 'function') { return conf.toString().replace(/\r\n?/g, '\n') } return conf } const variables = { partialIdentifier, 'cli-service': require('../package.json').version, 'cache-loader': require('cache-loader/package.json').version, env: process.env.NODE_ENV, test: !!process.env.VUE_CLI_TEST, config: [ fmtFunc(this.service.projectOptions.chainWebpack), fmtFunc(this.service.projectOptions.configureWebpack) ] } if (!Array.isArray(configFiles)) { configFiles = [configFiles] } configFiles = configFiles.concat([ 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml' ]) const readConfig = file => { const absolutePath = this.resolve(file) if (!fs.existsSync(absolutePath)) { return } if (absolutePath.endsWith('.js')) { // should evaluate config scripts to reflect environment variable changes try { return JSON.stringify(require(absolutePath)) } catch (e) { return fs.readFileSync(absolutePath, 'utf-8') } } else { return fs.readFileSync(absolutePath, 'utf-8') } } for (const file of configFiles) { const content = readConfig(file) if (content) { variables.configFiles = content.replace(/\r\n?/g, '\n') break } } const cacheIdentifier = hash(variables) return { cacheDirectory, cacheIdentifier } } } module.exports = PluginAPI