sidebarDepth |
---|
3 |
There are two major parts of the system:
@vue/cli
: globally installed, exposes thevue create <app>
command;@vue/cli-service
: locally installed, exposes thevue-cli-service
commands.
Both utilize a plugin-based architecture.
Creator is the class created when invoking vue create <app>
. Responsible for prompting for preferences, invoking generators and installing dependencies.
Service is the class created when invoking vue-cli-service <command> [...args]
. Responsible for managing the internal webpack configuration, and exposes commands for serving and building the project.
A CLI plugin is an npm package that can add additional features to a @vue/cli
project. It should always contain a Service Plugin as its main export, and can optionally contain a Generator and a Prompt File.
A typical CLI plugin's folder structure looks like the following:
.
βββ README.md
βββ generator.js # generator (optional)
βββ prompts.js # prompts file (optional)
βββ index.js # service plugin
βββ package.json
Service plugins are loaded automatically when a Service instance is created - i.e. every time the vue-cli-service
command is invoked inside a project.
Note the concept of a "service plugin" we are discussing here is narrower than that of a "CLI plugin", which is published as an npm package. The former only refers to a module that will be loaded by @vue/cli-service
when it's initialized, and is usually a part of the latter.
In addition, @vue/cli-service
's built-in commands and config modules are also all implemented as service plugins.
A service plugin should export a function which receives two arguments:
-
A PluginAPI instance
-
An object containing project local options specified in
vue.config.js
, or in the"vue"
field inpackage.json
.
The API allows service plugins to extend/modify the internal webpack config for different environments and inject additional commands to vue-cli-service
. Example:
module.exports = (api, projectOptions) => {
api.chainWebpack(webpackConfig => {
// modify webpack config with webpack-chain
})
api.configureWebpack(webpackConfig => {
// modify webpack config
// or return object to be merged with webpack-merge
})
api.registerCommand('test', args => {
// register `vue-cli-service test`
})
}
Note: the way plugins set modes has been changed in beta.10.
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 }
:
module.exports = api => {
api.registerCommand('build', () => {
// ...
})
}
module.exports.defaultModes = {
build: 'production'
}
This is because the command's expected mode needs to be known before loading environment variables, which in turn needs to happen before loading user options / applying the plugins.
A plugin can retrieve the resolved webpack config by calling api.resolveWebpackConfig()
. Every call generates a fresh webpack config which can be further mutated as needed:
module.exports = api => {
api.registerCommand('my-build', args => {
const configA = api.resolveWebpackConfig()
const configB = api.resolveWebpackConfig()
// mutate configA and configB for different purposes...
})
}
// make sure to specify the default mode for correct env variables
module.exports.defaultModes = {
'my-build': 'production'
}
Alternatively, a plugin can also obtain a fresh chainable config by calling api.resolveChainableWebpackConfig()
:
api.registerCommand('my-build', args => {
const configA = api.resolveChainableWebpackConfig()
const configB = api.resolveChainableWebpackConfig()
// chain-modify configA and configB for different purposes...
const finalConfigA = configA.toConfig()
const finalConfigB = configB.toConfig()
})
The exports from vue.config.js
will be validated against a schema to avoid typos and wrong config values. However, a 3rd party plugin can still allow the user to configure its behavior via the pluginOptions
field. For example, with the following vue.config.js
:
module.exports = {
pluginOptions: {
foo: { /* ... */ }
}
}
The 3rd party plugin can read projectOptions.pluginOptions.foo
to determine conditional configurations.
A CLI plugin published as a package can contain a generator.js
or generator/index.js
file. The generator inside a plugin will be invoked in two possible scenarios:
-
During a project's initial creation, if the CLI plugin is installed as part of the project creation preset.
-
When the plugin is installed after project's creation and invoked individually via
vue invoke
.
The GeneratorAPI allows a generator to inject additional dependencies or fields into package.json
and add files to the project.
A generator should export a function which receives three arguments:
-
A
GeneratorAPI
instance; -
The generator options for this plugin. These options are resolved during the prompt phase of project creation, or loaded from a saved preset in
~/.vuerc
. For example, if the saved~/.vuerc
looks like this:{ "presets" : { "foo": { "plugins": { "@vue/cli-plugin-foo": { "option": "bar" } } } } }
And if the user creates a project using the
foo
preset, then the generator of@vue/cli-plugin-foo
will receive{ option: 'bar' }
as its second argument.For a 3rd party plugin, the options will be resolved from the prompts or command line arguments when the user executes
vue invoke
(see Prompts for 3rd Party Plugins). -
The entire preset (
presets.foo
) will be passed as the third argument.
Example:
module.exports = (api, options, rootOptions) => {
// modify package.json fields
api.extendPackage({
scripts: {
test: 'vue-cli-service test'
}
})
// copy and render all files in ./template with ejs
api.render('./template')
if (options.foo) {
// conditionally generate files
}
}
When you call api.render('./template')
, the generator will render files in ./template
(resolved relative to the generator file) with EJS.
In addition, you can inherit and replace parts of an existing template file (even from another package) using YAML front-matter:
---
extend: '@vue/cli-service/generator/template/src/App.vue'
replace: !!js/regexp /<script>[^]*?<\/script>/
---
<script>
export default {
// Replace default script
}
</script>
It's also possible to do multiple replaces, although you will need to wrap your replace strings within <%# REPLACE %>
and <%# END_REPLACE %>
blocks:
---
extend: '@vue/cli-service/generator/template/src/App.vue'
replace:
- !!js/regexp /Welcome to Your Vue\.js App/
- !!js/regexp /<script>[^]*?<\/script>/
---
<%# REPLACE %>
Replace Welcome Message
<%# END_REPLACE %>
<%# REPLACE %>
<script>
export default {
// Replace default script
}
</script>
<%# END_REPLACE %>
Only built-in plugins have the ability to customize the initial prompts when creating a new project, and the prompt modules are located inside the @vue/cli
package.
A prompt module should export a function that receives a PromptModuleAPI instance. The prompts are presented using inquirer under the hood:
module.exports = api => {
// a feature object should be a valid inquirer choice object
api.injectFeature({
name: 'Some great feature',
value: 'my-feature'
})
// injectPrompt expects a valid inquirer prompt object
api.injectPrompt({
name: 'someFlag',
// make sure your prompt only shows up if user has picked your feature
when: answers => answers.features.include('my-feature'),
message: 'Do you want to turn on flag foo?',
type: 'confirm'
})
// when all prompts are done, inject your plugin into the options that
// will be passed on to Generators
api.onPromptComplete((answers, options) => {
if (answers.features.includes('my-feature')) {
options.plugins['vue-cli-plugin-my-feature'] = {
someFlag: answers.someFlag
}
}
})
}
3rd party plugins are typically installed manually after a project is already created, and the user will initialize the plugin by calling vue invoke
. If the plugin contains a prompts.js
in its root directory, it will be used during invocation. The file should export an array of Questions that will be handled by Inquirer.js. The resolved answers object will be passed to the plugin's generator as options.
Alternatively, the user can skip the prompts and directly initialize the plugin by passing options via the command line, e.g.:
vue invoke my-plugin --mode awesome
For a CLI plugin to be usable by other developers, it must be published on npm following the name convention vue-cli-plugin-<name>
. Following the name convention allows your plugin to be:
- Discoverable by
@vue/cli-service
; - Discoverable by other developers via searching;
- Installable via
vue add <name>
orvue invoke <name>
.
::: tip Note
This section only applies if you are working on a built-in plugin inside the vuejs/vue-cli
repository itself.
:::
A plugin with a generator that injects additional dependencies other than packages in this repo (e.g. chai
is injected by @vue/cli-plugin-unit-mocha/generator/index.js
) should have those dependencies listed in its own devDependencies
field. This ensures that:
-
the package always exist in this repo's root
node_modules
so that we don't have to reinstall them on every test. -
yarn.lock
stays consistent so that CI can better use it for inferring caching behavior.